Skip to content

Instantly share code, notes, and snippets.

@drench
Last active February 11, 2024 21:15
Show Gist options
  • Save drench/2d3967bd7019e11480699d5d129a79e8 to your computer and use it in GitHub Desktop.
Save drench/2d3967bd7019e11480699d5d129a79e8 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Bandcamp Collection Extras
// @description Adds extras to Bandcamp collection pages
// @grant unsafeWindow
// @match https://bandcamp.com/*
// @run-at document-idle
// @version 1.0.0
// ==/UserScript==
class BandCampCollection {
constructor(win, collectionType = "wishlist") {
this.window = win;
this.collectionType = collectionType;
this.initialized = false;
}
initialize() {
if (this.initialized) {
return console.log("already initialized!");
}
if (! this.collectionItemsElement) {
return console.log("No collectionItemsElement; cannot initialize");
}
this.populateInitialItems();
this.startObserver();
this.initialized = true;
}
formatPrice(itemAttr) {
if (!itemAttr.price) return "Free or NYP";
let price = this.window.TextFormat.money(itemAttr.price, itemAttr.currency);
if (itemAttr.currency == "USD") return price;
let usd = this.window.TextFormat.money(itemAttr.price * this.window.CurrencyData.rates[itemAttr.currency], "USD");
return `${price} ${itemAttr.currency} (${usd} USD)`;
}
get observer() {
this._observer ||= (new MutationObserver(this.observerCallback));
return this._observer;
}
get observerCallback() {
let self = this;
this._observerCallback ||= (function(mutationsList, observer) {
mutationsList.forEach(function (mutation) {
mutation.addedNodes.forEach(function (itemNode) {
let itemId = itemNode.dataset.itemid;
if (!itemId) return console.log(`No itemId found`, itemNode);
let itemType = itemNode.dataset.itemtype[0] || "a";
let itemAttr = self.getItemCache(`${itemType}${itemId}`);
if (!itemAttr) return console.log(`Item ${itemId} not found!`);
self.decorateItem(itemNode, itemAttr);
});
});
});
return this._observerCallback;
}
get collectionGrid() {
return this.collectionItemsElement.querySelector(".collection-grid");
}
startObserver() {
if (this.collectionGrid) {
this.observer.observe(this.collectionGrid, { childList: true });
}
else {
console.log("Cannot find the collection-grid in the document.");
}
}
get api() { return this.window.FanCollectionAPI }
get collectionItemsElement() {
return this.document.getElementById(`${this.collectionType}-items`);
}
get document() { return this.window.document }
get fanId() { return this.window.FanData.fan_id }
get itemCache() { return this.window.ItemCache[this.collectionType] }
getItemCache(fullId) {
return this.itemCache[fullId];
}
setItemCache(fullId, attr) {
this.itemCache[fullId] = attr;
}
decorateItem(itemNode, itemAttr) {
if (!itemNode) {
return console.log(`itemNode ${itemAttr.item_id} not found`);
}
if (!itemAttr.is_purchasable) return;
let priceDiv = this.document.createElement('div');
priceDiv.innerText = this.formatPrice(itemAttr);
let itemContainer =
itemNode.querySelector("div.collection-item-gallery-container");
if (itemContainer) {
itemContainer.appendChild(priceDiv);
}
else {
console.log(`Cannot find a node to add pricing for ${itemAttr.item_id}`);
}
}
populateInitialItems() {
const token = `${Math.ceil((new Date()) / 1000)}::a::`;
let self = this;
this.api.getItems(this.fanId, token, this.collectionType, false, null, 20).then((items) => {
items.forEach((itemAttr) => {
let fullId = `${itemAttr.tralbum_type}${itemAttr.tralbum_id}`;
self.setItemCache(fullId, itemAttr);
let itemNode =
self.document.querySelector(`li.collection-item-container[data-tralbumid='${itemAttr.tralbum_id}']`);
if (!itemNode) return console.log(`itemNode ${itemAttr.tralbum_id} not found`);
let itemId = itemNode.dataset.itemid;
if (!itemId) return console.log(`No itemId found`, itemNode);
self.decorateItem(itemNode, itemAttr);
});
});
}
}
const bcWishList = new BandCampCollection(unsafeWindow, "wishlist");
bcWishList.initialize();
const bcCollection = new BandCampCollection(unsafeWindow, "collection");
bcCollection.initialize();
@drench
Copy link
Author

drench commented Aug 7, 2021

I love Bandcamp, but several things about their site (and Android app for that matter) are less great than I'd like them to be. Take the wishlist page. If you're like me, you continually add interesting looking things to your Bandcamp wishlist. Then Bandcamp Friday rolls around, you're looking to fill up your cart but in order to see prices you have to click through each album. It's tedious.

It turns out Bandcamp populates a lot of information in JavaScript objects, which includes not only (almost all) pricing information but also exchange rates(!)

So here's this userscript (tested only on Firefox for Mac) that adds prices (with conversion to USD) to your wishlist page, where it can.

This is still pretty raw, but it's already useful for me. Here's what it looks like:

Screen Shot 2021-08-06 at 7 57 14 PM

The big caveat is that you will see no prices for the first few rows. From what I've seen, the initial page load doesn't populate the price attributes, at least anywhere I can find them. If you click the "See all NNN items" button, the prices should be visible as subsequent items load.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment