diff --git a/frigate/api/app.py b/frigate/api/app.py index fb32529ea..1482b07a6 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -25,7 +25,7 @@ from pydantic import ValidationError from frigate.api.auth import allow_any_authenticated, allow_public, require_role from frigate.api.defs.query.app_query_parameters import AppTimelineHourlyQueryParameters -from frigate.api.defs.request.app_body import AppConfigSetBody +from frigate.api.defs.request.app_body import AppConfigSetBody, MediaSyncBody from frigate.api.defs.tags import Tags from frigate.config import FrigateConfig from frigate.config.camera.updater import ( @@ -34,6 +34,7 @@ from frigate.config.camera.updater import ( ) from frigate.ffmpeg_presets import FFMPEG_HWACCEL_VAAPI, _gpu_selector from frigate.models import Event, Timeline +from frigate.record.util import sync_all_media from frigate.stats.prometheus import get_metrics, update_metrics from frigate.util.builtin import ( clean_camera_user_pass, @@ -602,6 +603,68 @@ def restart(): ) +@router.post("/media/sync", dependencies=[Depends(require_role(["admin"]))]) +def sync_media(body: MediaSyncBody = Body(...)): + """Sync media files with database - remove orphaned files. + + Syncs specified media types: event snapshots, event thumbnails, review thumbnails, + previews, exports, and/or recordings. + + Args: + body: MediaSyncBody with dry_run flag and media_types list. + media_types can include: 'all', 'event_snapshots', 'event_thumbnails', + 'review_thumbnails', 'previews', 'exports', 'recordings' + + Returns: + JSON response with sync results for each requested media type. + """ + try: + results = sync_all_media( + dry_run=body.dry_run, media_types=body.media_types, force=body.force + ) + + # Check if any operations were aborted or had errors + has_errors = False + for result_name in [ + "event_snapshots", + "event_thumbnails", + "review_thumbnails", + "previews", + "exports", + "recordings", + ]: + result = getattr(results, result_name, None) + if result and (result.aborted or result.error): + has_errors = True + break + + content = { + "success": not has_errors, + "dry_run": body.dry_run, + "media_types": body.media_types, + "results": results.to_dict(), + } + + if has_errors: + content["message"] = ( + "Some sync operations were aborted or had errors; check logs for details." + ) + + return JSONResponse( + content=content, + status_code=200, + ) + except Exception as e: + logger.error(f"Error syncing media files: {e}") + return JSONResponse( + content={ + "success": False, + "message": f"Error syncing media files: {str(e)}", + }, + status_code=500, + ) + + @router.get("/labels", dependencies=[Depends(allow_any_authenticated())]) def get_labels(camera: str = ""): try: diff --git a/frigate/api/defs/request/app_body.py b/frigate/api/defs/request/app_body.py index c4129d8da..6059daf6e 100644 --- a/frigate/api/defs/request/app_body.py +++ b/frigate/api/defs/request/app_body.py @@ -1,6 +1,6 @@ -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional -from pydantic import BaseModel +from pydantic import BaseModel, Field class AppConfigSetBody(BaseModel): @@ -27,3 +27,16 @@ class AppPostLoginBody(BaseModel): class AppPutRoleBody(BaseModel): role: str + + +class MediaSyncBody(BaseModel): + dry_run: bool = Field( + default=True, description="If True, only report orphans without deleting them" + ) + media_types: List[str] = Field( + default=["all"], + description="Types of media to sync: 'all', 'event_snapshots', 'event_thumbnails', 'review_thumbnails', 'previews', 'exports', 'recordings'", + ) + force: bool = Field( + default=False, description="If True, bypass safety threshold checks" + )