bandcamp-explorer
A terminal browser and Python library for Bandcamp.
Search for artists and albums, discover releases by genre and location, browse artist/label profiles and discographies, all from the command line.
Install
Requires Python 3.12+.
Terminal CLI
uv tool install bandcamp-explorer
# or
pip install bandcamp-explorer
Discord bot
uv tool install bandcamp-explorer[discord]
# or
pip install bandcamp-explorer[discord]
Create a bot application at the Discord Developer Portal,
enable the bot scope with Send Messages and Use Slash Commands permissions,
then invite it to your server with the generated OAuth2 URL.
Set your bot token and run:
export DISCORD_TOKEN=your-bot-token
bandcamp-discord
# or with a .env file in the current directory
bandcamp-discord
Use --guild GUILD_ID to sync slash commands instantly to a specific server
(global sync can take up to an hour).
Slash commands (all under /bandcamp):
| Command | Description |
|---|---|
/bandcamp search <query> |
Search everything |
/bandcamp album <query> |
Search albums |
/bandcamp artist <query> |
Search artists/labels |
/bandcamp track <query> |
Search tracks |
/bandcamp discover <tag> |
Browse releases by tag (with optional slice and location filters) |
Development
git clone https://github.com/gabriel-jung/bandcamp-explorer.git
cd bandcamp-explorer
uv sync
CLI
Search
bandcamp "caladan brood" # search everything
bandcamp "erang" --artist # artists/labels only
bandcamp "echoes of battle" --album # albums only
bandcamp "a forest whisper" --track # tracks only
Browse by tag
bandcamp --tag dungeon-synth # newest arrivals (default)
bandcamp --tag black-metal --top # best-selling
bandcamp --tag dungeon-synth --rand # surprise me
bandcamp --tag dungeon-synth --location france
bandcamp --tag dungeon-synth --location paris
bandcamp --tag dungeon-synth black-metal # multi-tag
Slices: --new (default), --top, --rand.
Locations are resolved to geoname IDs via Bandcamp's autocomplete and
cached locally; force a refresh with --refresh-location.
Direct URLs
bandcamp https://erang.bandcamp.com/album/tome-iv
bandcamp https://erang.bandcamp.com
Interactive navigation
After selecting a result, you enter an interactive browser:
- Artists: view bio, browse discography, select an album to see its tracklist, select a track to view its page, navigate to the label.
- Albums: header with tracklist, description, and lyrics; navigate to the artist/host page or select a track.
Press 0 to go back, Ctrl+C to quit.
Output modes
bandcamp "erang" --artist --json # output as JSON
bandcamp "erang" --limit 10 # cap results
bandcamp --tag dungeon-synth --json --limit 100 # cap tag dump
bandcamp https://erang.bandcamp.com/album/tome-iv --json
bandcamp https://erang.bandcamp.com/album/tome-iv --full # all sections at once
bandcamp -v ... # enable debug logging
Terminal images
Album covers and artist images render inline on terminals that support the iTerm2 or Kitty image protocol (iTerm2, Kitty, WezTerm, Mintty).
Library
The core module has no terminal dependencies; use it in scripts,
pipelines, or other tools. All data is returned as plain dicts with a
_type discriminator key.
from bandcamp_explorer.core import (
BandcampClient, AlbumAPI, ArtistAPI, DiscoverWebAPI, SearchAPI,
NotFoundError, resolve_geoname,
)
with BandcampClient() as client:
# Search (one call returns the whole result set)
results = SearchAPI(client).search("caladan brood", item_type="album")
# Discover releases by tag (new discover_web endpoint)
discover = DiscoverWebAPI(client)
releases, cursor, total = discover.discover(tags=["dungeon-synth"], slice_="new")
all_releases = discover.discover_all(tags=["dungeon-synth"], max_pages=3)
# Fetch album details (skip cover-art bytes with fetch_art=False)
album = AlbumAPI(client).get("https://erang.bandcamp.com/album/tome-iv")
for track in album["tracks"]:
print(f" {track['position']}. {track['title']} ({track['duration']})")
# Fetch artist/label profile
artist = ArtistAPI(client).get("https://erang.bandcamp.com")
for item in artist["discography"]:
print(f" {item['title']}")
# Location filtering (geoname-based)
geoname_id = resolve_geoname(client, "paris")
releases, _, _ = discover.discover(tags=["dungeon-synth"], geoname_id=geoname_id)
# Download images
client.download_image(album.get("image_url"), output_dir="./images/")
Errors
AlbumAPI.get and ArtistAPI.get raise NotFoundError when a page 404s, so
callers can tell a deleted release from a failed fetch. Every other transport
failure returns None. If Bandcamp answers with its bot-defence interstitial
(HTTP 200 with no content in it), the client raises ChallengeError and then
fails fast for two minutes rather than hammering a blocked endpoint. Never
treat a ChallengeError as a missing resource; it means "ask again later".
from bandcamp_explorer.core import ChallengeError, NotFoundError
try:
album = AlbumAPI(client).get(url)
except NotFoundError:
... # gone for good, stop retrying
except ChallengeError:
... # blocked for now, retry later
TLS fingerprints
Bandcamp soft-blocks some TLS fingerprints by answering HTTP 404 to pages a
different fingerprint fetches fine, so a bare 404 is not proof of deletion. The
client re-checks every 404 against a short list of known-good fingerprints
before raising NotFoundError. If one of them serves the page, that fingerprint
takes over the session for the rest of the client's life, so the extra request
is paid once rather than on every later 404.
# Pick the fingerprint yourself (default: curl_cffi's floating "chrome" alias).
client = BandcampClient(impersonate="chrome124")
# Change or disable the re-check ladder.
client = BandcampClient(fallback_impersonate=("chrome131", "chrome124"))
client = BandcampClient(fallback_impersonate=()) # every 404 raises at once
client.impersonate # the fingerprint currently in use, after any promotion
Names come from curl_cffi's impersonate targets; entries the installed version
does not know are skipped rather than raising. Which fingerprints are blocked
varies by vantage point: two hosts can see the same cutoff between builds with
the sides swapped, one serving the recent ones and challenging the old, the
other the reverse. Measure from the machine that will run the fetches with
python scripts/probe_fingerprints.py, which reads response bodies rather than
status codes because a blocked fingerprint answers HTTP 200 with the
interstitial.
The ladder only ever refuses to believe a 404, it never invents one. A fallback that errors, is challenged, or is unknown proves nothing, so it is skipped and the original 404 stands. A challenged fallback never arms the client's challenge backoff either: that session is a throwaway and says nothing about the primary.
NotFoundError.confirmed_by names the fallback fingerprints that independently
saw the same 404. It is empty when nobody could check, which is much weaker
evidence than a 404 two working fingerprints agreed on. Callers that flag rows
deleted should require it:
try:
album = AlbumAPI(client).get(url)
except NotFoundError as e:
if e.confirmed_by:
mark_deleted(url) # two independent fingerprints agree it is gone
else:
... # nothing could corroborate it; leave the row alone and retry
The default ladder deliberately spans browser families. Two Chrome builds share a failure axis: Bandcamp splits Chrome between 131 and 133a, and which side is served depends on where you fetch from, so a pair of Chrome fallbacks can land on the blocked side together and rescue nothing. Firefox and Safari are off that axis.
Bandcamp removed the
dig_deeperhub endpoint, soDiscoverAPIwas dropped in 0.6.0; useDiscoverWebAPI.resolve_locationwent with it: it resolved Bandcamp's internal location tag ids, which only that endpoint accepted.DiscoverWebAPIfilters bygeoname_id, soresolve_geonameis the one you want.
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file bandcamp_explorer-0.7.0.tar.gz.
File metadata
- Download URL: bandcamp_explorer-0.7.0.tar.gz
- Upload date:
- Size: 28.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
864296f3d40a1403e8fc5153b03feec92d6b33fd601d202ba4775063a288573e
|
|
| MD5 |
f920d6a17f41f41d742b11fb5854ed67
|
|
| BLAKE2b-256 |
1b512d99d6a90a3e589755a5a2f430550fe6001c32f43e9b9f913cbb44f7cd09
|
Provenance
The following attestation bundles were made for bandcamp_explorer-0.7.0.tar.gz:
Publisher:
publish.yml on gabriel-jung/bandcamp-explorer
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bandcamp_explorer-0.7.0.tar.gz -
Subject digest:
864296f3d40a1403e8fc5153b03feec92d6b33fd601d202ba4775063a288573e - Sigstore transparency entry: 2568467637
- Sigstore integration time:
-
Permalink:
gabriel-jung/bandcamp-explorer@8298806fedf0047ed10fe0cb04d8cbf29ab025a2 -
Branch / Tag:
refs/tags/v0.7.0 - Owner: https://github.com/gabriel-jung
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8298806fedf0047ed10fe0cb04d8cbf29ab025a2 -
Trigger Event:
push
-
Statement type:
File details
Details for the file bandcamp_explorer-0.7.0-py3-none-any.whl.
File metadata
- Download URL: bandcamp_explorer-0.7.0-py3-none-any.whl
- Upload date:
- Size: 35.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b9e43ed0cc66f39bcba88bbefbd9f9b14171da782e3b36258322225390a9bdbe
|
|
| MD5 |
6cdf79679b66b197546e8a565b2a79dc
|
|
| BLAKE2b-256 |
2d84c28016f1507a0d3b0ea8f039ef17237d0a793a8c3ea85d916f1aa5e07b04
|
Provenance
The following attestation bundles were made for bandcamp_explorer-0.7.0-py3-none-any.whl:
Publisher:
publish.yml on gabriel-jung/bandcamp-explorer
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bandcamp_explorer-0.7.0-py3-none-any.whl -
Subject digest:
b9e43ed0cc66f39bcba88bbefbd9f9b14171da782e3b36258322225390a9bdbe - Sigstore transparency entry: 2568467642
- Sigstore integration time:
-
Permalink:
gabriel-jung/bandcamp-explorer@8298806fedf0047ed10fe0cb04d8cbf29ab025a2 -
Branch / Tag:
refs/tags/v0.7.0 - Owner: https://github.com/gabriel-jung
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8298806fedf0047ed10fe0cb04d8cbf29ab025a2 -
Trigger Event:
push
-
Statement type: