record.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. import datetime
  2. import itertools
  3. import json
  4. import logging
  5. import os
  6. import queue
  7. import subprocess as sp
  8. import threading
  9. import time
  10. from collections import defaultdict
  11. from pathlib import Path
  12. import psutil
  13. from frigate.config import FrigateConfig
  14. from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
  15. logger = logging.getLogger(__name__)
  16. SECONDS_IN_DAY = 60 * 60 * 24
  17. def remove_empty_directories(directory):
  18. # list all directories recursively and sort them by path,
  19. # longest first
  20. paths = sorted(
  21. [x[0] for x in os.walk(RECORD_DIR)],
  22. key=lambda p: len(str(p)),
  23. reverse=True,
  24. )
  25. for path in paths:
  26. # don't delete the parent
  27. if path == RECORD_DIR:
  28. continue
  29. if len(os.listdir(path)) == 0:
  30. os.rmdir(path)
  31. class RecordingMaintainer(threading.Thread):
  32. def __init__(self, config: FrigateConfig, stop_event):
  33. threading.Thread.__init__(self)
  34. self.name = "recording_maint"
  35. self.config = config
  36. self.stop_event = stop_event
  37. def move_files(self):
  38. recordings = [
  39. d
  40. for d in os.listdir(RECORD_DIR)
  41. if os.path.isfile(os.path.join(RECORD_DIR, d)) and d.endswith(".mp4")
  42. ]
  43. files_in_use = []
  44. for process in psutil.process_iter():
  45. try:
  46. if process.name() != "ffmpeg":
  47. continue
  48. flist = process.open_files()
  49. if flist:
  50. for nt in flist:
  51. if nt.path.startswith(RECORD_DIR):
  52. files_in_use.append(nt.path.split("/")[-1])
  53. except:
  54. continue
  55. for f in recordings:
  56. if f in files_in_use:
  57. continue
  58. basename = os.path.splitext(f)[0]
  59. camera, date = basename.rsplit("-", maxsplit=1)
  60. start_time = datetime.datetime.strptime(date, "%Y%m%d%H%M%S")
  61. ffprobe_cmd = [
  62. "ffprobe",
  63. "-v",
  64. "error",
  65. "-show_entries",
  66. "format=duration",
  67. "-of",
  68. "default=noprint_wrappers=1:nokey=1",
  69. f"{os.path.join(RECORD_DIR, f)}",
  70. ]
  71. p = sp.run(ffprobe_cmd, capture_output=True)
  72. if p.returncode == 0:
  73. duration = float(p.stdout.decode().strip())
  74. else:
  75. logger.info(f"bad file: {f}")
  76. os.remove(os.path.join(RECORD_DIR, f))
  77. continue
  78. directory = os.path.join(
  79. RECORD_DIR, start_time.strftime("%Y-%m/%d/%H"), camera
  80. )
  81. if not os.path.exists(directory):
  82. os.makedirs(directory)
  83. file_name = f"{start_time.strftime('%M.%S.mp4')}"
  84. os.rename(os.path.join(RECORD_DIR, f), os.path.join(directory, file_name))
  85. def expire_files(self):
  86. delete_before = {}
  87. for name, camera in self.config.cameras.items():
  88. delete_before[name] = (
  89. datetime.datetime.now().timestamp()
  90. - SECONDS_IN_DAY * camera.record.retain_days
  91. )
  92. for p in Path("/media/frigate/recordings").rglob("*.mp4"):
  93. if not p.parent.name in delete_before:
  94. continue
  95. if p.stat().st_mtime < delete_before[p.parent.name]:
  96. p.unlink(missing_ok=True)
  97. def run(self):
  98. for counter in itertools.cycle(range(60)):
  99. if self.stop_event.wait(10):
  100. logger.info(f"Exiting recording maintenance...")
  101. break
  102. # only expire events every 10 minutes, but check for new files every 10 seconds
  103. if counter == 0:
  104. self.expire_files()
  105. remove_empty_directories(RECORD_DIR)
  106. self.move_files()