Greasy Fork

Greasy Fork is available in English.

TracklistToRYM

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

当前为 2024-06-14 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name               TracklistToRYM
// @name:de            TracklistToRYM
// @name:en            TracklistToRYM
// @namespace          sun/userscripts
// @version            1.44.2
// @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://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://rateyourmusic.com/releases/ac
// @include            https://rateyourmusic.com/releases/ac?*
// @match              https://rateyourmusic.com/releases/ac
// @match              https://rateyourmusic.com/releases/ac?*
// @connect            45cat.com
// @connect            45worlds.com
// @connect            7digital.com
// @connect            allmusic.com
// @connect            amazon.com
// @connect            archive.org
// @connect            awa.fm
// @connect            azurewebsites.net
// @connect            baer.works
// @connect            bandcamp.com
// @connect            beatbump.io
// @connect            beatport.com
// @connect            bleep.com
// @connect            boomplay.com
// @connect            bugs.co.kr
// @connect            castalbums.org
// @connect            deezer.com
// @connect            discogs.com
// @connect            e-onkyo.com
// @connect            freemusicarchive.org
// @connect            genie.co.kr
// @connect            genius.com
// @connect            gnudb.org
// @connect            hungama.com
// @connect            insprill.net
// @connect            itch.zone
// @connect            jam.coop
// @connect            jiosaavn.com
// @connect            junodownload.com
// @connect            karent.jp
// @connect            khinsider.com
// @connect            last.fm
// @connect            line.me
// @connect            loot.co.za
// @connect            maniadb.com
// @connect            melon.com
// @connect            metal-archives.com
// @connect            migalmoreno.com
// @connect            mirlo.space
// @connect            mora.jp
// @connect            music-flo.com
// @connect            musicbrainz.org
// @connect            musik-sammler.de
// @connect            musixmatch.com
// @connect            mysound.jp
// @connect            napster.com
// @connect            naver.com
// @connect            naxos.com
// @connect            nts.live
// @connect            open.audio
// @connect            oricon.co.jp
// @connect            ototoy.jp
// @connect            pandora.com
// @connect            pulsewidth.org.uk
// @connect            qobuz.com
// @connect            qq.com
// @connect            radiofreefedi.net
// @connect            rateyourmusic.com
// @connect            rauversion.com
// @connect            recochoku.jp
// @connect            resonate.coop
// @connect            secondhandsongs.com
// @connect            setlist.fm
// @connect            sny.sh
// @connect            sonemic.com
// @connect            soundcloud.com
// @connect            spirit-of-rock.com
// @connect            streetvoice.com
// @connect            touhoudb.com
// @connect            tower.jp
// @connect            traxsource.com
// @connect            utaitedb.net
// @connect            vgmdb.net
// @connect            vinyl-digital.com
// @connect            vocadb.net
// @connect            yandex.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.registerMenuCommand
// @grant              GM_registerMenuCommand
// @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://forgejo.sny.sh/sun/userscripts/raw/branch/main/icons/TracklistToRYM.png
// @copyright          2020-present, Sunny (https://sny.sh/)
// @license            Hippocratic License; https://forgejo.sny.sh/sun/userscripts/src/branch/main/LICENSE.md
// ==/UserScript==

(async function () {
  "use strict";

  GM.registerMenuCommand("Settings", openSettings);
  GM.registerMenuCommand("Reset", openReset);

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

  const sitestmp = [
    {
      name: "7digital",
      extractor: "node",
      placeholder: "https://www.7digital.com/artist/*/release/*",
      artist: ".release-info-artist a",
      album: ".release-info-title",
      parent: ".release-track",
      index: ".release-track-preview-text",
      title: ".release-track-name p",
      length: ".release-track-time",
    },
    {
      name: "45cat",
      extractor: "node",
      placeholder: "https://www.45cat.com/record/*",
      artist: "[href^='/artist/']",
      album: false,
      parent: ".tablegrey tr:not(.tableheader)",
      index: "td:first-child b",
      title: "td:nth-child(3)",
      length: false,
    },
    {
      name: "45worlds",
      extractor: "node",
      placeholder: "https://www.45worlds.com/*/*/*",
      artist: "[href*='/artist/']",
      album: false,
      parent: ".tablegrey tr:not(.tableheader)",
      index: "td:first-child b",
      title: "td:nth-child(3)",
      length: false,
    },
    {
      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",
    },
    {
      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",
    },
    {
      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",
    },
    {
      name: "AWA",
      extractor: "node",
      placeholder: "https://s.awa.fm/album/*",
      artist: "a[href*='/artist/']",
      album: "h1",
      parent: "._2omln0eOHzVUC3XnofU23C",
      index: "._1l99Wo9zW6q9fpUh0OC2Zz",
      title: "a[href*='/track/']",
      length: "._2Nydhx7SzJ7HE2wvcjgZGa",
    },
    {
      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",
    },
    {
      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,
    },
    {
      name: "Beatbump",
      extractor: "node",
      placeholder: "https://beatbump.io/release?*",
      artist: ".secondary a",
      album: ".box-title",
      parent: ".m-item",
      index: ".index span:last-child",
      title: ".title",
      length: ".length",
    },
    {
      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",
    },
    {
      name: "blamscamp",
      extractor: "node",
      placeholder: "https://html-classic.itch.zone/html/*/index.html",
      artist: ".artist",
      album: ".album",
      parent: ".song_list li",
      index: "p",
      title: ".title",
      length: "i",
    },
    {
      name: "Bleep",
      extractor: "node",
      placeholder: "https://bleep.com/release/*",
      artist: ".artist span",
      album: ".release-title",
      parent: ".track-list > li",
      index: ".track-number",
      title: ".track-name span[itemprop]",
      length: ".track-duration",
    },
    {
      name: "Boomplay",
      extractor: "node",
      placeholder: "https://boomplay.com/albums/*",
      artist: ".ownerWrap strong",
      album: ".summaryWrap h1",
      parent: ".morePart_musics li",
      index: ".serialNum",
      title: ".songName",
      length: "time",
    },
    {
      name: "Bugs!",
      extractor: "node",
      placeholder: "https://music.bugs.co.kr/album/*",
      artist: ".basicInfo a[href*='/artist/']",
      album: ".pgTitle h1",
      parent: ".trackList tbody tr",
      index: ".trackIndex em",
      title: ".title a",
      length: false,
    },
    {
      name: "CastAlbums",
      extractor: "node",
      placeholder: "https://castalbums.org/recordings/*/*",
      artist: false,
      album: "li.active a",
      parent: ".tracks-table-divided",
      index: "td:first-child",
      title: "td a",
      length: "td:last-child",
      modifier: async (data) => {
        data = new DOMParser().parseFromString(data, "text/html");
        Array.from(
          data.querySelectorAll(".tracks-table-divided + tr:not([class])"),
        )
          .reverse()
          .forEach((row) => {
            row.previousElementSibling.getElementsByTagName(
              "a",
            )[0].textContent +=
              " / " + row.getElementsByTagName("a")[0].textContent;
            row.previousElementSibling.lastElementChild.textContent =
              row.lastElementChild.textContent;
          });
        data = new XMLSerializer().serializeToString(data);
        return data;
      },
    },
    {
      name: "cdr",
      extractor: "node",
      placeholder: "https://baer.works/cdr/",
      artist: false,
      album: false,
      parent: "#playlist li",
      index: false,
      title: ".track",
      length: 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",
    },
    {
      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",
    },
    {
      name: "Encyclopaedia Metallum",
      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']",
    },
    {
      name: "e-onkyo music",
      extractor: "node",
      placeholder: "https://www.e-onkyo.com/music/album/*",
      artist: ".jacketDetailArea .artistsName a",
      album: ".jacketDetailArea .packageTtl",
      parent: ".musicBoxDetail",
      index: ".musicListNo",
      title: ".musicTtl span",
      length: ".musicTime",
    },
    {
      name: "Faircamp",
      extractor: "node",
      placeholder: "https://faircamp.radiofreefedi.net/*/*",
      artist: ".release_artists a",
      album: "h1",
      parent: ".track",
      index: ".track_number",
      title: ".track_title",
      length: ".duration",
      modifier: async (data) => {
        data = new DOMParser().parseFromString(data, "text/html");
        data.querySelectorAll(".track_time").forEach((y) => y.remove());
        data = new XMLSerializer().serializeToString(data);
        return data;
      },
    },
    {
      name: "FLO",
      extractor: "json",
      placeholder: "https://www.music-flo.com/detail/album/*",
      artist: "data.list.0.representationArtist.name",
      album: "data.list.0.album.title",
      parent: "data.list",
      index: "trackNo",
      title: "name",
      length: "playTime",
      transformer: async (link) => {
        return (
          "https://www.music-flo.com/api/meta/v1/album/" +
          new URL(link).pathname.split("/")[3] +
          "/track"
        );
      },
    },
    {
      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",
    },
    {
      name: "Funkwhale",
      extractor: "json",
      placeholder: "https://open.audio/library/albums/*",
      artist: "results.0.artist.name",
      album: "results.0.album.title",
      parent: "results",
      index: "position",
      title: "title",
      length: "uploads.0.duration",
      transformer: async (link) => {
        return (
          new URL(link).origin +
          "/api/v1/tracks/?ordering=disc_number,position&include_channels=true&album=" +
          new URL(link).pathname.split("/")[3]
        );
      },
      modifier: async (data) => {
        data = JSON.parse(data);
        data.results.forEach(
          (y) =>
            (y.uploads[0].duration = new Date(y.uploads[0].duration * 1000)
              .toISOString()
              .slice(11, 19)),
        );
        data = JSON.stringify(data);
        return data;
      },
    },
    {
      name: "genie",
      extractor: "node",
      placeholder: "https://genie.co.kr/detail/albumInfo?axnm=*",
      artist: "a[onclick*=artistInfo]",
      album: ".name",
      parent: "tbody tr.list",
      index: ".number",
      title: ".title",
      length: false,
      modifier: async (data) => {
        data = new DOMParser().parseFromString(data, "text/html");
        data.querySelector(".icon").remove();
        data = new XMLSerializer().serializeToString(data);
        return data;
      },
    },
    {
      name: "Genius",
      extractor: "node",
      placeholder: "https://genius.com/albums/*/*",
      artist: "h2 a",
      album: "h1",
      parent: ".chart_row",
      index: ".chart_row-number_container-number span",
      title: ".chart_row-content-title",
      length: false,
    },
    {
      name: "GnuDB",
      extractor: "node",
      placeholder: "https://gnudb.org/cd/*",
      artist: "h1",
      album: "h2",
      parent: "tr:not(:first-child)",
      index: "td:first-child",
      title: "td:last-child",
      length: "td[style]",
    },
    {
      name: "HDtracks",
      extractor: "json",
      placeholder: "https://www.hdtracks.com/#/album/*",
      artist: "mainArtist",
      album: "name",
      parent: "tracks",
      index: "index",
      title: "name",
      length: "duration",
      transformer: async (link) => {
        return (
          "https://hdtracks.azurewebsites.net/api/v1/album/" +
          new URL(link).hash.split("/")[2]
        );
      },
      modifier: async (data) => {
        data = JSON.parse(data);
        data.tracks.forEach(
          (y) =>
            (y.duration = new Date(y.duration * 1000)
              .toISOString()
              .slice(11, 19)),
        );
        data = JSON.stringify(data);
        return data;
      },
    },
    {
      name: "Hungama",
      extractor: "node",
      placeholder: "https://www.hungama.com/album/*",
      artist: ".artist-details #pajax_a",
      album: "h1",
      parent: ".block-songs [role=row] td",
      index: false,
      title: "h4 a",
      length: false,
    },
    {
      name: "Intellectual",
      extractor: "node",
      placeholder: "https://intellectual.insprill.net/albums/*/*",
      artist: "cite",
      album: ".title",
      parent: ".song",
      index: false,
      title: ".song-title",
      length: 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,
    },
    {
      name: "jam.coop",
      extractor: "node",
      placeholder: "https://jam.coop/artists/*/albums/*",
      artist: "h2 a",
      album: "h1",
      parent: ".flex.mb-2",
      index: "div",
      title: "span:first-child",
      length: "span:last-child",
    },
    {
      name: "JioSaavn",
      extractor: "node",
      placeholder: "https://jiosaavn.com/album/*",
      artist: "h1 + p a:first-child",
      album: "h1",
      parent: ".o-list-bare li",
      index: ".o-snippet__item:first-child .o-snippet__action-final",
      title: "h4 a",
      length: 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",
    },
    {
      name: "KARENT",
      extractor: "node",
      placeholder: "https://karent.jp/album/*",
      artist: ".album__deta-artist a",
      album: ".album__title",
      parent: ".songlist__box",
      index: false,
      title: ".songlist__title",
      length: false,
      modifier: async (data) => {
        data = new DOMParser().parseFromString(data, "text/html");
        data
          .querySelectorAll(".album__deta-artist a img")
          .forEach((y) => y.remove());
        data.querySelectorAll(".songlist__num").forEach((y) => y.remove());
        data = new XMLSerializer().serializeToString(data);
        return data;
      },
    },
    {
      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",
    },
    {
      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",
    },
    {
      name: "LINE MUSIC",
      extractor: "json",
      placeholder: "https://music.line.me/webapp/album/*",
      artist: "response.result.tracks.0.artists.0.artistName",
      album: "response.result.tracks.0.album.albumTitle",
      parent: "response.result.tracks",
      index: "trackNumber",
      title: "trackTitle",
      length: false,
      transformer: async (link) => {
        return (
          "https://music.line.me/api2/album/" +
          new URL(link).pathname.split("/")[3] +
          "/tracks.v1"
        );
      },
    },
    {
      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,
    },
    {
      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",
    },
    {
      name: "Melon",
      extractor: "node",
      placeholder: "https://www.melon.com/album/detail.htm?albumId=*",
      artist: ".artist_name span",
      album: ".song_name",
      parent: "tbody tr",
      index: ".rank",
      title: ".ellipsis a",
      length: false,
      modifier: async (data) => {
        data = new DOMParser().parseFromString(data, "text/html");
        data.querySelector(".song_name strong").remove();
        data = new XMLSerializer().serializeToString(data);
        return data;
      },
    },
    {
      name: "Mirlo",
      extractor: "json",
      placeholder: "https://mirlo.space/*/release/*",
      artist: "result.artist.name",
      album: "result.title",
      parent: "result.tracks",
      index: "order",
      title: "title",
      length: "metadata.duration",
      transformer: async (link) => {
        return (
          "https://api.mirlo.space/v1/trackGroups/" +
          new URL(link).pathname.split("/")[3] +
          "?artistId=" +
          new URL(link).pathname.split("/")[1]
        );
      },
      modifier: async (data) => {
        data = JSON.parse(data);
        data.result.tracks.forEach(
          (y) =>
            (y.metadata.duration = new Date(y.metadata.duration * 1000)
              .toISOString()
              .slice(11, 19)),
        );
        data = JSON.stringify(data);
        return data;
      },
    },
    {
      name: "mora",
      extractor: "json",
      placeholder: "https://mora.jp/package/*/*",
      artist: "artistName",
      album: "title",
      parent: "trackList",
      index: "trackNo",
      title: "title",
      length: "durationStr",
      transformer: async (link) => {
        return await new Promise((resolve) => {
          GM.xmlHttpRequest({
            method: "GET",
            url: link,
            onload: async (response) => {
              let data = new DOMParser().parseFromString(
                response.responseText,
                "text/html",
              );
              data = JSON.parse(
                data
                  .querySelector("[name='msApplication-Arguments']")
                  .getAttribute("content"),
              );
              resolve(
                "https://cf.mora.jp/contents/package/" +
                  data.mountPoint +
                  "/" +
                  data.labelId +
                  "/" +
                  data.materialNo.slice(0, -6).padStart(4, "0") +
                  "/" +
                  data.materialNo.slice(-6, -3) +
                  "/" +
                  data.materialNo.slice(-3) +
                  "/packageMeta.json",
              );
            },
          });
        });
      },
    },
    {
      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;
      },
    },
    {
      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",
    },
    {
      name: "Musixmatch",
      extractor: "node",
      placeholder: "https://www.musixmatch.com/album/*/*",
      artist: ".mxm-album-banner__artist a",
      album: ".mxm-album-banner__name",
      parent: ".mui-collection--list li",
      index: ".mui-cell__index-view",
      title: ".mui-cell__title",
      length: false,
    },
    {
      name: "mysound",
      extractor: "node",
      placeholder: "https://mysound.jp/album/*",
      artist: ".common__lower__topBox .artist a",
      album: ".common__lower__topBox h2",
      parent: ".album__recordingList__contents li",
      index: ".num",
      title: ".title a",
      length: ".time",
    },
    {
      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,
    },
    {
      name: "NAVER VIBE",
      extractor: "xml",
      placeholder: "https://vibe.naver.com/album/*",
      artist: "track:first-child album artists artist artistName",
      album: "track:first-child album albumTitle",
      parent: "track",
      index: "trackNumber",
      title: "trackTitle",
      length: "playTime",
      transformer: async (link) => {
        return (
          "https://apis.naver.com/vibeWeb/musicapiweb/album/" +
          new URL(link).pathname.split("/")[2] +
          "/tracks"
        );
      },
    },
    {
      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)",
    },
    {
      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,
    },
    {
      name: "ORICON MUSIC",
      extractor: "node",
      placeholder: "https://music.oricon.co.jp/php/cd/CdTop.php?cd=*",
      artist: ".info .headline3 span",
      album: ".info h1",
      parent: ".search_list tr[itemprop=tracks]",
      index: ".rank01",
      title: ".truncate_h2 span",
      length: "td:nth-last-child(3)",
      modifier: async (data) => {
        data = new DOMParser().parseFromString(data, "text/html");
        data
          .querySelectorAll("meta[itemprop=duration]")
          .forEach((y) => y.remove());
        data = new XMLSerializer().serializeToString(data);
        return data;
      },
    },
    {
      name: "OTOTOY",
      extractor: "node",
      placeholder: "https://ototoy.jp/_/default/p/*",
      artist: ".album-artist a",
      album: ".album-title",
      parent: "#tracklist tr[class]",
      index: "[id^=cvs_]",
      title: "[id^=title-]",
      length: ".item.center",
      modifier: async (data) => {
        data = new DOMParser().parseFromString(data, "text/html");
        data.querySelectorAll(".album-artist a i").forEach((y) => y.remove());
        data = new XMLSerializer().serializeToString(data);
        return data;
      },
    },
    {
      name: "Pandora",
      extractor: "regex",
      placeholder: "https://www.pandora.com/artist/*/*",
      artist: /(?<="artistName":").*?(?=")/,
      album: /(?<="albumTitle":").*?(?=")/,
      parent: /(?<={"musicId").*?(?=(true|false)})/g,
      index: /(?<="trackNum":).*?(?=,)/,
      title: /(?<="songTitle":").*?(?=",)/,
      length: /(?<="trackLength":).*?(?=,)/,
      modifier: async (data) => {
        data = data.replace(/(?<="trackLength":\d+)(?=,)/g, "000");
        return data;
      },
    },
    {
      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",
    },
    {
      name: "QQ Music",
      extractor: "node",
      placeholder: "https://y.qq.com/n/ryqq/albumDetail/*",
      artist: ".data__singer_txt",
      album: ".data__name_txt",
      parent: ".songlist__list li",
      index: ".songlist__number",
      title: ".songlist__songname_txt a",
      length: ".songlist__time",
    },
    {
      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");
        data
          .querySelectorAll(".tracklist_title .artist, .tracklist_title .work")
          .forEach((link) => {
            const title = document.createTextNode(
              link.hasAttribute("title")
                ? "[" +
                    link.getAttribute("title").slice(1, -1) +
                    "," +
                    link.innerText +
                    "]"
                : link.innerText,
            );
            link.parentNode.replaceChild(title, link);
          });
        data.querySelectorAll(".tracklist_title b").forEach((highlight) => {
          const title = document.createTextNode(
            "[b]" + highlight.innerText + "[/b]",
          );
          highlight.parentNode.replaceChild(title, highlight);
        });
        data = new XMLSerializer().serializeToString(data);
        return data;
      },
      default: true,
    },
    {
      name: "Rauversion",
      extractor: "node",
      placeholder: "https://rauversion.com/playlists/*",
      artist: "h5 a",
      album: "h4 a",
      parent: ".divide-muted li",
      index: false,
      title: "p:not([class])",
      length: false,
    },
    {
      name: "RecoChoku",
      extractor: "node",
      placeholder: "https://recochoku.jp/album/*",
      artist: ".c-product-main-detail__artist-inner",
      album: ".c-product-main-detail__title",
      parent: ".album-track-list__item",
      index: ".album-track-list__number",
      title: ".album-track-list__title-inner",
      length: ".album-track-list__spec",
    },
    {
      name: "Resonate",
      extractor: "json",
      placeholder: "https://stream.resonate.coop/artist/*/release/*",
      artist: "release.data.display_artist",
      album: "release.data.title",
      parent: "release.data.items",
      index: "index",
      title: "track.title",
      length: "track.duration",
      modifier: async (data) => {
        data = data.match(/window\.initialState=JSON\.parse\('(.*?)'\)/)[1];
        data = JSON.parse(data);
        data.release.data.items.forEach(
          (y) =>
            (y.track.duration = new Date(y.track.duration * 1000)
              .toISOString()
              .slice(11, 19)),
        );
        data = JSON.stringify(data);
        return data;
      },
    },
    {
      name: "SecondHandSongs",
      extractor: "node",
      placeholder: "https://secondhandsongs.com/release/*",
      artist: ".entity-title .link-performer",
      album: ".entity-title .link-release",
      parent: "tbody tr",
      index: false,
      title: ".link-performance span",
      length: false,
    },
    {
      name: "setlist.fm",
      extractor: "node",
      placeholder: "https://www.setlist.fm/setlist/*/*/*",
      artist: ".artistImageBlurred span",
      album: false,
      parent: ".song",
      index: false,
      title: ".songLabel",
      length: false,
    },
    {
      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",
    },
    {
      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=").*?(?=" \/>)/,
    },
    {
      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=").*?(?=" \/>)/,
    },
    {
      name: "Spirit of Rock",
      extractor: "node",
      placeholder: "https://www.spirit-of-rock.com/en/album/*/*",
      artist: "#BandInfo h3",
      album: "#album h2",
      parent: "#tracklist tr",
      index: "td:first-child div:first-child",
      title: "td:first-child div:last-child",
      length: "td:last-child",
      modifier: async (data) => {
        data = new DOMParser().parseFromString(data, "text/html");
        data.querySelectorAll("#tracklist tr").forEach((track) => {
          const title = document.createElement("div");
          title.append(
            document.createTextNode(track.firstChild.lastChild.textContent),
          );
          track.firstChild.appendChild(title);
        });
        data = new XMLSerializer().serializeToString(data);
        return data;
      },
    },
    {
      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,
    },
    {
      name: "Tent",
      extractor: "node",
      placeholder: "https://tent.sny.sh/release.php?*",
      artist: "h1 a",
      album: "#ttrym-album",
      parent: ".tracks tr",
      index: "td:first-child",
      title: "td:nth-child(2)",
      length: "td:last-child",
      modifier: async (data) => {
        data = new DOMParser().parseFromString(data, "text/html");
        data.body.insertAdjacentHTML(
          "beforeend",
          "<p id='ttrym-album'>" +
            data.querySelector("h1").lastChild.textContent.slice(2) +
            "</p>",
        );
        data
          .querySelectorAll(".tracks tr a")
          .forEach((track) => (track.outerHTML = track.textContent));
        data
          .querySelectorAll(".tracks tr audio")
          .forEach((track) => track.parentElement.parentElement.remove());
        data = new XMLSerializer().serializeToString(data);
        return data;
      },
    },
    {
      name: "TouhouDB",
      extractor: "json",
      placeholder: "https://touhoudb.com/Al/*",
      artist: "artistLinks.0.name",
      album: "name",
      parent: "songs",
      index: "trackNumber",
      title: "name",
      length: "song.lengthSeconds",
      transformer: async (link) => {
        return (
          "https://touhoudb.com/api/albums/" +
          new URL(link).pathname.split("/")[2] +
          "/details"
        );
      },
      modifier: async (data) => {
        data = JSON.parse(data);
        data.artistLinks = data.artistLinks.filter(
          (y) => y.categories === "Producer",
        );
        data.songs.forEach(
          (y) =>
            (y.song.lengthSeconds = new Date(y.song.lengthSeconds * 1000)
              .toISOString()
              .slice(11, 19)),
        );
        data = JSON.stringify(data);
        return data;
      },
    },
    {
      name: "TOWER RECORDS MUSIC",
      extractor: "node",
      placeholder: "https://music.tower.jp/album/detail/*",
      artist: ".p-content__name",
      album: ".p-content__title",
      parent: "[data-type=tracklist] .c-grid__item",
      index: false,
      title: ".c-media__title a",
      length: false,
    },
    {
      name: "Traxsource",
      extractor: "node",
      placeholder: "https://www.traxsource.com/title/*/*",
      artist: "h1.artists",
      album: "h1.title",
      parent: ".trk-row",
      index: ".tnum",
      title: ".title a",
      length: ".duration",
    },
    {
      name: "Tubo",
      extractor: "json",
      placeholder: "https://tubo.migalmoreno.com/playlist?url=*",
      artist: "uploader-name",
      album: "name",
      parent: "related-streams",
      index: false,
      title: "name",
      length: "duration",
      transformer: async (link) => {
        return (
          "https://tubo.migalmoreno.com/api/v1/playlists/" +
          encodeURIComponent(new URL(link).searchParams.get("url"))
        );
      },
      modifier: async (data) => {
        data = JSON.parse(data);
        data["related-streams"].forEach(
          (y) =>
            (y.duration = new Date(y.duration * 1000)
              .toISOString()
              .slice(11, 19)),
        );
        data = JSON.stringify(data);
        return data;
      },
    },
    {
      name: "UtaiteDB",
      extractor: "json",
      placeholder: "https://utaitedb.net/Al/*",
      artist: "artistLinks.0.name",
      album: "name",
      parent: "songs",
      index: "trackNumber",
      title: "name",
      length: "song.lengthSeconds",
      transformer: async (link) => {
        return (
          "https://utaitedb.net/api/albums/" +
          new URL(link).pathname.split("/")[2] +
          "/details"
        );
      },
      modifier: async (data) => {
        data = JSON.parse(data);
        data.artistLinks = data.artistLinks.filter(
          (y) => y.categories === "Producer",
        );
        data.songs.forEach(
          (y) =>
            (y.song.lengthSeconds = new Date(y.song.lengthSeconds * 1000)
              .toISOString()
              .slice(11, 19)),
        );
        data = JSON.stringify(data);
        return data;
      },
    },
    {
      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 .rolebit",
      index: ".label",
      title: "[colspan='2']",
      length: ".time",
      modifier: async (data) => {
        data = new DOMParser().parseFromString(data, "text/html");
        let lists = Array.from(data.querySelectorAll("#tlnav a")).map(
          (list) => list.textContent,
        );
        if (lists.length === 1)
          return new XMLSerializer().serializeToString(data);
        document.body.insertAdjacentHTML(
          "beforeend",
          "<dialog style='top:50%;left:50%;transform:translate(-50%,-50%);background:var(--mono-f8);color:var(--text-primary);border: 1px var(--mono-d) solid;font-size:1.25em;padding:1em'>" +
            "<form method='dialog'>" +
            "<div class='submit_step_header' style='margin:0;margin-bottom:.5em'>" +
            GM.info.script.name +
            ": <span class='submit_step_header_title'>VGMdb</span></div>" +
            "<p>The selected release has multiple tracklists.<br>This usually occurs when a release (and its tracks) have multiple translations.<br>Please select the tracklist you want to import from the list below:</p>" +
            "<div style='display:flex'><select style='flex:1'>" +
            lists.map((x) => "<option>" + x + "</option>") +
            "</select><button class='btn flat_btn' style='margin-left:1em;margin-right:0'>Import</button></div>" +
            "</form>" +
            "</dialog>",
        );
        const dialog = document.getElementsByTagName("dialog")[0];
        const select = dialog.getElementsByTagName("select")[0];
        dialog.showModal();
        dialog.addEventListener("cancel", (e) => e.preventDefault());
        await new Promise((resolve) => (dialog.onclose = () => resolve()));
        const list = data.getElementById("tracklist");
        list.replaceWith(list.children[lists.indexOf(select.value)]);
        data = new XMLSerializer().serializeToString(data);
        return data;
      },
    },
    {
      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])",
    },
    {
      name: "VocaDB",
      extractor: "json",
      placeholder: "https://vocadb.net/Al/*",
      artist: "artistLinks.0.name",
      album: "name",
      parent: "songs",
      index: "trackNumber",
      title: "name",
      length: "song.lengthSeconds",
      transformer: async (link) => {
        return (
          "https://vocadb.net/api/albums/" +
          new URL(link).pathname.split("/")[2] +
          "/details"
        );
      },
      modifier: async (data) => {
        data = JSON.parse(data);
        data.artistLinks = data.artistLinks.filter(
          (y) => y.categories === "Producer",
        );
        data.songs.forEach(
          (y) =>
            (y.song.lengthSeconds = new Date(y.song.lengthSeconds * 1000)
              .toISOString()
              .slice(11, 19)),
        );
        data = JSON.stringify(data);
        return data;
      },
    },
    {
      name: "Yandex Music",
      extractor: "json",
      placeholder: "https://music.yandex.com/album/*",
      artist: "byArtist.name",
      album: "inAlbum.name",
      parent: "inAlbum.track",
      index: false,
      title: "name",
      length: "duration",
      modifier: async (data) => {
        data = data.match(
          /<script .*? type="application\/ld\+json" .*? >(.*?)<\/script>/,
        )[1];
        return data;
      },
    },
    {
      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,
    },
  ];

  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.find((x) => x.default).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);
  if ((await GM.getValue("button")) === undefined)
    await GM.setValue("button", "keep");

  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]);

  if ((await GM.getValue("button")) === "remove") parent.replaceChildren();
  else
    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'>",
    );

  parent.style.width = "500px";
  parent.insertAdjacentHTML(
    "beforeend",
    "<p style='display:flex;margin-bottom:0'><a href='https://forgejo.sny.sh/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;min-width:0'></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.find(
      (x) => x.name === this.value,
    ).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) => {
            unsafeWindow.deleteFiledUnder(Number(element.href.match(/\d+/)));
          });

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

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

        if (!globToRegex(input.placeholder).test(link)) {
          const suggestion = sites.find((x) =>
            globToRegex(x.placeholder).test(link),
          );
          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.",
            );
          }
        }

        const unsupported = [];
        for (const data in input)
          if (!input[data])
            unsupported.push(
              {
                artist: "the artist name",
                album: "the release title",
                index: "track positions",
                title: "track names",
                length: "track durations",
              }[data],
            );
        if (unsupported.length)
          printMessage(
            "warning",
            "This site does not support importing " +
              new Intl.ListFormat().format(unsupported) +
              ".",
          );
        if (!input.index)
          printMessage(
            "warning",
            "Fallback values (1, 2, 3, ...) will be used for track numbering.",
          );

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

        GM.xmlHttpRequest({
          method: "GET",
          url: link,
          headers: {
            "User-Agent": GM.info.script.name + "/" + 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://forgejo.sny.sh/sun/userscripts/issues'>Forgejo</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) => {
                  eval(
                    new DOMParser()
                      .parseFromString(response.responseText, "text/html")
                      .getElementsByClassName("result")[0]
                      .getAttribute("onClick")
                      .replace("window.parent.", ""),
                  );
                },
              });
            }
            if (await GM.getValue("release"))
              document.getElementById("title").value = album;

            unsafeWindow.goAdvanced();
            document.getElementById("track_advanced").value =
              (await GM.getValue("append"))
                ? document.getElementById("track_advanced").value + result
                : result;
            unsafeWindow.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 + " track" + (amount === 1 ? "" : "s") + ".",
            );
          },

          onerror: (response) => {
            document.getElementById("ttrym-submit").disabled = false;
            printMessage(
              "error",
              response.responseText ||
                "Error during request. Please check your URL and try again.",
            );
          },
        });
      } catch (e) {
        document.getElementById("ttrym-submit").disabled = false;
        printMessage("error", e.toString());
      }
    });

  document
    .getElementById("ttrym-settings")
    .addEventListener("click", openSettings);

  if ((await GM.getValue("button")) === "take") {
    unsafeWindow.copyTracks = () => {
      if (
        !Array.from(document.getElementById("ttrym-site").options).find(
          (option) => option.value === "Rate Your Music",
        )
      )
        return alert(
          "Rate Your Music is not in the list of enabled sites. Please enable it to continue.",
        );

      GM.xmlHttpRequest({
        method: "POST",
        url: "https://rateyourmusic.com/go/process",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
        },
        data:
          "action=AlbumInfo||" +
          document.getElementById("copy_id").value.match(/\d+/)?.[0],
        onload: async (response) => {
          if (response.status !== 200)
            return alert(
              "No or invalid shortcut specified. Please check your input. It should look like one of the following examples:\n\n[Album12345]\nAlbum12345\n12345",
            );

          let data = response.responseText;
          data = "https://rateyourmusic.com" + data.match(/href="(.*?)"/)[1];
          document.getElementById("ttrym-site").value = "Rate Your Music";
          document.getElementById("ttrym-link").value = data;
          document.getElementById("ttrym-submit").click();
        },
      });
    };
  }

  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, ".*"),
    );
  }

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

  async function openSettings() {
    document.body.style.overflow = "hidden";

    document.body.insertAdjacentHTML(
      "beforeend",
      `
      <div id="ttrym-settings-wrapper">
        <div class="submit_step_box">
          <span class="submit_step_header">${GM.info.script.name}: <span class="submit_step_header_title">Settings</span></span>
          <div class="submit_field_header_separator"></div>
          <p>
            <b class="submit_field_header">Supply additional data <code>(artist, release)</code></b><br />
            While ${GM.info.script.name}'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" />
          <label for="ttrym-artist">Artist name <span>Step 1.3 (“File under”)</span></label><br />
          <input id="ttrym-release" name="ttrym-release" type="checkbox" />
          <label for="ttrym-release">Release title <span>Step 2.1 (“Title”)</span></label>
          <div class="submit_field_header_separator"></div>
          <p>
            <b class="submit_field_header">Manage sites <code>(sitesv2)</code></b><br />
            Choose which sites to show and which ones to hide in the ${GM.info.script.name} selection box.<br />
            If your site is missing, send a request via <a href="https://forgejo.sny.sh/sun/userscripts/issues">Forgejo</a> or <a href="mailto:[email protected]">e-mail</a> or <a href="https://forgejo.sny.sh/sun/userscripts/src/branch/main/assets/tutorial.md">add it yourself</a>.
          </p>
          ${sitestmp
            .map(
              (x) => `
            <input type="checkbox" class="ttrym-checkbox" id='ttrym-site-${x.name.replace(/\s/g, "")}' name="${x.name}" />
            <label for='ttrym-site-${x.name.replace(/\s/g, "")}'>${x.name} <span>${x.placeholder}</span></label><br />
          `,
            )
            .join("")}
          <br />
          <div>
            <button id="ttrym-enable">Enable all sites</button>
            <button id="ttrym-invert">Invert selection</button>
            <button id="ttrym-disable">Disable all sites</button>
          </div>
          <div class="submit_field_header_separator"></div>
          <p>
            <b class="submit_field_header">Set default site <code>(default)</code></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"></div>
          <p>
            <b class="submit_field_header">Auto-select sites <code>(guess)</code></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" />
          <label for="ttrym-change">Guess and automatically select sites</label>
          <div class="submit_field_header_separator"></div>
          <p>
            <b class="submit_field_header">Enable new sites by default <code>(enbydef)</code></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" />
          <label for="ttrym-enbydef">Automatically enable newly supported sites</label>
          <div class="submit_field_header_separator"></div>
          <p>
            <b class="submit_field_header">Append instead of replace <code>(append)</code></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" />
          <label for="ttrym-append">Append tracks to list</label>
          <div class="submit_field_header_separator"></div>
          <p>
            <b class="submit_field_header">Add URL to sources <code>(sources)</code></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" />
          <label for="ttrym-sources">Automatically add URLs to sources</label>
          <div class="submit_field_header_separator"></div>
          <p>
            <b class="submit_field_header">“Copy Tracks” button behavior <code>(button)</code></b><br />
            Rate Your Music provides a “Copy Tracks” button, which allows you to import the tracklist of other Rate Your Music releases.<br />
            Below, you can choose what ${GM.info.script.name} should do with this button.
          </p>
          <p>
            <b>Keep button:</b> Do not alter the button's behavior in any way.<br />
            <b>Take over:</b> Fulfill requests via ${GM.info.script.name} instead of natively.<br />
            <b>Remove button:</b> Remove the button entirely.
          </p>
          <select id="ttrym-button">
            <option value="keep">Keep button</option>
            <option value="take">Take over</option>
            <option value="remove">Remove button</option>
          </select>
          <div class="submit_field_header_separator"></div>
          <p>FYI: You can also directly edit these settings in your userscript manager:</p>
          <p>
            <b><a href="https://addons.mozilla.org/firefox/addon/firemonkey/">FireMonkey</a>:</b> Options → Script & CSS → ${GM.info.script.name} → ⋮ → Storage<br />
            <b><a href="https://docs.scriptcat.org/">ScriptCat</a>:</b> ⌂ → Install Script → ${GM.info.script.name} → 工具 → 脚本储存<br />
            <b><a href="https://www.tampermonkey.net/">Tampermonkey</a>:</b> Dashboard → Installed userscripts → ${GM.info.script.name} → Edit → Storage<br />
            <b><a href="https://addons.mozilla.org/firefox/addon/userunified-script-injector/">USI</a>:</b> all Userscripts → ${GM.info.script.name} → ⋮ → GM Values show<br />
            <b><a href="https://violentmonkey.github.io/">Violentmonkey</a>:</b> Open Dashboard → Installed scripts → ${GM.info.script.name} → Edit → Values
          </p>
          <div>
            <button id="ttrym-save">Save and reload page</button>
            <button id="ttrym-discard">Close window without saving</button>
            <button id="ttrym-reset">Reset and reload page</button>
          </div>
        </div>
        <style>
          #ttrym-settings-wrapper {
            box-sizing: border-box;
            width: 100vw;
            height: 100vh;
            position: fixed;
            top: 42px;
            background: var(--background);
            padding: 50px;
            z-index: 80;
          }
          #ttrym-settings-wrapper .submit_step_box {
            padding: 25px;
            height: calc(100% - 50px);
            overflow: auto;
          }
          #ttrym-settings-wrapper .submit_step_header {
            margin: 0 !important;
          }
          #ttrym-settings-wrapper .submit_field_header_separator {
            margin-top: 15px;
            margin-bottom: 15px;
          }
          #ttrym-settings-wrapper .submit_field_header {
            display: block;
            margin-top: 1em;
            margin-bottom: -1em;
          }
          #ttrym-settings-wrapper .submit_field_header code {
            opacity: 0.5;
          }
          #ttrym-settings-wrapper input {
            margin-bottom: 0.25em;
          }
          #ttrym-settings-wrapper label {
            position: relative;
            bottom: 1px;
          }
          #ttrym-settings-wrapper label span {
            opacity: 0.5;
            font-weight: lighter;
          }
          #ttrym-settings-wrapper button:not(:first-child) {
            margin-left: 10px;
          }
          #ttrym-settings-wrapper p + p {
            margin-top: 0.5em;
          }
        </style>
      </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-button").value = await GM.getValue("button");

    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", openReset);

    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,
        );
        await GM.setValue(
          "button",
          document.getElementById("ttrym-button").value,
        );

        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 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;
  }
})();