summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-25 00:29:24 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-25 00:29:24 +0200
commitd427118bd91d67f1a041e5daf267aebcd34ca9d7 (patch)
treeff9e0e71d4b115a871cf0902739942e6214b27d0 /packages/meshbay-node/src/meshbay_node
parent06c101154d544290d27f18d6fc08fb5f58a4e5d5 (diff)
downloadmeshbay-d427118bd91d67f1a041e5daf267aebcd34ca9d7.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')
-rw-r--r--packages/meshbay-node/src/meshbay_node/daemon.py25
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: