diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-24 15:57:41 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-24 15:57:41 +0200 |
| commit | c5585beab3d6adefaa2ef9444946dd3816960a7c (patch) | |
| tree | a321e540c2db0458716d526e4a045fca123937b2 /packages/meshbay-node/tests/test_indexer.py | |
| parent | 317f09328ed8bf20148b707470c9b0fe82e59575 (diff) | |
| download | meshbay-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.py | 76 |
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 |