Greasy Fork

Greasy Fork is available in English.

TracklistToRYM

Imports an album's tracklist from various sources into Rate Your Music.

当前为 2022-08-18 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt MIT
/* eslint-env browser, greasemonkey */
/* jshint asi: true, esversion: 11 */
/* globals deleteFiledUnder, goAdvanced, goSimple */

// ==UserScript==
// @name               TracklistToRYM
// @name:de            TracklistToRYM
// @name:en            TracklistToRYM
// @namespace          TheLastZombie/userscripts
// @version            1.30.1
// @description        Imports an album's tracklist from various sources into Rate Your Music.
// @description:de     Importiert die Tracklist eines Albums von verschiedenen Quellen in Rate Your Music.
// @description:en     Imports an album's tracklist from various sources into Rate Your Music.
// @compatible         chrome
// @compatible         edge
// @compatible         firefox
// @compatible         opera
// @compatible         safari
// @homepageURL        https://codeberg.org/sun/userscripts
// @supportURL         https://codeberg.org/sun/userscripts/issues/new
// @contributionURL    https://ko-fi.com/rcrsch
// @contributionAmount €1.00
// @author             TheLastZombie <[email protected]>
// @include            https://rateyourmusic.com/releases/ac
// @include            https://rateyourmusic.com/releases/ac?*
// @match              https://rateyourmusic.com/releases/ac
// @match              https://rateyourmusic.com/releases/ac?*
// @connect            allmusic.com
// @connect            amazon.com
// @connect            apple.com
// @connect            archive.org
// @connect            bandcamp.com
// @connect            beatport.com
// @connect            deezer.com
// @connect            discogs.com
// @connect            freemusicarchive.org
// @connect            genius.com
// @connect            junodownload.com
// @connect            khinsider.com
// @connect            last.fm
// @connect            loot.co.za
// @connect            maniadb.com
// @connect            metal-archives.com
// @connect            musicbrainz.org
// @connect            musik-sammler.de
// @connect            napster.com
// @connect            naxos.com
// @connect            nts.live
// @connect            pulsewidth.org.uk
// @connect            qobuz.com
// @connect            rateyourmusic.com
// @connect            sonemic.com
// @connect            soundcloud.com
// @connect            streetvoice.com
// @connect            vgmdb.net
// @connect            vinyl-digital.com
// @connect            youtube.com
// @connect            *
// @run-at             document-end
// @inject-into        auto
// @grant              GM.deleteValue
// @grant              GM_deleteValue
// @grant              GM.getValue
// @grant              GM_getValue
// @grant              GM.info
// @grant              GM_info
// @grant              GM.listValues
// @grant              GM_listValues
// @grant              GM.setValue
// @grant              GM_setValue
// @grant              GM.xmlHttpRequest
// @grant              GM_xmlhttpRequest
// @noframes
// @require            https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
// @icon               https://codeberg.org/sun/userscripts/raw/branch/main/icons/TracklistToRYM.png
// @copyright          2020-2022, TheLastZombie (https://eric.jetzt/)
// @license            MIT; https://codeberg.org/sun/userscripts/src/branch/main/LICENSE
// ==/UserScript==

// ==OpenUserJS==
// @author             TheLastZombie
// ==/OpenUserJS==

(async function () {
  "use strict";

  const parent = document.querySelector(
    "input[value='Copy Tracks']"
  ).parentNode;
  let msgPosted = false;

  const sitestmp = [
    {
      name: "AllMusic",
      extractor: "node",
      placeholder: "https://www.allmusic.com/album/*",
      artist: ".album-artist a",
      album: ".album-title",
      parent: ".track",
      index: ".tracknum",
      title: ".title a",
      length: ".time",
      default: false,
    },
    {
      name: "Amazon",
      extractor: "node",
      placeholder: "https://www.amazon.com/dp/*",
      artist: "#ProductInfoArtistLink",
      album: "h1",
      parent: "#dmusic_tracklist_content tbody > .a-text-left",
      index: "div.TrackNumber-Default-Color",
      title: ".TitleLink",
      length: ".a-size-base-plus.a-color-secondary",
      default: false,
    },
    {
      name: "Apple Music",
      extractor: "node",
      placeholder: "https://music.apple.com/*/album/*",
      artist: ".product-creator a",
      album: "h1",
      parent: ".songs-list-row--song",
      index: ".songs-list-row__column-data",
      title: ".songs-list-row__song-name",
      length: ".songs-list-row__length",
      default: false,
    },
    {
      name: "a-tisket",
      extractor: "node",
      placeholder: "https://atisket.pulsewidth.org.uk/*",
      artist: ".artist",
      album: ".album-title cite",
      parent: ".track",
      index: ".track-num",
      title: ".track-name",
      length: ".duration",
      default: false,
    },
    {
      name: "Bandcamp",
      extractor: "node",
      placeholder: "https://*.bandcamp.com/album/*",
      artist: "#name-section h3 a",
      album: "h2",
      parent: ".title-col",
      index: false,
      title: ".track-title",
      length: ".time",
      default: false,
    },
    {
      name: "Bandcamp (track)",
      extractor: "node",
      placeholder: "https://*.bandcamp.com/track/*",
      artist: "#name-section h3 a",
      album: "h2",
      parent: ".trackView",
      index: false,
      title: ".trackTitle",
      length: false,
      default: false,
    },
    {
      name: "Beatport",
      extractor: "node",
      placeholder: "https://www.beatport.com/release/*/*",
      artist: ".interior-release-chart-content-item--desktop [data-label]",
      album: "h1",
      parent: ".track",
      index: ".buk-track-num",
      title: ".buk-track-primary-title",
      length: ".buk-track-length",
      default: false,
    },
    {
      name: "Deezer",
      extractor: "node",
      placeholder: "https://deezer.com/album/*",
      artist: "#naboo_album_artist a span",
      album: "#naboo_album_title",
      parent: ".song",
      index: ".number",
      title: "[itemprop='name']",
      length: ".timestamp",
      default: false,
    },
    {
      name: "Discogs",
      extractor: "node",
      placeholder: "https://discogs.com/release/*",
      artist: "h1 a",
      album: "h1",
      parent: "tr[data-track-position]",
      index: "td[class^=trackPos]",
      title: "td[class^=trackTitle] span",
      length: "td[class^=duration] span",
      default: false,
    },
    {
      name: "Free Music Archive",
      extractor: "node",
      placeholder: "https://freemusicarchive.org/music/*/*",
      artist: ".bcrumb > a:last-of-type",
      album: "h1",
      parent: ".play-item",
      index: ".playtxt > b",
      title: ".playtxt > a > b",
      length: ".playtxt",
      default: false,
    },
    {
      name: "Genius",
      extractor: "node",
      placeholder: "https://genius.com/albums/*/*",
      artist: "h2 a",
      album: "h1",
      parent: ".chart_row",
      index: "chart_row-number_container-number",
      title: ".chart_row-content-title",
      length: false,
      default: false,
    },
    {
      name: "Internet Archive",
      extractor: "node",
      placeholder: "https://archive.org/details/*",
      artist: ".metadata-definition span a",
      album: ".item-title .breaker-breaker",
      parent: ".related-track-row",
      index: false,
      title: ".track-title",
      length: false,
      default: false,
    },
    {
      name: "Juno Download",
      extractor: "node",
      placeholder: "https://www.junodownload.com/products/*",
      artist: ".product-artist a",
      album: "h1",
      parent: ".product-tracklist-track",
      index: ".track-title",
      title: "[itemprop='name']",
      length: ".col-1",
      default: false,
    },
    {
      name: "Kingdom Hearts Insider",
      extractor: "node",
      placeholder: "https://downloads.khinsider.com/game-soundtracks/album/*",
      artist: false,
      album: "h2",
      parent: "#songlist tr:not(#songlist_header):not(#songlist_footer)",
      index: "td[style='padding-right: 8px;']",
      title: ".clickable-row:not([align='right']) a",
      length: ".clickable-row[align='right'] a",
      default: false,
    },
    {
      name: "Last.fm",
      extractor: "node",
      placeholder: "https://www.last.fm/music/*/*",
      artist: ".header-new-crumb span",
      album: "h1",
      parent: ".chartlist-row",
      index: ".chartlist-index",
      title: ".chartlist-name a",
      length: ".chartlist-duration",
      default: false,
    },
    {
      name: "Loot.co.za",
      extractor: "node",
      placeholder: "https://www.loot.co.za/product/*/*",
      artist: "h2 a",
      album: false,
      parent: "#tabs div:nth-last-child(2) .productDetails tr:not([style])",
      index: "td[width]",
      title: "td:not([width])",
      length: false,
      default: false,
    },
    {
      name: "maniadb.com",
      extractor: "node",
      placeholder: "http://www.maniadb.com/album/*",
      artist: ".album-artist",
      album: ".album-title",
      parent: ".album-tracks tr[onmouseover]",
      index: ".trackno",
      title: ".song a",
      length: ".runningtime",
      default: false,
    },
    {
      name: "Metal Archives",
      extractor: "node",
      placeholder: "https://www.metal-archives.com/albums/*/*/*",
      artist: "#album_sidebar > a",
      album: ".album_name > a",
      parent: ".table_lyrics .even, .table_lyrics .odd",
      index: "td",
      title: ".wrapWords",
      length: "td[align='right']",
      default: false,
    },
    {
      name: "MusicBrainz",
      extractor: "json",
      placeholder: "https://musicbrainz.org/release/*",
      artist: "artist-credit.0.name",
      album: "title",
      parent: "media",
      index: "number",
      title: "title",
      length: "length",
      transformer: async (link) => {
        return (
          "https://musicbrainz.org/ws/2/release/" +
          link.match(/[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/)[0] +
          "?inc=artists+recordings&fmt=json"
        );
      },
      modifier: async (data) => {
        data = JSON.parse(data);
        data.media = data.media.map((y) => y.tracks).flat();
        data = JSON.stringify(data);
        return data;
      },
      default: false,
    },
    {
      name: "Musik-Sammler",
      extractor: "node",
      placeholder: "https://www.musik-sammler.de/release/*",
      artist: ".header-span a",
      album: "h1 span[itemprop='name']",
      parent: "[itemprop='track'] tbody tr",
      index: ".track-position",
      title: ".track-title span",
      length: ".track-time",
      default: false,
    },
    {
      name: "Napster",
      extractor: "node",
      placeholder: "https://napster.com/artist/*/album/*",
      artist: ".show-for-medium .artist-link",
      album: "#page-name",
      parent: ".track-item",
      index: ".track-number div",
      title: ".track-title",
      length: false,
      default: false,
    },
    {
      name: "Naxos Records",
      extractor: "node",
      placeholder: "https://www.naxos.com/catalogue/item.asp?item_code=*",
      artist: ".composers a",
      album: false,
      parent: "table[valign='top']",
      index: "td:first-child",
      title: "td:nth-child(4) b",
      length: "td:nth-child(4)",
      default: false,
    },
    {
      name: "NTS Radio",
      extractor: "node",
      placeholder: "https://www.nts.live/shows/*/episodes/*",
      artist: ".bio__artist-link__a",
      album: "#H1-4",
      parent: ".track",
      index: false,
      title: ".track__title",
      length: false,
      default: false,
    },
    {
      name: "Qobuz",
      extractor: "node",
      placeholder: "https://www.qobuz.com/*/album/*/*",
      artist: ".album-meta__artist",
      album: ".album-meta__title",
      parent: ".track",
      index: ".track__item--number span",
      title: ".track__item--name span",
      length: ".track__item--duration",
      default: false,
    },
    {
      name: "Rate Your Music",
      extractor: "node",
      placeholder: "https://rateyourmusic.com/release/album/*/*",
      artist: ".album_artist_small a",
      album: ".album_title",
      parent: "#tracks .track:not([style='text-align:right;'])",
      index: ".tracklist_num",
      title: "[itemprop='name'] .rendered_text",
      length: ".tracklist_duration",
      modifier: async (data) => {
        data = new DOMParser().parseFromString(data, "text/html");
        Array.from(data.querySelectorAll(".tracklist_title .artist")).forEach(
          (artist) => {
            const title = document.createTextNode(
              artist.getAttribute("title") || artist.innerText
            );
            artist.parentNode.replaceChild(title, artist);
          }
        );
        data = new XMLSerializer().serializeToString(data);
        return data;
      },
      default: true,
    },
    {
      name: "Sonemic",
      extractor: "node",
      placeholder: "https://sonemic.com/release/album/*/*",
      artist: "#page_object_header .music_artist",
      album: ".page_object_header_title",
      parent: ".page_fragment_track_track",
      index: ".page_fragment_track_num",
      title: ".page_fragment_track_title .song",
      length: ".page_fragment_track_duration",
      default: false,
    },
    {
      name: "SoundCloud",
      extractor: "regex",
      placeholder: "https://soundcloud.com/*/sets/*",
      artist: /(?<=by <a href=".*?">).*?(?=<\/a>)/g,
      album: /(?<=<a itemprop="url" href=".*?">).*?(?=<\/a>)/g,
      parent: /<article itemprop="track".*?<\/article>/g,
      index: false,
      title: /(?<=<a itemprop="url" href=".*?">).*?(?=<\/a>)/,
      length: /(?<=<meta itemprop="duration" content=").*?(?=" \/>)/,
      default: false,
    },
    {
      name: "SoundCloud (track)",
      extractor: "regex",
      placeholder: "https://soundcloud.com/*/*",
      artist: /(?<=by <a href=".*?">).*?(?=<\/a>)/g,
      album: /(?<=<a itemprop="url" href=".*?">).*?(?=<\/a>)/g,
      parent: /<header>.*?<\/header>/g,
      index: false,
      title: /(?<=<a itemprop="url" href=".*?">).*?(?=<\/a>)/,
      length: /(?<=<meta itemprop="duration" content=").*?(?=" \/>)/,
      default: false,
    },
    {
      name: "StreetVoice",
      extractor: "node",
      placeholder: "https://streetvoice.com/*/songs/album/*",
      artist: ".user-info a",
      album: "h1",
      parent: "#item_box_list > li",
      index: ".work-item-number h4",
      title: ".work-item-info h4 a",
      length: false,
      default: false,
    },
    {
      name: "VGMdb",
      extractor: "node",
      placeholder: "https://vgmdb.net/album/*",
      artist: "td .artistname[style='display:inline']:not([title='Composer'])",
      album: "h1 .albumtitle[lang='en']",
      parent: ".tl:first-child .rolebit",
      index: ".label",
      title: "[colspan='2']",
      length: ".time",
      default: false,
    },
    {
      name: "VGMdb (secondary language)",
      extractor: "node",
      placeholder: "https://vgmdb.net/album/*",
      artist: "td .artistname[style='display:inline']:not([title='Composer'])",
      album: "h1 .albumtitle[lang='en']",
      parent: ".tl:nth-child(2) .rolebit",
      index: ".label",
      title: "[colspan='2']",
      length: ".time",
      default: false,
    },
    {
      name: "VGMdb (tertiary language)",
      extractor: "node",
      placeholder: "https://vgmdb.net/album/*",
      artist: "td .artistname[style='display:inline']:not([title='Composer'])",
      album: "h1 .albumtitle[lang='en']",
      parent: ".tl:nth-child(3) .rolebit",
      index: ".label",
      title: "[colspan='2']",
      length: ".time",
      default: false,
    },
    {
      name: "Vinyl Digital",
      extractor: "node",
      placeholder: "https://vinyl-digital.com/*/*",
      artist: "#test_othersartist",
      album: "#test_product_name",
      parent: "#playlist_table tr:not(:first-child):not([style])",
      index: ".track",
      title: ".tracktitle span",
      length: "td:not([class])",
      default: false,
    },
    {
      name: "YouTube Music",
      extractor: "regex",
      placeholder: "https://music.youtube.com/playlist?list=*",
      artist: /(?<=\\"musicArtist\\".*?\\"name\\":\\").*?(?=\\",)/g,
      album: /(?<=\\"musicAlbumRelease\\".*?\\"title\\":\\").*?(?=\\",)/g,
      parent: /{\\"musicTrack\\":.*?}}}},/g,
      index: /(?<=\\"albumTrackIndex\\":\\").*?(?=\\",)/,
      title: /(?<=\\"title\\":\\").*?(?=\\",)/,
      length: false,
      default: false,
    },
  ];

  if (localStorage.getItem("ttrym-sites")) {
    await GM.setValue("sites", localStorage.getItem("ttrym-sites").split(","));
    await GM.setValue(
      "default",
      localStorage.getItem("ttrym-sites").split(",").sort()[0]
    );
    localStorage.removeItem("ttrym-sites");
  }
  if (await GM.getValue("sites")) {
    await GM.setValue("sitesv2", sitesV1ToV2(await GM.getValue("sites")));
    await GM.deleteValue("sites");
  }

  if ((await GM.getValue("artist")) === undefined)
    await GM.setValue("artist", false);
  if ((await GM.getValue("release")) === undefined)
    await GM.setValue("release", false);
  if ((await GM.getValue("sitesv2")) === undefined)
    await GM.setValue("sitesv2", sitesV1ToV2(sitestmp.map((x) => x.name)));
  if (
    (await GM.getValue("default")) === undefined ||
    !(await GM.getValue("sitesv2"))[await GM.getValue("default")]
  ) {
    const sitesv2 = await GM.getValue("sitesv2");
    const name = sitestmp.filter((x) => x.default)[0].name;
    sitesv2[name] = true;
    await GM.setValue("sitesv2", sitesv2);
    await GM.setValue("default", name);
  }
  if ((await GM.getValue("guess")) === undefined)
    await GM.setValue("guess", true);
  if ((await GM.getValue("enbydef")) === undefined)
    await GM.setValue("enbydef", true);
  if ((await GM.getValue("append")) === undefined)
    await GM.setValue("append", false);
  if ((await GM.getValue("sources")) === undefined)
    await GM.setValue("sources", true);

  const sitesv2 = await GM.getValue("sitesv2");
  for (const x of sitestmp) {
    if (sitesv2[x.name] === undefined)
      sitesv2[x.name] = await GM.getValue("enbydef");
  }
  await GM.setValue("sitesv2", sitesv2);

  const asyncFilterHelper = await GM.getValue("sitesv2");
  const sites = sitestmp.filter((x) => asyncFilterHelper[x.name]);

  parent.style.width = "500px";
  parent.insertAdjacentHTML(
    "beforeend",
    "<br><hr style='margin-top:1em;margin-bottom:1em;border:none;height:1px;background:var(--mono-d);width:calc(100% + 20px);position:relative;left:-10px'>" +
      "<p style='display:flex;margin-bottom:0'><a href='https://codeberg.org/sun/userscripts' target='_blank' style='position:relative;top:3px;color:inherit'>TTRYM</a>" +
      "<select id='ttrym-site' style='max-width:0;margin-left:.5em;border-radius:3px 0 0 3px'>" +
      sites
        .map((x) => "<option value='" + x.name + "'>" + x.name + "</option>")
        .join("") +
      "</select>" +
      "<input id='ttrym-link' placeholder='Album URL' style='flex:1;border-left:none;border-radius:0 3px 3px 0;padding-left:5px'></input>" +
      "<button id='ttrym-submit' style='font-family:\"Font Awesome 5 Free\";border:none;background:none;color:inherit;font-size:1.5em;margin-left:.5em;cursor:pointer' title='Import'>&#xf00c;</button>" +
      "<button id='ttrym-settings' style='font-family:\"Font Awesome 5 Free\";border:none;background:none;color:inherit;font-size:1.5em;margin-left:.5em;cursor:pointer' title='Settings'>&#xf013;</button></p>"
  );

  document.getElementById("ttrym-site").addEventListener("change", function () {
    document.getElementById("ttrym-link").placeholder = sites.filter(
      (x) => x.name === this.value
    )[0].placeholder;
  });
  document.getElementById("ttrym-site").value = await GM.getValue("default");
  document.getElementById("ttrym-site").dispatchEvent(new Event("change"));

  document.addEventListener("click", function (e) {
    if (e.target?.id === "ttrym-dismiss") clearMessages();
  });

  document
    .getElementById("ttrym-submit")
    .addEventListener("click", async function () {
      clearMessages();
      if (!document.getElementById("ttrym-link").value)
        return printMessage(
          "error",
          "No URL specified! Please enter one and try again."
        );
      printMessage("progress", "Importing, please wait...");
      document.getElementById("ttrym-submit").disabled = true;

      if (await GM.getValue("artist"))
        document
          .querySelectorAll(".filed_under_delete a")
          .forEach((element) => {
            deleteFiledUnder(Number(element.href.match(/\d+/)));
          });

      try {
        const site = document.getElementById("ttrym-site").value;
        let input = sites.filter((x) => x.name === site)[0];
        let link = document.getElementById("ttrym-link").value;

        if (!link.match(/^https?:\/\//)) link = "https://" + link;

        if (!globToRegex(input.placeholder).test(link)) {
          const suggestion = sites.filter((x) =>
            globToRegex(x.placeholder).test(link)
          )[0];
          if (suggestion && (await GM.getValue("guess"))) {
            printMessage(
              "progress",
              "Using " + suggestion.name + " instead of " + input.name + "."
            );
            input = suggestion;
            document.getElementById("ttrym-site").value = input.name;
          } else {
            printMessage(
              "warning",
              "Entered URL does not match the selected site's placeholder. Request may not succeed."
            );
          }
        }

        link = input.transformer ? await input.transformer(link) : link;

        GM.xmlHttpRequest({
          method: "GET",
          url: link,
          headers: {
            "User-Agent": "TracklistToRYM/" + GM.info.script.version,
          },
          onload: async (response) => {
            let data = response.responseText;
            data = input.modifier ? await input.modifier(data) : data;

            let artist = "";
            let album = "";
            let result = "";
            let amount = 0;

            switch (input.extractor) {
              case "json":
                reduceJson(data, input.parent).forEach((element) => {
                  amount++;
                  const index = input.index
                    ? reduceJson(element, input.index)
                    : amount;
                  const title = input.title
                    ? reduceJson(element, input.title)
                    : "";
                  const length = input.length
                    ? reduceJson(element, input.length)
                    : "";
                  result += getResult(index, title, length);
                });
                artist = input.artist ? reduceJson(data, input.artist) : "";
                album = input.album ? reduceJson(data, input.album) : "";
                break;

              case "node":
              case "xml":
                const mime =
                  input.extractor === "xml" ? "text/xml" : "text/html";
                new DOMParser()
                  .parseFromString(data, mime)
                  .querySelectorAll(input.parent)
                  .forEach((element) => {
                    amount++;
                    const index =
                      parseNode(element.querySelector(input.index)) || amount;
                    const title =
                      parseNode(element.querySelector(input.title)) || "";
                    const length =
                      parseNode(element.querySelector(input.length)) || "";
                    result += getResult(index, title, length);
                  });
                artist =
                  parseNode(
                    new DOMParser()
                      .parseFromString(data, mime)
                      .querySelector(input.artist)
                  ) || "";
                album =
                  parseNode(
                    new DOMParser()
                      .parseFromString(data, mime)
                      .querySelector(input.album)
                  ) || "";
                break;

              case "regex":
                data
                  .replace(/\n/g, "")
                  .match(input.parent)
                  .forEach(function (i) {
                    amount++;
                    i = i.replace(/\n/g, "");
                    const index = input.index
                      ? i.match(input.index)[0].toString()
                      : amount;
                    const title = input.title
                      ? decodeHTML(i.match(input.title)[0])
                      : "";
                    const length = input.length ? i.match(input.length)[0] : "";
                    result += getResult(index, title, length);
                  });
                artist = input.artist
                  ? decodeHTML(data.match(input.artist)[0])
                  : "";
                album = input.album
                  ? decodeHTML(data.match(input.album)[0])
                  : "";
                break;

              default:
                document.getElementById("ttrym-submit").disabled = false;
                return printMessage(
                  "error",
                  input.extractor +
                    " is not a valid extractor. This is (probably) not your fault, please report this on <a href='https://codeberg.org/sun/userscripts/issues/new?title=" +
                    input.extractor +
                    "%20is%20not%20a%20valid%20extractor'>Codeberg</a>."
                );
            }

            if (amount === 0) {
              document.getElementById("ttrym-submit").disabled = false;
              return printMessage(
                "warning",
                "Did not find any tracks. Please check your URL and try again."
              );
            }

            artist = parseArtist(artist);
            album = parseAlbum(album);

            if (await GM.getValue("artist")) {
              GM.xmlHttpRequest({
                method: "GET",
                url:
                  "https://rateyourmusic.com/go/searchcredits?target=filedunder&searchterm=" +
                  encodeURIComponent(artist),
                onload: async (response) => {
                  /* jshint ignore:start */
                  // eslint-disable-next-line no-eval
                  eval(
                    new DOMParser()
                      .parseFromString(response.responseText, "text/html")
                      .getElementsByClassName("result")[0]
                      .getAttribute("onClick")
                      .replace("window.parent.", "")
                  );
                  /* jshint ignore:end */
                },
              });
            }
            if (await GM.getValue("release"))
              document.getElementById("title").value = album;

            goAdvanced();
            document.getElementById("track_advanced").value =
              (await GM.getValue("append"))
                ? document.getElementById("track_advanced").value + result
                : result;
            goSimple();

            if (
              (await GM.getValue("sources")) &&
              !document
                .getElementById("notes")
                .value.includes(document.getElementById("ttrym-link").value)
            ) {
              document.getElementById("notes").value =
                document.getElementById("notes").value +
                (document.getElementById("notes").value === "" ? "" : "\n") +
                document.getElementById("ttrym-link").value;
            }
            document.getElementById("ttrym-link").value = "";

            document.getElementById("ttrym-submit").disabled = false;
            printMessage("success", "Imported " + amount + " tracks.");
          },

          onerror: (response) => {
            document.getElementById("ttrym-submit").disabled = false;
            printMessage("error", response.responseText);
          },
        });
      } catch (e) {
        document.getElementById("ttrym-submit").disabled = false;
        printMessage("error", e.toString());
      }
    });

  document
    .getElementById("ttrym-settings")
    .addEventListener("click", async function () {
      document.body.style.overflow = "hidden";

      document.body.insertAdjacentHTML(
        "beforeend",
        "<div id='ttrym-settings-wrapper' style='box-sizing:border-box;width:100vw;height:100vh;position:fixed;top:45px;background:rgba(255,255,255,0.75);padding:50px;z-index:80'>" +
          "<div class='submit_step_box' style='padding:25px;height:calc(100% - 50px);overflow:auto'><span class='submit_step_header' style='margin:0!important'>" +
          "TracklistToRYM: <span class='submit_step_header_title'>Settings</span></span>" +
          "<div class='submit_field_header_separator' style='margin-top:15px;margin-bottom:15px'></div>" +
          "<p><b class='submit_field_header' style='display:block;margin-bottom:-1em'>Supply additional data</b><br>" +
          "While TracklistToRYM's main goal is to fill in tracklists, it can also enter additional metadata.<br>" +
          "Keep in mind that if enabled and used, any previously input data will be replaced.</p>" +
          "<input id='ttrym-artist' name='ttrym-artist' type='checkbox' style='margin-bottom:.25em'></input><label for='ttrym-artist' style='position:relative;bottom:1px'> Artist name <span style='opacity:0.5;font-weight:lighter'>Step 1.3 (“File under”)</span></label><br>" +
          "<input id='ttrym-release' name='ttrym-release' type='checkbox' style='margin-bottom:.25em'></input><label for='ttrym-release' style='position:relative;bottom:1px'> Release title <span style='opacity:0.5;font-weight:lighter'>Step 2.1 (“Title”)</span></label>" +
          "<div class='submit_field_header_separator' style='margin-top:15px;margin-bottom:15px'></div>" +
          "<p><b class='submit_field_header' style='display:block;margin-bottom:-1em'>Manage sites</b><br>" +
          "Choose which sites to show and which ones to hide in the TracklistToRYM selection box.</p>" +
          sitestmp
            .map(
              (x) =>
                "<input type='checkbox' style='margin-bottom:.25em' class='ttrym-checkbox' id='ttrym-site-" +
                x.name.replace(/\s/g, "") +
                "' name='" +
                x.name +
                "'><label for='ttrym-site-" +
                x.name.replace(/\s/g, "") +
                "' style='position:relative;bottom:1px'> " +
                x.name +
                " <span style='opacity:0.5;font-weight:lighter'>" +
                x.placeholder +
                "</span></label><br>"
            )
            .join("") +
          "<div style='margin-top:15px'><button id='ttrym-enable'>Enable all sites</button><button id='ttrym-invert' style='margin-left:10px'>Invert selection</button><button id='ttrym-disable' style='margin-left:10px'>Disable all sites</button></div>" +
          "<div class='submit_field_header_separator' style='margin-top:15px;margin-bottom:15px'></div>" +
          "<p><b class='submit_field_header' style='display:block;margin-bottom:-1em'>Set default site</b><br>" +
          "Choose which site should be selected by default; you may choose the site that you use the most.<br>" +
          "If the chosen site isn't already, it will be enabled automatically.</p>" +
          "<select id='ttrym-default'>" +
          sitestmp
            .map(
              (x) => "<option value='" + x.name + "'>" + x.name + "</option>"
            )
            .join("") +
          "</select>" +
          "<div class='submit_field_header_separator' style='margin-top:15px;margin-bottom:15px'></div>" +
          "<p><b class='submit_field_header' style='display:block;margin-bottom:-1em'>Auto-select sites</b><br>" +
          "Select whether to guess sites from their URL and automatically select them.<br>" +
          "This has been the default behavior since version 1.10.0.</p>" +
          "<input id='ttrym-change' name='ttrym-change' type='checkbox' style='margin-bottom:.25em'></input><label for='ttrym-change' style='position:relative;bottom:1px'> Guess and automatically select sites</label>" +
          "<div class='submit_field_header_separator' style='margin-top:15px;margin-bottom:15px'></div>" +
          "<p><b class='submit_field_header' style='display:block;margin-bottom:-1em'>Enable new sites by default</b><br>" +
          "Select whether to automatically enable new sites for which support has been added after an update.<br>" +
          "This has been the default behavior since version 1.26.0.</p>" +
          "<input id='ttrym-enbydef' name='ttrym-enbydef' type='checkbox' style='margin-bottom:.25em'></input><label for='ttrym-enbydef' style='position:relative;bottom:1px'> Automatically enable newly supported sites</label>" +
          "<div class='submit_field_header_separator' style='margin-top:15px;margin-bottom:15px'></div>" +
          "<p><b class='submit_field_header' style='display:block;margin-bottom:-1em'>Append instead of replace</b><br>" +
          "Enabling this will allow you to combine multiple releases into one by keeping previous tracks when inserting new ones.</p>" +
          "<input id='ttrym-append' name='ttrym-append' type='checkbox' style='margin-bottom:.25em'></input><label for='ttrym-append' style='position:relative;bottom:1px'> Append tracks to list</label>" +
          "<div class='submit_field_header_separator' style='margin-top:15px;margin-bottom:15px'></div>" +
          "<p><b class='submit_field_header' style='display:block;margin-bottom:-1em'>Add URL to sources</b><br>" +
          "Select whether to automatically add the entered URL to the submission sources in step five.<br>" +
          "This has been the default behavior since version 1.3.0.</p>" +
          "<input id='ttrym-sources' name='ttrym-sources' type='checkbox' style='margin-bottom:.25em'></input><label for='ttrym-sources' style='position:relative;bottom:1px'> Automatically add URLs to sources</label>" +
          "<div class='submit_field_header_separator' style='margin-top:15px;margin-bottom:15px'></div>" +
          "<p>FYI: You can also directly edit these settings in your userscript manager:</p>" +
          "<p><b><a href='https://www.tampermonkey.net/'>Tampermonkey</a>:</b> Dashboard → Installed userscripts → TracklistToRYM → Edit → Storage<br>" +
          "<b><a href='https://violentmonkey.github.io/'>Violentmonkey</a>:</b> Open Dashboard → Installed scripts → TracklistToRYM → Edit → Values</p>" +
          "<div style='margin-bottom:25px'><button id='ttrym-save'>Save and reload page</button><button id='ttrym-discard' style='margin-left:10px'>Close window without saving</button><button id='ttrym-reset' style='margin-left:10px'>Reset and reload page</button></div>" +
          "</div></div>"
      );

      document.getElementById("ttrym-artist").checked = await GM.getValue(
        "artist"
      );
      document.getElementById("ttrym-release").checked = await GM.getValue(
        "release"
      );
      Array.from(document.getElementsByClassName("ttrym-checkbox")).forEach(
        (element) => {
          if (sites.map((x) => x.name).includes(element.name))
            element.checked = true;
        }
      );
      document.getElementById("ttrym-default").value = await GM.getValue(
        "default"
      );
      document.getElementById("ttrym-change").checked = await GM.getValue(
        "guess"
      );
      document.getElementById("ttrym-enbydef").checked = await GM.getValue(
        "enbydef"
      );
      document.getElementById("ttrym-append").checked = await GM.getValue(
        "append"
      );
      document.getElementById("ttrym-sources").checked = await GM.getValue(
        "sources"
      );

      document
        .getElementById("ttrym-enable")
        .addEventListener("click", function () {
          Array.from(document.getElementsByClassName("ttrym-checkbox")).forEach(
            (element) => {
              element.checked = true;
            }
          );
        });

      document
        .getElementById("ttrym-invert")
        .addEventListener("click", function () {
          Array.from(document.getElementsByClassName("ttrym-checkbox")).forEach(
            (element) => {
              element.checked = !element.checked;
            }
          );
        });

      document
        .getElementById("ttrym-disable")
        .addEventListener("click", function () {
          Array.from(document.getElementsByClassName("ttrym-checkbox")).forEach(
            (element) => {
              element.checked = false;
            }
          );
        });

      document
        .getElementById("ttrym-reset")
        .addEventListener("click", async function () {
          if (confirm("Do you really want to reset all preferences?")) {
            (await GM.listValues()).forEach(
              async (setting) => await GM.deleteValue(setting)
            );
            location.reload();
          }
        });

      document
        .getElementById("ttrym-save")
        .addEventListener("click", async function () {
          const sites = Array.from(
            document.querySelectorAll(".ttrym-checkbox:checked")
          ).map((x) => x.name);

          await GM.setValue(
            "artist",
            document.getElementById("ttrym-artist").checked
          );
          await GM.setValue(
            "release",
            document.getElementById("ttrym-release").checked
          );
          await GM.setValue("sitesv2", sitesV1ToV2(sites));
          await GM.setValue(
            "default",
            document.getElementById("ttrym-default").value
          );
          await GM.setValue(
            "guess",
            document.getElementById("ttrym-change").checked
          );
          await GM.setValue(
            "enbydef",
            document.getElementById("ttrym-enbydef").checked
          );
          await GM.setValue(
            "append",
            document.getElementById("ttrym-append").checked
          );
          await GM.setValue(
            "sources",
            document.getElementById("ttrym-sources").checked
          );

          if (!sites.includes(document.getElementById("ttrym-default").value))
            await GM.setValue(
              "sitesv2",
              sitesV1ToV2(
                sites.concat(document.getElementById("ttrym-default").value)
              )
            );

          location.reload();
        });

      document
        .getElementById("ttrym-discard")
        .addEventListener("click", function () {
          document.body.style.overflow = "initial";
          document
            .getElementById("ttrym-settings-wrapper")
            .parentNode.removeChild(
              document.getElementById("ttrym-settings-wrapper")
            );
        });
    });

  function clearMessages(levels) {
    if (!levels) levels = ["progress", "success", "warning", "error"];
    if (!Array.isArray(levels)) levels = [levels];
    document
      .querySelectorAll(levels.map((x) => "#ttrym-" + x).join(","))
      .forEach((x) => x.parentNode.removeChild(x));
    msgPosted = false;
    if (document.getElementById("ttrym-dismiss"))
      document
        .getElementById("ttrym-dismiss")
        .parentNode.removeChild(document.getElementById("ttrym-dismiss"));
  }

  function decodeHTML(input) {
    if (!input) return;
    const dom = new DOMParser().parseFromString(input, "text/html");
    return dom.documentElement.textContent;
  }

  function getResult(index, title, length) {
    return (
      parseIndex(index) +
      "|" +
      parseTitle(title) +
      "|" +
      parseLength(length) +
      "\n"
    );
  }

  function globToRegex(glob) {
    return new RegExp(
      glob.replace(/[.+\-?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")
    );
  }

  function parseAlbum(album) {
    return album.trim().replace(/^–\s/, "");
  }

  function parseArtist(artist) {
    return artist.trim();
  }

  function parseIndex(index) {
    return index.toString().trim().replace(/^0+/, "").replace(/\.$/, "");
  }

  function parseLength(length) {
    if (!length) return "";
    if (typeof length !== "string") length = length.toString();
    if (length === "?:??" || length === "-") return "";
    if (length.match(/PT\d{2}H\d{2}M\d{2}S/))
      length = length.replace(/[PTS]/g, "").replace(/[HM]/g, ":");
    if (Number(length))
      length = new Date(Number(length)).toISOString().split(/[TZ]/)[1];
    let matches = length.match(/(\d+:)+\d+/);
    if (matches) {
      matches = matches[0].replace(/^(0+:?)+/, "");
      if (!matches.includes(":")) {
        if (matches < 10) matches = "0" + matches;
        matches = "0:" + matches;
      }
      matches = matches.replace(/\..*/, "");
      return matches;
    }
    return length;
  }

  function parseNode(node) {
    return node ? (node.firstChild ? node.firstChild.nodeValue : "") : "";
  }

  function parseTitle(title) {
    return title.trim().replace(/^(& {2})?(- )/, "");
  }

  function printMessage(level, message) {
    const colors = {
      progress: "#777",
      success: "green",
      warning: "orange",
      error: "red",
    };
    parent.insertAdjacentHTML(
      "beforeend",
      "<p id='ttrym-" +
        level +
        "' style='color:" +
        colors[level] +
        ";" +
        (msgPosted ? "" : "margin-top:.5em;") +
        "margin-bottom:0'>" +
        level.charAt(0).toUpperCase() +
        level.slice(1) +
        ": " +
        message +
        "</p>"
    );
    msgPosted = true;
    if (!document.getElementById("ttrym-dismiss"))
      document
        .getElementById("ttrym-settings")
        .insertAdjacentHTML(
          "beforebegin",
          "<button id='ttrym-dismiss' style='font-family:\"Font Awesome 5 Free\";border:none;background:none;color:inherit;font-size:1.5em;margin-left:.5em;cursor:pointer' title='Dismiss'>&#xf0c9;</button>"
        );
  }

  function reduceJson(object, path) {
    if (typeof object !== "object") object = JSON.parse(object);
    return path.split(".").reduce((acc, cur) => acc[cur], object);
  }

  function sitesV1ToV2(sites) {
    Array.isArray(sites) || (sites = sites.split(","));
    const output = {};
    sitestmp.forEach((x) => {
      output[x.name] = sites.includes(x.name);
    });
    return output;
  }
})();

// @license-end