mirror of
https://github.com/ep1cman/unifi-protect-backup.git
synced 2025-12-15 15:53:44 +00:00
Add the ability to change the way the clip files are structured
This commit is contained in:
25
README.md
25
README.md
@@ -81,6 +81,12 @@ Options:
|
|||||||
multiple IDs. If being set as an environment
|
multiple IDs. If being set as an environment
|
||||||
variable the IDs should be separated by
|
variable the IDs should be separated by
|
||||||
whitespace.
|
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.
|
-v, --verbose How verbose the logging output should be.
|
||||||
|
|
||||||
None: Only log info messages created by
|
None: Only log info messages created by
|
||||||
@@ -121,6 +127,25 @@ always take priority over environment variables):
|
|||||||
- `RCLONE_ARGS`
|
- `RCLONE_ARGS`
|
||||||
- `IGNORE_CAMERAS`
|
- `IGNORE_CAMERAS`
|
||||||
- `DETECTION_TYPES`
|
- `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
|
## Docker Container
|
||||||
You can run this tool as a container if you prefer with the following command.
|
You can run this tool as a container if you prefer with the following command.
|
||||||
|
|||||||
@@ -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 "
|
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.",
|
"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(
|
@click.option(
|
||||||
'-v',
|
'-v',
|
||||||
'--verbose',
|
'--verbose',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import asyncio
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import pathlib
|
import pathlib
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
from asyncio.exceptions import TimeoutError
|
from asyncio.exceptions import TimeoutError
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
@@ -175,7 +176,8 @@ class UnifiProtectBackup:
|
|||||||
rclone_args (str): Extra args passed directly to `rclone rcat`.
|
rclone_args (str): Extra args passed directly to `rclone rcat`.
|
||||||
ignore_cameras (List[str]): List of camera IDs for which to not backup events
|
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.
|
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
|
_download_queue (asyncio.Queue): Queue of events that need to be backed up
|
||||||
_unsub (Callable): Unsubscribe from the websocket callback
|
_unsub (Callable): Unsubscribe from the websocket callback
|
||||||
_has_ffprobe (bool): If ffprobe was found on the host
|
_has_ffprobe (bool): If ffprobe was found on the host
|
||||||
@@ -192,6 +194,7 @@ class UnifiProtectBackup:
|
|||||||
rclone_args: str,
|
rclone_args: str,
|
||||||
detection_types: List[str],
|
detection_types: List[str],
|
||||||
ignore_cameras: List[str],
|
ignore_cameras: List[str],
|
||||||
|
file_structure_format: str,
|
||||||
verbose: int,
|
verbose: int,
|
||||||
port: int = 443,
|
port: int = 443,
|
||||||
):
|
):
|
||||||
@@ -213,6 +216,7 @@ class UnifiProtectBackup:
|
|||||||
`rclone` (https://rclone.org/docs/#bwlimit-bandwidth-spec)
|
`rclone` (https://rclone.org/docs/#bwlimit-bandwidth-spec)
|
||||||
detection_types (List[str]): List of which detection types to backup.
|
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.
|
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.
|
verbose (int): How verbose to setup logging, see :func:`setup_logging` for details.
|
||||||
"""
|
"""
|
||||||
setup_logging(verbose)
|
setup_logging(verbose)
|
||||||
@@ -233,10 +237,12 @@ class UnifiProtectBackup:
|
|||||||
logger.debug(f" {ignore_cameras=}")
|
logger.debug(f" {ignore_cameras=}")
|
||||||
logger.debug(f" {verbose=}")
|
logger.debug(f" {verbose=}")
|
||||||
logger.debug(f" {detection_types=}")
|
logger.debug(f" {detection_types=}")
|
||||||
|
logger.debug(f" {file_structure_format=}")
|
||||||
|
|
||||||
self.rclone_destination = rclone_destination
|
self.rclone_destination = rclone_destination
|
||||||
self.retention = retention
|
self.retention = retention
|
||||||
self.rclone_args = rclone_args
|
self.rclone_args = rclone_args
|
||||||
|
self.file_structure_format = file_structure_format
|
||||||
|
|
||||||
self.address = address
|
self.address = address
|
||||||
self.port = port
|
self.port = port
|
||||||
@@ -589,12 +595,15 @@ class UnifiProtectBackup:
|
|||||||
async def generate_file_path(self, event: Event) -> pathlib.Path:
|
async def generate_file_path(self, event: Event) -> pathlib.Path:
|
||||||
"""Generates the rclone destination path for the provided event.
|
"""Generates the rclone destination path for the provided event.
|
||||||
|
|
||||||
Generates paths in the following structure:
|
Generates rclone destination path for the given even based upon the format string
|
||||||
::
|
in `self.file_structure_format`.
|
||||||
rclone_destination
|
|
||||||
|- Camera Name
|
Provides the following fields to the format string:
|
||||||
|- {Date}
|
event: The `Event` object as per
|
||||||
|- {start timestamp} {event type} ({detections}).mp4
|
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:
|
Args:
|
||||||
event: The event for which to create an output path
|
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
|
pathlib.Path: The rclone path the event should be backed up to
|
||||||
|
|
||||||
"""
|
"""
|
||||||
path = pathlib.Path(self.rclone_destination)
|
|
||||||
assert isinstance(event.camera_id, str)
|
assert isinstance(event.camera_id, str)
|
||||||
path /= await self._get_camera_name(event.camera_id) # directory per camera
|
assert isinstance(event.start, datetime)
|
||||||
path /= event.start.strftime("%Y-%m-%d") # Directory per day
|
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:
|
file_path = self.file_structure_format.format(**format_context)
|
||||||
detections = " ".join(event.smart_detect_types)
|
file_path = re.sub(r'[^\w\-_\.\(\)/ ]', '', file_path) # Sanitize any invalid chars
|
||||||
file_name += f" ({detections})"
|
|
||||||
file_name += ".mp4"
|
|
||||||
|
|
||||||
path /= file_name
|
return pathlib.Path(f"{self.rclone_destination}/{file_path}")
|
||||||
|
|
||||||
return path
|
|
||||||
|
|
||||||
async def _get_camera_name(self, id: str):
|
async def _get_camera_name(self, id: str):
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user