Skip to main content

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

One case does not follow that split. The root page of an artist subdomain that no longer exists never answers 404: it answers HTTP 200 with either the bot-defence interstitial or Bandcamp's signup page, so nothing read off the root tells a deleted host from a live one. Since 0.8.0, when a host root produces no artist, /music on the same host is asked before any conclusion is drawn, and a 404 there raises NotFoundError where earlier versions raised ChallengeError or returned an artist with no name. Any other answer keeps the old behaviour: only a real 404 is read as gone. It costs one extra request per dead host, once.

TLS fingerprints

impersonate picks the curl_cffi fingerprint the session uses, defaulting to the floating "chrome" alias. Worth changing if Bandcamp starts refusing the default one.

client = BandcampClient(impersonate="firefox144")

fallback_impersonate is an escape hatch for a host that finds one fingerprint answered a 404 where another served the page: it re-checks a 404 on other fingerprints before raising, and names the ones that agreed in NotFoundError.confirmed_by. That situation has not been reproduced here, and the ladder is off by default since it costs an extra request per fallback on every genuine 404. scripts/probe_fingerprints.py measures which fingerprints work from the machine that will run the fetches, reading response bodies rather than status codes because a blocked fingerprint answers HTTP 200 with a bot-defence interstitial, which a status-code-only probe scores as success.

from bandcamp_explorer.core import SUGGESTED_FALLBACK_IMPERSONATE

client = BandcampClient(fallback_impersonate=SUGGESTED_FALLBACK_IMPERSONATE)

Bandcamp removed the dig_deeper hub endpoint, so DiscoverAPI was dropped in 0.6.0; use DiscoverWebAPI. resolve_location went with it: it resolved Bandcamp's internal location tag ids, which only that endpoint accepted. DiscoverWebAPI filters by geoname_id, so resolve_geoname is 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

bandcamp_explorer-0.8.0.tar.gz (31.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bandcamp_explorer-0.8.0-py3-none-any.whl (38.0 kB view details)

Uploaded Python 3

File details

Details for the file bandcamp_explorer-0.8.0.tar.gz.

File metadata

  • Download URL: bandcamp_explorer-0.8.0.tar.gz
  • Upload date:
  • Size: 31.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for bandcamp_explorer-0.8.0.tar.gz
Algorithm Hash digest
SHA256 9c22f87e9ab6785a8db70f6730ced7a74d9070c959da04099610f39f47dd89a6
MD5 ea183e6a041959c10af1ad341c4bc376
BLAKE2b-256 227f9a585c22f7526603fa83a5c0e3c90f4acde3ce3ac1993f828e636048a49e

See more details on using hashes here.

Provenance

The following attestation bundles were made for bandcamp_explorer-0.8.0.tar.gz:

Publisher: publish.yml on gabriel-jung/bandcamp-explorer

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file bandcamp_explorer-0.8.0-py3-none-any.whl.

File metadata

File hashes

Hashes for bandcamp_explorer-0.8.0-py3-none-any.whl
Algorithm Hash digest
SHA256 63a81122ad1124f81721f3894ad0da6d643799737acb8275c58fe6c2353acbc6
MD5 a8986500a890fb90cd358a53bfbed186
BLAKE2b-256 955972a65ca44de79280273ac7bb55bb21458a0f3e747f4330dfe670ba10dcc5

See more details on using hashes here.

Provenance

The following attestation bundles were made for bandcamp_explorer-0.8.0-py3-none-any.whl:

Publisher: publish.yml on gabriel-jung/bandcamp-explorer

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.8.1

2 files

This release

0.8.0 This release

2 files

0.7.1

2 files

0.7.0

2 files

0.6.0

2 files

0.5.3

2 files

0.5.2

2 files

0.5.0

2 files

0.4.0

2 files

0.3.1

2 files

0.3.0

2 files

0.2.0

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page