8000 feat: add support for new download method in Protect 4.x by ep1cman · Pull Request #249 · uilibs/uiprotect · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat: add support for new download method in Protect 4.x #249

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 149 additions & 12 deletions src/uiprotect/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
SmartDetectObjectType,
SmartDetectTrack,
Version,
VideoExportType,
Viewer,
WSPacket,
WSSubscriptionMessage,
Expand Down Expand Up @@ -88,6 +89,8 @@

If your Protect instance has a lot of events, this request will take much longer then expected. It is recommended adding additional filters to speed the request up."""

# Version where new download mechanism was added
NEW_DOWNLOAD_VERSION = Version("4.0.0")

_LOGGER = logging.getLogger(__name__)
_COOKIE_RE = re.compile(r"^set-cookie: ", re.IGNORECASE)
Expand Down Expand Up @@ -1482,6 +1485,145 @@
if progress_callback is not None:
await progress_callback(step, current, total)

async def download_camera_video(
self,
camera_id: str,
filename: str,
output_file: Path | None = None,
iterator_callback: IteratorCallback | None = None,
progress_callback: ProgressCallback | None = None,
chunk_size: int = 65536,
) -> bytes | None:
"""
Downloads a prepared MP4 video from a given camera.

This is the newer implementation of retrieving video from Unifi Protect.
You need to supply the filename returned from prepare_camera_video().

It is recommended to provide an output file or progress callback for larger
video clips, otherwise the full video must be downloaded to memory before
being written.
"""
if self.bootstrap.nvr.version < NEW_DOWNLOAD_VERSION:
raise ValueError(

Check warning on line 1508 in src/uiprotect/api.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1507-L1508

Added lines #L1507 - L1508 were not covered by tests
"This method is only support from Unifi Protect version >= 4.0.0."
)

params = {

Check warning on line 1512 in src/uiprotect/api.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1512

Added line #L1512 was not covered by tests
"camera": camera_id,
"filename": filename,
}

if (

Check warning on line 1517 in src/uiprotect/api.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1517

Added line #L1517 was not covered by tests
iterator_callback is None
and progress_callback is None
and output_file is None
):
return await self.api_request_raw(

Check warning on line 1522 in src/uiprotect/api.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1522

Added line #L1522 was not covered by tests
"video/download",
params=params,
raise_exception=False,
)

r = await self.request(

Check warning on line 1528 in src/uiprotect/api.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1528

Added line #L1528 was not covered by tests
"get",
f"{self.api_path}video/download",
auto_close=False,
timeout=0,
params=params,
)
if output_file is not None:
async with aiofiles.open(output_file, "wb") as output:

Check warning on line 1536 in src/uiprotect/api.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1535-L1536

Added lines #L1535 - L1536 were not covered by tests

async def callback(total: int, chunk: bytes | None) -> None:
if iterator_callback is not None:
await iterator_callback(total, chunk)
if chunk is not None:
await output.write(chunk)

Check warning on line 1542 in src/uiprotect/api.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1538-L1542

Added lines #L1538 - L1542 were not covered by tests

await self._stream_response(r, chunk_size, callback, progress_callback)

Check warning on line 1544 in src/uiprotect/api.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1544

Added line #L1544 was not covered by tests
else:
await self._stream_response(

Check warning on line 1546 in src/uiprotect/api.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1546

Added line #L1546 was not covered by tests
r,
chunk_size,
iterator_callback,
progress_callback,
)
r.close()
return None

Check warning on line 1553 in src/uiprotect/api.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1552-L1553

Added lines #L1552 - L1553 were not covered by tests

async def _validate_channel_id(self, camera_id: str, channel_index: int) -> None:
"""Validate if camera_id and channel_index are valid."""
if self._bootstrap is None:
await self.update()

Check warning on line 1558 in src/uiprotect/api.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1558

Added line #L1558 was not covered by tests
try:
camera = self._bootstrap.cameras[camera_id] # type: ignore[union-attr]
camera.channels[channel_index]
except (IndexError, AttributeError, KeyError) as e:
raise BadRequest(f"Invalid input: {e}") from e

Check warning on line 1563 in src/uiprotect/api.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1562-L1563

Added lines #L1562 - L1563 were not covered by tests

async def prepare_camera_video(
self,
camera_id: str,
start: datetime,
end: datetime,
channel_index: int = 0,
validate_channel_id: bool = True,
fps: int | None = None,
filename: str | None = None,
) -> list[Any] | dict[str, Any] | None:
"""
Prepares MP4 video from a given camera at a specific time.

This is the newer implementation of retrieving video from Unifi Protect.
When using this method, it should be followed up with download_camera_video().

Start/End of video export are approximate. It may be +/- a few seconds.

Providing the `fps` parameter creates a "timelapse" export wtih the given FPS
value. Protect app gives the options for 60x (fps=4), 120x (fps=8), 300x
(fps=20), and 600x (fps=40).

You will receive a filename and an expiry time in seconds.
"""
if self.bootstrap.nvr.version < NEW_DOWNLOAD_VERSION:
raise ValueError(

Check warning on line 1590 in src/uiprotect/api.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1589-L1590

Added lines #L1589 - L1590 were not covered by tests
"This method is only support from Unifi Protect version >= 4.0.0."
)

if validate_channel_id:
await self._validate_channel_id(camera_id, channel_index)

Check warning on line 1595 in src/uiprotect/api.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1594-L1595

Added lines #L1594 - L1595 were not covered by tests

params = {

Check warning on line 1597 in src/uiprotect/api.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1597

Added line #L1597 was not covered by tests
"camera": camera_id,
"start": to_js_time(start),
"end": to_js_time(end),
}

if channel_index == 3:
params.update({"lens": 2})

Check warning on line 1604 in src/uiprotect/api.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1603-L1604

Added lines #L1603 - L1604 were not covered by tests
else:
params.update({"channel": channel_index})

Check warning on line 1606 in src/uiprotect/api.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1606

Added line #L1606 was not covered by tests

if fps is not None and fps > 0:
params["fps"] = fps
params["type"] = VideoExportType.TIMELAPSE

Check warning on line 1610 in src/uiprotect/api.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1608-L1610

Added lines #L1608 - L1610 were not covered by tests
else:
params["type"] = VideoExportType.ROTATING

Check warning on line 1612 in src/uiprotect/api.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1612

Added line #L1612 was not covered by tests

if not filename:
start_str = start.strftime("%m-%d-%Y, %H.%M.%S %Z")
end_str = end.strftime("%m-%d-%Y, %H.%M.%S %Z")
filename = f"{camera_id} {start_str} - {end_str}.mp4"

Check warning on line 1617 in src/uiprotect/api.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1614-L1617

Added lines #L1614 - L1617 were not covered by tests

params["filename"] = filename

Check warning on line 1619 in src/uiprotect/api.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1619

Added line #L1619 was not covered by tests

return await self.api_request(

Check warning on line 1621 in src/uiprotect/api.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1621

Added line #L1621 was not covered by tests
"video/prepare",
params=params,
raise_exception=True,
)

async def get_camera_video(
self,
camera_id: str,
Expand All @@ -1500,51 +1642,46 @@

Start/End of video export are approximate. It may be +/- a few seconds.

It is recommended to provide a output file or progress callback for larger
It is recommended to provide an output file or progress callback for larger
video clips, otherwise the full video must be downloaded to memory before
being written.

Providing the `fps` parameter creates a "timelapse" export wtih the given FPS
value. Protect app gives the options for 60x (fps=4), 120x (fps=8), 300x
(fps=20), and 600x (fps=40).
"""
if validate_channel_id and self._bootstrap is not None:
camera = self._bootstrap.cameras[camera_id]
try:
camera.channels[channel_index]
except IndexError as e:
raise BadRequest from e
if validate_channel_id:
await self._validate_channel_id(camera_id, channel_index)

params = {
"camera": camera_id,
"start": to_js_time(start),
"end": to_js_time(end),
}

if fps is not None:
if fps is not None and fps > 0:
params["fps"] = fps
params["type"] = "timelapse"
params["type"] = VideoExportType.TIMELAPSE

Check warning on line 1664 in src/uiprotect/api.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1664

Added line #L1664 was not covered by tests

if channel_index == 3:
params.update({"lens": 2})
else:
params.update({"channel": channel_index})

path = "video/export"
if (
iterator_callback is None
and progress_callback is None
and output_file is None
):
return await self.api_request_raw(
path,
"video/export",
params=params,
raise_exception=False,
)

r = await self.request(
"get",
f"{self.api_path}{path}",
f"{self.api_path}video/export",
auto_close=False,
timeout=0,
params=params,
Expand Down
2 changes: 2 additions & 0 deletions src/uiprotect/data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
StateType,
StorageType,
Version,
VideoExportType,
VideoMode,
WDRLevel,
)
Expand Down Expand Up @@ -145,6 +146,7 @@
"UserLocation",
"Version",
"VideoMode",
"VideoExportType",
"Viewer",
"WDRLevel",
"WSAction",
Expand Down
6 changes: 6 additions & 0 deletions src/uiprotect/data/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,12 @@ class LensType(str, enum.Enum):
DLSR_17 = "m43"


@enum.unique
class VideoExportType(str, ValuesEnumMixin, enum.Enum):
TIMELAPSE = "timelapse"
ROTATING = "rotating"


class DoorbellText(ConstrainedStr):
max_length = 30

Expand Down
Loading
0