diff --git a/frigate/http.py b/frigate/http.py index acb2eaf12..b6bb1a856 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -20,6 +20,7 @@ from flask import ( Flask, Response, current_app, + escape, jsonify, make_response, request, @@ -532,10 +533,14 @@ def event_thumbnail(id, max_cache_age=2592000): if tracked_obj is not None: thumbnail_bytes = tracked_obj.get_thumbnail() except Exception: - return "Event not found", 404 + return make_response( + jsonify({"success": False, "message": "Event not found"}), 404 + ) if thumbnail_bytes is None: - return "Event not found", 404 + return make_response( + jsonify({"success": False, "message": "Event not found"}), 404 + ) # android notifications prefer a 2:1 ratio if format == "android": @@ -630,7 +635,9 @@ def event_snapshot(id): event = Event.get(Event.id == id, Event.end_time != None) event_complete = True if not event.has_snapshot: - return "Snapshot not available", 404 + return make_response( + jsonify({"success": False, "message": "Snapshot not available"}), 404 + ) # read snapshot from disk with open( os.path.join(CLIPS_DIR, f"{event.camera}-{id}.jpg"), "rb" @@ -652,12 +659,18 @@ def event_snapshot(id): quality=request.args.get("quality", default=70, type=int), ) except Exception: - return "Event not found", 404 + return make_response( + jsonify({"success": False, "message": "Event not found"}), 404 + ) except Exception: - return "Event not found", 404 + return make_response( + jsonify({"success": False, "message": "Event not found"}), 404 + ) if jpg_bytes is None: - return "Event not found", 404 + return make_response( + jsonify({"success": False, "message": "Event not found"}), 404 + ) response = make_response(jpg_bytes) response.headers["Content-Type"] = "image/jpeg" @@ -710,10 +723,14 @@ def event_clip(id): try: event: Event = Event.get(Event.id == id) except DoesNotExist: - return "Event not found.", 404 + return make_response( + jsonify({"success": False, "message": "Event not found"}), 404 + ) if not event.has_clip: - return "Clip not available", 404 + return make_response( + jsonify({"success": False, "message": "Clip not available"}), 404 + ) file_name = f"{event.camera}-{id}.mp4" clip_path = os.path.join(CLIPS_DIR, file_name) @@ -1019,7 +1036,9 @@ def config_raw(): config_file = config_file_yaml if not os.path.isfile(config_file): - return "Could not find file", 410 + return make_response( + jsonify({"success": False, "message": "Could not find file"}), 404 + ) with open(config_file, "r") as f: raw_config = f.read() @@ -1035,7 +1054,12 @@ def config_save(): new_config = request.get_data().decode() if not new_config: - return "Config with body param is required", 400 + return make_response( + jsonify( + {"success": False, "message": "Config with body param is required"} + ), + 400, + ) # Validate the config schema try: @@ -1045,7 +1069,7 @@ def config_save(): jsonify( { "success": False, - "message": f"\nConfig Error:\n\n{str(traceback.format_exc())}", + "message": f"\nConfig Error:\n\n{escape(str(traceback.format_exc()))}", } ), 400, @@ -1080,14 +1104,30 @@ def config_save(): restart_frigate() except Exception as e: logging.error(f"Error restarting Frigate: {e}") - return "Config successfully saved, unable to restart Frigate", 200 + return make_response( + jsonify( + { + "success": True, + "message": "Config successfully saved, unable to restart Frigate", + } + ), + 200, + ) - return ( - "Config successfully saved, restarting (this can take up to one minute)...", + return make_response( + jsonify( + { + "success": True, + "message": "Config successfully saved, restarting (this can take up to one minute)...", + } + ), 200, ) else: - return "Config successfully saved.", 200 + return make_response( + jsonify({"success": True, "message": "Config successfully saved."}), + 200, + ) @bp.route("/config/set", methods=["PUT"]) @@ -1127,9 +1167,20 @@ def config_set(): ) except Exception as e: logging.error(f"Error updating config: {e}") - return "Error updating config", 500 + return make_response( + jsonify({"success": False, "message": "Error updating config"}), + 500, + ) - return "Config successfully updated, restart to apply", 200 + return make_response( + jsonify( + { + "success": True, + "message": "Config successfully updated, restart to apply", + } + ), + 200, + ) @bp.route("/config/schema.json") @@ -1179,7 +1230,10 @@ def mjpeg_feed(camera_name): mimetype="multipart/x-mixed-replace; boundary=frame", ) else: - return "Camera named {} not found".format(camera_name), 404 + return make_response( + jsonify({"success": False, "message": "Camera not found"}), + 404, + ) @bp.route("//ptz/info") @@ -1187,7 +1241,10 @@ def camera_ptz_info(camera_name): if camera_name in current_app.frigate_config.cameras: return jsonify(current_app.onvif.get_camera_info(camera_name)) else: - return "Camera named {} not found".format(camera_name), 404 + return make_response( + jsonify({"success": False, "message": "Camera not found"}), + 404, + ) @bp.route("//latest.jpg") @@ -1229,7 +1286,10 @@ def latest_frame(camera_name): width = int(height * frame.shape[1] / frame.shape[0]) if frame is None: - return "Unable to get valid frame from {}".format(camera_name), 500 + return make_response( + jsonify({"success": False, "message": "Unable to get valid frame"}), + 500, + ) if height < 1 or width < 1: return ( @@ -1265,7 +1325,10 @@ def latest_frame(camera_name): response.headers["Cache-Control"] = "no-store" return response else: - return "Camera named {} not found".format(camera_name), 404 + return make_response( + jsonify({"success": False, "message": "Camera not found"}), + 404, + ) @bp.route("//recordings//snapshot.png") @@ -1315,7 +1378,15 @@ def get_snapshot_from_recording(camera_name: str, frame_time: str): response.headers["Content-Type"] = "image/png" return response except DoesNotExist: - return "Recording not found for {} at {}".format(camera_name, frame_time), 404 + return make_response( + jsonify( + { + "success": False, + "message": "Recording not found at {}".format(frame_time), + } + ), + 404, + ) @bp.route("/recordings/storage", methods=["GET"]) @@ -1517,7 +1588,15 @@ def recording_clip(camera_name, start_ts, end_ts): if p.returncode != 0: logger.error(p.stderr) - return f"Could not create clip from recordings for {camera_name}.", 500 + return make_response( + jsonify( + { + "success": False, + "message": "Could not create clip from recordings", + } + ), + 500, + ) else: logger.debug( f"Ignoring subsequent request for {path} as it already exists in the cache." @@ -1573,7 +1652,15 @@ def vod_ts(camera_name, start_ts, end_ts): if not clips: logger.error("No recordings found for the requested time range") - return "No recordings found.", 404 + return make_response( + jsonify( + { + "success": False, + "message": "No recordings found.", + } + ), + 404, + ) hour_ago = datetime.now() - timedelta(hours=1) return jsonify( @@ -1616,11 +1703,27 @@ def vod_event(id): event: Event = Event.get(Event.id == id) except DoesNotExist: logger.error(f"Event not found: {id}") - return "Event not found.", 404 + return make_response( + jsonify( + { + "success": False, + "message": "Event not found.", + } + ), + 404, + ) if not event.has_clip: logger.error(f"Event does not have recordings: {id}") - return "Recordings not available", 404 + return make_response( + jsonify( + { + "success": False, + "message": "Recordings not available.", + } + ), + 404, + ) clip_path = os.path.join(CLIPS_DIR, f"{event.camera}-{id}.mp4") @@ -1697,7 +1800,15 @@ def export_recording(camera_name: str, start_time, end_time): else PlaybackFactorEnum.realtime, ) exporter.start() - return "Starting export of recording", 200 + return make_response( + jsonify( + { + "success": True, + "message": "Starting export of recording.", + } + ), + 200, + ) @bp.route("/export/", methods=["DELETE"]) @@ -1711,7 +1822,15 @@ def export_delete(file_name: str): ) os.unlink(file) - return "Successfully deleted file", 200 + return make_response( + jsonify( + { + "success": True, + "message": "Successfully deleted file.", + } + ), + 200, + ) def imagestream(detected_frames_processor, camera_name, fps, height, draw_options): @@ -1811,8 +1930,11 @@ def logs(service: str): } service_location = log_locations.get(service) - if not service: - return f"{service} is not a valid service", 404 + if not service_location: + return make_response( + jsonify({"success": False, "message": "Not a valid service"}), + 404, + ) try: file = open(service_location, "r") @@ -1820,4 +1942,7 @@ def logs(service: str): file.close() return contents, 200 except FileNotFoundError as e: - return f"Could not find log file: {e}", 500 + return make_response( + jsonify({"success": False, "message": f"Could not find log file: {e}"}), + 500, + )