Skip to main content

updl

CI PyPI Python License: MIT

Resumable, verifiable archiver for UniFi Protect footage.

Back up a UniFi Protect system to local storage in a way you can re-run at any time: it downloads only what is genuinely missing, remembers the hours the NVR had no footage for, retries the ones that failed, and can prove that what is already on disk is still intact.

$ updl sync /srv/protect
Archive currently holds 367 segment(s) (122 empty, 245 ok)
0 files downloaded (0.0 b), 367 already archived, 0 files skipped, 0 files failed

That run took 0.35 seconds and made no requests to the NVR at all, because everything was already held. The first run of the same command downloaded 58.5 GB.

Not affiliated with, endorsed by, or supported by Ubiquiti Inc. "UniFi" and "Protect" are trademarks of Ubiquiti Inc., used here only to describe what this tool talks to.

Install

pip install updl

Python 3.10+. ffprobe (from FFmpeg) is optional and only needed for --verify=deep and --require-audio.

Quick start

The destination must already exist. Then, on a new machine:

updl -a 192.168.1.1 -u archiver -p '...' -d /srv/protect

If the account uses multi-factor authentication you are prompted for a code once. The session token is cached, so you are not asked again until it expires.

Every run after that needs no arguments at all:

updl

The console address, account and destination are remembered from the last successful run. Your password is never written to disk — it is asked for again only when the cached token expires.

Prefer the environment to flags for the password, since a command-line argument is visible to every other process on the machine:

export PROTECT_PASSWORD='...'
updl

Every option has a matching PROTECT_* environment variable. Precedence is: an explicit flag, then the environment variable, then what was remembered, then the built-in default.

Where state is kept — override the first two with UPDL_CONFIG and PROTECT_SESSION_STORE:

What Windows Linux / macOS
Remembered settings %LOCALAPPDATA%\updl\config.json ~/.config/updl/config.json
Session token %LOCALAPPDATA%\protect-archiver\sessions.json ~/.local/state/protect-archiver/sessions.json
Archive index DEST\.protect-archive\manifest.db DEST/.protect-archive/manifest.db

Commands

updl on its own runs sync. Sub-commands can always be named explicitly:

Command What it does
updl sync [DEST] Incremental mirror. Sweeps each camera's retention window and fetches only what is missing or damaged.
updl verify [DEST] Audits an archive offline — never contacts the NVR, so it is safe to run against a backup copy.
updl download DEST One-off download of an explicit --start/--end range.
updl events DEST Motion and smart-detection event clips only.

How re-running avoids re-downloading

A SQLite manifest at DEST/.protect-archive/manifest.db holds one row per hour per camera, keyed on (camera_id, start_ms), recording the path, size, SHA-256 and a status:

  • ok — downloaded and accounted for.
  • empty — the NVR reported no footage for that hour, so it is never requested again and the gap stays visible as a deliberate record rather than as an error.
  • failed — the download failed; a later run retries exactly that hour.

Because the manifest is authoritative, sync sweeps the whole retention window every run and still costs nothing when there is nothing to do. Gaps left by earlier failures are filled automatically, and footage that has since aged off the NVR stays in the archive.

Verifying what is on disk

--verify controls how hard sync works to prove an existing file is intact before skipping it. updl verify accepts the same levels for an offline audit.

Level Checks Cost
none trusts the manifest touches no files
quick (default) file exists at the recorded size one stat per segment
hash SHA-256 recomputed reads the whole archive
deep + ffprobe decodes it catches a file that is the right size and hash but unplayable
# Audit, then queue anything damaged for the next sync to re-fetch.
updl verify /srv/protect --level hash --repair

Other verify flags: --require-audio (see below), --rehash to fill in hashes for rows adopted from disk, --fix-timestamps, and --clean-partials.

Things this handles that are easy to get wrong

Audio is silently dropped for under-permissioned accounts. If the Protect account lacks readmedia on a camera, the export still returns 200 with intact video — just no audio track. Nothing fails, so an archive can accumulate for weeks before anyone plays a clip and finds it silent. updl verify DEST --require-audio inspects the streams and reports it; add --repair to re-fetch. If it flags everything, grant the account camera media permission and run it again.

"No footage" is reported as an HTTP 404 carrying {"error": 502}. Neither number means what it looks like. Treated as a failure, every hour a camera was offline would be re-requested forever; updl records it as empty instead.

Downloads are atomic. Bytes land in a .part file and are renamed into place only on success, so an interrupted run cannot leave a truncated MP4 that later runs mistake for a complete one. Stale partials are swept at startup.

Transient server errors are retried. The export endpoint routinely returns 500 on a busy NVR. updl backs off exponentially on 5xx/429/408, does not retry other 4xx, and refreshes an expired session on 401 without spending a retry.

Files are dated by recording time, not download time. Protect stamps each exported MP4 at the moment of export, so an archive fetched today would otherwise show today's date for last week's footage.

Accounts and multi-factor authentication

The account must be able to read camera media. A Protect account lacking readmedia on a camera gets video without audio rather than an error — see above.

For consoles backed by Ubiquiti SSO with a second factor, updl handles the two-step login and caches the session token (--no-session-store disables this), so a code is needed occasionally rather than on every run. Supply one non-interactively with --mfa-code / PROTECT_MFA_CODE; otherwise it prompts. With no terminal attached it fails with an explanation rather than hanging, which is what a scheduled run needs.

Where the second factor is delivered by email there is no shared secret, so no run can obtain a new token unattended — but an existing token is long-lived, so a scheduled run works until it expires.

Recovering an archive whose manifest was lost

updl sync DEST --reconcile reads the footage already on disk and rebuilds the manifest rows from the filenames, rather than downloading terabytes again. Adopted rows carry no hash until updl verify DEST --rehash records one.

Credits

A fork of danielfernau/unifi-protect-video-downloader by Daniel Fernau, David Cramer and contributors, which does the hard work of talking to the Protect API. See NOTICE for what this fork changed and why. MIT licensed; the original copyright notice is retained in LICENSE.

Contributing

poetry install
poetry run pytest
poetry run mypy .
poetry run flake8 protect_archiver conftest.py

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

updl-3.0.0.tar.gz (57.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

updl-3.0.0-py3-none-any.whl (77.3 kB view details)

Uploaded Python 3

File details

Details for the file updl-3.0.0.tar.gz.

File metadata

  • Download URL: updl-3.0.0.tar.gz
  • Upload date:
  • Size: 57.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for updl-3.0.0.tar.gz
Algorithm Hash digest
SHA256 c33cf7180ad988371c893f5d57b3f065c740e90898e7dea0b9b509af264623ae
MD5 59b18f74fccffe524de4046d01cde5cc
BLAKE2b-256 049e27a647f77f78a7bffb654a6b234609d6b588830beef0eff07fc74a097e7b

See more details on using hashes here.

Provenance

The following attestation bundles were made for updl-3.0.0.tar.gz:

Publisher: publish.yml on tribixbite/updl

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file updl-3.0.0-py3-none-any.whl.

File metadata

  • Download URL: updl-3.0.0-py3-none-any.whl
  • Upload date:
  • Size: 77.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for updl-3.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c282f8ebdfeacdd06a5dc2e1c931bb797256771f1773b699b7a4ec82429f1b01
MD5 84e876482dbce6aaddbc06d32d354d9e
BLAKE2b-256 5a8f19d15847dec4ce591f0e098a8c40f32adf5f73bb2fecd7c16eef32b24ddf

See more details on using hashes here.

Provenance

The following attestation bundles were made for updl-3.0.0-py3-none-any.whl:

Publisher: publish.yml on tribixbite/updl

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

3.0.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page