diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-24 10:04:46 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-24 10:04:46 +0200 |
| commit | 6af05abf410bbd038ce7fa6915a659defc509071 (patch) | |
| tree | 09b1c941fa446b077ff51282fa18250998528263 /packages/meshbay-node/tests/test_startup_scan_enrichment.py | |
| parent | c4981454078a59f776d484f0f1828f2fc5eaad09 (diff) | |
| download | meshbay-6af05abf410bbd038ce7fa6915a659defc509071.tar.gz | |
feat(node,hub): add Videos group app (poster grid, flat list, TMDB metadata)
Implements docs/mediacenter.md: a "Videos" group application built on the
existing files index rather than a separate catalogue. On the node side,
new indexer enrichment (technical probe, filename/season parsing, thumbnail
generation) runs per-file once an operator has chosen a video_root for the
group, plus a TMDB client for on-demand poster/metadata lookups (never
client-side, thumbnails delivered over the existing chunk path). On the hub
side, a new video-app.js renders a lazily-mounted poster grid or a
thumbnail-only flat list, with TMDB entirely optional per group.
Along the way: the global apps registry now drives Settings' default-tab
picker instead of a hardcoded list, and the video_root is configured from
group Settings (like uploads) rather than from Files, with the node
refusing to run any TMDB/thumbnail work until one is set.
Fixes several bugs found via live testing against a real library, notably
a race between two effects writing the same "image ready" state that could
leave a poster grid spinning forever on a same-tab revisit — see
mediacenter.md §5.4 for the full account of each one.
Diffstat (limited to 'packages/meshbay-node/tests/test_startup_scan_enrichment.py')
| -rw-r--r-- | packages/meshbay-node/tests/test_startup_scan_enrichment.py | 92 |
1 files changed, 92 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/test_startup_scan_enrichment.py b/packages/meshbay-node/tests/test_startup_scan_enrichment.py new file mode 100644 index 0000000..cdadad9 --- /dev/null +++ b/packages/meshbay-node/tests/test_startup_scan_enrichment.py @@ -0,0 +1,92 @@ +""" +Regression: indexer.initial_scan() (run once at startup, daemon.py's +_bg_scan) never itself calls on_change — that predates the Videos app, and +every existing caller only cared about the scan finishing, not about +notifying anyone. Enrichment (duration/thumb_hash/display_title/...) hangs +entirely off on_change (daemon._broadcast_index_change). + +Without an explicit call to _on_index_change right after the startup scan, +a file already on disk at boot — the common case, an existing library — +would never get enriched at all: only a file added later, while the node +is already running (seen by the watchdog), would trigger it. Found live +against a real library after the first restart with this feature enabled. + +Enrichment only runs once a group has a video_root configured (a group +with none set gets no TMDB/thumbnail work at all, docs/mediacenter.md +§5.2/§10) — this test's fake roster reports the shared root itself as the +configured video_root, so the enrichment-scheduling behaviour under test +is exercised the same way a real operator's group would be. +""" + +import asyncio + +import pytest +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from unittest.mock import MagicMock + +from meshbay_common.crypto import generate_gek +from meshbay_node.config import Config, HubConfig, NodeConfig, GroupConfig, KeystoreConfig +from meshbay_node.daemon import NodeDaemon +from meshbay_node.indexer import DirectoryIndexer +from meshbay_node.indexer.enrich import Enricher +from meshbay_node.media_cache import MediaCache + +from conftest import one_root + +pytestmark = pytest.mark.asyncio + + +def _free_port() -> int: + import socket + with socket.socket() as s: + s.bind(("127.0.0.1", 0)) + return s.getsockname()[1] + + +async def test_a_file_already_on_disk_at_startup_gets_enrichment_scheduled(tmp_path): + shared = tmp_path / "shared" + shared.mkdir() + (shared / "movie.mkv").write_bytes(b"not a real video, just needs to be indexed as one") + + config = Config( + hub=HubConfig(url="http://localhost:9999", username="testuser"), + node=NodeConfig(quic_port=_free_port(), ui_port=_free_port()), + groups=[GroupConfig( + id="a" * 32, name="test-group", shared_dir=str(shared), + visibility="private", quic_port=29012, + )], + keystore=KeystoreConfig(path=tmp_path / "keystore.enc"), + data_dir=tmp_path / "data", + ) + class _StubRoster: + async def video_root(self, group_id): + return "shared" # the root itself, i.e. "enrich the whole thing" + + daemon = NodeDaemon(config) + daemon._broadcast_coalesce_secs = 0.01 # real value would make this test wait 0.5s + daemon._media_cache = MediaCache(db_path=tmp_path / "media_cache.db") + await daemon._media_cache.open() + daemon._enricher = Enricher(daemon._media_cache) + daemon._roster = _StubRoster() + + sk_node = Ed25519PrivateKey.generate() + indexer = DirectoryIndexer( + roots=one_root(shared), group_id="a" * 32, + sk_node=sk_node, gek=generate_gek()) + + # Mirrors _bg_scan's actual sequence in daemon.py. + await indexer.initial_scan() + assert not daemon._enriched_attempted, ( + "nothing should be scheduled before _on_index_change is ever called") + + await daemon._on_index_change(indexer) + await asyncio.sleep(0.05) # let the coalescing timer fire _broadcast_index_change + + entry = next(iter(indexer.index.entries)) + assert entry.id in daemon._enriched_attempted, ( + "a file already on disk at startup must get enrichment scheduled the " + "first time its group's index is broadcast, not only on a later " + "watchdog-detected change to it") + + await daemon._media_cache.close() |