From 43ade86796c7ebd354c1173a54da6a18f64deb77 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 26 Apr 2023 05:08:53 -0600 Subject: [PATCH 1/3] Support Controlling PTZ Cameras Via WebUI (#4715) * Add support for ptz commands via websocket * Fix startup issues * Fix bugs * Set config manually * Add more commands * Add presets * Add zooming * Fixes * Set name * Cleanup * Add ability to set presets from UI * Add ability to set preset from UI * Cleanup for errors * Ui tweaks * Add visual design for pan / tilt * Add pan/tilt support * Support zooming * Try to set wsdl * Fix duplicate logs * Catch auth errors * Don't init onvif for disabled cameras * Fix layout sizing * Don't comment out * Fix formatting * Add ability to control camera with keyboard shortcuts * Disallow user selection * Fix mobile pressing * Remove logs * Substitute onvif password * Add ptz controls ot birdseye * Put wsdl back * Add padding * Formatting * Catch onvif error * Optimize layout for mobile and web * Place ptz controls next to birdseye view in large layout * Fix pt support * Center text titles * Update tests * Update docs * Write camera docs for PTZ * Add MQTT docs for PTZ * Add ptz info docs for http * Fix test * Make half width when full screen * Fix preset panel logic * Fix parsing * Update mqtt.md * Catch preset error * Add onvif example to docs * Remove template example from main camera docs --- docs/docs/configuration/cameras.md | 18 ++ docs/docs/configuration/index.md | 21 ++ docs/docs/integrations/api.md | 4 + docs/docs/integrations/mqtt.md | 13 +- frigate/app.py | 10 +- frigate/comms/dispatcher.py | 29 ++- frigate/comms/mqtt.py | 6 + frigate/config.py | 19 ++ frigate/http.py | 11 + frigate/log.py | 8 + frigate/ptz.py | 219 +++++++++++++++++++ frigate/test/test_http.py | 59 ++++- requirements-wheels.txt | 1 + web/src/api/ws.jsx | 9 + web/src/components/CameraControlPanel.jsx | 248 ++++++++++++++++++++++ web/src/icons/ArrowDownDouble.jsx | 19 ++ web/src/icons/ArrowLeftDouble.jsx | 19 ++ web/src/icons/ArrowRightDouble.jsx | 11 +- web/src/icons/ArrowUpDouble.jsx | 19 ++ web/src/routes/Birdseye.jsx | 34 ++- web/src/routes/Camera.jsx | 8 + 21 files changed, 769 insertions(+), 16 deletions(-) create mode 100644 frigate/ptz.py create mode 100644 web/src/components/CameraControlPanel.jsx create mode 100644 web/src/icons/ArrowDownDouble.jsx create mode 100644 web/src/icons/ArrowLeftDouble.jsx create mode 100644 web/src/icons/ArrowUpDouble.jsx diff --git a/docs/docs/configuration/cameras.md b/docs/docs/configuration/cameras.md index d8fefed8f..8f907cb3f 100644 --- a/docs/docs/configuration/cameras.md +++ b/docs/docs/configuration/cameras.md @@ -48,3 +48,21 @@ cameras: ``` For camera model specific settings check the [camera specific](camera_specific.md) infos. + +## Setting up camera PTZ controls + +Add onvif config to camera + +```yaml +cameras: + back: + ffmpeg: + ... + onvif: + host: 10.0.10.10 + port: 8000 + user: admin + password: password +``` + +then PTZ controls will be available in the cameras WebUI. diff --git a/docs/docs/configuration/index.md b/docs/docs/configuration/index.md index 113884d6d..93a266b56 100644 --- a/docs/docs/configuration/index.md +++ b/docs/docs/configuration/index.md @@ -55,6 +55,14 @@ mqtt: - path: rtsp://{FRIGATE_RTSP_USER}:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:8554/unicast ``` +```yaml +onvif: + host: 10.0.10.10 + port: 8000 + user: "{FRIGATE_RTSP_USER}" + password: "{FRIGATE_RTSP_PASSWORD}" +``` + ```yaml mqtt: # Optional: Enable mqtt server (default: shown below) @@ -497,6 +505,19 @@ cameras: # Optional: Whether or not to show the camera in the Frigate UI (default: shown below) dashboard: True + # Optional: connect to ONVIF camera + # to enable PTZ controls. + onvif: + # Required: host of the camera being connected to. + host: 0.0.0.0 + # Optional: ONVIF port for device (default: shown below). + port: 8000 + # Optional: username for login. + # NOTE: Some devices require admin to access ONVIF. + user: admin + # Optional: password for login. + password: admin + # Optional ui: # Optional: Set the default live mode for cameras in the UI (default: shown below) diff --git a/docs/docs/integrations/api.md b/docs/docs/integrations/api.md index cc5a6576a..9aec392a4 100644 --- a/docs/docs/integrations/api.md +++ b/docs/docs/integrations/api.md @@ -291,3 +291,7 @@ Get ffprobe output for camera feed paths. | param | Type | Description | | ------- | ------ | ---------------------------------- | | `paths` | string | `,` separated list of camera paths | + +### `GET /api//ptz/info` + +Get PTZ info for the camera. diff --git a/docs/docs/integrations/mqtt.md b/docs/docs/integrations/mqtt.md index 2a2195abd..814656258 100644 --- a/docs/docs/integrations/mqtt.md +++ b/docs/docs/integrations/mqtt.md @@ -157,4 +157,15 @@ Topic to adjust motion contour area for a camera. Expected value is an integer. ### `frigate//motion_contour_area/state` -Topic with current motion contour area for a camera. Published value is an integer. \ No newline at end of file +Topic with current motion contour area for a camera. Published value is an integer. + +### `frigate//ptz` + +Topic to send PTZ commands to camera. + +| Command | Description | +| ---------------------- | --------------------------------------------------------------------------------------- | +| `preset-` | send command to move to preset with name `` | +| `MOVE_` | send command to continuously move in ``, possible values are [UP, DOWN, LEFT, RIGHT] | +| `ZOOM_` | send command to continuously zoom ``, possible values are [IN, OUT] | +| `STOP` | send command to stop moving | diff --git a/frigate/app.py b/frigate/app.py index 8c1cd4433..54d2825c8 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -27,6 +27,7 @@ from frigate.models import Event, Recordings, Timeline from frigate.object_processing import TrackedObjectProcessor from frigate.output import output_frames from frigate.plus import PlusApi +from frigate.ptz import OnvifController from frigate.record import RecordingCleanup, RecordingMaintainer from frigate.stats import StatsEmitter, stats_init from frigate.storage import StorageMaintainer @@ -173,9 +174,13 @@ class FrigateApp: self.stats_tracking, self.detected_frames_processor, self.storage_maintainer, + self.onvif_controller, self.plus_api, ) + def init_onvif(self) -> None: + self.onvif_controller = OnvifController(self.config) + def init_dispatcher(self) -> None: comms: list[Communicator] = [] @@ -183,7 +188,9 @@ class FrigateApp: comms.append(MqttClient(self.config)) comms.append(WebSocketClient(self.config)) - self.dispatcher = Dispatcher(self.config, self.camera_metrics, comms) + self.dispatcher = Dispatcher( + self.config, self.onvif_controller, self.camera_metrics, comms + ) def start_detectors(self) -> None: for name in self.config.cameras.keys(): @@ -382,6 +389,7 @@ class FrigateApp: self.set_log_levels() self.init_queues() self.init_database() + self.init_onvif() self.init_dispatcher() except Exception as e: print(e) diff --git a/frigate/comms/dispatcher.py b/frigate/comms/dispatcher.py index d304509e4..7a2c98392 100644 --- a/frigate/comms/dispatcher.py +++ b/frigate/comms/dispatcher.py @@ -7,6 +7,7 @@ from typing import Any, Callable from abc import ABC, abstractmethod from frigate.config import FrigateConfig +from frigate.ptz import OnvifController, OnvifCommandEnum from frigate.types import CameraMetricsTypes from frigate.util import restart_frigate @@ -39,10 +40,12 @@ class Dispatcher: def __init__( self, config: FrigateConfig, + onvif: OnvifController, camera_metrics: dict[str, CameraMetricsTypes], communicators: list[Communicator], ) -> None: self.config = config + self.onvif = onvif self.camera_metrics = camera_metrics self.comms = communicators @@ -63,12 +66,21 @@ class Dispatcher: """Handle receiving of payload from communicators.""" if topic.endswith("set"): try: + # example /cam_name/detect/set payload=ON|OFF camera_name = topic.split("/")[-3] command = topic.split("/")[-2] self._camera_settings_handlers[command](camera_name, payload) - except Exception as e: + except IndexError as e: logger.error(f"Received invalid set command: {topic}") return + elif topic.endswith("ptz"): + try: + # example /cam_name/ptz payload=MOVE_UP|MOVE_DOWN|STOP... + camera_name = topic.split("/")[-2] + self._on_ptz_command(camera_name, payload) + except IndexError as e: + logger.error(f"Received invalid ptz command: {topic}") + return elif topic == "restart": restart_frigate() @@ -204,3 +216,18 @@ class Dispatcher: snapshots_settings.enabled = False self.publish(f"{camera_name}/snapshots/state", payload, retain=True) + + def _on_ptz_command(self, camera_name: str, payload: str) -> None: + """Callback for ptz topic.""" + try: + if "preset" in payload.lower(): + command = OnvifCommandEnum.preset + param = payload.lower().split("-")[1] + else: + command = OnvifCommandEnum[payload.lower()] + param = "" + + self.onvif.handle_command(camera_name, command, param) + logger.info(f"Setting ptz command to {command} for {camera_name}") + except KeyError as k: + logger.error(f"Invalid PTZ command {payload}: {k}") diff --git a/frigate/comms/mqtt.py b/frigate/comms/mqtt.py index d106aae71..37fdd44ae 100644 --- a/frigate/comms/mqtt.py +++ b/frigate/comms/mqtt.py @@ -167,6 +167,12 @@ class MqttClient(Communicator): # type: ignore[misc] self.on_mqtt_command, ) + if self.config.cameras[name].onvif.host: + self.client.message_callback_add( + f"{self.mqtt_config.topic_prefix}/{name}/ptz", + self.on_mqtt_command, + ) + self.client.message_callback_add( f"{self.mqtt_config.topic_prefix}/restart", self.on_mqtt_command ) diff --git a/frigate/config.py b/frigate/config.py index b62fd29fe..b7d965282 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -125,6 +125,13 @@ class MqttConfig(FrigateBaseModel): return v +class OnvifConfig(FrigateBaseModel): + host: str = Field(default="", title="Onvif Host") + port: int = Field(default=8000, title="Onvif Port") + user: Optional[str] = Field(title="Onvif Username") + password: Optional[str] = Field(title="Onvif Password") + + class RetainModeEnum(str, Enum): all = "all" motion = "motion" @@ -607,6 +614,9 @@ class CameraConfig(FrigateBaseModel): detect: DetectConfig = Field( default_factory=DetectConfig, title="Object detection configuration." ) + onvif: OnvifConfig = Field( + default_factory=OnvifConfig, title="Camera Onvif Configuration." + ) ui: CameraUiConfig = Field( default_factory=CameraUiConfig, title="Camera UI Modifications." ) @@ -939,6 +949,15 @@ class FrigateConfig(FrigateBaseModel): for input in camera_config.ffmpeg.inputs: input.path = input.path.format(**FRIGATE_ENV_VARS) + # ONVIF substitution + if camera_config.onvif.user or camera_config.onvif.password: + camera_config.onvif.user = camera_config.onvif.user.format( + **FRIGATE_ENV_VARS + ) + camera_config.onvif.password = camera_config.onvif.password.format( + **FRIGATE_ENV_VARS + ) + # Add default filters object_keys = camera_config.objects.track if camera_config.objects.filters is None: diff --git a/frigate/http.py b/frigate/http.py index efe99c182..cd2a0c523 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -36,6 +36,7 @@ from frigate.const import CLIPS_DIR, MAX_SEGMENT_DURATION, RECORD_DIR from frigate.models import Event, Recordings, Timeline from frigate.object_processing import TrackedObject from frigate.plus import PlusApi +from frigate.ptz import OnvifController from frigate.stats import stats_snapshot from frigate.util import ( clean_camera_user_pass, @@ -59,6 +60,7 @@ def create_app( stats_tracking, detected_frames_processor, storage_maintainer: StorageMaintainer, + onvif: OnvifController, plus_api: PlusApi, ): app = Flask(__name__) @@ -77,6 +79,7 @@ def create_app( app.stats_tracking = stats_tracking app.detected_frames_processor = detected_frames_processor app.storage_maintainer = storage_maintainer + app.onvif = onvif app.plus_api = plus_api app.camera_error_image = None app.hwaccel_errors = [] @@ -994,6 +997,14 @@ def mjpeg_feed(camera_name): return "Camera named {} not found".format(camera_name), 404 +@bp.route("//ptz/info") +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 + + @bp.route("//latest.jpg") def latest_frame(camera_name): draw_options = { diff --git a/frigate/log.py b/frigate/log.py index a8041592f..67866942c 100644 --- a/frigate/log.py +++ b/frigate/log.py @@ -19,6 +19,10 @@ from frigate.util import clean_camera_user_pass def listener_configurer() -> None: root = logging.getLogger() + + if root.hasHandlers(): + root.handlers.clear() + console_handler = logging.StreamHandler() formatter = logging.Formatter( "[%(asctime)s] %(name)-30s %(levelname)-8s: %(message)s", "%Y-%m-%d %H:%M:%S" @@ -31,6 +35,10 @@ def listener_configurer() -> None: def root_configurer(queue: Queue) -> None: h = handlers.QueueHandler(queue) root = logging.getLogger() + + if root.hasHandlers(): + root.handlers.clear() + root.addHandler(h) root.setLevel(logging.INFO) diff --git a/frigate/ptz.py b/frigate/ptz.py new file mode 100644 index 000000000..e2c21618e --- /dev/null +++ b/frigate/ptz.py @@ -0,0 +1,219 @@ +"""Configure and control camera via onvif.""" + +import logging +import site + +from enum import Enum +from onvif import ONVIFCamera, ONVIFError + +from frigate.config import FrigateConfig + + +logger = logging.getLogger(__name__) + + +class OnvifCommandEnum(str, Enum): + """Holds all possible move commands""" + + init = "init" + move_down = "move_down" + move_left = "move_left" + move_right = "move_right" + move_up = "move_up" + preset = "preset" + stop = "stop" + zoom_in = "zoom_in" + zoom_out = "zoom_out" + + +class OnvifController: + def __init__(self, config: FrigateConfig) -> None: + self.cams: dict[str, ONVIFCamera] = {} + + for cam_name, cam in config.cameras.items(): + if not cam.enabled: + continue + + if cam.onvif.host: + try: + self.cams[cam_name] = { + "onvif": ONVIFCamera( + cam.onvif.host, + cam.onvif.port, + cam.onvif.user, + cam.onvif.password, + wsdl_dir=site.getsitepackages()[0].replace( + "dist-packages", "site-packages" + ) + + "/wsdl", + ), + "init": False, + "active": False, + "presets": {}, + } + except ONVIFError as e: + logger.error(f"Onvif connection to {cam.name} failed: {e}") + + def _init_onvif(self, camera_name: str) -> bool: + onvif: ONVIFCamera = self.cams[camera_name]["onvif"] + + # create init services + media = onvif.create_media_service() + + try: + profile = media.GetProfiles()[0] + except ONVIFError as e: + logger.error(f"Unable to connect to camera: {camera_name}: {e}") + return False + + ptz = onvif.create_ptz_service() + request = ptz.create_type("GetConfigurationOptions") + request.ConfigurationToken = profile.PTZConfiguration.token + + # setup moving request + move_request = ptz.create_type("ContinuousMove") + move_request.ProfileToken = profile.token + self.cams[camera_name]["move_request"] = move_request + + # setup existing presets + try: + presets: list[dict] = ptz.GetPresets({"ProfileToken": profile.token}) + except ONVIFError as e: + logger.error(f"Unable to get presets from camera: {camera_name}: {e}") + return False + + for preset in presets: + self.cams[camera_name]["presets"][preset["Name"].lower()] = preset["token"] + + # get list of supported features + ptz_config = ptz.GetConfigurationOptions(request) + supported_features = [] + + if ptz_config.Spaces and ptz_config.Spaces.ContinuousPanTiltVelocitySpace: + supported_features.append("pt") + + if ptz_config.Spaces and ptz_config.Spaces.ContinuousZoomVelocitySpace: + supported_features.append("zoom") + + self.cams[camera_name]["features"] = supported_features + + self.cams[camera_name]["init"] = True + return True + + def _stop(self, camera_name: str) -> None: + onvif: ONVIFCamera = self.cams[camera_name]["onvif"] + move_request = self.cams[camera_name]["move_request"] + onvif.get_service("ptz").Stop( + { + "ProfileToken": move_request.ProfileToken, + "PanTilt": True, + "Zoom": True, + } + ) + self.cams[camera_name]["active"] = False + + def _move(self, camera_name: str, command: OnvifCommandEnum) -> None: + if self.cams[camera_name]["active"]: + logger.warning( + f"{camera_name} is already performing an action, stopping..." + ) + self._stop(camera_name) + + self.cams[camera_name]["active"] = True + onvif: ONVIFCamera = self.cams[camera_name]["onvif"] + move_request = self.cams[camera_name]["move_request"] + + if command == OnvifCommandEnum.move_left: + move_request.Velocity = {"PanTilt": {"x": -0.5, "y": 0}} + elif command == OnvifCommandEnum.move_right: + move_request.Velocity = {"PanTilt": {"x": 0.5, "y": 0}} + elif command == OnvifCommandEnum.move_up: + move_request.Velocity = { + "PanTilt": { + "x": 0, + "y": 0.5, + } + } + elif command == OnvifCommandEnum.move_down: + move_request.Velocity = { + "PanTilt": { + "x": 0, + "y": -0.5, + } + } + + onvif.get_service("ptz").ContinuousMove(move_request) + + def _move_to_preset(self, camera_name: str, preset: str) -> None: + if not preset in self.cams[camera_name]["presets"]: + logger.error(f"{preset} is not a valid preset for {camera_name}") + return + + self.cams[camera_name]["active"] = True + move_request = self.cams[camera_name]["move_request"] + onvif: ONVIFCamera = self.cams[camera_name]["onvif"] + preset_token = self.cams[camera_name]["presets"][preset] + onvif.get_service("ptz").GotoPreset( + { + "ProfileToken": move_request.ProfileToken, + "PresetToken": preset_token, + } + ) + self.cams[camera_name]["active"] = False + + def _zoom(self, camera_name: str, command: OnvifCommandEnum) -> None: + if self.cams[camera_name]["active"]: + logger.warning( + f"{camera_name} is already performing an action, stopping..." + ) + self._stop(camera_name) + + self.cams[camera_name]["active"] = True + onvif: ONVIFCamera = self.cams[camera_name]["onvif"] + move_request = self.cams[camera_name]["move_request"] + + if command == OnvifCommandEnum.zoom_in: + move_request.Velocity = {"Zoom": {"x": 0.5}} + elif command == OnvifCommandEnum.zoom_out: + move_request.Velocity = {"Zoom": {"x": -0.5}} + + onvif.get_service("ptz").ContinuousMove(move_request) + + def handle_command( + self, camera_name: str, command: OnvifCommandEnum, param: str = "" + ) -> None: + if camera_name not in self.cams.keys(): + logger.error(f"Onvif is not setup for {camera_name}") + return + + if not self.cams[camera_name]["init"]: + if not self._init_onvif(camera_name): + return + + if command == OnvifCommandEnum.init: + # already init + return + elif command == OnvifCommandEnum.stop: + self._stop(camera_name) + elif command == OnvifCommandEnum.preset: + self._move_to_preset(camera_name, param) + elif ( + command == OnvifCommandEnum.zoom_in or command == OnvifCommandEnum.zoom_out + ): + self._zoom(camera_name, command) + else: + self._move(camera_name, command) + + def get_camera_info(self, camera_name: str) -> dict[str, any]: + if camera_name not in self.cams.keys(): + logger.error(f"Onvif is not setup for {camera_name}") + return {} + + if not self.cams[camera_name]["init"]: + self._init_onvif(camera_name) + + return { + "name": camera_name, + "features": self.cams[camera_name]["features"], + "presets": list(self.cams[camera_name]["presets"].keys()), + } diff --git a/frigate/test/test_http.py b/frigate/test/test_http.py index 12105926e..bc08ec010 100644 --- a/frigate/test/test_http.py +++ b/frigate/test/test_http.py @@ -114,7 +114,13 @@ class TestHttp(unittest.TestCase): def test_get_event_list(self): app = create_app( - FrigateConfig(**self.minimal_config), self.db, None, None, None, PlusApi() + FrigateConfig(**self.minimal_config), + self.db, + None, + None, + None, + None, + PlusApi(), ) id = "123456.random" id2 = "7890.random" @@ -143,7 +149,13 @@ class TestHttp(unittest.TestCase): def test_get_good_event(self): app = create_app( - FrigateConfig(**self.minimal_config), self.db, None, None, None, PlusApi() + FrigateConfig(**self.minimal_config), + self.db, + None, + None, + None, + None, + PlusApi(), ) id = "123456.random" @@ -157,7 +169,13 @@ class TestHttp(unittest.TestCase): def test_get_bad_event(self): app = create_app( - FrigateConfig(**self.minimal_config), self.db, None, None, None, PlusApi() + FrigateConfig(**self.minimal_config), + self.db, + None, + None, + None, + None, + PlusApi(), ) id = "123456.random" bad_id = "654321.other" @@ -170,7 +188,13 @@ class TestHttp(unittest.TestCase): def test_delete_event(self): app = create_app( - FrigateConfig(**self.minimal_config), self.db, None, None, None, PlusApi() + FrigateConfig(**self.minimal_config), + self.db, + None, + None, + None, + None, + PlusApi(), ) id = "123456.random" @@ -185,7 +209,13 @@ class TestHttp(unittest.TestCase): def test_event_retention(self): app = create_app( - FrigateConfig(**self.minimal_config), self.db, None, None, None, PlusApi() + FrigateConfig(**self.minimal_config), + self.db, + None, + None, + None, + None, + PlusApi(), ) id = "123456.random" @@ -204,7 +234,13 @@ class TestHttp(unittest.TestCase): def test_set_delete_sub_label(self): app = create_app( - FrigateConfig(**self.minimal_config), self.db, None, None, None, PlusApi() + FrigateConfig(**self.minimal_config), + self.db, + None, + None, + None, + None, + PlusApi(), ) id = "123456.random" sub_label = "sub" @@ -232,7 +268,13 @@ class TestHttp(unittest.TestCase): def test_sub_label_list(self): app = create_app( - FrigateConfig(**self.minimal_config), self.db, None, None, None, PlusApi() + FrigateConfig(**self.minimal_config), + self.db, + None, + None, + None, + None, + PlusApi(), ) id = "123456.random" sub_label = "sub" @@ -255,6 +297,7 @@ class TestHttp(unittest.TestCase): None, None, None, + None, PlusApi(), ) @@ -270,6 +313,7 @@ class TestHttp(unittest.TestCase): None, None, None, + None, PlusApi(), ) id = "123456.random" @@ -288,6 +332,7 @@ class TestHttp(unittest.TestCase): None, None, None, + None, PlusApi(), ) mock_stats.return_value = self.test_stats diff --git a/requirements-wheels.txt b/requirements-wheels.txt index b19c7947f..f20dd4c6d 100644 --- a/requirements-wheels.txt +++ b/requirements-wheels.txt @@ -4,6 +4,7 @@ imutils == 0.5.* matplotlib == 3.6.* mypy == 0.942 numpy == 1.23.* +onvif_zeep == 0.2.12 opencv-python-headless == 4.5.5.* paho-mqtt == 1.6.* peewee == 3.15.* diff --git a/web/src/api/ws.jsx b/web/src/api/ws.jsx index 734200215..8995a065b 100644 --- a/web/src/api/ws.jsx +++ b/web/src/api/ws.jsx @@ -120,6 +120,15 @@ export function useSnapshotsState(camera) { return { payload, send, connected }; } +export function usePtzCommand(camera) { + const { + value: { payload }, + send, + connected, + } = useWs(`${camera}/ptz`, `${camera}/ptz`); + return { payload, send, connected }; +} + export function useRestart() { const { value: { payload }, diff --git a/web/src/components/CameraControlPanel.jsx b/web/src/components/CameraControlPanel.jsx new file mode 100644 index 000000000..90bf3ef27 --- /dev/null +++ b/web/src/components/CameraControlPanel.jsx @@ -0,0 +1,248 @@ +import { h } from 'preact'; +import { useState } from 'preact/hooks'; +import useSWR from 'swr'; +import { usePtzCommand } from '../api/ws'; +import ActivityIndicator from './ActivityIndicator'; +import ArrowRightDouble from '../icons/ArrowRightDouble'; +import ArrowUpDouble from '../icons/ArrowUpDouble'; +import ArrowDownDouble from '../icons/ArrowDownDouble'; +import ArrowLeftDouble from '../icons/ArrowLeftDouble'; +import Button from './Button'; +import Heading from './Heading'; + +export default function CameraControlPanel({ camera = '' }) { + const { data: ptz } = useSWR(`${camera}/ptz/info`); + const [currentPreset, setCurrentPreset] = useState(''); + + const { payload: _, send: sendPtz } = usePtzCommand(camera); + + const onSetPreview = async (e) => { + e.stopPropagation(); + + if (currentPreset == 'none') { + return; + } + + sendPtz(`preset-${currentPreset}`); + setCurrentPreset(''); + }; + + const onSetMove = async (e, dir) => { + e.stopPropagation(); + sendPtz(`MOVE_${dir}`); + setCurrentPreset(''); + }; + + const onSetZoom = async (e, dir) => { + e.stopPropagation(); + sendPtz(`ZOOM_${dir}`); + setCurrentPreset(''); + }; + + const onSetStop = async (e) => { + e.stopPropagation(); + sendPtz('STOP'); + }; + + if (!ptz) { + return ; + } + + document.addEventListener('keydown', (e) => { + if (!e) { + return; + } + + if (e.repeat) { + e.preventDefault(); + return; + } + + if (ptz.features.includes('pt')) { + if (e.key === 'ArrowLeft') { + e.preventDefault(); + onSetMove(e, 'LEFT'); + } else if (e.key === 'ArrowRight') { + e.preventDefault(); + onSetMove(e, 'RIGHT'); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + onSetMove(e, 'UP'); + } else if (e.key === 'ArrowDown') { + e.preventDefault(); + onSetMove(e, 'DOWN'); + } + + if (ptz.features.includes('zoom')) { + if (e.key == '+') { + e.preventDefault(); + onSetZoom(e, 'IN'); + } else if (e.key == '-') { + e.preventDefault(); + onSetZoom(e, 'OUT'); + } + } + } + }); + + document.addEventListener('keyup', (e) => { + if (!e || e.repeat) { + return; + } + + if ( + e.key === 'ArrowLeft' || + e.key === 'ArrowRight' || + e.key === 'ArrowUp' || + e.key === 'ArrowDown' || + e.key === '+' || + e.key === '-' + ) { + e.preventDefault(); + onSetStop(e); + } + }); + + return ( +
+ {ptz.features.includes('pt') && ( +
+
+ + Pan / Tilt + +
+ +
+
+ + +
+
+ +
+
+
+ )} + + {ptz.features.includes('zoom') && ( +
+ + Zoom + +
+ +
+
+
+ +
+
+ )} + + {(ptz.presets || []).length > 0 && ( +
+ + Presets + +
+ +
+ + +
+ )} +
+ ); +} diff --git a/web/src/icons/ArrowDownDouble.jsx b/web/src/icons/ArrowDownDouble.jsx new file mode 100644 index 000000000..7685542e9 --- /dev/null +++ b/web/src/icons/ArrowDownDouble.jsx @@ -0,0 +1,19 @@ +import { h } from 'preact'; +import { memo } from 'preact/compat'; + +export function ArrowDownDouble({ className = '' }) { + return ( + + + + ); +} + +export default memo(ArrowDownDouble); diff --git a/web/src/icons/ArrowLeftDouble.jsx b/web/src/icons/ArrowLeftDouble.jsx new file mode 100644 index 000000000..eaa25395c --- /dev/null +++ b/web/src/icons/ArrowLeftDouble.jsx @@ -0,0 +1,19 @@ +import { h } from 'preact'; +import { memo } from 'preact/compat'; + +export function ArrowLeftDouble({ className = '' }) { + return ( + + + + ); +} + +export default memo(ArrowLeftDouble); diff --git a/web/src/icons/ArrowRightDouble.jsx b/web/src/icons/ArrowRightDouble.jsx index 7487a4d5c..fc1960836 100644 --- a/web/src/icons/ArrowRightDouble.jsx +++ b/web/src/icons/ArrowRightDouble.jsx @@ -3,8 +3,15 @@ import { memo } from 'preact/compat'; export function ArrowRightDouble({ className = '' }) { return ( - - + + ); } diff --git a/web/src/icons/ArrowUpDouble.jsx b/web/src/icons/ArrowUpDouble.jsx new file mode 100644 index 000000000..7468a2b91 --- /dev/null +++ b/web/src/icons/ArrowUpDouble.jsx @@ -0,0 +1,19 @@ +import { h } from 'preact'; +import { memo } from 'preact/compat'; + +export function ArrowUpDouble({ className = '' }) { + return ( + + + + ); +} + +export default memo(ArrowUpDouble); diff --git a/web/src/routes/Birdseye.jsx b/web/src/routes/Birdseye.jsx index 91c97fcb9..c23713bb9 100644 --- a/web/src/routes/Birdseye.jsx +++ b/web/src/routes/Birdseye.jsx @@ -6,6 +6,8 @@ import Heading from '../components/Heading'; import WebRtcPlayer from '../components/WebRtcPlayer'; import MsePlayer from '../components/MsePlayer'; import useSWR from 'swr'; +import { useMemo } from 'preact/hooks'; +import CameraControlPanel from '../components/CameraControlPanel'; export default function Birdseye() { const { data: config } = useSWR('config'); @@ -16,6 +18,16 @@ export default function Birdseye() { ); const sourceValues = ['mse', 'webrtc', 'jsmpeg']; + const ptzCameras = useMemo(() => { + if (!config) { + return []; + } + + return Object.entries(config.cameras) + .filter(([_, conf]) => conf.onvif?.host) + .map(([_, camera]) => camera.name); + }, [config]); + if (!config || !sourceIsLoaded) { return ; } @@ -25,7 +37,7 @@ export default function Birdseye() { if ('MediaSource' in window) { player = ( -
+
@@ -42,7 +54,7 @@ export default function Birdseye() { } else if (viewSource == 'webrtc' && config.birdseye.restream) { player = ( -
+
@@ -50,7 +62,7 @@ export default function Birdseye() { } else { player = ( -
+
@@ -79,7 +91,21 @@ export default function Birdseye() { )}
- {player} +
+ {player} + + {ptzCameras && ( +
+ Control Panel + {ptzCameras.map((camera) => ( +
+ {camera.replaceAll('_', ' ')} + +
+ ))} +
+ )} +
); } diff --git a/web/src/routes/Camera.jsx b/web/src/routes/Camera.jsx index 7a50d530a..4a415e32d 100644 --- a/web/src/routes/Camera.jsx +++ b/web/src/routes/Camera.jsx @@ -15,6 +15,7 @@ import { useApiHost } from '../api'; import useSWR from 'swr'; import WebRtcPlayer from '../components/WebRtcPlayer'; import MsePlayer from '../components/MsePlayer'; +import CameraControlPanel from '../components/CameraControlPanel'; const emptyObject = Object.freeze({}); @@ -188,6 +189,13 @@ export default function Camera({ camera }) { {player} + {cameraConfig?.onvif?.host && ( +
+ Control Panel + +
+ )} +
Tracked objects
From 6dc82b6cef53d2f52a3040c1075e2cf019f738d7 Mon Sep 17 00:00:00 2001 From: Blake Blackshear Date: Wed, 26 Apr 2023 06:20:29 -0500 Subject: [PATCH 2/3] dependency updates (#6246) * python * easy web deps --- requirements-wheels.txt | 6 +- requirements.txt | 2 +- web/package-lock.json | 741 +++++++++++++++++++--------------------- web/package.json | 16 +- 4 files changed, 370 insertions(+), 395 deletions(-) diff --git a/requirements-wheels.txt b/requirements-wheels.txt index f20dd4c6d..e8e92408b 100644 --- a/requirements-wheels.txt +++ b/requirements-wheels.txt @@ -1,19 +1,19 @@ click == 8.1.* Flask == 2.2.* imutils == 0.5.* -matplotlib == 3.6.* +matplotlib == 3.7.* mypy == 0.942 numpy == 1.23.* onvif_zeep == 0.2.12 opencv-python-headless == 4.5.5.* paho-mqtt == 1.6.* peewee == 3.15.* -peewee_migrate == 1.6.* +peewee_migrate == 1.7.* psutil == 5.9.* pydantic == 1.10.* PyYAML == 6.0 pytz == 2023.3 -tzlocal == 4.2 +tzlocal == 4.3 types-PyYAML == 6.0.* requests == 2.28.* types-requests == 2.28.* diff --git a/requirements.txt b/requirements.txt index ac8407b6f..90780e2b4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -scikit-build == 0.17.1 +scikit-build == 0.17.* nvidia-pyindex diff --git a/web/package-lock.json b/web/package-lock.json index 9a5952935..d3a42cb90 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "dependencies": { "@cycjimmy/jsmpeg-player": "^6.0.5", - "axios": "^1.3.5", + "axios": "^1.3.6", "copy-to-clipboard": "3.3.3", "date-fns": "^2.29.3", "idb-keyval": "^6.2.0", @@ -32,23 +32,23 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/preact": "^3.2.3", "@testing-library/user-event": "^14.4.3", - "@typescript-eslint/eslint-plugin": "^5.58.0", - "@typescript-eslint/parser": "^5.58.0", + "@typescript-eslint/eslint-plugin": "^5.59.1", + "@typescript-eslint/parser": "^5.59.1", "@vitest/coverage-c8": "^0.30.1", "@vitest/ui": "^0.30.1", "autoprefixer": "^10.4.14", - "eslint": "^8.38.0", + "eslint": "^8.39.0", "eslint-config-preact": "^1.3.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-vitest-globals": "^1.3.1", "fake-indexeddb": "^4.0.1", "jsdom": "^21.1.1", "msw": "^1.2.1", - "postcss": "^8.4.19", - "prettier": "^2.8.7", - "tailwindcss": "^3.3.1", + "postcss": "^8.4.23", + "prettier": "^2.8.8", + "tailwindcss": "^3.3.2", "typescript": "^5.0.4", - "vite": "^4.2.1", + "vite": "^4.3.2", "vitest": "^0.30.1" } }, @@ -58,6 +58,18 @@ "integrity": "sha512-+u76oB43nOHrF4DDWRLWDCtci7f3QJoEBigemIdIeTi1ODqjx6Tad9NCVnPRwewWlKkVab5PlK8DCtPTyX7S8g==", "dev": true }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -946,9 +958,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.38.0.tgz", - "integrity": "sha512-IoD2MfUnOV58ghIHCiil01PcohxjbYR/qCxsoC+xNgUwh1EY8jOOrYmu3d3a71+tJJ23uscEV4X2HJWMsPJu4g==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz", + "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1775,15 +1787,15 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.58.0.tgz", - "integrity": "sha512-vxHvLhH0qgBd3/tW6/VccptSfc8FxPQIkmNTVLWcCOVqSBvqpnKkBTYrhcGlXfSnd78azwe+PsjYFj0X34/njA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.1.tgz", + "integrity": "sha512-AVi0uazY5quFB9hlp2Xv+ogpfpk77xzsgsIEWyVS7uK/c7MZ5tw7ZPbapa0SbfkqE0fsAMkz5UwtgMLVk2BQAg==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.58.0", - "@typescript-eslint/type-utils": "5.58.0", - "@typescript-eslint/utils": "5.58.0", + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/type-utils": "5.59.1", + "@typescript-eslint/utils": "5.59.1", "debug": "^4.3.4", "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", @@ -1809,13 +1821,13 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.58.0.tgz", - "integrity": "sha512-b+w8ypN5CFvrXWQb9Ow9T4/6LC2MikNf1viLkYTiTbkQl46CnR69w7lajz1icW0TBsYmlpg+mRzFJ4LEJ8X9NA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz", + "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/visitor-keys": "5.58.0" + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1826,9 +1838,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.58.0.tgz", - "integrity": "sha512-JYV4eITHPzVQMnHZcYJXl2ZloC7thuUHrcUmxtzvItyKPvQ50kb9QXBkgNAt90OYMqwaodQh2kHutWZl1fc+1g==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz", + "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1839,13 +1851,13 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.58.0.tgz", - "integrity": "sha512-cRACvGTodA+UxnYM2uwA2KCwRL7VAzo45syNysqlMyNyjw0Z35Icc9ihPJZjIYuA5bXJYiJ2YGUB59BqlOZT1Q==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz", + "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/visitor-keys": "5.58.0", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1866,17 +1878,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.58.0.tgz", - "integrity": "sha512-gAmLOTFXMXOC+zP1fsqm3VceKSBQJNzV385Ok3+yzlavNHZoedajjS4UyS21gabJYcobuigQPs/z71A9MdJFqQ==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.1.tgz", + "integrity": "sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.58.0", - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/typescript-estree": "5.58.0", + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/typescript-estree": "5.59.1", "eslint-scope": "^5.1.1", "semver": "^7.3.7" }, @@ -1892,12 +1904,12 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.58.0.tgz", - "integrity": "sha512-/fBraTlPj0jwdyTwLyrRTxv/3lnU2H96pNTVM6z3esTWLtA5MZ9ghSMJ7Rb+TtUAdtEw9EyJzJ0EydIMKxQ9gA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz", + "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.58.0", + "@typescript-eslint/types": "5.59.1", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -1931,9 +1943,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz", - "integrity": "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -1965,14 +1977,14 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.58.0.tgz", - "integrity": "sha512-ixaM3gRtlfrKzP8N6lRhBbjTow1t6ztfBvQNGuRM8qH1bjFFXIJ35XY+FC0RRBKn3C6cT+7VW1y8tNm7DwPHDQ==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.1.tgz", + "integrity": "sha512-nzjFAN8WEu6yPRDizIFyzAfgK7nybPodMNFGNH0M9tei2gYnYszRDqVA0xlnRjkl7Hkx2vYrEdb6fP2a21cG1g==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.58.0", - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/typescript-estree": "5.58.0", + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/typescript-estree": "5.59.1", "debug": "^4.3.4" }, "engines": { @@ -1992,13 +2004,13 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.58.0.tgz", - "integrity": "sha512-b+w8ypN5CFvrXWQb9Ow9T4/6LC2MikNf1viLkYTiTbkQl46CnR69w7lajz1icW0TBsYmlpg+mRzFJ4LEJ8X9NA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz", + "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/visitor-keys": "5.58.0" + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2009,9 +2021,9 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.58.0.tgz", - "integrity": "sha512-JYV4eITHPzVQMnHZcYJXl2ZloC7thuUHrcUmxtzvItyKPvQ50kb9QXBkgNAt90OYMqwaodQh2kHutWZl1fc+1g==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz", + "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2022,13 +2034,13 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.58.0.tgz", - "integrity": "sha512-cRACvGTodA+UxnYM2uwA2KCwRL7VAzo45syNysqlMyNyjw0Z35Icc9ihPJZjIYuA5bXJYiJ2YGUB59BqlOZT1Q==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz", + "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/visitor-keys": "5.58.0", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2049,12 +2061,12 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.58.0.tgz", - "integrity": "sha512-/fBraTlPj0jwdyTwLyrRTxv/3lnU2H96pNTVM6z3esTWLtA5MZ9ghSMJ7Rb+TtUAdtEw9EyJzJ0EydIMKxQ9gA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz", + "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.58.0", + "@typescript-eslint/types": "5.59.1", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -2066,9 +2078,9 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/semver": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz", - "integrity": "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -2098,13 +2110,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.58.0.tgz", - "integrity": "sha512-FF5vP/SKAFJ+LmR9PENql7fQVVgGDOS+dq3j+cKl9iW/9VuZC/8CFmzIP0DLKXfWKpRHawJiG70rVH+xZZbp8w==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.1.tgz", + "integrity": "sha512-ZMWQ+Oh82jWqWzvM3xU+9y5U7MEMVv6GLioM3R5NJk6uvP47kZ7YvlgSHJ7ERD6bOY7Q4uxWm25c76HKEwIjZw==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.58.0", - "@typescript-eslint/utils": "5.58.0", + "@typescript-eslint/typescript-estree": "5.59.1", + "@typescript-eslint/utils": "5.59.1", "debug": "^4.3.4", "tsutils": "^3.21.0" }, @@ -2125,13 +2137,13 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.58.0.tgz", - "integrity": "sha512-b+w8ypN5CFvrXWQb9Ow9T4/6LC2MikNf1viLkYTiTbkQl46CnR69w7lajz1icW0TBsYmlpg+mRzFJ4LEJ8X9NA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz", + "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/visitor-keys": "5.58.0" + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2142,9 +2154,9 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.58.0.tgz", - "integrity": "sha512-JYV4eITHPzVQMnHZcYJXl2ZloC7thuUHrcUmxtzvItyKPvQ50kb9QXBkgNAt90OYMqwaodQh2kHutWZl1fc+1g==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz", + "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2155,13 +2167,13 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.58.0.tgz", - "integrity": "sha512-cRACvGTodA+UxnYM2uwA2KCwRL7VAzo45syNysqlMyNyjw0Z35Icc9ihPJZjIYuA5bXJYiJ2YGUB59BqlOZT1Q==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz", + "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/visitor-keys": "5.58.0", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2182,17 +2194,17 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.58.0.tgz", - "integrity": "sha512-gAmLOTFXMXOC+zP1fsqm3VceKSBQJNzV385Ok3+yzlavNHZoedajjS4UyS21gabJYcobuigQPs/z71A9MdJFqQ==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.1.tgz", + "integrity": "sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.58.0", - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/typescript-estree": "5.58.0", + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/typescript-estree": "5.59.1", "eslint-scope": "^5.1.1", "semver": "^7.3.7" }, @@ -2208,12 +2220,12 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.58.0.tgz", - "integrity": "sha512-/fBraTlPj0jwdyTwLyrRTxv/3lnU2H96pNTVM6z3esTWLtA5MZ9ghSMJ7Rb+TtUAdtEw9EyJzJ0EydIMKxQ9gA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz", + "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.58.0", + "@typescript-eslint/types": "5.59.1", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -2247,9 +2259,9 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/semver": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz", - "integrity": "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -2886,9 +2898,9 @@ } }, "node_modules/axios": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.5.tgz", - "integrity": "sha512-glL/PvG/E+xCWwV8S6nCHcrfg1exGx7vxyUIivIA1iL7BIh6bePylCfVHwp6k13ao7SATxB6imau2kqY+I67kw==", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.6.tgz", + "integrity": "sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg==", "dependencies": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -4014,15 +4026,15 @@ } }, "node_modules/eslint": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.38.0.tgz", - "integrity": "sha512-pIdsD2jwlUGf/U38Jv97t8lq6HpaU/G9NKbYmpWpZGw3LdTNhZLbJePqxOXGB5+JEKfOPU/XLxYxFh03nr1KTg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz", + "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", "@eslint/eslintrc": "^2.0.2", - "@eslint/js": "8.38.0", + "@eslint/js": "8.39.0", "@humanwhocodes/config-array": "^0.11.8", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -4032,7 +4044,7 @@ "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", + "eslint-scope": "^7.2.0", "eslint-visitor-keys": "^3.4.0", "espree": "^9.5.1", "esquery": "^1.4.2", @@ -4239,9 +4251,9 @@ "dev": true }, "node_modules/eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", @@ -4249,6 +4261,9 @@ }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-utils": { @@ -6468,9 +6483,9 @@ } }, "node_modules/lilconfig": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", - "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", "dev": true, "engines": { "node": ">=10" @@ -6866,14 +6881,6 @@ "monaco-editor": ">=0.30" } }, - "node_modules/monaco-yaml/node_modules/yaml": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.3.tgz", - "integrity": "sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg==", - "engines": { - "node": ">= 14" - } - }, "node_modules/mpd-parser": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.1.1.tgz", @@ -7073,10 +7080,16 @@ } }, "node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -7615,9 +7628,9 @@ } }, "node_modules/postcss": { - "version": "8.4.21", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", - "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "version": "8.4.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", + "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", "dev": true, "funding": [ { @@ -7627,10 +7640,14 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -7639,9 +7656,9 @@ } }, "node_modules/postcss-import": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", - "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", "dev": true, "dependencies": { "postcss-value-parser": "^4.0.0", @@ -7649,16 +7666,16 @@ "resolve": "^1.1.7" }, "engines": { - "node": ">=10.0.0" + "node": ">=14.0.0" }, "peerDependencies": { "postcss": "^8.0.0" } }, "node_modules/postcss-js": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz", - "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", "dev": true, "dependencies": { "camelcase-css": "^2.0.1" @@ -7671,20 +7688,20 @@ "url": "https://opencollective.com/postcss/" }, "peerDependencies": { - "postcss": "^8.3.3" + "postcss": "^8.4.21" } }, "node_modules/postcss-load-config": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", - "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", + "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", "dev": true, "dependencies": { "lilconfig": "^2.0.5", - "yaml": "^1.10.2" + "yaml": "^2.1.1" }, "engines": { - "node": ">= 10" + "node": ">= 14" }, "funding": { "type": "opencollective", @@ -7704,12 +7721,12 @@ } }, "node_modules/postcss-nested": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz", - "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", "dev": true, "dependencies": { - "postcss-selector-parser": "^6.0.10" + "postcss-selector-parser": "^6.0.11" }, "engines": { "node": ">=12.0" @@ -7773,9 +7790,9 @@ } }, "node_modules/prettier": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", - "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "bin": { "prettier": "bin-prettier.js" }, @@ -7919,18 +7936,6 @@ } ] }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/react": { "name": "@preact/compat", "version": "17.1.2", @@ -8070,12 +8075,12 @@ "dev": true }, "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", "dev": true, "dependencies": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.11.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -8134,9 +8139,9 @@ } }, "node_modules/rollup": { - "version": "3.20.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.20.2.tgz", - "integrity": "sha512-3zwkBQl7Ai7MFYQE0y1MeQ15+9jsi7XxfrqwTb/9EK8D9C9+//EBR4M+CuA1KODRaNbFez/lWxA5vhEGZp4MUg==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.0.tgz", + "integrity": "sha512-ANPhVcyeHvYdQMUyCbczy33nbLzI7RzrBje4uvNiTDJGIMtlKoOStmympwr9OtS1LZxiDmE2wvxHyVhoLtf1KQ==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -8648,53 +8653,43 @@ } }, "node_modules/tailwindcss": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.1.tgz", - "integrity": "sha512-Vkiouc41d4CEq0ujXl6oiGFQ7bA3WEhUZdTgXAhtKxSy49OmKs8rEfQmupsfF0IGW8fv2iQkp1EVUuapCFrZ9g==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.2.tgz", + "integrity": "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w==", "dev": true, "dependencies": { + "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", - "color-name": "^1.1.4", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.2.12", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.17.2", - "lilconfig": "^2.0.6", + "jiti": "^1.18.2", + "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", - "postcss": "^8.0.9", - "postcss-import": "^14.1.0", - "postcss-js": "^4.0.0", - "postcss-load-config": "^3.1.4", - "postcss-nested": "6.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", "postcss-value-parser": "^4.2.0", - "quick-lru": "^5.1.1", - "resolve": "^1.22.1", - "sucrase": "^3.29.0" + "resolve": "^1.22.2", + "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" }, "engines": { - "node": ">=12.13.0" - }, - "peerDependencies": { - "postcss": "^8.0.9" + "node": ">=14.0.0" } }, - "node_modules/tailwindcss/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -9195,15 +9190,14 @@ } }, "node_modules/vite": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.2.1.tgz", - "integrity": "sha512-7MKhqdy0ISo4wnvwtqZkjke6XN4taqQ2TBaTccLIpOKv7Vp2h4Y+NpmWCnGDeSvvn45KxvWgGyb0MkHvY1vgbg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.2.tgz", + "integrity": "sha512-9R53Mf+TBoXCYejcL+qFbZde+eZveQLDYd9XgULILLC1a5ZwPaqgmdVpL8/uvw2BM/1TzetWjglwm+3RO+xTyw==", "dev": true, "dependencies": { "esbuild": "^0.17.5", "postcss": "^8.4.21", - "resolve": "^1.22.1", - "rollup": "^3.18.0" + "rollup": "^3.21.0" }, "bin": { "vite": "bin/vite.js" @@ -9655,12 +9649,11 @@ "dev": true }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", + "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==", "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/yargs": { @@ -9710,6 +9703,12 @@ "integrity": "sha512-+u76oB43nOHrF4DDWRLWDCtci7f3QJoEBigemIdIeTi1ODqjx6Tad9NCVnPRwewWlKkVab5PlK8DCtPTyX7S8g==", "dev": true }, + "@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true + }, "@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -10254,9 +10253,9 @@ } }, "@eslint/js": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.38.0.tgz", - "integrity": "sha512-IoD2MfUnOV58ghIHCiil01PcohxjbYR/qCxsoC+xNgUwh1EY8jOOrYmu3d3a71+tJJ23uscEV4X2HJWMsPJu4g==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz", + "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==", "dev": true }, "@humanwhocodes/config-array": { @@ -10930,15 +10929,15 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.58.0.tgz", - "integrity": "sha512-vxHvLhH0qgBd3/tW6/VccptSfc8FxPQIkmNTVLWcCOVqSBvqpnKkBTYrhcGlXfSnd78azwe+PsjYFj0X34/njA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.1.tgz", + "integrity": "sha512-AVi0uazY5quFB9hlp2Xv+ogpfpk77xzsgsIEWyVS7uK/c7MZ5tw7ZPbapa0SbfkqE0fsAMkz5UwtgMLVk2BQAg==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.58.0", - "@typescript-eslint/type-utils": "5.58.0", - "@typescript-eslint/utils": "5.58.0", + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/type-utils": "5.59.1", + "@typescript-eslint/utils": "5.59.1", "debug": "^4.3.4", "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", @@ -10948,29 +10947,29 @@ }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.58.0.tgz", - "integrity": "sha512-b+w8ypN5CFvrXWQb9Ow9T4/6LC2MikNf1viLkYTiTbkQl46CnR69w7lajz1icW0TBsYmlpg+mRzFJ4LEJ8X9NA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz", + "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/visitor-keys": "5.58.0" + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1" } }, "@typescript-eslint/types": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.58.0.tgz", - "integrity": "sha512-JYV4eITHPzVQMnHZcYJXl2ZloC7thuUHrcUmxtzvItyKPvQ50kb9QXBkgNAt90OYMqwaodQh2kHutWZl1fc+1g==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz", + "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.58.0.tgz", - "integrity": "sha512-cRACvGTodA+UxnYM2uwA2KCwRL7VAzo45syNysqlMyNyjw0Z35Icc9ihPJZjIYuA5bXJYiJ2YGUB59BqlOZT1Q==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz", + "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/visitor-keys": "5.58.0", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -10979,28 +10978,28 @@ } }, "@typescript-eslint/utils": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.58.0.tgz", - "integrity": "sha512-gAmLOTFXMXOC+zP1fsqm3VceKSBQJNzV385Ok3+yzlavNHZoedajjS4UyS21gabJYcobuigQPs/z71A9MdJFqQ==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.1.tgz", + "integrity": "sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.58.0", - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/typescript-estree": "5.58.0", + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/typescript-estree": "5.59.1", "eslint-scope": "^5.1.1", "semver": "^7.3.7" } }, "@typescript-eslint/visitor-keys": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.58.0.tgz", - "integrity": "sha512-/fBraTlPj0jwdyTwLyrRTxv/3lnU2H96pNTVM6z3esTWLtA5MZ9ghSMJ7Rb+TtUAdtEw9EyJzJ0EydIMKxQ9gA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz", + "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.58.0", + "@typescript-eslint/types": "5.59.1", "eslint-visitor-keys": "^3.3.0" } }, @@ -11021,9 +11020,9 @@ "dev": true }, "semver": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz", - "integrity": "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -11041,41 +11040,41 @@ } }, "@typescript-eslint/parser": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.58.0.tgz", - "integrity": "sha512-ixaM3gRtlfrKzP8N6lRhBbjTow1t6ztfBvQNGuRM8qH1bjFFXIJ35XY+FC0RRBKn3C6cT+7VW1y8tNm7DwPHDQ==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.1.tgz", + "integrity": "sha512-nzjFAN8WEu6yPRDizIFyzAfgK7nybPodMNFGNH0M9tei2gYnYszRDqVA0xlnRjkl7Hkx2vYrEdb6fP2a21cG1g==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.58.0", - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/typescript-estree": "5.58.0", + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/typescript-estree": "5.59.1", "debug": "^4.3.4" }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.58.0.tgz", - "integrity": "sha512-b+w8ypN5CFvrXWQb9Ow9T4/6LC2MikNf1viLkYTiTbkQl46CnR69w7lajz1icW0TBsYmlpg+mRzFJ4LEJ8X9NA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz", + "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/visitor-keys": "5.58.0" + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1" } }, "@typescript-eslint/types": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.58.0.tgz", - "integrity": "sha512-JYV4eITHPzVQMnHZcYJXl2ZloC7thuUHrcUmxtzvItyKPvQ50kb9QXBkgNAt90OYMqwaodQh2kHutWZl1fc+1g==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz", + "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.58.0.tgz", - "integrity": "sha512-cRACvGTodA+UxnYM2uwA2KCwRL7VAzo45syNysqlMyNyjw0Z35Icc9ihPJZjIYuA5bXJYiJ2YGUB59BqlOZT1Q==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz", + "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/visitor-keys": "5.58.0", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -11084,19 +11083,19 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.58.0.tgz", - "integrity": "sha512-/fBraTlPj0jwdyTwLyrRTxv/3lnU2H96pNTVM6z3esTWLtA5MZ9ghSMJ7Rb+TtUAdtEw9EyJzJ0EydIMKxQ9gA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz", + "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.58.0", + "@typescript-eslint/types": "5.59.1", "eslint-visitor-keys": "^3.3.0" } }, "semver": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz", - "integrity": "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -11115,41 +11114,41 @@ } }, "@typescript-eslint/type-utils": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.58.0.tgz", - "integrity": "sha512-FF5vP/SKAFJ+LmR9PENql7fQVVgGDOS+dq3j+cKl9iW/9VuZC/8CFmzIP0DLKXfWKpRHawJiG70rVH+xZZbp8w==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.1.tgz", + "integrity": "sha512-ZMWQ+Oh82jWqWzvM3xU+9y5U7MEMVv6GLioM3R5NJk6uvP47kZ7YvlgSHJ7ERD6bOY7Q4uxWm25c76HKEwIjZw==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "5.58.0", - "@typescript-eslint/utils": "5.58.0", + "@typescript-eslint/typescript-estree": "5.59.1", + "@typescript-eslint/utils": "5.59.1", "debug": "^4.3.4", "tsutils": "^3.21.0" }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.58.0.tgz", - "integrity": "sha512-b+w8ypN5CFvrXWQb9Ow9T4/6LC2MikNf1viLkYTiTbkQl46CnR69w7lajz1icW0TBsYmlpg+mRzFJ4LEJ8X9NA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz", + "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/visitor-keys": "5.58.0" + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1" } }, "@typescript-eslint/types": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.58.0.tgz", - "integrity": "sha512-JYV4eITHPzVQMnHZcYJXl2ZloC7thuUHrcUmxtzvItyKPvQ50kb9QXBkgNAt90OYMqwaodQh2kHutWZl1fc+1g==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz", + "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.58.0.tgz", - "integrity": "sha512-cRACvGTodA+UxnYM2uwA2KCwRL7VAzo45syNysqlMyNyjw0Z35Icc9ihPJZjIYuA5bXJYiJ2YGUB59BqlOZT1Q==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz", + "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/visitor-keys": "5.58.0", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -11158,28 +11157,28 @@ } }, "@typescript-eslint/utils": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.58.0.tgz", - "integrity": "sha512-gAmLOTFXMXOC+zP1fsqm3VceKSBQJNzV385Ok3+yzlavNHZoedajjS4UyS21gabJYcobuigQPs/z71A9MdJFqQ==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.1.tgz", + "integrity": "sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.58.0", - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/typescript-estree": "5.58.0", + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/typescript-estree": "5.59.1", "eslint-scope": "^5.1.1", "semver": "^7.3.7" } }, "@typescript-eslint/visitor-keys": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.58.0.tgz", - "integrity": "sha512-/fBraTlPj0jwdyTwLyrRTxv/3lnU2H96pNTVM6z3esTWLtA5MZ9ghSMJ7Rb+TtUAdtEw9EyJzJ0EydIMKxQ9gA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz", + "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.58.0", + "@typescript-eslint/types": "5.59.1", "eslint-visitor-keys": "^3.3.0" } }, @@ -11200,9 +11199,9 @@ "dev": true }, "semver": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz", - "integrity": "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -11675,9 +11674,9 @@ "dev": true }, "axios": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.5.tgz", - "integrity": "sha512-glL/PvG/E+xCWwV8S6nCHcrfg1exGx7vxyUIivIA1iL7BIh6bePylCfVHwp6k13ao7SATxB6imau2kqY+I67kw==", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.6.tgz", + "integrity": "sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg==", "requires": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -12531,15 +12530,15 @@ } }, "eslint": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.38.0.tgz", - "integrity": "sha512-pIdsD2jwlUGf/U38Jv97t8lq6HpaU/G9NKbYmpWpZGw3LdTNhZLbJePqxOXGB5+JEKfOPU/XLxYxFh03nr1KTg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz", + "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", "@eslint/eslintrc": "^2.0.2", - "@eslint/js": "8.38.0", + "@eslint/js": "8.39.0", "@humanwhocodes/config-array": "^0.11.8", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -12549,7 +12548,7 @@ "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", + "eslint-scope": "^7.2.0", "eslint-visitor-keys": "^3.4.0", "espree": "^9.5.1", "esquery": "^1.4.2", @@ -12768,9 +12767,9 @@ "dev": true }, "eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", "dev": true, "requires": { "esrecurse": "^4.3.0", @@ -14333,9 +14332,9 @@ } }, "lilconfig": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", - "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", "dev": true }, "lines-and-columns": { @@ -14629,13 +14628,6 @@ "vscode-languageserver-types": "^3.0.0", "vscode-uri": "^3.0.0", "yaml": "^2.0.0" - }, - "dependencies": { - "yaml": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.3.tgz", - "integrity": "sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg==" - } } }, "mpd-parser": { @@ -14784,9 +14776,9 @@ } }, "nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", "dev": true }, "natural-compare": { @@ -15176,20 +15168,20 @@ } }, "postcss": { - "version": "8.4.21", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", - "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "version": "8.4.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", + "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", "dev": true, "requires": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "postcss-import": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", - "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", "dev": true, "requires": { "postcss-value-parser": "^4.0.0", @@ -15198,31 +15190,31 @@ } }, "postcss-js": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz", - "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", "dev": true, "requires": { "camelcase-css": "^2.0.1" } }, "postcss-load-config": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", - "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", + "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", "dev": true, "requires": { "lilconfig": "^2.0.5", - "yaml": "^1.10.2" + "yaml": "^2.1.1" } }, "postcss-nested": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz", - "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", "dev": true, "requires": { - "postcss-selector-parser": "^6.0.10" + "postcss-selector-parser": "^6.0.11" } }, "postcss-selector-parser": { @@ -15264,9 +15256,9 @@ "dev": true }, "prettier": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", - "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==" + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==" }, "pretty-format": { "version": "27.5.1", @@ -15368,12 +15360,6 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, - "quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true - }, "react": { "version": "npm:@preact/compat@17.1.2", "resolved": "https://registry.npmjs.org/@preact/compat/-/compat-17.1.2.tgz", @@ -15494,12 +15480,12 @@ "dev": true }, "resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", "dev": true, "requires": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.11.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" } @@ -15536,9 +15522,9 @@ } }, "rollup": { - "version": "3.20.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.20.2.tgz", - "integrity": "sha512-3zwkBQl7Ai7MFYQE0y1MeQ15+9jsi7XxfrqwTb/9EK8D9C9+//EBR4M+CuA1KODRaNbFez/lWxA5vhEGZp4MUg==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.0.tgz", + "integrity": "sha512-ANPhVcyeHvYdQMUyCbczy33nbLzI7RzrBje4uvNiTDJGIMtlKoOStmympwr9OtS1LZxiDmE2wvxHyVhoLtf1KQ==", "dev": true, "requires": { "fsevents": "~2.3.2" @@ -15934,43 +15920,34 @@ } }, "tailwindcss": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.1.tgz", - "integrity": "sha512-Vkiouc41d4CEq0ujXl6oiGFQ7bA3WEhUZdTgXAhtKxSy49OmKs8rEfQmupsfF0IGW8fv2iQkp1EVUuapCFrZ9g==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.2.tgz", + "integrity": "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w==", "dev": true, "requires": { + "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", - "color-name": "^1.1.4", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.2.12", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.17.2", - "lilconfig": "^2.0.6", + "jiti": "^1.18.2", + "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", - "postcss": "^8.0.9", - "postcss-import": "^14.1.0", - "postcss-js": "^4.0.0", - "postcss-load-config": "^3.1.4", - "postcss-nested": "6.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", "postcss-value-parser": "^4.2.0", - "quick-lru": "^5.1.1", - "resolve": "^1.22.1", - "sucrase": "^3.29.0" - }, - "dependencies": { - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - } + "resolve": "^1.22.2", + "sucrase": "^3.32.0" } }, "test-exclude": { @@ -16371,16 +16348,15 @@ } }, "vite": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.2.1.tgz", - "integrity": "sha512-7MKhqdy0ISo4wnvwtqZkjke6XN4taqQ2TBaTccLIpOKv7Vp2h4Y+NpmWCnGDeSvvn45KxvWgGyb0MkHvY1vgbg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.2.tgz", + "integrity": "sha512-9R53Mf+TBoXCYejcL+qFbZde+eZveQLDYd9XgULILLC1a5ZwPaqgmdVpL8/uvw2BM/1TzetWjglwm+3RO+xTyw==", "dev": true, "requires": { "esbuild": "^0.17.5", "fsevents": "~2.3.2", "postcss": "^8.4.21", - "resolve": "^1.22.1", - "rollup": "^3.18.0" + "rollup": "^3.21.0" } }, "vite-node": { @@ -16664,10 +16640,9 @@ "dev": true }, "yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", + "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==" }, "yargs": { "version": "17.6.2", diff --git a/web/package.json b/web/package.json index 91292a6aa..cd32ce888 100644 --- a/web/package.json +++ b/web/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@cycjimmy/jsmpeg-player": "^6.0.5", - "axios": "^1.3.5", + "axios": "^1.3.6", "copy-to-clipboard": "3.3.3", "date-fns": "^2.29.3", "idb-keyval": "^6.2.0", @@ -36,23 +36,23 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/preact": "^3.2.3", "@testing-library/user-event": "^14.4.3", - "@typescript-eslint/eslint-plugin": "^5.58.0", - "@typescript-eslint/parser": "^5.58.0", + "@typescript-eslint/eslint-plugin": "^5.59.1", + "@typescript-eslint/parser": "^5.59.1", "@vitest/coverage-c8": "^0.30.1", "@vitest/ui": "^0.30.1", "autoprefixer": "^10.4.14", - "eslint": "^8.38.0", + "eslint": "^8.39.0", "eslint-config-preact": "^1.3.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-vitest-globals": "^1.3.1", "fake-indexeddb": "^4.0.1", "jsdom": "^21.1.1", "msw": "^1.2.1", - "postcss": "^8.4.19", - "prettier": "^2.8.7", - "tailwindcss": "^3.3.1", + "postcss": "^8.4.23", + "prettier": "^2.8.8", + "tailwindcss": "^3.3.2", "typescript": "^5.0.4", - "vite": "^4.2.1", + "vite": "^4.3.2", "vitest": "^0.30.1" } } From e451f44cedda3a4f16be662d77674868d87268bc Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 26 Apr 2023 07:25:26 -0600 Subject: [PATCH 3/3] Move recording management to separate process (#6248) * Move recordings management to own process and ensure db multiprocess access * remove reference to old threads * Cleanup directory remover * Mypy fixes * Fix mypy * Add support back for setting record via MQTT and WS * Formatting * Fix rebase issue --- frigate/app.py | 46 +-- frigate/comms/dispatcher.py | 10 +- frigate/const.py | 6 +- frigate/record/cleanup.py | 248 ++++++++++++++++ frigate/{record.py => record/maintainer.py} | 310 +++----------------- frigate/record/record.py | 53 ++++ frigate/record/util.py | 19 ++ frigate/types.py | 4 + 8 files changed, 402 insertions(+), 294 deletions(-) create mode 100644 frigate/record/cleanup.py rename frigate/{record.py => record/maintainer.py} (56%) create mode 100644 frigate/record/record.py create mode 100644 frigate/record/util.py diff --git a/frigate/app.py b/frigate/app.py index 54d2825c8..095a51e36 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -28,14 +28,14 @@ from frigate.object_processing import TrackedObjectProcessor from frigate.output import output_frames from frigate.plus import PlusApi from frigate.ptz import OnvifController -from frigate.record import RecordingCleanup, RecordingMaintainer +from frigate.record.record import manage_recordings from frigate.stats import StatsEmitter, stats_init from frigate.storage import StorageMaintainer from frigate.timeline import TimelineProcessor from frigate.version import VERSION from frigate.video import capture_camera, track_camera from frigate.watchdog import FrigateWatchdog -from frigate.types import CameraMetricsTypes +from frigate.types import CameraMetricsTypes, RecordMetricsTypes logger = logging.getLogger(__name__) @@ -50,6 +50,7 @@ class FrigateApp: self.log_queue: Queue = mp.Queue() self.plus_api = PlusApi() self.camera_metrics: dict[str, CameraMetricsTypes] = {} + self.record_metrics: dict[str, RecordMetricsTypes] = {} def set_environment_vars(self) -> None: for key, value in self.config.environment_vars.items(): @@ -109,6 +110,11 @@ class FrigateApp: "capture_process": None, "process": None, } + self.record_metrics[camera_name] = { + "record_enabled": mp.Value( + "i", self.config.cameras[camera_name].record.enabled + ) + } def set_log_levels(self) -> None: logging.getLogger().setLevel(self.config.logger.default.value.upper()) @@ -158,6 +164,20 @@ class FrigateApp: migrate_db.close() + def init_recording_manager(self) -> None: + recording_process = mp.Process( + target=manage_recordings, + name="recording_manager", + args=(self.config, self.recordings_info_queue, self.record_metrics), + ) + recording_process.daemon = True + self.recording_process = recording_process + recording_process.start() + logger.info(f"Recording process started: {recording_process.pid}") + + def bind_database(self) -> None: + """Bind db to the main process.""" + # NOTE: all db accessing processes need to be created before the db can be bound to the main process self.db = SqliteQueueDatabase(self.config.database.path) models = [Event, Recordings, Timeline] self.db.bind(models) @@ -189,7 +209,11 @@ class FrigateApp: comms.append(WebSocketClient(self.config)) self.dispatcher = Dispatcher( - self.config, self.onvif_controller, self.camera_metrics, comms + self.config, + self.onvif_controller, + self.camera_metrics, + self.record_metrics, + comms, ) def start_detectors(self) -> None: @@ -318,16 +342,6 @@ class FrigateApp: self.event_cleanup = EventCleanup(self.config, self.stop_event) self.event_cleanup.start() - def start_recording_maintainer(self) -> None: - self.recording_maintainer = RecordingMaintainer( - self.config, self.recordings_info_queue, self.stop_event - ) - self.recording_maintainer.start() - - def start_recording_cleanup(self) -> None: - self.recording_cleanup = RecordingCleanup(self.config, self.stop_event) - self.recording_cleanup.start() - def start_storage_maintainer(self) -> None: self.storage_maintainer = StorageMaintainer(self.config, self.stop_event) self.storage_maintainer.start() @@ -390,6 +404,8 @@ class FrigateApp: self.init_queues() self.init_database() self.init_onvif() + self.init_recording_manager() + self.bind_database() self.init_dispatcher() except Exception as e: print(e) @@ -406,8 +422,6 @@ class FrigateApp: self.start_timeline_processor() self.start_event_processor() self.start_event_cleanup() - self.start_recording_maintainer() - self.start_recording_cleanup() self.start_stats_emitter() self.start_watchdog() self.check_shm() @@ -443,8 +457,6 @@ class FrigateApp: self.detected_frames_processor.join() self.event_processor.join() self.event_cleanup.join() - self.recording_maintainer.join() - self.recording_cleanup.join() self.stats_emitter.join() self.frigate_watchdog.join() self.db.stop() diff --git a/frigate/comms/dispatcher.py b/frigate/comms/dispatcher.py index 7a2c98392..4f3bdd2fd 100644 --- a/frigate/comms/dispatcher.py +++ b/frigate/comms/dispatcher.py @@ -8,7 +8,7 @@ from abc import ABC, abstractmethod from frigate.config import FrigateConfig from frigate.ptz import OnvifController, OnvifCommandEnum -from frigate.types import CameraMetricsTypes +from frigate.types import CameraMetricsTypes, RecordMetricsTypes from frigate.util import restart_frigate @@ -42,11 +42,13 @@ class Dispatcher: config: FrigateConfig, onvif: OnvifController, camera_metrics: dict[str, CameraMetricsTypes], + record_metrics: dict[str, RecordMetricsTypes], communicators: list[Communicator], ) -> None: self.config = config self.onvif = onvif self.camera_metrics = camera_metrics + self.record_metrics = record_metrics self.comms = communicators for comm in self.comms: @@ -192,13 +194,15 @@ class Dispatcher: record_settings = self.config.cameras[camera_name].record if payload == "ON": - if not record_settings.enabled: + if not self.record_metrics[camera_name]["record_enabled"].value: logger.info(f"Turning on recordings for {camera_name}") record_settings.enabled = True + self.record_metrics[camera_name]["record_enabled"].value = True elif payload == "OFF": - if record_settings.enabled: + if self.record_metrics[camera_name]["record_enabled"].value: logger.info(f"Turning off recordings for {camera_name}") record_settings.enabled = False + self.record_metrics[camera_name]["record_enabled"].value = False self.publish(f"{camera_name}/recordings/state", payload, retain=True) diff --git a/frigate/const.py b/frigate/const.py index f0d76d940..8e1e42bb9 100644 --- a/frigate/const.py +++ b/frigate/const.py @@ -8,7 +8,6 @@ CACHE_DIR = "/tmp/cache" YAML_EXT = (".yaml", ".yml") PLUS_ENV_VAR = "PLUS_API_KEY" PLUS_API_HOST = "https://api.frigate.video" -MAX_SEGMENT_DURATION = 600 BTBN_PATH = "/usr/lib/btbn-ffmpeg" # Regex Consts @@ -23,3 +22,8 @@ DRIVER_ENV_VAR = "LIBVA_DRIVER_NAME" DRIVER_AMD = "radeonsi" DRIVER_INTEL_i965 = "i965" DRIVER_INTEL_iHD = "iHD" + +# Record Values + +MAX_SEGMENT_DURATION = 600 +SECONDS_IN_DAY = 60 * 60 * 24 diff --git a/frigate/record/cleanup.py b/frigate/record/cleanup.py new file mode 100644 index 000000000..74c7eadf2 --- /dev/null +++ b/frigate/record/cleanup.py @@ -0,0 +1,248 @@ +"""Cleanup recordings that are expired based on retention config.""" + +import datetime +import itertools +import logging +import subprocess as sp +import threading +from pathlib import Path + +from peewee import DoesNotExist +from multiprocessing.synchronize import Event as MpEvent + +from frigate.config import RetainModeEnum, FrigateConfig +from frigate.const import RECORD_DIR, SECONDS_IN_DAY +from frigate.models import Event, Recordings +from frigate.record.util import remove_empty_directories + +logger = logging.getLogger(__name__) + + +class RecordingCleanup(threading.Thread): + """Cleanup existing recordings based on retention config.""" + + def __init__(self, config: FrigateConfig, stop_event: MpEvent) -> None: + threading.Thread.__init__(self) + self.name = "recording_cleanup" + self.config = config + self.stop_event = stop_event + + def clean_tmp_clips(self) -> None: + # delete any clips more than 5 minutes old + for p in Path("/tmp/cache").rglob("clip_*.mp4"): + logger.debug(f"Checking tmp clip {p}.") + if p.stat().st_mtime < (datetime.datetime.now().timestamp() - 60 * 1): + logger.debug("Deleting tmp clip.") + + # empty contents of file before unlinking https://github.com/blakeblackshear/frigate/issues/4769 + with open(p, "w"): + pass + p.unlink(missing_ok=True) + + def expire_recordings(self) -> None: + logger.debug("Start expire recordings (new).") + + logger.debug("Start deleted cameras.") + # Handle deleted cameras + expire_days = self.config.record.retain.days + expire_before = ( + datetime.datetime.now() - datetime.timedelta(days=expire_days) + ).timestamp() + no_camera_recordings: Recordings = Recordings.select().where( + Recordings.camera.not_in(list(self.config.cameras.keys())), + Recordings.end_time < expire_before, + ) + + deleted_recordings = set() + for recording in no_camera_recordings: + Path(recording.path).unlink(missing_ok=True) + deleted_recordings.add(recording.id) + + logger.debug(f"Expiring {len(deleted_recordings)} recordings") + Recordings.delete().where(Recordings.id << deleted_recordings).execute() + logger.debug("End deleted cameras.") + + logger.debug("Start all cameras.") + for camera, config in self.config.cameras.items(): + logger.debug(f"Start camera: {camera}.") + # Get the timestamp for cutoff of retained days + expire_days = config.record.retain.days + expire_date = ( + datetime.datetime.now() - datetime.timedelta(days=expire_days) + ).timestamp() + + # Get recordings to check for expiration + recordings: Recordings = ( + Recordings.select() + .where( + Recordings.camera == camera, + Recordings.end_time < expire_date, + ) + .order_by(Recordings.start_time) + ) + + # Get all the events to check against + events: Event = ( + Event.select() + .where( + Event.camera == camera, + # need to ensure segments for all events starting + # before the expire date are included + Event.start_time < expire_date, + Event.has_clip, + ) + .order_by(Event.start_time) + .objects() + ) + + # loop over recordings and see if they overlap with any non-expired events + # TODO: expire segments based on segment stats according to config + event_start = 0 + deleted_recordings = set() + for recording in recordings.objects().iterator(): + keep = False + # Now look for a reason to keep this recording segment + for idx in range(event_start, len(events)): + event = events[idx] + + # if the event starts in the future, stop checking events + # and let this recording segment expire + if event.start_time > recording.end_time: + keep = False + break + + # if the event is in progress or ends after the recording starts, keep it + # and stop looking at events + if event.end_time is None or event.end_time >= recording.start_time: + keep = True + break + + # if the event ends before this recording segment starts, skip + # this event and check the next event for an overlap. + # since the events and recordings are sorted, we can skip events + # that end before the previous recording segment started on future segments + if event.end_time < recording.start_time: + event_start = idx + + # Delete recordings outside of the retention window or based on the retention mode + if ( + not keep + or ( + config.record.events.retain.mode == RetainModeEnum.motion + and recording.motion == 0 + ) + or ( + config.record.events.retain.mode + == RetainModeEnum.active_objects + and recording.objects == 0 + ) + ): + Path(recording.path).unlink(missing_ok=True) + deleted_recordings.add(recording.id) + + logger.debug(f"Expiring {len(deleted_recordings)} recordings") + # delete up to 100,000 at a time + max_deletes = 100000 + deleted_recordings_list = list(deleted_recordings) + for i in range(0, len(deleted_recordings_list), max_deletes): + Recordings.delete().where( + Recordings.id << deleted_recordings_list[i : i + max_deletes] + ).execute() + + logger.debug(f"End camera: {camera}.") + + logger.debug("End all cameras.") + logger.debug("End expire recordings (new).") + + def expire_files(self) -> None: + logger.debug("Start expire files (legacy).") + + default_expire = ( + datetime.datetime.now().timestamp() + - SECONDS_IN_DAY * self.config.record.retain.days + ) + delete_before = {} + + for name, camera in self.config.cameras.items(): + delete_before[name] = ( + datetime.datetime.now().timestamp() + - SECONDS_IN_DAY * camera.record.retain.days + ) + + # find all the recordings older than the oldest recording in the db + try: + oldest_recording = Recordings.select().order_by(Recordings.start_time).get() + + p = Path(oldest_recording.path) + oldest_timestamp = p.stat().st_mtime - 1 + except DoesNotExist: + oldest_timestamp = datetime.datetime.now().timestamp() + except FileNotFoundError: + logger.warning(f"Unable to find file from recordings database: {p}") + Recordings.delete().where(Recordings.id == oldest_recording.id).execute() + return + + logger.debug(f"Oldest recording in the db: {oldest_timestamp}") + process = sp.run( + ["find", RECORD_DIR, "-type", "f", "!", "-newermt", f"@{oldest_timestamp}"], + capture_output=True, + text=True, + ) + files_to_check = process.stdout.splitlines() + + for f in files_to_check: + p = Path(f) + try: + if p.stat().st_mtime < delete_before.get(p.parent.name, default_expire): + p.unlink(missing_ok=True) + except FileNotFoundError: + logger.warning(f"Attempted to expire missing file: {f}") + + logger.debug("End expire files (legacy).") + + def sync_recordings(self) -> None: + logger.debug("Start sync recordings.") + + # get all recordings in the db + recordings: Recordings = Recordings.select() + + # get all recordings files on disk + process = sp.run( + ["find", RECORD_DIR, "-type", "f"], + capture_output=True, + text=True, + ) + files_on_disk = process.stdout.splitlines() + + recordings_to_delete = [] + for recording in recordings.objects().iterator(): + if not recording.path in files_on_disk: + recordings_to_delete.append(recording.id) + + logger.debug( + f"Deleting {len(recordings_to_delete)} recordings with missing files" + ) + # delete up to 100,000 at a time + max_deletes = 100000 + for i in range(0, len(recordings_to_delete), max_deletes): + Recordings.delete().where( + Recordings.id << recordings_to_delete[i : i + max_deletes] + ).execute() + + logger.debug("End sync recordings.") + + def run(self) -> None: + # on startup sync recordings with disk (disabled due to too much CPU usage) + # self.sync_recordings() + + # Expire tmp clips every minute, recordings and clean directories every hour. + for counter in itertools.cycle(range(self.config.record.expire_interval)): + if self.stop_event.wait(60): + logger.info(f"Exiting recording cleanup...") + break + self.clean_tmp_clips() + + if counter == 0: + self.expire_recordings() + self.expire_files() + remove_empty_directories(RECORD_DIR) diff --git a/frigate/record.py b/frigate/record/maintainer.py similarity index 56% rename from frigate/record.py rename to frigate/record/maintainer.py index 4ec3ff9a1..651bd1214 100644 --- a/frigate/record.py +++ b/frigate/record/maintainer.py @@ -1,5 +1,6 @@ +"""Maintain recording segments in cache.""" + import datetime -import itertools import logging import multiprocessing as mp import os @@ -8,51 +9,40 @@ import random import string import subprocess as sp import threading -from collections import defaultdict -from pathlib import Path - import psutil -from peewee import JOIN, DoesNotExist + +from collections import defaultdict +from multiprocessing.synchronize import Event as MpEvent +from pathlib import Path +from typing import Any, Tuple from frigate.config import RetainModeEnum, FrigateConfig from frigate.const import CACHE_DIR, MAX_SEGMENT_DURATION, RECORD_DIR from frigate.models import Event, Recordings +from frigate.types import RecordMetricsTypes from frigate.util import area logger = logging.getLogger(__name__) -SECONDS_IN_DAY = 60 * 60 * 24 - - -def remove_empty_directories(directory): - # list all directories recursively and sort them by path, - # longest first - paths = sorted( - [x[0] for x in os.walk(RECORD_DIR)], - key=lambda p: len(str(p)), - reverse=True, - ) - for path in paths: - # don't delete the parent - if path == RECORD_DIR: - continue - if len(os.listdir(path)) == 0: - os.rmdir(path) - class RecordingMaintainer(threading.Thread): def __init__( - self, config: FrigateConfig, recordings_info_queue: mp.Queue, stop_event + self, + config: FrigateConfig, + recordings_info_queue: mp.Queue, + process_info: dict[str, RecordMetricsTypes], + stop_event: MpEvent, ): threading.Thread.__init__(self) - self.name = "recording_maint" + self.name = "recording_maintainer" self.config = config self.recordings_info_queue = recordings_info_queue + self.process_info = process_info self.stop_event = stop_event - self.recordings_info = defaultdict(list) - self.end_time_cache = {} + self.recordings_info: dict[str, Any] = defaultdict(list) + self.end_time_cache: dict[str, Tuple[datetime.datetime, float]] = {} - def move_files(self): + def move_files(self) -> None: cache_files = sorted( [ d @@ -77,14 +67,14 @@ class RecordingMaintainer(threading.Thread): continue # group recordings by camera - grouped_recordings = defaultdict(list) - for f in cache_files: + grouped_recordings: defaultdict[str, list[dict[str, Any]]] = defaultdict(list) + for cache in cache_files: # Skip files currently in use - if f in files_in_use: + if cache in files_in_use: continue - cache_path = os.path.join(CACHE_DIR, f) - basename = os.path.splitext(f)[0] + cache_path = os.path.join(CACHE_DIR, cache) + basename = os.path.splitext(cache)[0] camera, date = basename.rsplit("-", maxsplit=1) start_time = datetime.datetime.strptime(date, "%Y%m%d%H%M%S") @@ -104,8 +94,8 @@ class RecordingMaintainer(threading.Thread): f"Unable to keep up with recording segments in cache for {camera}. Keeping the {keep_count} most recent segments out of {segment_count} and discarding the rest..." ) to_remove = grouped_recordings[camera][:-keep_count] - for f in to_remove: - cache_path = f["cache_path"] + for rec in to_remove: + cache_path = rec["cache_path"] Path(cache_path).unlink(missing_ok=True) self.end_time_cache.pop(cache_path, None) grouped_recordings[camera] = grouped_recordings[camera][-keep_count:] @@ -138,7 +128,7 @@ class RecordingMaintainer(threading.Thread): # Just delete files if recordings are turned off if ( not camera in self.config.cameras - or not self.config.cameras[camera].record.enabled + or not self.process_info[camera]["record_enabled"].value ): Path(cache_path).unlink(missing_ok=True) self.end_time_cache.pop(cache_path, None) @@ -170,7 +160,7 @@ class RecordingMaintainer(threading.Thread): else: if duration == -1: logger.warning( - f"Failed to probe corrupt segment {cache_path}: {p.returncode} - {p.stderr}" + f"Failed to probe corrupt segment {cache_path} : {p.returncode} - {str(p.stderr)}" ) logger.warning( @@ -241,7 +231,9 @@ class RecordingMaintainer(threading.Thread): camera, start_time, end_time, duration, cache_path, record_mode ) - def segment_stats(self, camera, start_time, end_time): + def segment_stats( + self, camera: str, start_time: datetime.datetime, end_time: datetime.datetime + ) -> Tuple[int, int]: active_count = 0 motion_count = 0 for frame in self.recordings_info[camera]: @@ -266,13 +258,13 @@ class RecordingMaintainer(threading.Thread): def store_segment( self, - camera, + camera: str, start_time: datetime.datetime, end_time: datetime.datetime, - duration, - cache_path, + duration: float, + cache_path: str, store_mode: RetainModeEnum, - ): + ) -> None: motion_count, active_count = self.segment_stats(camera, start_time, end_time) # check if the segment shouldn't be stored @@ -363,9 +355,9 @@ class RecordingMaintainer(threading.Thread): # clear end_time cache self.end_time_cache.pop(cache_path, None) - def run(self): + def run(self) -> None: # Check for new files every 5 seconds - wait_time = 5 + wait_time = 5.0 while not self.stop_event.wait(wait_time): run_start = datetime.datetime.now().timestamp() @@ -380,7 +372,7 @@ class RecordingMaintainer(threading.Thread): regions, ) = self.recordings_info_queue.get(False) - if self.config.cameras[camera].record.enabled: + if self.process_info[camera]["record_enabled"].value: self.recordings_info[camera].append( ( frame_time, @@ -403,231 +395,3 @@ class RecordingMaintainer(threading.Thread): wait_time = max(0, 5 - duration) logger.info(f"Exiting recording maintenance...") - - -class RecordingCleanup(threading.Thread): - def __init__(self, config: FrigateConfig, stop_event): - threading.Thread.__init__(self) - self.name = "recording_cleanup" - self.config = config - self.stop_event = stop_event - - def clean_tmp_clips(self): - # delete any clips more than 5 minutes old - for p in Path("/tmp/cache").rglob("clip_*.mp4"): - logger.debug(f"Checking tmp clip {p}.") - if p.stat().st_mtime < (datetime.datetime.now().timestamp() - 60 * 1): - logger.debug("Deleting tmp clip.") - - # empty contents of file before unlinking https://github.com/blakeblackshear/frigate/issues/4769 - with open(p, "w"): - pass - p.unlink(missing_ok=True) - - def expire_recordings(self): - logger.debug("Start expire recordings (new).") - - logger.debug("Start deleted cameras.") - # Handle deleted cameras - expire_days = self.config.record.retain.days - expire_before = ( - datetime.datetime.now() - datetime.timedelta(days=expire_days) - ).timestamp() - no_camera_recordings: Recordings = Recordings.select().where( - Recordings.camera.not_in(list(self.config.cameras.keys())), - Recordings.end_time < expire_before, - ) - - deleted_recordings = set() - for recording in no_camera_recordings: - Path(recording.path).unlink(missing_ok=True) - deleted_recordings.add(recording.id) - - logger.debug(f"Expiring {len(deleted_recordings)} recordings") - Recordings.delete().where(Recordings.id << deleted_recordings).execute() - logger.debug("End deleted cameras.") - - logger.debug("Start all cameras.") - for camera, config in self.config.cameras.items(): - logger.debug(f"Start camera: {camera}.") - # Get the timestamp for cutoff of retained days - expire_days = config.record.retain.days - expire_date = ( - datetime.datetime.now() - datetime.timedelta(days=expire_days) - ).timestamp() - - # Get recordings to check for expiration - recordings: Recordings = ( - Recordings.select() - .where( - Recordings.camera == camera, - Recordings.end_time < expire_date, - ) - .order_by(Recordings.start_time) - ) - - # Get all the events to check against - events: Event = ( - Event.select() - .where( - Event.camera == camera, - # need to ensure segments for all events starting - # before the expire date are included - Event.start_time < expire_date, - Event.has_clip, - ) - .order_by(Event.start_time) - .objects() - ) - - # loop over recordings and see if they overlap with any non-expired events - # TODO: expire segments based on segment stats according to config - event_start = 0 - deleted_recordings = set() - for recording in recordings.objects().iterator(): - keep = False - # Now look for a reason to keep this recording segment - for idx in range(event_start, len(events)): - event = events[idx] - - # if the event starts in the future, stop checking events - # and let this recording segment expire - if event.start_time > recording.end_time: - keep = False - break - - # if the event is in progress or ends after the recording starts, keep it - # and stop looking at events - if event.end_time is None or event.end_time >= recording.start_time: - keep = True - break - - # if the event ends before this recording segment starts, skip - # this event and check the next event for an overlap. - # since the events and recordings are sorted, we can skip events - # that end before the previous recording segment started on future segments - if event.end_time < recording.start_time: - event_start = idx - - # Delete recordings outside of the retention window or based on the retention mode - if ( - not keep - or ( - config.record.events.retain.mode == RetainModeEnum.motion - and recording.motion == 0 - ) - or ( - config.record.events.retain.mode - == RetainModeEnum.active_objects - and recording.objects == 0 - ) - ): - Path(recording.path).unlink(missing_ok=True) - deleted_recordings.add(recording.id) - - logger.debug(f"Expiring {len(deleted_recordings)} recordings") - # delete up to 100,000 at a time - max_deletes = 100000 - deleted_recordings_list = list(deleted_recordings) - for i in range(0, len(deleted_recordings_list), max_deletes): - Recordings.delete().where( - Recordings.id << deleted_recordings_list[i : i + max_deletes] - ).execute() - - logger.debug(f"End camera: {camera}.") - - logger.debug("End all cameras.") - logger.debug("End expire recordings (new).") - - def expire_files(self): - logger.debug("Start expire files (legacy).") - - default_expire = ( - datetime.datetime.now().timestamp() - - SECONDS_IN_DAY * self.config.record.retain.days - ) - delete_before = {} - - for name, camera in self.config.cameras.items(): - delete_before[name] = ( - datetime.datetime.now().timestamp() - - SECONDS_IN_DAY * camera.record.retain.days - ) - - # find all the recordings older than the oldest recording in the db - try: - oldest_recording = Recordings.select().order_by(Recordings.start_time).get() - - p = Path(oldest_recording.path) - oldest_timestamp = p.stat().st_mtime - 1 - except DoesNotExist: - oldest_timestamp = datetime.datetime.now().timestamp() - except FileNotFoundError: - logger.warning(f"Unable to find file from recordings database: {p}") - Recordings.delete().where(Recordings.id == oldest_recording.id).execute() - return - - logger.debug(f"Oldest recording in the db: {oldest_timestamp}") - process = sp.run( - ["find", RECORD_DIR, "-type", "f", "!", "-newermt", f"@{oldest_timestamp}"], - capture_output=True, - text=True, - ) - files_to_check = process.stdout.splitlines() - - for f in files_to_check: - p = Path(f) - try: - if p.stat().st_mtime < delete_before.get(p.parent.name, default_expire): - p.unlink(missing_ok=True) - except FileNotFoundError: - logger.warning(f"Attempted to expire missing file: {f}") - - logger.debug("End expire files (legacy).") - - def sync_recordings(self): - logger.debug("Start sync recordings.") - - # get all recordings in the db - recordings: Recordings = Recordings.select() - - # get all recordings files on disk - process = sp.run( - ["find", RECORD_DIR, "-type", "f"], - capture_output=True, - text=True, - ) - files_on_disk = process.stdout.splitlines() - - recordings_to_delete = [] - for recording in recordings.objects().iterator(): - if not recording.path in files_on_disk: - recordings_to_delete.append(recording.id) - - logger.debug( - f"Deleting {len(recordings_to_delete)} recordings with missing files" - ) - # delete up to 100,000 at a time - max_deletes = 100000 - for i in range(0, len(recordings_to_delete), max_deletes): - Recordings.delete().where( - Recordings.id << recordings_to_delete[i : i + max_deletes] - ).execute() - - logger.debug("End sync recordings.") - - def run(self): - # on startup sync recordings with disk (disabled due to too much CPU usage) - # self.sync_recordings() - - # Expire tmp clips every minute, recordings and clean directories every hour. - for counter in itertools.cycle(range(self.config.record.expire_interval)): - if self.stop_event.wait(60): - logger.info(f"Exiting recording cleanup...") - break - self.clean_tmp_clips() - - if counter == 0: - self.expire_recordings() - self.expire_files() - remove_empty_directories(RECORD_DIR) diff --git a/frigate/record/record.py b/frigate/record/record.py new file mode 100644 index 000000000..59fda095b --- /dev/null +++ b/frigate/record/record.py @@ -0,0 +1,53 @@ +"""Run recording maintainer and cleanup.""" + +import logging +import multiprocessing as mp +import signal +import threading + +from setproctitle import setproctitle +from types import FrameType +from typing import Optional + +from playhouse.sqliteq import SqliteQueueDatabase + +from frigate.config import FrigateConfig +from frigate.models import Event, Recordings, Timeline +from frigate.record.cleanup import RecordingCleanup +from frigate.record.maintainer import RecordingMaintainer +from frigate.types import RecordMetricsTypes +from frigate.util import listen + +logger = logging.getLogger(__name__) + + +def manage_recordings( + config: FrigateConfig, + recordings_info_queue: mp.Queue, + process_info: dict[str, RecordMetricsTypes], +) -> None: + stop_event = mp.Event() + + def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None: + stop_event.set() + + signal.signal(signal.SIGTERM, receiveSignal) + signal.signal(signal.SIGINT, receiveSignal) + + threading.current_thread().name = "process:recording_manager" + setproctitle("frigate.recording_manager") + listen() + + db = SqliteQueueDatabase(config.database.path) + models = [Event, Recordings, Timeline] + db.bind(models) + + maintainer = RecordingMaintainer( + config, recordings_info_queue, process_info, stop_event + ) + maintainer.start() + + cleanup = RecordingCleanup(config, stop_event) + cleanup.start() + + logger.info("recording_manager: exiting subprocess") diff --git a/frigate/record/util.py b/frigate/record/util.py new file mode 100644 index 000000000..d9692c25e --- /dev/null +++ b/frigate/record/util.py @@ -0,0 +1,19 @@ +"""Recordings Utilities.""" + +import os + + +def remove_empty_directories(directory: str) -> None: + # list all directories recursively and sort them by path, + # longest first + paths = sorted( + [x[0] for x in os.walk(directory)], + key=lambda p: len(str(p)), + reverse=True, + ) + for path in paths: + # don't delete the parent + if path == directory: + continue + if len(os.listdir(path)) == 0: + os.rmdir(path) diff --git a/frigate/types.py b/frigate/types.py index 04339e366..9da4027c9 100644 --- a/frigate/types.py +++ b/frigate/types.py @@ -24,6 +24,10 @@ class CameraMetricsTypes(TypedDict): skipped_fps: Synchronized +class RecordMetricsTypes(TypedDict): + record_enabled: Synchronized + + class StatsTrackingTypes(TypedDict): camera_metrics: dict[str, CameraMetricsTypes] detectors: dict[str, ObjectDetectProcess]