From 2d144d76cee55cf8faaacf196e716a0930dfd7e9 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 26 Aug 2026 01:08:58 +0200 Subject: fix(node,hub): key music/media metadata lookups by file_id, not path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IndexEntry.path is the *folder* a file is in (indexer.py's _virtual_dir docstring: "the directory a file appears in"), not the file itself. GroupIndex.get_entry_by_path() treated it as if it named one file, and every one of its four callers did too: _do_music_meta_request, _do_media_meta_request, _do_tmdb_override, and _admin_exec_tmdb_override. Any two files sharing a folder — an album is one folder with many tracks, a season is one folder with many episodes — collided: a lookup by path silently returned whichever entry the index happened to iterate to first, regardless of which file the client actually asked about. Found live (2026-08-25): three unrelated albums ("High Tone - Various", two "Le Peuple de l'Herbe" albums) all showed the same MusicBrainz cover, because all their representative tracks happened to sit in one "high_tone" folder alongside a track that legitimately matched that cover. A force-reload didn't help — the bug is server-side, not a stale client state. Fixed by keying these four request/response pairs by `file_id` (the entry's own content hash — already unique, already how every other lookup in the system identifies a file) instead of `path`, both in the wire messages (music_meta_req/resp, media_meta_req/resp, tmdb_override) and in music-app.js/video-app.js's own hooks. GroupIndex.get_entry_by_path is now unused and removed — GroupIndex.get_entry(file_id) already did the right thing. No test previously exercised either handler with two entries sharing a folder — the only existing coverage (test_tmdb_override_policy.py) gave each entry its own folder, so the bug never had a chance to show up. Added that scenario there and in two new test files, all confirmed failing against the pre-fix code before being confirmed green against the fix. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_013XSohfUQQiaE77qyFLgSv3 --- packages/meshbay-hub/src/meshbay_hub/static/music-app.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'packages/meshbay-hub/src/meshbay_hub/static/music-app.js') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/music-app.js b/packages/meshbay-hub/src/meshbay_hub/static/music-app.js index 298e259..ae482ad 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/music-app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/music-app.js @@ -131,21 +131,21 @@ function groupMusicEntries(entries, audioRoot) { // -- album cover, MusicBrainz fetched lazily and only when actually needed -- -function useMusicMeta(transportRef, path, active) { +function useMusicMeta(transportRef, fileId, active) { const [meta, setMeta] = useState(null); useEffect(() => { - if (!active || !path) return; + if (!active || !fileId) return; let cancelled = false; (async () => { const transport = transportRef.current; if (!transport || !transport.connected) return; try { - const resp = await transport.fetchMusicMeta(path); + const resp = await transport.fetchMusicMeta(fileId); if (!cancelled) setMeta(resp); } catch { if (!cancelled) setMeta({ confidence: 0 }); } })(); return () => { cancelled = true; }; - }, [path, active]); + }, [fileId, active]); return meta; } @@ -196,7 +196,7 @@ function AlbumCard({ album, transportRef, gekRef, musicbrainzEnabled, onOpen }) // Only when nothing in the library already gives us a cover -- the common // case (a well-tagged rip with embedded art) needs no network call at all. const needsLookup = musicbrainzEnabled && !repTrack.thumb_hash; - const meta = useMusicMeta(transportRef, repTrack.path, needsLookup); + const meta = useMusicMeta(transportRef, repTrack.id, needsLookup); const coverHash = repTrack.thumb_hash || (meta && meta.cover_thumb_hash) || null; return html` @@ -218,7 +218,7 @@ function AlbumCard({ album, transportRef, gekRef, musicbrainzEnabled, onOpen }) function MusicDetailModal({ album, transportRef, gekRef, musicbrainzEnabled, onClose, onPlayQueue }) { const repTrack = album.tracks.find((tr) => tr.thumb_hash) || album.tracks[0]; const needsLookup = musicbrainzEnabled && !repTrack.thumb_hash; - const meta = useMusicMeta(transportRef, repTrack.path, needsLookup); + const meta = useMusicMeta(transportRef, repTrack.id, needsLookup); const coverHash = repTrack.thumb_hash || (meta && meta.cover_thumb_hash) || null; return html` -- cgit v1.2.3