Greasy Fork

TracklistToRYM

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

目前为 2023-12-29 提交的版本。查看 最新版本

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

// ==UserScript==
// @name               TracklistToRYM
// @name:de            TracklistToRYM
// @name:en            TracklistToRYM
// @namespace          sun/userscripts
// @version            1.42.0
// @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/new
// @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            allmusic.com
// @connect            amazon.com
// @connect            archive.org
// @connect            awa.fm
// @connect            baer.works
// @connect            bandcamp.com
// @connect            beatbump.io
// @connect            beatport.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            itch.zone
// @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            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            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            recochoku.jp
// @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            utaitedb.net
// @connect            vgmdb.net
// @connect            vinyl-digital.com
// @connect            vocadb.net
// @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            For Good Eyes Only; https://forgejo.sny.sh/sun/userscripts/src/branch/main/LICENSE.md
// ==/UserScript==

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

(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: "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: "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: "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",
      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: "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: "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: "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: "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: "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: "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: "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'>TracklistToRYM: <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: "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);

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

        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://forgejo.sny.sh/sun/userscripts/issues/new?title=" +
                    input.extractor +
                    "%20is%20not%20a%20valid%20extractor'>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) => {
                  /* 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;

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

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

  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' style='box-sizing:border-box;width:100vw;height:100vh;position:fixed;top:42px;background:var(--background);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.<br>" +
        "If your site is missing, send a request via <a href='https://forgejo.sny.sh/sun/userscripts/issues/new'>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' 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", 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,
        );

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

// @license-end