Implement NVR download for Reolink recordings (#144121)

This commit is contained in:
starkillerOG 2025-05-26 15:23:11 +02:00 committed by GitHub
parent dafda420e5
commit a3b7cd7b4d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 29 additions and 18 deletions

View File

@ -27,6 +27,8 @@ from .views import async_generate_playback_proxy_url
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
VOD_SPLIT_TIME = dt.timedelta(minutes=5)
async def async_get_media_source(hass: HomeAssistant) -> ReolinkVODMediaSource: async def async_get_media_source(hass: HomeAssistant) -> ReolinkVODMediaSource:
"""Set up camera media source.""" """Set up camera media source."""
@ -60,11 +62,13 @@ class ReolinkVODMediaSource(MediaSource):
"""Resolve media to a url.""" """Resolve media to a url."""
identifier = ["UNKNOWN"] identifier = ["UNKNOWN"]
if item.identifier is not None: if item.identifier is not None:
identifier = item.identifier.split("|", 5) identifier = item.identifier.split("|", 6)
if identifier[0] != "FILE": if identifier[0] != "FILE":
raise Unresolvable(f"Unknown media item '{item.identifier}'.") raise Unresolvable(f"Unknown media item '{item.identifier}'.")
_, config_entry_id, channel_str, stream_res, filename = identifier _, config_entry_id, channel_str, stream_res, filename, start_time, end_time = (
identifier
)
channel = int(channel_str) channel = int(channel_str)
host = get_host(self.hass, config_entry_id) host = get_host(self.hass, config_entry_id)
@ -75,12 +79,19 @@ class ReolinkVODMediaSource(MediaSource):
return VodRequestType.DOWNLOAD return VodRequestType.DOWNLOAD
return VodRequestType.PLAYBACK return VodRequestType.PLAYBACK
if host.api.is_nvr: if host.api.is_nvr:
return VodRequestType.FLV return VodRequestType.NVR_DOWNLOAD
return VodRequestType.RTMP return VodRequestType.RTMP
vod_type = get_vod_type() vod_type = get_vod_type()
if vod_type in [VodRequestType.DOWNLOAD, VodRequestType.PLAYBACK]: if vod_type == VodRequestType.NVR_DOWNLOAD:
filename = f"{start_time}_{end_time}"
if vod_type in {
VodRequestType.DOWNLOAD,
VodRequestType.NVR_DOWNLOAD,
VodRequestType.PLAYBACK,
}:
proxy_url = async_generate_playback_proxy_url( proxy_url = async_generate_playback_proxy_url(
config_entry_id, channel, filename, stream_res, vod_type.value config_entry_id, channel, filename, stream_res, vod_type.value
) )
@ -358,7 +369,7 @@ class ReolinkVODMediaSource(MediaSource):
day, day,
) )
_, vod_files = await host.api.request_vod_files( _, vod_files = await host.api.request_vod_files(
channel, start, end, stream=stream channel, start, end, stream=stream, split_time=VOD_SPLIT_TIME
) )
for file in vod_files: for file in vod_files:
file_name = f"{file.start_time.time()} {file.duration}" file_name = f"{file.start_time.time()} {file.duration}"
@ -372,7 +383,7 @@ class ReolinkVODMediaSource(MediaSource):
children.append( children.append(
BrowseMediaSource( BrowseMediaSource(
domain=DOMAIN, domain=DOMAIN,
identifier=f"FILE|{config_entry_id}|{channel}|{stream}|{file.file_name}", identifier=f"FILE|{config_entry_id}|{channel}|{stream}|{file.file_name}|{file.start_time_id}|{file.end_time_id}",
media_class=MediaClass.VIDEO, media_class=MediaClass.VIDEO,
media_content_type=MediaType.VIDEO, media_content_type=MediaType.VIDEO,
title=file_name, title=file_name,

View File

@ -51,8 +51,10 @@ TEST_DAY = 14
TEST_DAY2 = 15 TEST_DAY2 = 15
TEST_HOUR = 13 TEST_HOUR = 13
TEST_MINUTE = 12 TEST_MINUTE = 12
TEST_FILE_NAME = f"{TEST_YEAR}{TEST_MONTH}{TEST_DAY}{TEST_HOUR}{TEST_MINUTE}00" TEST_START = f"{TEST_YEAR}{TEST_MONTH}{TEST_DAY}{TEST_HOUR}{TEST_MINUTE}"
TEST_FILE_NAME_MP4 = f"{TEST_YEAR}{TEST_MONTH}{TEST_DAY}{TEST_HOUR}{TEST_MINUTE}00.mp4" TEST_END = f"{TEST_YEAR}{TEST_MONTH}{TEST_DAY}{TEST_HOUR}{TEST_MINUTE + 5}"
TEST_FILE_NAME = f"{TEST_START}00"
TEST_FILE_NAME_MP4 = f"{TEST_START}00.mp4"
TEST_STREAM = "main" TEST_STREAM = "main"
TEST_CHANNEL = "0" TEST_CHANNEL = "0"
TEST_CAM_NAME = "Cam new name" TEST_CAM_NAME = "Cam new name"
@ -92,17 +94,15 @@ async def test_resolve(
await hass.async_block_till_done() await hass.async_block_till_done()
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
file_id = ( file_id = f"FILE|{config_entry.entry_id}|{TEST_CHANNEL}|{TEST_STREAM}|{TEST_FILE_NAME}|{TEST_START}|{TEST_END}"
f"FILE|{config_entry.entry_id}|{TEST_CHANNEL}|{TEST_STREAM}|{TEST_FILE_NAME}" reolink_connect.get_vod_source.return_value = (TEST_MIME_TYPE_MP4, TEST_URL)
)
reolink_connect.get_vod_source.return_value = (TEST_MIME_TYPE, TEST_URL)
play_media = await async_resolve_media( play_media = await async_resolve_media(
hass, f"{URI_SCHEME}{DOMAIN}/{file_id}", None hass, f"{URI_SCHEME}{DOMAIN}/{file_id}", None
) )
assert play_media.mime_type == TEST_MIME_TYPE assert play_media.mime_type == TEST_MIME_TYPE_MP4
file_id = f"FILE|{config_entry.entry_id}|{TEST_CHANNEL}|{TEST_STREAM}|{TEST_FILE_NAME_MP4}" file_id = f"FILE|{config_entry.entry_id}|{TEST_CHANNEL}|{TEST_STREAM}|{TEST_FILE_NAME_MP4}|{TEST_START}|{TEST_END}"
reolink_connect.get_vod_source.return_value = (TEST_MIME_TYPE_MP4, TEST_URL2) reolink_connect.get_vod_source.return_value = (TEST_MIME_TYPE_MP4, TEST_URL2)
play_media = await async_resolve_media( play_media = await async_resolve_media(
@ -117,9 +117,7 @@ async def test_resolve(
) )
assert play_media.mime_type == TEST_MIME_TYPE_MP4 assert play_media.mime_type == TEST_MIME_TYPE_MP4
file_id = ( file_id = f"FILE|{config_entry.entry_id}|{TEST_CHANNEL}|{TEST_STREAM}|{TEST_FILE_NAME}|{TEST_START}|{TEST_END}"
f"FILE|{config_entry.entry_id}|{TEST_CHANNEL}|{TEST_STREAM}|{TEST_FILE_NAME}"
)
reolink_connect.get_vod_source.return_value = (TEST_MIME_TYPE, TEST_URL) reolink_connect.get_vod_source.return_value = (TEST_MIME_TYPE, TEST_URL)
play_media = await async_resolve_media( play_media = await async_resolve_media(
@ -217,6 +215,8 @@ async def test_browsing(
mock_vod_file.start_time = datetime( mock_vod_file.start_time = datetime(
TEST_YEAR, TEST_MONTH, TEST_DAY, TEST_HOUR, TEST_MINUTE TEST_YEAR, TEST_MONTH, TEST_DAY, TEST_HOUR, TEST_MINUTE
) )
mock_vod_file.start_time_id = TEST_START
mock_vod_file.end_time_id = TEST_END
mock_vod_file.duration = timedelta(minutes=15) mock_vod_file.duration = timedelta(minutes=15)
mock_vod_file.file_name = TEST_FILE_NAME mock_vod_file.file_name = TEST_FILE_NAME
reolink_connect.request_vod_files.return_value = ([mock_status], [mock_vod_file]) reolink_connect.request_vod_files.return_value = ([mock_status], [mock_vod_file])
@ -224,7 +224,7 @@ async def test_browsing(
browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{browse_day_0_id}") browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{browse_day_0_id}")
browse_files_id = f"FILES|{entry_id}|{TEST_CHANNEL}|{TEST_STREAM}" browse_files_id = f"FILES|{entry_id}|{TEST_CHANNEL}|{TEST_STREAM}"
browse_file_id = f"FILE|{entry_id}|{TEST_CHANNEL}|{TEST_STREAM}|{TEST_FILE_NAME}" browse_file_id = f"FILE|{entry_id}|{TEST_CHANNEL}|{TEST_STREAM}|{TEST_FILE_NAME}|{TEST_START}|{TEST_END}"
assert browse.domain == DOMAIN assert browse.domain == DOMAIN
assert ( assert (
browse.title browse.title