// ==UserScript== // @name khinsider mass downloader // @description mass downloader for downloads.khinsider.com // @version 1.0.1 // @namespace https://venipa.net/ // @license GPL-3.0 // @author Venipa // @icon https://www.google.com/s2/favicons?sz=64&domain=downloads.khinsider.com // @match https://*.khinsider.com/game-soundtracks/* // @grant GM_xmlhttpRequest // @run-at document-end // @require https://cdn.jsdelivr.net/npm/jszip@3.9.1/dist/jszip.min.js // @require https://cdn.jsdelivr.net/npm/file-saver@2.0.4/dist/FileSaver.min.js // @connect vgmsite.com // @connect vgmtreasurechest.com // @downloadURL none // ==/UserScript== (function() { "use strict"; const a = (e) => { if (["interactive", "complete"].indexOf(document.readyState) > -1) e(); else { let t = false; document.addEventListener("DOMContentLoaded", () => { t || (t = true, setTimeout(e, 1)); }); } }; const appInit = function() { 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(input2, replacement) { var sanitized = input2.replace(illegalRe, replacement).replace(controlRe, replacement).replace(reservedRe, replacement).replace(windowsReservedRe, replacement); return sanitized.split("").splice(0, 255).join(""); } return function(input2, options2) { var replacement = options2 && options2.replacement || ""; var output = sanitize(input2, 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 = { /** * * @param {string} type */ DOWNLOAD(type) { return "Download Album (" + type + ")"; }, LOADING: "LOADING...", /** * * @param {number} value * @param {number} max * @param {string} type */ PREPARE(max, type) { return "Preparing audio downloads... (Audio Files: " + max + ") (" + type + ")"; }, /** * * @param {number} value * @param {number} max * @param {string} type */ PROGRESS_ITEM(value, max, type) { const maxLength = max.toString().length; return "Fetching... (" + value.toString().padStart(maxLength) + " / " + max + ") (" + type + ")"; }, ARCHIVE_START(value, type) { return "Compressing... " + value + " (" + type + ")"; } }; var dlButton = function(type) { var el = document.createElement("button"); "btn khinsider-massdl".split(" ").forEach((cl) => el.classList.add(cl)); el.innerText = TEXTS.DOWNLOAD(type || "default"); el.dataset.type = type; return el; }; var checkFlac = () => Array.from(document.querySelectorAll("#songlist_header th>b")).findIndex( (x) => x.innerText.trim() === "FLAC" ) !== -1; var spacerEl = function(x, y) { var el = document.createElement("div"); el.style.width = (x || 0) + "px"; el.style.height = (y || 0) + "px"; el.style.display = "inline-block"; return el; }; var mp3DL = dlButton("mp3"); var flacDL = dlButton("flac"); var hasFlac = checkFlac(); const setDisabledState = function(state) { mp3DL.disabled = state; if (hasFlac) flacDL.disabled = state; }; const 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)); }, 1e3); } }, 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 (typeOfDL) => { if (!downloadStatus.running && queue.length > 0) { const dl = typeOfDL === "flac" ? flacDL : mp3DL; const zip = new JSZip(); let i = 0; let l = queue.length; downloadStatus.running = true; dl.innerText = TEXTS.PREPARE(l, typeOfDL); 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, typeOfDL); continue; } let fname = url.split("/").reverse()[0]; fname = fname.slice(0, fname.lastIndexOf(".")); let fext = typeOfDL === "flac" ? "flac" : "mp3"; if (data.type === "audio/mpeg") fext = "mp3"; zip.file( sanitizeFilename(title).replace(/\.(mp3|flac)$/g, "") + "." + fext, data ); queue.shift(); dl.innerText = TEXTS.PROGRESS_ITEM(++i, l, typeOfDL); } while (queue.length > 0); downloadStatus.running = false; dl.innerText = TEXTS.ARCHIVE_START("0%", typeOfDL); return await zip.generateAsync({ type: "blob" }, (progress) => { dl.innerText = TEXTS.ARCHIVE_START( progress.percent.toFixed(2) + "%", typeOfDL ); }).catch((err) => { console.error("failed to generate zip", err); return Promise.reject(err); }); } return null; }; const onClick = function(ev) { ev.preventDefault(); if (ev.target.disabled) return; ev.target.disabled = true; setDisabledState(true); const typeOfDL = ev.target.dataset.type; const typeOfExt = typeOfDL === "flac" ? ".flac" : typeOfDL === "mp3" ? ".mp3" : "null"; 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"); let 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 }; title = title.replace(/\.(mp3|flac)$/g, ""); return { title: (meta.CD ? meta.CD + "-" : "") + (meta.PIECE ? meta.PIECE.trim().match(/(\d+)/i)[0] + " " : "") + title + typeOfExt, ext: typeOfExt, url }; }); if (urls.length === 0) { ev.target.disabled = false; setDisabledState(false); return; } const pageName = document.querySelector("#pageContent>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 = Array.from( container.querySelectorAll(".songDownloadLink") ).map( (s) => s.parentElement ).find((d) => d.href.endsWith(x.ext)).href; return get(fileUrl, "blob", 2); }).catch((err) => { console.error(err); return null; }) }; } catch (ex) { console.error(ex); return { url: x, data: null }; } }; }) ); startQueue(typeOfDL).then((data) => { ev.target.disabled = false; setDisabledState(false); if (data) { saveAs(data, pageName + ".zip"); ev.target.innerText = TEXTS.DOWNLOAD(typeOfDL); } }); }; mp3DL.onclick = onClick; if (hasFlac) flacDL.onclick = onClick; if (btns) { btns.appendChild(mp3DL); if (hasFlac) { btns.appendChild(spacerEl(8, 0)); btns.appendChild(flacDL); } } }; a(appInit); })();