diff --git a/frigate/test/test_media.py b/frigate/test/test_media.py index f490239cf..13a90e1cd 100644 --- a/frigate/test/test_media.py +++ b/frigate/test/test_media.py @@ -6,9 +6,9 @@ from peewee_migrate import Router from playhouse.sqlite_ext import SqliteExtDatabase from playhouse.sqliteq import SqliteQueueDatabase -from frigate.models import Recordings, RecordingsToDelete +from frigate.models import Previews, Recordings, RecordingsToDelete from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS -from frigate.util.media import sync_recordings +from frigate.util.media import sync_previews, sync_recordings class TestMediaSync(unittest.TestCase): @@ -19,7 +19,7 @@ class TestMediaSync(unittest.TestCase): migrate_db.close() self.db = SqliteQueueDatabase(TEST_DB) - models = [Recordings, RecordingsToDelete] + models = [Previews, Recordings, RecordingsToDelete] self.db.bind(models) self.root_a = tempfile.mkdtemp() @@ -81,6 +81,24 @@ class TestMediaSync(unittest.TestCase): assert result.files_checked == 0 assert result.orphans_found == 0 + def test_sync_previews_scans_configured_recording_roots(self): + preview_dir = os.path.join(self.root_b, "preview", "front_door") + os.makedirs(preview_dir, exist_ok=True) + orphan_path = os.path.join(preview_dir, "100-200.mp4") + + with open(orphan_path, "w"): + pass + + result = sync_previews( + dry_run=True, + force=True, + recordings_roots=[self.root_a, self.root_b], + ) + + assert result.files_checked == 1 + assert result.orphans_found == 1 + assert orphan_path in result.orphan_paths + if __name__ == "__main__": unittest.main() diff --git a/frigate/util/media.py b/frigate/util/media.py index aba7206a9..3cc73e81a 100644 --- a/frigate/util/media.py +++ b/frigate/util/media.py @@ -506,7 +506,11 @@ def sync_review_thumbnails(dry_run: bool = False, force: bool = False) -> SyncRe return result -def sync_previews(dry_run: bool = False, force: bool = False) -> SyncResult: +def sync_previews( + dry_run: bool = False, + force: bool = False, + recordings_roots: list[str] | None = None, +) -> SyncResult: """Sync preview files - delete files not referenced by any preview record. Preview files can exist in camera-specific recording roots at: @@ -528,6 +532,12 @@ def sync_previews(dry_run: bool = False, force: bool = False) -> SyncResult: for path in preview_paths if path and path.endswith(".mp4") } + + # Include configured recordings roots so orphaned previews are found even + # when they are not referenced by any DB row. + for root in recordings_roots or [RECORD_DIR]: + preview_dirs.add(os.path.join(root, "preview")) + preview_dirs.add(os.path.join(CLIPS_DIR, "previews")) preview_files: list[str] = [] @@ -809,7 +819,11 @@ def sync_all_media( results.review_thumbnails = sync_review_thumbnails(dry_run=dry_run, force=force) if sync_all or "previews" in media_types: - results.previews = sync_previews(dry_run=dry_run, force=force) + results.previews = sync_previews( + dry_run=dry_run, + force=force, + recordings_roots=recordings_roots, + ) if sync_all or "exports" in media_types: results.exports = sync_exports(dry_run=dry_run, force=force)