fix: copy all clip snapshots to output_dir on ingest #93
Loading…
Reference in a new issue
No description provided.
Delete branch "refs/pull/93/head"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Previously _copy_thumbnail saved only the middle snapshot alongside the MP4. With delete_after_transcode = true every other snapshot for the clip was permanently deleted, leaving the Snapshots page with at most one image.
_copy_thumbnail is replaced by _persist_snapshots(clip, camera_root, output_path) which copies the full set of clip snapshots into a per-clip subdirectory (output_dir/camera/filename_stem/) preserving the relative path structure from camera_root so that _snapshot_time and _snapshot_label continue to work. The middle frame is still written as the .jpg preview thumbnail alongside the MP4.
A new collect_snapshots_from_output() function in the snapshots route looks in this directory as a fallback when the camera-root search returns nothing, slotting in before the existing single-thumbnail fallback so old clips continue to work.
Closes #92
Review responses — commit
e20d058This comment addresses all review comments on this PR.
🔴 Fixed:
_persist_snapshotsblocks the event loop (comments 16639880, 16633757)Fixed in commit
e20d058. Both_doneclosures now call_persist_snapshotsvialoop.run_in_executor(None, ...)so the blockingshutil.copy2loop runs in a thread-pool thread rather than on the event loop. Existing testtest_on_done_copies_snapshotcontinues to cover the full behaviour.🔴 Fixed: OSError silently discarded (comment 16633796)
Fixed in commit
e20d058. Added a module-level_logger = logging.getLogger(__name__)and changed the bareexcept OSError: return Noneto log aWARNINGwithexc_info=Truebefore returningNone. New testtest_persist_snapshots_logs_on_oserrorverifies the warning is emitted.✅ Not actioned: symlink path-traversal (comments 16633676, 16633679, 16633682) — stale
These issues were already resolved by commit
1c0b46e('extract _build_allowed_roots helper, resolve symlinks in path checks') which landed onmainbefore this PR was rebased._resolve_snapshotsandclip_snapshot_imageboth now call.resolve()on both sides before theis_relative_tocheck.✅ Not actioned:
allowed_rootsduplication (comments 16633706, 16633709, 16633763, 16633790) — staleAlready fixed by the
_build_allowed_roots(cfg)helper introduced in1c0b46e. Both route handlers now call it.✅ Not actioned:
_safe_filenameis dead code (comment 16639865)_safe_filenameis active code, called at lines 67 and 132 oflifespan.pyin both_scan_cameraand the_on_filewatcher handler.✅ Not actioned:
str | Noneannotation style (comment 16633703)str | Noneis the correct Python 3.10+ union syntax and is preferred overOptional[str]in this codebase (Python ≥ 3.11 per project config). No change needed.✅ Not actioned: regex recompilation (comment 16633745)
_LEADING_DIGITS_REis already compiled once at module level (line 13 ofsnapshots.py). There is no per-call compilation.✅ Not actioned:
collect_snapshots_from_outputempty-directory case (comment 16633721)sorted(clip_dir.glob("**/*.jpg"))evaluates to[]when the directory contains no.jpgfiles. The case is already handled correctly.✅ Not actioned:
mkdirinside copy loop (comments 16639859, 16639883)Each snapshot maps to a unique subdirectory within
clip_dir(Dahua structure:<date>/<channel>/jpg/<HH>/<MM>/). Thedst.parent.mkdir(parents=True, exist_ok=True)call must stay per-snapshot because different snapshots land in different directories. Theexist_ok=Trueflag makes re-entrant calls cheap; extracting a pre-pass would add complexity without meaningfully reducing I/O.✅ Not actioned: fallback logic duplicated in two routes (comment 16639862)
The two-line pattern appears in
clip_snapshot_albumandclip_snapshot_image. Extracting a helper would require threadingcfgandclipthrough another function for negligible gain. Left as-is per the project's guideline against premature abstractions.✅ Not actioned: test path construction duplication (comment 16639868)
The snapshot directory paths are constructed in two distinct tests with different assertions. Extracting a fixture would obscure what each test is actually setting up.
✅ Not actioned: recursive glob performance (comments 16639874, 16633760)
collect_snapshots_from_outputis a fallback path called at most once per UI request, and only when camera-root snapshots are absent. The**/*.jpgglob is correct and readable; micro-optimising a cold path is not warranted.✅ Not actioned:
sorted()materialises full list (comment 16639877)The sorted list is required for stable integer indexing (
snapshots[n]). A lazy generator cannot be indexed. No change possible.✅ Not actioned:
_doneclosure duplication (comments 16633712, 16633715)The two
_doneclosures capture different outer-scope bindings (cam.rootvs_cam.root,clipvsmatch). Extracting a shared factory would require passing all five captured variables as arguments, adding indirection without reducing lines.✅ Not actioned: per-request scan caching (comments 16633751, 16633754)
Caching scan results across HTTP requests is a separate architectural concern unrelated to this PR. Left for a dedicated refactor if needed.
✅ Not actioned: SRP violation in
_persist_snapshots(comment 16633781)The function performs one logical operation: persist a clip's snapshot assets. Copying all frames and deriving the thumbnail are inseparable steps of that single responsibility.
✅ Not actioned: fallback chain fragility (comment 16633793)
The three-step chain (
collect_snapshots_for_clip→collect_snapshots_from_output→_resolve_snapshots) has clear, linear semantics. Each step handles a distinct case.✅ Not actioned:
_resolve_snapshotsnaming (comment 16633784)_resolve_snapshotspre-dates this PR. Renaming it is out of scope.✅ Not actioned: directory-structure coupling (comment 16633787)
collect_snapshots_from_outputand_persist_snapshotsmust agree on the directory layout — that is intentional and documented incollect_snapshots_from_output's docstring.✅ Not actioned:
_snapshot_labelcalled per-frame in template (comment 16633766)Pre-existing code outside the scope of this PR.
✅ Not actioned:
allowed_rootsper-request cost (comment 16633748)_build_allowed_rootsis a single comprehension over a small in-memory list. The cost is negligible and no caching is needed.