|
// ==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/[email protected]/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); |
|
}); |
|
})(); |