py_bandcamp

August 2, 2026 · View on GitHub

py_bandcamp is a Python scraper for Bandcamp. It fetches album, track, and artist metadata, stream URLs, lyrics, recommendations, and search results.

It returns mediavocab Release and Entity objects for typed, structured metadata.

mediavocab>=1.0.0 is a hard runtime dependency. Every search and recommendation helper returns validated Release or Entity models. There is no dict fallback.

Install

pip install py_bandcamp

For search pages protected by Fastly's JS bot challenge, add the stealth extra:

pip install "py_bandcamp[stealth]"
export PYBANDCAMP_TRANSPORT=curl_cffi

Quick start

from py_bandcamp import BandCamp, BandcampTrack, BandcampAlbum, BandcampArtist

# Get a streamable MP3 URL
url = BandCamp.get_stream_url("https://deadunicorn.bandcamp.com/track/astronaut-problems")
print(url)  # https://t4.bcbits.com/stream/...

# Search: returns mediavocab Release objects
for release in BandCamp.search_tracks("astronaut problems"):
    artist = release.work.credits[0].entity.name if release.work.credits else ""
    print(release.work.title, artist, release.uri)

for release in BandCamp.search_albums("black metal"):
    artist = release.work.credits[0].entity.name if release.work.credits else ""
    print(release.work.title, artist)

# Search artists/labels: returns mediavocab Entity objects
for entity in BandCamp.search_artists("Perturbator"):
    print(entity.name, entity.extra.get("genre"), entity.extra.get("location"))

# Browse by genre tag
for release in BandCamp.search_tag("doom-metal", albums=True, tracks=False, max_pages=2):
    print(release.work.title, release.uri)

# Discover related albums and artists from a seed
for release in BandCamp.get_recommendations("https://naxatras.bandcamp.com/album/iii"):
    artist = release.work.credits[0].entity.name if release.work.credits else ""
    print(release.work.title, artist, release.uri)

for entity in BandCamp.get_related_artists("https://naxatras.bandcamp.com/album/iii"):
    print(entity.name, entity.extra.get("artist_url"))

Return types at a glance

Release, from search_tracks, search_albums, search_tag, get_recommendations:

release.uri                                       # canonical Bandcamp permalink (no query string)
release.image                                     # artwork URL ("" when unavailable)
release.work.title                                # track or album title
release.work.media_type                           # MediaType.MUSIC
release.work.runtime                              # duration in seconds (tracks only)
release.work.content_genres                       # list of GENRE_* tokens + raw Bandcamp tags
release.work.credits[0].entity.name               # artist display name (if available)
release.work.credits[0].relation_role             # RelationRole.PERFORMER (tracks) / CREATOR (albums)
release.work.tracklist                            # list[Appearance], populated by album_to_release(...)
release.release_date                              # IsoDate-validated string or None
release.license                                   # License object (.identifier "CC-BY-SA-4.0") or None
release.license.is_open() if release.license else False   # True for CC*/CC0/PD, False otherwise
release.codec                                     # "mp3", Bandcamp's free streaming preview
release.bitrate                                   # "128" (kbps)
release.audio_channels                            # "stereo"
release.label                                     # EntityRef for the imprint, or None when self-released
release.external_ids["bandcamp_album_url"]        # full Bandcamp URL for albums
release.external_ids["bandcamp_track_url"]        # full Bandcamp URL for tracks
release.external_ids["bandcamp_band_url"]         # canonical artist root URL
release.external_ids["bandcamp_band_id"]          # numeric artist id
release.external_ids["bandcamp_album_id"]         # numeric album id
release.external_ids["bandcamp_track_id"]         # numeric track id

Full-fidelity album conversion

BandCamp.search_* keeps payloads small by skipping the per-album track fetch. To get the ordered tracklist, use album_to_release:

from py_bandcamp import BandCamp

# Album → mediavocab Release with full tracklist
release = BandCamp.album_to_release(
    "https://naxatras.bandcamp.com/album/iii",
    include_tracklist=True,
)
print(release.work.title, release.release_date)
for a in release.work.tracklist:
    print(a.position, a.work.title, a.work.runtime)

# Direct MP3-128 stream URL for a track
url = BandCamp.get_stream_url(
    "https://deadunicorn.bandcamp.com/track/astronaut-problems"
)
print(url)  # https://t4.bcbits.com/stream/...

# Search: yields Release objects
for release in BandCamp.search_albums("black metal"):
    artist = release.work.credits[0].entity.name if release.work.credits else ""
    print(release.work.title, artist, release.uri)

Public API

BandCamp: main facade (py_bandcamp/__init__.py)

MethodReturnsNotes
BandCamp.album_to_release(url, include_tracklist=True)ReleaseFetches album page. Tracklist populated when include_tracklist=True
BandCamp.track_to_release(url)ReleaseFetches track page
BandCamp.get_stream_url(url)strDirect MP3-128 CDN token URL (time-limited)
BandCamp.get_streams(urls)list[str]Batch of get_stream_url
BandCamp.get_track_lyrics(url)strLyrics text or "lyrics unavailable"
BandCamp.get_recommendations(url)list[Release]"If you like…" albums
BandCamp.get_related_artists(url)list[Entity]Unique artists from recommendations
BandCamp.search(name, ...)Iterator[Release|Entity]Paginated. Auto-deduplicates
BandCamp.search_tracks(name)Iterator[Release]
BandCamp.search_albums(name)Iterator[Release]
BandCamp.search_artists(name)Iterator[Entity]
BandCamp.search_labels(name)Iterator[Entity]
BandCamp.search_tag(tag, ...)Iterator[Release|Entity]Tag/genre browse
BandCamp.tags(tag_list=True)list[str] or dictAll known Bandcamp genre tags

Internal scraper models (py_bandcamp/models.py)

Classfrom_urlKey properties
BandcampAlbumyestitle, tracks, artist, recommendations, album_id, band_id
BandcampTrackyestitle, stream, duration, track_id, band_id, album_id
BandcampArtistyesname, albums, band_id, location, genre
BandcampSingleyestitle, artist, tracks (one-element list)
BandcampLabelyesname, location, tags

mediavocab converters (py_bandcamp/__init__.py)

FunctionInput → Output
BandCamp.album_to_release(album_or_url)BandcampAlbum|strRelease
BandCamp.track_to_release(track_or_url)BandcampTrack|strRelease

Search helpers (search_*, get_recommendations) call the internal converters automatically. You only call them directly when you already hold a raw model object.

Search caveat: Fastly JS challenge

Bandcamp's /search endpoint sits behind a Fastly bot challenge that rejects vanilla requests TLS fingerprints. Static HTML parsing, such as tag browse and album or track page fetches, is unaffected. For live keyword search, install the [stealth] extra and set PYBANDCAMP_TRANSPORT=curl_cffi (see docs/transport.md). Playwright is not required.

mediavocab integration

BandCamp.album_to_release and BandCamp.track_to_release return a fully typed mediavocab.Release. Key fields:

release.uri                            # canonical URL (query string stripped)
release.work.title                     # album or track title
release.work.credits[0].entity.name    # artist name
release.work.content_genres            # GENRE_* tokens + raw Bandcamp tags
release.work.tracklist                 # list[Appearance] (album_to_release only)
release.release_date                   # ISO-8601 string or None
release.license                        # License object (.identifier "CC-BY-SA-4.0") or None
release.codec / .bitrate / .audio_channels  # "mp3" / "128" / "stereo"
release.label                          # EntityRef or None (self-released)
release.external_ids                   # bandcamp_album_id/track_id/band_id + URLs

See docs/converters.md for the full field walk-through.

Pluggable session + PYBANDCAMP_TRANSPORT

import requests
from py_bandcamp import set_session, BandCamp

# Global override
s = requests.Session()
s.headers["User-Agent"] = "my-app/1.0"
set_session(s)

# Per-instance (leaves global session untouched)
bc = BandCamp(session=s)
list(bc.search_tracks("doom metal"))

PYBANDCAMP_TRANSPORT=curl_cffi makes default_session() return a curl_cffi.requests.Session(impersonate="chrome"). If curl_cffi is not installed, it falls back silently to plain requests.

Examples

ScriptWhat it demonstrates
examples/01_quickstart.pyFetch one album by URL
examples/02_track_details.pyTrack metadata, stream URL, lyrics
examples/03_artist_discography.pyArtist albums, singles, band_id
examples/04_get_streams.pyBatch stream URL extraction
examples/05_lyrics.pyFetch and print track lyrics
examples/06_recommendations.py"If you like…" albums + related artists
examples/07_search_static.pyTag browse (works without curl_cffi)
examples/08_to_mediavocab.pyFull Release/Entity field walk-through
examples/09_custom_session.pyInject session, optional curl_cffi
examples/10_label_catalog.pyLabel-level browsing
examples/11_crawl.pyFrontier crawling via related-artist BFS

Frontier crawling

BandCamp.crawl() walks the Bandcamp artist graph through album recommendation links ("fans also bought") without touching the Cloudflare-protected search endpoint. Pass a seen set to resume across multiple calls:

from py_bandcamp import BandCamp

seen = set()
for entity in BandCamp.crawl(["https://enslaved.bandcamp.com"], max_artists=5, seen=seen):
    print(entity.name, entity.extra.get("artist_url"))
# resume: already-visited URLs are skipped
for entity in BandCamp.crawl(["https://neurosis.bandcamp.com"], max_artists=5, seen=seen):
    print(entity.name)

Docs

Notes

  • Stream URLs are time-limited CDN tokens. Do not cache them.
  • Bandcamp has no public API. This library scrapes HTML and may break on markup changes.

License

Apache 2.0