aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_indexer.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-24 15:57:41 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-24 15:57:41 +0200
commitc5585beab3d6adefaa2ef9444946dd3816960a7c (patch)
treea321e540c2db0458716d526e4a045fca123937b2 /packages/meshbay-node/tests/test_indexer.py
parent317f09328ed8bf20148b707470c9b0fe82e59575 (diff)
downloadmeshbay-c5585beab3d6adefaa2ef9444946dd3816960a7c.tar.gz
fix(node,hub): HEVC transcode fallback, live-add progress, per-group TMDB toggle
Three bugs found live testing the Videos app against a real HEVC/EAC3 show, plus a design change requested afterward: - Streaming always did "-c:v copy", which faithfully reports a source's real hev1 codec string but is unplayable in a browser with no HEVC decoder (most Chrome/Linux builds). The node now transcodes to H264 whenever the probed codec is browser-incompatible (media_probe.py's new BROWSER_INCOMPATIBLE_VIDEO_CODECS), with a `transcode_incompatible_video` node.toml opt-out for operators who know their viewers already decode it. - Dropping a whole season into an already-watched folder gave no scanning indicator and no progress bar: IndexProgress was only ever updated by the two bulk scan paths, never by the real-time per-file watchdog path (_schedule_update/_debounce/_update_entry). That path now accounts a "burst" the same way, without double-counting a file rewritten mid-debounce. - A stray literal "0" rendered in the video detail modal when there was no TMDB match (`meta.confidence` is 0, and `0 && x` renders "0" in JSX/htm, not nothing) — `confident` is now a real boolean. - Whether TMDB is used at all moves from a node-wide setting to per-group (OP_TMDB_ENABLED/tmdb_enabled/tmdb_enabled_ack, scoped like OP_VIDEO_ROOT): an operator running a real media-library group alongside test/demo groups on one node wants outbound TMDB traffic for the one that needs it, not all of them. The custom API token and query language stay node-wide, one shared credential/cache (tmdb_config/OP_TMDB_CONFIG, unchanged reasoning). MNP_VERSION 0.6 -> 0.7, additive. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LAmyXtc6dAADsH23ydXQpY
Diffstat (limited to 'packages/meshbay-node/tests/test_indexer.py')
-rw-r--r--packages/meshbay-node/tests/test_indexer.py76
1 files changed, 76 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/test_indexer.py b/packages/meshbay-node/tests/test_indexer.py
index 68ccc4c..3eac39e 100644
--- a/packages/meshbay-node/tests/test_indexer.py
+++ b/packages/meshbay-node/tests/test_indexer.py
@@ -356,6 +356,82 @@ async def test_progress_stops_even_when_hashing_raises(tmp_path, sk_node, gek):
"an exception mid-scan must not leave the scanning flag stuck on"
+@pytest.mark.asyncio
+async def test_realtime_watchdog_add_reports_progress_like_a_bulk_scan(tmp_path, sk_node, gek):
+ """
+ Found live: dropping a whole season into an already-watched folder gave
+ no scanning indicator and no progress bar at all — only the initial scan
+ and the periodic reconcile backstop ever touched `progress`, never the
+ real-time per-file watchdog path (_schedule_update/_debounce/
+ _update_entry). Drives that path directly (as _WatchdogHandler would),
+ without a real filesystem observer, exactly like test_root_availability.py
+ already does for _update_entry alone.
+ """
+ d = tmp_path / "shared"
+ d.mkdir()
+ paths = []
+ for i in range(3):
+ p = d / f"ep{i}.mkv"
+ p.write_bytes(os.urandom(1024 * (i + 1)))
+ paths.append(p)
+ total_size = sum(p.stat().st_size for p in paths)
+
+ indexer = DirectoryIndexer(roots=one_root(d), group_id="g", sk_node=sk_node,
+ gek=gek, debounce_secs=0.01)
+ indexer._loop = asyncio.get_running_loop()
+ assert indexer.progress.scanning is False
+
+ for p in paths:
+ indexer._schedule_update(p)
+ # _schedule_update only posts _debounce via call_soon_threadsafe (it is
+ # written to be called from the watchdog thread) — give the loop one
+ # turn to actually run the three posted calls before asserting on them.
+ await asyncio.sleep(0)
+
+ # All three scheduled near-simultaneously (as a burst of watchdog events
+ # for one `mv` would arrive) — the indicator must flip on immediately,
+ # before any single file has actually finished hashing.
+ assert indexer.progress.scanning is True
+ assert indexer.progress.total_bytes == total_size
+ assert indexer.progress.scanned_bytes == 0
+
+ await asyncio.sleep(0.05) # past debounce_secs; lets all three fire and finish
+
+ assert indexer.progress.scanning is False, "must end idle, not stuck scanning"
+ assert indexer.progress.scanned_bytes == total_size
+ assert indexer.progress.total_bytes == total_size
+ assert len(indexer.index.entries) == 3
+
+
+@pytest.mark.asyncio
+async def test_realtime_watchdog_rapid_rewrite_does_not_double_count(tmp_path, sk_node, gek):
+ """A file rewritten during its own debounce window (on_modified firing
+ again before the first timer fires) must count its size once, not once
+ per event — the old timer is cancelled, and its accounting must transfer
+ to whichever fire() actually runs rather than being counted twice."""
+ d = tmp_path / "shared"
+ d.mkdir()
+ p = d / "ep0.mkv"
+ p.write_bytes(os.urandom(2048))
+
+ indexer = DirectoryIndexer(roots=one_root(d), group_id="g", sk_node=sk_node,
+ gek=gek, debounce_secs=0.05)
+ indexer._loop = asyncio.get_running_loop()
+
+ indexer._schedule_update(p)
+ indexer._schedule_update(p) # re-triggered before the first timer fires
+ indexer._schedule_update(p)
+ await asyncio.sleep(0) # let the three posted _debounce calls actually run
+
+ assert indexer.progress.total_bytes == p.stat().st_size, \
+ "one file re-triggered must count its size once, not three times"
+
+ await asyncio.sleep(0.1)
+
+ assert indexer.progress.scanning is False
+ assert indexer.progress.scanned_bytes == p.stat().st_size
+
+
# ── Off-loop directory walks, reconcile backoff ─────────────────────────────
@pytest.mark.asyncio