// ==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("
  1. ") .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); }, }); }); } })();