Skip to content

Instantly share code, notes, and snippets.

@webketje
Last active October 16, 2025 09:56
Show Gist options
  • Save webketje/8cd2e6ae8a86dbe0533c5d2c612c42c6 to your computer and use it in GitHub Desktop.
Save webketje/8cd2e6ae8a86dbe0533c5d2c612c42c6 to your computer and use it in GitHub Desktop.

Revisions

  1. webketje revised this gist Jan 5, 2024. 3 changed files with 20 additions and 10 deletions.
    11 changes: 7 additions & 4 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -5,9 +5,7 @@ Adds a 'Download' button to all single track views.

    ![](https://gist.github.com/webketje/8cd2e6ae8a86dbe0533c5d2c612c42c6/raw/d1e05661f73e95ab6235fe018e7b8a60fd3e33bf/scdlc.png)

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

    Features:
    ## Features

    * No third-party embeds, redirects or ads, directly uses the Soundcloud API.
    * Works with ad-blocker on.
    @@ -16,4 +14,9 @@ Features:
    * 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.
    ## Caveats

    * Soundcloud Go+ tracks will only download the 30 seconds preview sample.
    * Some tracks on Soundcloud are only provided as AES-encrypted [HLS](https://en.wikipedia.org/wiki/HTTP_Live_Streaming) playlist files. Soundcloud Downloader Clean is not able to download these and will instead display a modal with links to third-party Soundcloud downloaders:

    ![](https://gist.github.com/webketje/8cd2e6ae8a86dbe0533c5d2c612c42c6/raw/fb3b575de753ca56dcc49ac6ac1143332b4b409c/scdlc-modal.png)
    17 changes: 12 additions & 5 deletions changelog.txt
    Original file line number Diff line number Diff line change
    @@ -1,9 +1,16 @@
    2019-11-14 - v0.1
    - Initial release
    2024-01-05 - v1.0.0
    - feat: download button on HLS tracks will open a modal with third-party services
    - fix: don't display download button on user sets
    - fix: wait for soundActions bar to render for 5s
    - added: error log formatting
    - Updated README.md to include caveats
    - Reverse changelog to most recent first
    2020-01-14 - v0.2.1
    - Removed unnecessary dependency
    - Added changelog
    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
    2019-11-14 - v0.1
    - Initial release
    2 changes: 1 addition & 1 deletion scdlc.js
    Original file line number Diff line number Diff line change
    @@ -1,7 +1,7 @@
    // ==UserScript==
    // @name Soundcloud Downloader Clean
    // @namespace https://openuserjs.org/users/webketje
    // @version 0.2.1
    // @version 1.0.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
  2. webketje revised this gist Jan 4, 2024. 1 changed file with 93 additions and 21 deletions.
    114 changes: 93 additions & 21 deletions scdlc.js
    Original file line number Diff line number Diff line change
    @@ -13,7 +13,7 @@
    // @noframes
    // @match https://soundcloud.com/*
    // @grant unsafeWindow
    // @require https://cdn.jsdelivr.net/npm/[email protected].2/dist/FileSaver.min.js
    // @require https://cdn.jsdelivr.net/npm/[email protected].5/dist/FileSaver.min.js
    // ==/UserScript==

    /* globals saveAs */
    @@ -22,7 +22,6 @@
    'use strict';

    var win = unsafeWindow || window;
    var lang = document.documentElement.lang
    var containerSelector = '.soundActions.sc-button-toolbar .sc-button-group';

    var scdl = {
    @@ -32,6 +31,90 @@
    modalId: 'scdl-third-party-modal'
    };

    var labels = ({
    en: {
    download: 'Download',
    downloading: 'Downloading',
    copy: 'Copy',
    copy_success: 'Copied to clipboard',
    copy_failure: 'Failed to copy to clipboard!',
    close: 'Close',
    modal_title: 'could not download this track. Use one of these third-party services instead?'
    },
    es: {
    download: 'Descargar',
    downloading: 'Descargando..',
    copy: 'Copiar',
    copy_success: 'Copiada al portapapeles',
    copy_failure: '¡No se pudo copiar al portapapeles!',
    close: '',
    modal_title: 'no se pudo descargar esta banda sonora. ¿Utilizar uno de estos servicios de terceros en su lugar?'
    },
    fr: {
    download: 'Télécharger',
    downloading: 'Téléchargement..',
    copy: 'Copier',
    copy_success: 'Copié dans le presse-papiers!',
    copy_failure: 'Échec de la copie dans le presse-papiers !',
    close: 'Fermer',
    modal_title: 'ne peut pas télécharger ce fichier. Utiliser l’un de ces services tiers ?'
    },
    nl: {
    download: 'Downloaden',
    downloading: 'Downloaden..',
    copy: 'Kopiëren',
    copy_success: 'Naar klembord gekopieerd!',
    copy_failure: 'Kopiëren naar klembord mislukt!',
    close: 'Sluiten',
    modal_title: 'kon dit bestand niet downloaden. Een van deze externe diensten gebruiken?'
    },
    de: {
    download: 'Herunterladen',
    downloading: 'Herunterladen..',
    copy: 'Kopieren',
    copy_success: 'In die Zwischenablage kopiert',
    copy_failure: 'Kopieren in die Zwischenablage fehlgeschlagen!',
    close: 'Schließen',
    modal_title: 'konnte diesen Sound nicht herunterladen. Nutzen Sie stattdessen einen dieser Drittanbieterdienste?'
    },
    pl: {
    download: 'Ściągnij',
    downloading: 'Ściąganie..',
    copy: 'Kopiuj',
    copy_success: 'Skopiowano do schowka',
    copy_failure: 'Nie udało się skopiować do schowka!!',
    close: 'Zamknij',
    modal_title: 'nie udało się pobrać tego utworu. Zamiast tego skorzystać z jednej z usług stron trzecich?'
    },
    it: {
    download: 'Scaricare',
    downloading: 'Scaricando..',
    copy: 'Copia',
    copy_success: 'Copiato negli appunti',
    copy_failure: 'Impossibile copiare negli appunti!',
    close: 'Chiudi',
    modal_title: 'non è stato possibile scaricare questo suono. Utilizzi invece uno di questi servizi di terze parti?'
    },
    pt_BR: {
    download: 'Baixar',
    downloading: 'Baixando..',
    copy: 'Copiar',
    copy_success: 'Copiado para a área de transferência',
    copy_failure: 'Falha ao copiar para a área de transferência!!',
    close: 'Fechar',
    modal_title: 'não foi possível baixar este som. Usar um desses serviços de terceiros?'
    },
    sv: {
    download: 'Ladda ner',
    downloading: 'Laddar ner..',
    copy: 'Kopiera',
    copy_success: 'Kopierat till urklipp',
    copy_failure: 'Det gick inte att kopiera till urklipp!',
    close: 'Stäng',
    modal_title: 'han kunde inte ladda ner det här ljudet. Använd någon av dessa tredjepartstjänster istället?'
    }
    })[document.documentElement.lang || 'en']

    /**
    * @desc Log to console only if debug is true
    */
    @@ -117,32 +200,21 @@
    };

    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();
    var dlButton = document.getElementById(scdl.dlButtonId)
    if (dlButton) {
    dlButton.textContent = 'Downloading...'
    dlButton.textContent = labels.downloading;
    }
    setTimeout(function() {
    saveAs(e.target.href, e.target.dataset.title);
    if (dlButton) {
    dlButton.textContent = scdl.button.label[lang]
    dlButton.textContent = labels.download;
    }
    }, 0)
    }, 100)
    },
    render: function(href, title, onClick) {
    var label = scdl.button.label[lang];
    var label = labels.download;
    var a = document.createElement('a');
    a.className = "sc-button sc-button-medium sc-button-responsive sc-button-download";
    a.href = href;
    @@ -194,17 +266,17 @@
    const html = [
    '<div class="modal g-z-index-modal-background g-opacity-transition g-z-index-overlay modalWhiteout showBackground g-backdrop-filter-grayscale" style="outline: none; padding-right: 0px; display: flex; justify-content: center;" tabindex="-1" id="scdl-third-party-modal">',
    '<div class="modal__modal sc-border-box g-z-index-modal-content transparentBackground" style="height: auto;">',
    '<button type="button" title="Close" class="modal__closeButton">Close</button>',
    '<button type="button" title="' + labels.close + '" class="modal__closeButton">' + labels.close + '</button>',
    '<div class="modal__content"><div class="tabs"><div class="tabs__content"><div class="tabs__contentSlot" style="display: block;"><article class="shareContent">',
    '<div class="publicShare"><section class="g-modal-section sc-clearfix sc-pt-2x">',
    '<h2 class="sc-orange">Soundcloud Downloader Clean could not download this track. Use one of these third-party services instead?</h2>',
    '<h2 class="sc-orange">Soundcloud Downloader Clean ' + labels.modal_title + '</h2>',
    '</section><section class="g-modal-section sc-clearfix sc-pt-2x">',
    '<h3 style="margin-bottom: 0.5rem;">Download <em>' + title + '</em> via: </h3>',
    '<h3 style="margin-bottom: 0.5rem;">' + labels.download + ' <em>' + title + '</em> via: </h3>',
    this.providers.map(p => ['<div><a href="', win.atob(p), '" target="_blank" style="display: inline-block; font-size: 14px; padding: 0.25rem 0;">', win.atob(p), '</a></div>'].join('')).join(''),
    '<div class="shareLink sc-clearfix publicShare__link sc-pt-2x m-showPositionOption" style="margin-top: 1rem;">',
    '<label for="shareLink__field" style="margin-right:0.5rem;">Link</label>',
    '<input type="text" value="' + win.location.href + '" class="shareLink__field sc-input" id="shareLink__field" readonly="readonly">',
    '<button class="sc-button sc-button-copy">Copy</button>',
    '<button class="sc-button sc-button-copy">' + labels.copy + '</button>',
    '<span class="sc-copy-feedback" style="margin-left: 1rem;"></span>',
    '</div>',
    '</section></div></article></div></div></div></div></div></div>'
  3. webketje revised this gist Dec 27, 2023. 2 changed files with 75 additions and 4 deletions.
    Binary file added scdlc-modal.png
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.
    79 changes: 75 additions & 4 deletions scdlc.js
    Original file line number Diff line number Diff line change
    @@ -22,12 +22,14 @@
    'use strict';

    var win = unsafeWindow || window;
    var lang = document.documentElement.lang
    var containerSelector = '.soundActions.sc-button-toolbar .sc-button-group';

    var scdl = {
    debug: false,
    client_id: '',
    dlButtonId: 'scdlc-btn'
    dlButtonId: 'scdlc-btn',
    modalId: 'scdl-third-party-modal'
    };

    /**
    @@ -128,10 +130,19 @@
    },
    download: function(e) {
    e.preventDefault();
    saveAs(e.target.href, e.target.dataset.title);
    var dlButton = document.getElementById(scdl.dlButtonId)
    if (dlButton) {
    dlButton.textContent = 'Downloading...'
    }
    setTimeout(function() {
    saveAs(e.target.href, e.target.dataset.title);
    if (dlButton) {
    dlButton.textContent = scdl.button.label[lang]
    }
    }, 0)
    },
    render: function(href, title, onClick) {
    var label = scdl.button.label[document.documentElement.lang];
    var label = scdl.button.label[lang];
    var a = document.createElement('a');
    a.className = "sc-button sc-button-medium sc-button-responsive sc-button-download";
    a.href = href;
    @@ -172,6 +183,62 @@
    }
    };

    scdl.modal = {
    providers: [
    'aHR0cHM6Ly9zY2xvdWRkb3dubG9hZGVyLm5ldA==',
    'aHR0cHM6Ly93d3cuc291bmRjbG91ZG1wMy5vcmc=',
    'aHR0cHM6Ly9zb3VuZGNsb3VkbWUuY29t'
    ],
    render: function(title) {
    var temp = document.createElement('div'), self = this
    const html = [
    '<div class="modal g-z-index-modal-background g-opacity-transition g-z-index-overlay modalWhiteout showBackground g-backdrop-filter-grayscale" style="outline: none; padding-right: 0px; display: flex; justify-content: center;" tabindex="-1" id="scdl-third-party-modal">',
    '<div class="modal__modal sc-border-box g-z-index-modal-content transparentBackground" style="height: auto;">',
    '<button type="button" title="Close" class="modal__closeButton">Close</button>',
    '<div class="modal__content"><div class="tabs"><div class="tabs__content"><div class="tabs__contentSlot" style="display: block;"><article class="shareContent">',
    '<div class="publicShare"><section class="g-modal-section sc-clearfix sc-pt-2x">',
    '<h2 class="sc-orange">Soundcloud Downloader Clean could not download this track. Use one of these third-party services instead?</h2>',
    '</section><section class="g-modal-section sc-clearfix sc-pt-2x">',
    '<h3 style="margin-bottom: 0.5rem;">Download <em>' + title + '</em> via: </h3>',
    this.providers.map(p => ['<div><a href="', win.atob(p), '" target="_blank" style="display: inline-block; font-size: 14px; padding: 0.25rem 0;">', win.atob(p), '</a></div>'].join('')).join(''),
    '<div class="shareLink sc-clearfix publicShare__link sc-pt-2x m-showPositionOption" style="margin-top: 1rem;">',
    '<label for="shareLink__field" style="margin-right:0.5rem;">Link</label>',
    '<input type="text" value="' + win.location.href + '" class="shareLink__field sc-input" id="shareLink__field" readonly="readonly">',
    '<button class="sc-button sc-button-copy">Copy</button>',
    '<span class="sc-copy-feedback" style="margin-left: 1rem;"></span>',
    '</div>',
    '</section></div></article></div></div></div></div></div></div>'
    ].join('')
    temp.innerHTML = html
    var cnt = temp.firstElementChild
    cnt.addEventListener('click', function(e) {
    if (this === e.target || e.target.classList.contains('modal__closeButton')) {
    self.remove()
    } else if (e.target.classList.contains('sc-button-copy')) {
    navigator.clipboard.writeText(win.location.href)
    .then(function() {
    var f = cnt.querySelector('.sc-copy-feedback')
    f.innerHTML = '<span style="color: green;">Copied to clipboard!</span>'
    }, function(err) {
    log('Failed to write URL to the clipboard.', err)
    var f = cnt.querySelector('.sc-copy-feedback')
    f.innerHTML = '<span style="color: red;">Failed to copy to clipboard!</span>'
    })
    }
    })
    return cnt
    },
    attach: function() {
    this.remove()
    document.body.appendChild(this.render.apply(this, arguments))
    },
    remove: function() {
    var modal = document.getElementById(scdl.modalId);
    if (modal)
    modal.parentNode.removeChild(modal);
    }
    }

    scdl.parseClientIdFromURL = function(url) {
    var search = /client_id=([\w\d]+)&*/;
    return url && url.match(search) && url.match(search)[1];
    @@ -205,7 +272,11 @@
    },
    function onError() {
    log('%c No compatible media transcoding found.', 'color: #FF0000;');
    scdl.button.remove();
    scdl.button.attach('javascript:void(0);', 'None', function() {
    var title = document.querySelector('.soundTitle__title')
    var artist = document.querySelector('.soundTitle__username')
    scdl.modal.attach([artist.textContent.trim(), '-', title.textContent.trim()].join(' '))
    })
    }
    );
    };
  4. webketje revised this gist Dec 27, 2023. 1 changed file with 4 additions and 4 deletions.
    8 changes: 4 additions & 4 deletions scdlc.js
    Original file line number Diff line number Diff line change
    @@ -154,12 +154,12 @@
    var intv = setInterval(function() {
    var f = document.querySelector(containerSelector)
    iterations++
    if (f) {
    if (f && !document.getElementById(scdl.dlButtonId)) {
    f.insertAdjacentElement('beforeend', self.render.apply(self, args));
    log('Attaching download button to element:', f)
    clearInterval(intv)
    // stop after trying to find the element 20x
    } else if (iterations === 20) {
    // stop after trying to find the element for 5s
    } else if (iterations === 50) {
    log('%c Couldn\'t find element "' + containerSelector + '" after 2 seconds', 'color: #FF0000;')
    clearInterval(intv)
    }
    @@ -185,7 +185,7 @@

    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)) {
    if (/^(\/(you|stations|discover|stream|upload|search|settings|.+?\/sets))/.test(win.location.pathname)) {
    scdl.button.remove();
    return;
    }
  5. webketje revised this gist Dec 26, 2023. 1 changed file with 20 additions and 9 deletions.
    29 changes: 20 additions & 9 deletions scdlc.js
    Original file line number Diff line number Diff line change
    @@ -23,6 +23,7 @@

    var win = unsafeWindow || window;
    var containerSelector = '.soundActions.sc-button-toolbar .sc-button-group';

    var scdl = {
    debug: false,
    client_id: '',
    @@ -35,8 +36,8 @@
    function log() {
    var stamp = new Date().toLocaleString(),
    args = [].slice.call(arguments),
    prefix = ['SCDLC', stamp, '-'];
    if (scdl.debug) console.log.apply(console, prefix.concat(args));
    prefix = ['SCDLC', stamp, '-'].join(' ');
    if (scdl.debug) console.log.apply(console, [prefix + args[0]].concat(args.slice(1)));
    };

    /**
    @@ -66,8 +67,6 @@
    };

    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';
    @@ -149,11 +148,22 @@
    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));
    var args = arguments, self = this, iterations = 0

    // account for rendering delays
    var intv = setInterval(function() {
    var f = document.querySelector(containerSelector)
    iterations++
    if (f) {
    f.insertAdjacentElement('beforeend', self.render.apply(self, args));
    log('Attaching download button to element:', f)
    clearInterval(intv)
    // stop after trying to find the element 20x
    } else if (iterations === 20) {
    log('%c Couldn\'t find element "' + containerSelector + '" after 2 seconds', 'color: #FF0000;')
    clearInterval(intv)
    }
    }, 100)
    },
    remove: function() {
    var btn = document.getElementById(scdl.dlButtonId);
    @@ -194,6 +204,7 @@
    }
    },
    function onError() {
    log('%c No compatible media transcoding found.', 'color: #FF0000;');
    scdl.button.remove();
    }
    );
  6. webketje revised this gist Jan 13, 2020. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion scdlc.js
    Original file line number Diff line number Diff line change
    @@ -1,7 +1,7 @@
    // ==UserScript==
    // @name Soundcloud Downloader Clean
    // @namespace https://openuserjs.org/users/webketje
    // @version 0.2
    // @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
  7. webketje revised this gist Jan 13, 2020. 3 changed files with 11 additions and 2 deletions.
    1 change: 1 addition & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -14,5 +14,6 @@ Features:
    * 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.
    9 changes: 9 additions & 0 deletions changelog.txt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,9 @@
    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
    3 changes: 1 addition & 2 deletions scdlc.js
    Original file line number Diff line number Diff line change
    @@ -14,7 +14,6 @@
    // @match https://soundcloud.com/*
    // @grant unsafeWindow
    // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/FileSaver.min.js
    // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/polyfill.min.js
    // ==/UserScript==

    /* globals saveAs */
    @@ -25,7 +24,7 @@
    var win = unsafeWindow || window;
    var containerSelector = '.soundActions.sc-button-toolbar .sc-button-group';
    var scdl = {
    debug: true,
    debug: false,
    client_id: '',
    dlButtonId: 'scdlc-btn'
    };
  8. webketje revised this gist Jan 13, 2020. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -3,7 +3,7 @@
    An ad-less, multilingual, clean Soundcloud downloader with robust code.
    Adds a 'Download' button to all single track views.

    ![](https://gist.github.com/webketje/8cd2e6ae8a86dbe0533c5d2c612c42c6/raw/cba5f4524b6ecba55819af3f7b16ab3a38408b46/scdlc.png)
    ![](https://gist.github.com/webketje/8cd2e6ae8a86dbe0533c5d2c612c42c6/raw/d1e05661f73e95ab6235fe018e7b8a60fd3e33bf/scdlc.png)

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

  9. webketje revised this gist Jan 13, 2020. 3 changed files with 72 additions and 21 deletions.
    16 changes: 14 additions & 2 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,18 @@
    # Tampermonkey userscript - Soundcloud Downloader Clean

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

    ![](https://i.imgur.com/DsqGjv3.png)
    ![](https://gist.github.com/webketje/8cd2e6ae8a86dbe0533c5d2c612c42c6/raw/cba5f4524b6ecba55819af3f7b16ab3a38408b46/scdlc.png)

    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.

    **Note:** Soundcloud Go+ tracks will only download the 30 seconds preview sample.
    77 changes: 58 additions & 19 deletions scdlc.js
    Original file line number Diff line number Diff line change
    @@ -1,19 +1,20 @@
    // ==UserScript==
    // @name Soundcloud Downloader Clean
    // @namespace https://openuserjs.org/users/webketje
    // @version 0.1.0
    // @version 0.2
    // @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
    // @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
    // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/polyfill.min.js
    // ==/UserScript==

    /* globals saveAs */
    @@ -22,7 +23,12 @@
    'use strict';

    var win = unsafeWindow || window;
    var containerSelector = '.listenEngagement__footer .sc-button-toolbar';
    var containerSelector = '.soundActions.sc-button-toolbar .sc-button-group';
    var scdl = {
    debug: true,
    client_id: '',
    dlButtonId: 'scdlc-btn'
    };

    /**
    * @desc Log to console only if debug is true
    @@ -31,7 +37,7 @@
    var stamp = new Date().toLocaleString(),
    args = [].slice.call(arguments),
    prefix = ['SCDLC', stamp, '-'];
    scdl.debug && console.log(prefix.concat(args).join(' '));
    if (scdl.debug) console.log.apply(console, prefix.concat(args));
    };

    /**
    @@ -53,32 +59,60 @@
    };
    };

    var scdl = {
    debug: false,
    client_id: '',
    dlButtonId: 'scdlc-btn'
    };

    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);
    onresolve(trackJSON.errors || !trackJSON.stream_url ? false : {
    stream_url: trackJSON.stream_url + '?client_id=' + this.client_id,
    track_name: this.getTrackName(trackJSON)
    });
    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.soundcloud.com/resolve?url=' + encodeURIComponent(url) + '&client_id=' + this.client_id);
    xhr.open('GET', 'https://api-v2.soundcloud.com/resolve?url=' + encodeURIComponent(url) + '&client_id=' + this.client_id);
    xhr.send();
    };

    @@ -101,23 +135,26 @@
    render: function(href, title, onClick) {
    var label = scdl.button.label[document.documentElement.lang];
    var a = document.createElement('a');
    a.className = "sc-button";
    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('afterend', this.render.apply(this, arguments));
    f.insertAdjacentElement('beforeend', this.render.apply(this, arguments));
    },
    remove: function() {
    var btn = document.getElementById(scdl.dlButtonId);
    @@ -157,7 +194,9 @@
    );
    }
    },
    scdl.button.remove
    function onError() {
    scdl.button.remove();
    }
    );
    };

    @@ -169,7 +208,7 @@
    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;
    Binary file added scdlc.png
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.
  10. webketje revised this gist Nov 11, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -3,4 +3,4 @@
    An ad-less, multilingual, clean Soundcloud downloader with robust code.
    Adds a 'Download' button to all single track views.

    ![](https://imgur.com/DsqGjv3)
    ![](https://i.imgur.com/DsqGjv3.png)
  11. webketje revised this gist Nov 11, 2019. 1 changed file with 3 additions and 1 deletion.
    4 changes: 3 additions & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,6 @@
    # 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.

    ![](https://imgur.com/DsqGjv3)
  12. webketje revised this gist Nov 11, 2019. 1 changed file with 3 additions and 1 deletion.
    4 changes: 3 additions & 1 deletion scdlc.js
    Original file line number Diff line number Diff line change
    @@ -2,12 +2,14 @@
    // @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 to all single track views.
    // @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
  13. webketje revised this gist Nov 11, 2019. 1 changed file with 6 additions and 2 deletions.
    8 changes: 6 additions & 2 deletions scdlc.js
    Original file line number Diff line number Diff line change
    @@ -1,9 +1,13 @@
    // ==UserScript==
    // @name Soundcloud Downloader Clean
    // @namespace http://tampermonkey.net/
    // @version 0.1
    // @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 to all single track views.
    // @author webketje
    // @license MIT
    // @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
    // @noframes
    // @match https://soundcloud.com/*
    // @grant unsafeWindow
  14. webketje created this gist Nov 11, 2019.
    4 changes: 4 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,4 @@
    # Tampermonkey userscript - Soundcloud Downloader Clean

    An ad-less, multilingual, clean Soundcloud downloader with robust code.
    Adds a 'Download' button to all single track views.
    172 changes: 172 additions & 0 deletions scdlc.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,172 @@
    // ==UserScript==
    // @name Soundcloud Downloader Clean
    // @namespace http://tampermonkey.net/
    // @version 0.1
    // @description An ad-less, multilingual, clean Soundcloud downloader with robust code. Adds a 'Download' button to all single track views.
    // @author webketje
    // @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);
    });
    })();