From 9e66c11b15b7103e963dcecf88fb152ed1e74253 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Tue, 25 Aug 2026 12:52:32 +0200 Subject: fix(node): make Photos/Video/Music enrichment survive node restarts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- packages/meshbay-node/tests/test_enrich_photo.py | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) (limited to 'packages/meshbay-node/tests/test_enrich_photo.py') diff --git a/packages/meshbay-node/tests/test_enrich_photo.py b/packages/meshbay-node/tests/test_enrich_photo.py index e0c1b73..877a71d 100644 --- a/packages/meshbay-node/tests/test_enrich_photo.py +++ b/packages/meshbay-node/tests/test_enrich_photo.py @@ -61,6 +61,34 @@ async def test_enricher_populates_dimensions_and_stores_thumbnail(tmp_path, medi assert max(thumb.size) <= 480 +@pytest.mark.asyncio +async def test_enricher_reuses_cached_meta_without_touching_the_file_again(tmp_path, media_cache): + """ + The gap this closes: enrichment fields only ever lived in the in-memory + GroupIndex, so every daemon restart re-ran Pillow over every photo in + every configured root from scratch, even though media_cache.db (the + thumbnail bytes) already had the answer. A second PhotoEnricher sharing + the same media_cache — standing in for "the daemon restarted" — must + reuse it instead. Proven strongly: the source file is deleted between + the two runs, so a second real decode attempt would fail outright + rather than merely being redundant. + """ + img = tmp_path / "reused.jpg" + _save_jpeg(img, size=(300, 200)) + entry = IndexEntry(id="fileid_reuse", name=img.name, path=img.name, + size=img.stat().st_size, type="image", added_at=0) + + first_enricher = PhotoEnricher(media_cache) + _, first_fields = await _run(first_enricher, entry, img) + assert first_fields.get("thumb_hash") + + img.unlink() # a real second decode would now raise, not just be wasteful + second_enricher = PhotoEnricher(media_cache) + _, second_fields = await _run(second_enricher, entry, img) + + assert second_fields == first_fields + + @pytest.mark.asyncio async def test_enricher_no_exif_degrades_gracefully(tmp_path, media_cache): """A screenshot or a re-saved image with no EXIF block at all is the -- cgit v1.2.3