// ==UserScript== // @name Deezer Artist Dumper // @namespace http://tampermonkey.net/ // @version 1.1 // @description Adds the feature to add all artists songs to a playlist // @author Bababoiiiii // @match https://www.deezer.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=deezer.com // @grant GM_getValue // @grant GM_setValue // @downloadURL none // ==/UserScript== function set_css() { const css = document.createElement("style"); css.type = "text/css"; css.textContent = ` .main_btn { min-width: 32px; border-radius: 50%; transition-duration: 0.2s; } .main_btn svg path { fill: currentcolor; } .main_btn.active svg path{ fill: var(--tempo-colors-text-accent-primary-default); } .main_btn:hover { background-color: var(--tempo-colors-background-neutral-tertiary-hovered); } .main_div { position: absolute; left: 110%; transform: translateY(-60%); width: 500px; overflow: auto; display: none; resize: horizontal; border-radius: 8px; background-color: var(--tempo-colors-background-neutral-secondary-default); cursor: pointer; z-index: 300; } .main_div * { font-size: 14px; color: currentcolor; } .my_textarea { position: relative; width: 100%; height: 75px; line-height: 1.5; background-color: var(--tempo-colors-background-neutral-secondary-default); border: 0.5px solid var(--tempo-colors-divider-neutral-primary-default); color: var(--tempo-colors-text-neutral-secondary-default); padding: 5px; resize: vertical; overflow-y: auto; } .my_textarea:hover { border-color: var(--tempo-colors-text-neutral-secondary-default); } .toggles { padding: 5px 5px; border-bottom: 1px solid var(--tempo-colors-divider-neutral-primary-default); } .toggles label { margin-left: 10px; } .toggles input { margin-left: 5px; } .my_dropdown { margin-left: 10px; font-size: 14px; background-color: var(--tempo-colors-background-neutral-secondary-default); border: 0.5px solid var(--tempo-colors-divider-neutral-primary-default); border-radius: 4px; } .my_dropdown:hover { border-color: var(--tempo-colors-text-neutral-secondary-default); } .new_playlist_btn { width: 100%; display: flex; align-items: center; gap: 8px; padding: 8px 11px; } .new_playlist_btn svg { width: 24px; height: 24px; fill: var(--tempo-colors-text-accent-primary-default); } .new_playlist_btn svg path{ fill: var(--tempo-colors-text-accent-primary-default); } .new_playlist_btn span { color: var(--tempo-colors-text-accent-primary-default); } .playlist_ul { width: 100%; height: 200px; overflow: auto; position: relative; top: 6px; } .playlist_ul button { width: 100%; padding: 12px 16px; text-align: left; } .playlist_ul button:hover { background-color: var(--tempo-colors-bg-contrast); } .playlist_ul button[selected=""] { background-color: #463554a1; } .action_btn { width: 100%; position: relative; background-color: var(--tempo-colors-background-accent-primary-default); font-weight: bold; font-size: 20px; border-radius: 5px; padding: 10px; } .action_btn:hover { background-color: var(--tempo-colors-background-accent-primary-hovered); } ` document.querySelector("head").appendChild(css); } // data stuff async function get_user_data() { const r = await fetch("https://www.deezer.com/ajax/gw-light.php?method=deezer.getUserData&input=3&api_version=1.0&api_token=", { "body": "{}", "method": "POST", }); const resp = await r.json(); return resp; } async function get_auth_token() { const r = await fetch("https://auth.deezer.com/login/renew?jo=p&rto=c&i=c", { "method": "POST", "credentials": "include" }); const resp = await r.json(); return resp.jwt; } function get_api_token() { return user_data.results.checkForm; } function get_user_id() { return user_data.results.USER.USER_ID; } function get_current_artist_id() { return location.pathname.split("/artist/")[1].split("/", 1)[0]; } function get_current_artist_name() { return document.querySelector("meta[itemprop='name']").content } function get_playlists() { return JSON.parse(localStorage.getItem("PLAYLISTS_"+get_user_id())); } function get_config() { const config = GM_getValue("artist_dumper_config"); return config ? JSON.parse(config): { // default settings toggles: { ep: true, singles: true, album: true, featured: false, }, order: "RELEASE_DATE", regexes: "(?:\(slowed\)|\(sped up\)|\(reverb\))#i" } } function set_config() { GM_setValue("artist_dumper_config", JSON.stringify(config)); } async function get_all_songs(auth_token, artist_id) { async function get_all_albums() { async function get_albums(last_song) { // everything is an album const r = await fetch("https://pipe.deezer.com/api", { "headers": { "authorization": "Bearer "+auth_token, "Content-Type": "application/json" }, "body": JSON.stringify({ "operationName": "ArtistDiscographyByType", "variables": { "artistId": artist_id, "nb": 500, "cursor": last_song, "subType": null, "roles": [ "MAIN", ...(config.toggles.featured ? ['FEATURED'] : []) ], "order": config.order, "types": [ // thx chatgpt, wtf is this ...(config.toggles.ep ? ['EP'] : []), ...(config.toggles.singles ? ['SINGLES'] : []), ...(config.toggles.album ? ['ALBUM'] : []) ] }, "query": "query ArtistDiscographyByType($artistId: String!, $nb: Int!, $roles: [ContributorRoles!]!, $types: [AlbumTypeInput!]!, $subType: AlbumSubTypeInput, $cursor: String, $order: AlbumOrder) {\n artist(artistId: $artistId) {\n albums(\n after: $cursor\n first: $nb\n onlyCanonical: true\n roles: $roles\n types: $types\n subType: $subType\n order: $order\n ) {\n edges {\n node {\n ...AlbumBase\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n}\n\nfragment AlbumBase on Album {\n id\n displayTitle\n}" }), "method": "POST", }); const resp = await r.json(); return resp.data; } const albums = []; let data = await get_albums(null); for (let album of data.artist.albums.edges) { albums.push([album.node.id, album.node.displayTitle]); } // could prob do it better recursively while (data.artist.albums.pageInfo.hasNextPage) { data = await get_albums(data.artist.albums.pageInfo.endCursor); for (let album of data.artist.albums.edges) { albums.push([album.node.id, album.node.displayTitle]); } } return albums; } async function get_all_songs_from_album(album_id) { const r = await fetch("https://www.deezer.com/ajax/gw-light.php?method=song.getListByAlbum&input=3&api_version=1.0&api_token="+get_api_token(), { "body": JSON.stringify({ "alb_id": album_id, "start": 0, "nb": 500 }), "method": "POST", "credentials": "include" }); const resp = await r.json(); const album_songs = []; for (let album_song of resp.results.data) { let is_from_artist = false; for (let artist of album_song.ARTISTS) { if (artist.ART_ID === artist_id) { is_from_artist = true; break; } } if (is_from_artist) { album_songs.push([album_song.SNG_ID, `${album_song.SNG_TITLE} ${album_song.VERSION}`]); } } return album_songs; } // get all songs from albums asynchronous, 10 at a time to avoid ratelimits const albums = await get_all_albums(); let songs = {}; let promises = []; for (let i = 0; i < albums.length; i += 10) { const chunk = albums.slice(i, i + 10); let albumPromises = chunk.map(async album => { output(INFO, "Getting songs for " + album[1]); const albumSongs = await get_all_songs_from_album(album[0]); for (let song of albumSongs) { songs[song[0]] = song[1]; } }); await Promise.all(albumPromises); } if (last_dump?.artist_id === artist_id) { for (let last_dump_song_id of last_dump.song_ids) { if (songs[last_dump_song_id] !== undefined) { output(INFO, `Not adding ${songs[last_dump_song_id]} as it was present in the last dump`); delete songs[last_dump_song_id]; } } } return songs; } async function create_playlist(songs, artist_name) { const time = new Date() const formatted_time = time.toLocaleDateString(); const r = await fetch("https://www.deezer.com/ajax/gw-light.php?method=playlist.create&input=3&api_version=1.0&api_token="+get_api_token(), { "body": JSON.stringify({ "title": artist_name, "description": `A playlist containing all of ${artist_name} songs as of ${formatted_time}.`, "songs": songs.map((s) => [s]), "status": 1 }), "method": "POST", }); const resp = await r.json(); return resp; } async function add_songs_to_playlist(playlist_id, songs) { const r = await fetch("https://www.deezer.com/ajax/gw-light.php?method=playlist.addSongs&input=3&api_version=1.0&api_token="+get_api_token(), { "body": JSON.stringify({ "playlist_id": playlist_id, "songs": songs.map((s) => [s, 0]), "offset": -1, "ctxt": { "id": null, "t": null, } }), "method": "POST", "credentials": "include" }); const resp = await r.json(); return resp; } async function update_playlist_picture_to_current_artist(playlist_id) { const img_url = document.querySelector("#page_naboo_artist > div.container > div > div > div > img").src let r = await fetch(img_url); const img_blob = await r.blob(); const form_data = new FormData(); form_data.append("image", img_blob, "img.jpg") r = await fetch(`https://upload.deezer.com/?sid=${user_data.results.SESSION_ID}&id=${playlist_id}&resize=1&directory=playlist&type=picture&referer=FR&file=img.jpg`, { "body": form_data, "method": "POST", }); const resp = await r.json() return resp; } async function get_songs_in_playlist(playlist_id) { const r = await fetch("https://www.deezer.com/ajax/gw-light.php?method=playlist.getSongs&input=3&api_version=1.0&api_token="+get_api_token(), { "body": JSON.stringify({ "playlist_id": playlist_id, "start": 0, "nb": 2000 }), "method": "POST", "credentials": "include" }); const resp = await r.json() return resp; } function validate_regex(regex_str) { try { const l = regex_str.split("#") const flags = l[l.length-1] regex_str = regex_str.substr(0, regex_str.length-flags.length-1) // remove the flags return RegExp(regex_str, flags); } catch (e) { return null; } } function download_dump(data, time) { const formatted_time = time.toLocaleString('sv-SE', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }).replaceAll("-", "").replaceAll(':', '').replace(" ", "_"); const link = document.createElement('a'); link.download = `artistdump_${get_current_artist_name().replaceAll(" ", "_")}_${formatted_time}.json`; if (typeof(data) === "object") { data = JSON.stringify(data, null, 4) } const blob = new Blob([data], { type: 'application/json' }); link.href = URL.createObjectURL(blob); document.body.appendChild(link); link.click(); document.body.removeChild(link); } async function submit() { set_config(); let regexes_str = config.regexes.split(/(? ` let show = false; main_btn.onclick = () => { show = !show main_div.style.display = show ? "block" : "none"; main_btn.querySelector("button").className = show ? "main_btn active": "main_btn"; } return main_btn; } function create_main_div() { const main_div = document.createElement("div"); main_div.className = "main_div"; return main_div; } function create_blacklist_textarea() { const blacklist_textarea = document.createElement("textarea"); blacklist_textarea.className = "my_textarea"; blacklist_textarea.placeholder = "Regex pattern(s) to blacklist song titles. Javascript flags are the last part, seperated from the rest by a # (e.g. #igd). If a regex is invalid the whole process will be stopped before adding songs. 1 Pattern/Line"; blacklist_textarea.title = "Regex pattern(s) to blacklist song titles. Javascript flags are the last part, seperated from the rest by a # (e.g. #igd). If a regex is invalid the whole process will be stopped before adding songs. 1 Pattern/Line"; blacklist_textarea.value = config.regexes; blacklist_textarea.spellcheck = false; blacklist_textarea.oninput = () => { config.regexes = blacklist_textarea.value; } return blacklist_textarea; } function create_song_types_options() { const options_ul = document.createElement("ul"); options_ul.className = "toggles"; options_ul.role = "group"; options_ul.setAttribute("data-orientation", "horizontal"); const types = ["EP", "Singles", "Album", "Featured"] let inpt, lbl; for (let type of types) { inpt = document.createElement("input"); inpt.type = "checkbox"; inpt.title = `Wether to include ${type} or not` lbl = document.createElement("label"); lbl.textContent = type; type = type.toLowerCase(); inpt.checked = config.toggles[type] inpt.onclick = () => { config.toggles[type] = !config.toggles[type]; } options_ul.append(lbl, inpt); } const opts = [document.createElement('option'), document.createElement('option')]; opts[0].textContent = "Release Date"; opts[1].textContent = "Popularity"; const order_dropdown = document.createElement("select"); order_dropdown.className = "my_dropdown"; order_dropdown.title = "Order of songs. Not really important as you can just sort the playlist"; order_dropdown.append(...opts) order_dropdown.onchange = () => { // since we only have two elements, we know that if it changes it is the other option config.order = config.order === "RELEASE_DATE" ? "RANK" : "RELEASE_DATE"; } options_ul.appendChild(order_dropdown); return options_ul; } function create_new_playlist_btn() { const new_playlist_btn = document.createElement("button"); new_playlist_btn.type = "button"; new_playlist_btn.className = "new_playlist_btn"; new_playlist_btn.title = "(Recommended) Creates a new private playlist with the name and picture of the artist where the songs will be added to."; new_playlist_btn.setAttribute("data-id", "-1"); new_playlist_btn.innerHTML = ` New Playlist` new_playlist_btn.onclick = () => { change_selected_playlist(new_playlist_btn); } return new_playlist_btn; } function create_playlists_btns(playlists, new_playlist_btn) { const playlist_ul = document.createElement("ul"); playlist_ul.className = "playlist_ul"; let playlist_li = document.createElement("li"); playlist_li.appendChild(new_playlist_btn); playlist_ul.appendChild(playlist_li); let playlist, playlist_btn; for (playlist of playlists.data) { playlist_btn = document.createElement("button"); playlist_btn.title = `Add the songs to ${playlist.TITLE}` playlist_btn.textContent = playlist.TITLE playlist_btn.onclick = (e) => { change_selected_playlist(e.target); } playlist_btn.setAttribute("data-id", playlist.PLAYLIST_ID); playlist_li = document.createElement("li"); playlist_li.appendChild(playlist_btn); playlist_ul.appendChild(playlist_li); } return playlist_ul; } function create_submit_btn() { const submit_btn = document.createElement("button"); submit_btn.textContent = "Submit"; submit_btn.className = "action_btn"; submit_btn.style.top = "10px"; submit_btn.style.marginBottom = "10px"; submit_btn.title = "Starts the whole process. The settings (regex, checkboxes) will be saved locally for the next use." submit_btn.onclick = submit; return submit_btn; } function create_output_textarea() { const output_textarea = document.createElement("textarea"); output_textarea.className = "my_textarea"; output_textarea.placeholder = "Output (Click to Copy)"; output_textarea.title = "Outputs information about the progess. Click to Copy."; output_textarea.readOnly = true; output_textarea.onmouseup = () => { if (window.getSelection().toString() === "") { navigator.clipboard.writeText(output_textarea.value); } } return output_textarea; } function create_load_btn(data, time) { const file_inpt = document.createElement("input"); file_inpt.type = "file"; file_inpt.style.display = "none"; const load_btn = document.createElement("button"); load_btn.textContent = "Load Dump"; load_btn.className = "action_btn"; load_btn.title = "Load data from an earlier dump." load_btn.onclick = () => { file_inpt.click(); }; file_inpt.onchange = (e) => { const file = e.target.files[0]; let reader = new FileReader(); reader.readAsText(file, "UTF-8"); reader.onload = (re) => { last_dump = JSON.parse(re.target.result); load_btn.textContent = file.name } } return load_btn; } function create_download_btn(data, time) { const download_btn = document.createElement("button"); download_btn.textContent = "Download Dump"; download_btn.className = "action_btn"; download_btn.title = "Download data for this dump." download_btn.style.marginTop = "1px"; download_btn.onclick = () => download_dump(data, time); return download_btn; } // globals let config; let selected_playlist; let user_data; let output_textarea; let main_div; let download_btn; let last_dump; const ERROR = "ERROR"; const INFO = "INFO"; const SUCCESS = "SUCCESS"; let last_url = location.href; navigation.addEventListener('navigate', (e) => { const target_url = e.destination.url; console.log("change", last_url, target_url); const last_id = last_url.split("/artist/") const target_id = target_url.split("/artist/"); if (target_id.length > 1) { // current page is an artist if (last_id.length > 1) { // the last page was also an artist if (target_id[1].split("/", 1)[0] !== last_id[1].split("/", 1)[0]) { // the current and last artist arent the same main(); } } else { main(); } } last_url = target_url; }); if (location.pathname.includes("/artist/")) { main(); } async function main() { user_data = await get_user_data(); let main_ul; const wait = setInterval(() => { console.log("waiting"); main_ul = document.querySelector("#page_naboo_artist > div.container > div > ul[role='group']"); if (main_ul !== null) { clearInterval(wait); console.log("found"); if (document.querySelector(".main_btn") !== null) { return; } config = get_config() last_dump = null; set_css(); main_ul.style.position = "relative"; main_div = create_main_div(); const blacklist_textarea = create_blacklist_textarea(); const options_ul = create_song_types_options(); let new_playlist_btn = create_new_playlist_btn(); new_playlist_btn.setAttribute("selected", ""); selected_playlist = new_playlist_btn; const playlist_ul = create_playlists_btns(get_playlists(), new_playlist_btn); const submit_btn = create_submit_btn(); const load_btn = create_load_btn(); output_textarea = create_output_textarea(); const main_btn = create_main_btn(main_div); main_div.append(blacklist_textarea, options_ul, playlist_ul, submit_btn, output_textarea, load_btn); main_ul.append(main_btn, main_div); } }, 200) }