Soundcloud Downloader Clean


SUBMITTED BY: ccont2246

DATE: Dec. 22, 2022, 2:30 p.m.

FORMAT: Text only

SIZE: 7.3 kB

HITS: 40138

  1. // ==UserScript==
  2. // @name Soundcloud Downloader Clean
  3. // @namespace https://openuserjs.org/users/webketje
  4. // @version 0.2.1
  5. // @description An ad-less, multilingual, clean Soundcloud downloader with robust code. Adds a 'Download' button in the toolbar of all single track views.
  6. // @author webketje
  7. // @license MIT
  8. // @icon https://a-v2.sndcdn.com/assets/images/sc-icons/favicon-2cadd14bdb.ico
  9. // @homepageURL https://gist.github.com/webketje/8cd2e6ae8a86dbe0533c5d2c612c42c6
  10. // @supportURL https://gist.github.com/webketje/8cd2e6ae8a86dbe0533c5d2c612c42c6#comments
  11. // @noframes
  12. // @match https://soundcloud.com/*
  13. // @grant unsafeWindow
  14. // @require https://cdn.jsdelivr.net/npm/file-saver@2.0.2/dist/FileSaver.min.js
  15. // ==/UserScript==
  16. /* globals saveAs */
  17. (function() {
  18. 'use strict';
  19. var win = unsafeWindow || window;
  20. var containerSelector = '.soundActions.sc-button-toolbar .sc-button-group';
  21. var scdl = {
  22. debug: false,
  23. client_id: '',
  24. dlButtonId: 'scdlc-btn'
  25. };
  26. /**
  27. * @desc Log to console only if debug is true
  28. */
  29. function log() {
  30. var stamp = new Date().toLocaleString(),
  31. args = [].slice.call(arguments),
  32. prefix = ['SCDLC', stamp, '-'];
  33. if (scdl.debug) console.log.apply(console, prefix.concat(args));
  34. };
  35. /**
  36. * @desc There is no other way to retrieve a Soundcloud client_id than by spying on existing requests.
  37. * We temporarily patch the XHR.send method to retrieve the url passed to it.
  38. * @param restoreIfTrue - restores the original prototype method when true is returned
  39. * @param onRestore - a function to exec when the restoreIfTrue condition is met
  40. */
  41. function patchXHR(restoreIfTrue, onRestore) {
  42. var originalXHR = win.XMLHttpRequest.prototype.open;
  43. win.XMLHttpRequest.prototype.open = function() {
  44. originalXHR.apply(this, arguments);
  45. var restore = restoreIfTrue.apply(this, arguments);
  46. if (restore) {
  47. win.XMLHttpRequest.prototype.open = originalXHR;
  48. onRestore(restore);
  49. }
  50. };
  51. };
  52. scdl.getTrackName = function(trackJSON) {
  53. return [
  54. trackJSON.user.username,
  55. trackJSON.title
  56. ].join(' - ');
  57. };
  58. scdl.getMediaURL = function(json, onresolve, onerror) {
  59. //if (json.download_url) return onresolve(json.download_url + '&client_id=' + scdl.client_id);
  60. //if (json.stream_url) return onresolve(json.stream_url + '&client_id=' + scdl.client_id);
  61. if (json.media && json.media.transcodings) {
  62. var found = json.media.transcodings.filter(function(tc) {
  63. return tc.format && tc.format.protocol === 'progressive';
  64. })[0];
  65. if (found) {
  66. var xhr = new XMLHttpRequest();
  67. xhr.onload = function() {
  68. var result;
  69. try {
  70. result = JSON.parse(xhr.responseText);
  71. } catch (err) {}
  72. if (result && result.url)
  73. onresolve(result.url);
  74. else
  75. onerror(false);
  76. };
  77. xhr.onerror = onerror;
  78. xhr.open('GET', found.url + '?client_id=' + scdl.client_id);
  79. xhr.send();
  80. } else {
  81. onerror(false);
  82. }
  83. } else {
  84. onerror(false);
  85. }
  86. };
  87. scdl.getStreamURL = function(url, onresolve, onerror) {
  88. var xhr = new XMLHttpRequest();
  89. xhr.onload = function() {
  90. var trackJSON = JSON.parse(xhr.responseText);
  91. scdl.getMediaURL(trackJSON, function resolve(url) {
  92. onresolve({
  93. stream_url: url,
  94. track_name: scdl.getTrackName(trackJSON)
  95. });
  96. }, function reject() {
  97. onerror(false);
  98. })
  99. }.bind(this);
  100. xhr.onerror = function() {
  101. onerror(false);
  102. };
  103. xhr.open('GET', 'https://api-v2.soundcloud.com/resolve?url=' + encodeURIComponent(url) + '&client_id=' + this.client_id);
  104. xhr.send();
  105. };
  106. scdl.button = {
  107. label: {
  108. en: 'Download',
  109. es: 'Descargar',
  110. fr: 'Télécharger',
  111. nl: 'Download',
  112. de: 'Download',
  113. pl: 'Ściągnij',
  114. it: 'Scaricare',
  115. pt_BR: 'Baixar',
  116. sv: 'Ladda ner'
  117. },
  118. download: function(e) {
  119. e.preventDefault();
  120. saveAs(e.target.href, e.target.dataset.title);
  121. },
  122. render: function(href, title, onClick) {
  123. var label = scdl.button.label[document.documentElement.lang];
  124. var a = document.createElement('a');
  125. a.className = "sc-button sc-button-medium sc-button-responsive sc-button-download";
  126. a.href = href;
  127. a.id = scdl.dlButtonId;
  128. a.textContent = label;
  129. a.title = label;
  130. a.dataset.title = title + '.mp3';
  131. a.setAttribute('download', title + '.mp3');
  132. a.target = '_blank';
  133. a.onclick = onClick;
  134. a.style.marginLeft = '5px';
  135. a.style.cssFloat = 'left';
  136. a.style.border = '1px solid orangered';
  137. return a;
  138. },
  139. attach:function() {
  140. this.remove();
  141. var f = document.querySelector(containerSelector);
  142. log('Attaching download button', f);
  143. if (f)
  144. f.insertAdjacentElement('beforeend', this.render.apply(this, arguments));
  145. },
  146. remove: function() {
  147. var btn = document.getElementById(scdl.dlButtonId);
  148. if (btn)
  149. btn.parentNode.removeChild(btn);
  150. }
  151. };
  152. scdl.parseClientIdFromURL = function(url) {
  153. var search = /client_id=([\w\d]+)&*/;
  154. return url && url.match(search) && url.match(search)[1];
  155. };
  156. scdl.getClientID = function(onClientIDFound) {
  157. patchXHR(function(method, url) {
  158. return scdl.parseClientIdFromURL(url);
  159. }, onClientIDFound);
  160. };
  161. scdl.load = function(url) {
  162. // for now only make available for single track pages
  163. if (/^(\/(you|stations|discover|stream|upload|search|settings))/.test(win.location.pathname)) {
  164. scdl.button.remove();
  165. return;
  166. }
  167. scdl.getStreamURL(url,
  168. function onSuccess(result) {
  169. if (!result) {
  170. scdl.button.remove();
  171. } else {
  172. log('Detected valid Soundcloud artist track URL. Requesting info...');
  173. scdl.button.attach(
  174. result.stream_url,
  175. result.track_name,
  176. scdl.button.download
  177. );
  178. }
  179. },
  180. function onError() {
  181. scdl.button.remove();
  182. }
  183. );
  184. };
  185. // patch front-end navigation
  186. ['pushState','replaceState','forward','back','go'].forEach(function(event) {
  187. var tmp = win.history.pushState;
  188. win.history[event] = function() {
  189. tmp.apply(win.history, arguments);
  190. scdl.load(win.location.href);
  191. }
  192. });
  193. if (scdl.debug) win.scdl = scdl;
  194. scdl.getClientID(function(id) {
  195. log('Found Soundcloud client id:', id, '. Initializing...');
  196. scdl.client_id = id;
  197. scdl.load(win.location.href);
  198. });
  199. })();

comments powered by Disqus