mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
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:
parent
b4b2162ada
commit
bbdb8d36ca
@ -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),
|
||||||
)
|
)
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user