diff --git a/frigate/http.py b/frigate/http.py index d9bd5c29f..d634e9b8e 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -275,6 +275,13 @@ def send_to_plus(id): box, event.label, ) + except ValueError: + message = "Error uploading annotation, unsupported label provided." + logger.error(message) + return make_response( + jsonify({"success": False, "message": message}), + 400, + ) except Exception as ex: logger.exception(ex) return make_response( @@ -346,6 +353,13 @@ def false_positive(id): event.model_type, event.detector_type, ) + except ValueError: + message = "Error uploading false positive, unsupported label provided." + logger.error(message) + return make_response( + jsonify({"success": False, "message": message}), + 400, + ) except Exception as ex: logger.exception(ex) return make_response( diff --git a/frigate/plus.py b/frigate/plus.py index 88e025596..2e6144ce3 100644 --- a/frigate/plus.py +++ b/frigate/plus.py @@ -171,6 +171,17 @@ class PlusApi: ) if not r.ok: + try: + error_response = r.json() + errors = error_response.get("errors", []) + for error in errors: + if ( + error.get("param") == "label" + and error.get("type") == "invalid_enum_value" + ): + raise ValueError(f"Unsupported label value provided: {label}") + except ValueError as e: + raise e raise Exception(r.text) def add_annotation( @@ -193,6 +204,17 @@ class PlusApi: ) if not r.ok: + try: + error_response = r.json() + errors = error_response.get("errors", []) + for error in errors: + if ( + error.get("param") == "label" + and error.get("type") == "invalid_enum_value" + ): + raise ValueError(f"Unsupported label value provided: {label}") + except ValueError as e: + raise e raise Exception(r.text) def get_model_download_url( diff --git a/web/src/routes/Events.jsx b/web/src/routes/Events.jsx index 2b2b546ef..0777829a8 100644 --- a/web/src/routes/Events.jsx +++ b/web/src/routes/Events.jsx @@ -7,7 +7,7 @@ import Link from '../components/Link'; import { useApiHost } from '../api'; import useSWR from 'swr'; import useSWRInfinite from 'swr/infinite'; -import axios from 'axios'; +import axios, { AxiosError } from 'axios'; import { useState, useRef, useCallback, useMemo } from 'preact/hooks'; import VideoPlayer from '../components/VideoPlayer'; import { StarRecording } from '../icons/StarRecording'; @@ -79,6 +79,7 @@ export default function Events({ path, ...props }) { validBox: null, }); const [uploading, setUploading] = useState([]); + const [uploadErrors, setUploadErrors] = useState([]); const [viewEvent, setViewEvent] = useState(props.event); const [eventOverlay, setEventOverlay] = useState(); const [eventDetailType, setEventDetailType] = useState('clip'); @@ -328,27 +329,40 @@ export default function Events({ path, ...props }) { setUploading((prev) => [...prev, id]); - const response = false_positive - ? await axios.put(`events/${id}/false_positive`) - : await axios.post(`events/${id}/plus`, validBox ? { include_annotation: 1 } : {}); + try { + const response = false_positive + ? await axios.put(`events/${id}/false_positive`) + : await axios.post(`events/${id}/plus`, validBox ? { include_annotation: 1 } : {}); - if (response.status === 200) { - mutate( - (pages) => - pages.map((page) => - page.map((event) => { - if (event.id === id) { - return { ...event, plus_id: response.data.plus_id }; - } - return event; - }) - ), - false - ); + if (response.status === 200) { + mutate( + (pages) => + pages.map((page) => + page.map((event) => { + if (event.id === id) { + return { ...event, plus_id: response.data.plus_id }; + } + return event; + }) + ), + false + ); + } + } catch (e) { + if ( + e instanceof AxiosError && + (e.response.data.message === 'Error uploading annotation, unsupported label provided.' || + e.response.data.message === 'Error uploading false positive, unsupported label provided.') + ) { + setUploadErrors((prev) => [...prev, { id, isUnsupported: true }]); + return; + } + setUploadErrors((prev) => [...prev, { id }]); + throw e; + } finally { + setUploading((prev) => prev.filter((i) => i !== id)); } - setUploading((prev) => prev.filter((i) => i !== id)); - if (state.showDownloadMenu && downloadEvent.id === id) { setState({ ...state, showDownloadMenu: false }); } @@ -681,6 +695,7 @@ export default function Events({ path, ...props }) { viewEvent={viewEvent} setViewEvent={setViewEvent} uploading={uploading} + uploadErrors={uploadErrors} handleEventDetailTabChange={handleEventDetailTabChange} onEventFrameSelected={onEventFrameSelected} onDelete={onDelete} @@ -721,6 +736,7 @@ export default function Events({ path, ...props }) { lastEvent={lastEvent} lastEventRef={lastEventRef} uploading={uploading} + uploadErrors={uploadErrors} handleEventDetailTabChange={handleEventDetailTabChange} onEventFrameSelected={onEventFrameSelected} onDelete={onDelete} @@ -760,6 +776,7 @@ function Event({ lastEvent, lastEventRef, uploading, + uploadErrors, handleEventDetailTabChange, onEventFrameSelected, onDelete, @@ -769,6 +786,19 @@ function Event({ onSave, showSubmitToPlus, }) { + const getUploadButtonState = (eventId) => { + const isUploading = uploading.includes(eventId); + const hasUploadError = uploadErrors.find((event) => event.id === eventId); + if (hasUploadError) { + if (hasUploadError.isUnsupported) { + return { isDisabled: true, label: 'Unsupported label' }; + } + return { isDisabled: isUploading, label: 'Upload error' }; + } + + const label = isUploading ? 'Uploading...' : 'Send to Frigate+'; + return { isDisabled: isUploading, label }; + }; const apiHost = useApiHost(); return ( @@ -849,10 +879,10 @@ function Event({ ) : ( )}