# https://free-mp3-download.net/ Spotify Automatic Downloader ### Future Notes - https://github.com/automation9417/automation-samples/tree/main/ScrapingSpotifyList - This could be useful for full automation - At some point, I might be able to build an OpenFin app that does this all in one step # Instructions Here. Updated 2024-02-28 ## Step 0: Fire up Opus 1. I'm on a slow dyno, so might as well start preloading the website you'll use to get your playlist 1. Open up a tab 1. Go to https://opusopus.co/ 1. Let it do whatever it does to load. In the meantime move to Step 1 ## Step 1: Get Your Spotify Playlist Code 1. Go to your Spotify Playlist 1. Right Click -> Share -> Copy link to playlist 1. What you want is the code for your playlist, which should be after `playlist/` and before the `?` - e.g. for this link: - `https://open.spotify.com/playlist/6DqoxpjdG9vZAwSxaQHqB2?si=85dcf9b7f9734569` - you'd want to just copy this playlist code: - `6DqoxpjdG9vZAwSxaQHqB2` 1. You're ready for the next step ## Step 2: Get your download script 1. Go to https://opusopus.co/ 1. Right click the screen -> Click Inspect 1. Go to the Console tab 1. Paste the following in the Console, including your playlist code in quotation marks: - `grabTracksFromPlaylistAndCopyScript("playlist_id_here")` - e.g. `grabTracksFromPlaylistAndCopyScript("6DqoxpjdG9vZAwSxaQHqB2")` 1. Congratulations, in one step, Opus has grabbed all of the tracks from your playlist, and copied those tracks, along with a script, to your clipboard. 1. If you lose your copied clipboard text, you can just run the same script again ## Warning - From this point on, this is done in the Chrome browser. If you use a different browser, you may have to take different steps. ## Step 3: Prepare for downloading - In this step, we're disabling the Chrome Save dialog, so you don't have to click Save every time you download a track. - This is something you'll want to re-enable once you're done for security purposes. 1. Go to your download settings by pasting this address into your bar: - chrome://settings/downloads 1. You can also go to your download settings by doing the following: - Click the 3 dots in the top right of your Chrome browser - Click Settings - On the left panel, click Downloads 1. Disable "Ask where to save each file before downloading" ## Step 4: Prepare for downloading Part 2 - In this step, we'll be passing the Captcha for the mp3 download website. That way, you only need to click the Download button to download a track in each tab, instead of clicking both the Captcha and the Download button 1. Go to https://free-mp3-download.net/ 2. Search for a random song, any song. 3. Solve the captcha and download it - This serves as a good test to make sure everything's working right 1. Your System is ready for big downloading! ## Step 5: Big Download Part 1 - In this step, we'll get all of the songs you want to download into individual tabs 1. Go to https://free-mp3-download.net/ 1. Right click the screen -> Click Inspect - **NOTE: The site has put in a new anti-hacking measure: the debugger. When you Right click the screen, this should pop up** - ![image](https://hackmd.io/_uploads/HJxjbun2T.png) - This will prevent you from running code in your console, BUT there's an easy workaround. You just have to do the following: - See that top-right icon of the mini-arrow with a slash in it? If you hover over it, it should say `Deactivate breakpoints`. Click it to enable it. This turns off debuggers. - See that blue arrow 5 icons to the left of the `Deactivate breakpoints` button? If you hover over it, it should say `Resume script execution`. Click it. - You're all set! 1. Go to the Console tab 3. Paste the script that should currently be on your clipboard. - If you don't have the script copied, go back to Step 2 4. You should see the search box automatically search for songs, and tabs start to pop up #### Some tips for this step: - Sometimes, the search will stop. - I've found that this happens when the tab doing the searching gets buried in other tabs. - To work around this, you'll want to keep focus on the tab. Some options: - Use this Chrome Extension: https://chromewebstore.google.com/detail/force-background-tab/gidlfommnbibbmegmgajdbikelkdcmcl?hl=en&pli=1 - Use `Cmd + Shift + [` to keep the focus back on the searching tab each time it searches - Click on the tab whenever it freezes - You can watch the progress of your downloads in the console. - I've set up a crude matching algorithm to detect if free mp3 download's search (which isn't great) actually found the song you were asking for, which spits out a percentage. - It'll also let you know when it couldn't find anything at all. - After the script goes through all your songs, it'll give you two lists: one of songs it couldn't find, and another of songs that had a low similarity percentage - Whatever you do, especially after the search is done, do NOT use that first tab at all. - Closing the tab, clicking on the icon, attempting a search, many of those things can completely clear your console, which makes you lose any data you had on your search. You'll have to go through your downloads manually to see if you downloaded anything accidentally. ## Step 6: Big Download Part 2 - You're ready to download your songs! - This should be as simple as going to each tab and clicking download. - I personally: - Zoom out on the page so the Download button is visible without having to scroll - Use the `Cmd + Shift + [` shortcut on to switch between tabs - Have my right thumb on the trackpad to click on the download button without having to let go of the keyboard shortcut - By disabling the save dialog, passing the captcha check, and zooming out on the page, you can download all of your songs in less than a minute, vs playing the clicking-and-waiting game of navigating between tabs and save dialogs. It saves a LOT of time. ## Step X: Cleanup - Go back to your Download Settings and re-enable "Ask where to save each file before downloading" - Enjoy your downloads # # # # EVERYTHING BELOW IS DEPRECATED. USE INSTRUCTIONS ABOVE INSTEAD # EVERYTHING BELOW IS DEPRECATED. USE INSTRUCTIONS ABOVE INSTEAD # EVERYTHING BELOW IS DEPRECATED. USE INSTRUCTIONS ABOVE INSTEAD ## Step 1: Get Your Playlist 1. Go to https://open.spotify.com/. 2. Log in. 3. Go to the page of the playlist that you want to download. 4. Scroll ALLLL the way down on your playlist, otherwise you may miss songs that aren't loaded onto the page. 5. Hit Ctrl + Shift + i to open up the Chrome Devtools (It might be different on Mac). 6. Go to the Console tab. 7. Hit Ctrl + L to clear the console. (You can also clear it by clicking the little circle with a line through it on the top left). 8. Copy and paste the code below into the console, then hit enter. 9. You should now have a big list of track names copied to your clipboard. Save it somewhere. I personally just paste it into that console and leave it there for later. > Important note, you will likely have some extra songs that weren't originally on your playlist. This is usually from the Recommended Songs at the bottom of the page. ``` javascript // UPDATED AS OF 4/27/2023 to include missing tracks check // UPDATED AS OF 5/9/2023 to include a re-copy of the correct track list if too many are added trackNames = document.querySelectorAll('[data-testId="tracklist-row"]') formattedTrackNames = []; // Format the tracks properly for (let track of trackNames) { let trackText = track.children[0].children[0].children[1].getAttribute("aria-label"); console.log(trackText) trackText = trackText.slice(5); trackText = trackText.split("by"); trackText = trackText.reverse().join(" ") formattedTrackNames.push(trackText); } // Copy the tracks to the clipboard function copyToClipboard(text) { var dummy = document.createElement("textarea"); document.body.appendChild(dummy); dummy.value = text; dummy.select(); document.execCommand("copy"); document.body.removeChild(dummy); } formattedTrackNamesText = formattedTrackNames.join(`\n`) copyToClipboard(formattedTrackNamesText); console.log(formattedTrackNamesText); numPlaylistTracks = document.getElementsByClassName("contentSpacing")[0].lastChild.lastChild.lastChild.innerText.match(/[0-9]+/)[0]; numCopiedTracks = formattedTrackNames.length; numMissingTracks = numPlaylistTracks - formattedTrackNames.length console.log("Number of tracks in Playlist: ", numPlaylistTracks) console.log("Number of songs in your copied list: ", numCopiedTracks) if (numMissingTracks > 0){ console.log("Missing ", numMissingTracks, " tracks.") } if (numMissingTracks < 0){ console.log("Extra ", numMissingTracks, " tracks.") formattedTrackNamesText = formattedTrackNames.slice(0, numPlaylistTracks).join(`\n`) copyToClipboard(formattedTrackNamesText); console.log(formattedTrackNamesText); } function copyTracks() { var dummy = document.createElement("textarea"); document.body.appendChild(dummy); dummy.value = formattedTrackNamesText; dummy.select(); document.execCommand("copy"); document.body.removeChild(dummy); } ``` ## Step 2: Prepare for Downloading This next step here is to get everything ready for downloading. 1. Go to https://free-mp3-download.net/ 2. You need to allow popups: a. Right click the lock icon in the top left b. Click Site Settings c. Scroll down to Pop-ups and redirects d. Toggle to Allow ## Step 3: Time to Download 1. Go to https://free-mp3-download.net/ 2. Open up the Chrome Devtools (Ctrl + Shift + i). 3. Go to the Console tab. 4. Hit Ctrl + L to clear the console. (You can also clear it by clicking the little circle with a line through it on the top left). 5. Copy all of the code below, and paste it into the console. 6. Hit enter to give it a test and see if it breaks/if there are any snags. You want to test with this smaller subset of tracks instead of the dozens of tracks you may potentially have later. (NOTE: If you do this, you have refresh the page). 7. Remember that big blob of track names from earlier? Go get that and copy it again. 8. In the console, delete all of the text inside the `playlistString` variable. Once you only have two apostrophes left (``), paste your big blob of track names inbetween them. Keep the formatting as it was, and make sure there are no spaces/newlines between the apostrophes and the text. It should look basically exactly as it does below. 9. Hit enter, and the script should run and start downloading tracks. Let it do its thing, don't try to save tracks or anything just yet. 10. If you watch the console, it'll give you red warnings if you hit any errors (which usually happens if it can't find a song). Those errors will get saved, and once the script is done, it'll tell you what songs it missed. 11. Once the script is done, you should see logs in the console that say `Script has completed!`. 12. Now, you should have a Save prompt in Chrome. Hit Save on that, and another one should pop up. On my computer, I can literally just hold down Enter and it'll just keep automatically hitting Save for me. Just keep hitting Save as fast as you can and everything should download. 13. Finish up by manually downloading the tracks the script missed. 14. And that's it! Enjoy your playlist! ``` javascript // Updated 4/27/2023 to adapt to free-mp3-download. Could be improved // Updated 5/4/2023 to add special character filtering of words for accents, and adds similarity comparisons // // Ignore, for similiarity comparison function similarity(s1, s2) { var longer = s1; var shorter = s2; if (s1.length < s2.length) { longer = s2; shorter = s1; } var longerLength = longer.length; if (longerLength == 0) { return 1.0; } return (longerLength - editDistance(longer, shorter)) / parseFloat(longerLength); } function editDistance(s1, s2) { s1 = s1.toLowerCase(); s2 = s2.toLowerCase(); var costs = new Array(); for (var i = 0; i <= s1.length; i++) { var lastValue = i; for (var j = 0; j <= s2.length; j++) { if (i == 0) costs[j] = j; else { if (j > 0) { var newValue = costs[j - 1]; if (s1.charAt(i - 1) != s2.charAt(j - 1)) newValue = Math.min(Math.min(newValue, lastValue), costs[j]) + 1; costs[j - 1] = lastValue; lastValue = newValue; } } } if (i > 0) costs[s2.length] = lastValue; } return costs[s2.length]; } // All the songs you want to download go here, with new lines between songs. playlistString = `DJ Lycox Não Se Mete DJ Lycox Esse é Do Guetto Tia Maria Produções, DJ Bboy Moh Cota` // Grab the search bar and button elements searchBar = document.getElementById("q"); searchButton = document.getElementById("snd"); // Turn our fat string into an array songsArray = playlistString.split("\n"); console.log('songsArray', songsArray); // Set up some timeout functions so that the script doesn't move too fast, // otherwise you'll miss downloads. // Feel free to increase the 2000 number if you find you keep missing downloads. function timeout(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function sleep(fn) { await timeout(1000); // 1/28/2022 Added the await here so that when we retry, we don't go to the next song await fn(); await timeout(500); } // Array to store songs that didn't download properly. errorSongs = []; // Array to store songs that might not be the correctly downloaded songs dissimilarSongs = []; for (let song of songsArray) { console.log("Default Song Name", song); // Remove symbols noSpecialChars = song.replace(/[$,\[\]@%-]/g, ''); words = noSpecialChars.split(" ") // Remove words with weird characters filteredWords = words.filter((word) => !/[^a-zA-Z0-9 ]/g.test(word)) filteredSong = filteredWords.join(" ") // Put the song name in and kick off the search. console.log("Filtered Song Name", filteredSong) // Put the song name in and kick off the search. searchBar.value = filteredSong; searchButton.click(); console.log('search button clicked for', song) // Wait some time and then try to download the song. await sleep(async () => { try { firstDownloadButton = document.getElementsByClassName("darken-4")[0] console.log('firstDownloadButton got for', song) firstResultName = document.getElementById('results_t').firstChild.firstChild.innerText console.log('firstResultName for firstDownloadButton: ', firstResultName) similarityPercentage = similarity(song, firstResultName) console.log('Similarity Percentage = ', similarityPercentage) if (similarityPercentage < 0.5) { dissimilarSongs.push({song, firstResultName, similarityPercentage}) } } catch (e) { console.log('error attempting to search', song, e) errorSongs.push(song); return; } try { console.log('attempting to window.open for', firstDownloadButton.parentNode.href) window.open(firstDownloadButton.parentNode.href, "_blank") window.focus() } catch (e) { console.log('error attempting to open', song, e) errorSongs.push(song); } }); } // General info console.log("Script has completed!"); console.log("Script has completed!"); console.log("Script has completed!"); console.log("Script has completed!"); console.log("Script has completed!"); console.log("Script has completed!"); console.log("Script has completed!"); console.log("Script has completed!"); console.log("Script has completed!"); console.log("Script has completed!"); console.log(`${songsArray.length - errorSongs.length} songs successfully downloaded out of ${songsArray.length}`) console.log(`${errorSongs.length} songs still need to be downloaded, here they are:`); console.log(errorSongs); console.log("Dissimilar Songs:") console.log(dissimilarSongs) ``` ## opus hack grabTracksFromPlaylist("playlist_id here") ``` js async function grabTracksFromPlaylist(playlist_id) { function copyToClipboard(text) { var dummy = document.createElement("textarea"); document.body.appendChild(dummy); dummy.value = text; dummy.select(); document.execCommand("copy"); document.body.removeChild(dummy); } const playlist = await PingSpotifyAPIAJAX(`https://api.spotify.com/v1/playlists/${playlist_id}`) trackList = [] console.log(playlist); playlist.tracks.items.forEach((item) => { artists = []; item.track.artists.forEach((artist) => artists.push(artist.name)) trackList.push(`${artists.join(" ")} - ${item.track.name}`) }) formattedTrackList = trackList.join(`\n`) copyToClipboard(formattedTrackList) console.log(formattedTrackList) } ``` ## paste this in, then call: grabTracksFromPlaylistAndCopyScript("playlist_id here") ``` js async function grabTracksFromPlaylistAndCopyScript(playlist_id) { function copyToClipboard(text) { var dummy = document.createElement("textarea"); document.body.appendChild(dummy); dummy.value = text; dummy.select(); document.execCommand("copy"); document.body.removeChild(dummy); } const playlist = await PingSpotifyAPIAJAX(`https://api.spotify.com/v1/playlists/${playlist_id}`) trackList = [] console.log(playlist); playlist.tracks.items.forEach((item) => { artists = []; item.track.artists.forEach((artist) => artists.push(artist.name)) trackList.push(`${artists.join(" ")} - ${item.track.name}`) }) formattedTrackList = trackList.join(`\n`) console.log(formattedTrackList) textToCopy = `function similarity(s1, s2) { var longer = s1; var shorter = s2; if (s1.length < s2.length) { longer = s2; shorter = s1; } var longerLength = longer.length; if (longerLength == 0) { return 1.0; } return (longerLength - editDistance(longer, shorter)) / parseFloat(longerLength); } function editDistance(s1, s2) { s1 = s1.toLowerCase(); s2 = s2.toLowerCase(); var costs = new Array(); for (var i = 0; i <= s1.length; i++) { var lastValue = i; for (var j = 0; j <= s2.length; j++) { if (i == 0) costs[j] = j; else { if (j > 0) { var newValue = costs[j - 1]; if (s1.charAt(i - 1) != s2.charAt(j - 1)) newValue = Math.min(Math.min(newValue, lastValue), costs[j]) + 1; costs[j - 1] = lastValue; lastValue = newValue; } } } if (i > 0) costs[s2.length] = lastValue; } return costs[s2.length]; } // All the songs you want to download go here, with new lines between songs. playlistString = \`${formattedTrackList}\` // Grab the search bar and button elements searchBar = document.getElementById("q"); searchButton = document.getElementById("snd"); // Turn our fat string into an array songsArray = playlistString.split("\\n"); console.log('songsArray', songsArray); // Set up some timeout functions so that the script doesn't move too fast, // otherwise you'll miss downloads. // Feel free to increase the 2000 number if you find you keep missing downloads. function timeout(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function sleep(fn) { await timeout(1000); // 1/28/2022 Added the await here so that when we retry, we don't go to the next song await fn(); await timeout(500); } // Array to store songs that didn't download properly. errorSongs = []; // Array to store songs that might not be the correctly downloaded songs dissimilarSongs = []; for (let song of songsArray) { console.log("Default Song Name", song); // Remove symbols noSpecialChars = song.replace(/[$,\[\]@%-]/g, ''); words = noSpecialChars.split(" ") // Remove words with weird characters filteredWords = words.filter((word) => !/[^a-zA-Z0-9 ]/g.test(word)) filteredSong = filteredWords.join(" ") // Put the song name in and kick off the search. console.log("Filtered Song Name", filteredSong) // Put the song name in and kick off the search. searchBar.value = filteredSong; searchButton.click(); console.log('search button clicked for', song) // Wait some time and then try to download the song. await sleep(async () => { try { firstDownloadButton = document.getElementsByClassName("darken-4")[0] console.log('firstDownloadButton got for', song) firstResultName = document.getElementById('results_t').firstChild.firstChild.innerText console.log('firstResultName for firstDownloadButton: ', firstResultName) similarityPercentage = similarity(song, firstResultName) console.log('Similarity Percentage = ', similarityPercentage) if (similarityPercentage < 0.5) { dissimilarSongs.push({song, firstResultName, similarityPercentage}) } } catch (e) { console.log('error attempting to search', song, e) errorSongs.push(song); return; } try { console.log('attempting to window.open for', firstDownloadButton.parentNode.href) window.open(firstDownloadButton.parentNode.href, "_blank") window.focus() } catch (e) { console.log('error attempting to open', song, e) errorSongs.push(song); } }); } // General info console.log("Script has completed!"); console.log("Script has completed!"); console.log("Script has completed!"); console.log("Script has completed!"); console.log("Script has completed!"); console.log("Script has completed!"); console.log("Script has completed!"); console.log("Script has completed!"); console.log("Script has completed!"); console.log("Script has completed!"); console.log(\`\${songsArray.length - errorSongs.length} songs successfully downloaded out of \${songsArray.length}\`) console.log(\`\${errorSongs.length} songs still need to be downloaded, here they are:\`); console.log(errorSongs); console.log("Dissimilar Songs:") console.log(dissimilarSongs) ` copyToClipboard(textToCopy); } ```