|
@@ -0,0 +1,113 @@
|
|
|
+import datetime
|
|
|
+import json
|
|
|
+import logging
|
|
|
+import os
|
|
|
+import queue
|
|
|
+import subprocess as sp
|
|
|
+import threading
|
|
|
+import time
|
|
|
+from collections import defaultdict
|
|
|
+from pathlib import Path
|
|
|
+
|
|
|
+import psutil
|
|
|
+
|
|
|
+from frigate.config import FrigateConfig
|
|
|
+
|
|
|
+logger = logging.getLogger(__name__)
|
|
|
+
|
|
|
+SECONDS_IN_DAY = 60 * 60 * 24
|
|
|
+
|
|
|
+class RecordingMaintainer(threading.Thread):
|
|
|
+ def __init__(self, config: FrigateConfig, stop_event):
|
|
|
+ threading.Thread.__init__(self)
|
|
|
+ self.name = 'recording_maint'
|
|
|
+ self.config = config
|
|
|
+ record_dirs = list(set([camera.record.record_dir for camera in self.config.cameras.values()]))
|
|
|
+ self.record_dir = None if len(record_dirs) == 0 else record_dirs[0]
|
|
|
+ self.stop_event = stop_event
|
|
|
+
|
|
|
+ def move_files(self):
|
|
|
+ if self.record_dir is None:
|
|
|
+ return
|
|
|
+
|
|
|
+ recordings = [d for d in os.listdir(self.record_dir) if os.path.isfile(os.path.join(self.record_dir, d)) and d.endswith(".mp4")]
|
|
|
+
|
|
|
+ files_in_use = []
|
|
|
+ for process in psutil.process_iter():
|
|
|
+ if process.name() != 'ffmpeg':
|
|
|
+ continue
|
|
|
+ try:
|
|
|
+ flist = process.open_files()
|
|
|
+ if flist:
|
|
|
+ for nt in flist:
|
|
|
+ if nt.path.startswith(self.record_dir):
|
|
|
+ files_in_use.append(nt.path.split('/')[-1])
|
|
|
+ except:
|
|
|
+ continue
|
|
|
+
|
|
|
+ for f in recordings:
|
|
|
+ if f in files_in_use:
|
|
|
+ continue
|
|
|
+
|
|
|
+ camera = '-'.join(f.split('-')[:-1])
|
|
|
+ start_time = datetime.datetime.strptime(f.split('-')[-1].split('.')[0], '%Y%m%d%H%M%S')
|
|
|
+
|
|
|
+ ffprobe_cmd = " ".join([
|
|
|
+ 'ffprobe',
|
|
|
+ '-v',
|
|
|
+ 'error',
|
|
|
+ '-show_entries',
|
|
|
+ 'format=duration',
|
|
|
+ '-of',
|
|
|
+ 'default=noprint_wrappers=1:nokey=1',
|
|
|
+ f"{os.path.join(self.record_dir,f)}"
|
|
|
+ ])
|
|
|
+ p = sp.Popen(ffprobe_cmd, stdout=sp.PIPE, shell=True)
|
|
|
+ (output, err) = p.communicate()
|
|
|
+ p_status = p.wait()
|
|
|
+ if p_status == 0:
|
|
|
+ duration = float(output.decode('utf-8').strip())
|
|
|
+ else:
|
|
|
+ logger.info(f"bad file: {f}")
|
|
|
+ os.remove(os.path.join(self.record_dir,f))
|
|
|
+ continue
|
|
|
+
|
|
|
+ directory = os.path.join(self.record_dir, start_time.strftime('%Y-%m/%d/%H'), camera)
|
|
|
+
|
|
|
+ if not os.path.exists(directory):
|
|
|
+ os.makedirs(directory)
|
|
|
+
|
|
|
+ file_name = f"{start_time.strftime('%M.%S.mp4')}"
|
|
|
+
|
|
|
+ os.rename(os.path.join(self.record_dir,f), os.path.join(directory,file_name))
|
|
|
+
|
|
|
+ def expire_files(self):
|
|
|
+ delete_before = {}
|
|
|
+ for name, camera in self.config.cameras.items():
|
|
|
+ delete_before[name] = datetime.datetime.now().timestamp() - SECONDS_IN_DAY*camera.record.retain_days
|
|
|
+
|
|
|
+ for p in Path('/media/frigate/recordings').rglob("*.mp4"):
|
|
|
+ if not p.parent in delete_before:
|
|
|
+ continue
|
|
|
+ if p.stat().st_mtime < delete_before[p.parent]:
|
|
|
+ p.unlink(missing_ok=True)
|
|
|
+
|
|
|
+ def run(self):
|
|
|
+ counter = 0
|
|
|
+ self.expire_files()
|
|
|
+ while(True):
|
|
|
+ if self.stop_event.is_set():
|
|
|
+ logger.info(f"Exiting recording maintenance...")
|
|
|
+ break
|
|
|
+
|
|
|
+ # only expire events every 10 minutes, but check for new files every 10 seconds
|
|
|
+ time.sleep(10)
|
|
|
+ counter = counter + 1
|
|
|
+ if counter < 60:
|
|
|
+ self.expire_files()
|
|
|
+ counter = 0
|
|
|
+
|
|
|
+ self.move_files()
|
|
|
+
|
|
|
+
|
|
|
+
|