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'></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'></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, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } 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'></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; } })();