khinsider mass downloader

mass downloader for downloads.khinsider.com

// ==UserScript==
// @name        khinsider mass downloader
// @description mass downloader for downloads.khinsider.com
// @version     1.1.1
// @namespace   https://venipa.net/
// @license     GPL-3.0
// @author      Venipa <[email protected]>
// @icon        https://www.google.com/s2/favicons?sz=64&domain=downloads.khinsider.com
// @match       https://*.khinsider.com/game-soundtracks/*
// @grant       GM_xmlhttpRequest
// @grant       unsafeWindow
// @run-at      document-end
// @require     https://cdn.jsdelivr.net/npm/[email protected]/dist/jszip.min.js
// @require     https://cdn.jsdelivr.net/npm/[email protected]/dist/FileSaver.min.js
// @connect     vgmsite.com
// @connect     vgmtreasurechest.com
// ==/UserScript==

(function() {
  "use strict";
  ((e) => {
    if (["interactive", "complete"].indexOf(document.readyState) > -1)
      e();
    else {
      let t = !1;
      document.addEventListener("DOMContentLoaded", () => {
        t || (t = !0, setTimeout(e, 1));
      });
    }
  })(function() {
    function sanitizeFilename(input, options) {
      var illegalRe = /[\/\?<>\\:\*\|":]/g, controlRe = /[\x00-\x1f\x80-\x9f]/g, reservedRe = /^\.+$/, 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 || "", output = sanitize(input2, replacement);
        return replacement === "" ? output : sanitize(output, "");
      }(input, options);
    }
    const downloadStatus = {
      running: !1,
      skip: !1
    }, 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 + ")";
      }
    }, dlButton = function(type) {
      var el = document.createElement("button");
      return "btn khinsider-massdl".split(" ").forEach((cl) => el.classList.add(cl)), el.innerText = TEXTS.DOWNLOAD(type || "default"), el.dataset.type = type, el;
    }, dlCheck = function() {
      var el = document.createElement("input"), elContainer = document.createElement("label");
      "khinsider-massdl khinsider-massdl-compress-check".split(" ").forEach((cl) => el.classList.add(cl)), el.id = "khmdl-compress-check", el.type = "checkbox", elContainer.setAttribute("for", `#${el.id}`);
      var label = document.createElement("span");
      return label.innerText = "Compress items to zip", elContainer.appendChild(el), elContainer.appendChild(label), { id: el.id, checkbox: el, container: elContainer };
    }, checkFlac = () => Array.from(document.querySelectorAll("#songlist_header th>b")).findIndex(
      (x) => x.innerText.trim() === "FLAC"
    ) !== -1, spacerEl = function(x, y) {
      var el = document.createElement("div");
      return el.style.width = (x || 0) + "px", el.style.height = (y || 0) + "px", el.style.display = "inline-block", el;
    }, mp3DL = dlButton("mp3"), flacDL = dlButton("flac"), compressCheck = dlCheck(), hasFlac = checkFlac(), setDisabledState = function(state) {
      mp3DL.disabled = state, hasFlac && (flacDL.disabled = state);
    }, get = (url, responseType = "json", retry = 3, ext = ".mp3") => new Promise((resolve, reject) => {
      try {
        GM_xmlhttpRequest({
          method: "GET",
          url,
          responseType,
          onerror: (e) => {
            retry === 0 ? reject(e) : (console.warn("Network error, retry."), e.status == 415 && (url = url.slice(0, url.lastIndexOf(".")) + ext), setTimeout(() => {
              resolve(get(url, responseType, retry - 1));
            }, 1e3));
          },
          onload: ({ status, response }) => {
            [200, 206].includes(status) ? resolve(response) : status === 415 ? setTimeout(() => {
              resolve(
                get(
                  url.slice(0, url.lastIndexOf(".")) + ext,
                  responseType,
                  retry - 1
                )
              );
            }, 500) : retry === 0 ? reject(`${status} ${url}`) : (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 }) => {
            status === 200 && resolve(response), reject(error);
          }
        });
      } catch (error) {
        reject(error);
      }
    }), startQueue = async (typeOfDL, compress) => {
      if (!downloadStatus.running && queue.length > 0) {
        const dl = typeOfDL === "flac" ? flacDL : mp3DL, zip = compress && new JSZip() || null;
        let i = 0, l = queue.length;
        downloadStatus.running = !0, dl.innerText = TEXTS.PREPARE(l, typeOfDL);
        do {
          const { url: meta, data } = await queue[0](), { 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";
          data.type === "audio/mpeg" && (fext = "mp3"), compress ? zip.file(
            sanitizeFilename(title).replace(/\.(mp3|flac)$/g, "") + "." + fext,
            data
          ) : saveAs(
            data,
            sanitizeFilename(title).replace(/\.(mp3|flac)$/g, "") + "." + fext
          ), queue.shift(), dl.innerText = TEXTS.PROGRESS_ITEM(++i, l, typeOfDL);
        } while (queue.length > 0);
        if (downloadStatus.running = !1, compress)
          return dl.innerText = TEXTS.ARCHIVE_START("0%", typeOfDL), 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), Promise.reject(err)));
      }
      return null;
    }, onClick = function(ev) {
      if (ev.preventDefault(), ev.target.disabled)
        return;
      ev.target.disabled = !0, setDisabledState(!0);
      const typeOfDL = ev.target.dataset.type, typeOfExt = typeOfDL === "flac" ? ".flac" : typeOfDL === "mp3" ? ".mp3" : "null", header = Array.from(
        document.querySelectorAll("#songlist #songlist_header > th")
      ), hasCD = !!header.find(
        (x) => x.innerText && x.innerText.trim() === "CD"
      ), hasNumber = !!header.find(
        (x) => x.innerText && x.innerText.trim() === "#"
      ), compressToZip = compressCheck.checkbox.checked, 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
        };
        return title = title.replace(/\.(mp3|flac)$/g, ""), {
          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 = !1, setDisabledState(!1);
        return;
      }
      const pageName = document.querySelector("#pageContent>h2").innerText;
      queue.push(
        ...urls.map((x) => 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, x.ext);
              }).catch((err) => (console.error(err), null))
            };
          } catch (ex) {
            return console.error(ex), { url: x, data: null };
          }
        })
      ), startQueue(typeOfDL, compressToZip).then((data) => {
        ev.target.disabled = !1, setDisabledState(!1), data && saveAs(data, pageName + ` (${String(typeOfDL).toUpperCase()}).zip`), ev.target.innerText = TEXTS.DOWNLOAD(typeOfDL);
      });
    };
    mp3DL.onclick = onClick, compressCheck.checkbox.checked = compressCheck.checkbox.value = !0, hasFlac && (flacDL.onclick = onClick), btns && (btns.appendChild(mp3DL), hasFlac && (btns.appendChild(spacerEl(8, 0)), btns.appendChild(flacDL)), btns.appendChild(spacerEl(8, 0)), btns.appendChild(compressCheck.container)), unsafeWindow.onbeforeunload = (ev) => downloadStatus.running ? "khinsider downloader is still running, do you still want to cancel?" : mp3DL.disabled ? "khinsider downloader is currently compressing, do you still want to cancel?" : null;
  });
})();