Skip to content

Instantly share code, notes, and snippets.

@Grdflo
Forked from webketje/README.md
Created February 2, 2022 17:38
Show Gist options
  • Save Grdflo/bbcb5726f0a5467e081e189e2da9ecf3 to your computer and use it in GitHub Desktop.
Save Grdflo/bbcb5726f0a5467e081e189e2da9ecf3 to your computer and use it in GitHub Desktop.
Soundcloud Downloader Clean - Tampermonkey userscript OR bookmarklet

Tampermonkey userscript - Soundcloud Downloader Clean

An ad-less, multilingual, clean Soundcloud downloader with robust code. Adds a 'Download' button to all single track views.

Adds a 'Download' button to all single-track views.

Features:

  • No third-party embeds, redirects or ads, directly uses the Soundcloud API.
  • Works with ad-blocker on.
  • Can be used without Soundcloud account or logged out.
  • Works with all Soundcloud languages.
  • Responsive, blends in with Soundcloud style.
  • To enable debug logging, set debug: true at the start of the userscript.

Note: Soundcloud Go+ tracks will only download the 30 seconds preview sample.

2019-11-14 - v0.1
- Initial release
2020-01-14 - v0.2
- Updated to Soundcloud API v2
- Use Soundcloud download button style
- Updated screenshot and readme
2020-01-14 - v0.2.1
- Removed unnecessary dependency
- Added changelog
// ==UserScript==
// @name Soundcloud Downloader Clean
// @namespace https://openuserjs.org/users/webketje
// @version 0.2.1
// @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#comments
// @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/file-saver@2.0.2/dist/FileSaver.min.js
// ==/UserScript==
/* globals saveAs */
(function() {
'use strict';
var win = unsafeWindow || window;
var containerSelector = '.soundActions.sc-button-toolbar .sc-button-group';
var scdl = {
debug: false,
client_id: '',
dlButtonId: 'scdlc-btn'
};
/**
* @desc Log to console only if debug is true
*/
function log() {
var stamp = new Date().toLocaleString(),
args = [].slice.call(arguments),
prefix = ['SCDLC', stamp, '-'];
if (scdl.debug) console.log.apply(console, prefix.concat(args));
};
/**
* @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);
}
};
};
scdl.getTrackName = function(trackJSON) {
return [
trackJSON.user.username,
trackJSON.title
].join(' - ');
};
scdl.getMediaURL = function(json, onresolve, onerror) {
//if (json.download_url) return onresolve(json.download_url + '&client_id=' + scdl.client_id);
//if (json.stream_url) return onresolve(json.stream_url + '&client_id=' + scdl.client_id);
if (json.media && json.media.transcodings) {
var found = json.media.transcodings.filter(function(tc) {
return tc.format && tc.format.protocol === 'progressive';
})[0];
if (found) {
var xhr = new XMLHttpRequest();
xhr.onload = function() {
var result;
try {
result = JSON.parse(xhr.responseText);
} catch (err) {}
if (result && result.url)
onresolve(result.url);
else
onerror(false);
};
xhr.onerror = onerror;
xhr.open('GET', found.url + '?client_id=' + scdl.client_id);
xhr.send();
} else {
onerror(false);
}
} else {
onerror(false);
}
};
scdl.getStreamURL = function(url, onresolve, onerror) {
var xhr = new XMLHttpRequest();
xhr.onload = function() {
var trackJSON = JSON.parse(xhr.responseText);
scdl.getMediaURL(trackJSON, function resolve(url) {
onresolve({
stream_url: url,
track_name: scdl.getTrackName(trackJSON)
});
}, function reject() {
onerror(false);
})
}.bind(this);
xhr.onerror = function() {
onerror(false);
};
xhr.open('GET', 'https://api-v2.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 sc-button-medium sc-button-responsive sc-button-download";
a.href = href;
a.id = scdl.dlButtonId;
a.textContent = label;
a.title = 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';
a.style.border = '1px solid orangered';
return a;
},
attach:function() {
this.remove();
var f = document.querySelector(containerSelector);
log('Attaching download button', f);
if (f)
f.insertAdjacentElement('beforeend', 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
);
}
},
function onError() {
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);
}
});
if (scdl.debug) win.scdl = scdl;
scdl.getClientID(function(id) {
log('Found Soundcloud client id:', id, '. Initializing...');
scdl.client_id = id;
scdl.load(win.location.href);
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment