Artlist DL

Allows you to download artlist.io Music & SFX

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        Artlist DL
// @namespace   http://tampermonkey.net/
// @description Allows you to download artlist.io Music & SFX
// @author      Mia @ github.com/xNasuni
// @match       *://*.artlist.io/*
// @grant       GM_xmlhttpRequest
// @connect     cms-public-artifacts.artlist.io
// @connect     cms-artifacts.artlist.io
// @require     https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
// @version     2.9
// @run-at	    document-start
// @supportURL  https://github.com/xNasuni/artlist-downloader/issues
// ==/UserScript==

const LoadedMusicLists = []
const LoadedSfxLists = []
const LoadedSfxsList = []
const LoadedSongsList = []
const LoadedSstemsLists = []
const ModifiedMusicButtonColor = '#82ff59'
const ModifiedSfxButtonColor = '#ff90bf'
const ErrorButtonColor = '#ff3333'
const UNKNOWN_DATATYPE = '_unknown'
const NEXTRSC_DATATYPE = '_rsc'
const SINGLE_SOUND_EFFECT_DATATYPE = '_ssfx'
const SINGLE_SONG_DATATYPE = '_ssong'
const MUSIC_ALBUM_PAGETYPE = '_amusic'
const SONGS_PAGETYPE = '_songs'
const MUSIC_PAGETYPE = '_music'
const SFXS_PAGETYPE = '_sfxs'
const SFXP_PAGETYPE = '_sfxp'
const SFX_PAGETYPE = '_sfx'
const SONG_STEMS_PAGETYPE = '_sstem'
const oldXMLHttpRequestOpen = unsafeWindow.XMLHttpRequest.prototype.open
const oldFetch = unsafeWindow.fetch

var AudioTable
var TBody
var LastChangeObserver
var ActionContainer
var SongPage
var LastInterval = -1
var RequestsInterval = -1
var RSCInterval = -1
var DontPoll = false
var SingleSoundEffectData = 'none'
var SingleSongData = 'none'

async function ShowSaveFilePickerForURL(url, filename) {
    if (!url) {
        throw new Error('no url passed in')
    }

    let blobDataFromURL = null

    if (typeof GM_xmlhttpRequest !== 'undefined') {
        blobDataFromURL = await new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                responseType: 'blob',
                onload: res => {
                    if (res.response) resolve(res.response)
                    else reject(new Error('empty response'))
                },
                onerror: err => reject(err)
            })
        })
    } else {
        console.warn('using native fetch, GM_xmlhttpRequest not found')
        blobDataFromURL = await fetch(url).then(r => r.blob())
    }

    try {
        if (unsafeWindow.showSaveFilePicker) {
            const BlobData = new Blob([blobDataFromURL], {
                type: 'audio/aac'
            })
            const Handle = await unsafeWindow.showSaveFilePicker({
                suggestedName: filename,
                types: [
                    {
                        description: 'AAC File (Compressed MP3)',
                        accept: {
                            'audio/aac': ['.aac']
                        }
                    }
                ]
            })
            const Writable = await Handle.createWritable()
            await Writable.write(BlobData)
            await Writable.close()
        } else {
            const blobURL = URL.createObjectURL(blobDataFromURL)
            const a = document.createElement('a')
            a.href = blobURL
            a.download = filename
            document.body.appendChild(a)
            a.click()
            document.body.removeChild(a)
            URL.revokeObjectURL(blobURL)
        }
    } catch (e) {
        console.error('Error saving file:', e)
    }
}

async function ShowSaveFilePickerForURLsZipped(files, filename) {
    try {
        const zip = new JSZip()

        for (const file of files) {
            const blobDataFromURL = await fetch(file.URL).then(r => r.blob())
            zip.file(file.Filename, blobDataFromURL)
        }

        const zipBlob = await zip.generateAsync({
            type: 'blob'
        })
        if (unsafeWindow.showSaveFilePicker) {
            const handle = await unsafeWindow.showSaveFilePicker({
                suggestedName: filename,
                types: [
                    {
                        description: 'ZIP File',
                        accept: {
                            'application/zip': ['.zip']
                        }
                    }
                ]
            })

            const writable = await handle.createWritable()
            await writable.write(zipBlob)
            await writable.close()
        } else {
            const blobURL = URL.createObjectURL(zipBlob)
            const a = document.createElement('a')
            a.href = blobURL
            a.download = filename
            document.body.appendChild(a)
            a.click()
            document.body.removeChild(a)
            URL.revokeObjectURL(blobURL)
        }
    } catch (e) {
        console.warn('Error saving zip:', e)
    }
}

function Until(testFunc) {
    // https://stackoverflow.com/a/52657929
    const poll = resolve => {
        if (DontPoll) {
            resolve()
        }
        if (testFunc()) {
            resolve()
        } else setTimeout(_ => poll(resolve), 100)
    }
    return new Promise(poll)
}

function GetPagetype() {
    const PathSplit = unsafeWindow.location.pathname.split('/')
    if (
        unsafeWindow.location.host === 'artlist.io' &&
        PathSplit[1] === 'royalty-free-music' &&
        (PathSplit[2] === 'song' || PathSplit[2] === 'artist')
    ) {
        return SONGS_PAGETYPE
    }
    if (
        unsafeWindow.location.host === 'artlist.io' &&
        PathSplit[1] === 'royalty-free-music' &&
        PathSplit[2] === 'album'
    ) {
        return MUSIC_ALBUM_PAGETYPE
    }
    if (
        unsafeWindow.location.host === 'artlist.io' &&
        PathSplit[1] === 'royalty-free-music'
    ) {
        return MUSIC_PAGETYPE
    }
    if (
        unsafeWindow.location.host === 'artlist.io' &&
        PathSplit[1] === 'sfx' &&
        PathSplit[2] === 'track'
    ) {
        return SFXS_PAGETYPE
    }
    if (
        unsafeWindow.location.host === 'artlist.io' &&
        PathSplit[1] === 'sfx' &&
        PathSplit[2] === 'pack'
    ) {
        return SFXP_PAGETYPE
    }
    if (
        unsafeWindow.location.host == 'artlist.io' &&
        (PathSplit[1] === 'sfx' ||
            (PathSplit[1] === 'sfx' &&
                (PathSplit[2] === 'search' || PathSplit[2] === 'pack')))
    ) {
        return SFX_PAGETYPE
    }
    return UNKNOWN_DATATYPE
}

function GetDatatype(Data) {
    var Datatype = UNKNOWN_DATATYPE

    try {
        if (
            Data.data.sfxList != undefined &&
            Data.data.sfxList.songs != undefined
        ) {
            Datatype = SFX_PAGETYPE
        }
    } catch (e) {}
    try {
        if (
            Data.data.songList != undefined &&
            Data.data.songList.songs != undefined
        ) {
            Datatype = MUSIC_PAGETYPE
        }
    } catch (e) {}
    try {
        if (
            Data.data.sfxs != undefined &&
            Data.data.sfxs.length === 1 &&
            Data.data.sfxs[0].similarList != undefined
        ) {
            Datatype = SFXS_PAGETYPE
        }
    } catch (e) {}
    try {
        if (Data.data.pack != undefined && Data.data.pack.songs != undefined) {
            Datatype = SFXP_PAGETYPE
        }
    } catch (e) {}
    try {
        if (
            Data.data.sfxs != undefined &&
            Data.data.sfxs.length === 1 &&
            Data.data.sfxs[0].songName != undefined
        ) {
            Datatype = SINGLE_SOUND_EFFECT_DATATYPE
        }
    } catch (e) {}
    try {
        if (
            Data.data.songs != undefined &&
            Data.data.songs.length === 1 &&
            Data.data.songs[0].songName != undefined
        ) {
            Datatype = SINGLE_SONG_DATATYPE
        }
    } catch (e) {}
    try {
        if (
            Data.data.songs != undefined &&
            Data.data.songs.length === 1 &&
            Data.data.songs[0].similarSongs != undefined
        ) {
            Datatype = SONGS_PAGETYPE
        }
    } catch (e) {}

    try {
        if (
            Data.data.songs != undefined &&
            Data.data.songs.length === 1 &&
            Data.data.songs[0].stems != undefined
        ) {
            Datatype = SONG_STEMS_PAGETYPE
        }
    } catch (e) {}

    return Datatype
}

function MatchURL(Url) {
    const Pagetype = GetPagetype()
    let URLObject

    try {
        URLObject = new URL(Url)
    } catch (e) {
        return UNKNOWN_DATATYPE
    }

    const hasRsc = URLObject.searchParams.has('_rsc')
    if (hasRsc) {
        return NEXTRSC_DATATYPE
    }

    if (
        Pagetype !== UNKNOWN_DATATYPE &&
        URLObject.host === 'search-api.artlist.io' &&
        (URLObject.pathname === '/v1/graphql' ||
            URLObject.pathname === '/v2/graphql')
    ) {
        return Pagetype
    }

    return UNKNOWN_DATATYPE
}

async function GetSfxInfo(Id) {
    const Query = `query Sfxs($ids: [Int!]!) {
  sfxs(ids: $ids) {
    songId
    songName
    artistId
    artistName
    albumId
    albumName
    assetTypeId
    duration
    sitePlayableFilePath
  }
}
`
    const Variables = {
        ids: [Id]
    }

    const Payload = {
        query: Query,
        variables: Variables
    }

    const Response = await fetch('https://search-api.artlist.io/v1/graphql', {
        method: 'POST',
        headers: {
            'content-type': 'application/json'
        },
        body: JSON.stringify(Payload)
    })
    const JSONData = await Response.json()

    var Data

    try {
        Data = JSONData.data.sfxs[0]
    } catch (e) {}

    if (Data === undefined) {
        return false
    }

    return Data
}

async function GetSongInfo(Id) {
    const Query = `query Songs($ids: [String!]!) {
  songs(ids: $ids) {
    songId
    songName
    artistId
    artistName
    albumId
    albumName
    assetTypeId
    duration
    sitePlayableFilePath
  }
}
`
    const Variables = {
        ids: [Id.toString()]
    }

    const Payload = {
        query: Query,
        variables: Variables
    }

    const Response = await fetch('https://search-api.artlist.io/v1/graphql', {
        method: 'POST',
        headers: {
            'content-type': 'application/json'
        },
        body: JSON.stringify(Payload)
    })
    const JSONData = await Response.json()

    var Data

    try {
        Data = JSONData.data.songs[0]
    } catch (e) {}

    if (Data === undefined) {
        return false
    }

    return Data
}

async function LoadAssetInfo(Id) {
    const Pagetype = GetPagetype()
    if (Pagetype === SFXS_PAGETYPE) {
        SingleSoundEffectData = await GetSfxInfo(Id)
        return true
    }
    if (Pagetype === SONGS_PAGETYPE) {
        SingleSongData = await GetSongInfo(Id)
        return true
    }
    return false
}

function GetAudioTable() {
    return unsafeWindow.document.querySelector(
        'table.w-full.table-fixed[data-testid=AudioTable]'
    )
}

function GetSongPage() {
    return (
        unsafeWindow.document.querySelector('div[data-testid=SongPage]') ||
        unsafeWindow.document.querySelector('div#song-page-react')
    )
}

function GetBanner(SongPage) {
    return SongPage.querySelector('div')
}

function GetActionRow(SongPage) {
    if (window.innerWidth >= 1024) {
        // page layout changes depending on viewport size
        return SongPage.querySelector('div.hidden')
    }
    return SongPage.querySelector('div.block.py-4.px-6')
}

function GetTBody() {
    return (
        unsafeWindow.document.querySelector(
            'div.w-full[data-testid=ComposableAudioList]'
        ) ||
        unsafeWindow.document.querySelector(
            'table[data-testid=AudioTable]>tbody'
        )
    )
}

function GetTBodyEdgeCase() {
    const TBody =
        unsafeWindow.document.querySelector('div[data-testid=Wrapper]') ||
        unsafeWindow.document.querySelector('div[data-testid=ArtistContent]') ||
        unsafeWindow.document.querySelector('div#song-page-tab-panel-1') ||
        unsafeWindow.document.querySelector(
            'div[data-testid=ComposableAudioList]'
        )
    if (TBody === null) {
        return
    }
    if (TBody.parentNode.classList.contains('hidden')) {
        return
    }
    if (
        TBody.querySelector(
            '[data-testid=AudioRow], [data-testid=AlbumRow], [data-testid=SongVariantsWrapper]'
        ) == null
    ) {
        return
    }

    return TBody
}

function GetAudioRowData(AudioRow, Pagetype) {
    var Data = {
        AudioTitle: 'none',
        RawTitle: 'None',
        Artists: [],
        Button: null,
        Pagetype: Pagetype
    }
    var AlbumsAndArtists = AudioRow.querySelector(
        'td[data-testid=AlbumsAndArtists]'
    )
    var DataAndActions = AudioRow.querySelector(
        'td[data-testid=DataAndActions]'
    )

    if (Pagetype === SONGS_PAGETYPE) {
        AlbumsAndArtists = AudioRow.querySelector(
            'div[data-testid=AudioDetails]'
        )
        DataAndActions = AudioRow.querySelector(
            'div[data-testid=AnimatedToggleContainer]'
        )
    }

    if (Pagetype == MUSIC_PAGETYPE || Pagetype == MUSIC_ALBUM_PAGETYPE) {
        AlbumsAndArtists = AudioRow.querySelector(
            'div.flex[data-testid=AudioDetails]'
        )
        DataAndActions = AudioRow.querySelector(
            'div[data-testid=AnimatedToggleContainer]'
        )
    }

    if (
        DataAndActions == null &&
        AudioRow.querySelector('span[data-testid=stems-player-stem-name]') &&
        AudioRow.parentNode.getAttribute('data-testid') != 'ComposableAudioList'
    ) {
        // most likely a song stem, so default to audio row
        const StemContainer = AudioRow.parentNode.parentNode
        const Title = StemContainer.querySelector(
            'span[data-testid=stems-player-song-name]'
        )
        const Artists = StemContainer.querySelectorAll(
            'span[data-testid=stems-player-song-artist]'
        )

        Data.Pagetype = SONG_STEMS_PAGETYPE
        Data.AudioTitle = `${AudioRow.querySelector('span[data-testid=stems-player-stem-name]').innerText} of ${Title.innerText}`
        Data.RawTitle = AudioRow.querySelector(
            'span[data-testid=stems-player-stem-name]'
        ).innerText

        for (const Artist of Artists) {
            Data.Artists.push(Artist.innerText)
        }

        DataAndActions = AudioRow
    }

    if (!DataAndActions) {
        console.warn('DataAndActions not found in', Pagetype, AudioRow)
    }

    var Button =
        DataAndActions.querySelector("button[aria-label='download']") ||
        DataAndActions.querySelector("button[aria-label='Download']")

    if (Button) {
        Data.Button = Button
    }

    if (AlbumsAndArtists == null || DataAndActions == null) {
        return Data
    }

    const AudioTitle = AlbumsAndArtists.querySelector(
        'a.truncate[data-testid=Link]'
    )
    const Artists = AlbumsAndArtists.querySelectorAll(
        'a.truncate.text-gray-200[data-testid=Link]'
    )

    if (AudioTitle) {
        Data.AudioTitle = AudioTitle.childNodes[0].textContent.trim()
        Data.RawTitle = Data.AudioTitle
    }
    if (Artists) {
        for (const Artist of Artists) {
            Data.Artists.push(Artist.textContent.replaceAll(',', '').trim())
        }
    }

    if (
        Data.AudioTitle === 'none' &&
        Data.Artists.length === 0 &&
        Data.Button == null
    ) {
        return false
    }
    if (
        (Data.AudioTitle === 'none' || Data.Artists.length === 0) &&
        Data.Button !== null
    ) {
        Data.Button.style.color = ErrorButtonColor
    }

    return Data
}

function GetBannerData(SongPage, Pagetype) {
    const Data = {
        AudioTitle: 'none',
        RawTitle: 'none',
        Artists: [],
        Button: null,
        Pagetype: Pagetype
    }

    const Banner = GetBanner(SongPage)
    const ActionRow = GetActionRow(SongPage)

    if (Banner === null || ActionRow === null) {
        return false
    }

    const Titles = Banner.querySelectorAll('h1')
    const Artists = Banner.querySelectorAll('a[data-testid=Link]')
    const Button = ActionRow.querySelector(
        "button[aria-label='direct download']"
    )

    if (Titles.length != 1 || Artists.length <= 0 || Button == null) {
        return Data
    }

    Data.AudioTitle = Titles[0].textContent
    Data.RawTitle = Data.AudioTitle
    Data.Button = Button

    for (const Artist of Artists) {
        Data.Artists.push(Artist.textContent.replaceAll(',', '').trim())
    }

    if (
        Data.AudioTitle === 'none' &&
        Data.Artists.length == 0 &&
        Data.Button == null
    ) {
        return false
    }
    if (
        (Data.AudioTitle === 'none' || Data.Artists.length == 0) &&
        Data.Button != null
    ) {
        Data.Button.style.color = ErrorButtonColor
        Data.Button.style.borderColor = ErrorButtonColor
    }

    return Data
}

function MakeFilename(AssetData, Pagetype) {
    const NoAlbum = AssetData.albumId === undefined
    return `${Pagetype === MUSIC_PAGETYPE || Pagetype === SONGS_PAGETYPE || Pagetype === SONG_STEMS_PAGETYPE ? (Pagetype == SONG_STEMS_PAGETYPE ? 'Music Stem' : 'Music') : 'Sfx'} ${AssetData.artistName} - ${AssetData.songName} ${AssetData.songName != AssetData.albumName ? `on ${AssetData.albumName} ` : ''}(${AssetData.artistId}.${NoAlbum ? '' : AssetData.albumId + '.'}${AssetData.songId})`
}

function WriteAudio(RowData, AudioData) {
    const Pagetype = RowData.Pagetype
    const ChosenColor =
        Pagetype === MUSIC_ALBUM_PAGETYPE ||
        Pagetype === MUSIC_PAGETYPE ||
        Pagetype === SONGS_PAGETYPE ||
        Pagetype == SONG_STEMS_PAGETYPE
            ? ModifiedMusicButtonColor
            : ModifiedSfxButtonColor
    const FileName = MakeFilename(AudioData, Pagetype)
    RowData.Button.setAttribute('artlist-dl-processed', 'true')
    RowData.Button.style.color = ChosenColor
    RowData.Button.addEventListener(
        'click',
        function (event) {
            event.stopImmediatePropagation() // prevent premium popup upsell
            ShowSaveFilePickerForURL(
                AudioData.sitePlayableFilePath || AudioData.playableFileUrl,
                FileName + '.aac'
            )
        },
        true
    )
}

function WriteBanner(BannerData, AudioData) {
    const Pagetype = BannerData.Pagetype
    const ChosenColor =
        Pagetype === MUSIC_PAGETYPE ||
        Pagetype === SONGS_PAGETYPE ||
        Pagetype == SONG_STEMS_PAGETYPE
            ? ModifiedMusicButtonColor
            : ModifiedSfxButtonColor
    const FileName = MakeFilename(AudioData, Pagetype)
    BannerData.Button.setAttribute('artlist-dl-processed', 'true')
    BannerData.Button.style.color = ChosenColor
    BannerData.Button.style.borderColor = ChosenColor
    BannerData.Button.addEventListener(
        'click',
        function (event) {
            event.stopImmediatePropagation() // prevent premium popup upsell
            ShowSaveFilePickerForURL(
                AudioData.sitePlayableFilePath || AudioData.playableFileUrl,
                FileName + '.aac'
            )
        },
        true
    )
}

var changeBackTimeout = -1
function WriteDownloadAllStems(StemsContainer, DownloadButton) {
    const Pagetype = GetPagetype()
    const ChosenColor = ModifiedMusicButtonColor
    DownloadButton.setAttribute('artlist-dl-processed', 'true')
    DownloadButton.style.backgroundColor = ChosenColor
    DownloadButton.style.borderColor = ChosenColor
    DownloadButton.style.color = 'black'
    DownloadButton.addEventListener('click', async function (event) {
        event.stopImmediatePropagation() // prevent dropdown
        clearInterval(changeBackTimeout)
        changeBackTimeout = setTimeout(() => {
            DownloadButton.querySelector('span.whitespace-nowrap').innerText =
                'Download All Stems'
            DownloadButton.disabled = false
        }, 10000)
        DownloadButton.querySelector('span.whitespace-nowrap').innerText =
            'Please Wait...'
        DownloadButton.disabled = true
        const dataList = []
        var firstData = null
        for (const StemDescendant of StemsContainer.querySelectorAll(
            'span[data-testid=stems-player-stem-name]'
        )) {
            const Stem = StemDescendant.parentNode
            try {
                const AudioData = GetAudioDataFromRowData(
                    GetAudioRowData(Stem, SONG_STEMS_PAGETYPE)
                )
                if (!firstData) {
                    firstData = AudioData
                }

                dataList.push({
                    URL:
                        AudioData.sitePlayableFilePath ||
                        AudioData.playableFileUrl,
                    Filename:
                        MakeFilename(AudioData, SONG_STEMS_PAGETYPE) + '.aac'
                })
            } catch (e) {
                console.warn('Exception while handling stem rows:', e)
            }
        }

        const fileName = `Music Stems ${firstData.artistName} - ${firstData._songName}${firstData._songName == firstData._albumName ? '' : ` on ${firstData._albumName}`}.zip`
        await ShowSaveFilePickerForURLsZipped(dataList, fileName)
        clearInterval(changeBackTimeout)
        DownloadButton.querySelector('span.whitespace-nowrap').innerText =
            'Download All Stems'
        DownloadButton.disabled = false
    })
}

function MatchAudioToRow(AudioData, RowData, SkipArtistCheck) {
    return (
        (AudioData.songName || AudioData.name).trim() ===
            RowData.RawTitle.trim() &&
        (SkipArtistCheck ||
            RowData.Artists.indexOf(AudioData.artistName.trim()) != -1)
    )
}

function OnRowAdded(AudioRow, RowData, AudioData) {
    AudioRow.setAttribute('artlist-dl-state', 'modified')
    if (AudioData !== undefined) {
        WriteAudio(RowData, AudioData)
    } else {
        console.warn('No data given for row', RowData)
        if (RowData.Button !== null) {
            RowData.Button.style.color = ErrorButtonColor
        }
    }
}

function cloneref(object) {
    return {
        ...object
    }
}

function transformIndexedObject2Array(object) {
    return Object.values(object)
}

function GetAudioDataFromRowData(RowData) {
    if (RowData.Pagetype === SFX_PAGETYPE) {
        if (LoadedSfxLists.length <= 0) {
            console.warn('No loaded sound effects to loop through.')
            return
        }
        for (const SfxList of LoadedSfxLists) {
            for (const SfxData of SfxList) {
                if (MatchAudioToRow(SfxData, RowData)) {
                    return SfxData
                }
            }
        }
    }
    if (
        RowData.Pagetype === MUSIC_PAGETYPE ||
        RowData.Pagetype === MUSIC_ALBUM_PAGETYPE
    ) {
        if (LoadedMusicLists.length <= 0) {
            console.warn('No loaded songs to loop through.')
            return
        }
        for (const MusicList of LoadedMusicLists) {
            for (const SongData of MusicList) {
                if (MatchAudioToRow(SongData, RowData)) {
                    return SongData
                }
            }
        }
    }
    if (
        RowData.Pagetype === SFXS_PAGETYPE ||
        RowData.Pagetype === SFXP_PAGETYPE ||
        RowData.Pagetype == SONGS_PAGETYPE
    ) {
        if (LoadedSfxsList.length <= 0) {
            console.warn('No loaded sfxs to loop through.')
            return
        }
        for (const SfxsList of LoadedSfxsList) {
            for (const SfxData of SfxsList) {
                if (MatchAudioToRow(SfxData, RowData)) {
                    return SfxData
                }
            }
        }
    }
    if (RowData.Pagetype === SONGS_PAGETYPE) {
        if (LoadedSongsList.length <= 0) {
            console.warn('No loaded similar songs to loop through.')
            return
        }
        for (const SongsList of LoadedSongsList) {
            for (const SongData of SongsList) {
                if (MatchAudioToRow(SongData, RowData)) {
                    return SongData
                }
            }
        }
    }
    if (RowData.Pagetype === SONG_STEMS_PAGETYPE) {
        if (LoadedSstemsLists.length <= 0) {
            console.warn('No loaded song stems to loop through.')
            return
        }
        for (const StemData of LoadedSstemsLists) {
            for (const Stem of StemData.stems) {
                if (MatchAudioToRow(Stem, RowData, true)) {
                    const clonedData = cloneref(StemData)
                    clonedData.sitePlayableFilePath = Stem.playableFileUrl
                    clonedData._songName = clonedData.songName
                    clonedData._albumName = clonedData.albumName
                    clonedData.songName = `${Stem.name} of ${StemData.songName}`
                    clonedData.albumName = `${Stem.name} of ${StemData.albumName}`
                    return clonedData
                }
            }
        }
    }

    console.warn("Couldn't handle data:", RowData)
}

function HandleJSONData(Data) {
    const Datatype = GetDatatype(Data)
    if (Datatype === SONGS_PAGETYPE) {
        LoadedSongsList.push(Data.data.songs[0].similarSongs)
        return
    }
    if (Datatype === MUSIC_PAGETYPE) {
        LoadedMusicLists.push(Data.data.songList.songs)
        return
    }
    if (Datatype === SFXS_PAGETYPE) {
        LoadedSfxsList.push(Data.data.sfxs[0].similarList)
        return
    }
    if (Datatype == SFXP_PAGETYPE) {
        LoadedSfxsList.push(Data.data.pack.songs)
        return
    }
    if (Datatype === SFX_PAGETYPE) {
        LoadedSfxLists.push(Data.data.sfxList.songs)
        return
    }
    if (
        Datatype === SONG_STEMS_PAGETYPE &&
        Data.data.songs &&
        Data.data.songs[0] &&
        Data.data.songs[0].stems
    ) {
        LoadedSstemsLists.push(Data.data.songs[0])
        return
    }

    console.warn('Not processed:', Datatype, Data)
}

function HandleRSCEntry(DataStr) {
    var Data = null

    const [_, right] = DataStr.split(/:(.+)/)

    Data = JSON.parse(right)

    var found = null

    for (const k in Data) {
        const v = Data[k]

        if (
            v &&
            typeof v === 'object' &&
            ('artistSongs' in v ||
                'songData' in v ||
                'pack' in v ||
                'album' in v ||
                v?.data?.sfxs)
        ) {
            found = v
        }
    }

    if (!found) {
        throw new Error('no audio data found')
    }

    try {
        const datatype = GetDatatype(found)
        if (datatype != UNKNOWN_DATATYPE) {
            HandleJSONData(found)
            return
        }

        if (!!found.album) {
            HandleJSONData({
                data: {
                    songList: {
                        songs: found.album.songs
                    }
                }
            })
        }

        if (!!found.pack) {
            HandleJSONData({
                data: {
                    pack: found.pack
                }
            })
        }

        if (!!found.songData) {
            HandleJSONData({
                data: {
                    sfxs: [
                        {
                            similarList: [found.songData]
                        }
                    ]
                }
            })
        }
        if (!!found.artistSongs) {
            for (var song of found.artistSongs) {
                song.songId = song.audioDetails.audioId
                song.songName = song.audioDetails.audioName
                song.artistName = song.audioDetails.artists[0].name
                song.artistId = song.audioDetails.artists[0].id
                song.sitePlayableFilePath = song.audioUrl
            }
            HandleJSONData({
                data: {
                    sfxList: {
                        page: 1,
                        songs: transformIndexedObject2Array(found.artistSongs),
                        suggestion: []
                    }
                }
            })
        }
    } catch (e) {
        console.warn(found)
        console.error(e)
        alert(
            `Artlist Downloader: please report this in https://github.com/xNasuni/artlist-downloader\n#HNRSC>${String(e)}`
        )
        throw e
    }
}

function HookNextRSC() {
    var oldNextFPush = null
    var handler = function () {
        try {
            const Container = arguments[0]
            if (Container[0] != 1) {
                throw new Error('invalid format')
            }

            const DataStr = Container[1]
            HandleRSCEntry(DataStr)
        } catch (e) {}

        return oldNextFPush.apply(this, arguments)
    }

    if (RSCInterval != -1) {
        clearInterval(RSCInterval)
    }
    RSCInterval = setInterval(() => {
        if (!unsafeWindow.__next_f || !unsafeWindow.__next_f.push) {
            return
        }
        if (unsafeWindow.__next_f.push == handler) {
            return
        }

        oldNextFPush = unsafeWindow.__next_f.push
        unsafeWindow.__next_f.push = handler
    })
}

function HandleListRSC(responseText) {
    const Entries = responseText.split('\n').filter(v => !v.trim() == '')

    for (var Entry of Entries) {
        try {
            HandleRSCEntry(Entry)
        } catch (e) {}
    }
}

function ApplyXHR(XHR, Datatype) {
    if (Datatype !== UNKNOWN_DATATYPE) {
        XHR.addEventListener('readystatechange', function () {
            if (XHR.readyState == XMLHttpRequest.DONE) {
                if (Datatype == NEXTRSC_DATATYPE) {
                    HandleListRSC(XHR.responseText)
                } else {
                    var JSONData
                    try {
                        JSONData = JSON.parse(XHR.responseText)
                    } catch (e) {
                        console.warn(
                            `Couldn't parse as json: ${XHR.responseText}`
                        )
                        return
                    }
                    HandleJSONData(JSONData)
                }
            }
        })
    }
}

function HookRequests() {
    var handler = function () {
        const Method = arguments[0]
        const URL = arguments[1]

        const UrlDatatype = MatchURL(URL)
        if (UrlDatatype != UNKNOWN_DATATYPE) {
            ApplyXHR(this, UrlDatatype)
        }

        return oldXMLHttpRequestOpen.apply(this, arguments)
    }
    var handler_fetch = async function () {
        const URL = arguments[0]

        const data = await oldFetch.apply(this, arguments)

        const UrlDatatype = MatchURL(URL)
        if (UrlDatatype != UNKNOWN_DATATYPE) {
            const text = await data.clone().text()
            HandleListRSC(text)
        }

        return data
    }

    if (RequestsInterval != -1) {
        clearInterval(RequestsInterval)
    }
    RequestsInterval = setInterval(() => {
        unsafeWindow.XMLHttpRequest.prototype.open = handler
        unsafeWindow.fetch = handler_fetch
    })
}

// this makes the user-script support the [←] Back and [→] Right navigations
// aswell as switching pages because artlist doesn't navigate, but instead
// changes their HTML dynamically so that the end-user does not have to
// reload the entire page.

// by polling the changes in an albeit bad way, we can detect when this
// occurs, and as far as i know there's no other better way to do it.
// please make an issue on github and educate me if there is.

async function Initialize() {
    DontPoll = false

    const Pagetype = GetPagetype()

    if (Pagetype === SONGS_PAGETYPE || Pagetype === SFXS_PAGETYPE) {
        // const Id = location.pathname.split('/')[4]
        // const NumId = new Number(Id)
        // if (NumId.toString() !== 'NaN') {
        //     LoadAssetInfo(NumId)
        // }
        console.log('searching for banner...')

        SongPage = GetSongPage()
        await Until(() => {
            const Data = GetBannerData(SongPage, Pagetype)
            return Data != false && Data.Button != null
        })
        const RowData = GetBannerData(SongPage, Pagetype)
        await Until(() => {
            return GetAudioDataFromRowData(RowData) != null
        })
        const AudioData = GetAudioDataFromRowData(RowData)
        if (RowData.AudioTitle && RowData.Artists.length >= 1) {
            WriteBanner(RowData, AudioData)
        }
    }

    console.log('searching for table...')
    if (Pagetype === SONGS_PAGETYPE || Pagetype == MUSIC_ALBUM_PAGETYPE) {
        await Until(() => {
            return GetTBodyEdgeCase() != undefined
        })
        TBody = GetTBodyEdgeCase()
    } else {
        await Until(() => {
            return GetTBody() != undefined && document.contains(GetTBody())
        })
        TBody = GetTBody()
        console.log('tbody', TBody)
    }

    function OnAudioRowAdded(AudioRow) {
        if (AudioRow.getAttribute('artlist-dl-processed') === 'true') {
            return
        }
        if (AudioRow.classList.contains('hidden')) {
            return
        }
        const RowData = GetAudioRowData(AudioRow, GetPagetype())
        const AudioData = GetAudioDataFromRowData(RowData)

        OnRowAdded(AudioRow, RowData, AudioData)
    }

    console.log('found')
    LastInterval = setInterval(() => {
        if (!TBody) {
            return
        }

        for (const AudioRow of TBody.querySelectorAll(
            '[data-testid=AudioRow], [data-testid=AlbumRow], [data-testid=SongVariantsWrapper]'
        )) {
            if (!AudioRow.hasAttribute('artlist-dl-processed')) {
                try {
                    OnAudioRowAdded(AudioRow)
                    AudioRow.setAttribute('artlist-dl-processed', 'true')
                } catch (e) {
                    console.warn(e)
                }
            }
        }

        for (const modal of document.querySelectorAll('.ReactModal__Content')) {
            for (const AllStemsDownload of modal.querySelectorAll(
                'button[data-testid=renderButton]'
            )) {
                if (
                    !AllStemsDownload.hasAttribute('artlist-dl-processed') &&
                    AllStemsDownload.parentNode.getAttribute('data-testid') ==
                        'download-all-stems-dropdown'
                ) {
                    WriteDownloadAllStems(modal, AllStemsDownload)
                }
            }
            for (const StemContainer of modal.querySelectorAll(
                'span[data-testid=stems-player-stem-name]'
            )) {
                const Stem = StemContainer.parentNode
                if (
                    !Stem.hasAttribute('artlist-dl-processed') &&
                    document.contains(Stem)
                ) {
                    try {
                        OnAudioRowAdded(Stem)
                        Stem.setAttribute('artlist-dl-processed', 'true')
                    } catch (e) {} // will fail on false positives so just hide errors
                }
            }
        }
    }, 500)
}

HookNextRSC()
HookRequests()
document.addEventListener('DOMContentLoaded', () => {
    Initialize()
    setInterval(() => {
        if (TBody != null && !document.contains(TBody)) {
            console.log('Re-updating...')
            DontPoll = true
            TBody = null
            AudioTable = null
            SongPage = null
            Initialize()
        }
    }, 1000)
})