aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/tests')
-rw-r--r--packages/meshbay-node/tests/test_enrich.py73
-rw-r--r--packages/meshbay-node/tests/test_enrich_audio.py80
-rw-r--r--packages/meshbay-node/tests/test_enrich_photo.py28
3 files changed, 181 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/test_enrich.py b/packages/meshbay-node/tests/test_enrich.py
index cff4d50..2205c7e 100644
--- a/packages/meshbay-node/tests/test_enrich.py
+++ b/packages/meshbay-node/tests/test_enrich.py
@@ -101,6 +101,79 @@ async def test_enricher_populates_fields_and_stores_thumbnail(tmp_path, media_ca
assert stored is not None and len(stored) > 0
+async def _run(enricher, entry, path):
+ done = asyncio.get_event_loop().create_future()
+
+ async def on_done(file_id, fields):
+ done.set_result((file_id, fields))
+
+ enricher.spawn(entry, path, on_done)
+ return await asyncio.wait_for(done, timeout=30)
+
+
+@pytestmark_ffmpeg
+@pytest.mark.asyncio
+async def test_enricher_reuses_cached_probe_and_thumbnail_without_touching_the_file_again(
+ tmp_path, media_cache):
+ """
+ The gap this closes: duration/width/height/thumb_hash only ever lived
+ on the in-memory GroupIndex entry, so every daemon restart re-ran
+ ffprobe and ffmpeg over every video in every group from scratch, even
+ though media_cache.db already had the answer. Proven strongly: the
+ source file is deleted between the two runs, so a real second probe or
+ frame grab would fail outright rather than merely being redundant.
+ """
+ clip = tmp_path / "Some.Movie.2015.1080p.mkv"
+ _make_clip(clip)
+ entry = IndexEntry(id="fileid_reuse", name=clip.name, path=clip.name,
+ size=clip.stat().st_size, type="video", added_at=0)
+
+ first_enricher = Enricher(media_cache)
+ _, first_fields = await _run(first_enricher, entry, clip)
+ assert first_fields.get("thumb_hash")
+
+ clip.unlink()
+ second_enricher = Enricher(media_cache)
+ _, second_fields = await _run(second_enricher, entry, clip)
+
+ assert second_fields == first_fields
+
+
+@pytestmark_ffmpeg
+@pytest.mark.asyncio
+async def test_a_renamed_file_re_parses_its_title_even_with_a_cached_probe(tmp_path, media_cache):
+ """
+ A cached probe/thumbnail must never leak into a stale title parse.
+ display_title/season/episode come from guessit against the *filename*,
+ which is exactly what a rename needs re-derived
+ (test_rename_reenrichment.py covers the scheduling half of this) — the
+ new cache-hit path added alongside them must not accidentally reuse a
+ stale parse just because it took the same shortcut for duration/width/
+ height. Proven the same strong way: the renamed file never exists on
+ disk at all, so a correct implementation still succeeds (guessit only
+ reads entry.name) while a regression that tried to re-probe or
+ re-thumbnail the "new" path would fail outright.
+ """
+ clip = tmp_path / "Old.Name.2015.mkv"
+ _make_clip(clip)
+ entry = IndexEntry(id="fileid_rename", name=clip.name, path=clip.name,
+ size=clip.stat().st_size, type="video", added_at=0)
+
+ enricher = Enricher(media_cache)
+ _, first_fields = await _run(enricher, entry, clip)
+ assert first_fields["display_title"] == "Old Name"
+
+ renamed_path = tmp_path / "New.Name.2020.mkv" # never created — proves nothing re-reads it
+ renamed_entry = IndexEntry(id="fileid_rename", name=renamed_path.name,
+ path=renamed_path.name, size=entry.size, type="video", added_at=0)
+ _, second_fields = await _run(enricher, renamed_entry, renamed_path)
+
+ assert second_fields["display_title"] == "New Name"
+ assert second_fields["width"] == first_fields["width"]
+ assert second_fields["height"] == first_fields["height"]
+ assert second_fields["thumb_hash"] == first_fields["thumb_hash"]
+
+
@pytestmark_ffmpeg
@pytest.mark.asyncio
async def test_enricher_handles_episode_with_season_from_folder(tmp_path, media_cache):
diff --git a/packages/meshbay-node/tests/test_enrich_audio.py b/packages/meshbay-node/tests/test_enrich_audio.py
index 5d52317..1485a1f 100644
--- a/packages/meshbay-node/tests/test_enrich_audio.py
+++ b/packages/meshbay-node/tests/test_enrich_audio.py
@@ -292,6 +292,86 @@ async def test_enricher_uses_a_sibling_cover_file_when_no_embedded_art(tmp_path,
assert stored == b"\xff\xd8\xff\xe0fake-jpeg-bytes"
+async def _run(enricher, entry, path, root_path=None):
+ done = asyncio.get_event_loop().create_future()
+
+ async def on_done(file_id, fields):
+ done.set_result((file_id, fields))
+
+ enricher.spawn(entry, path, on_done, root_path)
+ return await asyncio.wait_for(done, timeout=30)
+
+
+@pytestmark_ffmpeg
+@pytest.mark.asyncio
+async def test_enricher_reuses_a_cached_cover_without_rereading_it(tmp_path, media_cache):
+ """
+ The gap this closes: a cover already cached under this exact content
+ hash used to be re-extracted (or, for a sibling file, re-read from
+ disk) on every run regardless — media_cache.db had the answer and
+ nothing checked it first. Proven strongly: the sibling cover file is
+ deleted between the two runs, so a real second lookup would find
+ nothing rather than merely redo cheap work.
+ """
+ folder = tmp_path / "Some Artist" / "Some Album"
+ folder.mkdir(parents=True)
+ clip = folder / "01 - A Track.mp3"
+ _make_clip(clip)
+ (folder / "Folder.jpg").write_bytes(b"\xff\xd8\xff\xe0fake-jpeg-bytes")
+ entry = IndexEntry(id="fileid_reuse", name=clip.name,
+ path=str(clip.relative_to(tmp_path)),
+ size=clip.stat().st_size, type="audio", added_at=0)
+
+ first_enricher = AudioEnricher(media_cache)
+ _, first_fields = await _run(first_enricher, entry, clip, tmp_path)
+ assert first_fields.get("thumb_hash")
+
+ (folder / "Folder.jpg").unlink()
+ second_enricher = AudioEnricher(media_cache)
+ _, second_fields = await _run(second_enricher, entry, clip, tmp_path)
+
+ assert second_fields["thumb_hash"] == first_fields["thumb_hash"]
+
+
+@pytestmark_ffmpeg
+@pytest.mark.asyncio
+async def test_a_moved_track_re_resolves_artist_album_with_a_cached_cover(tmp_path, media_cache):
+ """
+ A cached cover must never leak into stale artist/album folder context.
+ Those come from _artist_album_from_ancestors against the file's
+ *location*, which is exactly what a move needs re-derived
+ (test_rename_reenrichment.py's audio equivalent covers the scheduling
+ half) — skipping the cover lookup must not accidentally skip that too.
+ """
+ old_folder = tmp_path / "Old Artist" / "Old Album"
+ old_folder.mkdir(parents=True)
+ clip = old_folder / "01 - A Track.mp3"
+ _make_clip(clip)
+ (old_folder / "Folder.jpg").write_bytes(b"\xff\xd8\xff\xe0fake-jpeg-bytes")
+ entry = IndexEntry(id="fileid_move", name=clip.name,
+ path=str(clip.relative_to(tmp_path)),
+ size=clip.stat().st_size, type="audio", added_at=0)
+
+ enricher = AudioEnricher(media_cache)
+ _, first_fields = await _run(enricher, entry, clip, tmp_path)
+ assert first_fields["artist"] == "Old Artist"
+ assert first_fields.get("thumb_hash")
+
+ new_folder = tmp_path / "New Artist" / "New Album"
+ new_folder.mkdir(parents=True)
+ new_clip = new_folder / clip.name
+ clip.rename(new_clip) # no cover moved along with it
+ moved_entry = IndexEntry(id="fileid_move", name=new_clip.name,
+ path=str(new_clip.relative_to(tmp_path)),
+ size=entry.size, type="audio", added_at=0)
+ _, second_fields = await _run(enricher, moved_entry, new_clip, tmp_path)
+
+ assert second_fields["artist"] == "New Artist"
+ assert second_fields["album"] == "New Album"
+ assert second_fields["thumb_hash"] == first_fields["thumb_hash"], (
+ "the cover is content-derived, not location-derived — it must still be reused")
+
+
@pytestmark_ffmpeg
def test_extract_cover_returns_none_when_no_apic_frame(tmp_path):
from mutagen import File as MutagenFile
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
@@ -62,6 +62,34 @@ async def test_enricher_populates_dimensions_and_stores_thumbnail(tmp_path, medi
@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
ordinary case, not an error — must not raise and must leave taken_at/