Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ updates:
# Needs Python 3.12 and numpy 2
- dependency-name: "librosa"
versions: [">=1.0"]
# pandas 3.1 requires numpy 2
- dependency-name: "pandas"
versions: [">=3.1"]
# Needs Python 3.12
- dependency-name: "scipy"
versions: [">=1.18"]
Expand Down
2 changes: 2 additions & 0 deletions docs/static/frigate-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6371,6 +6371,7 @@ paths:
required: false
schema:
type: integer
minimum: 0
description: Padding to apply to the vod.
default: 0
title: Padding
Expand Down Expand Up @@ -7576,6 +7577,7 @@ paths:
required: false
schema:
type: integer
minimum: 1
default: 30
title: Scale
responses:
Expand Down
4 changes: 4 additions & 0 deletions frigate/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -966,6 +966,10 @@ def login(request: Request, body: AppPostLoginBody):
try:
db_user: User = User.get_by_id(user)
except DoesNotExist:
# Hash anyway so response timing doesn't reveal which usernames exist
hash_password(
password, iterations=request.app.frigate_config.auth.hash_iterations
)
Comment thread
hawkeye217 marked this conversation as resolved.
Resolved
logger.warning(f"Login failed for unknown user '{user}' from {remote_addr}")
failed_logins.record(user, time.time())
return JSONResponse(content={"message": "Login failed"}, status_code=401)
Expand Down
4 changes: 2 additions & 2 deletions frigate/api/defs/query/recordings_query_parameters.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pydantic import BaseModel
from pydantic import BaseModel, Field
from pydantic.json_schema import SkipJsonSchema


Expand All @@ -11,7 +11,7 @@ class MediaRecordingsAvailabilityQueryParams(BaseModel):
cameras: str = "all"
before: float | SkipJsonSchema[None] = None
after: float | SkipJsonSchema[None] = None
scale: int = 30
scale: int = Field(default=30, ge=1)


class RecordingsDeleteQueryParams(BaseModel):
Expand Down
8 changes: 4 additions & 4 deletions frigate/api/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,12 +291,12 @@ def events(
clauses.append(
reduce(
operator.or_,
[(start_hour_fun > time_after), (start_hour_fun < time_before)],
[(start_hour_fun >= time_after), (start_hour_fun < time_before)],
)
)
# all other cases should be and operator
else:
clauses.append(start_hour_fun > time_after)
clauses.append(start_hour_fun >= time_after)
clauses.append(start_hour_fun < time_before)

if has_clip is not None:
Expand Down Expand Up @@ -749,12 +749,12 @@ def events_search(
event_filters.append(
reduce(
operator.or_,
[(start_hour_fun > time_after), (start_hour_fun < time_before)],
[(start_hour_fun >= time_after), (start_hour_fun < time_before)],
)
)
# all other cases should be and operator
else:
event_filters.append(start_hour_fun > time_after)
event_filters.append(start_hour_fun >= time_after)
event_filters.append(start_hour_fun < time_before)

# Perform semantic search
Expand Down
2 changes: 1 addition & 1 deletion frigate/api/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -1227,7 +1227,7 @@ def bulk_delete_exports(body: ExportBulkDeleteBody):
files_in_use = _get_files_in_use()

for export in exports:
if export.video_path.split("/")[-1] in files_in_use:
if export.in_progress or export.video_path.split("/")[-1] in files_in_use:
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
Comment thread
NickM-27 marked this conversation as resolved.
Resolved
return JSONResponse(
content={
"success": False,
Expand Down
14 changes: 9 additions & 5 deletions frigate/api/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -862,7 +862,7 @@ async def vod_hour(
async def vod_event(
request: Request,
event_id: str,
padding: int = Query(0, description="Padding to apply to the vod."),
padding: int = Query(0, ge=0, description="Padding to apply to the vod."),
):
try:
event: Event = Event.get(Event.id == event_id)
Expand Down Expand Up @@ -1521,6 +1521,7 @@ async def preview_gif(
| ((start_ts > Previews.start_time) & (end_ts < Previews.end_time))
)
.where(Previews.camera == camera_name)
.order_by(Previews.start_time.asc())
.limit(1)
.get()
)
Expand All @@ -1530,7 +1531,8 @@ async def preview_gif(
status_code=404,
)

diff = start_ts - preview.start_time
clip_start = max(start_ts, preview.start_time)
diff = clip_start - preview.start_time
minutes = int(diff / 60)
seconds = int(diff % 60)
config: FrigateConfig = request.app.frigate_config
Expand All @@ -1542,7 +1544,7 @@ async def preview_gif(
"-ss",
f"00:{minutes}:{seconds}",
"-t",
f"{end_ts - start_ts}",
f"{end_ts - clip_start}",
"-i",
preview.path,
"-r",
Expand Down Expand Up @@ -1709,6 +1711,7 @@ async def preview_mp4(
| ((start_ts > Previews.start_time) & (end_ts < Previews.end_time))
)
.where(Previews.camera == camera_name)
.order_by(Previews.start_time.asc())
.limit(1)
.get()
)
Expand All @@ -1721,7 +1724,8 @@ async def preview_mp4(
status_code=404,
)

diff = start_ts - preview.start_time
clip_start = max(start_ts, preview.start_time)
diff = clip_start - preview.start_time
minutes = int(diff / 60)
seconds = int(diff % 60)
config: FrigateConfig = request.app.frigate_config
Expand All @@ -1734,7 +1738,7 @@ async def preview_mp4(
"-ss",
f"00:{minutes}:{seconds}",
"-t",
f"{end_ts - start_ts}",
f"{end_ts - clip_start}",
"-i",
preview.path,
"-r",
Expand Down
3 changes: 1 addition & 2 deletions frigate/api/review.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,7 @@ async def review(

# Apply ordering and limit
review_query = (
review_query.order_by(ReviewSegment.severity.asc())
.order_by(ReviewSegment.start_time.desc())
review_query.order_by(ReviewSegment.start_time.desc())
.limit(limit)
.dicts()
.iterator()
Expand Down
7 changes: 6 additions & 1 deletion frigate/record/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -1470,6 +1470,9 @@ def run(self) -> None:

try:
self._run_export(video_path, thumb_path)
except Exception:
self._discard_failed_export(video_path, thumb_path)
raise
finally:
# staged runs hold a full copy of the export, so they must not
# survive a failure the way the small chapter file could
Expand All @@ -1479,7 +1482,8 @@ def _discard_failed_export(self, video_path: str, thumb_path: str) -> None:
"""Drop the partial output and the row that promised it."""
Path(video_path).unlink(missing_ok=True)
Export.delete().where(Export.id == self.export_id).execute()
Path(thumb_path).unlink(missing_ok=True)
if thumb_path:
Path(thumb_path).unlink(missing_ok=True)

def _run_export(self, video_path: str, thumb_path: str) -> None:
try:
Expand All @@ -1498,6 +1502,7 @@ def _run_export(self, video_path: str, thumb_path: str) -> None:
else:
ffmpeg_cmd, playlist_lines = self.get_preview_export_command(video_path)
except DoesNotExist:
self._discard_failed_export(video_path, thumb_path)
return

# When neither custom ffmpeg arg is set the default path uses
Expand Down
10 changes: 8 additions & 2 deletions frigate/test/http_api/test_http_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,8 +393,8 @@ def test_event_retention(self):
def test_event_time_filtering(self):
morning_id = "123456.random"
evening_id = "654321.random"
morning = 1656590400 # 06/30/2022 6 am (GMT)
evening = 1656633600 # 06/30/2022 6 pm (GMT)
morning = 1656568800 # 06/30/2022 6 am (GMT)
evening = 1656612000 # 06/30/2022 6 pm (GMT)

with AuthTestClient(self.app) as client:
super().insert_mock_event(morning_id, morning)
Expand All @@ -417,6 +417,12 @@ def test_event_time_filtering(self):
).json()
assert events
assert len(events) == 1
# an event in the first minute of the range is included
events = client.get(
"/events",
params={"time_range": "06:00,24:00"},
).json()
assert len(events) == 2

def test_set_delete_sub_label(self):
mock_event_updater = Mock(spec=EventMetadataPublisher)
Expand Down
24 changes: 24 additions & 0 deletions frigate/test/http_api/test_http_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,30 @@ def test_rename_export_rejected_while_in_progress(self):
assert response.status_code == 400
assert Export.get(Export.id == "front_door_running").video_path == video

def test_delete_export_rejected_while_in_progress(self):
with tempfile.TemporaryDirectory() as tmpdir:
video = os.path.join(tmpdir, "front_door_abc123.mp4")
with open(video, "wb") as handle:
handle.write(b"video")

Export.create(
id="front_door_running",
camera="front_door",
name="front door export",
date=100,
video_path=video,
thumb_path=os.path.join(tmpdir, "t.webp"),
in_progress=True,
)

with AuthTestClient(self.app) as client:
response = client.post(
"/exports/delete", json={"ids": ["front_door_running"]}
)

assert response.status_code == 400
assert os.path.exists(video)

def test_rename_export_missing_file_leaves_the_row_alone(self):
with tempfile.TemporaryDirectory() as tmpdir:
video = os.path.join(tmpdir, "front_door_gone_abc123.mp4")
Expand Down
4 changes: 4 additions & 0 deletions frigate/test/http_api/test_http_media.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,10 @@ async def allow_both_cameras(request: Request):
{"start_time": 1160, "end_time": 1300},
]

# a non-positive scale would never advance the gap scan
response = client.get("/recordings/unavailable", params={"scale": 0})
assert response.status_code == 422

def test_recordings_summary_day_attribution_by_start_time(self):
"""
A recording spanning midnight marks only its start day.
Expand Down
7 changes: 0 additions & 7 deletions web/public/locales/en/views/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -1086,13 +1086,6 @@
"title": "Confirm Password",
"placeholder": "Confirm Password"
},
"strength": {
"title": "Password strength: ",
"weak": "Weak",
"medium": "Medium",
"strong": "Strong",
"veryStrong": "Very Strong"
},
"requirements": {
"title": "Password requirements:",
"length": "At least 12 characters"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -415,10 +415,19 @@ export default function NotificationsSettingsExtras({
pushSubscription.unsubscribe();
workerRegistration.unregister();
setRegistration(null);
removeMessage("notification_settings", "registration");
});
})
.catch(() => {
toast.error(t("notification.toast.error.registerFailed"), {
position: "top-center",
});
workerRegistration.unregister();
setRegistration(null);
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
removeMessage("notification_settings", "registration");
});
},
[addMessage, publicKey, t],
[addMessage, removeMessage, publicKey, t],
);

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,9 @@ export function KnownPlatesField(props: FieldProps) {
);

const handleAddEntry = useCallback(() => {
if ("" in data) {
return;
}
const next = { ...data, "": [""] };
onChange(next, fieldPath);
}, [data, fieldPath, onChange]);
Expand Down
37 changes: 7 additions & 30 deletions web/src/components/overlay/CreateUserDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,7 @@ import { useTranslation } from "react-i18next";
import { isDesktop, isMobile } from "react-device-detect";
import { cn } from "@/lib/utils";
import { FrigateConfig } from "@/types/frigateConfig";
import {
calculatePasswordStrength,
getPasswordRequirements,
getPasswordStrengthLabel,
getPasswordStrengthColor,
} from "@/utils/passwordUtil";
import { getPasswordRequirements } from "@/utils/passwordUtil";
import {
MobilePage,
MobilePageContent,
Expand Down Expand Up @@ -123,12 +118,6 @@ export default function CreateUserDialog({
const passwordsMatch = password === confirmPassword;
const showMatchIndicator = password && confirmPassword;

// Password strength calculation
const passwordStrength = useMemo(
() => calculatePasswordStrength(password),
[password],
);

const requirements = useMemo(
() => getPasswordRequirements(password),
[password],
Expand Down Expand Up @@ -224,6 +213,12 @@ export default function CreateUserDialog({
type={showPasswordVisible ? "text" : "password"}
className="h-10 pr-10"
{...field}
onChange={(e) => {
field.onChange(e);
if (form.getValues("confirmPassword")) {
form.trigger("confirmPassword");
}
}}
/>
<Button
type="button"
Expand Down Expand Up @@ -255,21 +250,6 @@ export default function CreateUserDialog({

{password && (
<div className="mt-2 space-y-2">
<div className="flex h-1.5 w-full overflow-hidden rounded-full bg-secondary-foreground">
<div
className={`${getPasswordStrengthColor(
password,
)} transition-all duration-300`}
style={{ width: `${passwordStrength * 100}%` }}
/>
</div>
<p className="text-xs text-muted-foreground">
{t("users.dialog.form.password.strength.title")}
<span className="font-medium">
{getPasswordStrengthLabel(password, t)}
</span>
</p>

<div className="space-y-1 rounded-md bg-muted/50 p-2">
<p className="text-xs font-medium text-muted-foreground">
{t("users.dialog.form.password.requirements.title")}
Expand Down Expand Up @@ -297,8 +277,6 @@ export default function CreateUserDialog({
</div>
</div>
)}

<FormMessage />
</FormItem>
)}
/>
Expand Down Expand Up @@ -366,7 +344,6 @@ export default function CreateUserDialog({
)}
</div>
)}
<FormMessage />
</FormItem>
)}
/>
Expand Down
Loading
Loading