2020-11-30 04:31:02 +01:00
|
|
|
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
|
2020-12-01 14:22:23 +01:00
|
|
|
from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
|
2020-11-30 04:31:02 +01:00
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
SECONDS_IN_DAY = 60 * 60 * 24
|
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
|
2020-12-01 04:08:47 +01:00
|
|
|
def remove_empty_directories(directory):
|
2021-02-17 14:23:32 +01:00
|
|
|
# list all directories recursively and sort them by path,
|
|
|
|
# longest first
|
|
|
|
paths = sorted(
|
|
|
|
[x[0] for x in os.walk(RECORD_DIR)],
|
|
|
|
key=lambda p: len(str(p)),
|
|
|
|
reverse=True,
|
|
|
|
)
|
|
|
|
for path in paths:
|
|
|
|
# don't delete the parent
|
|
|
|
if path == RECORD_DIR:
|
|
|
|
continue
|
|
|
|
if len(os.listdir(path)) == 0:
|
|
|
|
os.rmdir(path)
|
|
|
|
|
2020-12-01 04:08:47 +01:00
|
|
|
|
2020-11-30 04:31:02 +01:00
|
|
|
class RecordingMaintainer(threading.Thread):
|
|
|
|
def __init__(self, config: FrigateConfig, stop_event):
|
|
|
|
threading.Thread.__init__(self)
|
2021-02-17 14:23:32 +01:00
|
|
|
self.name = "recording_maint"
|
2020-11-30 04:31:02 +01:00
|
|
|
self.config = config
|
|
|
|
self.stop_event = stop_event
|
|
|
|
|
|
|
|
def move_files(self):
|
2021-02-17 14:23:32 +01:00
|
|
|
recordings = [
|
|
|
|
d
|
|
|
|
for d in os.listdir(RECORD_DIR)
|
|
|
|
if os.path.isfile(os.path.join(RECORD_DIR, d)) and d.endswith(".mp4")
|
|
|
|
]
|
2020-11-30 04:31:02 +01:00
|
|
|
|
|
|
|
files_in_use = []
|
|
|
|
for process in psutil.process_iter():
|
|
|
|
try:
|
2021-02-17 14:23:32 +01:00
|
|
|
if process.name() != "ffmpeg":
|
2020-12-24 21:23:59 +01:00
|
|
|
continue
|
2020-11-30 04:31:02 +01:00
|
|
|
flist = process.open_files()
|
|
|
|
if flist:
|
|
|
|
for nt in flist:
|
2020-12-01 14:22:23 +01:00
|
|
|
if nt.path.startswith(RECORD_DIR):
|
2021-02-17 14:23:32 +01:00
|
|
|
files_in_use.append(nt.path.split("/")[-1])
|
2020-11-30 04:31:02 +01:00
|
|
|
except:
|
|
|
|
continue
|
|
|
|
|
|
|
|
for f in recordings:
|
|
|
|
if f in files_in_use:
|
|
|
|
continue
|
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
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(RECORD_DIR,f)}",
|
|
|
|
]
|
|
|
|
)
|
2020-11-30 04:31:02 +01:00
|
|
|
p = sp.Popen(ffprobe_cmd, stdout=sp.PIPE, shell=True)
|
|
|
|
(output, err) = p.communicate()
|
|
|
|
p_status = p.wait()
|
|
|
|
if p_status == 0:
|
2021-02-17 14:23:32 +01:00
|
|
|
duration = float(output.decode("utf-8").strip())
|
2020-11-30 04:31:02 +01:00
|
|
|
else:
|
|
|
|
logger.info(f"bad file: {f}")
|
2021-02-17 14:23:32 +01:00
|
|
|
os.remove(os.path.join(RECORD_DIR, f))
|
2020-11-30 04:31:02 +01:00
|
|
|
continue
|
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
directory = os.path.join(
|
|
|
|
RECORD_DIR, start_time.strftime("%Y-%m/%d/%H"), camera
|
|
|
|
)
|
2020-11-30 04:31:02 +01:00
|
|
|
|
|
|
|
if not os.path.exists(directory):
|
|
|
|
os.makedirs(directory)
|
|
|
|
|
|
|
|
file_name = f"{start_time.strftime('%M.%S.mp4')}"
|
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
os.rename(os.path.join(RECORD_DIR, f), os.path.join(directory, file_name))
|
2020-11-30 04:31:02 +01:00
|
|
|
|
|
|
|
def expire_files(self):
|
|
|
|
delete_before = {}
|
|
|
|
for name, camera in self.config.cameras.items():
|
2021-02-17 14:23:32 +01:00
|
|
|
delete_before[name] = (
|
|
|
|
datetime.datetime.now().timestamp()
|
|
|
|
- SECONDS_IN_DAY * camera.record.retain_days
|
|
|
|
)
|
2020-11-30 04:31:02 +01:00
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
for p in Path("/media/frigate/recordings").rglob("*.mp4"):
|
2020-12-22 01:17:25 +01:00
|
|
|
if not p.parent.name in delete_before:
|
2020-11-30 04:31:02 +01:00
|
|
|
continue
|
2020-12-22 01:17:25 +01:00
|
|
|
if p.stat().st_mtime < delete_before[p.parent.name]:
|
2020-11-30 04:31:02 +01:00
|
|
|
p.unlink(missing_ok=True)
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
counter = 0
|
|
|
|
self.expire_files()
|
2021-02-17 14:23:32 +01:00
|
|
|
while True:
|
2020-11-30 04:31:02 +01:00
|
|
|
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
|
2020-12-01 04:08:47 +01:00
|
|
|
if counter > 60:
|
2020-11-30 04:31:02 +01:00
|
|
|
self.expire_files()
|
2020-12-01 14:22:23 +01:00
|
|
|
remove_empty_directories(RECORD_DIR)
|
2020-12-01 04:08:47 +01:00
|
|
|
counter = 0
|
2020-11-30 04:31:02 +01:00
|
|
|
|
|
|
|
self.move_files()
|