mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-05 14:47:40 +03:00
add verbose mode to media sync
writes a report to /config/media_sync showing all of the orphaned paths by media type
This commit is contained in:
parent
2c9a25e678
commit
102be124d4
@ -864,7 +864,10 @@ def sync_media(body: MediaSyncBody = Body(...)):
|
||||
202 Accepted with job_id, or 409 Conflict if job already running.
|
||||
"""
|
||||
job_id = start_media_sync_job(
|
||||
dry_run=body.dry_run, media_types=body.media_types, force=body.force
|
||||
dry_run=body.dry_run,
|
||||
media_types=body.media_types,
|
||||
force=body.force,
|
||||
verbose=body.verbose,
|
||||
)
|
||||
|
||||
if job_id is None:
|
||||
|
||||
@ -45,3 +45,7 @@ class MediaSyncBody(BaseModel):
|
||||
force: bool = Field(
|
||||
default=False, description="If True, bypass safety threshold checks"
|
||||
)
|
||||
verbose: bool = Field(
|
||||
default=False,
|
||||
description="If True, write full orphan file list to disk",
|
||||
)
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
"""Media sync job management with background execution."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from frigate.comms.inter_process import InterProcessRequestor
|
||||
from frigate.const import UPDATE_JOB_STATE
|
||||
from frigate.const import CONFIG_DIR, UPDATE_JOB_STATE
|
||||
from frigate.jobs.job import Job
|
||||
from frigate.jobs.manager import (
|
||||
get_current_job,
|
||||
@ -16,7 +17,7 @@ from frigate.jobs.manager import (
|
||||
set_current_job,
|
||||
)
|
||||
from frigate.types import JobStatusTypesEnum
|
||||
from frigate.util.media import sync_all_media
|
||||
from frigate.util.media import sync_all_media, write_orphan_report
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -29,6 +30,7 @@ class MediaSyncJob(Job):
|
||||
dry_run: bool = False
|
||||
media_types: list[str] = field(default_factory=lambda: ["all"])
|
||||
force: bool = False
|
||||
verbose: bool = False
|
||||
|
||||
|
||||
class MediaSyncRunner(threading.Thread):
|
||||
@ -61,6 +63,21 @@ class MediaSyncRunner(threading.Thread):
|
||||
force=self.job.force,
|
||||
)
|
||||
|
||||
# Write verbose report if requested
|
||||
if self.job.verbose:
|
||||
report_dir = os.path.join(CONFIG_DIR, "media_sync")
|
||||
os.makedirs(report_dir, exist_ok=True)
|
||||
report_path = os.path.join(report_dir, f"{self.job.id}.txt")
|
||||
write_orphan_report(
|
||||
results,
|
||||
report_path,
|
||||
job_id=self.job.id,
|
||||
dry_run=self.job.dry_run,
|
||||
)
|
||||
logger.info(
|
||||
"Media sync verbose orphan report written to %s", report_path
|
||||
)
|
||||
|
||||
# Store results and mark as complete
|
||||
self.job.results = results.to_dict()
|
||||
self.job.status = JobStatusTypesEnum.success
|
||||
@ -95,6 +112,7 @@ def start_media_sync_job(
|
||||
dry_run: bool = False,
|
||||
media_types: Optional[list[str]] = None,
|
||||
force: bool = False,
|
||||
verbose: bool = False,
|
||||
) -> Optional[str]:
|
||||
"""Start a new media sync job if none is currently running.
|
||||
|
||||
@ -113,6 +131,7 @@ def start_media_sync_job(
|
||||
dry_run=dry_run,
|
||||
media_types=media_types or ["all"],
|
||||
force=force,
|
||||
verbose=verbose,
|
||||
)
|
||||
|
||||
logger.debug(f"Creating new media sync job: {job.id}")
|
||||
|
||||
@ -49,6 +49,7 @@ class SyncResult:
|
||||
orphans_found: int = 0
|
||||
orphans_deleted: int = 0
|
||||
orphan_paths: list[str] = field(default_factory=list)
|
||||
orphan_db_paths: list[str] = field(default_factory=list)
|
||||
aborted: bool = False
|
||||
error: str | None = None
|
||||
|
||||
@ -132,7 +133,7 @@ def sync_recordings(
|
||||
)
|
||||
|
||||
result.orphans_found += len(recordings_to_delete)
|
||||
result.orphan_paths.extend(
|
||||
result.orphan_db_paths.extend(
|
||||
[
|
||||
recording["path"]
|
||||
for recording in recordings_to_delete
|
||||
@ -773,6 +774,61 @@ class MediaSyncResults:
|
||||
return results
|
||||
|
||||
|
||||
def write_orphan_report(
|
||||
results: "MediaSyncResults",
|
||||
path: str,
|
||||
job_id: str = "",
|
||||
dry_run: bool = False,
|
||||
) -> None:
|
||||
"""Write a verbose orphan report file listing all orphan paths by media type.
|
||||
|
||||
Args:
|
||||
results: The completed MediaSyncResults.
|
||||
path: File path to write the report to.
|
||||
job_id: Job ID for the report header.
|
||||
dry_run: Whether the sync was a dry run, for the report header.
|
||||
"""
|
||||
try:
|
||||
with open(path, "w") as f:
|
||||
f.write("# Media Sync Orphan Report\n")
|
||||
f.write(f"# Job: {job_id}\n")
|
||||
f.write(
|
||||
f"# Date: {datetime.datetime.now().astimezone(datetime.timezone.utc).isoformat()}\n"
|
||||
)
|
||||
f.write(f"# Mode: dry_run={dry_run}\n\n")
|
||||
|
||||
for name, result in [
|
||||
("recordings", results.recordings),
|
||||
("event_snapshots", results.event_snapshots),
|
||||
("event_thumbnails", results.event_thumbnails),
|
||||
("review_thumbnails", results.review_thumbnails),
|
||||
("previews", results.previews),
|
||||
("exports", results.exports),
|
||||
]:
|
||||
if result is None:
|
||||
continue
|
||||
|
||||
if result.orphan_db_paths:
|
||||
f.write(
|
||||
f"## {name} - orphaned db entries ({len(result.orphan_db_paths)})\n"
|
||||
)
|
||||
for orphan_path in result.orphan_db_paths:
|
||||
f.write(f"{orphan_path}\n")
|
||||
f.write("\n")
|
||||
|
||||
if result.orphan_paths:
|
||||
f.write(
|
||||
f"## {name} - orphaned files ({len(result.orphan_paths)})\n"
|
||||
)
|
||||
for orphan_path in result.orphan_paths:
|
||||
f.write(f"{orphan_path}\n")
|
||||
f.write("\n")
|
||||
|
||||
logger.debug("Wrote verbose orphan report to %s", path)
|
||||
except OSError as e:
|
||||
logger.error("Failed to write orphan report to %s: %s", path, e)
|
||||
|
||||
|
||||
def sync_all_media(
|
||||
dry_run: bool = False, media_types: list[str] = ["all"], force: bool = False
|
||||
) -> MediaSyncResults:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user