diff --git a/frigate/api/record.py b/frigate/api/record.py index 6eeb9fbe6..4ab4b0af1 100644 --- a/frigate/api/record.py +++ b/frigate/api/record.py @@ -1,5 +1,6 @@ """Recording APIs.""" +import datetime as dt import logging from datetime import datetime, timedelta from functools import reduce @@ -97,45 +98,22 @@ def all_recordings_summary( days: dict[str, bool] = {} for period_start, period_end, period_offset in dst_periods: - hours_offset = int(period_offset / 60 / 60) - minutes_offset = int(period_offset / 60 - hours_offset * 60) - period_hour_modifier = f"{hours_offset} hour" - period_minute_modifier = f"{minutes_offset} minute" + day_expr = ((Recordings.start_time + period_offset) / 86400).cast("int") period_query = ( - Recordings.select( - fn.strftime( - "%Y-%m-%d", - fn.datetime( - Recordings.start_time, - "unixepoch", - period_hour_modifier, - period_minute_modifier, - ), - ).alias("day") - ) + Recordings.select(day_expr.alias("day_idx")) .where( (Recordings.camera << camera_list) & (Recordings.end_time >= period_start) & (Recordings.start_time <= period_end) ) - .group_by( - fn.strftime( - "%Y-%m-%d", - fn.datetime( - Recordings.start_time, - "unixepoch", - period_hour_modifier, - period_minute_modifier, - ), - ) - ) - .order_by(Recordings.start_time.desc()) + .distinct() .namedtuples() ) for g in period_query: - days[g.day] = True + day_str = (dt.date(1970, 1, 1) + dt.timedelta(days=g.day_idx)).isoformat() + days[day_str] = True return JSONResponse(content=dict(sorted(days.items()))) diff --git a/web/src/components/overlay/ReviewActivityCalendar.tsx b/web/src/components/overlay/ReviewActivityCalendar.tsx index 86bc424ba..fe9256c23 100644 --- a/web/src/components/overlay/ReviewActivityCalendar.tsx +++ b/web/src/components/overlay/ReviewActivityCalendar.tsx @@ -3,7 +3,7 @@ import { Calendar } from "../ui/calendar"; import { ButtonHTMLAttributes, useEffect, useMemo, useRef } from "react"; import { FaCircle } from "react-icons/fa"; import { getUTCOffset } from "@/utils/dateUtil"; -import { type DayButtonProps, TZDate } from "react-day-picker"; +import { type DayButtonProps } from "react-day-picker"; import { LAST_24_HOURS_KEY } from "@/types/filter"; import { useUserPersistence } from "@/hooks/use-user-persistence"; import { cn } from "@/lib/utils"; @@ -38,60 +38,46 @@ export default function ReviewActivityCalendar({ }, []); const modifiers = useMemo(() => { - const recordings: Date[] = []; - const alerts: Date[] = []; - const detections: Date[] = []; + const recordingsSet = new Set(); + const alertsSet = new Set(); + const detectionsSet = new Set(); - // Handle recordings if (recordingsSummary) { - Object.keys(recordingsSummary).forEach((date) => { - if (date === LAST_24_HOURS_KEY) { - return; + for (const date of Object.keys(recordingsSummary)) { + if (date !== LAST_24_HOURS_KEY) { + recordingsSet.add(date); } - - const parts = date.split("-"); - const cal = new TZDate(date + "T00:00:00", timezone); - - cal.setFullYear( - parseInt(parts[0]), - parseInt(parts[1]) - 1, - parseInt(parts[2]), - ); - - recordings.push(cal); - }); + } } - // Handle reviews if present if (reviewSummary) { - Object.entries(reviewSummary).forEach(([date, data]) => { - if (date === LAST_24_HOURS_KEY) { - return; - } - - const parts = date.split("-"); - const cal = new TZDate(date + "T00:00:00", timezone); - - cal.setFullYear( - parseInt(parts[0]), - parseInt(parts[1]) - 1, - parseInt(parts[2]), - ); + for (const [date, data] of Object.entries(reviewSummary)) { + if (date === LAST_24_HOURS_KEY) continue; if (data.total_alert > data.reviewed_alert) { - alerts.push(cal); + alertsSet.add(date); } else if (data.total_detection > data.reviewed_detection) { - detections.push(cal); + detectionsSet.add(date); } - }); + } } - return { alerts, detections, recordings }; - }, [reviewSummary, recordingsSummary, timezone]); + const formatDay = (day: Date) => { + const y = day.getFullYear(); + const m = String(day.getMonth() + 1).padStart(2, "0"); + const d = String(day.getDate()).padStart(2, "0"); + return `${y}-${m}-${d}`; + }; + + return { + recordings: (day: Date) => recordingsSet.has(formatDay(day)), + alerts: (day: Date) => alertsSet.has(formatDay(day)), + detections: (day: Date) => detectionsSet.has(formatDay(day)), + }; + }, [reviewSummary, recordingsSummary]); return (