Improve review data (#10246)

* Adjust remaining summary items when items are marked as reviewed

* Add api for filtering and show correct number when filtering

* Fix default group config

* Update review summary when data is reloaded

* Fix quick items not getting reviewed
This commit is contained in:
Nicolas Mowen 2024-03-05 05:02:34 -07:00 committed by GitHub
parent b4b2162ada
commit bbdb8d36ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 112 additions and 21 deletions

View File

@ -77,6 +77,29 @@ def review_summary():
hour_modifier, minute_modifier, seconds_offset = get_tz_modifiers(tz_name) hour_modifier, minute_modifier, seconds_offset = get_tz_modifiers(tz_name)
month_ago = (datetime.now() - timedelta(days=30)).timestamp() month_ago = (datetime.now() - timedelta(days=30)).timestamp()
cameras = request.args.get("cameras", "all")
labels = request.args.get("labels", "all")
clauses = [(ReviewSegment.start_time > month_ago)]
if cameras != "all":
camera_list = cameras.split(",")
clauses.append((ReviewSegment.camera << camera_list))
if labels != "all":
# use matching so segments with multiple labels
# still match on a search where any label matches
label_clauses = []
filtered_labels = labels.split(",")
for label in filtered_labels:
label_clauses.append(
(ReviewSegment.data["objects"].cast("text") % f'*"{label}"*')
)
label_clause = reduce(operator.or_, label_clauses)
clauses.append((label_clause))
groups = ( groups = (
ReviewSegment.select( ReviewSegment.select(
fn.strftime( fn.strftime(
@ -161,7 +184,7 @@ def review_summary():
) )
).alias("total_motion"), ).alias("total_motion"),
) )
.where(ReviewSegment.start_time > month_ago) .where(reduce(operator.and_, clauses))
.group_by( .group_by(
(ReviewSegment.start_time + seconds_offset).cast("int") / (3600 * 24), (ReviewSegment.start_time + seconds_offset).cast("int") / (3600 * 24),
) )

View File

@ -1168,7 +1168,7 @@ class FrigateConfig(FrigateBaseModel):
) )
cameras: Dict[str, CameraConfig] = Field(title="Camera configuration.") cameras: Dict[str, CameraConfig] = Field(title="Camera configuration.")
camera_groups: Dict[str, CameraGroupConfig] = Field( camera_groups: Dict[str, CameraGroupConfig] = Field(
default_factory=CameraGroupConfig, title="Camera group configuration" default_factory=dict, title="Camera group configuration"
) )
timestamp_style: TimestampStyleConfig = Field( timestamp_style: TimestampStyleConfig = Field(
default_factory=TimestampStyleConfig, default_factory=TimestampStyleConfig,

View File

@ -25,7 +25,7 @@ type PreviewPlayerProps = {
allPreviews?: Preview[]; allPreviews?: Preview[];
scrollLock?: boolean; scrollLock?: boolean;
onTimeUpdate?: React.Dispatch<React.SetStateAction<number | undefined>>; onTimeUpdate?: React.Dispatch<React.SetStateAction<number | undefined>>;
setReviewed: (reviewId: string) => void; setReviewed: (review: ReviewSegment) => void;
onClick: (reviewId: string, ctrl: boolean) => void; onClick: (reviewId: string, ctrl: boolean) => void;
}; };
@ -65,13 +65,13 @@ export default function PreviewThumbnailPlayer({
); );
const swipeHandlers = useSwipeable({ const swipeHandlers = useSwipeable({
onSwipedLeft: () => (setReviewed ? setReviewed(review.id) : null), onSwipedLeft: () => (setReviewed ? setReviewed(review) : null),
onSwipedRight: () => setPlayback(true), onSwipedRight: () => setPlayback(true),
preventScrollOnSwipe: true, preventScrollOnSwipe: true,
}); });
const handleSetReviewed = useCallback( const handleSetReviewed = useCallback(
() => setReviewed(review.id), () => setReviewed(review),
[review, setReviewed], [review, setReviewed],
); );
@ -237,7 +237,7 @@ export default function PreviewThumbnailPlayer({
type PreviewContentProps = { type PreviewContentProps = {
review: ReviewSegment; review: ReviewSegment;
relevantPreview: Preview | undefined; relevantPreview: Preview | undefined;
setReviewed?: () => void; setReviewed: () => void;
setIgnoreClick: (ignore: boolean) => void; setIgnoreClick: (ignore: boolean) => void;
isPlayingBack: (ended: boolean) => void; isPlayingBack: (ended: boolean) => void;
onTimeUpdate?: (time: number | undefined) => void; onTimeUpdate?: (time: number | undefined) => void;
@ -280,7 +280,7 @@ const PREVIEW_PADDING = 16;
type VideoPreviewProps = { type VideoPreviewProps = {
review: ReviewSegment; review: ReviewSegment;
relevantPreview: Preview; relevantPreview: Preview;
setReviewed?: () => void; setReviewed: () => void;
setIgnoreClick: (ignore: boolean) => void; setIgnoreClick: (ignore: boolean) => void;
isPlayingBack: (ended: boolean) => void; isPlayingBack: (ended: boolean) => void;
onTimeUpdate?: (time: number | undefined) => void; onTimeUpdate?: (time: number | undefined) => void;
@ -366,6 +366,10 @@ function VideoPreview({
setLastPercent(playerPercent); setLastPercent(playerPercent);
if (playerPercent > 100) { if (playerPercent > 100) {
if (!review.has_been_reviewed) {
setReviewed();
}
if (isMobile) { if (isMobile) {
isPlayingBack(false); isPlayingBack(false);
@ -483,7 +487,7 @@ function VideoPreview({
const MIN_LOAD_TIMEOUT_MS = 200; const MIN_LOAD_TIMEOUT_MS = 200;
type InProgressPreviewProps = { type InProgressPreviewProps = {
review: ReviewSegment; review: ReviewSegment;
setReviewed?: (reviewId: string) => void; setReviewed: (reviewId: string) => void;
setIgnoreClick: (ignore: boolean) => void; setIgnoreClick: (ignore: boolean) => void;
isPlayingBack: (ended: boolean) => void; isPlayingBack: (ended: boolean) => void;
onTimeUpdate?: (time: number | undefined) => void; onTimeUpdate?: (time: number | undefined) => void;
@ -518,6 +522,10 @@ function InProgressPreview({
} }
if (key == previewFrames.length - 1) { if (key == previewFrames.length - 1) {
if (!review.has_been_reviewed) {
setReviewed(review.id);
}
if (isMobile) { if (isMobile) {
isPlayingBack(false); isPlayingBack(false);

View File

@ -4,7 +4,12 @@ import { useTimezone } from "@/hooks/use-date-utils";
import useOverlayState from "@/hooks/use-overlay-state"; import useOverlayState from "@/hooks/use-overlay-state";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { Preview } from "@/types/preview"; import { Preview } from "@/types/preview";
import { ReviewFilter, ReviewSegment, ReviewSeverity } from "@/types/review"; import {
ReviewFilter,
ReviewSegment,
ReviewSeverity,
ReviewSummary,
} from "@/types/review";
import EventView from "@/views/events/EventView"; import EventView from "@/views/events/EventView";
import RecordingView from "@/views/events/RecordingView"; import RecordingView from "@/views/events/RecordingView";
import axios from "axios"; import axios from "axios";
@ -104,16 +109,25 @@ export default function Events() {
const onLoadNextPage = useCallback(() => setSize(size + 1), [size, setSize]); const onLoadNextPage = useCallback(() => setSize(size + 1), [size, setSize]);
const reloadData = useCallback(() => setBeforeTs(Date.now() / 1000), []);
// review summary // review summary
const { data: reviewSummary } = useSWR([ const { data: reviewSummary, mutate: updateSummary } = useSWR<
ReviewSummary[]
>([
"review/summary", "review/summary",
{ timezone: timezone }, {
timezone: timezone,
cameras: reviewSearchParams["cameras"] ?? null,
labels: reviewSearchParams["labels"] ?? null,
},
{ revalidateOnFocus: false }, { revalidateOnFocus: false },
]); ]);
const reloadData = useCallback(() => {
setBeforeTs(Date.now() / 1000);
updateSummary();
}, [updateSummary]);
// preview videos // preview videos
const previewTimes = useMemo(() => { const previewTimes = useMemo(() => {
@ -145,8 +159,8 @@ export default function Events() {
// review status // review status
const markItemAsReviewed = useCallback( const markItemAsReviewed = useCallback(
async (reviewId: string) => { async (review: ReviewSegment) => {
const resp = await axios.post(`review/${reviewId}/viewed`); const resp = await axios.post(`review/${review.id}/viewed`);
if (resp.status == 200) { if (resp.status == 200) {
updateSegments( updateSegments(
@ -158,7 +172,9 @@ export default function Events() {
const newData: ReviewSegment[][] = []; const newData: ReviewSegment[][] = [];
data.forEach((page) => { data.forEach((page) => {
const reviewIndex = page.findIndex((item) => item.id == reviewId); const reviewIndex = page.findIndex(
(item) => item.id == review.id,
);
if (reviewIndex == -1) { if (reviewIndex == -1) {
newData.push([...page]); newData.push([...page]);
@ -175,9 +191,47 @@ export default function Events() {
}, },
{ revalidate: false, populateCache: true }, { revalidate: false, populateCache: true },
); );
updateSummary(
(data: ReviewSummary[] | undefined) => {
if (!data) {
return data;
}
const day = new Date(review.start_time * 1000);
const key = `${day.getFullYear()}-${("0" + (day.getMonth() + 1)).slice(-2)}-${("0" + day.getDate()).slice(-2)}`;
const index = data.findIndex((summary) => summary.day == key);
if (index == -1) {
return data;
}
const item = data[index];
return [
...data.slice(0, index),
{
...item,
reviewed_alert:
review.severity == "alert"
? item.reviewed_alert + 1
: item.reviewed_alert,
reviewed_detection:
review.severity == "detection"
? item.reviewed_detection + 1
: item.reviewed_detection,
reviewed_motion:
review.severity == "significant_motion"
? item.reviewed_motion + 1
: item.reviewed_motion,
},
...data.slice(index + 1),
];
},
{ revalidate: false, populateCache: true },
);
} }
}, },
[updateSegments], [updateSegments, updateSummary],
); );
// selected items // selected items

View File

@ -45,7 +45,7 @@ type EventViewProps = {
severity: ReviewSeverity; severity: ReviewSeverity;
setSeverity: (severity: ReviewSeverity) => void; setSeverity: (severity: ReviewSeverity) => void;
loadNextPage: () => void; loadNextPage: () => void;
markItemAsReviewed: (reviewId: string) => void; markItemAsReviewed: (review: ReviewSegment) => void;
onOpenReview: (reviewId: string) => void; onOpenReview: (reviewId: string) => void;
pullLatestData: () => void; pullLatestData: () => void;
updateFilter: (filter: ReviewFilter) => void; updateFilter: (filter: ReviewFilter) => void;
@ -72,7 +72,7 @@ export default function EventView({
// review counts // review counts
const reviewCounts = useMemo(() => { const reviewCounts = useMemo(() => {
if (!reviewSummary) { if (!reviewSummary || reviewSummary.length == 0) {
return { alert: 0, detection: 0, significant_motion: 0 }; return { alert: 0, detection: 0, significant_motion: 0 };
} }
@ -80,7 +80,13 @@ export default function EventView({
if (filter?.before == undefined) { if (filter?.before == undefined) {
summary = reviewSummary[0]; summary = reviewSummary[0];
} else { } else {
summary = reviewSummary[0]; const day = new Date(filter.before * 1000);
const key = `${day.getFullYear()}-${("0" + (day.getMonth() + 1)).slice(-2)}-${("0" + day.getDate()).slice(-2)}`;
summary = reviewSummary.find((check) => check.day == key);
}
if (!summary) {
return { alert: 0, detection: 0, significant_motion: 0 };
} }
if (filter?.showReviewed == 1) { if (filter?.showReviewed == 1) {
@ -303,7 +309,7 @@ type DetectionReviewProps = {
reachedEnd: boolean; reachedEnd: boolean;
timeRange: { before: number; after: number }; timeRange: { before: number; after: number };
loadNextPage: () => void; loadNextPage: () => void;
markItemAsReviewed: (id: string) => void; markItemAsReviewed: (review: ReviewSegment) => void;
onSelectReview: (id: string, ctrl: boolean) => void; onSelectReview: (id: string, ctrl: boolean) => void;
pullLatestData: () => void; pullLatestData: () => void;
}; };