aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/indexer/indexer.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/indexer/indexer.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/indexer/indexer.py66
1 files changed, 54 insertions, 12 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/indexer/indexer.py b/packages/meshbay-node/src/meshbay_node/indexer/indexer.py
index eea5b4f..33e7210 100644
--- a/packages/meshbay-node/src/meshbay_node/indexer/indexer.py
+++ b/packages/meshbay-node/src/meshbay_node/indexer/indexer.py
@@ -609,8 +609,7 @@ class DirectoryIndexer:
for root, available in changed:
if available:
log.info("Root %r is back — rescanning", root.name)
- self._drop_root_entries(root)
- await self._scan_root(root)
+ await self._rescan_root(root)
touched = True
else:
# Frozen: entries stay, marked unavailable to members through
@@ -711,17 +710,61 @@ class DirectoryIndexer:
return [e for e in self._index.entries
if fold(e.path).split("/", 1)[0] == prefix]
+ # Everything on an IndexEntry that a scan does not produce. `_scan_root`
+ # fills id/name/path/size/type/added_at/hash_version from the file itself;
+ # every field below was derived by one of the enrichment passes and is
+ # nowhere on disk to be read back.
+ _ENRICHED_FIELDS = (
+ "duration", "thumb_hash", "width", "height",
+ "display_title", "season", "episode",
+ "artist", "album", "track_no", "taken_at", "camera",
+ "uploader_id", "uploader_pk",
+ )
+
+ async def _rescan_root(self, root: Root) -> int:
+ """
+ Rebuild one root's entries from disk, keeping what the files still say.
+
+ The two callers — `reconcile` when a root reappears, `plug_root` when
+ the operator plugs one back in — have to re-walk: the drive may have
+ changed while it was away. What they must not do is throw away the
+ enrichment. An entry's id is its content hash, so an entry that comes
+ back under the same id, name and path is the same bytes in the same
+ place, and every field the Videos, Music and Photos passes derived from
+ it still holds. Re-deriving them means minutes of tag reads, ffprobe
+ runs and rate-limited metadata lookups during which the operator's
+ library sits empty — which is exactly what a replug looked like.
+
+ Anything that does *not* match is left bare on purpose: a different id
+ is different content, and a different name or path can change the
+ filename and folder fallbacks that `display_title`, `track_no`,
+ `artist` and `album` fall back to. Those are the entries
+ `daemon._broadcast_index_change` re-enriches, off `rescanned_ids`.
+ """
+ carried = {(e.id, e.name, e.path): e for e in self._entries_under(root)}
+ self._drop_root_entries(root)
+ count = await self._scan_root(root)
+ for entry in self._entries_under(root):
+ old = carried.get((entry.id, entry.name, entry.path))
+ if old is None:
+ continue
+ for field in self._ENRICHED_FIELDS:
+ setattr(entry, field, getattr(old, field))
+ # It came back intact, so it is not one of the entries the daemon
+ # needs to enrich again.
+ self.rescanned_ids.discard(entry.id)
+ return count
+
def _drop_root_entries(self, root: Root) -> None:
"""
- Throw away a root's entries, always in order to rescan it.
+ Throw away a root's entries. Only ever called to rebuild them.
- Both callers — `reconcile` when a root reappears, `plug_root` when the
- operator plugs one back in — rebuild immediately, so nothing outside
- ever observes the gap: no deletion is broadcast, and the entries that
- come back have the same content-hash ids they had before. What they do
- not have is anything enrichment put on them, which is why the ids are
- recorded for `daemon._broadcast_index_change` to re-enrich rather than
- simply forgotten.
+ The ids are recorded because nothing outside can otherwise tell they
+ were rebuilt: no deletion is broadcast (the rescan is immediate) and
+ the entries come back under the same content-hash ids, so a diff
+ against the last broadcast reports neither an addition nor a deletion.
+ `_rescan_root` clears the ones it managed to carry over intact; what is
+ left is genuinely new to the apps and is re-enriched by the daemon.
"""
for entry in self._entries_under(root):
self.rescanned_ids.add(entry.id)
@@ -794,8 +837,7 @@ class DirectoryIndexer:
root.available = root.is_live()
if root.available:
log.info("Root %r plugged — rescanning", root.name)
- self._drop_root_entries(root)
- await self._scan_root(root)
+ await self._rescan_root(root)
self._restart_observer()
self._index.roots = self.roots.describe()
self._index.version = int(time.time())