mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Add Synology Photos support (#86894)
Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> Co-authored-by: mib1185 <mail@mib85.de> Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
5001a50876
commit
263901841f
@ -12,6 +12,7 @@ from synology_dsm.api.core.upgrade import SynoCoreUpgrade
|
|||||||
from synology_dsm.api.core.utilization import SynoCoreUtilization
|
from synology_dsm.api.core.utilization import SynoCoreUtilization
|
||||||
from synology_dsm.api.dsm.information import SynoDSMInformation
|
from synology_dsm.api.dsm.information import SynoDSMInformation
|
||||||
from synology_dsm.api.dsm.network import SynoDSMNetwork
|
from synology_dsm.api.dsm.network import SynoDSMNetwork
|
||||||
|
from synology_dsm.api.photos import SynoPhotos
|
||||||
from synology_dsm.api.storage.storage import SynoStorage
|
from synology_dsm.api.storage.storage import SynoStorage
|
||||||
from synology_dsm.api.surveillance_station import SynoSurveillanceStation
|
from synology_dsm.api.surveillance_station import SynoSurveillanceStation
|
||||||
from synology_dsm.exceptions import (
|
from synology_dsm.exceptions import (
|
||||||
@ -56,6 +57,7 @@ class SynoApi:
|
|||||||
self.network: SynoDSMNetwork = None
|
self.network: SynoDSMNetwork = None
|
||||||
self.security: SynoCoreSecurity = None
|
self.security: SynoCoreSecurity = None
|
||||||
self.storage: SynoStorage = None
|
self.storage: SynoStorage = None
|
||||||
|
self.photos: SynoPhotos = None
|
||||||
self.surveillance_station: SynoSurveillanceStation = None
|
self.surveillance_station: SynoSurveillanceStation = None
|
||||||
self.system: SynoCoreSystem = None
|
self.system: SynoCoreSystem = None
|
||||||
self.upgrade: SynoCoreUpgrade = None
|
self.upgrade: SynoCoreUpgrade = None
|
||||||
@ -66,6 +68,7 @@ class SynoApi:
|
|||||||
self._with_information = True
|
self._with_information = True
|
||||||
self._with_security = True
|
self._with_security = True
|
||||||
self._with_storage = True
|
self._with_storage = True
|
||||||
|
self._with_photos = True
|
||||||
self._with_surveillance_station = True
|
self._with_surveillance_station = True
|
||||||
self._with_system = True
|
self._with_system = True
|
||||||
self._with_upgrade = True
|
self._with_upgrade = True
|
||||||
@ -163,6 +166,7 @@ class SynoApi:
|
|||||||
self._fetching_entities.get(SynoCoreSecurity.API_KEY)
|
self._fetching_entities.get(SynoCoreSecurity.API_KEY)
|
||||||
)
|
)
|
||||||
self._with_storage = bool(self._fetching_entities.get(SynoStorage.API_KEY))
|
self._with_storage = bool(self._fetching_entities.get(SynoStorage.API_KEY))
|
||||||
|
self._with_photos = bool(self._fetching_entities.get(SynoStorage.API_KEY))
|
||||||
self._with_upgrade = bool(self._fetching_entities.get(SynoCoreUpgrade.API_KEY))
|
self._with_upgrade = bool(self._fetching_entities.get(SynoCoreUpgrade.API_KEY))
|
||||||
self._with_utilisation = bool(
|
self._with_utilisation = bool(
|
||||||
self._fetching_entities.get(SynoCoreUtilization.API_KEY)
|
self._fetching_entities.get(SynoCoreUtilization.API_KEY)
|
||||||
@ -180,6 +184,13 @@ class SynoApi:
|
|||||||
self.dsm.reset(self.security)
|
self.dsm.reset(self.security)
|
||||||
self.security = None
|
self.security = None
|
||||||
|
|
||||||
|
if not self._with_photos:
|
||||||
|
LOGGER.debug(
|
||||||
|
"Disable photos api from being updated or '%s'", self._entry.unique_id
|
||||||
|
)
|
||||||
|
self.dsm.reset(self.photos)
|
||||||
|
self.photos = None
|
||||||
|
|
||||||
if not self._with_storage:
|
if not self._with_storage:
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"Disable storage api from being updatedf or '%s'", self._entry.unique_id
|
"Disable storage api from being updatedf or '%s'", self._entry.unique_id
|
||||||
@ -219,6 +230,10 @@ class SynoApi:
|
|||||||
LOGGER.debug("Enable security api updates for '%s'", self._entry.unique_id)
|
LOGGER.debug("Enable security api updates for '%s'", self._entry.unique_id)
|
||||||
self.security = self.dsm.security
|
self.security = self.dsm.security
|
||||||
|
|
||||||
|
if self._with_photos:
|
||||||
|
LOGGER.debug("Enable photos api updates for '%s'", self._entry.unique_id)
|
||||||
|
self.photos = self.dsm.photos
|
||||||
|
|
||||||
if self._with_storage:
|
if self._with_storage:
|
||||||
LOGGER.debug("Enable storage api updates for '%s'", self._entry.unique_id)
|
LOGGER.debug("Enable storage api updates for '%s'", self._entry.unique_id)
|
||||||
self.storage = self.dsm.storage
|
self.storage = self.dsm.storage
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
"name": "Synology DSM",
|
"name": "Synology DSM",
|
||||||
"codeowners": ["@hacf-fr", "@Quentame", "@mib1185"],
|
"codeowners": ["@hacf-fr", "@Quentame", "@mib1185"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
|
"dependencies": ["http"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/synology_dsm",
|
"documentation": "https://www.home-assistant.io/integrations/synology_dsm",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["synology_dsm"],
|
"loggers": ["synology_dsm"],
|
||||||
|
231
homeassistant/components/synology_dsm/media_source.py
Normal file
231
homeassistant/components/synology_dsm/media_source.py
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
"""Expose Synology DSM as a media source."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import mimetypes
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
from synology_dsm.api.photos import SynoPhotosAlbum, SynoPhotosItem
|
||||||
|
from synology_dsm.exceptions import SynologyDSMException
|
||||||
|
|
||||||
|
from homeassistant.components import http
|
||||||
|
from homeassistant.components.media_player import MediaClass
|
||||||
|
from homeassistant.components.media_source import (
|
||||||
|
BrowseError,
|
||||||
|
BrowseMediaSource,
|
||||||
|
MediaSource,
|
||||||
|
MediaSourceItem,
|
||||||
|
PlayMedia,
|
||||||
|
Unresolvable,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .models import SynologyDSMData
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
|
||||||
|
"""Set up Synology media source."""
|
||||||
|
entries = hass.config_entries.async_entries(DOMAIN)
|
||||||
|
hass.http.register_view(SynologyDsmMediaView(hass))
|
||||||
|
return SynologyPhotosMediaSource(hass, entries)
|
||||||
|
|
||||||
|
|
||||||
|
class SynologyPhotosMediaSourceIdentifier:
|
||||||
|
"""Synology Photos item identifier."""
|
||||||
|
|
||||||
|
def __init__(self, identifier: str) -> None:
|
||||||
|
"""Split identifier into parts."""
|
||||||
|
parts = identifier.split("/")
|
||||||
|
|
||||||
|
self.unique_id = None
|
||||||
|
self.album_id = None
|
||||||
|
self.cache_key = None
|
||||||
|
self.file_name = None
|
||||||
|
|
||||||
|
if parts:
|
||||||
|
self.unique_id = parts[0]
|
||||||
|
if len(parts) > 1:
|
||||||
|
self.album_id = parts[1]
|
||||||
|
if len(parts) > 2:
|
||||||
|
self.cache_key = parts[2]
|
||||||
|
if len(parts) > 3:
|
||||||
|
self.file_name = parts[3]
|
||||||
|
|
||||||
|
|
||||||
|
class SynologyPhotosMediaSource(MediaSource):
|
||||||
|
"""Provide Synology Photos as media sources."""
|
||||||
|
|
||||||
|
name = "Synology Photos"
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, entries: list[ConfigEntry]) -> None:
|
||||||
|
"""Initialize Synology source."""
|
||||||
|
super().__init__(DOMAIN)
|
||||||
|
self.hass = hass
|
||||||
|
self.entries = entries
|
||||||
|
|
||||||
|
async def async_browse_media(
|
||||||
|
self,
|
||||||
|
item: MediaSourceItem,
|
||||||
|
) -> BrowseMediaSource:
|
||||||
|
"""Return media."""
|
||||||
|
if not self.hass.data.get(DOMAIN):
|
||||||
|
raise BrowseError("Diskstation not initialized")
|
||||||
|
return BrowseMediaSource(
|
||||||
|
domain=DOMAIN,
|
||||||
|
identifier=None,
|
||||||
|
media_class=MediaClass.DIRECTORY,
|
||||||
|
media_content_type=MediaClass.IMAGE,
|
||||||
|
title="Synology Photos",
|
||||||
|
can_play=False,
|
||||||
|
can_expand=True,
|
||||||
|
children_media_class=MediaClass.DIRECTORY,
|
||||||
|
children=[
|
||||||
|
*await self._async_build_diskstations(item),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _async_build_diskstations(
|
||||||
|
self, item: MediaSourceItem
|
||||||
|
) -> list[BrowseMediaSource]:
|
||||||
|
"""Handle browsing different diskstations."""
|
||||||
|
if not item.identifier:
|
||||||
|
ret = []
|
||||||
|
for entry in self.entries:
|
||||||
|
ret.append(
|
||||||
|
BrowseMediaSource(
|
||||||
|
domain=DOMAIN,
|
||||||
|
identifier=entry.unique_id,
|
||||||
|
media_class=MediaClass.DIRECTORY,
|
||||||
|
media_content_type=MediaClass.IMAGE,
|
||||||
|
title=f"{entry.title} - {entry.unique_id}",
|
||||||
|
can_play=False,
|
||||||
|
can_expand=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return ret
|
||||||
|
identifier = SynologyPhotosMediaSourceIdentifier(item.identifier)
|
||||||
|
diskstation: SynologyDSMData = self.hass.data[DOMAIN][identifier.unique_id]
|
||||||
|
|
||||||
|
if identifier.album_id is None:
|
||||||
|
# Get Albums
|
||||||
|
try:
|
||||||
|
albums = await diskstation.api.photos.get_albums()
|
||||||
|
except SynologyDSMException:
|
||||||
|
return []
|
||||||
|
|
||||||
|
ret = [
|
||||||
|
BrowseMediaSource(
|
||||||
|
domain=DOMAIN,
|
||||||
|
identifier=f"{item.identifier}/0",
|
||||||
|
media_class=MediaClass.DIRECTORY,
|
||||||
|
media_content_type=MediaClass.IMAGE,
|
||||||
|
title="All images",
|
||||||
|
can_play=False,
|
||||||
|
can_expand=True,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
for album in albums:
|
||||||
|
ret.append(
|
||||||
|
BrowseMediaSource(
|
||||||
|
domain=DOMAIN,
|
||||||
|
identifier=f"{item.identifier}/{album.album_id}",
|
||||||
|
media_class=MediaClass.DIRECTORY,
|
||||||
|
media_content_type=MediaClass.IMAGE,
|
||||||
|
title=album.name,
|
||||||
|
can_play=False,
|
||||||
|
can_expand=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
# Request items of album
|
||||||
|
# Get Items
|
||||||
|
album = SynoPhotosAlbum(int(identifier.album_id), "", 0)
|
||||||
|
try:
|
||||||
|
album_items = await diskstation.api.photos.get_items_from_album(
|
||||||
|
album, 0, 1000
|
||||||
|
)
|
||||||
|
except SynologyDSMException:
|
||||||
|
return []
|
||||||
|
|
||||||
|
ret = []
|
||||||
|
for album_item in album_items:
|
||||||
|
mime_type, _ = mimetypes.guess_type(album_item.file_name)
|
||||||
|
assert isinstance(mime_type, str)
|
||||||
|
if mime_type.startswith("image/"):
|
||||||
|
# Force small small thumbnails
|
||||||
|
album_item.thumbnail_size = "sm"
|
||||||
|
ret.append(
|
||||||
|
BrowseMediaSource(
|
||||||
|
domain=DOMAIN,
|
||||||
|
identifier=f"{identifier.unique_id}/{identifier.album_id}/{album_item.thumbnail_cache_key}/{album_item.file_name}",
|
||||||
|
media_class=MediaClass.IMAGE,
|
||||||
|
media_content_type=mime_type,
|
||||||
|
title=album_item.file_name,
|
||||||
|
can_play=True,
|
||||||
|
can_expand=False,
|
||||||
|
thumbnail=await self.async_get_thumbnail(
|
||||||
|
album_item, diskstation
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
|
||||||
|
"""Resolve media to a url."""
|
||||||
|
identifier = SynologyPhotosMediaSourceIdentifier(item.identifier)
|
||||||
|
if identifier.album_id is None:
|
||||||
|
raise Unresolvable("No album id")
|
||||||
|
if identifier.file_name is None:
|
||||||
|
raise Unresolvable("No file name")
|
||||||
|
mime_type, _ = mimetypes.guess_type(identifier.file_name)
|
||||||
|
if not isinstance(mime_type, str):
|
||||||
|
raise Unresolvable("No file extension")
|
||||||
|
return PlayMedia(
|
||||||
|
f"/synology_dsm/{identifier.unique_id}/{identifier.cache_key}/{identifier.file_name}",
|
||||||
|
mime_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_get_thumbnail(
|
||||||
|
self, item: SynoPhotosItem, diskstation: SynologyDSMData
|
||||||
|
) -> str | None:
|
||||||
|
"""Get thumbnail."""
|
||||||
|
try:
|
||||||
|
thumbnail = await diskstation.api.photos.get_item_thumbnail_url(item)
|
||||||
|
except SynologyDSMException:
|
||||||
|
return None
|
||||||
|
return str(thumbnail)
|
||||||
|
|
||||||
|
|
||||||
|
class SynologyDsmMediaView(http.HomeAssistantView):
|
||||||
|
"""Synology Media Finder View."""
|
||||||
|
|
||||||
|
url = "/synology_dsm/{source_dir_id}/{location:.*}"
|
||||||
|
name = "synology_dsm"
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
|
"""Initialize the media view."""
|
||||||
|
self.hass = hass
|
||||||
|
|
||||||
|
async def get(
|
||||||
|
self, request: web.Request, source_dir_id: str, location: str
|
||||||
|
) -> web.Response:
|
||||||
|
"""Start a GET request."""
|
||||||
|
if not self.hass.data.get(DOMAIN):
|
||||||
|
raise web.HTTPNotFound()
|
||||||
|
# location: {cache_key}/{filename}
|
||||||
|
cache_key, file_name = location.split("/")
|
||||||
|
image_id = cache_key.split("_")[0]
|
||||||
|
mime_type, _ = mimetypes.guess_type(file_name)
|
||||||
|
if not isinstance(mime_type, str):
|
||||||
|
raise web.HTTPNotFound()
|
||||||
|
diskstation: SynologyDSMData = self.hass.data[DOMAIN][source_dir_id]
|
||||||
|
|
||||||
|
item = SynoPhotosItem(image_id, "", "", "", cache_key, "")
|
||||||
|
try:
|
||||||
|
image = await diskstation.api.photos.download_item(item)
|
||||||
|
except SynologyDSMException as exc:
|
||||||
|
raise web.HTTPNotFound() from exc
|
||||||
|
return web.Response(body=image, content_type=mime_type)
|
@ -4,6 +4,9 @@ from unittest.mock import AsyncMock, patch
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
|
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
|
||||||
@ -14,6 +17,12 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]:
|
|||||||
yield mock_setup
|
yield mock_setup
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def setup_media_source(hass: HomeAssistant) -> None:
|
||||||
|
"""Set up media source."""
|
||||||
|
assert await async_setup_component(hass, "media_source", {})
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="mock_dsm")
|
@pytest.fixture(name="mock_dsm")
|
||||||
def fixture_dsm():
|
def fixture_dsm():
|
||||||
"""Set up SynologyDSM API fixture."""
|
"""Set up SynologyDSM API fixture."""
|
||||||
|
412
tests/components/synology_dsm/test_media_source.py
Normal file
412
tests/components/synology_dsm/test_media_source.py
Normal file
@ -0,0 +1,412 @@
|
|||||||
|
"""Tests for Synology DSM Media Source."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import tempfile
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from synology_dsm.api.photos import SynoPhotosAlbum, SynoPhotosItem
|
||||||
|
from synology_dsm.exceptions import SynologyDSMException
|
||||||
|
|
||||||
|
from homeassistant.components.media_player import MediaClass
|
||||||
|
from homeassistant.components.media_source import (
|
||||||
|
BrowseError,
|
||||||
|
BrowseMedia,
|
||||||
|
MediaSourceItem,
|
||||||
|
Unresolvable,
|
||||||
|
)
|
||||||
|
from homeassistant.components.synology_dsm.const import DOMAIN
|
||||||
|
from homeassistant.components.synology_dsm.media_source import (
|
||||||
|
SynologyDsmMediaView,
|
||||||
|
SynologyPhotosMediaSource,
|
||||||
|
async_get_media_source,
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_MAC,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_PORT,
|
||||||
|
CONF_SSL,
|
||||||
|
CONF_USERNAME,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.util.aiohttp import MockRequest, web
|
||||||
|
|
||||||
|
from .consts import HOST, MACS, PASSWORD, PORT, USE_SSL, USERNAME
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def dsm_with_photos() -> MagicMock:
|
||||||
|
"""Set up SynologyDSM API fixture."""
|
||||||
|
dsm = MagicMock()
|
||||||
|
dsm.login = AsyncMock(return_value=True)
|
||||||
|
dsm.update = AsyncMock(return_value=True)
|
||||||
|
dsm.network.update = AsyncMock(return_value=True)
|
||||||
|
dsm.surveillance_station.update = AsyncMock(return_value=True)
|
||||||
|
dsm.upgrade.update = AsyncMock(return_value=True)
|
||||||
|
|
||||||
|
dsm.photos.get_albums = AsyncMock(return_value=[SynoPhotosAlbum(1, "Album 1", 10)])
|
||||||
|
dsm.photos.get_items_from_album = AsyncMock(
|
||||||
|
return_value=[SynoPhotosItem(10, "", "filename.jpg", 12345, "10_1298753", "sm")]
|
||||||
|
)
|
||||||
|
dsm.photos.get_item_thumbnail_url = AsyncMock(
|
||||||
|
return_value="http://my.thumbnail.url"
|
||||||
|
)
|
||||||
|
return dsm
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("setup_media_source")
|
||||||
|
async def test_get_media_source(hass: HomeAssistant) -> None:
|
||||||
|
"""Test the async_get_media_source function and SynologyPhotosMediaSource constructor."""
|
||||||
|
|
||||||
|
source = await async_get_media_source(hass)
|
||||||
|
assert isinstance(source, SynologyPhotosMediaSource)
|
||||||
|
assert source.domain == DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("setup_media_source")
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("identifier", "exception_msg"),
|
||||||
|
[
|
||||||
|
("unique_id", "No album id"),
|
||||||
|
("unique_id/1", "No file name"),
|
||||||
|
("unique_id/1/cache_key", "No file name"),
|
||||||
|
("unique_id/1/cache_key/filename", "No file extension"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_resolve_media_bad_identifier(
|
||||||
|
hass: HomeAssistant, identifier: str, exception_msg: str
|
||||||
|
) -> None:
|
||||||
|
"""Test resolve_media with bad identifiers."""
|
||||||
|
source = await async_get_media_source(hass)
|
||||||
|
item = MediaSourceItem(hass, DOMAIN, identifier, None)
|
||||||
|
with pytest.raises(Unresolvable, match=exception_msg):
|
||||||
|
await source.async_resolve_media(item)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("setup_media_source")
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("identifier", "url", "mime_type"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"ABC012345/10/27643_876876/filename.jpg",
|
||||||
|
"/synology_dsm/ABC012345/27643_876876/filename.jpg",
|
||||||
|
"image/jpeg",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"ABC012345/12/12631_47189/filename.png",
|
||||||
|
"/synology_dsm/ABC012345/12631_47189/filename.png",
|
||||||
|
"image/png",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_resolve_media_success(
|
||||||
|
hass: HomeAssistant, identifier: str, url: str, mime_type: str
|
||||||
|
) -> None:
|
||||||
|
"""Test successful resolving an item."""
|
||||||
|
source = await async_get_media_source(hass)
|
||||||
|
item = MediaSourceItem(hass, DOMAIN, identifier, None)
|
||||||
|
result = await source.async_resolve_media(item)
|
||||||
|
|
||||||
|
assert result.url == url
|
||||||
|
assert result.mime_type == mime_type
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("setup_media_source")
|
||||||
|
async def test_browse_media_unconfigured(hass: HomeAssistant) -> None:
|
||||||
|
"""Test browse_media without any devices being configured."""
|
||||||
|
source = await async_get_media_source(hass)
|
||||||
|
item = MediaSourceItem(
|
||||||
|
hass, DOMAIN, "unique_id/album_id/cache_key/filename.jpg", None
|
||||||
|
)
|
||||||
|
with pytest.raises(BrowseError, match="Diskstation not initialized"):
|
||||||
|
await source.async_browse_media(item)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("setup_media_source")
|
||||||
|
async def test_browse_media_album_error(
|
||||||
|
hass: HomeAssistant, dsm_with_photos: MagicMock
|
||||||
|
) -> None:
|
||||||
|
"""Test browse_media with unknown album."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.synology_dsm.common.SynologyDSM",
|
||||||
|
return_value=dsm_with_photos,
|
||||||
|
), patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]):
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={
|
||||||
|
CONF_HOST: HOST,
|
||||||
|
CONF_PORT: PORT,
|
||||||
|
CONF_SSL: USE_SSL,
|
||||||
|
CONF_USERNAME: USERNAME,
|
||||||
|
CONF_PASSWORD: PASSWORD,
|
||||||
|
CONF_MAC: MACS[0],
|
||||||
|
},
|
||||||
|
unique_id="mocked_syno_dsm_entry",
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
|
||||||
|
# exception in get_albums()
|
||||||
|
dsm_with_photos.photos.get_albums = AsyncMock(
|
||||||
|
side_effect=SynologyDSMException("", None)
|
||||||
|
)
|
||||||
|
|
||||||
|
source = await async_get_media_source(hass)
|
||||||
|
|
||||||
|
item = MediaSourceItem(hass, DOMAIN, entry.unique_id, None)
|
||||||
|
result = await source.async_browse_media(item)
|
||||||
|
|
||||||
|
assert result
|
||||||
|
assert result.identifier is None
|
||||||
|
assert len(result.children) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("setup_media_source")
|
||||||
|
async def test_browse_media_get_root(
|
||||||
|
hass: HomeAssistant, dsm_with_photos: MagicMock
|
||||||
|
) -> None:
|
||||||
|
"""Test browse_media returning root media sources."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.synology_dsm.common.SynologyDSM",
|
||||||
|
return_value=dsm_with_photos,
|
||||||
|
), patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]):
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={
|
||||||
|
CONF_HOST: HOST,
|
||||||
|
CONF_PORT: PORT,
|
||||||
|
CONF_SSL: USE_SSL,
|
||||||
|
CONF_USERNAME: USERNAME,
|
||||||
|
CONF_PASSWORD: PASSWORD,
|
||||||
|
CONF_MAC: MACS[0],
|
||||||
|
},
|
||||||
|
unique_id="mocked_syno_dsm_entry",
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
|
||||||
|
source = await async_get_media_source(hass)
|
||||||
|
item = MediaSourceItem(hass, DOMAIN, "", None)
|
||||||
|
result = await source.async_browse_media(item)
|
||||||
|
|
||||||
|
assert result
|
||||||
|
assert len(result.children) == 1
|
||||||
|
assert isinstance(result.children[0], BrowseMedia)
|
||||||
|
assert result.children[0].identifier == "mocked_syno_dsm_entry"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("setup_media_source")
|
||||||
|
async def test_browse_media_get_albums(
|
||||||
|
hass: HomeAssistant, dsm_with_photos: MagicMock
|
||||||
|
) -> None:
|
||||||
|
"""Test browse_media returning albums."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.synology_dsm.common.SynologyDSM",
|
||||||
|
return_value=dsm_with_photos,
|
||||||
|
), patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]):
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={
|
||||||
|
CONF_HOST: HOST,
|
||||||
|
CONF_PORT: PORT,
|
||||||
|
CONF_SSL: USE_SSL,
|
||||||
|
CONF_USERNAME: USERNAME,
|
||||||
|
CONF_PASSWORD: PASSWORD,
|
||||||
|
CONF_MAC: MACS[0],
|
||||||
|
},
|
||||||
|
unique_id="mocked_syno_dsm_entry",
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
|
||||||
|
source = await async_get_media_source(hass)
|
||||||
|
item = MediaSourceItem(hass, DOMAIN, "mocked_syno_dsm_entry", None)
|
||||||
|
result = await source.async_browse_media(item)
|
||||||
|
|
||||||
|
assert result
|
||||||
|
assert len(result.children) == 2
|
||||||
|
assert isinstance(result.children[0], BrowseMedia)
|
||||||
|
assert result.children[0].identifier == "mocked_syno_dsm_entry/0"
|
||||||
|
assert result.children[0].title == "All images"
|
||||||
|
assert isinstance(result.children[1], BrowseMedia)
|
||||||
|
assert result.children[1].identifier == "mocked_syno_dsm_entry/1"
|
||||||
|
assert result.children[1].title == "Album 1"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("setup_media_source")
|
||||||
|
async def test_browse_media_get_items_error(
|
||||||
|
hass: HomeAssistant, dsm_with_photos: MagicMock
|
||||||
|
) -> None:
|
||||||
|
"""Test browse_media returning albums."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.synology_dsm.common.SynologyDSM",
|
||||||
|
return_value=dsm_with_photos,
|
||||||
|
), patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]):
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={
|
||||||
|
CONF_HOST: HOST,
|
||||||
|
CONF_PORT: PORT,
|
||||||
|
CONF_SSL: USE_SSL,
|
||||||
|
CONF_USERNAME: USERNAME,
|
||||||
|
CONF_PASSWORD: PASSWORD,
|
||||||
|
CONF_MAC: MACS[0],
|
||||||
|
},
|
||||||
|
unique_id="mocked_syno_dsm_entry",
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
|
||||||
|
source = await async_get_media_source(hass)
|
||||||
|
|
||||||
|
# unknown album
|
||||||
|
dsm_with_photos.photos.get_items_from_album = AsyncMock(return_value=[])
|
||||||
|
item = MediaSourceItem(hass, DOMAIN, "mocked_syno_dsm_entry/1", None)
|
||||||
|
result = await source.async_browse_media(item)
|
||||||
|
|
||||||
|
assert result
|
||||||
|
assert result.identifier is None
|
||||||
|
assert len(result.children) == 0
|
||||||
|
|
||||||
|
# exception in get_items_from_album()
|
||||||
|
dsm_with_photos.photos.get_items_from_album = AsyncMock(
|
||||||
|
side_effect=SynologyDSMException("", None)
|
||||||
|
)
|
||||||
|
item = MediaSourceItem(hass, DOMAIN, "mocked_syno_dsm_entry/1", None)
|
||||||
|
result = await source.async_browse_media(item)
|
||||||
|
|
||||||
|
assert result
|
||||||
|
assert result.identifier is None
|
||||||
|
assert len(result.children) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("setup_media_source")
|
||||||
|
async def test_browse_media_get_items_thumbnail_error(
|
||||||
|
hass: HomeAssistant, dsm_with_photos: MagicMock
|
||||||
|
) -> None:
|
||||||
|
"""Test browse_media returning albums."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.synology_dsm.common.SynologyDSM",
|
||||||
|
return_value=dsm_with_photos,
|
||||||
|
), patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]):
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={
|
||||||
|
CONF_HOST: HOST,
|
||||||
|
CONF_PORT: PORT,
|
||||||
|
CONF_SSL: USE_SSL,
|
||||||
|
CONF_USERNAME: USERNAME,
|
||||||
|
CONF_PASSWORD: PASSWORD,
|
||||||
|
CONF_MAC: MACS[0],
|
||||||
|
},
|
||||||
|
unique_id="mocked_syno_dsm_entry",
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
|
||||||
|
source = await async_get_media_source(hass)
|
||||||
|
|
||||||
|
dsm_with_photos.photos.get_item_thumbnail_url = AsyncMock(
|
||||||
|
side_effect=SynologyDSMException("", None)
|
||||||
|
)
|
||||||
|
item = MediaSourceItem(hass, DOMAIN, "mocked_syno_dsm_entry/1", None)
|
||||||
|
result = await source.async_browse_media(item)
|
||||||
|
|
||||||
|
assert result
|
||||||
|
assert len(result.children) == 1
|
||||||
|
item = result.children[0]
|
||||||
|
assert isinstance(item, BrowseMedia)
|
||||||
|
assert item.thumbnail is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("setup_media_source")
|
||||||
|
async def test_browse_media_get_items(
|
||||||
|
hass: HomeAssistant, dsm_with_photos: MagicMock
|
||||||
|
) -> None:
|
||||||
|
"""Test browse_media returning albums."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.synology_dsm.common.SynologyDSM",
|
||||||
|
return_value=dsm_with_photos,
|
||||||
|
), patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]):
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={
|
||||||
|
CONF_HOST: HOST,
|
||||||
|
CONF_PORT: PORT,
|
||||||
|
CONF_SSL: USE_SSL,
|
||||||
|
CONF_USERNAME: USERNAME,
|
||||||
|
CONF_PASSWORD: PASSWORD,
|
||||||
|
CONF_MAC: MACS[0],
|
||||||
|
},
|
||||||
|
unique_id="mocked_syno_dsm_entry",
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
|
||||||
|
source = await async_get_media_source(hass)
|
||||||
|
|
||||||
|
item = MediaSourceItem(hass, DOMAIN, "mocked_syno_dsm_entry/1", None)
|
||||||
|
result = await source.async_browse_media(item)
|
||||||
|
|
||||||
|
assert result
|
||||||
|
assert len(result.children) == 1
|
||||||
|
item = result.children[0]
|
||||||
|
assert isinstance(item, BrowseMedia)
|
||||||
|
assert item.identifier == "mocked_syno_dsm_entry/1/10_1298753/filename.jpg"
|
||||||
|
assert item.title == "filename.jpg"
|
||||||
|
assert item.media_class == MediaClass.IMAGE
|
||||||
|
assert item.media_content_type == "image/jpeg"
|
||||||
|
assert item.can_play
|
||||||
|
assert not item.can_expand
|
||||||
|
assert item.thumbnail == "http://my.thumbnail.url"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("setup_media_source")
|
||||||
|
async def test_media_view(
|
||||||
|
hass: HomeAssistant, tmp_path: Path, dsm_with_photos: MagicMock
|
||||||
|
) -> None:
|
||||||
|
"""Test SynologyDsmMediaView returning albums."""
|
||||||
|
view = SynologyDsmMediaView(hass)
|
||||||
|
request = MockRequest(b"", DOMAIN)
|
||||||
|
|
||||||
|
# diskation not set uped
|
||||||
|
with pytest.raises(web.HTTPNotFound):
|
||||||
|
await view.get(request, "", "")
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.synology_dsm.common.SynologyDSM",
|
||||||
|
return_value=dsm_with_photos,
|
||||||
|
), patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]):
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={
|
||||||
|
CONF_HOST: HOST,
|
||||||
|
CONF_PORT: PORT,
|
||||||
|
CONF_SSL: USE_SSL,
|
||||||
|
CONF_USERNAME: USERNAME,
|
||||||
|
CONF_PASSWORD: PASSWORD,
|
||||||
|
CONF_MAC: MACS[0],
|
||||||
|
},
|
||||||
|
unique_id="mocked_syno_dsm_entry",
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
|
||||||
|
with pytest.raises(web.HTTPNotFound):
|
||||||
|
await view.get(request, "", "10_1298753/filename")
|
||||||
|
|
||||||
|
# exception in download_item()
|
||||||
|
dsm_with_photos.photos.download_item = AsyncMock(
|
||||||
|
side_effect=SynologyDSMException("", None)
|
||||||
|
)
|
||||||
|
with pytest.raises(web.HTTPNotFound):
|
||||||
|
await view.get(request, "mocked_syno_dsm_entry", "10_1298753/filename.jpg")
|
||||||
|
|
||||||
|
# success
|
||||||
|
dsm_with_photos.photos.download_item = AsyncMock(return_value=b"xxxx")
|
||||||
|
tempfile.tempdir = tmp_path
|
||||||
|
result = await view.get(request, "mocked_syno_dsm_entry", "10_1298753/filename.jpg")
|
||||||
|
assert isinstance(result, web.Response)
|
Loading…
x
Reference in New Issue
Block a user