A private, offline playlist generator for music you already own.
Scans your local library, enriches each track with Last.fm metadata (listener counts, genre tags), stores everything in a local SQLite database, and generates .m3u playlists — ranked by popularity, filtered by genre and decade — that import directly into Swinsian on macOS.
No streaming service. No internet after the initial scan. No recommendation engine. Just your files.
Scan (run once, ~80 min for 25k tracks): walks your music folder, reads file tags with mutagen, and asks Last.fm two things per track — how popular it is and what genre tags people have applied. Stores everything locally. Incremental: only scans new files on subsequent runs.
Generate (instant, fully offline): queries the local DB to write .m3u playlists. Filter by genre tag, year range, and minimum listener count. Rank by Last.fm listener count.
git clone https://github.com/yourname/crates
cd crates
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp config.example.py config.py
# edit config.py — set your Last.fm API key, MUSIC_DIR, and pathsGet a free Last.fm API key at last.fm/api/account/create. Any app name works.
# One-time scan (test on a small subfolder first)
python crates.py scan
# Library stats and match rate
python crates.py stats
# See what tags Last.fm actually assigned to your library
python crates.py tags
# Generate all preset playlists at once
python crates.py presets
# Artist playlists
python crates.py artist "Radiohead"
python crates.py artist "Radiohead" --limit 30 --deep-cuts
python crates.py radio "Radiohead"
# Custom genre playlist
python crates.py genre "Late 90s Rap" --tags "hip-hop" "hip hop" "rap" --years 1995 1999
# Best of a decade, optionally filtered by genre
python crates.py decade 1990
python crates.py decade 2000 --tags "electronic"
# Remove DB entries for files that have been deleted or moved
python crates.py clean # dry run — shows what would be removed
python crates.py clean --yes # actually deletescrates.py CLI. Subcommands above. Holds the PRESETS dict.
scanner.py File walker + tag reader (mutagen) + Last.fm enrichment. Writes to SQLite.
playlists.py All query/generation logic. No network calls. Reads SQLite, writes M3U.
config.py Your paths and API key. Gitignored — see config.example.py.
The scan is resume-safe: Ctrl+C anytime and re-run to continue. Failed tracks (network errors) are retried automatically. Permanent no-matches (track absent from Last.fm) are skipped on future runs.
Each track gets a status in the DB:
| status | meaning |
|---|---|
matched |
found on Last.fm, has listener count and tags |
no_match |
not found — permanent, won't retry |
failed |
network error — retried on next scan |
pending |
not yet processed |
Edit the PRESETS dict in crates.py to define your own named playlists. Run python crates.py tags after your first scan to see which tags actually exist in your library — Last.fm's tag vocabulary is user-generated and varies.
PRESETS = {
"My Playlist": {
"tags": ["tag1", "tag2"], # OR logic — any one tag matches
"year_range": (1990, 1999), # optional
"min_listeners": 50_000,
"limit": 50,
},
}- Match rate depends on how well-documented your artists are on Last.fm. Expect 95–100% for mainstream western music, lower for niche/local/non-English artists.
- Tag coverage is sparse for many tracks even when matched — Last.fm only returns tags that users have applied. Listener counts are more reliable than tags.
- Popularity ranking uses Last.fm global listener count. This reflects a track's global reach, not personal taste — use
--deep-cutsto invert it. - The quality ceiling is the source data. If a track isn't tagged on Last.fm, no query reaches it.
- Python 3.12+
- macOS (output paths and Swinsian import are macOS-native; M3U files are otherwise portable)
- Last.fm API key (free)
mutagen,requests— seerequirements.txt