* Allow deleting failed in progress exports

* Fix comparison and preview retrieval

* Fix stretching of event cards

* Reset edit state when group changes

* Allow specifying group
This commit is contained in:
Nicolas Mowen 2024-06-04 09:10:19 -06:00 committed by GitHub
parent 7917bf55ff
commit 2875e84cb5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 146 additions and 85 deletions

View File

@ -4,6 +4,7 @@ import logging
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
import psutil
from flask import ( from flask import (
Blueprint, Blueprint,
current_app, current_app,
@ -14,6 +15,7 @@ from flask import (
from peewee import DoesNotExist from peewee import DoesNotExist
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from frigate.const import EXPORT_DIR
from frigate.models import Export, Recordings from frigate.models import Export, Recordings
from frigate.record.export import PlaybackFactorEnum, RecordingExporter from frigate.record.export import PlaybackFactorEnum, RecordingExporter
@ -140,6 +142,27 @@ def export_delete(id: str):
404, 404,
) )
files_in_use = []
for process in psutil.process_iter():
try:
if process.name() != "ffmpeg":
continue
flist = process.open_files()
if flist:
for nt in flist:
if nt.path.startswith(EXPORT_DIR):
files_in_use.append(nt.path.split("/")[-1])
except psutil.Error:
continue
if export.video_path.split("/")[-1] in files_in_use:
return make_response(
jsonify(
{"success": False, "message": "Can not delete in progress export."}
),
400,
)
Path(export.video_path).unlink(missing_ok=True) Path(export.video_path).unlink(missing_ok=True)
if export.thumb_path: if export.thumb_path:

View File

@ -11,6 +11,8 @@ import threading
from enum import Enum from enum import Enum
from pathlib import Path from pathlib import Path
from peewee import DoesNotExist
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
from frigate.const import ( from frigate.const import (
CACHE_DIR, CACHE_DIR,
@ -72,30 +74,32 @@ class RecordingExporter(threading.Thread):
if datetime.datetime.fromtimestamp( if datetime.datetime.fromtimestamp(
self.start_time self.start_time
) < datetime.datetime.now().replace(minute=0, second=0): ) < datetime.datetime.now().astimezone(datetime.timezone.dst).replace(
minute=0, second=0, microsecond=0
):
# has preview mp4 # has preview mp4
preview: Previews = ( try:
Previews.select( preview: Previews = (
Previews.camera, Previews.select(
Previews.path, Previews.camera,
Previews.duration, Previews.path,
Previews.start_time, Previews.duration,
Previews.end_time, Previews.start_time,
) Previews.end_time,
.where(
Previews.start_time.between(self.start_time, self.end_time)
| Previews.end_time.between(self.start_time, self.end_time)
| (
(self.start_time > Previews.start_time)
& (self.end_time < Previews.end_time)
) )
.where(
Previews.start_time.between(self.start_time, self.end_time)
| Previews.end_time.between(self.start_time, self.end_time)
| (
(self.start_time > Previews.start_time)
& (self.end_time < Previews.end_time)
)
)
.where(Previews.camera == self.camera)
.limit(1)
.get()
) )
.where(Previews.camera == self.camera) except DoesNotExist:
.limit(1)
.get()
)
if not preview:
return "" return ""
diff = self.start_time - preview.start_time diff = self.start_time - preview.start_time

View File

@ -14,8 +14,12 @@ import { baseUrl } from "@/api/baseUrl";
type AnimatedEventCardProps = { type AnimatedEventCardProps = {
event: ReviewSegment; event: ReviewSegment;
selectedGroup?: string;
}; };
export function AnimatedEventCard({ event }: AnimatedEventCardProps) { export function AnimatedEventCard({
event,
selectedGroup,
}: AnimatedEventCardProps) {
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
const currentHour = useMemo(() => isCurrentHour(event.start_time), [event]); const currentHour = useMemo(() => isCurrentHour(event.start_time), [event]);
@ -53,7 +57,8 @@ export function AnimatedEventCard({ event }: AnimatedEventCardProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const onOpenReview = useCallback(() => { const onOpenReview = useCallback(() => {
navigate("review", { const url = selectedGroup ? `review?group=${selectedGroup}` : "review";
navigate(url, {
state: { state: {
severity: event.severity, severity: event.severity,
recording: { recording: {
@ -64,7 +69,7 @@ export function AnimatedEventCard({ event }: AnimatedEventCardProps) {
}, },
}); });
axios.post(`reviews/viewed`, { ids: [event.id] }); axios.post(`reviews/viewed`, { ids: [event.id] });
}, [navigate, event]); }, [navigate, selectedGroup, event]);
// image behavior // image behavior

View File

@ -109,43 +109,38 @@ export default function ExportCard({
"relative flex aspect-video items-center justify-center rounded-lg bg-black md:rounded-2xl", "relative flex aspect-video items-center justify-center rounded-lg bg-black md:rounded-2xl",
className, className,
)} )}
onMouseEnter={ onMouseEnter={isDesktop ? () => setHovered(true) : undefined}
isDesktop && !exportedRecording.in_progress onMouseLeave={isDesktop ? () => setHovered(false) : undefined}
? () => setHovered(true) onClick={isDesktop ? undefined : () => setHovered(!hovered)}
: undefined
}
onMouseLeave={
isDesktop && !exportedRecording.in_progress
? () => setHovered(false)
: undefined
}
onClick={
isDesktop || exportedRecording.in_progress
? undefined
: () => setHovered(!hovered)
}
> >
{hovered && ( {hovered && (
<> <>
<div className="absolute inset-0 z-10 rounded-lg bg-black bg-opacity-60 md:rounded-2xl" /> <div className="absolute inset-0 z-10 rounded-lg bg-black bg-opacity-60 md:rounded-2xl" />
<div className="absolute right-1 top-1 flex items-center gap-2"> <div className="absolute right-1 top-1 flex items-center gap-2">
<a {!exportedRecording.in_progress && (
className="z-20" <a
download className="z-20"
href={`${baseUrl}${exportedRecording.video_path.replace("/media/frigate/", "")}`} download
> href={`${baseUrl}${exportedRecording.video_path.replace("/media/frigate/", "")}`}
<Chip className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500"> >
<FaDownload className="size-4 text-white" /> <Chip className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500">
<FaDownload className="size-4 text-white" />
</Chip>
</a>
)}
{!exportedRecording.in_progress && (
<Chip
className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500"
onClick={() =>
setEditName({
original: exportedRecording.name,
update: "",
})
}
>
<MdEditSquare className="size-4 text-white" />
</Chip> </Chip>
</a> )}
<Chip
className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500"
onClick={() =>
setEditName({ original: exportedRecording.name, update: "" })
}
>
<MdEditSquare className="size-4 text-white" />
</Chip>
<Chip <Chip
className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500" className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500"
onClick={() => onClick={() =>
@ -159,15 +154,17 @@ export default function ExportCard({
</Chip> </Chip>
</div> </div>
<Button {!exportedRecording.in_progress && (
className="absolute left-1/2 top-1/2 z-20 h-20 w-20 -translate-x-1/2 -translate-y-1/2 cursor-pointer text-white hover:bg-transparent hover:text-white" <Button
variant="ghost" className="absolute left-1/2 top-1/2 z-20 h-20 w-20 -translate-x-1/2 -translate-y-1/2 cursor-pointer text-white hover:bg-transparent hover:text-white"
onClick={() => { variant="ghost"
onSelect(exportedRecording); onClick={() => {
}} onSelect(exportedRecording);
> }}
<FaPlay /> >
</Button> <FaPlay />
</Button>
)}
</> </>
)} )}
{exportedRecording.in_progress ? ( {exportedRecording.in_progress ? (

View File

@ -68,6 +68,19 @@ export default function Events() {
const [reviewFilter, setReviewFilter, reviewSearchParams] = const [reviewFilter, setReviewFilter, reviewSearchParams] =
useApiFilter<ReviewFilter>(); useApiFilter<ReviewFilter>();
useSearchEffect("group", (reviewGroup) => {
if (config && reviewGroup) {
const group = config.camera_groups[reviewGroup];
if (group) {
setReviewFilter({
...reviewFilter,
cameras: group.cameras,
});
}
}
});
const onUpdateFilter = useCallback( const onUpdateFilter = useCallback(
(newFilter: ReviewFilter) => { (newFilter: ReviewFilter) => {
setReviewFilter(newFilter); setReviewFilter(newFilter);

View File

@ -668,27 +668,32 @@ function Timeline({
<Skeleton className="size-full" /> <Skeleton className="size-full" />
) )
) : ( ) : (
<div <div className="h-full overflow-auto bg-secondary">
className={`grid h-full grid-cols-1 gap-4 overflow-auto bg-secondary p-4 ${isDesktop ? "" : "sm:grid-cols-2"}`} <div
> className={cn(
{mainCameraReviewItems.map((review) => { "grid h-auto grid-cols-1 gap-4 overflow-auto p-4",
if (review.severity == "significant_motion") { isMobile && "sm:grid-cols-2",
return; )}
} >
{mainCameraReviewItems.map((review) => {
if (review.severity == "significant_motion") {
return;
}
return ( return (
<ReviewCard <ReviewCard
key={review.id} key={review.id}
event={review} event={review}
currentTime={currentTime} currentTime={currentTime}
onClick={() => { onClick={() => {
setScrubbing(true); setScrubbing(true);
setCurrentTime(review.start_time - REVIEW_PADDING); setCurrentTime(review.start_time - REVIEW_PADDING);
setScrubbing(false); setScrubbing(false);
}} }}
/> />
); );
})} })}
</div>
</div> </div>
)} )}
</div> </div>

View File

@ -91,8 +91,16 @@ export default function DraggableGridLayout({
); );
}, [config]); }, [config]);
// editing
const [editGroup, setEditGroup] = useState(false); const [editGroup, setEditGroup] = useState(false);
useEffect(() => {
setEditGroup(false);
}, [cameraGroup]);
// camera state
const [currentCameras, setCurrentCameras] = useState<CameraConfig[]>(); const [currentCameras, setCurrentCameras] = useState<CameraConfig[]>();
const [currentIncludeBirdseye, setCurrentIncludeBirdseye] = const [currentIncludeBirdseye, setCurrentIncludeBirdseye] =
useState<boolean>(); useState<boolean>();

View File

@ -224,7 +224,13 @@ export default function LiveDashboardView({
<TooltipProvider> <TooltipProvider>
<div className="flex items-center gap-2 px-1"> <div className="flex items-center gap-2 px-1">
{events.map((event) => { {events.map((event) => {
return <AnimatedEventCard key={event.id} event={event} />; return (
<AnimatedEventCard
key={event.id}
event={event}
selectedGroup={cameraGroup}
/>
);
})} })}
</div> </div>
</TooltipProvider> </TooltipProvider>