record.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. import datetime
  2. import itertools
  3. import logging
  4. import os
  5. import random
  6. import string
  7. import subprocess as sp
  8. import threading
  9. from pathlib import Path
  10. import psutil
  11. from frigate.config import FrigateConfig
  12. from frigate.const import RECORD_DIR
  13. from frigate.models import Recordings
  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. end_time = start_time + datetime.timedelta(seconds=duration)
  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. file_path = os.path.join(directory, file_name)
  85. os.rename(os.path.join(RECORD_DIR, f), file_path)
  86. rand_id = "".join(
  87. random.choices(string.ascii_lowercase + string.digits, k=6)
  88. )
  89. Recordings.create(
  90. id=f"{start_time.timestamp()}-{rand_id}",
  91. camera=camera,
  92. path=file_path,
  93. start_time=start_time.timestamp(),
  94. end_time=end_time.timestamp(),
  95. duration=duration,
  96. )
  97. def expire_files(self):
  98. delete_before = {}
  99. for name, camera in self.config.cameras.items():
  100. delete_before[name] = (
  101. datetime.datetime.now().timestamp()
  102. - SECONDS_IN_DAY * camera.record.retain_days
  103. )
  104. for p in Path("/media/frigate/recordings").rglob("*.mp4"):
  105. if not p.parent.name in delete_before:
  106. continue
  107. if p.stat().st_mtime < delete_before[p.parent.name]:
  108. Recordings.delete().where(Recordings.path == str(p)).execute()
  109. p.unlink(missing_ok=True)
  110. def run(self):
  111. for counter in itertools.cycle(range(60)):
  112. if self.stop_event.wait(10):
  113. logger.info(f"Exiting recording maintenance...")
  114. break
  115. # only expire events every 10 minutes, but check for new files every 10 seconds
  116. if counter == 0:
  117. self.expire_files()
  118. remove_empty_directories(RECORD_DIR)
  119. self.move_files()