- Python 99.3%
- Dockerfile 0.7%
|
All checks were successful
ci / lint-and-test (push) Successful in 1m18s
The NTS v2 API rejects offset > 1000 ("requested offset is not
allowed") and silently clamps limit to 12, so only the newest ~1008
episodes of a show are reachable. `guests` reports 5914 episodes, so
the pagination loop 422'd at offset=1008 on every invocation.
fetch_episodes now stops before the cap, warns when the list is
truncated, and returns (episodes, total). fetch_show_data numbers the
reachable window with true global chronological positions
(total - reachable + index) so track numbers stay stable as new
episodes land.
AI-assisted-by: glm-5.3 (via OpenCode)
|
||
|---|---|---|
| .forgejo/workflows | ||
| beets_plugins | ||
| beetsplug | ||
| scripts | ||
| tests | ||
| vendor-build | ||
| .gitignore | ||
| .secrets.baseline | ||
| AGENTS.md | ||
| beet | ||
| CLAUDE.md | ||
| config.yaml | ||
| plan.md | ||
| prek.toml | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
beets-setup
Personal beets configuration and custom plugins for managing a music library on an SMB share. Tags from MusicBrainz, detects download source provenance (Tidal, Bandcamp, SoundCloud, Mixcloud, YouTube, and more), embeds cover art, applies ReplayGain 2.0, and works around Lyrion Music Server's multi-artist MBID limitation and empty MP4 atom parsing bug.
What's in here
config.yaml- beets config with 23 plugins wired togetherbeetsplug/- six custom plugins:- download_source - detects the download source (Tidal, Bandcamp, SoundCloud, Mixcloud, YouTube, Subvert, Soulseek, Archive.org) from file tags on import, stores it as a persistent
download_sourcefield, surfaces it in folder names via inline path formatting - multi_artist_mbid - strips multi-artist MusicBrainz IDs from tags on write, so Lyrion doesn't choke on semicolon-separated MBIDs
- fix_artist_mbid - adds a
./beet fixartistmbidcommand to re-sync only the artist MBIDs from MusicBrainz (db-only by default,-wto also write tags,-pto preview) - extractart_auto - extracts embedded art to a folder cover file when fetchart finds nothing (fallback for as-is imports, skips broadcast albums)
- clean_m4a - strips empty MP4 atoms from m4a files after every write, so Lyrion can parse album/genre tags. Also provides
./beet clean_m4a --forceto fix existing files. - bandcamp_tags - adds
./beet bandcamp_tagsto append bandcamp genres+style to the existinggenresfield
- download_source - detects the download source (Tidal, Bandcamp, SoundCloud, Mixcloud, YouTube, Subvert, Soulseek, Archive.org) from file tags on import, stores it as a persistent
beet- wrapper script that runsbeetwithBEETSDIRpointed at this directory, so config and database stay localtests/- pytest tests for all plugins
Prerequisites
- Python 3.11+
- uv for dependency management
- ffmpeg (for the replaygain backend)
- The music library mounted somewhere beets can read/write (default:
/mnt/media/music)
Setup
git clone <repo> ~/Code/beets
cd ~/Code/beets
uv sync
The beet wrapper handles the rest:
./beet import /path/to/music
./beet ls
It sets BEETSDIR to the repo root, so beets finds config.yaml here and writes library.db locally instead of on the SMB share.
Config
config.yaml points the library at /mnt/media/music and the database at ~/.local/share/beets/library.db. Change directory to match your setup.
MusicBrainz credentials go in the musicbrainz.user / musicbrainz.pass fields if you want to submit tags.
Path format
Albums land as:
Artist/1977 Album Name [Tidal]/01 Track.flac
Artist/2022 Album Name [Bandcamp]/01 Track.flac
Artist/2022 Album Name/01 Track.flac
Broadcasts (NTS episodes with albumtype=broadcast) land as:
Broadcast/Artist/NTS - Show Name/01 NTS - Show Name - 1st January 2026 [Soundcloud].m4a
Multi-disc albums get 1.01, 2.01 etc. The year is original_year when available, falling back to year. The [Source] suffix only appears when download_source is set.
Source detection
The download_source plugin checks each item on import:
- Tidal:
urlfield starts withhttps://tidal.com/ - Bandcamp:
commentsfield contains a*.bandcamp.comURL (after aVisitmarker), ordata_sourceisbandcamp(set by the beetcamp plugin) - SoundCloud / Mixcloud / YouTube / Archive.org:
url(orcontact) field is a link to the site, orcommentscontains the link after anextracted frommarker - Subvert:
encodedbytag mentionssubvert.fm
When no tag-based source is found, a trailing [Source] token on the containing folder name is used as a fallback (this also covers Soulseek, which has no tag signature).
If an album has mismatched sources across tracks, the field is left unset and a warning is logged.
Query by source later:
./beet ls download_source:Tidal
./beet ls download_source:Soundcloud
Multi-artist MBID handling
Lyrion Music Server can't handle musicbrainz_artistid when a track has multiple artist MBIDs (semicolon-separated). The multi_artist_mbid plugin hooks the write event and strips the relevant tags from the write payload, without touching the database. Single-artist MBIDs pass through untouched.
Empty MP4 atom cleanup
beets/mediafile writes empty list fields (e.g. composers=[]) as size-8 atoms with no data sub-atom. Lyrion's Audio::Scan parser chokes on these and stops reading before it reaches the album tag, dumping every affected track into a "no album" bucket.
The clean_m4a plugin hooks after_write and removes empty atoms from m4a files automatically. To fix existing files:
./beet clean_m4a --force
Testing
uv run pytest
Tests use mock beets objects, no real library needed.
Plugins enabled
| Plugin | Purpose |
|---|---|
| musicbrainz | Metadata matching and tagging |
| inline | Inline fields for path formatting |
| types | Registers download_source as a persistent field |
| fetchart | Cover art from Cover Art Archive |
| embedart | Embeds cover art into files |
| replaygain | EBU R128 loudness analysis via ffmpeg |
| chroma | AcoustID fingerprinting for poor-metadata files |
| mbsync | Re-sync metadata from MusicBrainz |
| lastgenre | Genre tagging from Last.fm |
| duplicates | Find duplicate tracks/albums |
| edit | Interactive metadata editing via $EDITOR |
| fromfilename | Extract metadata from filenames |
| bandcamp | Bandcamp autotagger, fallback when MusicBrainz has no match |
| discogs | Discogs autotagger, another metadata fallback |
| filetote | Moves non-music files (artwork, logs, cues) alongside imports |
| importsource | Records the path an album was imported from in a source_path field |
| convert | Transcodes lossless sources (e.g. ALAC) to FLAC on import |
| download_source | Custom: detect download source (Tidal, Bandcamp, SoundCloud, Mixcloud, etc.) |
| multi_artist_mbid | Custom: strip multi-artist MBIDs for Lyrion |
| fix_artist_mbid | Custom: ./beet fixartistmbid re-syncs only artist MBIDs |
| extractart_auto | Custom: extract embedded art as fallback (skips broadcasts) |
| clean_m4a | Custom: strip empty MP4 atoms for Lyrion |
| bandcamp_tags | Custom: append bandcamp genres+style |