Add the ability to change the way the clip files are structured

This commit is contained in:
Sebastian Goscik
2022-03-26 00:04:32 +00:00
parent af8ca90356
commit 381f90f497
3 changed files with 62 additions and 18 deletions

View File

@@ -81,6 +81,12 @@ Options:
multiple IDs. If being set as an environment
variable the IDs should be separated by
whitespace.
--file-structure-format TEXT A Python format string used to generate the
file structure/name on the rclone remote.For
details of the fields available, see the
projects `README.md` file. [default: {camer
a_name}/{event.start:%Y-%m-%d}/{event.end:%Y
-%m-%dT%H-%M-%S} {detection_type}.mp4]
-v, --verbose How verbose the logging output should be.
None: Only log info messages created by
@@ -121,6 +127,25 @@ always take priority over environment variables):
- `RCLONE_ARGS`
- `IGNORE_CAMERAS`
- `DETECTION_TYPES`
- `FILE_STRUCTURE_FORMAT`
## File path formatting
By default, the application will save clips in the following structure on the provided rclone remote:
```
{camera_name}/{event.start:%Y-%m-%d}/{event.end:%Y-%m-%dT%H-%M-%S} {detection_type}.mp4
```
If you wish for the clips to be structured differently you can do this using the `--file-structure-format`
option. It uses standard [python format string syntax](https://docs.python.org/3/library/string.html#formatstrings).
The following fields are provided to the format string:
- *event:* The `Event` object as per https://github.com/briis/pyunifiprotect/blob/master/pyunifiprotect/data/nvr.py
- *duration_seconds:* The duration of the event in seconds
- *detection_type:* A nicely formatted list of the event detection type and the smart detection types (if any)
- *camera_name:* The name of the camera that generated this event
You can optionally format the `event.start`/`event.end` timestamps as per the [`strftime` format](https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior) by appening it after a `:` e.g to get just the date without the time: `{event.start:%Y-%m-%d}`
## Docker Container
You can run this tool as a container if you prefer with the following command.

View File

@@ -73,6 +73,14 @@ def _parse_detection_types(ctx, param, value):
help="IDs of cameras for which events should not be backed up. Use multiple times to ignore "
"multiple IDs. If being set as an environment variable the IDs should be separated by whitespace.",
)
@click.option(
'--file-structure-format',
envvar='FILE_STRUCTURE_FORMAT',
default="{camera_name}/{event.start:%Y-%m-%d}/{event.end:%Y-%m-%dT%H-%M-%S} {detection_type}.mp4",
show_default=True,
help="A Python format string used to generate the file structure/name on the rclone remote."
"For details of the fields available, see the projects `README.md` file.",
)
@click.option(
'-v',
'--verbose',

View File

@@ -3,6 +3,7 @@ import asyncio
import json
import logging
import pathlib
import re
import shutil
from asyncio.exceptions import TimeoutError
from datetime import datetime, timedelta, timezone
@@ -175,7 +176,8 @@ class UnifiProtectBackup:
rclone_args (str): Extra args passed directly to `rclone rcat`.
ignore_cameras (List[str]): List of camera IDs for which to not backup events
verbose (int): How verbose to setup logging, see :func:`setup_logging` for details.
detection_types(List[str]): List of which detection types to backup.
detection_types (List[str]): List of which detection types to backup.
file_structure_format (str): A Python format string for output file path
_download_queue (asyncio.Queue): Queue of events that need to be backed up
_unsub (Callable): Unsubscribe from the websocket callback
_has_ffprobe (bool): If ffprobe was found on the host
@@ -192,6 +194,7 @@ class UnifiProtectBackup:
rclone_args: str,
detection_types: List[str],
ignore_cameras: List[str],
file_structure_format: str,
verbose: int,
port: int = 443,
):
@@ -213,6 +216,7 @@ class UnifiProtectBackup:
`rclone` (https://rclone.org/docs/#bwlimit-bandwidth-spec)
detection_types (List[str]): List of which detection types to backup.
ignore_cameras (List[str]): List of camera IDs for which to not backup events.
file_structure_format (str): A Python format string for output file path.
verbose (int): How verbose to setup logging, see :func:`setup_logging` for details.
"""
setup_logging(verbose)
@@ -233,10 +237,12 @@ class UnifiProtectBackup:
logger.debug(f" {ignore_cameras=}")
logger.debug(f" {verbose=}")
logger.debug(f" {detection_types=}")
logger.debug(f" {file_structure_format=}")
self.rclone_destination = rclone_destination
self.retention = retention
self.rclone_args = rclone_args
self.file_structure_format = file_structure_format
self.address = address
self.port = port
@@ -589,12 +595,15 @@ class UnifiProtectBackup:
async def generate_file_path(self, event: Event) -> pathlib.Path:
"""Generates the rclone destination path for the provided event.
Generates paths in the following structure:
::
rclone_destination
|- Camera Name
|- {Date}
|- {start timestamp} {event type} ({detections}).mp4
Generates rclone destination path for the given even based upon the format string
in `self.file_structure_format`.
Provides the following fields to the format string:
event: The `Event` object as per
https://github.com/briis/pyunifiprotect/blob/master/pyunifiprotect/data/nvr.py
duration_seconds: The duration of the event in seconds
detection_type: A nicely formatted list of the event detection type and the smart detection types (if any)
camera_name: The name of the camera that generated this event
Args:
event: The event for which to create an output path
@@ -603,21 +612,23 @@ class UnifiProtectBackup:
pathlib.Path: The rclone path the event should be backed up to
"""
path = pathlib.Path(self.rclone_destination)
assert isinstance(event.camera_id, str)
path /= await self._get_camera_name(event.camera_id) # directory per camera
path /= event.start.strftime("%Y-%m-%d") # Directory per day
assert isinstance(event.start, datetime)
assert isinstance(event.end, datetime)
file_name = f"{event.start.strftime('%Y-%m-%dT%H-%M-%S')} {event.type}"
format_context = {
"event": event,
"duration_seconds": (event.end - event.start).total_seconds(),
"detection_type": f"{event.type} ({' '.join(event.smart_detect_types)})"
if event.smart_detect_types
else f"{event.type}",
"camera_name": await self._get_camera_name(event.camera_id),
}
if event.smart_detect_types:
detections = " ".join(event.smart_detect_types)
file_name += f" ({detections})"
file_name += ".mp4"
file_path = self.file_structure_format.format(**format_context)
file_path = re.sub(r'[^\w\-_\.\(\)/ ]', '', file_path) # Sanitize any invalid chars
path /= file_name
return path
return pathlib.Path(f"{self.rclone_destination}/{file_path}")
async def _get_camera_name(self, id: str):
try: