// jshint browser: true, jquery: true
// ==UserScript==
// @name SoundCloud Downloader
// @namespace http://www.dieterholvoet.com
// @author Dieter Holvoet
// @description Adds a direct download button to all the tracks on SoundCloud (works with the new SoundCloud interface)
// @require https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js
// @include http://www.soundcloud.com/*
// @include http://soundcloud.com/*
// @include https://www.soundcloud.com/*
// @include https://soundcloud.com/*
// @grant GM_addStyle
// @grant GM_openInTab
// @version 1.2
// ==/UserScript==
//-----------------------------------------------------------------------------------
jQuery.noConflict();
(function ($) {
$(function () {
$.fn.exists = function () {
return this.length !== 0;
};
/** Client ID **/
var clientId = 'DQskPX1pntALRzMp4HSxya3Mc0AO66Ro';
/** Append stylesheet */
var icon_buy = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAQAAACR313BAAAAg0lEQVQY02NgIAzU/qtuxSP9/4zOdxkhPAqcPqr9R4Uo5gXdRZd2eIwknXoAXXrZJiTp0tmokl6f/hsjSXfm4dHLwLDZUv+3y5fYN5VPZ907cO3leRS9QK/x/d/7f8H/5v9p/z3+a//nw/Ca6laYVxAsJOB6z/UeOgvZ+An/J6CzKAUAAatiLSilSl4AAAAASUVORK5CYII=";
GM_addStyle(
".sc-button-small.sc-button-buy:before, .sc-button-medium.sc-button-buy:before {" +
"background-image: url("+icon_buy+")" +
"}"
);
/** Append download buttons */
setInterval(function () {
/**
* Playlist page
* - with official downloads: e.g. https://soundcloud.com/alexdoanofficial/sets/into-the-void-ep
* - without official downloads: e.g. https://soundcloud.com/mule-z/sets/tropical
* */
$(".trackList").find(".trackList__item").each(function () {
var $item = $(this).find(".trackItem__trackTitle").eq(0),
data = {
title: cleanTitle($item.text()),
url: $item.attr("href"),
id: "" // TODO: Fetch ID
},
track = new SoundCloudTrack($(this), data);
track.appendButton('small', true);
});
/**
* Track page
* e.g. https://soundcloud.com/mkjaff/dyrisk-the-tallest-man-mkj-remix
* */
if($(".listenDetails .commentsList").exists()) {
var $el = ($(".listenEngagement__footer").exists() ? $(".listenEngagement__footer") : $(".sound__footer")),
data = {
title: cleanTitle($(".soundTitle__title").eq(0).text()),
url: document.location.href,
id: "" // TODO: Fetch ID
},
track = new SoundCloudTrack($el, data);
track.appendButton('medium', false);
}
/**
* Homepage
* https://soundcloud.com/stream
*
* Likes
* https://soundcloud.com/you/likes
*
* Overview
* https://soundcloud.com/you/collection
*
* User tracks page
* https://soundcloud.com/lexer/tracks
* */
$(".lazyLoadingList").find(".soundList__item").each(function () {
if (!$(this).find(".sound").is(".playlist")) {
var data = {
title: cleanTitle($(this).find(".soundTitle__title").eq(0).text()),
url: $(this).find(".soundTitle__title").eq(0).attr("href"),
id: ""
},
track = new SoundCloudTrack($(this), data);
track.appendButton('small', false);
}
});
/**
* Charts
* e.g. https://soundcloud.com/charts/top
* */
$(".chartTracks").find(".chartTracks__item > .chartTrack").each(function () {
var $item = $(this).find(".chartTrack__title a").eq(0),
data = {
title: $item.text(),
url: $item.attr("href")
},
track = new SoundCloudTrack($(this), data);
track.appendButton('small', true);
});
/**
* Play history
* https://soundcloud.com/you/history
* */
$(".historicalPlays").find(".historicalPlays__item").each(function () {
var $item = $(this).find("a.soundTitle__title").eq(0),
data = {
title: cleanTitle($item.text()),
url: $item.attr("href")
},
track = new SoundCloudTrack($(this), data);
track.appendButton('small', false);
});
/**
* User profile page
* e.g. https://soundcloud.com/kiyokomusik
* */
$(".userStream").find(".soundList__item > .userStreamItem").each(function () {
if (!$(this).find(".sound").is(".playlist")) {
var data = {
title: cleanTitle($(this).find(".soundTitle__title").eq(0).text()),
url: $(this).find(".soundTitle__title").eq(0).attr("href")
},
track = new SoundCloudTrack($(this), data);
track.appendButton('small', false);
}
});
/**
* Search page
* e.g. https://soundcloud.com/search?q=addal
* */
$(".searchList").find(".searchList__item").each(function () {
if ($(this).find(".sound").is(".track")) {
var $item = $(this).find(".soundTitle__title").eq(0),
data = {
title: $item.text(),
url: $item.attr("href")
},
track = new SoundCloudTrack($(this), data);
track.appendButton('small', false);
} else if ($(this).find(".sound").is(".playlist")) {
// TO DO: Download playlist
}
});
}, 2000);
function SoundCloudGritter(track, title, isError, timeout) {
var $wrapper = $("#gritter-notice-wrapper"),
$gritters = $(".gritter-item-wrapper"),
id = 'gritter-item-'+$gritters.length+1,
$gritter = $('<div id="'+id+'" class="gritter-item-wrapper'+(isError ? ' error' : '')+'"><div class="gritter-top"></div><div class="gritter-item"><div class="gritter-close" style="display: none;"></div>'+(isError && track.findID() ? '' : '<img src="'+track.getArtworkURL(50)+'" class="gritter-image">')+'<div class="gritter-with-image">'+title+'<div style="clear:both"></div></div><div class="gritter-bottom"></div></div>');
if(!$wrapper.exists()) {
$(document).find('body').append('<div id="gritter-notice-wrapper" class="top-right"></div>');
$wrapper = $($wrapper.selector)
}
$wrapper.append($gritter);
setTimeout(function() {
$("#"+id).fadeOut();
}, timeout);
}
function SoundCloudTrack($el, data) {
// Set $el
this.$el = $el;
// Set title
if('title' in data)
this.title = cleanTitle(data.title);
else
console.error("Missing title.", this);
// Set url
if('url' in data && isValidTrackURL(cleanURL(data.url)))
this.url = cleanURL(data.url);
else
console.error("Missing or invalid track url.", this);
// Set id
data.id = this.findID();
if('id' in data)
this.id = data.id;
else
console.error("Couldn't find the ID of this song: ", this);
}
SoundCloudTrack.prototype.findButtonGroup = function() {
var $small = this.$el.find('.soundActions .sc-button-group-small'),
$medium = this.$el.find('.soundActions .sc-button-group-medium');
if($small.exists()) {
return $small;
} else if($medium.exists()) {
return $medium;
} else {
return false;
}
};
SoundCloudTrack.prototype.findID = function() {
if(exists(this.id))
return this.id;
var id = false,
track = this;
this.$el.find(".sc-artwork").each(function() {
var bg = $(this).css("background-image"),
results = /artworks-([a-zA-Z0-9]+)-/.exec(bg);
if(results != null && results.length > 0) {
id = results[1];
}
});
if(id) {
this.id = id;
}
return id;
};
SoundCloudTrack.prototype.makeDownloadButton = function(url, size, isIconOnly, isExternal) {
var $button = $('<a class="sc-button sc-button-'+size+' sc-button-responsive sc-button-download'+(isIconOnly ? ' sc-button-icon' : '')+'" sc-id="'+this.id+'" title="Download ' + this.title + '" >Download'+ (isExternal ? ' (external)' : '') +'</a>'),
track = this;
// Remove exit.sc from URL
if(url.indexOf("exit.sc") !== -1) {
url = (new URL(url).search.match(/(?:\?|&)url=([^&]+)/) || [])[1];
url = decodeURIComponent(url);
}
if(!isExternal && isValidTrackURL(url)) {
url = "https://api.soundcloud.com/resolve.json?client_id=" + clientId + "&url=" + url;
$button.on("click", function() {
var id = $(this).attr('sc-id');
if(exists(id))
new SoundCloudGritter(track, 'Download of <span class="gritter-title">'+track.title+'</span> will start in a moment.</div>', false, 3000);
$.get(url, function (data) {
if (data.hasOwnProperty('error') || !data.hasOwnProperty('stream_url')) {
var message = data.error;
if(track.isGeoblocked())
message = "not available in your country.";
else if(track.isGO())
message = "only for SoundCloud GO users.";
new SoundCloudGritter(track, 'Download of <span class="gritter-title">'+track.title+'</span> failed: '+message, true, 8000);
console.error("Download failed: " + message, track);
} else {
downloadUrl(data.stream_url + '?client_id=' + clientId);
}
}, "json");
});
} else {
$button.attr("href", url);
$button.attr("target", '_blank');
}
this.findButtonGroup().eq(0).append($button);
return $button;
};
SoundCloudTrack.prototype.makeBuyButton = function(url, size, iconOnly) {
var $button = $('<a href="'+url+'" target="_blank" class="sc-button sc-button-'+size+' sc-button-responsive sc-button-buy'+(iconOnly ? ' sc-button-icon' : '')+'" title="Buy ' + this.title + '" >Buy</a>');
this.findButtonGroup().eq(0).append($button);
return $button;
};
SoundCloudTrack.prototype.appendButton = function(size, iconOnly) {
// Set checked
if(this.isChecked())
return;
else
this.setChecked(true);
/** Find and check button group **/
if(!this.findButtonGroup()) {
if(this.isPreview()) {
console.error("Track is preview-only, can't be downloaded: " + url);
} else if(this.isGeoblocked()) {
console.error("Track is geoblocked, can't be downloaded: " + url);
} else {
console.error("No button-group found. Please verify selector.");
}
return;
}
// Append download button
if(this.findButtonGroup().find(".sc-button-download").exists()) {
/** Check presence of download button */
// console.error("Download button already present.");
} else if(this.findExternalFreeDownload()) {
/** Check presence of external free download link */
this.makeDownloadButton(this.findExternalFreeDownload().prop('href'), size, iconOnly, true);
this.findExternalFreeDownload().remove();
} else {
/** Fetch download URL */
this.makeDownloadButton(this.url, size, iconOnly, false);
}
// Append buy button
var $external = this.findExternalBuyLink();
if($external) {
this.makeBuyButton($external.prop('href'), size, iconOnly);
$external.remove();
}
};
SoundCloudTrack.prototype.findExternalFreeDownload = function() {
if(exists(this.$freedl))
return this.$freedl;
var $freedl = this.$el.parent().find('.soundActions__purchaseLink').eq(0),
strings = ['free download', 'free dl'],
websites = ['theartistunion', 'toneden', 'artistsunlimited.co', 'melodicsoundsnetwork.com', 'edmlead.net', 'click.dj', 'woox.agency', 'hypeddit.com', 'hive.co'],
hasExternalFreeDownload = false;
if($freedl.exists()) {
strings.forEach(function(elem) {
if($freedl.text().toLowerCase().indexOf(elem) !== -1)
hasExternalFreeDownload = true;
});
websites.forEach(function(elem) {
if($freedl.attr('href').toLowerCase().indexOf(elem) !== -1)
hasExternalFreeDownload = true;
});
}
if(hasExternalFreeDownload) {
this.$freedl = $freedl;
return $freedl;
} else {
return false;
}
};
SoundCloudTrack.prototype.findExternalBuyLink = function() {
var $buylink = this.$el.find('.soundActions__purchaseLink').eq(0),
strings = ['buy', 'spotify', 'beatport', 'juno', 'stream'],
websites = ['lnk.to', 'open.spotify.com', 'spoti.fi', 'junodownload.com', 'beatport.com', 'itunes.apple.com', 'play.google.com', 'deezer.com', 'napster.com', 'music.microsoft.com'],
hasExternalBuyLink = false;
if($buylink.exists()) {
strings.forEach(function(elem) {
if($buylink.text().toLowerCase().indexOf(elem) !== -1)
hasExternalBuyLink = true;
});
websites.forEach(function(elem) {
if($buylink.attr('href').toLowerCase().indexOf(elem) !== -1)
hasExternalBuyLink = true;
});
}
if(hasExternalBuyLink) {
return $buylink;
} else {
return false;
}
};
SoundCloudTrack.prototype.setChecked = function(checked) {
this.$el.attr('checked', checked);
};
SoundCloudTrack.prototype.isChecked = function(checked) {
return typeof this.$el.attr('checked') != 'undefined';
};
SoundCloudTrack.prototype.isGeoblocked = function() {
if(this.$el.find(".g-geoblocked-icon").exists()) {
return true;
} else if(this.$el.parent("trackItem__additional").find(".g-geoblocked-icon").exists()) {
return true;
}
return false;
};
SoundCloudTrack.prototype.isPreview = function() {
var $item = $([]);
if(this.$el.find(".sc-snippet-badge").exists()) {
$item = $item.find(".sc-snippet-badge");
} else if(this.$el.parent("trackItem__additional").find(".sc-snippet-badge").exists()) {
$item = $item.parent("trackItem__additional").find(".sc-snippet-badge");
}
return $item.eq(0).text() === "Preview";
};
SoundCloudTrack.prototype.isGO = function() {
return this.$el.find('.g-go-marker-artwork').exists();
};
SoundCloudTrack.prototype.getArtworkURL = function(size) {
return 'https://i1.sndcdn.com/artworks-'+this.id+'-0-t'+size+'x'+size+'.jpg'
};
/*
HELPERS
*/
function exists(thing) {
return (typeof thing != "undefined" || thing != null || ($.isArray(thing) && thing.length > 0))
}
function cleanTitle(title) {
title = title.replace(/"/g, "'");
title = $.trim(title);
return title;
}
function cleanURL(url) {
url = url.split(/[?#]/)[0]; // Strip query string
url = relativeToAbsoluteURL(url); // Convert to an absolute url if necessary
return url;
}
function isValidTrackURL(url) {
if(!url.match(/^(http|https):\/\/soundcloud\.com\/.+\/.+$/g)) return false;
if(url.match(/^(http|https):\/\/soundcloud\.com\/.+\/sets\/.+$/)) return false;
return true;
}
function relativeToAbsoluteURL(url) {
if(url.substr(0, 1) === '/')
return 'https://soundcloud.com'+url;
else
return url;
}
function downloadUrl(url) {
if (!$('.js-downloader').exists()) {
$('body').append('<a class="js-downloader" style="visibility: hidden; position: absolute"></a>');
}
var $downloader = $('.js-downloader');
$downloader.attr('href', url);
$downloader.attr('download', 'download');
$downloader[0].click();
}
});
})(jQuery);