record.py 3.9 KB

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