diff --git a/frigate/api/export.py b/frigate/api/export.py index 71c6ebe6c..c88e0146a 100644 --- a/frigate/api/export.py +++ b/frigate/api/export.py @@ -141,7 +141,10 @@ def export_delete(id: str): ) Path(export.video_path).unlink(missing_ok=True) - Path(export.thumb_path).unlink(missing_ok=True) + + if export.thumb_path: + Path(export.thumb_path).unlink(missing_ok=True) + export.delete_instance() return make_response( jsonify( @@ -152,4 +155,3 @@ def export_delete(id: str): ), 200, ) - diff --git a/frigate/app.py b/frigate/app.py index 0d7346405..68a5857bf 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -56,6 +56,7 @@ from frigate.plus import PlusApi from frigate.ptz.autotrack import PtzAutoTrackerThread from frigate.ptz.onvif import OnvifController from frigate.record.cleanup import RecordingCleanup +from frigate.record.export import migrate_exports from frigate.record.record import manage_recordings from frigate.review.review import manage_review_segments from frigate.stats.emitter import StatsEmitter @@ -331,6 +332,17 @@ class FrigateApp: ] self.db.bind(models) + def check_db_data_migrations(self) -> None: + # check if vacuum needs to be run + if not os.path.exists(f"{CONFIG_DIR}/.exports"): + try: + with open(f"{CONFIG_DIR}/.exports", "w") as f: + f.write(str(datetime.datetime.now().timestamp())) + except PermissionError: + logger.error("Unable to write to /config to save export state") + + migrate_exports(self.config.cameras.keys()) + def init_external_event_processor(self) -> None: self.external_event_processor = ExternalEventProcessor(self.config) @@ -631,6 +643,7 @@ class FrigateApp: self.init_review_segment_manager() self.init_go2rtc() self.bind_database() + self.check_db_data_migrations() self.init_inter_process_communicator() self.init_dispatcher() except Exception as e: diff --git a/frigate/record/export.py b/frigate/record/export.py index ffac0c21e..89980c663 100644 --- a/frigate/record/export.py +++ b/frigate/record/export.py @@ -249,3 +249,61 @@ class RecordingExporter(threading.Thread): ).execute() logger.debug(f"Finished exporting {video_path}") + + +def migrate_exports(camera_names: list[str]): + Path(os.path.join(CLIPS_DIR, "export")).mkdir(exist_ok=True) + + exports = [] + for export_file in os.listdir(EXPORT_DIR): + camera = "unknown" + + for cam_name in camera_names: + if cam_name in export_file: + camera = cam_name + break + + id = f"{camera}_{''.join(random.choices(string.ascii_lowercase + string.digits, k=6))}" + video_path = os.path.join(EXPORT_DIR, export_file) + thumb_path = os.path.join( + CLIPS_DIR, f"export/{id}.jpg" + ) # use jpg because webp encoder can't get quality low enough + + ffmpeg_cmd = [ + "ffmpeg", + "-hide_banner", + "-loglevel", + "warning", + "-i", + video_path, + "-vf", + "scale=-1:180", + "-frames", + "1", + "-q:v", + "8", + thumb_path, + ] + + process = sp.run( + ffmpeg_cmd, + capture_output=True, + ) + + if process.returncode != 0: + logger.error(process.stderr) + continue + + exports.append( + { + Export.id: id, + Export.camera: camera, + Export.name: export_file.replace(".mp4", ""), + Export.date: os.path.getctime(video_path), + Export.video_path: video_path, + Export.thumb_path: thumb_path, + Export.in_progress: False, + } + ) + + Export.insert_many(exports).execute() diff --git a/web/src/components/card/ExportCard.tsx b/web/src/components/card/ExportCard.tsx index 01415043b..3926691e9 100644 --- a/web/src/components/card/ExportCard.tsx +++ b/web/src/components/card/ExportCard.tsx @@ -27,7 +27,9 @@ export default function ExportCard({ onDelete, }: ExportProps) { const [hovered, setHovered] = useState(false); - const [loading, setLoading] = useState(true); + const [loading, setLoading] = useState( + exportedRecording.thumb_path.length > 0, + ); // editing name @@ -129,7 +131,7 @@ export default function ExportCard({