VGMLoaderX

Automatically downloads albums from KHInsider without an account.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name               VGMLoaderX
// @name:de            VGMLoaderX
// @name:en            VGMLoaderX
// @namespace          sun/userscripts
// @version            1.1.0
// @description        Automatically downloads albums from KHInsider without an account.
// @description:de     Lädt Alben von KHInsider automatisch und ohne Account herunter.
// @description:en     Automatically downloads albums from KHInsider without an account.
// @compatible         chrome
// @compatible         edge
// @compatible         firefox
// @compatible         opera
// @compatible         safari
// @homepageURL        https://forgejo.sny.sh/sun/userscripts
// @supportURL         https://forgejo.sny.sh/sun/userscripts/issues
// @contributionURL    https://liberapay.com/sun
// @contributionAmount €1.00
// @author             Sunny <[email protected]>
// @include            https://downloads.khinsider.com/game-soundtracks/album/*
// @match              https://downloads.khinsider.com/game-soundtracks/album/*
// @connect            vgmdownloads.com
// @connect            vgmsite.com
// @connect            vgmtreasurechest.com
// @run-at             document-end
// @inject-into        page
// @grant              GM.getValue
// @grant              GM_getValue
// @grant              GM.info
// @grant              GM_info
// @grant              GM.setValue
// @grant              GM_setValue
// @grant              GM.xmlHttpRequest
// @grant              GM_xmlhttpRequest
// @noframes
// @require            https://unpkg.com/@zip.js/zip.js/dist/zip.js
// @require            https://unpkg.com/file-saver
// @require            https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
// @icon               https://forgejo.sny.sh/sun/userscripts/raw/branch/main/icons/VGMLoaderX.ico
// @copyright          2021-present, Sunny (https://sny.sh/)
// @license            Hippocratic License; https://forgejo.sny.sh/sun/userscripts/src/branch/main/LICENSE.md
// ==/UserScript==

(() => {
  for (const x of document.querySelectorAll('a[href^="/cp/add_album/"]')) {
    x.addEventListener("click", async (e) => {
      e.preventDefault();

      const format = [
        ...document.querySelectorAll("#songlist_header th[align=right]"),
      ].map((x) => x.textContent);
      if (format.length === 1) {
        download(format[0]);
      } else {
        document.body.insertAdjacentHTML(
          "beforeend",
          `
          <dialog id="vgmloaderx-dialog">
            <p style="margin: auto">
              <b>${GM.info.script.name}</b>
            </p>
            <p>Please select your desired format:</p>
            <form>
              <p>
                <select id="vgmloaderx-select" style="width: 100%"></select>
              </p>
              <p style="display: flex; align-items: center; gap: 1ch">
                <input id="vgmloaderx-input" type="checkbox">
                <label for="vgmloaderx-input">Save as default format</label>
              </p>
              <p style="display: none">
                <i>
                  The selected format will be preselected in any future dialogs,<br>
                  unless the requested album is not available in said format,<br>
                  in which case the first available one will be chosen instead.
                </i>
              </p>
              <p style="float: right; margin: auto">
                <button id="vgmloaderx-confirm">OK</button>
                <button formmethod="dialog">Cancel</button>
              </p>
            </form>
          </dialog>
          <style>
            p:has(> #vgmloaderx-input:checked) + p {
              display: block !important;
            }
          </style>
        `,
        );

        const dialog = document.getElementById("vgmloaderx-dialog");
        const select = document.getElementById("vgmloaderx-select");
        const input = document.getElementById("vgmloaderx-input");
        const confirm = document.getElementById("vgmloaderx-confirm");

        for (const x of format) {
          const parent = document.createElement("option");
          const child = document.createTextNode(x);

          if (x === (await GM.getValue("format")))
            parent.setAttribute("selected", true);

          parent.appendChild(child);
          select.appendChild(parent);
        }

        dialog.addEventListener("close", async () => {
          dialog.remove();

          if (!dialog.returnValue) return;

          if (input.checked) await GM.setValue("format", dialog.returnValue);

          download(dialog.returnValue);
        });

        confirm.addEventListener("click", (event) => {
          event.preventDefault();
          dialog.close(select.value);
        });

        dialog.showModal();
      }

      function download(format) {
        const element = document.getElementsByClassName("albumMassDownload")[0];
        element.style.height = "auto";
        element.style.marginBottom = "2em";

        const input = eval(
          document
            .querySelector("#pageContent script")
            .textContent.slice(5, -3)
            .replace("function", "function x")
            .replace("return p}", "return p}x"),
        );

        const mediaPath = input.match(/mediaPath='(.+?)'/)[1];
        const tracks = JSON.parse(
          input.match(/tracks=(\[.+?,\])/)[1].replace(",]", "]"),
        );
        const output = tracks.map(
          (x) =>
            `${
              mediaPath + x.file.split(".").slice(0, -1).join(".")
            }.${format.toLowerCase()}`,
        );
        const names = tracks.map((x) => x.name);

        const blobWriter = new zip.BlobWriter("application/zip");
        const writer = new zip.ZipWriter(blobWriter);

        function forSync(i) {
          element.innerHTML = `Downloading track ${i + 1} of ${output.length} (${names[i] || `Track ${i + 1}`})...`;
          GM.xmlHttpRequest({
            method: "GET",
            url: output[i],
            responseType: "blob",
            onload: async (response) => {
              await writer.add(
                decodeURIComponent(output[i].split("/").pop()),
                new zip.BlobReader(response.response),
              );

              if (output[i + 1]) {
                forSync(i + 1);
              } else {
                await writer.close();
                const blob = await blobWriter.getData();
                saveAs(
                  blob,
                  `${document.getElementsByTagName("h2")[0].textContent}.zip`,
                );
                element.innerHTML =
                  "Album successfully downloaded. ZIP file has been passed to the browser.";
              }
            },
          });
        }
        forSync(0);
      }
    });
  }
})();