Merge pull request #9 from ibs0d/codex/fix-recording-roots-normalization-logic

Normalize recordings storage roots to avoid date/hour pseudo-roots
This commit is contained in:
ibs0d 2026-03-07 21:28:27 +11:00 committed by GitHub
commit ced95052ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 97 additions and 0 deletions

View File

@ -1,6 +1,7 @@
"""Handle storage retention and usage.""" """Handle storage retention and usage."""
import logging import logging
import re
import shutil import shutil
import threading import threading
from pathlib import Path from pathlib import Path
@ -178,6 +179,22 @@ class StorageMaintainer(threading.Thread):
if camera_segment in recording_path: if camera_segment in recording_path:
return recording_path.split(camera_segment, 1)[0].rstrip("/") or "/" return recording_path.split(camera_segment, 1)[0].rstrip("/") or "/"
# Prefer configured recording roots when available.
for configured_root in sorted(
self.config.get_recordings_paths(), key=len, reverse=True
):
if recording_path == configured_root or recording_path.startswith(
f"{configured_root}/"
):
return configured_root.rstrip("/") or "/"
# Support layouts like /root/YYYY-MM-DD/HH/... and normalize to /root.
date_hour_match = re.match(
r"^(?P<root>.+?)/\d{4}-\d{2}-\d{2}/\d{2}(?:/|$)", recording_path
)
if date_hour_match:
return date_hour_match.group("root").rstrip("/") or "/"
# Fallback for unexpected path layouts; expected format is root/camera/date/file # Fallback for unexpected path layouts; expected format is root/camera/date/file
path = Path(recording_path) path = Path(recording_path)
if len(path.parents) >= 3: if len(path.parents) >= 3:

View File

@ -227,3 +227,83 @@ class TestHttpRecordingsStorage(BaseTestHttp):
# Config metadata still reflects current root assignment. # Config metadata still reflects current root assignment.
assert roots["/media/frigate/recordings"]["configured_cameras"] == [] assert roots["/media/frigate/recordings"]["configured_cameras"] == []
assert roots["/mnt/new-root"]["configured_cameras"] == ["front_door"] assert roots["/mnt/new-root"]["configured_cameras"] == ["front_door"]
def test_recordings_storage_normalizes_date_hour_paths_to_configured_roots(self):
self.minimal_config["cameras"]["front_door"]["path"] = "/media/frigate/recordings"
self.minimal_config["cameras"]["back_yard"] = {
"ffmpeg": {
"inputs": [{"path": "rtsp://10.0.0.2:554/video", "roles": ["detect"]}]
},
"detect": {"height": 1080, "width": 1920, "fps": 5},
"path": "/video1",
}
self.test_stats["service"]["storage"]["/video1"] = {
"free": 600,
"mount_type": "ext4",
"total": 1000,
"used": 400,
}
app = self._build_app()
Recordings.insert_many(
[
{
"id": "front_08",
"path": "/media/frigate/recordings/2026-03-07/08/seg1.mp4",
"camera": "front_door",
"start_time": 100,
"end_time": 110,
"duration": 10,
"motion": 1,
"segment_size": 100,
},
{
"id": "front_09",
"path": "/media/frigate/recordings/2026-03-07/09/seg1.mp4",
"camera": "front_door",
"start_time": 120,
"end_time": 130,
"duration": 10,
"motion": 1,
"segment_size": 150,
},
{
"id": "video1_08",
"path": "/video1/2026-03-07/08/seg1.mp4",
"camera": "back_yard",
"start_time": 140,
"end_time": 150,
"duration": 10,
"motion": 1,
"segment_size": 200,
},
{
"id": "video1_09",
"path": "/video1/2026-03-07/09/seg1.mp4",
"camera": "back_yard",
"start_time": 160,
"end_time": 170,
"duration": 10,
"motion": 1,
"segment_size": 50,
},
]
).execute()
with AuthTestClient(app) as client:
payload = client.get("/recordings/storage").json()
roots = {root["path"]: root for root in payload["recording_roots"]}
assert set(roots.keys()) == {"/media/frigate/recordings", "/video1"}
assert roots["/media/frigate/recordings"]["recordings_size"] == 250
assert roots["/video1"]["recordings_size"] == 250
# Ensure date/hour pseudo-roots are not emitted.
assert "/media/frigate/recordings/2026-03-07/08" not in roots
assert "/media/frigate/recordings/2026-03-07/09" not in roots
assert "/video1/2026-03-07/08" not in roots
assert "/video1/2026-03-07/09" not in roots