mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +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) | ||||
|     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 = ( | ||||
|         ReviewSegment.select( | ||||
|             fn.strftime( | ||||
| @ -161,7 +184,7 @@ def review_summary(): | ||||
|                 ) | ||||
|             ).alias("total_motion"), | ||||
|         ) | ||||
|         .where(ReviewSegment.start_time > month_ago) | ||||
|         .where(reduce(operator.and_, clauses)) | ||||
|         .group_by( | ||||
|             (ReviewSegment.start_time + seconds_offset).cast("int") / (3600 * 24), | ||||
|         ) | ||||
|  | ||||
| @ -1168,7 +1168,7 @@ class FrigateConfig(FrigateBaseModel): | ||||
|     ) | ||||
|     cameras: Dict[str, CameraConfig] = Field(title="Camera configuration.") | ||||
|     camera_groups: Dict[str, CameraGroupConfig] = Field( | ||||
|         default_factory=CameraGroupConfig, title="Camera group configuration" | ||||
|         default_factory=dict, title="Camera group configuration" | ||||
|     ) | ||||
|     timestamp_style: TimestampStyleConfig = Field( | ||||
|         default_factory=TimestampStyleConfig, | ||||
|  | ||||
| @ -25,7 +25,7 @@ type PreviewPlayerProps = { | ||||
|   allPreviews?: Preview[]; | ||||
|   scrollLock?: boolean; | ||||
|   onTimeUpdate?: React.Dispatch<React.SetStateAction<number | undefined>>; | ||||
|   setReviewed: (reviewId: string) => void; | ||||
|   setReviewed: (review: ReviewSegment) => void; | ||||
|   onClick: (reviewId: string, ctrl: boolean) => void; | ||||
| }; | ||||
| 
 | ||||
| @ -65,13 +65,13 @@ export default function PreviewThumbnailPlayer({ | ||||
|   ); | ||||
| 
 | ||||
|   const swipeHandlers = useSwipeable({ | ||||
|     onSwipedLeft: () => (setReviewed ? setReviewed(review.id) : null), | ||||
|     onSwipedLeft: () => (setReviewed ? setReviewed(review) : null), | ||||
|     onSwipedRight: () => setPlayback(true), | ||||
|     preventScrollOnSwipe: true, | ||||
|   }); | ||||
| 
 | ||||
|   const handleSetReviewed = useCallback( | ||||
|     () => setReviewed(review.id), | ||||
|     () => setReviewed(review), | ||||
|     [review, setReviewed], | ||||
|   ); | ||||
| 
 | ||||
| @ -237,7 +237,7 @@ export default function PreviewThumbnailPlayer({ | ||||
| type PreviewContentProps = { | ||||
|   review: ReviewSegment; | ||||
|   relevantPreview: Preview | undefined; | ||||
|   setReviewed?: () => void; | ||||
|   setReviewed: () => void; | ||||
|   setIgnoreClick: (ignore: boolean) => void; | ||||
|   isPlayingBack: (ended: boolean) => void; | ||||
|   onTimeUpdate?: (time: number | undefined) => void; | ||||
| @ -280,7 +280,7 @@ const PREVIEW_PADDING = 16; | ||||
| type VideoPreviewProps = { | ||||
|   review: ReviewSegment; | ||||
|   relevantPreview: Preview; | ||||
|   setReviewed?: () => void; | ||||
|   setReviewed: () => void; | ||||
|   setIgnoreClick: (ignore: boolean) => void; | ||||
|   isPlayingBack: (ended: boolean) => void; | ||||
|   onTimeUpdate?: (time: number | undefined) => void; | ||||
| @ -366,6 +366,10 @@ function VideoPreview({ | ||||
|     setLastPercent(playerPercent); | ||||
| 
 | ||||
|     if (playerPercent > 100) { | ||||
|       if (!review.has_been_reviewed) { | ||||
|         setReviewed(); | ||||
|       } | ||||
| 
 | ||||
|       if (isMobile) { | ||||
|         isPlayingBack(false); | ||||
| 
 | ||||
| @ -483,7 +487,7 @@ function VideoPreview({ | ||||
| const MIN_LOAD_TIMEOUT_MS = 200; | ||||
| type InProgressPreviewProps = { | ||||
|   review: ReviewSegment; | ||||
|   setReviewed?: (reviewId: string) => void; | ||||
|   setReviewed: (reviewId: string) => void; | ||||
|   setIgnoreClick: (ignore: boolean) => void; | ||||
|   isPlayingBack: (ended: boolean) => void; | ||||
|   onTimeUpdate?: (time: number | undefined) => void; | ||||
| @ -518,6 +522,10 @@ function InProgressPreview({ | ||||
|     } | ||||
| 
 | ||||
|     if (key == previewFrames.length - 1) { | ||||
|       if (!review.has_been_reviewed) { | ||||
|         setReviewed(review.id); | ||||
|       } | ||||
| 
 | ||||
|       if (isMobile) { | ||||
|         isPlayingBack(false); | ||||
| 
 | ||||
|  | ||||
| @ -4,7 +4,12 @@ import { useTimezone } from "@/hooks/use-date-utils"; | ||||
| import useOverlayState from "@/hooks/use-overlay-state"; | ||||
| import { FrigateConfig } from "@/types/frigateConfig"; | ||||
| 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 RecordingView from "@/views/events/RecordingView"; | ||||
| import axios from "axios"; | ||||
| @ -104,16 +109,25 @@ export default function Events() { | ||||
| 
 | ||||
|   const onLoadNextPage = useCallback(() => setSize(size + 1), [size, setSize]); | ||||
| 
 | ||||
|   const reloadData = useCallback(() => setBeforeTs(Date.now() / 1000), []); | ||||
| 
 | ||||
|   // review summary
 | ||||
| 
 | ||||
|   const { data: reviewSummary } = useSWR([ | ||||
|   const { data: reviewSummary, mutate: updateSummary } = useSWR< | ||||
|     ReviewSummary[] | ||||
|   >([ | ||||
|     "review/summary", | ||||
|     { timezone: timezone }, | ||||
|     { | ||||
|       timezone: timezone, | ||||
|       cameras: reviewSearchParams["cameras"] ?? null, | ||||
|       labels: reviewSearchParams["labels"] ?? null, | ||||
|     }, | ||||
|     { revalidateOnFocus: false }, | ||||
|   ]); | ||||
| 
 | ||||
|   const reloadData = useCallback(() => { | ||||
|     setBeforeTs(Date.now() / 1000); | ||||
|     updateSummary(); | ||||
|   }, [updateSummary]); | ||||
| 
 | ||||
|   // preview videos
 | ||||
| 
 | ||||
|   const previewTimes = useMemo(() => { | ||||
| @ -145,8 +159,8 @@ export default function Events() { | ||||
|   // review status
 | ||||
| 
 | ||||
|   const markItemAsReviewed = useCallback( | ||||
|     async (reviewId: string) => { | ||||
|       const resp = await axios.post(`review/${reviewId}/viewed`); | ||||
|     async (review: ReviewSegment) => { | ||||
|       const resp = await axios.post(`review/${review.id}/viewed`); | ||||
| 
 | ||||
|       if (resp.status == 200) { | ||||
|         updateSegments( | ||||
| @ -158,7 +172,9 @@ export default function Events() { | ||||
|             const newData: ReviewSegment[][] = []; | ||||
| 
 | ||||
|             data.forEach((page) => { | ||||
|               const reviewIndex = page.findIndex((item) => item.id == reviewId); | ||||
|               const reviewIndex = page.findIndex( | ||||
|                 (item) => item.id == review.id, | ||||
|               ); | ||||
| 
 | ||||
|               if (reviewIndex == -1) { | ||||
|                 newData.push([...page]); | ||||
| @ -175,9 +191,47 @@ export default function Events() { | ||||
|           }, | ||||
|           { 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
 | ||||
|  | ||||
| @ -45,7 +45,7 @@ type EventViewProps = { | ||||
|   severity: ReviewSeverity; | ||||
|   setSeverity: (severity: ReviewSeverity) => void; | ||||
|   loadNextPage: () => void; | ||||
|   markItemAsReviewed: (reviewId: string) => void; | ||||
|   markItemAsReviewed: (review: ReviewSegment) => void; | ||||
|   onOpenReview: (reviewId: string) => void; | ||||
|   pullLatestData: () => void; | ||||
|   updateFilter: (filter: ReviewFilter) => void; | ||||
| @ -72,7 +72,7 @@ export default function EventView({ | ||||
|   // review counts
 | ||||
| 
 | ||||
|   const reviewCounts = useMemo(() => { | ||||
|     if (!reviewSummary) { | ||||
|     if (!reviewSummary || reviewSummary.length == 0) { | ||||
|       return { alert: 0, detection: 0, significant_motion: 0 }; | ||||
|     } | ||||
| 
 | ||||
| @ -80,7 +80,13 @@ export default function EventView({ | ||||
|     if (filter?.before == undefined) { | ||||
|       summary = reviewSummary[0]; | ||||
|     } 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) { | ||||
| @ -303,7 +309,7 @@ type DetectionReviewProps = { | ||||
|   reachedEnd: boolean; | ||||
|   timeRange: { before: number; after: number }; | ||||
|   loadNextPage: () => void; | ||||
|   markItemAsReviewed: (id: string) => void; | ||||
|   markItemAsReviewed: (review: ReviewSegment) => void; | ||||
|   onSelectReview: (id: string, ctrl: boolean) => void; | ||||
|   pullLatestData: () => void; | ||||
| }; | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user