Compare commits

...

3 Commits

Author SHA1 Message Date
Sebastian Goscik
4bd81e65d8 Bump version: 0.4.0 → 0.5.0 2022-03-06 18:10:29 +00:00
Sebastian Goscik
9930e18fd1 Updated changelog 2022-03-06 18:10:16 +00:00
Sebastian Goscik
08243411ed Added feature to check duration of downloaded clips if ffprobe is present 2022-03-06 18:06:08 +00:00
7 changed files with 60 additions and 8 deletions

View File

@@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 0.4.0 current_version = 0.5.0
commit = True commit = True
tag = True tag = True

View File

@@ -4,7 +4,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.4.0] - 2022-02-24 ## [0.5.0] - 2022-03-06
### Added
- If `ffprobe` is available, the downloaded clips length is checked and logged
### Fixed
- A time delay has been added before downloading clips to try to resolve an issue where
downloaded clips were too short
## [0.4.0] - 2022-03-05
### Added ### Added
- A `--version` command line option to show the tools version - A `--version` command line option to show the tools version
### Fixed ### Fixed

View File

@@ -4,8 +4,8 @@
FROM python:3.9-alpine FROM python:3.9-alpine
WORKDIR /app WORKDIR /app
RUN apk add gcc musl-dev zlib-dev jpeg-dev rclone RUN apk add gcc musl-dev zlib-dev jpeg-dev rclone ffmpeg
COPY dist/unifi-protect-backup-0.4.0.tar.gz sdist.tar.gz COPY dist/unifi-protect-backup-0.5.0.tar.gz sdist.tar.gz
RUN pip install sdist.tar.gz RUN pip install sdist.tar.gz
ENV UFP_USERNAME=unifi_protect_user ENV UFP_USERNAME=unifi_protect_user

View File

@@ -36,6 +36,7 @@ retention period.
1. Install `rclone`. Instructions for your platform can be found here: https://rclone.org/install/#quickstart 1. Install `rclone`. Instructions for your platform can be found here: https://rclone.org/install/#quickstart
2. Configure the `rclone` remote you want to backup to. Instructions can be found here: https://rclone.org/docs/#configure 2. Configure the `rclone` remote you want to backup to. Instructions can be found here: https://rclone.org/docs/#configure
3. `pip install unifi-protect-backup` 3. `pip install unifi-protect-backup`
4. Optional: Install `ffprobe` so that `unifi-protect-backup` can check the length of the clips it downloads
## Usage ## Usage

View File

@@ -1,7 +1,7 @@
[tool] [tool]
[tool.poetry] [tool.poetry]
name = "unifi-protect-backup" name = "unifi-protect-backup"
version = "0.4.0" version = "0.5.0"
homepage = "https://github.com/ep1cman/unifi-protect-backup" homepage = "https://github.com/ep1cman/unifi-protect-backup"
description = "Python tool to backup unifi event clips in realtime." description = "Python tool to backup unifi event clips in realtime."
authors = ["sebastian.goscik <sebastian@goscik.com>"] authors = ["sebastian.goscik <sebastian@goscik.com>"]

View File

@@ -2,6 +2,6 @@
__author__ = """sebastian.goscik""" __author__ = """sebastian.goscik"""
__email__ = 'sebastian@goscik.com' __email__ = 'sebastian@goscik.com'
__version__ = '0.4.0' __version__ = '0.5.0'
from .unifi_protect_backup import UnifiProtectBackup from .unifi_protect_backup import UnifiProtectBackup

View File

@@ -4,6 +4,7 @@ from datetime import datetime, timedelta, timezone
import logging import logging
import pathlib import pathlib
import shutil import shutil
import json
from asyncio.exceptions import TimeoutError from asyncio.exceptions import TimeoutError
from typing import Callable, List, Optional from typing import Callable, List, Optional
@@ -175,6 +176,7 @@ class UnifiProtectBackup:
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.
_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
""" """
def __init__( def __init__(
@@ -249,6 +251,8 @@ class UnifiProtectBackup:
self._download_queue: asyncio.Queue = asyncio.Queue() self._download_queue: asyncio.Queue = asyncio.Queue()
self._unsub: Callable[[], None] self._unsub: Callable[[], None]
self._has_ffprobe = False
async def start(self): async def start(self):
"""Bootstrap the backup process and kick off the main loop. """Bootstrap the backup process and kick off the main loop.
@@ -257,10 +261,16 @@ class UnifiProtectBackup:
""" """
logger.info("Starting...") logger.info("Starting...")
# Ensure rclone is installed and properly configured # Ensure `rclone` is installed and properly configured
logger.info("Checking rclone configuration...") logger.info("Checking rclone configuration...")
await self._check_rclone() await self._check_rclone()
# Check if `ffprobe` is available
ffprobe = shutil.which('ffprobe')
if ffprobe is not None:
logger.debug(f"ffprobe found: {ffprobe}")
self._has_ffprobe = True
# Start the pyunifiprotect connection by calling `update` # Start the pyunifiprotect connection by calling `update`
logger.info("Connecting to Unifi Protect...") logger.info("Connecting to Unifi Protect...")
await self._protect.update() await self._protect.update()
@@ -367,9 +377,9 @@ class UnifiProtectBackup:
""" """
rclone = shutil.which('rclone') rclone = shutil.which('rclone')
logger.debug(f"rclone found: {rclone}")
if not rclone: if not rclone:
raise RuntimeError("`rclone` is not installed on this system") raise RuntimeError("`rclone` is not installed on this system")
logger.debug(f"rclone found: {rclone}")
cmd = "rclone listremotes -vv" cmd = "rclone listremotes -vv"
proc = await asyncio.create_subprocess_shell( proc = await asyncio.create_subprocess_shell(
@@ -465,6 +475,21 @@ class UnifiProtectBackup:
destination = await self.generate_file_path(event) destination = await self.generate_file_path(event)
# Get the actual length of the downloaded video using ffprobe
if self._has_ffprobe:
try:
downloaded_duration = await self._get_video_length(video)
msg = f" Downloaded video length: {downloaded_duration:.3f}s" \
f"({downloaded_duration - duration:+.3f}s)"
if downloaded_duration < duration:
logger.warning(msg)
else:
logger.debug(msg)
except SubprocessException as e:
logger.warn(f" `ffprobe` failed")
logger.exception(e)
# Upload video
logger.debug(" Uploading video via rclone...") logger.debug(" Uploading video via rclone...")
logger.debug(f" To: {destination}") logger.debug(f" To: {destination}")
logger.debug(f" Size: {human_readable_size(len(video))}") logger.debug(f" Size: {human_readable_size(len(video))}")
@@ -514,6 +539,25 @@ class UnifiProtectBackup:
else: else:
raise SubprocessException(stdout.decode(), stderr.decode(), proc.returncode) raise SubprocessException(stdout.decode(), stderr.decode(), proc.returncode)
async def _get_video_length(self, video: bytes) -> float:
cmd = 'ffprobe -v quiet -show_streams -select_streams v:0 -of json -'
proc = await asyncio.create_subprocess_shell(
cmd,
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await proc.communicate(video)
if proc.returncode == 0:
logger.extra_debug(f"stdout:\n{stdout.decode()}") # type: ignore
logger.extra_debug(f"stderr:\n{stderr.decode()}") # type: ignore
json_data = json.loads(stdout.decode())
return float(json_data['streams'][0]['duration'])
else:
raise SubprocessException(stdout.decode(), stderr.decode(), proc.returncode)
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.