mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-14 01:15:26 +03:00
Compare commits
5 Commits
ab3c12b89e
...
0dbf648b3d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0dbf648b3d | ||
|
|
01d52a8bf6 | ||
|
|
91965dfa4c | ||
|
|
16237c69ad | ||
|
|
c487d0aa93 |
@ -56,7 +56,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- /path/to/your/config:/config
|
- /path/to/your/config:/config
|
||||||
- /path/to/your/storage:/media/frigate
|
- /path/to/your/storage:/media/frigate
|
||||||
- type: tmpfs # Optional: 1GB of memory, reduces SSD/SD Card wear
|
- type: tmpfs # Recommended: 1GB of memory
|
||||||
target: /tmp/cache
|
target: /tmp/cache
|
||||||
tmpfs:
|
tmpfs:
|
||||||
size: 1000000000
|
size: 1000000000
|
||||||
@ -310,7 +310,7 @@ services:
|
|||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
- /path/to/your/config:/config
|
- /path/to/your/config:/config
|
||||||
- /path/to/your/storage:/media/frigate
|
- /path/to/your/storage:/media/frigate
|
||||||
- type: tmpfs # Optional: 1GB of memory, reduces SSD/SD Card wear
|
- type: tmpfs # Recommended: 1GB of memory
|
||||||
target: /tmp/cache
|
target: /tmp/cache
|
||||||
tmpfs:
|
tmpfs:
|
||||||
size: 1000000000
|
size: 1000000000
|
||||||
|
|||||||
@ -762,6 +762,15 @@ async def recording_clip(
|
|||||||
.order_by(Recordings.start_time.asc())
|
.order_by(Recordings.start_time.asc())
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if recordings.count() == 0:
|
||||||
|
return JSONResponse(
|
||||||
|
content={
|
||||||
|
"success": False,
|
||||||
|
"message": "No recordings found for the specified time range",
|
||||||
|
},
|
||||||
|
status_code=400,
|
||||||
|
)
|
||||||
|
|
||||||
file_name = sanitize_filename(f"playlist_{camera_name}_{start_ts}-{end_ts}.txt")
|
file_name = sanitize_filename(f"playlist_{camera_name}_{start_ts}-{end_ts}.txt")
|
||||||
file_path = os.path.join(CACHE_DIR, file_name)
|
file_path = os.path.join(CACHE_DIR, file_name)
|
||||||
with open(file_path, "w") as file:
|
with open(file_path, "w") as file:
|
||||||
|
|||||||
@ -113,6 +113,7 @@ class StorageMaintainer(threading.Thread):
|
|||||||
recordings: Recordings = (
|
recordings: Recordings = (
|
||||||
Recordings.select(
|
Recordings.select(
|
||||||
Recordings.id,
|
Recordings.id,
|
||||||
|
Recordings.camera,
|
||||||
Recordings.start_time,
|
Recordings.start_time,
|
||||||
Recordings.end_time,
|
Recordings.end_time,
|
||||||
Recordings.segment_size,
|
Recordings.segment_size,
|
||||||
@ -137,7 +138,7 @@ class StorageMaintainer(threading.Thread):
|
|||||||
)
|
)
|
||||||
|
|
||||||
event_start = 0
|
event_start = 0
|
||||||
deleted_recordings = set()
|
deleted_recordings = []
|
||||||
for recording in recordings:
|
for recording in recordings:
|
||||||
# check if 1 hour of storage has been reclaimed
|
# check if 1 hour of storage has been reclaimed
|
||||||
if deleted_segments_size > hourly_bandwidth:
|
if deleted_segments_size > hourly_bandwidth:
|
||||||
@ -172,7 +173,7 @@ class StorageMaintainer(threading.Thread):
|
|||||||
if not keep:
|
if not keep:
|
||||||
try:
|
try:
|
||||||
clear_and_unlink(Path(recording.path), missing_ok=False)
|
clear_and_unlink(Path(recording.path), missing_ok=False)
|
||||||
deleted_recordings.add(recording.id)
|
deleted_recordings.append(recording)
|
||||||
deleted_segments_size += recording.segment_size
|
deleted_segments_size += recording.segment_size
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
# this file was not found so we must assume no space was cleaned up
|
# this file was not found so we must assume no space was cleaned up
|
||||||
@ -186,6 +187,9 @@ class StorageMaintainer(threading.Thread):
|
|||||||
recordings = (
|
recordings = (
|
||||||
Recordings.select(
|
Recordings.select(
|
||||||
Recordings.id,
|
Recordings.id,
|
||||||
|
Recordings.camera,
|
||||||
|
Recordings.start_time,
|
||||||
|
Recordings.end_time,
|
||||||
Recordings.path,
|
Recordings.path,
|
||||||
Recordings.segment_size,
|
Recordings.segment_size,
|
||||||
)
|
)
|
||||||
@ -201,7 +205,7 @@ class StorageMaintainer(threading.Thread):
|
|||||||
try:
|
try:
|
||||||
clear_and_unlink(Path(recording.path), missing_ok=False)
|
clear_and_unlink(Path(recording.path), missing_ok=False)
|
||||||
deleted_segments_size += recording.segment_size
|
deleted_segments_size += recording.segment_size
|
||||||
deleted_recordings.add(recording.id)
|
deleted_recordings.append(recording)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
# this file was not found so we must assume no space was cleaned up
|
# this file was not found so we must assume no space was cleaned up
|
||||||
pass
|
pass
|
||||||
@ -211,7 +215,50 @@ class StorageMaintainer(threading.Thread):
|
|||||||
logger.debug(f"Expiring {len(deleted_recordings)} recordings")
|
logger.debug(f"Expiring {len(deleted_recordings)} recordings")
|
||||||
# delete up to 100,000 at a time
|
# delete up to 100,000 at a time
|
||||||
max_deletes = 100000
|
max_deletes = 100000
|
||||||
deleted_recordings_list = list(deleted_recordings)
|
|
||||||
|
# Update has_clip for events that overlap with deleted recordings
|
||||||
|
if deleted_recordings:
|
||||||
|
# Group deleted recordings by camera
|
||||||
|
camera_recordings = {}
|
||||||
|
for recording in deleted_recordings:
|
||||||
|
if recording.camera not in camera_recordings:
|
||||||
|
camera_recordings[recording.camera] = {
|
||||||
|
"min_start": recording.start_time,
|
||||||
|
"max_end": recording.end_time,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
camera_recordings[recording.camera]["min_start"] = min(
|
||||||
|
camera_recordings[recording.camera]["min_start"],
|
||||||
|
recording.start_time,
|
||||||
|
)
|
||||||
|
camera_recordings[recording.camera]["max_end"] = max(
|
||||||
|
camera_recordings[recording.camera]["max_end"],
|
||||||
|
recording.end_time,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Find all events that overlap with deleted recordings time range per camera
|
||||||
|
events_to_update = []
|
||||||
|
for camera, time_range in camera_recordings.items():
|
||||||
|
overlapping_events = Event.select(Event.id).where(
|
||||||
|
Event.camera == camera,
|
||||||
|
Event.has_clip == True,
|
||||||
|
Event.start_time < time_range["max_end"],
|
||||||
|
Event.end_time > time_range["min_start"],
|
||||||
|
)
|
||||||
|
|
||||||
|
for event in overlapping_events:
|
||||||
|
events_to_update.append(event.id)
|
||||||
|
|
||||||
|
# Update has_clip to False for overlapping events
|
||||||
|
if events_to_update:
|
||||||
|
for i in range(0, len(events_to_update), max_deletes):
|
||||||
|
batch = events_to_update[i : i + max_deletes]
|
||||||
|
Event.update(has_clip=False).where(Event.id << batch).execute()
|
||||||
|
logger.debug(
|
||||||
|
f"Updated has_clip to False for {len(events_to_update)} events"
|
||||||
|
)
|
||||||
|
|
||||||
|
deleted_recordings_list = [r.id for r in deleted_recordings]
|
||||||
for i in range(0, len(deleted_recordings_list), max_deletes):
|
for i in range(0, len(deleted_recordings_list), max_deletes):
|
||||||
Recordings.delete().where(
|
Recordings.delete().where(
|
||||||
Recordings.id << deleted_recordings_list[i : i + max_deletes]
|
Recordings.id << deleted_recordings_list[i : i + max_deletes]
|
||||||
|
|||||||
@ -177,21 +177,22 @@ export function TrackingDetails({
|
|||||||
})
|
})
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const formattedEnd = config
|
const formattedEnd =
|
||||||
? formatUnixTimestampToDateTime(event.end_time ?? 0, {
|
config && event.end_time != null
|
||||||
timezone: config.ui.timezone,
|
? formatUnixTimestampToDateTime(event.end_time, {
|
||||||
date_format:
|
timezone: config.ui.timezone,
|
||||||
config.ui.time_format == "24hour"
|
date_format:
|
||||||
? t("time.formattedTimestamp.24hour", {
|
config.ui.time_format == "24hour"
|
||||||
ns: "common",
|
? t("time.formattedTimestamp.24hour", {
|
||||||
})
|
ns: "common",
|
||||||
: t("time.formattedTimestamp.12hour", {
|
})
|
||||||
ns: "common",
|
: t("time.formattedTimestamp.12hour", {
|
||||||
}),
|
ns: "common",
|
||||||
time_style: "medium",
|
}),
|
||||||
date_style: "medium",
|
time_style: "medium",
|
||||||
})
|
date_style: "medium",
|
||||||
: "";
|
})
|
||||||
|
: "";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!eventSequence || eventSequence.length === 0) return;
|
if (!eventSequence || eventSequence.length === 0) return;
|
||||||
@ -525,9 +526,16 @@ export function TrackingDetails({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="capitalize">{label}</span>
|
<span className="capitalize">{label}</span>
|
||||||
<span className="md:text-md text-xs text-secondary-foreground">
|
<div className="md:text-md flex items-center text-xs text-secondary-foreground">
|
||||||
{formattedStart ?? ""} - {formattedEnd ?? ""}
|
{formattedStart ?? ""}
|
||||||
</span>
|
{event.end_time != null ? (
|
||||||
|
<> - {formattedEnd}</>
|
||||||
|
) : (
|
||||||
|
<div className="inline-block">
|
||||||
|
<ActivityIndicator className="ml-3 size-4" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{event.data?.recognized_license_plate && (
|
{event.data?.recognized_license_plate && (
|
||||||
<>
|
<>
|
||||||
<span className="text-secondary-foreground">·</span>
|
<span className="text-secondary-foreground">·</span>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user