// ==UserScript==
// @name Bandcamp Downloader
// @namespace BigDuckie.BandcampDownloader
// @author Big Duckie
// @copyright 2025, Big Duckie (https://bigduckie.dev)
// @license MIT
// @version 2025.6.6
// @description Adds a sidebar to download songs from Bandcamp.
// @include *://*.bandcamp.com/*
// @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.9.1/jszip.min.js
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_download
// @connect bcbits.com
// ==/UserScript==
// Register Globals
let tracklist = TralbumData.trackinfo;
unsafeWindow.GM_download = GM_download;
(function () {
"use strict";
// Sidebar construction
jQuery("body")
.append(jQuery("
")
.addClass("dl-sidebar-container dl-sidebar dl-color-1 hidden")
.append(jQuery("").text("Click the files to download, or use the download album button."))
.append(jQuery("
").addClass("dl-tracklist"))
.append(jQuery("")
.addClass("dl-all-button dl-progress dl-color-2")
.append(jQuery("").addClass("dl-progress-bar"))
.append(jQuery("").addClass("dl-progress-text").text("Download Album"))))
.append(jQuery("")
.addClass("dl-sidebar-toggle dl-sidebar dl-color-1 hidden")
.text("Bandcamp Downloader"));
tracklist.forEach((track) => {
jQuery(".dl-tracklist").append(jQuery("")
.attr("id", "track-" + track.track_num)
.append(jQuery("")
.addClass("dl-link custom-color")
.attr("data-url", track.file["mp3-128"])
.attr("data-trackid", tracklist.indexOf(track))
.text(track.title)));
});
function css(strings) {
return strings;
}
// Style sidebar to match page
GM_addStyle(css`.dl-sidebar-toggle {
font-family: biolinum;
left: 0;
top: 50px;
position: fixed;
transform: rotate(90deg);
transform-origin: left bottom 0;
border-top-right-radius: 5px;
border-top-left-radius: 5px;
margin: 0;
padding: 5px;
z-index: 5001;
background: inherit;
border: 1px solid #000000;
border-bottom: none;
cursor: pointer
}
.dl-sidebar-toggle.active {
left: 400px
}
.dl-sidebar-container {
display: flex;
flex-direction: column;
width: 390px;
min-height: 300px;
left: -400px;
top: 50px;
max-height: 500px;
overflow-y: scroll;
padding: 5px;
position: fixed;
z-index: 5000;
border: 1px solid #000000;
border-left: none;
user-select: none;
cursor: default
}
.dl-sidebar-container.active {
left: 0
}
.dl-tracklist {
flex-grow: 1;
}
.dl-all-button {
margin-left: 0;
margin-right: 0;
cursor: pointer;
background-color: rgb(255, 255, 255);
color: rgb(0, 0, 0);
border-color: rgb(0, 0, 0)
}
.dl-progress {
position: relative;
z-index: 5001
}
.dl-progress,
.dl-progress-bar,
.dl-progress-text {
height: 25px;
left: 0;
right: 0;
top: 0;
bottom: 0
}
.dl-progress-bar {
width: 0;
overflow: hidden;
z-index: 5002;
-moz-transition: width 0.2s linear;
-webkit-transition: width 0.2s linear;
-o-transition: width 0.2s linear;
transition: width 0.2s linear;
background-color: rgb(0, 0, 0)
}
.dl-progress-text {
text-align: center;
z-index: 5003;
position: absolute;
line-height: 25px;
color: inherit
}
.dl-sidebar {
opacity: 1;
transition: opacity 1s, left 0.5s
}
.dl-sidebar.hidden {
opacity: 0
}
.dl-sidebar::-webkit-scrollbar {
width: 10px
}
.dl-sidebar::-webkit-scrollbar-track {
background: rgb(0, 0, 0)
}
.dl-sidebar::-webkit-scrollbar-thumb {
background: rgb(255, 255, 255)
}`);
jQuery(".dl-color-1").css({
"background-color": jQuery("#pgBd").css("background-color"),
color: jQuery("#pgBd").css("color"),
"border-color": jQuery("#pgBd").css("color"),
});
jQuery(".dl-color-2").css({
"background-color": jQuery("#follow-unfollow").css("background-color"),
color: jQuery("#follow-unfollow").css("color"),
"border-color": jQuery("#follow-unfollow").css("border-color"),
});
jQuery(".dl-progress-bar").css({
"background-color": jQuery("#recommendations_container").css("background-color"),
});
jQuery(".dl-sidebar").removeClass("hidden");
// Handle events
jQuery(".dl-sidebar-toggle").click((e) => {
jQuery(".dl-sidebar").toggleClass("active");
});
jQuery(".dl-link").click((e) => {
GM_download(jQuery(e.target).attr("data-url"), `[${pad(tracklist[jQuery(e.target).attr("data-trackid")].track_num, 2)}] ${TralbumData.artist} - ${tracklist[jQuery(e.target).attr("data-trackid")].title}.mp3`);
});
jQuery(".dl-all-button").click((e) => {
console.debug("downloading album");
downloadAlbum();
});
// Main functions
function downloadAlbum() {
let zip = new JSZip();
tracklist.forEach((track) => {
console.debug("downloading track", track);
zip.file(
`[${track.track_num}] ${TralbumData.artist} - ${track.title}.mp3`,
urlToPromise(track.file["mp3-128"]));
});
console.debug("generating zip");
zip.generateAsync({type: "blob"}, (metadata) => {
console.debug("processing zip");
jQuery(".dl-progress-bar").css("width", `${metadata.percent}%`);
})
.then((blob) => {
let url = URL.createObjectURL(blob);
console.debug("compiled zip", url);
GM_download(url, `${TralbumData.artist} - ${TralbumData.current.title}.zip`);
}, (e) => {
console.error(e);
});
}
// Supporting functions
function pad(n, width = 3, z = 0) {
return (String(z).repeat(width) + String(n)).slice(String(n).length);
}
function urlToPromise(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET", url: url, responseType: "blob", onload: (response) => {
console.debug("fetched song", url, response.response);
resolve(response.response);
}, onerror: (response) => {
reject(response.response);
},
});
});
}
})();