DiscograPY creates Spotify discography playlists. It ships as:
- A web app (Flask + vanilla HTML/CSS/JS) — search artists, configure filters, get a playlist with embedded preview.
- A CLI (
playlists.py) — same core logic, interactive terminal flow.
Built on the Spotify Web API via Spotipy. Artist metadata (genres, country) enriched from MusicBrainz.
- Features
- Requirements
- Installation
- Setup
- Getting a Refresh Token
- Usage — Web
- Usage — CLI
- Content Filters
- Project Structure
- Logging
- Deployment
- Notes & Limitations
- Troubleshooting
- License
- Search any artist; results show follower count, genres, and country (via MusicBrainz enrichment).
- 7 album type modes: Everything, LPs only, EPs only, Singles only, Compilations only, EPs + Singles, LPs + EPs + Singles.
- EP detection by track count (4–7 tracks = EP, 1–3 = Single) since Spotify reports both as
single. - Smart playlist suffix adjusted when requested types are missing for an artist.
- Content filters (web): exclude or include live versions, demos, remixes, and instrumentals per request.
- Smart deduplication (default on): keeps the best version of each track by normalized title and album release priority; eliminates regional duplicates and deluxe-edition redundancy.
- Parallel album track fetching (
ThreadPoolExecutor, up to 8 workers). - Tracks added in chronological release order.
- Optional artist image as playlist cover (
ugc-image-uploadscope required). - Dry-run mode (CLI): full discovery and filtering without creating the playlist.
- Retry logic with exponential backoff and
Retry-Afterheader support for HTTP 429. - UTF-8 logging to file and console; compatible with non-Latin artist names.
- Python 3.8+
- Spotify Developer account with app credentials
- Python packages:
flask,gunicorn,spotipy,python-dotenv,flask-cors,requests,pycountry
pip install -r requirements.txt- Go to developer.spotify.com/dashboard.
- Click Create an App, fill in name and description.
- After creation, note your Client ID and Client Secret.
In app settings → Edit Settings → Redirect URIs, add:
For local development:
http://127.0.0.1:5000/callback
For production (Railway):
https://discograpy.up.railway.app/callback
Click Add then Save for each.
Create a .env file in the project root:
SPOTIPY_CLIENT_ID=your_client_id_here
SPOTIPY_CLIENT_SECRET=your_client_secret_here
SPOTIPY_REDIRECT_URI=http://127.0.0.1:5000/callback
SPOTIPY_REFRESH_TOKEN=your_refresh_token_here
# Optional
FLASK_SECRET_KEY=change-me-in-production
SPOTIPY_USE_CACHE=falseSPOTIPY_REFRESH_TOKEN — required for the web app (server-side playlist creation). Obtain it with get_token.py (see below). Without it, each server restart requires re-authentication.
SPOTIPY_USE_CACHE — set to true to cache the OAuth token in .spotify_cache during local development. Keep false for production.
Never commit
.envto version control — it is listed in.gitignore.
Run this once locally to authenticate and print your refresh token:
python get_token.pyA browser window will open for Spotify OAuth. After authorizing, the terminal prints:
REFRESH TOKEN: AQA...
Copy that value into SPOTIPY_REFRESH_TOKEN in your .env.
Required scopes granted: playlist-modify-public playlist-modify-private ugc-image-upload.
Start locally:
python app.pyOpen http://127.0.0.1:5000.
Production-style local run:
gunicorn app:app --bind 0.0.0.0:5000- Search — type an artist name; results show name, followers, genres, and country.
- Configure — select album type, toggle content filters (live, demos, remixes, instrumentals, duplicate versions), optionally use artist image as cover.
- Result — embedded Spotify playlist preview after creation.
python playlists.py [--verbose] [--dry-run]| Flag | Effect |
|---|---|
-v / --verbose |
Debug-level logging to console |
--dry-run |
Discover and filter tracks; skip playlist creation |
- Enter artist name.
- Select from matching results (shows follower count and genres).
- Choose album type (0–6).
- Playlist is created and the URL is printed.
Note: CLI uses default content filter settings — live versions, demos, remixes, and instrumentals are excluded; smart deduplication is applied. To override filters, use the web interface.
| # | Label | What's included |
|---|---|---|
| 0 | Everything | All types combined |
| 1 | LPs only | Full-length albums |
| 2 | EPs only | Single-type releases with 4–7 tracks |
| 3 | Singles only | Single-type releases with 1–3 tracks |
| 4 | Compilations only | Compilation releases |
| 5 | EPs + Singles | EPs and Singles, no LPs or Compilations |
| 6 | LPs + EPs + Singles | Everything except Compilations |
Available in the web interface (passed as booleans to POST /api/create):
| Filter | Default | Effect when enabled |
|---|---|---|
include_live_versions |
off | Include tracks/albums with "live", "en vivo" etc. in the name |
include_demos |
off | Include tracks/albums with "demo", "rough mix" etc. |
include_remixes |
off | Include remixes, reworks, edits, extended mixes |
include_instrumentals |
off | Include instrumental versions (only if original also exists) |
include_duplicate_versions |
off | Disable deduplication; keep all versions of each track |
use_artist_image_as_cover |
off | Upload artist's Spotify image as playlist cover |
When deduplication is on (default), duplicate tracks are resolved by normalized title comparison — bracket content and filter keywords stripped — keeping the version from the album with the highest release priority (LP > EP > Single).
discograpy/
├── app.py # Flask entry point
├── playlists.py # CLI entry point
├── get_token.py # Local helper: obtain refresh token
├── src/
│ ├── config.py # Spotify client factory, env var validation
│ ├── logging_config.py # Logging setup (file + console, UTF-8)
│ ├── domain/
│ │ ├── album_types.py # Album type config, matching, suffix logic
│ │ ├── filters.py # Content filters and track deduplication
│ │ └── models.py # RunSummary dataclass
│ ├── services/
│ │ ├── discography.py # DiscographyService: orchestrates the full flow
│ │ ├── spotify_client.py # SpotifyClient: Spotipy wrapper + retry
│ │ ├── musicbrainz.py # MusicBrainz metadata enrichment (genres, country)
│ │ └── retry.py # retry_on_failure decorator with backoff
│ ├── web/
│ │ ├── __init__.py # Flask app factory, singleton client/service
│ │ └── routes.py # HTTP routes and API endpoints
│ └── cli/
│ ├── runner.py # CLI orchestration logic
│ └── ui.py # Spinner, menus, artist/summary display
├── templates/
│ └── index.html # Single-page frontend
├── requirements.txt
├── Procfile # Railway/Heroku process definition
├── railway.toml # Railway deployment config
└── README.md
Log format:
%(asctime)s - %(name)s - %(levelname)s - %(message)s
| Output | Level |
|---|---|
| Console | INFO (or DEBUG with --verbose) |
spotify_discography.log |
DEBUG always |
Console stream is reconfigured to UTF-8 with errors='replace' for Windows compatibility.
Deployed on Railway using Nixpacks.
Start command: gunicorn app:app --bind 0.0.0.0:$PORT
Health check path: /
Restart policy: on_failure
Required environment variables on Railway: SPOTIPY_CLIENT_ID, SPOTIPY_CLIENT_SECRET, SPOTIPY_REDIRECT_URI, SPOTIPY_REFRESH_TOKEN, FLASK_SECRET_KEY.
- Batch size: Spotify API limit of 100 tracks per add-to-playlist request. Handled automatically with parallel batching (up to 4 concurrent batch uploads).
- Regional duplicates: Spotify returns market-specific versions of albums separately. Deduplication reduces this, but enabling
include_duplicate_versionswill include all of them. - Playlists are public by default. To create private playlists, change
public=Truetopublic=Falseinsrc/services/spotify_client.pyinsidecreate_playlist. - Cover upload requires
ugc-image-uploadscope in the refresh token. If the token was obtained without it, re-runget_token.py. - MusicBrainz requests have a 1.8s timeout and are LRU-cached per process. Enrichment failures are non-fatal; missing metadata falls back to Spotify's own genre data.
Authentication error / invalid credentials
- Check
.envhas correctSPOTIPY_CLIENT_IDandSPOTIPY_CLIENT_SECRET. - Confirm redirect URI in
.envmatches exactly what's set in the Spotify Developer Dashboard. - Re-run
get_token.pyto get a freshSPOTIPY_REFRESH_TOKEN.
Rate limiting (HTTP 429)
The retry decorator reads the Retry-After header and waits accordingly with added jitter. Persistent rate limiting suggests the Spotify app is being throttled. Wait a few minutes and retry.
No content found for selected type
- Verify the artist actually has that release type on Spotify.
- Try option
0(Everything) to see all available content. - The app warns and adjusts the playlist name when a requested type is absent.
Cover upload rejected (401)
The SPOTIPY_REFRESH_TOKEN was generated without the ugc-image-upload scope. Re-run get_token.py and update the token in your environment.
Import errors (ModuleNotFoundError)
pip install -r requirements.txt
python --version # must be 3.8+MIT License. See LICENSE.
Developed for automation and music lovers.
- GitHub: @based-on-what
- Project: github.com/based-on-what/discograpy
Acknowledgments: Spotipy, Spotify Web API, MusicBrainz.