// ==UserScript== // @name khinsider mass downloader // @namespace https://venipa.net/ // @license GPL-3.0 // @version 0.1.2 // @description try to take over the world! // @author Venipa // @include /^https?://(\w+).khinsider\.com/game-soundtracks/album/(*.) // @match https://*.khinsider.com/game-soundtracks/* // @connect vgmsite.com // @require https://cdn.jsdelivr.net/npm/jszip@3.2.2/dist/jszip.min.js // @require https://cdn.jsdelivr.net/npm/file-saver@2.0.2/dist/FileSaver.min.js // @grant GM_xmlhttpRequest // @run-at document-end // @downloadURL none // ==/UserScript== (function () { "use strict"; function sanitizeFilename(input, options) { var illegalRe = /[\/\?<>\\:\*\|":]/g; var controlRe = /[\x00-\x1f\x80-\x9f]/g; var reservedRe = /^\.+$/; var windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i; function sanitize(input, replacement) { var sanitized = input .replace(illegalRe, replacement) .replace(controlRe, replacement) .replace(reservedRe, replacement) .replace(windowsReservedRe, replacement); return sanitized.split("").splice(0, 255).join(""); } return (function (input, options) { var replacement = (options && options.replacement) || ""; var output = sanitize(input, replacement); if (replacement === "") { return output; } return sanitize(output, ""); })(input, options); } const downloadStatus = { running: false, skip: false, }; const queue = []; console.log("loaded mass downloader"); var btns = document.querySelector('p[align="left"]'); const TEXTS = { DOWNLOAD: "Download Album", LOADING: "LOADING...", /** * * @param {number} value * @param {number} max */ PREPARE(max) { return "Preparing audio downloads... (Audio Files: " + max + ")"; }, /** * * @param {number} value * @param {number} max */ PROGRESS_ITEM(value, max) { const maxLength = max.toString().length; return ( "Fetching... (" + value.toString().padStart(maxLength) + " / " + max + ")" ); }, }; var dlButton = function () { var el = document.createElement("button"); "btn".split(" ").forEach((cl) => el.classList.add(cl)); el.innerText = TEXTS.DOWNLOAD; return el; }; var dl = dlButton(); const getFetch = (url, responseType = "json") => new Promise((resolve, reject) => { try { return fetch({ url: url, method: "GET", responseType: responseType }) .then((response) => response.blob()) .then(resolve); } catch (err) { reject(err); } }), get = (url, responseType = "json", retry = 3) => new Promise((resolve, reject) => { try { GM_xmlhttpRequest({ method: "GET", url, responseType, onerror: (e) => { if (retry === 0) reject(e); else { console.warn("Network error, retry."); if (e.status == 415) { url = url.slice(0, url.lastIndexOf(".")) + ".mp3"; } setTimeout(() => { resolve(get(url, responseType, retry - 1)); }, 1000); } }, onload: ({ status, response }) => { if ([200, 206].includes(status)) resolve(response); else if (status === 415) setTimeout(() => { resolve( get( url.slice(0, url.lastIndexOf(".")) + ".mp3", responseType, retry - 1 ) ); }, 500); else if (retry === 0) reject(`${status} ${url}`); else { console.warn(status, url); setTimeout(() => { resolve(get(url, responseType, retry - 1)); }, 500); } }, }); } catch (error) { reject(error); } }), requestPage = (url) => new Promise((resolve, reject) => { try { GM_xmlhttpRequest({ method: "GET", url, responseType: "text", onerror: reject, onload: ({ status, response, error }) => { if (status === 200) resolve(response); reject(error); }, }); } catch (error) { reject(error); } }); const startQueue = async () => { if (!downloadStatus.running && queue.length > 0) { const zip = new JSZip(); let i = 0; let l = queue.length; downloadStatus.running = true; dl.innerText = TEXTS.PREPARE(l); do { const { url: meta, data } = await queue[0](); const { url, title } = meta; if (!data || data.size <= 0) { queue.shift(); dl.innerText = TEXTS.PROGRESS_ITEM(i++, l); continue; } let fname = url.split("/").reverse()[0]; fname = fname.slice(0, fname.lastIndexOf(".")); let fext = "mp3"; if (data.type === "audio/mpeg") fext = "mp3"; zip.file(sanitizeFilename(title) + "." + fext, data); queue.shift(); dl.innerText = TEXTS.PROGRESS_ITEM(i++, l); } while (queue.length > 0); downloadStatus.running = false; return await zip.generateAsync({ type: "blob" }); } return null; }; /** * * @param ev {{target: HTMLButtonElement}} */ dl.onclick = function (ev) { ev.preventDefault(); if (ev.target.disabled) return; ev.target.disabled = true; const header = Array.from( document.querySelectorAll("#songlist #songlist_header > th") ); const hasCD = !!header.find( (x) => x.innerText && x.innerText.trim() === "CD" ), hasNumber = !!header.find( (x) => x.innerText && x.innerText.trim() === "#" ); const urls = Array.from( document.querySelectorAll("#songlist #songlist_header ~ tr") ) .filter((x) => x.querySelectorAll("td.clickable-row a").length > 0) .map((x) => { const fields = x.querySelectorAll("td"); const title = x.querySelectorAll("td.clickable-row a")[0].innerText, url = x.querySelector(".playlistDownloadSong a").href, meta = { CD: hasCD ? fields[1].innerText : null, PIECE: hasCD ? fields[2].innerText : hasNumber ? fields[1].innerText : null, }; return { title: (meta.CD ? meta.CD + "-" : "") + (meta.PIECE ? meta.PIECE.trim().match(/(\d+)/i)[0] + " " : "") + title + ".mp3", url: url, }; }); if (urls.length === 0) { ev.target.disabled = false; return; } const pageName = document.querySelector("#EchoTopic h2").innerText; queue.push( ...urls.map((x) => { return async () => { try { return { url: x, data: await requestPage(x.url) .then((page) => { const container = document.implementation.createHTMLDocument() .documentElement; container.style.display = "none"; container.innerHTML = page; const fileUrl = container.querySelector(".songDownloadLink") .parentElement.href; return get(fileUrl, "blob", 2); }) .catch((err) => { console.error(err); return null; }), }; } catch (ex) { console.error(ex); return { url: x, data: null }; } }; }) ); startQueue().then((data) => { ev.target.disabled = false; if (data) { saveAs(data, pageName + ".zip"); ev.target.innerText = TEXTS.DOWNLOAD; } }); }; if (btns) btns.appendChild(dl); })();