Greasy Fork is available in English.

TracklistToRYM

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

// ==UserScript==
// @name               TracklistToRYM
// @name:de            TracklistToRYM
// @name:en            TracklistToRYM
// @namespace          sun/userscripts
// @version            1.49.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
// @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            apple.com
// @connect            archive.org
// @connect            awa.fm
// @connect            azurewebsites.net
// @connect            baer.works
// @connect            bandcamp.com
// @connect            bandwagon.fm
// @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            google.com
// @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            spotify.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.getValues
// @grant              GM_getValues
// @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 () => {
  GM.registerMenuCommand("Settings", openSettings);
  GM.registerMenuCommand("Reset", openReset);

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

  const sitestmp = [
    {
      name: "7digital",
      icon: "https://css-cdn.7digital.com/static/build/images/favicons/7digital/favicon.ico",
      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",
      icon: "https://fastly-gce.allmusic.com/images/favicon/favicon.ico",
      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: "Apple Music",
      extractor: "json",
      placeholder: "https://music.apple.com/album/*/*",
      artist: "byArtist.0.name",
      album: "name",
      parent: "tracks",
      index: false,
      title: "name",
      length: "duration",
      modifier: async (data) => {
        let x = new DOMParser().parseFromString(data, "text/html");
        x = x.getElementById("schema:music-album").textContent;
        return x;
      },
    },
    {
      name: "a-tisket",
      icon: "https://atisket.pulsewidth.org.uk/resources/favicon.svg",
      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",
      icon: "https://s.awa.fm/static/favicon.ico",
      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",
      icon: "https://s4.bcbits.com/img/favicon/favicon.ico",
      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)",
      icon: "https://s4.bcbits.com/img/favicon/favicon.ico",
      extractor: "node",
      placeholder: "https://*.bandcamp.com/track/*",
      artist: "#name-section h3 a",
      album: "h2",
      parent: ".trackView",
      index: false,
      title: ".trackTitle",
      length: false,
    },
    {
      name: "Bandwagon",
      extractor: "node",
      placeholder: "https://bandwagon.fm/*",
      artist: "[aria-label*='Artist']",
      album: "h1",
      parent: ".track",
      index: ".align-right span",
      title: "[class='width-100%']",
      length: ".align-right:nth-last-child(2)",
    },
    {
      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",
      icon: "https://www.beatport.com/images/favicon-48x48.png",
      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: "Beatport Classic",
      icon: "https://www.beatport.com/images/favicon-48x48.png",
      extractor: "node",
      placeholder: "http://classic.beatport.com/release/*/*",
      artist: "h2 + div a",
      album: "h2",
      parent: ".track-grid-content",
      index: ".playColumn .artWrapper",
      title: ".titleColumn .txt-larger > span:not(.txt-grey)",
      length:
        "td:not(.playColumn):not(.titleColumn):not(.cartColumn) span:not(.genreList)",
    },
    {
      name: "blamscamp",
      icon: "https://suricrasia.online/favicon.ico",
      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",
      icon: "https://d1rgjmn2wmqeif.cloudfront.net/sf/s/1-1.png",
      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) => {
        let x = new DOMParser().parseFromString(data, "text/html");
        for (const row of Array.from(
          x.querySelectorAll(".tracks-table-divided + tr:not([class])"),
        ).reverse()) {
          row.previousElementSibling.getElementsByTagName("a")[0].textContent +=
            ` / ${row.getElementsByTagName("a")[0].textContent}`;
          row.previousElementSibling.lastElementChild.textContent =
            row.lastElementChild.textContent;
        }
        x = new XMLSerializer().serializeToString(x);
        return x;
      },
    },
    {
      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",
      icon: "https://simonrepp.com/faircamp/favicon.svg",
      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) => {
        let x = new DOMParser().parseFromString(data, "text/html");
        for (const y of data.querySelectorAll(".track_time")) y.remove();
        x = new XMLSerializer().serializeToString(x);
        return x;
      },
    },
    {
      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",
      icon: "https://freemusicarchive.org/img/favicon.svg",
      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) => {
        let x = JSON.parse(data);
        for (const y of x.results)
          y.uploads[0].duration = new Date(y.uploads[0].duration * 1000)
            .toISOString()
            .slice(11, 19);
        x = JSON.stringify(x);
        return x;
      },
    },
    {
      name: "genie",
      icon: "https://www.genie.co.kr/resources/favicon.ico",
      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) => {
        let x = new DOMParser().parseFromString(data, "text/html");
        x.querySelector(".icon").remove();
        x = new XMLSerializer().serializeToString(x);
        return x;
      },
    },
    {
      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: "Google Play",
      extractor: "node",
      placeholder: "https://play.google.com/store/music/album/*",
      artist: ".H51Agc a",
      album: "h1 span",
      parent: "[data-album-is-available]",
      index: "[data-update-url-on-play] div",
      title: "[itemprop='name']",
      length: "[aria-label]",
    },
    {
      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) => {
        let x = JSON.parse(data);
        for (const y of x.tracks)
          y.duration = new Date(y.duration * 1000).toISOString().slice(11, 19);
        x = JSON.stringify(x);
        return x;
      },
    },
    {
      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) => {
        let x = new DOMParser().parseFromString(data, "text/html");
        for (const y of x.querySelectorAll(".album__deta-artist a img"))
          y.remove();
        for (const y of x.querySelectorAll(".songlist__num")) y.remove();
        x = new XMLSerializer().serializeToString(x);
        return x;
      },
    },
    {
      name: "Kingdom Hearts Insider",
      icon: "https://downloads.khinsider.com/images/favicon.ico",
      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",
      icon: "https://linemusic-webapp.landpress.line.me/favicon.ico",
      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) => {
        let x = new DOMParser().parseFromString(data, "text/html");
        x.querySelector(".song_name strong").remove();
        x = new XMLSerializer().serializeToString(x);
        return x;
      },
    },
    {
      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) => {
        let x = JSON.parse(data);
        for (const y of x.result.tracks)
          y.metadata.duration = new Date(y.metadata.duration * 1000)
            .toISOString()
            .slice(11, 19);
        x = JSON.stringify(x);
        return x;
      },
    },
    {
      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) => {
        let x = JSON.parse(data);
        x.media = x.media.flatMap((y) => y.tracks);
        x = JSON.stringify(x);
        return x;
      },
    },
    {
      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",
      icon: "https://www.musixmatch.com/favicon.png",
      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",
      icon: "https://simg.mysound.jp/assets/image/common/favicon.ico",
      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",
      icon: "https://www.napster.com/wp-content/themes/napsterpitch/assets/favicon/favicon.ico",
      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) => {
        let x = new DOMParser().parseFromString(data, "text/html");
        for (const y of x.querySelectorAll("meta[itemprop=duration]"))
          y.remove();
        x = new XMLSerializer().serializeToString(x);
        return x;
      },
    },
    {
      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) => {
        let x = new DOMParser().parseFromString(data, "text/html");
        for (const y of x.querySelectorAll(".album-artist a i")) y.remove();
        x = new XMLSerializer().serializeToString(x);
        return x;
      },
    },
    {
      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) => {
        const x = data.replace(/(?<="trackLength":\d+)(?=,)/g, "000");
        return x;
      },
    },
    {
      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) => {
        let x = new DOMParser().parseFromString(data, "text/html");
        for (const link of x.querySelectorAll(
          ".tracklist_title .artist, .tracklist_title .work",
        )) {
          const title = document.createTextNode(
            link.hasAttribute("title")
              ? `[${link.getAttribute("title").slice(1, -1)},${link.innerText}]`
              : link.innerText,
          );
          link.parentNode.replaceChild(title, link);
        }
        for (const highlight of x.querySelectorAll(".tracklist_title b")) {
          const title = document.createTextNode(
            `[b]${highlight.innerText}[/b]`,
          );
          highlight.parentNode.replaceChild(title, highlight);
        }
        x = new XMLSerializer().serializeToString(x);
        return x;
      },
      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",
      icon: "https://static.resonate.is/pwa_assets/favicon.ico",
      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) => {
        let x = data.match(/window\.initialState=JSON\.parse\('(.*?)'\)/)[1];
        x = JSON.parse(x);
        for (const y of x.release.data.items)
          y.track.duration = new Date(y.track.duration * 1000)
            .toISOString()
            .slice(11, 19);
        x = JSON.stringify(x);
        return x;
      },
    },
    {
      name: "SecondHandSongs",
      icon: "https://secondhandsongs.com/art/favicon.png",
      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) => {
        let x = new DOMParser().parseFromString(data, "text/html");
        for (const track of x.querySelectorAll("#tracklist tr")) {
          const title = document.createElement("div");
          title.append(
            document.createTextNode(track.firstChild.lastChild.textContent),
          );
          track.firstChild.appendChild(title);
        }
        x = new XMLSerializer().serializeToString(x);
        return x;
      },
    },
    {
      name: "Spotify",
      extractor: "json",
      placeholder: "https://open.spotify.com/album/*",
      artist: "artists.0.name",
      album: "name",
      parent: "tracks.items",
      index: "track_number",
      title: "name",
      length: "duration_ms",
      modifier: async (data) => {
        let x = new DOMParser().parseFromString(data, "text/html");
        x = await GM.xmlHttpRequest({
          method: "GET",
          url: `https://api.spotify.com/v1/albums/${
            new URL(
              x
                .querySelector("meta[property='og:url']")
                .getAttribute("content"),
            ).pathname.split("/")[2]
          }`,
          headers: {
            Authorization: `Bearer ${
              JSON.parse(x.getElementById("session").textContent).accessToken
            }`,
          },
          onload: async (response) => {
            return JSON.stringify(response.responseText);
          },
        });
        x = x.responseText;
        return x;
      },
    },
    {
      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) => {
        let x = new DOMParser().parseFromString(data, "text/html");
        x.body.insertAdjacentHTML(
          "beforeend",
          `<p id='ttrym-album'>${x.querySelector("h1").lastChild.textContent.slice(2)}</p>`,
        );
        for (const track of x.querySelectorAll(".tracks tr a"))
          track.outerHTML = track.textContent;
        for (const track of x.querySelectorAll(".tracks tr audio"))
          track.parentElement.parentElement.remove();
        x = new XMLSerializer().serializeToString(x);
        return x;
      },
    },
    {
      name: "TouhouDB",
      icon: "https://static.touhoudb.com/img/favicon.ico",
      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) => {
        let x = JSON.parse(data);
        x.artistLinks = x.artistLinks.filter(
          (y) => y.categories === "Producer",
        );
        for (const y of x.songs)
          y.song.lengthSeconds = new Date(y.song.lengthSeconds * 1000)
            .toISOString()
            .slice(11, 19);
        x = JSON.stringify(x);
        return x;
      },
    },
    {
      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",
      icon: "https://geo-static.traxsource.com/img/fav_icon.png",
      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",
      icon: "https://tubo.migalmoreno.com/icons/tubo.svg",
      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) => {
        let x = JSON.parse(data);
        for (const y of x["related-streams"])
          y.duration = new Date(y.duration * 1000).toISOString().slice(11, 19);
        x = JSON.stringify(x);
        return x;
      },
    },
    {
      name: "UtaiteDB",
      icon: "https://static.utaitedb.net/img/favicon.ico",
      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) => {
        let x = JSON.parse(data);
        x.artistLinks = x.artistLinks.filter(
          (y) => y.categories === "Producer",
        );
        for (const y of x.songs)
          y.song.lengthSeconds = new Date(y.song.lengthSeconds * 1000)
            .toISOString()
            .slice(11, 19);
        x = JSON.stringify(x);
        return x;
      },
    },
    {
      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) => {
        let x = new DOMParser().parseFromString(data, "text/html");
        const lists = Array.from(x.querySelectorAll("#tlnav a")).map(
          (list) => list.textContent,
        );
        if (lists.length === 1) return new XMLSerializer().serializeToString(x);
        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 = x.getElementById("tracklist");
        list.replaceWith(list.children[lists.indexOf(select.value)]);
        x = new XMLSerializer().serializeToString(x);
        return x;
      },
    },
    {
      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",
      icon: "https://vocadb.net/Content/favicon.ico",
      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) => {
        let x = JSON.parse(data);
        x.artistLinks = x.artistLinks.filter(
          (y) => y.categories === "Producer",
        );
        for (const y of x.songs)
          y.song.lengthSeconds = new Date(y.song.lengthSeconds * 1000)
            .toISOString()
            .slice(11, 19);
        x = JSON.stringify(x);
        return x;
      },
    },
    {
      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) => {
        const x = data.match(
          /<script .*? type="application\/ld\+json" .*? >(.*?)<\/script>/,
        )[1];
        return x;
      },
    },
    {
      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");
  if ((await GM.getValue("favicon")) === undefined)
    await GM.setValue("favicon", "");

  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", (e) => {
    if (e.target?.id === "ttrym-dismiss") clearMessages();
  });

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

      if (await GM.getValue("artist"))
        for (const element of document.querySelectorAll(
          ".filed_under_delete a",
        ))
          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(
              "info",
              `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":
                for (const element of reduceJson(data, input.parent)) {
                  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";
                for (const element of new DOMParser()
                  .parseFromString(data, mime)
                  .querySelectorAll(input.parent)) {
                  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":
                for (let i of data.replace(/\n/g, "").match(input.parent)) {
                  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 error on Forgejo or via e-mail.`,
                );
            }

            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=filedunderperformer&label=performer&searchterm=${encodeURIComponent(artist)}`,
                onload: async (response) => {
                  eval(
                    `unsafeWindow.${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;

            const isAdvanced = document
              .getElementById("advancedhelp")
              .checkVisibility();
            if (!isAdvanced) unsafeWindow.goAdvanced();
            document.getElementById("track_advanced").value =
              (await GM.getValue("append"))
                ? document.getElementById("track_advanced").value + result
                : result;
            if (!isAdvanced) 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());
        printMessage("error", e.stack.trim().replaceAll("\n", ", "));
        printMessage(
          "error",
          "Please report this error on Forgejo or via e-mail.",
        );
      }
    });

  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() {
    const levels = ["info", "success", "warning", "error"];
    for (const x of document.querySelectorAll(
      levels.map((x) => `#ttrym-${x}`).join(","),
    ))
      x.remove();
    msgPosted = false;
    if (document.getElementById("ttrym-dismiss"))
      document.getElementById("ttrym-dismiss").remove();
  }

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

  function escapeHTML(input) {
    return input
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;")
      .replace(/'/g, "&#039;");
  }

  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?")) {
      for (const setting of await GM.listValues())
        await GM.deleteValue(setting);
      location.href = location.href;
    }
  }

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

    const icon = {
      allesedv: "https://f1.allesedv.com/%s",
      duckduckgo: "https://icons.duckduckgo.com/ip3/%s.ico",
      favicone: "https://favicone.com/%s",
      faviconim: "https://favicon.im/%s",
      faviconkit: "https://api.faviconkit.com/%s",
      google: "https://www.google.com/s2/favicons?domain=%s",
      hatena: "https://favicon.hatena.ne.jp/?url=https://%s",
      icoapi: "https://favicons.fuzqing.workers.dev/api/getFavicon?url=%s",
      iconhorse: "https://icon.horse/icon/%s",
      splitbee: "https://favicon.splitbee.io/?url=%s",
      twenty: "https://favicon.twenty.com/%s",
      unavatar: "https://unavatar.io/%s",
      xinac: "https://api.xinac.net/icon/?url=%s",
      yandex: "https://favicon.yandex.net/favicon/%s",
    }[await GM.getValue("favicon")];

    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"><i class="fas fa-info"></i>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"><i class="fas fa-globe"></i>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" target="_blank">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" target="_blank">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, "")}'>
              <img src="${icon ? icon.replace("%s", new URL(x.placeholder.replaceAll("*.", "")).hostname) : x.icon || `${new URL(x.placeholder.replaceAll("*.", "")).origin}/favicon.ico`}" onerror="this.style.visibility = 'hidden'" />
              ${x.name}
              <span>${x.placeholder}</span>
            </label><br />
          `,
            )
            .join("")}
          <br />
          <div>
            <button id="ttrym-enable" class="btn blue_btn btn_small">Enable all sites</button>
            <button id="ttrym-invert" class="btn flat_btn btn_small">Invert selection</button>
            <button id="ttrym-disable" class="btn flat_btn btn_small">Disable all sites</button>
          </div>
          <div class="submit_field_header_separator"></div>
          <p>
            <b class="submit_field_header"><i class="fas fa-bookmark"></i>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"><i class="fas fa-search"></i>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"><i class="fas fa-check"></i>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"><i class="fas fa-plus"></i>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"><i class="fas fa-link"></i>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"><i class="fas fa-copy"></i>“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>
            <b class="submit_field_header"><i class="fas fa-image"></i>Set favicon provider <code>(favicon)</code></b><br />
            If the icons above are slow to load or you have privacy concerns, you can choose another favicon provider below.
          </p>
          <select id="ttrym-favicon">
            <option value="">icon or favicon.ico</option>
            <option value="allesedv">AllesEDV</option>
            <option value="duckduckgo">DuckDuckGo</option>
            <option value="faviconkit">Favicon Kit</option>
            <option value="faviconim">Favicon.im</option>
            <option value="favicone">Favicone</option>
            <option value="google">Google</option>
            <option value="hatena">Hatena</option>
            <option value="icoapi">ICO API</option>
            <option value="iconhorse">Icon Horse</option>
            <option value="splitbee">Splitbee</option>
            <option value="twenty">Twenty</option>
            <option value="unavatar">unavatar</option>
            <option value="yandex">Yandex</option>
            <option value="xinac">新逸网络</option>
          </select>
          <div class="submit_field_header_separator"></div>
          <p>You can also directly edit these settings in your userscript manager:</p>
          <p>
            <b><a href="https://addons.mozilla.org/firefox/addon/firemonkey/" target="_blank"><img src="https://addons.mozilla.org/user-media/addon_icons/1019/1019336-64.png"> FireMonkey</a>:</b> Options → Script & CSS → ${GM.info.script.name} → ⋮ → Storage<br />
            <b><a href="https://docs.scriptcat.org/" target="_blank"><img src="https://docs.scriptcat.org/img/logo.png"> ScriptCat</a>:</b> ⌂ → Install Script → ${GM.info.script.name} → 工具 → 脚本储存<br />
            <b><a href="https://www.tampermonkey.net/" target="_blank"><img src="https://www.tampermonkey.net/images/icon48.png"> 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/" target="_blank"><img src="https://addons.mozilla.org/user-media/addon_icons/597/597912-64.png"> USI</a>:</b> all Userscripts → ${GM.info.script.name} → ⋮ → GM Values show<br />
            <b><a href="https://violentmonkey.github.io/" target="_blank"><img src="https://violentmonkey.github.io/_astro/vm.C4h557K-.png"> Violentmonkey</a>:</b> Open Dashboard → Installed scripts → ${GM.info.script.name} → Edit → Values
          </p>
          <p>Finally, you can view all values below. This does not include changes that haven't been saved yet.</p>
          <textarea rows="5" readonly>${JSON.stringify(await GM.getValues(await GM.listValues()), null, "\t")}</textarea>
          <div>
            <button id="ttrym-save" class="btn blue_btn btn_small">Save and reload page</button>
            <button id="ttrym-discard" class="btn flat_btn btn_small">Close window without saving</button>
            <button id="ttrym-reset" class="btn flat_btn btn_small">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 i {
            margin-right: 0.5em;
          }
          #ttrym-settings-wrapper .submit_field_header code {
            opacity: 0.5;
          }
          #ttrym-settings-wrapper input {
            margin-bottom: 0.25em;
          }
          #ttrym-settings-wrapper input[type="checkbox"],
          #ttrym-settings-wrapper input[type="checkbox"] + label {
            cursor: pointer;
            margin-right: 2px;
          }
          #ttrym-settings-wrapper img {
            width: 16px;
            height: 16px;
            object-fit: contain;
            position: relative;
            top: 4px;
            margin: 0 2px;
          }
          #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 textarea {
            margin-bottom: 1em;
            font-family: monospace;
            font-size: 1em;
            resize: vertical;
          }
          #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");
    for (const element of Array.from(
      document.getElementsByClassName("ttrym-checkbox"),
    ))
      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-favicon").value =
      await GM.getValue("favicon");

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

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

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

    document.getElementById("ttrym-reset").addEventListener("click", openReset);

    document
      .getElementById("ttrym-save")
      .addEventListener("click", async () => {
        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,
        );
        await GM.setValue(
          "favicon",
          document.getElementById("ttrym-favicon").value,
        );

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

        location.href = location.href;
      });

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

  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(input) {
    let output = input;
    if (!output) return "";
    if (typeof output !== "string") output = output.toString();
    if (output === "?:??" || output === "-") return "";
    if (output.match(/PT(\d+H)?\d+M\d+S/))
      output = output.replace(/[PTS]/g, "").replace(/[HM]/g, ":");
    if (Number(output))
      output = new Date(Number(output)).toISOString().split(/[TZ]/)[1];
    let matches = output.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 output;
  }

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

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

  function printMessage(level, message) {
    const capitalizedLevel = level.charAt(0).toUpperCase() + level.slice(1);
    parent.insertAdjacentHTML(
      "beforeend",
      `<p id='ttrym-${level}' style='font-size:small;line-height:1.5;text-wrap:nowrap;overflow:hidden;text-overflow:ellipsis;${msgPosted ? "" : "margin-top:.5em;"}margin-bottom:0' title='${escapeHTML(`${capitalizedLevel}: ${message}`)}'><span style='margin-right:.5em;padding:0 .25em;border-radius:.25em;background:var(--${level === "info" ? "text-primary" : `alert-${level}-background`});color:var(--background);font-size:smaller'>${capitalizedLevel}</span>${escapeHTML(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(input, path) {
    let output = input;
    if (typeof output !== "object") output = JSON.parse(output);
    return path.split(".").reduce((acc, cur) => acc[cur], output);
  }

  function sitesV1ToV2(input) {
    let tmp = input;
    if (!Array.isArray(tmp)) tmp = tmp.split(",");
    const output = {};
    for (const x of sitestmp) output[x.name] = tmp.includes(x.name);
    return output;
  }
})();