Skip to content
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

chore: update gitignore and remove Jetbrains IDEA files #46

Closed
wants to merge 9 commits into from
Closed
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,7 @@ event.json
test_websocket.py
test_raw.py
rawdata.json

# IDE settings
.vscode/
.idea/
9 changes: 0 additions & 9 deletions .idea/uiprotect.iml

This file was deleted.

65 changes: 0 additions & 65 deletions .idea/watcherTasks.xml

This file was deleted.

32 changes: 0 additions & 32 deletions .idea/workspace.xml

This file was deleted.

204 changes: 195 additions & 9 deletions src/uiprotect/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1451,7 +1451,134 @@
if progress_callback is not None:
await progress_callback(step, current, total)

async def get_camera_video(
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:
"""
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.
"""
path = "video/download"

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

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1473

Added line #L1473 was not covered by tests

params = {

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

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1475

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

if (
iterator_callback is None
and progress_callback is None
and output_file is None
):
return await self.api_request_raw(

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

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1485

Added line #L1485 was not covered by tests
path,
params=params,
raise_exception=False,
)

r = await self._os.request(

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

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1491

Added line #L1491 was not covered by tests
"get",
urljoin(self.api_path, path),
auto_close=False,
timeout=0,
params=params,
)
if output_file is not None:
async with aiofiles.open(output_file, "wb") as output:

async def callback(total: int, chunk: bytes | None) -> None:

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

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1501

Added line #L1501 was not covered by tests
if iterator_callback is not None:
await iterator_callback(total, chunk)

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

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1503

Added line #L1503 was not covered by tests
if chunk is not None:
await output.write(chunk)

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

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1505

Added line #L1505 was not covered by tests

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

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

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1507

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

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

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1509

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

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

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1515-L1516

Added lines #L1515 - L1516 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 validate_channel_id and self._bootstrap is not None:
try:
camera = self._bootstrap.cameras[camera_id]
camera.channels[channel_index]
except IndexError as e:
raise BadRequest from e

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

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1546-L1547

Added lines #L1546 - L1547 were not covered by tests

params = {
"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 1556 in src/uiprotect/api.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1556

Added line #L1556 was not covered by tests
else:
params.update({"channel": channel_index})

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

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

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1561-L1562

Added lines #L1561 - L1562 were not covered by tests
else:
params["type"] = "rotating"

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"

params["filename"] = filename

path = "video/prepare"

return await self.api_request(
path,
params=params,
raise_exception=True,
)

async def export_camera_video(
self,
camera_id: str,
start: datetime,
Expand All @@ -1463,13 +1590,13 @@
progress_callback: ProgressCallback | None = None,
chunk_size: int = 65536,
fps: int | None = None,
) -> bytes | None:
) -> bytes:
"""
Exports MP4 video from a given camera at a specific time.

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.

Expand All @@ -1478,8 +1605,8 @@
(fps=20), and 600x (fps=40).
"""
if validate_channel_id and self._bootstrap is not None:
camera = self._bootstrap.cameras[camera_id]
try:
camera = self._bootstrap.cameras[camera_id]
camera.channels[channel_index]
except IndexError as e:
raise BadRequest from e
Expand All @@ -1490,16 +1617,17 @@
"end": to_js_time(end),
}

if fps is not None:
params["fps"] = fps
params["type"] = "timelapse"

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

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

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

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1626-L1627

Added lines #L1626 - L1627 were not covered by tests

path = "video/export"

if (
iterator_callback is None
and progress_callback is None
Expand All @@ -1511,7 +1639,7 @@
raise_exception=False,
)

r = await self.request(
r = await self._os.request(

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

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1642

Added line #L1642 was not covered by tests
"get",
urljoin(self.api_path, path),
auto_close=False,
Expand All @@ -1538,6 +1666,64 @@
r.close()
return None

async def get_camera_video(
self,
camera_id: str,
start: datetime,
end: datetime,
channel_index: int = 0,
validate_channel_id: bool = True,
output_file: Path | None = None,
iterator_callback: IteratorCallback | None = None,
progress_callback: ProgressCallback | None = None,
chunk_size: int = 65536,
fps: int | None = None,
filename: str | None = None,
) -> bytes:
"""
Deprecated: maintained for backwards compatibility.

If you are using Unifi Protect 4 or later, please use
prepare_camera_video() and download_camera_video() instead.
"""
try:
prepare_response = await self.prepare_camera_video(
camera_id=camera_id,
start=start,
end=end,
channel_index=channel_index,
validate_channel_id=validate_channel_id,
fps=fps,
filename=filename,
)

if isinstance(prepare_response, dict):
download_filename = prepare_response["fileName"]
else:
raise Exception

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

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1703

Added line #L1703 was not covered by tests

return await self.download_camera_video(

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

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1705

Added line #L1705 was not covered by tests
camera_id=camera_id,
filename=download_filename,
output_file=output_file,
iterator_callback=iterator_callback,
progress_callback=progress_callback,
chunk_size=chunk_size,
)
except Exception:
return await self.export_camera_video(
camera_id=camera_id,
start=start,
end=end,
channel_index=channel_index,
validate_channel_id=validate_channel_id,
output_file=output_file,
iterator_callback=iterator_callback,
progress_callback=progress_callback,
chunk_size=chunk_size,
fps=fps,
)

async def _get_image_with_retry(
self,
path: str,
Expand Down
Loading