├── .github └── ISSUE_TEMPLATE │ └── bug-report.md ├── legacy-main.js ├── LICENSE ├── README.md └── artlist-downloader.user.js /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: BUG REPORT 3 | about: USE THIS TO REPORT A BUG IF SCRIPT ISN'T WORKING! 4 | title: '' 5 | labels: bug 6 | assignees: xNasuni 7 | 8 | --- 9 | 10 | Please provide the following information to make it easier for the developer to understand: 11 | - What userscript manager are you using? 12 | - What browser and browser version are you using? 13 | - What is the full url that is causing the issue? (the entire url where the issue occurs) 14 | 15 | > [!CAUTION] 16 | > Please try these fixes before making an issue. 17 | - Go to your userscript manager's extension settings in your browser, and try checking the box "Allow access to file URLs". 18 | 19 | If this doesn't work, go ahead and make the issue. 20 | -------------------------------------------------------------------------------- /legacy-main.js: -------------------------------------------------------------------------------- 1 | // this code no longer works but feel free to look at it 2 | 3 | (async function() { 4 | function DownloadURI(URI) { 5 | var Element = document.createElement('a') 6 | Element.href = URI 7 | Element.download = URI 8 | document.body.appendChild(Element) 9 | Element.click() 10 | Element.remove() 11 | } 12 | 13 | function GetCurrentAudioURI() { 14 | if (document.getElementsByTagName('audio')[0].src != '') { 15 | return document.getElementsByTagName('audio')[0].src 16 | } else { 17 | return document.getElementsByTagName('audio')[1].src 18 | } 19 | } 20 | 21 | var CurrentAudioURI = GetCurrentAudioURI() 22 | 23 | DownloadURI(CurrentAudioURI) 24 | })() 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, xNasuni 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # artlist-downloader **`v2.8`** 2 | 3 | > [!CAUTION] 4 | > The audio(s) that you download using this are **NOT** allowed for commerical use or for use in YouTube Videos and alike **[without the proper licensing](https://artlist.io/page/pricing/music-and-sfx)**. 5 | > If you dont want to be held responsible for pirating music/sfx, buy an artlist.io license at [their plans and pricing page](https://artlist.io/page/pricing/max). 6 | > You should also read their [terms of service](https://artlist.io/help-center/privacy-terms/terms-of-use/) and understand how you are violating them. 7 | > 8 | > I'm personally only using this to listen to the music outside of the website, locally. 9 | 10 | > [!WARNING] 11 | > By continuing to use this, you accept the risks and acknowledge that you have been warned. 12 | 13 | > [!IMPORTANT] 14 | > This user-script enables you to download **music** and **sound effects** from the artlist without making an account, and without any watermarks. The userscript overrides the download button in the UI to download the audio file linked to that music or sound effect instead of popping up a menu for you to make an account and get a license. 15 | 16 | # [`INSTALL USERSCRIPT`](https://github.com/xNasuni/artlist-downloader/raw/main/artlist-downloader.user.js) 17 | 18 | > [!NOTE] 19 | > In order to install the userscript you need to have a Userscript Manager, these are some I recommend: 20 | > * **[TamperMonkey](https://chromewebstore.google.com/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo) — Personal Choice** 21 | > * **[ViolentMonkey](https://chromewebstore.google.com/detail/violentmonkey/jinjaccalgkegednnccohejagnlnfdag)** 22 | > * **[OrangeMonkey](https://chromewebstore.google.com/detail/orangemonkey/ekmeppjgajofkpiofbebgcbohbmfldaf)** 23 | 24 | ## Extra Information 25 | 26 | > intended for [artlist.io](https://artlist.io)
27 | > please star the repo if you appreciate my work and thank you~! 28 | -------------------------------------------------------------------------------- /artlist-downloader.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Artlist DL 3 | // @namespace http://tampermonkey.net/ 4 | // @description Allows you to download artlist.io Music & SFX 5 | // @author Mia @ github.com/xNasuni 6 | // @match *://*.artlist.io/* 7 | // @grant GM_xmlhttpRequest 8 | // @connect cms-public-artifacts.artlist.io 9 | // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js 10 | // @version 2.8 11 | // @run-at document-start 12 | // @updateURL https://github.com/xNasuni/artlist-downloader/raw/main/artlist-downloader.user.js 13 | // @downloadURL https://github.com/xNasuni/artlist-downloader/raw/main/artlist-downloader.user.js 14 | // @supportURL https://github.com/xNasuni/artlist-downloader/issues 15 | // ==/UserScript== 16 | 17 | const LoadedMusicLists = [] 18 | const LoadedSfxLists = [] 19 | const LoadedSfxsList = [] 20 | const LoadedSongsList = [] 21 | const LoadedSstemsLists = [] 22 | const ModifiedMusicButtonColor = "#82ff59" 23 | const ModifiedSfxButtonColor = "#ff90bf" 24 | const ErrorButtonColor = "#ff3333" 25 | const UNKNOWN_DATATYPE = "_unknown" 26 | const SINGLE_SOUND_EFFECT_DATATYPE = "_ssfx" 27 | const SINGLE_SONG_DATATYPE = "_ssong" 28 | const MUSIC_ALBUM_PAGETYPE = "_amusic" 29 | const SONGS_PAGETYPE = "_songs" 30 | const MUSIC_PAGETYPE = "_music" 31 | const SFXS_PAGETYPE = "_sfxs" 32 | const SFX_PAGETYPE = "_sfx" 33 | const SONG_STEMS_PAGETYPE = "_sstem" 34 | const oldXMLHttpRequestOpen = window.XMLHttpRequest.prototype.open 35 | 36 | var AudioTable 37 | var TBody 38 | var LastChangeObserver 39 | var ActionContainer 40 | var SongPage 41 | var LastInterval = -1 42 | var RequestsInterval = -1 43 | var DontPoll = false 44 | var SingleSoundEffectData = "none" 45 | var SingleSongData = "none" 46 | 47 | async function ShowSaveFilePickerForURL(url, filename) { 48 | let blobDataFromURL = null; 49 | 50 | if (typeof GM_xmlhttpRequest !== "undefined") { 51 | blobDataFromURL = await new Promise((resolve, reject) => { 52 | GM_xmlhttpRequest({ 53 | method: "GET", 54 | url: url, 55 | responseType: "blob", 56 | onload: (res) => { 57 | if (res.response) resolve(res.response); 58 | else reject(new Error("empty response")); 59 | }, 60 | onerror: (err) => reject(err), 61 | }); 62 | }); 63 | } else { 64 | console.warn("using native fetch, GM_xmlhttpRequest not found"); 65 | blobDataFromURL = await fetch(url).then((r) => r.blob()); 66 | } 67 | 68 | try { 69 | if (unsafeWindow.showSaveFilePicker) { 70 | const BlobData = new Blob([blobDataFromURL], { 71 | type: "audio/aac" 72 | }); 73 | const Handle = await unsafeWindow.showSaveFilePicker({ 74 | suggestedName: filename, 75 | types: [{ 76 | description: "AAC File (Compressed MP3)", 77 | accept: { 78 | "audio/aac": [".aac"] 79 | }, 80 | },], 81 | }); 82 | const Writable = await Handle.createWritable(); 83 | await Writable.write(BlobData); 84 | await Writable.close(); 85 | } else { 86 | const blobURL = URL.createObjectURL(blobDataFromURL); 87 | const a = document.createElement("a"); 88 | a.href = blobURL; 89 | a.download = filename; 90 | document.body.appendChild(a); 91 | a.click(); 92 | document.body.removeChild(a); 93 | URL.revokeObjectURL(blobURL); 94 | } 95 | } catch (e) { 96 | console.error("Error saving file:", e); 97 | } 98 | } 99 | 100 | async function ShowSaveFilePickerForURLsZipped(files, filename) { 101 | try { 102 | const zip = new JSZip(); 103 | 104 | for (const file of files) { 105 | const blobDataFromURL = await fetch(file.URL).then((r) => r.blob()); 106 | zip.file(file.Filename, blobDataFromURL); 107 | } 108 | 109 | const zipBlob = await zip.generateAsync({ 110 | type: "blob" 111 | }); 112 | if (window.showSaveFilePicker) { 113 | const handle = await window.showSaveFilePicker({ 114 | suggestedName: filename, 115 | types: [{ 116 | description: "ZIP File", 117 | accept: { 118 | "application/zip": [".zip"] 119 | }, 120 | },], 121 | }); 122 | 123 | const writable = await handle.createWritable(); 124 | await writable.write(zipBlob); 125 | await writable.close(); 126 | } else { 127 | const blobURL = URL.createObjectURL(zipBlob); 128 | const a = document.createElement("a"); 129 | a.href = blobURL; 130 | a.download = filename; 131 | document.body.appendChild(a); 132 | a.click(); 133 | document.body.removeChild(a); 134 | URL.revokeObjectURL(blobURL); 135 | } 136 | } catch (e) { 137 | console.warn("Error saving zip:", e) 138 | } 139 | } 140 | 141 | function Until(testFunc) { 142 | // https://stackoverflow.com/a/52657929 143 | const poll = (resolve) => { 144 | if (DontPoll) { 145 | resolve() 146 | } 147 | if (testFunc()) { 148 | resolve(); 149 | } else setTimeout((_) => poll(resolve), 100); 150 | }; 151 | return new Promise(poll) 152 | } 153 | 154 | function GetPagetype() { 155 | const PathSplit = window.location.pathname.split('/') 156 | if (window.location.host === "artlist.io" && (PathSplit[1] === "royalty-free-music" && (PathSplit[2] === "song" || PathSplit[2] === "artist"))) { 157 | return SONGS_PAGETYPE 158 | } 159 | if (window.location.host === "artlist.io" && (PathSplit[1] === "royalty-free-music" && PathSplit[2] === "album")) { 160 | return MUSIC_ALBUM_PAGETYPE 161 | } 162 | if (window.location.host === "artlist.io" && (PathSplit[1] === "royalty-free-music")) { 163 | return MUSIC_PAGETYPE 164 | } 165 | if (window.location.host === "artlist.io" && (PathSplit[1] === "sfx" && PathSplit[2] === "track")) { 166 | return SFXS_PAGETYPE 167 | } 168 | if (window.location.host == "artlist.io" && (PathSplit[1] === "sfx" || (PathSplit[1] === "sfx" && (PathSplit[2] === "search" || PathSplit[2] === "pack")))) { 169 | return SFX_PAGETYPE 170 | } 171 | return UNKNOWN_DATATYPE 172 | } 173 | 174 | function GetDatatype(Data) { 175 | var Datatype = UNKNOWN_DATATYPE 176 | 177 | try { 178 | if (Data.data.sfxList != undefined && Data.data.sfxList.songs != undefined) { 179 | Datatype = SFX_PAGETYPE 180 | } 181 | } catch (e) { } 182 | try { 183 | if (Data.data.songList != undefined && Data.data.songList.songs != undefined) { 184 | Datatype = MUSIC_PAGETYPE 185 | } 186 | } catch (e) { } 187 | try { 188 | if (Data.data.sfxs != undefined && Data.data.sfxs.length === 1 && Data.data.sfxs[0].similarList != undefined) { 189 | Datatype = SFXS_PAGETYPE 190 | } 191 | } catch (e) { } 192 | try { 193 | if (Data.data.sfxs != undefined && Data.data.sfxs.length === 1 && Data.data.sfxs[0].songName != undefined) { 194 | Datatype = SINGLE_SOUND_EFFECT_DATATYPE 195 | } 196 | } catch (e) { } 197 | try { 198 | if (Data.data.songs != undefined && Data.data.songs.length === 1 && Data.data.songs[0].songName != undefined) { 199 | Datatype = SINGLE_SONG_DATATYPE 200 | } 201 | } catch (e) { } 202 | try { 203 | if (Data.data.songs != undefined && Data.data.songs.length === 1 && Data.data.songs[0].similarSongs != undefined) { 204 | Datatype = SONGS_PAGETYPE 205 | } 206 | } catch (e) { } 207 | 208 | try { 209 | if (Data.data.songs != undefined && Data.data.songs.length === 1 && Data.data.songs[0].stems != undefined) { 210 | Datatype = SONG_STEMS_PAGETYPE 211 | } 212 | } catch (e) { } 213 | 214 | return Datatype 215 | } 216 | 217 | function MatchURL(Url) { 218 | const Pagetype = GetPagetype() 219 | var URLObject 220 | try { 221 | URLObject = new URL(Url) 222 | } catch (e) { 223 | return false 224 | } 225 | if ((Pagetype === MUSIC_PAGETYPE || Pagetype === SFX_PAGETYPE || Pagetype === SONGS_PAGETYPE || Pagetype === SFXS_PAGETYPE || Pagetype == SONG_STEMS_PAGETYPE) && URLObject.host === "search-api.artlist.io" && (URLObject.pathname == "/v1/graphql" || URLObject.pathname == "/v2/graphql")) { 226 | return true 227 | } 228 | return false 229 | } 230 | 231 | async function GetSfxInfo(Id) { 232 | const Query = `query Sfxs($ids: [Int!]!) { 233 | sfxs(ids: $ids) { 234 | songId 235 | songName 236 | artistId 237 | artistName 238 | albumId 239 | albumName 240 | assetTypeId 241 | duration 242 | sitePlayableFilePath 243 | } 244 | } 245 | ` 246 | const Variables = { 247 | ids: [Id] 248 | } 249 | 250 | const Payload = { 251 | query: Query, 252 | variables: Variables 253 | } 254 | 255 | const Response = await fetch("https://search-api.artlist.io/v1/graphql", { 256 | method: "POST", 257 | headers: { 258 | "content-type": "application/json" 259 | }, 260 | body: JSON.stringify(Payload) 261 | }) 262 | const JSONData = await Response.json() 263 | 264 | var Data 265 | 266 | try { 267 | Data = JSONData.data.sfxs[0] 268 | } catch (e) { } 269 | 270 | if (Data === undefined) { 271 | return false 272 | } 273 | 274 | return Data 275 | } 276 | 277 | async function GetSongInfo(Id) { 278 | const Query = `query Songs($ids: [String!]!) { 279 | songs(ids: $ids) { 280 | songId 281 | songName 282 | artistId 283 | artistName 284 | albumId 285 | albumName 286 | assetTypeId 287 | duration 288 | sitePlayableFilePath 289 | } 290 | } 291 | ` 292 | const Variables = { 293 | ids: [Id.toString()] 294 | } 295 | 296 | const Payload = { 297 | query: Query, 298 | variables: Variables 299 | } 300 | 301 | const Response = await fetch("https://search-api.artlist.io/v1/graphql", { 302 | method: "POST", 303 | headers: { 304 | "content-type": "application/json" 305 | }, 306 | body: JSON.stringify(Payload) 307 | }) 308 | const JSONData = await Response.json() 309 | 310 | var Data 311 | 312 | try { 313 | Data = JSONData.data.songs[0] 314 | } catch (e) { } 315 | 316 | if (Data === undefined) { 317 | return false 318 | } 319 | 320 | return Data 321 | } 322 | 323 | async function LoadAssetInfo(Id) { 324 | const Pagetype = GetPagetype() 325 | if (Pagetype === SFXS_PAGETYPE) { 326 | SingleSoundEffectData = await GetSfxInfo(Id) 327 | return true 328 | } 329 | if (Pagetype === SONGS_PAGETYPE) { 330 | SingleSongData = await GetSongInfo(Id) 331 | return true 332 | } 333 | return false 334 | } 335 | 336 | function GetAudioTable() { 337 | return window.document.querySelector("table.w-full.table-fixed[data-testid=AudioTable]") 338 | } 339 | 340 | function GetSongPage() { 341 | return window.document.querySelector("div[data-testid=SongPage]") 342 | } 343 | 344 | function GetBanner(SongPage) { 345 | return SongPage.querySelector("div.relative.min-h-95.w-full") 346 | } 347 | 348 | function GetActionRow(SongPage) { 349 | if (window.innerWidth >= 1024) { // page layout changes depending on viewport size 350 | return SongPage.querySelector("div.hidden") 351 | } 352 | return SongPage.querySelector("div.block.py-4.px-6") 353 | } 354 | 355 | function GetTBody() { 356 | return window.document.querySelector("div.w-full[data-testid=ComposableAudioList]") || window.document.querySelector("table[data-testid=AudioTable]>tbody") 357 | } 358 | 359 | function GetTBodyEdgeCase() { 360 | const TBody = window.document.querySelector("div[data-testid=Wrapper]") || window.document.querySelector("div[data-testid=ArtistContent]") 361 | if (TBody === null) { 362 | return 363 | } 364 | if (TBody.parentNode.classList.contains("hidden")) { 365 | return 366 | } 367 | return TBody 368 | } 369 | 370 | function GetAudioRowData(AudioRow, Pagetype) { 371 | var Data = { 372 | AudioTitle: "none", 373 | RawTitle: "None", 374 | Artists: [], 375 | Button: null, 376 | Pagetype: Pagetype 377 | } 378 | var AlbumsAndArtists = AudioRow.querySelector("td[data-testid=AlbumsAndArtists]") 379 | var DataAndActions = AudioRow.querySelector("td[data-testid=DataAndActions]") 380 | 381 | if (Pagetype === SONGS_PAGETYPE) { 382 | AlbumsAndArtists = AudioRow.querySelector("div[data-testid=AudioDetails]") 383 | DataAndActions = AudioRow.querySelector("div[data-testid=AnimatedToggleContainer]") 384 | } 385 | 386 | if (Pagetype == MUSIC_PAGETYPE) { 387 | AlbumsAndArtists = AudioRow.querySelector("div.flex[data-testid=AudioDetails]") 388 | DataAndActions = AudioRow.querySelector("div[data-testid=AnimatedToggleContainer]") 389 | } 390 | 391 | 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 392 | const StemContainer = AudioRow.parentNode.parentNode 393 | const Title = StemContainer.querySelector("span[data-testid=stems-player-song-name]") 394 | const Artists = StemContainer.querySelectorAll("span[data-testid=stems-player-song-artist]") 395 | 396 | Data.Pagetype = SONG_STEMS_PAGETYPE 397 | Data.AudioTitle = `${AudioRow.querySelector("span[data-testid=stems-player-stem-name]").innerText} of ${Title.innerText}` 398 | Data.RawTitle = AudioRow.querySelector("span[data-testid=stems-player-stem-name]").innerText 399 | 400 | for (const Artist of Artists) { 401 | Data.Artists.push(Artist.innerText) 402 | } 403 | 404 | DataAndActions = AudioRow 405 | } 406 | 407 | if (!DataAndActions) { 408 | console.warn("DataAndActions not found in", Pagetype, AudioRow) 409 | } 410 | 411 | var Button = DataAndActions.querySelector("button[aria-label='download']") || DataAndActions.querySelector("button[aria-label='Download']") 412 | 413 | if (Button) { 414 | Data.Button = Button 415 | } 416 | 417 | if (AlbumsAndArtists == null || DataAndActions == null) { 418 | return Data 419 | } 420 | 421 | const AudioTitle = AlbumsAndArtists.querySelector("a.truncate[data-testid=Link]") 422 | const Artists = AlbumsAndArtists.querySelectorAll("a.truncate.text-gray-200[data-testid=Link]") 423 | 424 | if (AudioTitle) { 425 | Data.AudioTitle = AudioTitle.childNodes[0].textContent.trim() 426 | Data.RawTitle = Data.AudioTitle 427 | } 428 | if (Artists) { 429 | for (const Artist of Artists) { 430 | Data.Artists.push(Artist.textContent.replaceAll(",", "").trim()) 431 | } 432 | } 433 | 434 | if (Data.AudioTitle === "none" && Data.Artists.length === 0 && Data.Button == null) { 435 | return false 436 | } 437 | if ((Data.AudioTitle === "none" || Data.Artists.length === 0) && Data.Button !== null) { 438 | Data.Button.style.color = ErrorButtonColor 439 | } 440 | 441 | return Data 442 | } 443 | 444 | function GetBannerData(SongPage, Pagetype) { 445 | const Data = { 446 | AudioTitle: "none", 447 | RawTitle: "none", 448 | Artists: [], 449 | Button: null, 450 | Pagetype: Pagetype 451 | } 452 | 453 | const Banner = GetBanner(SongPage) 454 | const ActionRow = GetActionRow(SongPage) 455 | 456 | if (Banner === null || ActionRow === null) { 457 | return false 458 | } 459 | 460 | const Title = Banner.querySelector("h1[data-testid=Heading]") 461 | const Artists = Banner.querySelectorAll("a[data-testid=Link]") 462 | const Button = ActionRow.querySelector("button[aria-label='direct download']") 463 | 464 | if (Title == null || Artists.length <= 0 || Button == null) { 465 | return Data 466 | } 467 | 468 | Data.AudioTitle = Title.textContent 469 | Data.RawTitle = Data.AudioTitle 470 | Data.Button = Button 471 | 472 | for (const Artist of Artists) { 473 | Data.Artists.push(Artist.textContent.replaceAll(",", "").trim()) 474 | } 475 | 476 | if (Data.AudioTitle === "none" && Data.Artists.length == 0 && Data.Button == null) { 477 | return false 478 | } 479 | if ((Data.AudioTitle === "none" || Data.Artists.length == 0) && Data.Button != null) { 480 | Data.Button.style.color = ErrorButtonColor 481 | Data.Button.style.borderColor = ErrorButtonColor 482 | } 483 | 484 | return Data 485 | } 486 | 487 | function MakeFilename(AssetData, Pagetype) { 488 | const NoAlbum = AssetData.albumId === undefined 489 | 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})` 490 | } 491 | 492 | function WriteAudio(RowData, AudioData) { 493 | const Pagetype = RowData.Pagetype 494 | const ChosenColor = (Pagetype === MUSIC_PAGETYPE || Pagetype === SONGS_PAGETYPE || Pagetype == SONG_STEMS_PAGETYPE) ? ModifiedMusicButtonColor : ModifiedSfxButtonColor 495 | const FileName = MakeFilename(AudioData, Pagetype) 496 | RowData.Button.setAttribute("artlist-dl-processed", "true") 497 | RowData.Button.style.color = ChosenColor 498 | RowData.Button.addEventListener("click", function (event) { 499 | event.stopImmediatePropagation() // prevent premium popup upsell 500 | ShowSaveFilePickerForURL((AudioData.sitePlayableFilePath || AudioData.playableFileUrl), FileName + ".aac"); 501 | }, true) 502 | } 503 | 504 | function WriteBanner(BannerData, AudioData) { 505 | const Pagetype = BannerData.Pagetype 506 | const ChosenColor = (Pagetype === MUSIC_PAGETYPE || Pagetype === SONGS_PAGETYPE || Pagetype == SONG_STEMS_PAGETYPE) ? ModifiedMusicButtonColor : ModifiedSfxButtonColor 507 | const FileName = MakeFilename(AudioData, Pagetype) 508 | BannerData.Button.setAttribute("artlist-dl-processed", "true") 509 | BannerData.Button.style.color = ChosenColor 510 | BannerData.Button.style.borderColor = ChosenColor 511 | BannerData.Button.addEventListener("click", function (event) { 512 | event.stopImmediatePropagation() // prevent premium popup upsell 513 | ShowSaveFilePickerForURL((AudioData.sitePlayableFilePath || AudioData.playableFileUrl), FileName + ".aac"); 514 | }, true) 515 | } 516 | 517 | var changeBackTimeout = -1 518 | function WriteDownloadAllStems(StemsContainer, DownloadButton) { 519 | const Pagetype = GetPagetype() 520 | const ChosenColor = ModifiedMusicButtonColor 521 | DownloadButton.setAttribute("artlist-dl-processed", "true") 522 | DownloadButton.style.backgroundColor = ChosenColor 523 | DownloadButton.style.borderColor = ChosenColor 524 | DownloadButton.style.color = "black" 525 | DownloadButton.addEventListener("click", async function (event) { 526 | event.stopImmediatePropagation() // prevent dropdown 527 | clearInterval(changeBackTimeout) 528 | changeBackTimeout = setTimeout(() => { 529 | DownloadButton.querySelector("span.whitespace-nowrap").innerText = "Download All Stems" 530 | DownloadButton.disabled = false 531 | }, 10000) 532 | DownloadButton.querySelector("span.whitespace-nowrap").innerText = "Please Wait..." 533 | DownloadButton.disabled = true 534 | const dataList = [] 535 | var firstData = null 536 | for (const StemDescendant of StemsContainer.querySelectorAll("span[data-testid=stems-player-stem-name]")) { 537 | const Stem = StemDescendant.parentNode 538 | try { 539 | const AudioData = GetAudioDataFromRowData(GetAudioRowData(Stem, SONG_STEMS_PAGETYPE)) 540 | if (!firstData) { 541 | firstData = AudioData 542 | } 543 | 544 | dataList.push({ 545 | URL: (AudioData.sitePlayableFilePath || AudioData.playableFileUrl), 546 | Filename: MakeFilename(AudioData, SONG_STEMS_PAGETYPE) + ".aac" 547 | }) 548 | } catch (e) { 549 | console.warn("Exception while handling stem rows:", e) 550 | } 551 | } 552 | const fileName = `Music Stems ${firstData.artistName} - ${firstData._songName}${firstData._songName == firstData._albumName ? "" : ` on ${firstData._albumName}`}.zip` 553 | await ShowSaveFilePickerForURLsZipped(dataList, fileName) 554 | clearInterval(changeBackTimeout) 555 | DownloadButton.querySelector("span.whitespace-nowrap").innerText = "Download All Stems" 556 | DownloadButton.disabled = false 557 | }) 558 | } 559 | 560 | function MatchAudioToRow(AudioData, RowData, SkipArtistCheck) { 561 | return (AudioData.songName || AudioData.name).trim() === RowData.RawTitle.trim() && (SkipArtistCheck || RowData.Artists.indexOf(AudioData.artistName.trim()) != -1) 562 | } 563 | 564 | function OnRowAdded(AudioRow, RowData, AudioData) { 565 | AudioRow.setAttribute("artlist-dl-state", "modified") 566 | if (AudioData !== undefined) { 567 | WriteAudio(RowData, AudioData) 568 | } else { 569 | console.warn("No data given for row", RowData) 570 | if (RowData.Button !== null) { 571 | RowData.Button.style.color = ErrorButtonColor 572 | } 573 | } 574 | } 575 | 576 | function cloneref(object) { 577 | return { 578 | ...object 579 | } 580 | } 581 | 582 | function GetAudioDataFromRowData(RowData) { 583 | if (RowData.Pagetype === SFX_PAGETYPE) { 584 | if (LoadedSfxLists.length <= 0) { 585 | console.warn("No loaded sound effects to loop through."); 586 | return 587 | } 588 | for (const SfxList of LoadedSfxLists) { 589 | for (const SfxData of SfxList) { 590 | if (MatchAudioToRow(SfxData, RowData)) { 591 | return SfxData 592 | } 593 | } 594 | } 595 | } 596 | if (RowData.Pagetype === MUSIC_PAGETYPE) { 597 | if (LoadedMusicLists.length <= 0) { 598 | console.warn("No loaded songs to loop through."); 599 | return 600 | } 601 | for (const MusicList of LoadedMusicLists) { 602 | for (const SongData of MusicList) { 603 | if (MatchAudioToRow(SongData, RowData)) { 604 | return SongData 605 | } 606 | } 607 | } 608 | } 609 | if (RowData.Pagetype === SFXS_PAGETYPE) { 610 | if (LoadedSfxsList.length <= 0) { 611 | console.warn("No loaded similar sfxs to loop through."); 612 | return 613 | } 614 | for (const SfxsList of LoadedSfxsList) { 615 | for (const SfxData of SfxsList) { 616 | if (MatchAudioToRow(SfxData, RowData)) { 617 | return SfxData 618 | } 619 | } 620 | } 621 | } 622 | if (RowData.Pagetype === SONGS_PAGETYPE) { 623 | if (LoadedSongsList.length <= 0) { 624 | console.warn("No loaded similar songs to loop through."); 625 | return 626 | } 627 | for (const SongsList of LoadedSongsList) { 628 | for (const SongData of SongsList) { 629 | if (MatchAudioToRow(SongData, RowData)) { 630 | return SongData 631 | } 632 | } 633 | } 634 | } 635 | if (RowData.Pagetype === SONG_STEMS_PAGETYPE) { 636 | if (LoadedSstemsLists.length <= 0) { 637 | console.warn("No loaded song stems to loop through."); 638 | return 639 | } 640 | for (const StemData of LoadedSstemsLists) { 641 | for (const Stem of StemData.stems) { 642 | if (MatchAudioToRow(Stem, RowData, true)) { 643 | const clonedData = cloneref(StemData) 644 | clonedData.sitePlayableFilePath = Stem.playableFileUrl 645 | clonedData._songName = clonedData.songName 646 | clonedData._albumName = clonedData.albumName 647 | clonedData.songName = `${Stem.name} of ${StemData.songName}` 648 | clonedData.albumName = `${Stem.name} of ${StemData.albumName}` 649 | return clonedData 650 | } 651 | } 652 | } 653 | } 654 | 655 | console.warn("Couldn't handle data:", RowData) 656 | } 657 | 658 | function ApplyXHR(XHR) { 659 | const Pagetype = GetPagetype() 660 | 661 | if (Pagetype !== UNKNOWN_DATATYPE) { 662 | XHR.addEventListener("readystatechange", function () { 663 | if (XHR.readyState == XMLHttpRequest.DONE) { 664 | var JSONData 665 | try { 666 | JSONData = JSON.parse(XHR.responseText) 667 | } catch (e) { 668 | console.warn(`Couldn't parse as json: ${XHR.responseText}`) 669 | return 670 | } 671 | HandleJSONData(JSONData) 672 | } 673 | }) 674 | } 675 | } 676 | 677 | function HandleJSONData(Data) { 678 | console.log(Data) 679 | const Datatype = GetDatatype(Data) 680 | if (Datatype === SONGS_PAGETYPE) { 681 | LoadedSongsList.push(Data.data.songs[0].similarSongs) 682 | } 683 | if (Datatype === MUSIC_PAGETYPE) { 684 | LoadedMusicLists.push(Data.data.songList.songs) 685 | } 686 | if (Datatype === SFXS_PAGETYPE) { 687 | LoadedSfxsList.push(Data.data.sfxs[0].similarList) 688 | } 689 | if (Datatype === SFX_PAGETYPE) { 690 | LoadedSfxLists.push(Data.data.sfxList.songs) 691 | } 692 | if ((Datatype === SONG_STEMS_PAGETYPE && (Data.data.songs && Data.data.songs[0] && Data.data.songs[0].stems))) { 693 | LoadedSstemsLists.push(Data.data.songs[0]) 694 | } 695 | } 696 | 697 | function HookRequests() { 698 | var handler = function () { 699 | const Method = (arguments)[0] 700 | const URL = (arguments)[1] 701 | 702 | if (MatchURL(URL)) { 703 | ApplyXHR(this) 704 | } 705 | 706 | return oldXMLHttpRequestOpen.apply(this, arguments) 707 | } 708 | 709 | if (RequestsInterval != -1) { 710 | clearInterval(RequestsInterval) 711 | } 712 | RequestsInterval = setInterval(() => { 713 | window.XMLHttpRequest.prototype.open = handler 714 | }) 715 | } 716 | 717 | // this makes the user-script support the [←] Back and [→] Right navigations 718 | // aswell as switching pages because artlist doesn't navigate, but instead 719 | // changes their HTML dynamically so that the end-user does not have to 720 | // reload the entire page. 721 | 722 | // by polling the changes in an albeit bad way, we can detect when this 723 | // occurs, and as far as i know there's no other better way to do it. 724 | // please make an issue on github and educate me if there is. 725 | 726 | async function Initialize() { 727 | DontPoll = false 728 | 729 | const Pagetype = GetPagetype() 730 | 731 | console.log("searching for table...") 732 | 733 | if (Pagetype === SONGS_PAGETYPE || Pagetype === SFXS_PAGETYPE) { 734 | const Id = location.pathname.split("/")[4] 735 | const NumId = new Number(Id) 736 | if (NumId.toString() !== "NaN") { 737 | LoadAssetInfo(NumId) 738 | } 739 | SongPage = GetSongPage() 740 | await Until(() => { 741 | const Data = GetBannerData(SongPage, Pagetype) 742 | return Data != false && Data.Button != null 743 | }) 744 | const RowData = GetBannerData(SongPage, Pagetype) 745 | await Until(() => { 746 | return Pagetype === SONGS_PAGETYPE ? SingleSongData != "none" : SingleSoundEffectData != "none" 747 | }) 748 | const AudioData = Pagetype === SONGS_PAGETYPE ? SingleSongData : SingleSoundEffectData 749 | if (RowData.AudioTitle && RowData.Artists.length >= 1) { 750 | WriteBanner(RowData, AudioData) 751 | } 752 | } 753 | 754 | if (Pagetype === SONGS_PAGETYPE) { 755 | await Until(() => { 756 | return GetTBodyEdgeCase() != undefined 757 | }) 758 | TBody = GetTBodyEdgeCase() 759 | } else { 760 | await Until(() => { 761 | return GetTBody() != undefined 762 | }) 763 | TBody = GetTBody() 764 | console.log("tbody", TBody) 765 | } 766 | 767 | function OnAudioRowAdded(AudioRow) { 768 | if (AudioRow.getAttribute("artlist-dl-processed") === "true") { 769 | return 770 | } 771 | if (AudioRow.classList.contains("hidden")) { 772 | return 773 | } 774 | const RowData = GetAudioRowData(AudioRow, GetPagetype()) 775 | const AudioData = GetAudioDataFromRowData(RowData) 776 | 777 | OnRowAdded(AudioRow, RowData, AudioData) 778 | } 779 | 780 | LastInterval = setInterval(() => { 781 | for (const AudioRow of TBody.childNodes) { 782 | if (!AudioRow.hasAttribute("artlist-dl-processed")) { 783 | try { 784 | OnAudioRowAdded(AudioRow) 785 | AudioRow.setAttribute("artlist-dl-processed", "true") 786 | } catch (e) { 787 | console.warn(e) 788 | } 789 | } 790 | } 791 | 792 | for (const modal of document.querySelectorAll(".ReactModal__Content")) { 793 | for (const AllStemsDownload of modal.querySelectorAll("button[data-testid=renderButton]")) { 794 | if (!AllStemsDownload.hasAttribute("artlist-dl-processed") && AllStemsDownload.parentNode.getAttribute("data-testid") == "download-all-stems-dropdown") { 795 | WriteDownloadAllStems(modal, AllStemsDownload) 796 | } 797 | } 798 | for (const StemContainer of modal.querySelectorAll("span[data-testid=stems-player-stem-name]")) { 799 | const Stem = StemContainer.parentNode 800 | if (!Stem.hasAttribute("artlist-dl-processed") && document.contains(Stem)) { 801 | try { 802 | OnAudioRowAdded(Stem) 803 | Stem.setAttribute("artlist-dl-processed", "true") 804 | } catch (e) { } // will fail on false positives so just hide errors 805 | } 806 | } 807 | } 808 | }, 500) 809 | } 810 | 811 | HookRequests() 812 | document.addEventListener("DOMContentLoaded", () => { 813 | Initialize() 814 | setInterval(() => { 815 | if (TBody != null && !document.contains(TBody)) { 816 | console.log("Re-updating...") 817 | DontPoll = true 818 | TBody = null 819 | AudioTable = null 820 | SongPage = null 821 | Initialize() 822 | } 823 | }, 1000) 824 | }) 825 | --------------------------------------------------------------------------------