An ad-less, multilingual, clean Soundcloud downloader with robust code.
Adds a 'Download' button to all single track views.
-
-
Save webketje/8cd2e6ae8a86dbe0533c5d2c612c42c6 to your computer and use it in GitHub Desktop.
| // ==UserScript== | |
| // @name Soundcloud Downloader Clean | |
| // @namespace https://openuserjs.org/users/webketje | |
| // @version 0.1.0 | |
| // @description An ad-less, multilingual, clean Soundcloud downloader with robust code. Adds a 'Download' button in the toolbar of all single track views. | |
| // @author webketje | |
| // @license MIT | |
| // @icon https://a-v2.sndcdn.com/assets/images/sc-icons/favicon-2cadd14bdb.ico | |
| // @homepageURL https://gist.github.com/webketje/8cd2e6ae8a86dbe0533c5d2c612c42c6 | |
| // @supportURL https://gist.github.com/webketje/8cd2e6ae8a86dbe0533c5d2c612c42c6 | |
| // @updateURL https://openuserjs.org/meta/webketje/Soundcloud_Downloader_Clean.meta.js | |
| // @downloadURL https://openuserjs.org/install/webketje/Soundcloud_Downloader_Clean.user.js | |
| // @noframes | |
| // @match https://soundcloud.com/* | |
| // @grant unsafeWindow | |
| // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/FileSaver.min.js | |
| // ==/UserScript== | |
| /* globals saveAs */ | |
| (function() { | |
| 'use strict'; | |
| var win = unsafeWindow || window; | |
| var containerSelector = '.listenEngagement__footer .sc-button-toolbar'; | |
| /** | |
| * @desc Log to console only if debug is true | |
| */ | |
| function log() { | |
| var stamp = new Date().toLocaleString(), | |
| args = [].slice.call(arguments), | |
| prefix = ['SCDLC', stamp, '-']; | |
| scdl.debug && console.log(prefix.concat(args).join(' ')); | |
| }; | |
| /** | |
| * @desc There is no other way to retrieve a Soundcloud client_id than by spying on existing requests. | |
| * We temporarily patch the XHR.send method to retrieve the url passed to it. | |
| * @param restoreIfTrue - restores the original prototype method when true is returned | |
| * @param onRestore - a function to exec when the restoreIfTrue condition is met | |
| */ | |
| function patchXHR(restoreIfTrue, onRestore) { | |
| var originalXHR = win.XMLHttpRequest.prototype.open; | |
| win.XMLHttpRequest.prototype.open = function() { | |
| originalXHR.apply(this, arguments); | |
| var restore = restoreIfTrue.apply(this, arguments); | |
| if (restore) { | |
| win.XMLHttpRequest.prototype.open = originalXHR; | |
| onRestore(restore); | |
| } | |
| }; | |
| }; | |
| var scdl = { | |
| debug: false, | |
| client_id: '', | |
| dlButtonId: 'scdlc-btn' | |
| }; | |
| scdl.getTrackName = function(trackJSON) { | |
| return [ | |
| trackJSON.user.username, | |
| trackJSON.title | |
| ].join(' - '); | |
| }; | |
| scdl.getStreamURL = function(url, onresolve, onerror) { | |
| var xhr = new XMLHttpRequest(); | |
| xhr.onload = function() { | |
| var trackJSON = JSON.parse(xhr.responseText); | |
| onresolve(trackJSON.errors || !trackJSON.stream_url ? false : { | |
| stream_url: trackJSON.stream_url + '?client_id=' + this.client_id, | |
| track_name: this.getTrackName(trackJSON) | |
| }); | |
| }.bind(this); | |
| xhr.onerror = function() { | |
| onerror(false); | |
| }; | |
| xhr.open('GET', 'https://api.soundcloud.com/resolve?url=' + encodeURIComponent(url) + '&client_id=' + this.client_id); | |
| xhr.send(); | |
| }; | |
| scdl.button = { | |
| label: { | |
| en: 'Download', | |
| es: 'Descargar', | |
| fr: 'Télécharger', | |
| nl: 'Download', | |
| de: 'Download', | |
| pl: 'Ściągnij', | |
| it: 'Scaricare', | |
| pt_BR: 'Baixar', | |
| sv: 'Ladda ner' | |
| }, | |
| download: function(e) { | |
| e.preventDefault(); | |
| saveAs(e.target.href, e.target.dataset.title); | |
| }, | |
| render: function(href, title, onClick) { | |
| var label = scdl.button.label[document.documentElement.lang]; | |
| var a = document.createElement('a'); | |
| a.className = "sc-button"; | |
| a.href = href; | |
| a.id = scdl.dlButtonId; | |
| a.textContent = label; | |
| a.dataset.title = title + '.mp3'; | |
| a.setAttribute('download', title + '.mp3'); | |
| a.target = '_blank'; | |
| a.onclick = onClick; | |
| a.style.marginLeft = '5px'; | |
| a.style.cssFloat = 'left'; | |
| return a; | |
| }, | |
| attach:function() { | |
| this.remove(); | |
| var f = document.querySelector(containerSelector); | |
| if (f) | |
| f.insertAdjacentElement('afterend', this.render.apply(this, arguments)); | |
| }, | |
| remove: function() { | |
| var btn = document.getElementById(scdl.dlButtonId); | |
| if (btn) | |
| btn.parentNode.removeChild(btn); | |
| } | |
| }; | |
| scdl.parseClientIdFromURL = function(url) { | |
| var search = /client_id=([\w\d]+)&*/; | |
| return url && url.match(search) && url.match(search)[1]; | |
| }; | |
| scdl.getClientID = function(onClientIDFound) { | |
| patchXHR(function(method, url) { | |
| return scdl.parseClientIdFromURL(url); | |
| }, onClientIDFound); | |
| }; | |
| scdl.load = function(url) { | |
| // for now only make available for single track pages | |
| if (/^(\/(you|stations|discover|stream|upload|search|settings))/.test(win.location.pathname)) { | |
| scdl.button.remove(); | |
| return; | |
| } | |
| scdl.getStreamURL(url, | |
| function onSuccess(result) { | |
| if (!result) { | |
| scdl.button.remove(); | |
| } else { | |
| log('Detected valid Soundcloud artist track URL. Requesting info...'); | |
| scdl.button.attach( | |
| result.stream_url, | |
| result.track_name, | |
| scdl.button.download | |
| ); | |
| } | |
| }, | |
| scdl.button.remove | |
| ); | |
| }; | |
| // patch front-end navigation | |
| ['pushState','replaceState','forward','back','go'].forEach(function(event) { | |
| var tmp = win.history.pushState; | |
| win.history[event] = function() { | |
| tmp.apply(win.history, arguments); | |
| scdl.load(win.location.href); | |
| } | |
| }); | |
| scdl.getClientID(function(id) { | |
| log('Found Soundcloud client id:', id, '. Initializing...'); | |
| scdl.client_id = id; | |
| scdl.load(win.location.href); | |
| }); | |
| })(); |
@xxturboxx what browser are you on? For me it works perfectly (Chrome/ Firefox). Try for example this URL https://soundcloud.com/throttle/dreamer
Thanks mate, this one is awesome! Clean and functional as it should be <3
HOW TO DOWNLOAD 320KBPS ON soundcloud?
@RAGAGAG Last time I worked on this script, you couldn't. There was only 128kbps. Maybe Soundcloud added extra sources, I should have a look.
Another type of music this script cannot download is HLS (streaming) sources (which are m3u files composed of temporary links to chunks of data of a song). I tried to add it but it's too hard, the chunks are encrypted with AES and a private key through a third-party library I cannot obtain access to them
Thi script is incredible, thanks for releasing it.
the saveAs function doesn't work, you can use GM_download. Example:
GM_download({ url: e.target.href, name: e.target.dataset.title, saveAs: true });
@Cabbasca GM_download only works in the context of TamperMonkey. Filesaver.js also works as a bookmarklet. If it doesn't work for you, that means the page failed to load https://cdn.jsdelivr.net/npm/[email protected]/dist/FileSaver.min.js because I just tested and it works as intended.
Interestingly this stopped working for me, my script had version 0.2.1 but and greasyfork has 0.2.1 but I found 1.0.0 on openuserjs so updated from there - there was a 90%+ change (@@ -1,214 +1,370 @@)
and now everything works again
i'd strongly recommend updating the script at greasyfork with the downloadurl and updateurl of openuserjs
@nascentt thx I forgot to update it there, it's ok now
How to install this extension?
@sunset4myval You need to install the Tampermonkey browser extension and then click the "Install" button here: https://openuserjs.org/scripts/webketje/Soundcloud_Downloader_Clean
Hi all Im working from mac, very novice to extensions and all this tampermonkey business. I cant get it to operate on my chrome soundcloud url, any suggestions?
@AllKillahNoFillah if no script is running in Tampermonkey, you haven't properly installed or enabled this script.

I was so excited about this but it doesn't seem to load properly.