// ==UserScript==
// @name khinsider mass downloader
// @namespace https://venipa.net/
// @license GPL-3.0
// @version 0.2.1
// @description mass downloader for downloads.khinsider.com
// @author Venipa <admin@venipa.net>
// @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
// ==/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 = {
/**
*
* @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 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 (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;
return await zip.generateAsync(
{ type: "blob" },
function onUpdate(progress) {
dl.innerText = TEXTS.ARCHIVE_START(
progress.percent.toFixed(2) + "%",
typeOfDL
);
}
);
}
return null;
};
/**
*
* @param ev {{target: HTMLButtonElement}}
*/
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,
url: 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 =
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(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);
}
}
})();