Ability to retain specific clips / events indefinitely (#2831)

This commit is contained in:
Nicolas Mowen 2022-02-21 21:03:01 -07:00 committed by GitHub
parent cbf26e09a4
commit 4004048add
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 161 additions and 4 deletions

View File

@ -188,6 +188,14 @@ Returns data for a single event.
Permanently deletes the event along with any clips/snapshots.
### `POST /api/events/<id>/retain`
Sets retain to true for the event id.
### `DELETE /api/events/<id>/retain`
Sets retain to false for the event id (event may be deleted quickly after removing).
### `GET /api/events/<id>/thumbnail.jpg`
Returns a thumbnail for the event id optimized for notifications. Works while the event is in progress and after completion. Passing `?format=android` will convert the thumbnail to 2:1 aspect ratio.

View File

@ -147,6 +147,7 @@ class EventCleanup(threading.Thread):
Event.camera.not_in(self.camera_keys),
Event.start_time < expire_after,
Event.label == l.label,
Event.retain_indefinitely == False,
)
# delete the media from disk
for event in expired_events:
@ -166,6 +167,7 @@ class EventCleanup(threading.Thread):
Event.camera.not_in(self.camera_keys),
Event.start_time < expire_after,
Event.label == l.label,
Event.retain_indefinitely == False,
)
update_query.execute()
@ -192,6 +194,7 @@ class EventCleanup(threading.Thread):
Event.camera == name,
Event.start_time < expire_after,
Event.label == l.label,
Event.retain_indefinitely == False,
)
# delete the grabbed clips from disk
for event in expired_events:
@ -210,6 +213,7 @@ class EventCleanup(threading.Thread):
Event.camera == name,
Event.start_time < expire_after,
Event.label == l.label,
Event.retain_indefinitely == False,
)
update_query.execute()

View File

@ -120,6 +120,40 @@ def event(id):
return "Event not found", 404
@bp.route("/events/<id>/retain", methods=("POST",))
def set_retain(id):
try:
event = Event.get(Event.id == id)
except DoesNotExist:
return make_response(
jsonify({"success": False, "message": "Event" + id + " not found"}), 404
)
event.retain_indefinitely = True
event.save()
return make_response(
jsonify({"success": True, "message": "Event" + id + " retained"}), 200
)
@bp.route("/events/<id>/retain", methods=("DELETE",))
def delete_retain(id):
try:
event = Event.get(Event.id == id)
except DoesNotExist:
return make_response(
jsonify({"success": False, "message": "Event" + id + " not found"}), 404
)
event.retain_indefinitely = False
event.save()
return make_response(
jsonify({"success": True, "message": "Event" + id + " un-retained"}), 200
)
@bp.route("/events/<id>", methods=("DELETE",))
def delete_event(id):
try:

View File

@ -18,6 +18,7 @@ class Event(Model):
region = JSONField()
box = JSONField()
area = IntegerField()
retain_indefinitely = BooleanField(default=False)
class Recordings(Model):

View File

@ -0,0 +1,46 @@
"""Peewee migrations -- 007_add_retain_indefinitely.py.
Some examples (model - class or model name)::
> Model = migrator.orm['model_name'] # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.python(func, *args, **kwargs) # Run python code
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.drop_index(model, *col_names)
> migrator.add_not_null(model, *field_names)
> migrator.drop_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
"""
import datetime as dt
import peewee as pw
from playhouse.sqlite_ext import *
from decimal import ROUND_HALF_EVEN
from frigate.models import Event
try:
import playhouse.postgres_ext as pw_pext
except ImportError:
pass
SQL = pw.SQL
def migrate(migrator, database, fake=False, **kwargs):
migrator.add_fields(
Event,
retain_indefinitely=pw.BooleanField(default=False),
)
def rollback(migrator, database, fake=False, **kwargs):
migrator.remove_fields(Event, ["retain_indefinitely"])

View File

@ -117,6 +117,24 @@ export function useDelete() {
return deleteEvent;
}
export function useRetain() {
const { state } = useContext(Api);
async function retainEvent(eventId, shouldRetain) {
if (!eventId) return null;
if (shouldRetain) {
const response = await fetch(`${state.host}/api/events/${eventId}/retain`, { method: 'POST' });
return await (response.status < 300 ? response.json() : { success: true });
} else {
const response = await fetch(`${state.host}/api/events/${eventId}/retain`, { method: 'DELETE' });
return await (response.status < 300 ? response.json() : { success: true });
}
}
return retainEvent;
}
export function useApiHost() {
const { state } = useContext(Api);
return state.host;

View File

@ -17,6 +17,13 @@ const ButtonColors = {
text:
'text-red-500 hover:bg-red-500 hover:bg-opacity-20 focus:bg-red-500 focus:bg-opacity-40 active:bg-red-500 active:bg-opacity-40',
},
yellow: {
contained: 'bg-yellow-500 focus:bg-yellow-400 active:bg-yellow-600 ring-yellow-300',
outlined:
'text-yellow-500 border-2 border-yellow-500 hover:bg-yellow-500 hover:bg-opacity-20 focus:bg-yellow-500 focus:bg-opacity-40 active:bg-yellow-500 active:bg-opacity-40',
text:
'text-yellow-500 hover:bg-yellow-500 hover:bg-opacity-20 focus:bg-yellow-500 focus:bg-opacity-40 active:bg-yellow-500 active:bg-opacity-40',
},
green: {
contained: 'bg-green-500 focus:bg-green-400 active:bg-green-600 ring-green-300',
outlined:

View File

@ -0,0 +1,12 @@
import { h } from 'preact';
import { memo } from 'preact/compat';
export function StarRecording({ className = '' }) {
return (
<svg className={`fill-current ${className}`} viewBox="0 0 24 24">
<path d="M14 2H6a2 2 0 00-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6m.5 16.9L12 17.5 9.5 19l.7-2.8L8 14.3l2.9-.2 1.1-2.7 1.1 2.6 2.9.2-2.2 1.9.7 2.8M13 9V3.5L18.5 9H13z" />
</svg>
);
}
export default memo(StarRecording);

View File

@ -7,16 +7,21 @@ import ArrowDown from '../icons/ArrowDropdown';
import ArrowDropup from '../icons/ArrowDropup';
import Clip from '../icons/Clip';
import Close from '../icons/Close';
import StarRecording from '../icons/StarRecording';
import Delete from '../icons/Delete';
import Snapshot from '../icons/Snapshot';
import Dialog from '../components/Dialog';
import Heading from '../components/Heading';
import VideoPlayer from '../components/VideoPlayer';
import { Table, Thead, Tbody, Th, Tr, Td } from '../components/Table';
import { FetchStatus, useApiHost, useEvent, useDelete } from '../api';
import { FetchStatus, useApiHost, useEvent, useDelete, useRetain } from '../api';
const ActionButtonGroup = ({ className, handleClickDelete, close }) => (
const ActionButtonGroup = ({ className, isRetained, handleClickRetain, handleClickDelete, close }) => (
<div className={`space-y-2 space-x-2 sm:space-y-0 xs:space-x-4 ${className}`}>
<Button className="xs:w-auto" color={isRetained ? 'red' : 'yellow'} onClick={handleClickRetain}>
<StarRecording className="w-6" />
{isRetained ? ('Un-retain event') : ('Retain event')}
</Button>
<Button className="xs:w-auto" color="red" onClick={handleClickDelete}>
<Delete className="w-6" /> Delete event
</Button>
@ -54,6 +59,8 @@ export default function Event({ eventId, close, scrollRef }) {
const [showDetails, setShowDetails] = useState(false);
const [shouldScroll, setShouldScroll] = useState(true);
const [deleteStatus, setDeleteStatus] = useState(FetchStatus.NONE);
const [isRetained, setIsRetained] = useState(false);
const setRetainEvent = useRetain();
const setDeleteEvent = useDelete();
useEffect(() => {
@ -71,6 +78,22 @@ export default function Event({ eventId, close, scrollRef }) {
};
}, [data, scrollRef, eventId, shouldScroll]);
const handleClickRetain = useCallback(async () => {
let success;
try {
success = await setRetainEvent(eventId, !isRetained);
if (success) {
setIsRetained(!isRetained);
// Need to reload page otherwise retain button state won't stick if event is collapsed and re-opened.
window.location.reload();
}
} catch (e) {
}
}, [eventId, isRetained, setRetainEvent]);
const handleClickDelete = () => {
setShowDialog(true);
};
@ -98,6 +121,7 @@ export default function Event({ eventId, close, scrollRef }) {
return <ActivityIndicator />;
}
setIsRetained(data.retain_indefinitely);
const startime = new Date(data.start_time * 1000);
const endtime = data.end_time ? new Date(data.end_time * 1000) : null;
return (
@ -119,7 +143,7 @@ export default function Event({ eventId, close, scrollRef }) {
)}
</Button>
</div>
<ActionButtonGroup handleClickDelete={handleClickDelete} close={close} className="hidden sm:block" />
<ActionButtonGroup isRetained={isRetained} handleClickRetain={handleClickRetain} handleClickDelete={handleClickDelete} close={close} className="hidden sm:block" />
{showDialog ? (
<Dialog
onDismiss={handleDismissDeleteDialog}
@ -210,7 +234,7 @@ export default function Event({ eventId, close, scrollRef }) {
</div>
<div className="space-y-2 xs:space-y-0">
<DownloadButtonGroup apiHost={apiHost} eventId={eventId} className="block sm:hidden" />
<ActionButtonGroup handleClickDelete={handleClickDelete} close={close} className="block sm:hidden" />
<ActionButtonGroup isRetained={isRetained} handleClickRetain={handleClickRetain} handleClickDelete={handleClickDelete} close={close} className="block sm:hidden" />
</div>
</div>
);

View File

@ -9,6 +9,7 @@ const TableHead = () => (
<Th>Label</Th>
<Th>Score</Th>
<Th>Zones</Th>
<Th>Retain</Th>
<Th>Date</Th>
<Th>Start</Th>
<Th>End</Th>

View File

@ -22,6 +22,7 @@ const EventsRow = memo(
label,
top_score: score,
zones,
retain_indefinitely
}) => {
const [viewEvent, setViewEvent] = useState(null);
const { searchString, removeDefaultSearchKeys } = useSearchString(limit);
@ -100,6 +101,7 @@ const EventsRow = memo(
))}
</ul>
</Td>
<Td>{retain_indefinitely ? 'True' : 'False'}</Td>
<Td>{start.toLocaleDateString()}</Td>
<Td>{start.toLocaleTimeString()}</Td>
<Td>{end === null ? 'In progress' : end.toLocaleTimeString()}</Td>