No description
  • Python 99.3%
  • Dockerfile 0.7%
Find a file
niyari 1792f259d1
All checks were successful
ci / lint-and-test (push) Successful in 1m18s
Fix nts.py 422 on shows past the API offset cap
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)
2026-10-02 14:20:44 +01:00
.forgejo/workflows Update CI to run on any branch 2026-09-05 10:08:08 +01:00
beets_plugins feat: add clean_m4a plugin to strip empty MP4 atoms 2026-09-05 10:07:59 +01:00
beetsplug essentia fixes 2026-09-05 10:08:07 +01:00
scripts Fix nts.py 422 on shows past the API offset cap 2026-10-02 14:20:44 +01:00
tests Fix nts.py 422 on shows past the API offset cap 2026-10-02 14:20:44 +01:00
vendor-build Add Essentia mood-extraction pipeline (essentia_xtractor + essentia_mood) 2026-09-05 10:08:07 +01:00
.gitignore Delete blb 2026-09-05 10:08:08 +01:00
.secrets.baseline chore: add prek/ruff/mypy linting setup 2026-09-05 10:08:01 +01:00
AGENTS.md Credit station-made NTS shows to NTS Radio instead of the show name 2026-10-02 13:47:07 +01:00
beet chore: add gitignore, wrapper script, config, and lockfile 2026-09-05 10:07:50 +01:00
CLAUDE.md docs: add AGENTS.md with setup and usage instructions 2026-09-05 10:07:55 +01:00
config.yaml Add Essentia mood-extraction pipeline (essentia_xtractor + essentia_mood) 2026-09-05 10:08:07 +01:00
plan.md chore: add gitignore, wrapper script, config, and lockfile 2026-09-05 10:07:50 +01:00
prek.toml chore: add prek/ruff/mypy linting setup 2026-09-05 10:08:01 +01:00
pyproject.toml Add Forgejo CI workflow and bump deps/Python floor 2026-09-05 10:08:08 +01:00
README.md Tag NTS episodes as broadcast albums 2026-09-05 10:08:04 +01:00
uv.lock Add Forgejo CI workflow and bump deps/Python floor 2026-09-05 10:08:08 +01:00

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 together
  • beetsplug/ - 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_source field, 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 fixartistmbid command to re-sync only the artist MBIDs from MusicBrainz (db-only by default, -w to also write tags, -p to 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 --force to fix existing files.
    • bandcamp_tags - adds ./beet bandcamp_tags to append bandcamp genres+style to the existing genres field
  • beet - wrapper script that runs beet with BEETSDIR pointed at this directory, so config and database stay local
  • tests/ - 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: url field starts with https://tidal.com/
  • Bandcamp: comments field contains a *.bandcamp.com URL (after a Visit marker), or data_source is bandcamp (set by the beetcamp plugin)
  • SoundCloud / Mixcloud / YouTube / Archive.org: url (or contact) field is a link to the site, or comments contains the link after an extracted from marker
  • Subvert: encodedby tag mentions subvert.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