diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-25 00:29:24 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-25 00:29:24 +0200 |
| commit | d427118bd91d67f1a041e5daf267aebcd34ca9d7 (patch) | |
| tree | ff9e0e71d4b115a871cf0902739942e6214b27d0 /packages/meshbay-node/src/meshbay_node/daemon.py | |
| parent | 06c101154d544290d27f18d6fc08fb5f58a4e5d5 (diff) | |
| download | meshbay-0.7.tar.gz | |
fix(node): scope _enriched_attempted by group, not just content hash0.7
Major finding: entry.id is a content hash, so the exact same physical
file — the same MP3, byte-for-byte — indexed into two different groups
(a shared library reused across several demo/test groups, or genuinely
the same folder shared into two groups) produces the *same id* in both.
_enriched_attempted was a single flat set of bare ids shared across every
group this node hosts. The moment one group's copy got enriched, every
other group's otherwise-identical copy read as "already attempted" and
was skipped forever — nothing else ever revisits an id once it's in this
set. That group's Music tab (or Videos tab, same bug, same set) showed
every affected file at duration 0 with no artist/album/thumbnail,
permanently, no matter how long you waited or how many times you
reloaded — group A having been enriched first was enough to silently
starve every later group of the same content.
Now keyed by (group_id, entry.id) throughout — the enrichment gate, the
sweep, and the rename re-enrichment path, for both video and audio (they
already shared the one set, and the collision risk is identical for
both). New regression test constructs two groups with byte-identical
audio content and confirms both enrich independently.
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/daemon.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/daemon.py | 25 |
1 files changed, 17 insertions, 8 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/daemon.py b/packages/meshbay-node/src/meshbay_node/daemon.py index 0015f1a..667e1eb 100644 --- a/packages/meshbay-node/src/meshbay_node/daemon.py +++ b/packages/meshbay-node/src/meshbay_node/daemon.py @@ -160,8 +160,17 @@ class NodeDaemon: # restart retries everything, matching the "disposable, rebuildable" # stance the rest of this cache takes (docs/mediacenter.md §1/§2). # Shared across the video and audio enrichment paths — content- - # addressed ids never collide between the two. - self._enriched_attempted: set[str] = set() + # addressed ids never collide between the two. Keyed by + # (group_id, entry.id), not entry.id alone: the id is a content + # hash, so the same physical file shared into two different groups + # (found live — overlapping test libraries across several demo + # groups) produces the same id in both. A bare-id set marked the + # second group's copy "already attempted" the moment the first + # group's enrichment ran, even though nothing had ever populated + # *that* group's own index — every file in the second group stayed + # at duration 0 with no artist/album, permanently, since nothing + # ever revisits an id already in this set. + self._enriched_attempted: set[tuple[str, str]] = set() self._roster: Roster | None = None self._indexers: list[DirectoryIndexer] = [] self._tasks: list[asyncio.Task] = [] @@ -1098,14 +1107,14 @@ class NodeDaemon: if not video_root: return for entry in entries: - if entry.type != "video" or entry.id in self._enriched_attempted: + if entry.type != "video" or (indexer.group_id, entry.id) in self._enriched_attempted: continue if not _under_video_root(entry.path, video_root): continue file_path = entry_abs_path(indexer.roots, entry) if not file_path or not file_path.exists(): continue - self._enriched_attempted.add(entry.id) + self._enriched_attempted.add((indexer.group_id, entry.id)) async def on_done(file_id: str, fields: dict, _indexer=indexer) -> None: await self._on_enriched(_indexer, file_id, fields) @@ -1151,7 +1160,7 @@ class NodeDaemon: old = previous.get_entry(entry.id) if old is None or (old.name == entry.name and old.path == entry.path): continue - self._enriched_attempted.discard(entry.id) + self._enriched_attempted.discard((indexer.group_id, entry.id)) await self._enrich_new_video_entries(indexer, updates) async def _enrich_new_audio_entries(self, indexer: DirectoryIndexer, entries: list) -> None: @@ -1172,14 +1181,14 @@ class NodeDaemon: return root_boundary = indexer.roots.resolve(audio_root, require_available=False) for entry in entries: - if entry.type != "audio" or entry.id in self._enriched_attempted: + if entry.type != "audio" or (indexer.group_id, entry.id) in self._enriched_attempted: continue if not _under_audio_root(entry.path, audio_root): continue file_path = entry_abs_path(indexer.roots, entry) if not file_path or not file_path.exists(): continue - self._enriched_attempted.add(entry.id) + self._enriched_attempted.add((indexer.group_id, entry.id)) async def on_done(file_id: str, fields: dict, _indexer=indexer) -> None: await self._on_enriched(_indexer, file_id, fields) @@ -1223,7 +1232,7 @@ class NodeDaemon: old = previous.get_entry(entry.id) if old is None or (old.name == entry.name and old.path == entry.path): continue - self._enriched_attempted.discard(entry.id) + self._enriched_attempted.discard((indexer.group_id, entry.id)) await self._enrich_new_audio_entries(indexer, updates) async def _on_enriched(self, indexer: DirectoryIndexer, file_id: str, fields: dict) -> None: |