Syncing Bandcamp purchases to Android music library

Syncing Bandcamp purchases to Android music library
Photo by Alena Stepanova / Unsplash

Technical nerd post ahead!

Bandcamp would have a larger effect on the music industry if they built music library synchronization into their mobile app. Unfortunately their mobile app is mostly just a storefront.

I have over 450 Bandcamp albums, and I need to get them onto my Android device. How does one do this?

I knew that Bandcamp exposed public APIs to get purchase data. I also run Termux on Android, so I know that I could get a Linux-based solution to work.

There is an existing Python app called bandcamp-downloader which is fantastic. However, I could not get bandcamp-downloader to build in Termux because it uses a patched version of curl that I could not get to compile.

If you're going to run this process on a Mac or PC or Linux, I'd recommend using the Python app.

Rewrite

I re-wrote bandcamp-downloader my way, using Typescript, to get around the patched curl dependency. It was an extreme measure to take, but it was the easiest option to get this type of process to run on Android in Termux.

Source code: https://github.com/kindohm/bc-scraper

I wrapped my app's usage in a simple bash script, so now I can just run ./bc in my phone's terminal to sync my entire music library to Android.

OK nerds, here we go.

The app's process is pretty straightforward:

  • read bandcamp.com auth cookies from a cookies.txt file (more on that below)
  • call the public bandcamp API to get my purchases
  • visit each purchase HTML page and scrape out download data
  • download each album .zip file

Additionally, may app also extracts each .zip file to a target destination folder.

Bandcamp auth cookies

Bandcamp has no OAuth handshake to call their API, so you have to log in manually and export the bandcamp.com cookies, then use those cookies in your app code.

I use the cookies.txt browser extension in Firefox to export my cookies to a cookies.txt file. You can find similar extensions for Chrome and other browsers.

Retrieving your purchases

Bandcamp's public API has a couple of public endpoints that return information about your purchases:

The older_than_token

The older_than_token appears to require this form:

${unix_timestamp of date to query from}:${id of newest item}:${item_type}::

Yes, those two trailing colons are valid.

My function to generate this token string looks like this:

export const getOlderThanToken = (summary: CollectionSummary) => {
  const albumKeys = Object.keys(summary.collection_summary.tralbum_lookup);
  const firstKey = albumKeys[0];
  const { item_type, item_id } =
    summary.collection_summary.tralbum_lookup[firstKey];
  const now = new Date();
  const olderThanToken = `${now.getTime()}:${item_id}:${item_type}::`;
  return olderThanToken;
};

// the CollectionSummary types look like this:
type CollectionSummaryItem = {
  item_type: string;
  item_id: number;
  band_id: number;
  purchased: string;
};

export type CollectionSummary = {
  fan_id: number;
  collection_summary: {
    tralbum_lookup: Record<string, CollectionSummaryItem>;
  };
};

Actually downloading a .zip file

To actually download a collection item, you must navigate to it's "redownload page" and extract a JSON data blob from a <div> element.

That data blob contains the download data with the URL of the .zip file.

const redownloadUrl =
  items.redownload_urls[`${item.sale_item_type}${item.sale_item_id}`];

const downloadDocResponse = await axios.get(redownloadUrl, {
  headers: { Cookie: cookies },
});

const doc = new JSDOM(downloadDocResponse.data);
const pageDataDiv = doc.window.document.querySelector("#pagedata");
const dataBlob = pageDataDiv?.attributes.getNamedItem("data-blob")?.value;
if (!dataBlob) {
  console.warn("no data blob");
  return;
}
const data = JSON.parse(dataBlob);
const format = "mp3-320"; // hard-coded to always choose mp3s

if (!data.download_items[0].downloads) {
  console.warn("no downloads", item.band_name, item.item_title);
  continue;
}

const url = data.download_items[0]?.downloads[format]?.url;

Usage

node ./dist/index.js \
  --cookies [path to bandcamp cookies file] \
  --queryLimit [how many items to retrieve from your collection] \
  --days [how far back to look in your collection for purchases] \
  --downloadDir [where to download your collection's .zip files] \
  --extractDir [where to extract the .zip files] \
  --redownload [true|false, redownload .zip files already on disk] \
  --reextract [true|false, unzip .zip files if destination already exists] \

Conclusion

I don't recommend anybody re-write an app like this is one already exists! If I could have gotten that weird patched version of curl to compile in Termux, I would have used the Python app instead of doing a re-write.

Even though this app was written with Termux in mind, it runs anywhere that NodeJS will run! So you can use this app on Mac, PC, and a Linux desktop too.

Enjoy!