<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-node/tests/test_enrich_audio.py, branch main</title>
<subtitle>MeshBay — read-only public mirror</subtitle>
<id>https://git.meshbay.org/meshbay.git/atom?h=main</id>
<link rel='self' href='https://git.meshbay.org/meshbay.git/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/'/>
<updated>2026-08-26T16:09:44Z</updated>
<entry>
<title>fix(music): stop sharing one folder's cover across unrelated tracks, and merge various-artists compilations into one album</title>
<updated>2026-08-26T16:09:44Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-26T16:09:44Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=6fb948045b9c563ce591da289cdac1df6bed360c'/>
<id>urn:sha1:6fb948045b9c563ce591da289cdac1df6bed360c</id>
<content type='text'>
Two real, confirmed bugs in a large flat music library:

- enrich_audio.py's sibling-cover fallback assumed one folder is one
  release. A large flat "chart ranking" folder mixing dozens of unrelated
  artists carried several distinct WMP AlbumArt-cache guids (one per
  original album a track was ripped from), and the fallback picked
  whichever one WMP had copied to Folder.jpg — attaching one unrelated
  release's cover to every other track in the folder. Now refuses to pick
  a cover at all once 2+ distinct guids show up, rather than guess.

- music-app.js's groupMusicEntries grouped by artist first, album second,
  so a various-artists compilation (many genuinely different per-track
  artists, one shared album tag, no album-artist tag at all — a real
  ~20-track soundtrack rip has exactly this shape) could never be
  recognized as one release: every track landed alone in its own artist's
  bucket and got folded into a singleton pile. Now detects an album key
  shared across 2+ distinct artist keys and merges those tracks into one
  compilation card under a "Various" heading instead.

Both verified against real, previously-affected files and live in the
browser: the shared wrong cover is gone, and the compilation renders as
one card with all its tracks in order.
</content>
</entry>
<entry>
<title>fix(node): make Photos/Video/Music enrichment survive node restarts</title>
<updated>2026-08-25T10:52:32Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-25T10:52:32Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=9e66c11b15b7103e963dcecf88fb152ed1e74253'/>
<id>urn:sha1:9e66c11b15b7103e963dcecf88fb152ed1e74253</id>
<content type='text'>
Only thumbnail bytes were ever durable in media_cache.db — every other
derived field (photo width/height/EXIF, video ffprobe duration/dims,
audio cover art) lived solely on the in-memory GroupIndex entry, so a
node restart re-decoded every photo through Pillow, re-ran ffprobe on
every video, and re-scanned for every album cover from scratch, even
though the answers already sat in the cache.

Adds photo_meta and video_meta tables (content-only fields, keyed by
file_id) and checks them before doing the expensive work. Audio gets no
new table: mutagen reads tags and duration in one inseparable call, so
caching duration alone buys nothing — instead cover-art extraction alone
is skipped via a new skip_cover flag when a cached cover already exists.

Deliberately excluded from all three caches: anything derived from the
filename or folder path (video display_title/season/episode via guessit,
audio artist/album folder-fallback) — those must keep being recomputed
fresh so a rename/move is still correctly re-derived by the existing
_reenrich_renamed_*_entries mechanisms, instead of silently handing back
a stale parse under the new name/location.

Regression tests prove cache reuse by deleting the source file (or cover)
between two enrichment runs, and prove rename/move correctness survives
the new cache by renaming/moving to a path that never exists on disk.
</content>
</entry>
<entry>
<title>feat(node): recognize WMA and Musepack as audio, read their real tag keys</title>
<updated>2026-08-24T18:31:04Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-24T18:31:04Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=6c56877675958e40ae526e0965266ab68aae44f2'/>
<id>urn:sha1:6c56877675958e40ae526e0965266ab68aae44f2</id>
<content type='text'>
A real-library scan turned up 250 .wma and 23 .mpc files that the indexer
was silently classifying as "other" — genuinely lost from the Music app,
not a consolidation-rule artifact (checked separately: the grouping logic
itself drops nothing). Both are now indexed as audio and tagged properly:

- WMA has no mutagen "easy" wrapper, so the generic tag reader was reading
  nothing from it at all. Reads the real ASF keys directly instead
  (Title/Author/WM-AlbumTitle/WM-TrackNumber), confirmed against a real
  sample file before writing the mapping.
- Musepack's format auto-detection is unreliable enough (misidentified a
  real .mpc as MP3 in spot checks) that it now always opens by its own
  class instead of guessing from content.
- Filters out another placeholder value found along the way: a French
  ripping tool's auto-generated "Album inconnu (&lt;timestamp&gt;)".

Neither format decodes natively in a browser's &lt;audio&gt; element, so this
gets them correctly visible, tagged, and covered — not yet playable
in-browser. That would need server-side transcoding, deliberately left
out of this change.
</content>
</entry>
<entry>
<title>feat(hub): consolidate loose tracks, "&amp;"/"and" fold, player close/queue</title>
<updated>2026-08-24T17:46:06Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-24T17:46:06Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=62253b9592a83ce152d0c64471e20514218fb132'/>
<id>urn:sha1:62253b9592a83ce152d0c64471e20514218fb132</id>
<content type='text'>
Album-grid readability, part two:

- groupMusicEntries (music-app.js): an album bucket left with exactly
  one track - a real album tag, but only one song from it, not the
  whole release - clutters the grid the same way an untagged loose
  track does. Both kinds now fold into one "&lt;artist&gt; - Various" tile
  per artist, unless there is only one leftover track overall, where
  relabeling buys nothing and the track keeps its own name (or the
  generic placeholder, if it never had one).
- foldKey also normalizes "&amp;" vs "and" ("Artist &amp; The Band" / "Artist
  and The Band" is one act, tagged both ways across different rips of
  the same catalogue) alongside the existing case/whitespace fold.
- music-player.js: a close button pauses and tears the player down;
  an unmount cleanup effect (pause, revoke every cached blob URL)
  fires either way, whether that's the close button or the shell
  tearing the bar down on its own. A "current queue" button opens an
  overlay listing the whole playing queue with the current track
  highlighted, click any to jump to it - works identically regardless
  of how the queue was built (an album, the consolidated misc bucket,
  a single standalone track), since it only ever reads the player's
  own live tracks/order/pos.
- group-page.js: this component is not remounted when switching to a
  *different* group on the same /group/:id route (only the groupId
  prop changes) - so without an explicit reset, music from one group
  would carry into the next one opened. Resets musicQueue to null on
  groupId change; a tab switch inside one group still leaves it alone.
- Scrubbed real artist/band names that had leaked into code comments
  and test fixtures (enrich_audio.py's docstrings, several
  test_enrich_audio.py assertions, a music-app.js comment) - replaced
  with generic placeholders, no behavioural change.
- i18n: music.various, music.player_close, music.player_queue,
  music.queue_title added across all ten locales.

Client-side only except none of this touches the node at all. npm run
sync-ui re-run. Full suite: 1129 passed, no regressions.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01KBi7ALLGfwcjBXt57yNMcy
</content>
</entry>
<entry>
<title>fix(node): stop inventing a fake artist from the shared root's name</title>
<updated>2026-08-24T16:30:00Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-24T16:30:00Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=584730f9486e153c6c477293f03a95e78f5ab5f3'/>
<id>urn:sha1:584730f9486e153c6c477293f03a95e78f5ab5f3</id>
<content type='text'>
Music grouping was measured against a real ~5700-file library and came
back worse than a plain file listing. Root cause: the artist/album
ancestor walk always climbed exactly two levels (parent = album,
grandparent = artist) with no idea where the group's own shared root
was. Any file in a flat top-level folder — common here: bare
`Artist/track.mp3`, no album subfolder at all — had its "grandparent"
resolve to the root directory's own name, so the artist got replaced
by the share's name. Measured: 289 of 5664 tracks across 41 real,
unrelated artists (Ben Harper, Dire Straits, Jimi Hendrix, Janis
Joplin, ...) collapsed into one fake artist this way — the single
biggest bucket in the whole library, ahead of every real one.

- `_artist_album_from_ancestors` now takes the entry's own root
  boundary (daemon.py resolves it via `RootSet.split`) and refuses to
  read it as a name. A file sitting in a top-level folder — genuinely
  ambiguous, artist or a standalone album/compilation — is handled by
  `_split_top_level_folder`: split on "Artist - Album" when the
  (cleaned) folder name has that shape, otherwise the whole name
  becomes the artist alone, the more common real case here.
- `_clean_tag` treats known tagger placeholders ("No Artist", a French
  tool's "Nouvel artiste (334)") as absent rather than a real value —
  they were just as truthy as a real name and were locking out the
  fallback that would have done better. "Various Artists" is kept, a
  real compilation credit rather than a placeholder.
- A `title` tag that's the bare filename copied verbatim (track number
  included — found live on a whole CD-single) is stripped through the
  same prefix rule the filename parser already used
  (`title_parse.strip_track_prefix`), since a tag normally wins over
  the parsed title.
- Cover art: only 11% of a 400-file sample had embedded art (expected
  for this era of rip), but 267 loose cover images sit beside the
  tracks across the library (Windows Media Player's `Folder.jpg`/
  `AlbumArt_{guid}_*.jpg`, manual `cover.jpg`) and were never looked
  at. `_find_sibling_cover` checks the track's own folder before
  giving up — measured coverage 11% -&gt; 26% on the same library, zero
  network calls.

`_enriched_attempted` is in-memory and resets on restart, so a node
restart is enough to re-run enrichment over an already-scanned library
with the fixed logic — no rescan flag, no cache to clear by hand.

22 tests in test_enrich_audio.py (11 new), including the exact
regression case end to end through the real pool. Full suite: 1129
passed, no regressions.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01KBi7ALLGfwcjBXt57yNMcy
</content>
</entry>
<entry>
<title>feat(node): Music app node-side — indexing, MusicBrainz enrichment, protocol</title>
<updated>2026-08-24T15:12:36Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-24T15:12:36Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=941d1a135dd7b03834576855e8e9fdaa24c4e406'/>
<id>urn:sha1:941d1a135dd7b03834576855e8e9fdaa24c4e406</id>
<content type='text'>
Implements the node half of docs/musicbay.md against MNP 0.8:

- IndexEntry gains artist/album/track_no (reuses duration/thumb_hash/
  display_title, already generic). New musicbrainz_config/_enabled and
  music_meta_req/_resp message pairs, mirroring the TMDB shape.
- title_parse.parse_track_filename: track-number-prefix + title parsing,
  fallback-only (embedded tags are the primary source, unlike Videos).
- indexer.enrich_audio.AudioEnricher: mutagen-based tag/embedded-cover
  extraction through its own bounded pool (asyncio.to_thread, no
  subprocess — no ffmpeg-shaped deadlock risk). Gated on "music" in a
  group's enabled_apps rather than a video_root-style scoped folder.
- musicbrainz.py: MusicBrainzClient — no API key (unlike TMDB), just a
  self-imposed ~1 req/s pace and a configurable, non-default User-Agent
  contact string; inert (no calls at all) when no contact is configured,
  never sends an unidentified client.
- media_cache.py: file_mbid/mbid_meta tables alongside the existing TMDB
  ones, cover art reusing the thumbs table via a synthetic
  musicbrainz:{mbid} id, pruned on file deletion.
- roster.py/ops.py/webrtc_server.py: musicbrainz_contact (node-wide) and
  musicbrainz_enabled (per-group, from the start) as signed operator
  settings, ALLOWED_APPS gains "music", _do_music_meta_request resolves
  and caches a release-level MusicBrainz match per (artist, album).
- daemon.py: AudioEnricher/MusicBrainzClient wired alongside the video
  ones; a group's existing library is swept when "music" is newly
  enabled (no video_root equivalent — see musicbay.md §2.1).

41 new tests (musicbrainz.py against a mocked transport, admin-op policy
for both new settings, media_cache round-trip/pruning, enrich_audio
end-to-end against real ffmpeg-generated MP3s). Full suite (common +
node + hub): 1116 passed, no regressions.

Client-side (music-app.js, persistent player bar) not started yet.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01KBi7ALLGfwcjBXt57yNMcy
</content>
</entry>
</feed>
