diff --git a/homeassistant/components/jellyfin/__init__.py b/homeassistant/components/jellyfin/__init__.py index e1d8600530f..39085317a54 100644 --- a/homeassistant/components/jellyfin/__init__.py +++ b/homeassistant/components/jellyfin/__init__.py @@ -26,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) try: - _, connect_result = await validate_input(hass, dict(entry.data), client) + user_id, connect_result = await validate_input(hass, dict(entry.data), client) except CannotConnect as ex: raise ConfigEntryNotReady("Cannot connect to Jellyfin server") from ex except InvalidAuth: @@ -36,13 +36,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: server_info: dict[str, Any] = connect_result["Servers"][0] coordinators: dict[str, JellyfinDataUpdateCoordinator[Any]] = { - "sessions": SessionsDataUpdateCoordinator(hass, client, server_info), + "sessions": SessionsDataUpdateCoordinator(hass, client, server_info, user_id), } for coordinator in coordinators.values(): await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id] = JellyfinData( + client_device_id=entry.data[CONF_CLIENT_DEVICE_ID], jellyfin_client=client, coordinators=coordinators, ) diff --git a/homeassistant/components/jellyfin/browse_media.py b/homeassistant/components/jellyfin/browse_media.py new file mode 100644 index 00000000000..0e63cb2f5d2 --- /dev/null +++ b/homeassistant/components/jellyfin/browse_media.py @@ -0,0 +1,179 @@ +"""Support for media browsing.""" +from __future__ import annotations + +import asyncio +from typing import Any + +from jellyfin_apiclient_python import JellyfinClient + +from homeassistant.components.media_player import BrowseError, MediaClass, MediaType +from homeassistant.components.media_player.browse_media import BrowseMedia +from homeassistant.core import HomeAssistant + +from .client_wrapper import get_artwork_url +from .const import CONTENT_TYPE_MAP, MEDIA_CLASS_MAP, MEDIA_TYPE_NONE + +CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS: dict[str, str] = { + MediaType.MUSIC: MediaClass.MUSIC, + MediaType.SEASON: MediaClass.SEASON, + MediaType.TVSHOW: MediaClass.TV_SHOW, + "boxset": MediaClass.DIRECTORY, + "collection": MediaClass.DIRECTORY, + "library": MediaClass.DIRECTORY, +} + +JF_SUPPORTED_LIBRARY_TYPES = ["movies", "music", "tvshows"] + +PLAYABLE_MEDIA_TYPES = [ + MediaType.EPISODE, + MediaType.MOVIE, + MediaType.MUSIC, +] + + +async def item_payload( + hass: HomeAssistant, + client: JellyfinClient, + user_id: str, + item: dict[str, Any], +) -> BrowseMedia: + """Create response payload for a single media item.""" + title = item["Name"] + thumbnail = get_artwork_url(client, item, 600) + + media_content_id = item["Id"] + media_content_type = CONTENT_TYPE_MAP.get(item["Type"], MEDIA_TYPE_NONE) + + return BrowseMedia( + title=title, + media_content_id=media_content_id, + media_content_type=media_content_type, + media_class=MEDIA_CLASS_MAP.get(item["Type"], MediaClass.DIRECTORY), + can_play=bool(media_content_type in PLAYABLE_MEDIA_TYPES and media_content_id), + can_expand=bool(item.get("IsFolder")), + children_media_class=None, + thumbnail=thumbnail, + ) + + +async def build_root_response( + hass: HomeAssistant, client: JellyfinClient, user_id: str +) -> BrowseMedia: + """Create response payload for root folder.""" + folders = await hass.async_add_executor_job(client.jellyfin.get_media_folders) + + children = [ + await item_payload(hass, client, user_id, folder) + for folder in folders["Items"] + if folder["CollectionType"] in JF_SUPPORTED_LIBRARY_TYPES + ] + + return BrowseMedia( + media_content_id="", + media_content_type="root", + media_class=MediaClass.DIRECTORY, + children_media_class=MediaClass.DIRECTORY, + title="Jellyfin", + can_play=False, + can_expand=True, + children=children, + ) + + +async def build_item_response( + hass: HomeAssistant, + client: JellyfinClient, + user_id: str, + media_content_type: str | None, + media_content_id: str, +) -> BrowseMedia: + """Create response payload for the provided media query.""" + title, media, thumbnail = await get_media_info( + hass, client, user_id, media_content_type, media_content_id + ) + + if title is None or media is None: + raise BrowseError(f"Media not found: {media_content_type} / {media_content_id}") + + children = await asyncio.gather( + *(item_payload(hass, client, user_id, media_item) for media_item in media) + ) + + response = BrowseMedia( + media_class=CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS.get( + str(media_content_type), MediaClass.DIRECTORY + ), + media_content_id=media_content_id, + media_content_type=str(media_content_type), + title=title, + can_play=bool(media_content_type in PLAYABLE_MEDIA_TYPES and media_content_id), + can_expand=True, + children=children, + thumbnail=thumbnail, + ) + + response.calculate_children_class() + + return response + + +def fetch_item(client: JellyfinClient, item_id: str) -> dict[str, Any] | None: + """Fetch item from Jellyfin server.""" + result = client.jellyfin.get_item(item_id) + + if not result: + return None + + item: dict[str, Any] = result + return item + + +def fetch_items( + client: JellyfinClient, + params: dict[str, Any], +) -> list[dict[str, Any]] | None: + """Fetch items from Jellyfin server.""" + result = client.jellyfin.user_items(params=params) + + if not result or "Items" not in result or len(result["Items"]) < 1: + return None + + items: list[dict[str, Any]] = result["Items"] + + return [ + item + for item in items + if not item.get("IsFolder") + or (item.get("IsFolder") and item.get("ChildCount", 1) > 0) + ] + + +async def get_media_info( + hass: HomeAssistant, + client: JellyfinClient, + user_id: str, + media_content_type: str | None, + media_content_id: str, +) -> tuple[str | None, list[dict[str, Any]] | None, str | None]: + """Fetch media info.""" + thumbnail: str | None = None + title: str | None = None + media: list[dict[str, Any]] | None = None + + item = await hass.async_add_executor_job(fetch_item, client, media_content_id) + + if item is None: + return None, None, None + + title = item["Name"] + thumbnail = get_artwork_url(client, item) + + if item.get("IsFolder"): + media = await hass.async_add_executor_job( + fetch_items, client, {"parentId": media_content_id, "fields": "childCount"} + ) + + if not media or len(media) == 0: + media = None + + return title, media, thumbnail diff --git a/homeassistant/components/jellyfin/client_wrapper.py b/homeassistant/components/jellyfin/client_wrapper.py index c6ae67b4c80..ab771d405ea 100644 --- a/homeassistant/components/jellyfin/client_wrapper.py +++ b/homeassistant/components/jellyfin/client_wrapper.py @@ -15,7 +15,7 @@ from homeassistant import exceptions from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME from homeassistant.core import HomeAssistant -from .const import CLIENT_VERSION, USER_AGENT, USER_APP_NAME +from .const import CLIENT_VERSION, ITEM_KEY_IMAGE_TAGS, USER_AGENT, USER_APP_NAME async def validate_input( @@ -92,6 +92,25 @@ def _get_user_id(api: API) -> str: return userid +def get_artwork_url( + client: JellyfinClient, item: dict[str, Any], max_width: int = 600 +) -> str | None: + """Find a suitable thumbnail for an item.""" + artwork_id: str = item["Id"] + artwork_type = "Primary" + parent_backdrop_id: str | None = item.get("ParentBackdropItemId") + + if "Backdrop" in item[ITEM_KEY_IMAGE_TAGS]: + artwork_type = "Backdrop" + elif parent_backdrop_id: + artwork_type = "Backdrop" + artwork_id = parent_backdrop_id + elif "Primary" not in item[ITEM_KEY_IMAGE_TAGS]: + return None + + return str(client.jellyfin.artwork(artwork_id, artwork_type, max_width)) + + class CannotConnect(exceptions.HomeAssistantError): """Error to indicate the server is unreachable.""" diff --git a/homeassistant/components/jellyfin/const.py b/homeassistant/components/jellyfin/const.py index 67956899cab..865e05a0081 100644 --- a/homeassistant/components/jellyfin/const.py +++ b/homeassistant/components/jellyfin/const.py @@ -2,6 +2,7 @@ import logging from typing import Final +from homeassistant.components.media_player import MediaClass, MediaType from homeassistant.const import Platform, __version__ as hass_version DOMAIN: Final = "jellyfin" @@ -32,7 +33,6 @@ ITEM_TYPE_MOVIE: Final = "Movie" MAX_IMAGE_WIDTH: Final = 500 MAX_STREAMING_BITRATE: Final = "140000000" - MEDIA_SOURCE_KEY_PATH: Final = "Path" MEDIA_TYPE_AUDIO: Final = "Audio" @@ -44,5 +44,29 @@ SUPPORTED_COLLECTION_TYPES: Final = [COLLECTION_TYPE_MUSIC, COLLECTION_TYPE_MOVI USER_APP_NAME: Final = "Home Assistant" USER_AGENT: Final = f"Home-Assistant/{CLIENT_VERSION}" -PLATFORMS = [Platform.SENSOR] +CONTENT_TYPE_MAP = { + "Audio": MediaType.MUSIC, + "Episode": MediaType.EPISODE, + "Season": MediaType.SEASON, + "Series": MediaType.TVSHOW, + "Movie": MediaType.MOVIE, + "CollectionFolder": "collection", + "AggregateFolder": "library", + "Folder": "library", + "BoxSet": "boxset", +} +MEDIA_CLASS_MAP = { + "MusicAlbum": MediaClass.ALBUM, + "MusicArtist": MediaClass.ARTIST, + "Audio": MediaClass.MUSIC, + "Series": MediaClass.DIRECTORY, + "Movie": MediaClass.MOVIE, + "CollectionFolder": MediaClass.DIRECTORY, + "Folder": MediaClass.DIRECTORY, + "BoxSet": MediaClass.DIRECTORY, + "Episode": MediaClass.EPISODE, + "Season": MediaClass.SEASON, +} + +PLATFORMS = [Platform.MEDIA_PLAYER, Platform.SENSOR] LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/jellyfin/coordinator.py b/homeassistant/components/jellyfin/coordinator.py index 3e277711f6c..626b2126fee 100644 --- a/homeassistant/components/jellyfin/coordinator.py +++ b/homeassistant/components/jellyfin/coordinator.py @@ -32,18 +32,20 @@ class JellyfinDataUpdateCoordinator(DataUpdateCoordinator[JellyfinDataT]): hass: HomeAssistant, api_client: JellyfinClient, system_info: dict[str, Any], + user_id: str, ) -> None: """Initialize the coordinator.""" super().__init__( hass=hass, logger=LOGGER, name=DOMAIN, - update_interval=timedelta(seconds=30), + update_interval=timedelta(seconds=10), ) self.api_client: JellyfinClient = api_client self.server_id: str = system_info["Id"] self.server_name: str = system_info["Name"] self.server_version: str | None = system_info.get("Version") + self.user_id: str = user_id async def _async_update_data(self) -> JellyfinDataT: """Get the latest data from Jellyfin.""" diff --git a/homeassistant/components/jellyfin/media_player.py b/homeassistant/components/jellyfin/media_player.py new file mode 100644 index 00000000000..aea9fd4a594 --- /dev/null +++ b/homeassistant/components/jellyfin/media_player.py @@ -0,0 +1,295 @@ +"""Support for the Jellyfin media player.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.media_player import ( + MediaPlayerEntity, + MediaPlayerEntityDescription, + MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, +) +from homeassistant.components.media_player.browse_media import BrowseMedia +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util.dt import parse_datetime + +from .browse_media import build_item_response, build_root_response +from .client_wrapper import get_artwork_url +from .const import CONTENT_TYPE_MAP, DOMAIN +from .coordinator import JellyfinDataUpdateCoordinator +from .entity import JellyfinEntity +from .models import JellyfinData + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Jellyfin media_player from a config entry.""" + jellyfin_data: JellyfinData = hass.data[DOMAIN][entry.entry_id] + coordinator = jellyfin_data.coordinators["sessions"] + + async_add_entities( + ( + JellyfinMediaPlayer(coordinator, session_id, session_data) + for session_id, session_data in coordinator.data.items() + if session_data["DeviceId"] != jellyfin_data.client_device_id + and session_data["Client"] != "Home Assistant" + ), + ) + + +class JellyfinMediaPlayer(JellyfinEntity, MediaPlayerEntity): + """Represents a Jellyfin Player device.""" + + def __init__( + self, + coordinator: JellyfinDataUpdateCoordinator, + session_id: str, + session_data: dict[str, Any], + ) -> None: + """Initialize the Jellyfin Media Player entity.""" + super().__init__( + coordinator, + MediaPlayerEntityDescription( + key=session_id, + ), + ) + + self.session_id = session_id + self.session_data: dict[str, Any] | None = session_data + self.device_id: str = session_data["DeviceId"] + self.device_name: str = session_data["DeviceName"] + self.client_name: str = session_data["Client"] + self.app_version: str = session_data["ApplicationVersion"] + + self.capabilities: dict[str, Any] = session_data["Capabilities"] + self.now_playing: dict[str, Any] | None = session_data.get("NowPlayingItem") + self.play_state: dict[str, Any] | None = session_data.get("PlayState") + + if self.capabilities.get("SupportsPersistentIdentifier", False): + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.device_id)}, + manufacturer="Jellyfin", + model=self.client_name, + name=self.device_name, + sw_version=self.app_version, + via_device=(DOMAIN, coordinator.server_id), + ) + else: + self._attr_device_info = None + self._attr_has_entity_name = False + self._attr_name = self.device_name + + self._update_from_session_data() + + @callback + def _handle_coordinator_update(self) -> None: + self.session_data = ( + self.coordinator.data.get(self.session_id) + if self.coordinator.data is not None + else None + ) + + if self.session_data is not None: + self.now_playing = self.session_data.get("NowPlayingItem") + self.play_state = self.session_data.get("PlayState") + else: + self.now_playing = None + self.play_state = None + + self._update_from_session_data() + super()._handle_coordinator_update() + + @callback + def _update_from_session_data(self) -> None: + """Process session data to update entity properties.""" + state = None + media_content_type = None + media_content_id = None + media_title = None + media_series_title = None + media_season = None + media_episode = None + media_album_name = None + media_album_artist = None + media_artist = None + media_track = None + media_duration = None + media_position = None + media_position_updated = None + volume_muted = False + volume_level = None + + if self.session_data is not None: + state = MediaPlayerState.IDLE + media_position_updated = ( + parse_datetime(self.session_data["LastPlaybackCheckIn"]) + if self.now_playing + else None + ) + + if self.now_playing is not None: + state = MediaPlayerState.PLAYING + media_content_type = CONTENT_TYPE_MAP.get(self.now_playing["Type"], None) + media_content_id = self.now_playing["Id"] + media_title = self.now_playing["Name"] + media_duration = int(self.now_playing["RunTimeTicks"] / 10000000) + + if media_content_type == MediaType.EPISODE: + media_content_type = MediaType.TVSHOW + media_series_title = self.now_playing.get("SeriesName") + media_season = self.now_playing.get("ParentIndexNumber") + media_episode = self.now_playing.get("IndexNumber") + elif media_content_type == MediaType.MUSIC: + media_album_name = self.now_playing.get("Album") + media_album_artist = self.now_playing.get("AlbumArtist") + media_track = self.now_playing.get("IndexNumber") + if media_artists := self.now_playing.get("Artists"): + media_artist = str(media_artists[0]) + + if self.play_state is not None: + if self.play_state.get("IsPaused"): + state = MediaPlayerState.PAUSED + + media_position = ( + int(self.play_state["PositionTicks"] / 10000000) + if "PositionTicks" in self.play_state + else None + ) + volume_muted = bool(self.play_state.get("IsMuted", False)) + volume_level = ( + float(self.play_state["VolumeLevel"] / 100) + if "VolumeLevel" in self.play_state + else None + ) + + self._attr_state = state + self._attr_is_volume_muted = volume_muted + self._attr_volume_level = volume_level + self._attr_media_content_type = media_content_type + self._attr_media_content_id = media_content_id + self._attr_media_title = media_title + self._attr_media_series_title = media_series_title + self._attr_media_season = media_season + self._attr_media_episode = media_episode + self._attr_media_album_name = media_album_name + self._attr_media_album_artist = media_album_artist + self._attr_media_artist = media_artist + self._attr_media_track = media_track + self._attr_media_duration = media_duration + self._attr_media_position = media_position + self._attr_media_position_updated_at = media_position_updated + self._attr_media_image_remotely_accessible = True + + @property + def media_image_url(self) -> str | None: + """Image url of current playing media.""" + # We always need the now playing item. + # If there is none, there's also no url + if self.now_playing is None: + return None + + return get_artwork_url(self.coordinator.api_client, self.now_playing, 150) + + @property + def supported_features(self) -> int: + """Flag media player features that are supported.""" + commands: list[str] = self.capabilities.get("SupportedCommands", []) + controllable = self.capabilities.get("SupportsMediaControl", False) + features = 0 + + if controllable: + features |= ( + MediaPlayerEntityFeature.BROWSE_MEDIA + | MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.PAUSE + | MediaPlayerEntityFeature.PLAY + | MediaPlayerEntityFeature.STOP + | MediaPlayerEntityFeature.SEEK + ) + + if "Mute" in commands: + features |= MediaPlayerEntityFeature.VOLUME_MUTE + + if "VolumeSet" in commands: + features |= MediaPlayerEntityFeature.VOLUME_SET + + return features + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self.coordinator.last_update_success and self.session_data is not None + + def media_seek(self, position: float) -> None: + """Send seek command.""" + self.coordinator.api_client.jellyfin.remote_seek( + self.session_id, int(position * 10000000) + ) + + def media_pause(self) -> None: + """Send pause command.""" + self.coordinator.api_client.jellyfin.remote_pause(self.session_id) + self._attr_state = MediaPlayerState.PAUSED + + def media_play(self) -> None: + """Send play command.""" + self.coordinator.api_client.jellyfin.remote_unpause(self.session_id) + self._attr_state = MediaPlayerState.PLAYING + + def media_play_pause(self) -> None: + """Send the PlayPause command to the session.""" + self.coordinator.api_client.jellyfin.remote_playpause(self.session_id) + + def media_stop(self) -> None: + """Send stop command.""" + self.coordinator.api_client.jellyfin.remote_stop(self.session_id) + self._attr_state = MediaPlayerState.IDLE + + def play_media( + self, media_type: str, media_id: str, **kwargs: dict[str, Any] + ) -> None: + """Play a piece of media.""" + self.coordinator.api_client.jellyfin.remote_play_media( + self.session_id, [media_id] + ) + + def set_volume_level(self, volume: float) -> None: + """Set volume level, range 0..1.""" + self.coordinator.api_client.jellyfin.remote_set_volume( + self.session_id, int(volume * 100) + ) + + def mute_volume(self, mute: bool) -> None: + """Mute the volume.""" + if mute: + self.coordinator.api_client.jellyfin.remote_mute(self.session_id) + else: + self.coordinator.api_client.jellyfin.remote_unmute(self.session_id) + + async def async_browse_media( + self, media_content_type: str | None = None, media_content_id: str | None = None + ) -> BrowseMedia: + """Return a BrowseMedia instance. + + The BrowseMedia instance will be used by the "media_player/browse_media" websocket command. + + """ + if media_content_id is None or media_content_id == "media-source://jellyfin": + return await build_root_response( + self.hass, self.coordinator.api_client, self.coordinator.user_id + ) + + return await build_item_response( + self.hass, + self.coordinator.api_client, + self.coordinator.user_id, + media_content_type, + media_content_id, + ) diff --git a/homeassistant/components/jellyfin/models.py b/homeassistant/components/jellyfin/models.py index 913e40e14d5..b6365042127 100644 --- a/homeassistant/components/jellyfin/models.py +++ b/homeassistant/components/jellyfin/models.py @@ -12,5 +12,6 @@ from .coordinator import JellyfinDataUpdateCoordinator class JellyfinData: """Data for the Jellyfin integration.""" + client_device_id: str jellyfin_client: JellyfinClient coordinators: dict[str, JellyfinDataUpdateCoordinator] diff --git a/tests/components/jellyfin/conftest.py b/tests/components/jellyfin/conftest.py index 65b66e5b663..423e4ad3950 100644 --- a/tests/components/jellyfin/conftest.py +++ b/tests/components/jellyfin/conftest.py @@ -73,6 +73,12 @@ def mock_api() -> MagicMock: jf_api.get_user_settings.return_value = load_json_fixture("get-user-settings.json") jf_api.sessions.return_value = load_json_fixture("sessions.json") + jf_api.artwork.side_effect = api_artwork_side_effect + jf_api.user_items.side_effect = api_user_items_side_effect + jf_api.get_item.side_effect = api_get_item_side_effect + jf_api.get_media_folders.return_value = load_json_fixture("get-media-folders.json") + jf_api.user_items.side_effect = api_user_items_side_effect + return jf_api @@ -121,3 +127,27 @@ async def init_integration( await hass.async_block_till_done() return mock_config_entry + + +def api_artwork_side_effect(*args, **kwargs): + """Handle variable responses for artwork method.""" + item_id = args[0] + art = args[1] + ext = "jpg" + + return f"http://localhost/Items/{item_id}/Images/{art}.{ext}" + + +def api_get_item_side_effect(*args): + """Handle variable responses for get_item method.""" + return load_json_fixture("get-item-collection.json") + + +def api_user_items_side_effect(*args, **kwargs): + """Handle variable responses for items method.""" + params = kwargs.get("params", {}) if kwargs else {} + + if "parentId" in params: + return load_json_fixture("user-items-parent-id.json") + + return load_json_fixture("user-items.json") diff --git a/tests/components/jellyfin/fixtures/get-item-collection.json b/tests/components/jellyfin/fixtures/get-item-collection.json new file mode 100644 index 00000000000..90ad63a39e4 --- /dev/null +++ b/tests/components/jellyfin/fixtures/get-item-collection.json @@ -0,0 +1,504 @@ +{ + "Name": "FOLDER", + "OriginalTitle": "string", + "ServerId": "SERVER-UUID", + "Id": "FOLDER-UUID", + "Etag": "string", + "SourceType": "string", + "PlaylistItemId": "string", + "DateCreated": "2019-08-24T14:15:22Z", + "DateLastMediaAdded": "2019-08-24T14:15:22Z", + "ExtraType": "string", + "AirsBeforeSeasonNumber": 0, + "AirsAfterSeasonNumber": 0, + "AirsBeforeEpisodeNumber": 0, + "CanDelete": true, + "CanDownload": true, + "HasSubtitles": true, + "PreferredMetadataLanguage": "string", + "PreferredMetadataCountryCode": "string", + "SupportsSync": true, + "Container": "string", + "SortName": "string", + "ForcedSortName": "string", + "Video3DFormat": "HalfSideBySide", + "PremiereDate": "2019-08-24T14:15:22Z", + "ExternalUrls": [ + { + "Name": "string", + "Url": "string" + } + ], + "MediaSources": [ + { + "Protocol": "File", + "Id": "string", + "Path": "string", + "EncoderPath": "string", + "EncoderProtocol": "File", + "Type": "Default", + "Container": "string", + "Size": 0, + "Name": "string", + "IsRemote": true, + "ETag": "string", + "RunTimeTicks": 0, + "ReadAtNativeFramerate": true, + "IgnoreDts": true, + "IgnoreIndex": true, + "GenPtsInput": true, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "IsInfiniteStream": true, + "RequiresOpening": true, + "OpenToken": "string", + "RequiresClosing": true, + "LiveStreamId": "string", + "BufferMs": 0, + "RequiresLooping": true, + "SupportsProbing": true, + "VideoType": "VideoFile", + "IsoType": "Dvd", + "Video3DFormat": "HalfSideBySide", + "MediaStreams": [ + { + "Codec": "string", + "CodecTag": "string", + "Language": "string", + "ColorRange": "string", + "ColorSpace": "string", + "ColorTransfer": "string", + "ColorPrimaries": "string", + "DvVersionMajor": 0, + "DvVersionMinor": 0, + "DvProfile": 0, + "DvLevel": 0, + "RpuPresentFlag": 0, + "ElPresentFlag": 0, + "BlPresentFlag": 0, + "DvBlSignalCompatibilityId": 0, + "Comment": "string", + "TimeBase": "string", + "CodecTimeBase": "string", + "Title": "string", + "VideoRange": "string", + "VideoRangeType": "string", + "VideoDoViTitle": "string", + "LocalizedUndefined": "string", + "LocalizedDefault": "string", + "LocalizedForced": "string", + "LocalizedExternal": "string", + "DisplayTitle": "string", + "NalLengthSize": "string", + "IsInterlaced": true, + "IsAVC": true, + "ChannelLayout": "string", + "BitRate": 0, + "BitDepth": 0, + "RefFrames": 0, + "PacketLength": 0, + "Channels": 0, + "SampleRate": 0, + "IsDefault": true, + "IsForced": true, + "Height": 0, + "Width": 0, + "AverageFrameRate": 0, + "RealFrameRate": 0, + "Profile": "string", + "Type": "Audio", + "AspectRatio": "string", + "Index": 0, + "Score": 0, + "IsExternal": true, + "DeliveryMethod": "Encode", + "DeliveryUrl": "string", + "IsExternalUrl": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "string", + "PixelFormat": "string", + "Level": 0, + "IsAnamorphic": true + } + ], + "MediaAttachments": [ + { + "Codec": "string", + "CodecTag": "string", + "Comment": "string", + "Index": 0, + "FileName": "string", + "MimeType": "string", + "DeliveryUrl": "string" + } + ], + "Formats": ["string"], + "Bitrate": 0, + "Timestamp": "None", + "RequiredHttpHeaders": { + "property1": "string", + "property2": "string" + }, + "TranscodingUrl": "string", + "TranscodingSubProtocol": "string", + "TranscodingContainer": "string", + "AnalyzeDurationMs": 0, + "DefaultAudioStreamIndex": 0, + "DefaultSubtitleStreamIndex": 0 + } + ], + "CriticRating": 0, + "ProductionLocations": ["string"], + "Path": "string", + "EnableMediaSourceDisplay": true, + "OfficialRating": "string", + "CustomRating": "string", + "ChannelId": "04b0b2a5-93cb-474d-8ea9-3df0f84eb0ff", + "ChannelName": "string", + "Overview": "string", + "Taglines": ["string"], + "Genres": ["string"], + "CommunityRating": 0, + "CumulativeRunTimeTicks": 0, + "RunTimeTicks": 0, + "PlayAccess": "Full", + "AspectRatio": "string", + "ProductionYear": 0, + "IsPlaceHolder": true, + "Number": "string", + "ChannelNumber": "string", + "IndexNumber": 0, + "IndexNumberEnd": 0, + "ParentIndexNumber": 0, + "RemoteTrailers": [ + { + "Url": "string", + "Name": "string" + } + ], + "ProviderIds": { + "property1": "string", + "property2": "string" + }, + "IsHD": true, + "IsFolder": true, + "ParentId": "c54e2d15-b5eb-48b7-9b04-53f376904b1e", + "Type": "CollectionFolder", + "People": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43", + "Role": "string", + "Type": "string", + "PrimaryImageTag": "string", + "ImageBlurHashes": { + "Primary": { + "property1": "string", + "property2": "string" + }, + "Art": { + "property1": "string", + "property2": "string" + }, + "Backdrop": { + "property1": "string", + "property2": "string" + }, + "Banner": { + "property1": "string", + "property2": "string" + }, + "Logo": { + "property1": "string", + "property2": "string" + }, + "Thumb": { + "property1": "string", + "property2": "string" + }, + "Disc": { + "property1": "string", + "property2": "string" + }, + "Box": { + "property1": "string", + "property2": "string" + }, + "Screenshot": { + "property1": "string", + "property2": "string" + }, + "Menu": { + "property1": "string", + "property2": "string" + }, + "Chapter": { + "property1": "string", + "property2": "string" + }, + "BoxRear": { + "property1": "string", + "property2": "string" + }, + "Profile": { + "property1": "string", + "property2": "string" + } + } + } + ], + "Studios": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "GenreItems": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "ParentLogoItemId": "c78d400f-de5c-421e-8714-4fb05d387233", + "ParentBackdropItemId": "c22fd826-17fc-44f4-9b04-1eb3e8fb9173", + "ParentBackdropImageTags": ["string"], + "LocalTrailerCount": 0, + "UserData": { + "Rating": 0, + "PlayedPercentage": 0, + "UnplayedItemCount": 0, + "PlaybackPositionTicks": 0, + "PlayCount": 0, + "IsFavorite": true, + "Likes": true, + "LastPlayedDate": "2019-08-24T14:15:22Z", + "Played": true, + "Key": "string", + "ItemId": "string" + }, + "RecursiveItemCount": 0, + "ChildCount": 0, + "SeriesName": "string", + "SeriesId": "c7b70af4-4902-4a7e-95ab-28349b6c7afc", + "SeasonId": "badb6463-e5b7-45c5-8141-71204420ec8f", + "SpecialFeatureCount": 0, + "DisplayPreferencesId": "string", + "Status": "string", + "AirTime": "string", + "AirDays": ["Sunday"], + "Tags": ["string"], + "PrimaryImageAspectRatio": 0, + "Artists": ["string"], + "ArtistItems": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "Album": "string", + "CollectionType": "string", + "DisplayOrder": "string", + "AlbumId": "21af9851-8e39-43a9-9c47-513d3b9e99fc", + "AlbumPrimaryImageTag": "string", + "SeriesPrimaryImageTag": "string", + "AlbumArtist": "string", + "AlbumArtists": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "SeasonName": "string", + "MediaStreams": [ + { + "Codec": "string", + "CodecTag": "string", + "Language": "string", + "ColorRange": "string", + "ColorSpace": "string", + "ColorTransfer": "string", + "ColorPrimaries": "string", + "DvVersionMajor": 0, + "DvVersionMinor": 0, + "DvProfile": 0, + "DvLevel": 0, + "RpuPresentFlag": 0, + "ElPresentFlag": 0, + "BlPresentFlag": 0, + "DvBlSignalCompatibilityId": 0, + "Comment": "string", + "TimeBase": "string", + "CodecTimeBase": "string", + "Title": "string", + "VideoRange": "string", + "VideoRangeType": "string", + "VideoDoViTitle": "string", + "LocalizedUndefined": "string", + "LocalizedDefault": "string", + "LocalizedForced": "string", + "LocalizedExternal": "string", + "DisplayTitle": "string", + "NalLengthSize": "string", + "IsInterlaced": true, + "IsAVC": true, + "ChannelLayout": "string", + "BitRate": 0, + "BitDepth": 0, + "RefFrames": 0, + "PacketLength": 0, + "Channels": 0, + "SampleRate": 0, + "IsDefault": true, + "IsForced": true, + "Height": 0, + "Width": 0, + "AverageFrameRate": 0, + "RealFrameRate": 0, + "Profile": "string", + "Type": "Audio", + "AspectRatio": "string", + "Index": 0, + "Score": 0, + "IsExternal": true, + "DeliveryMethod": "Encode", + "DeliveryUrl": "string", + "IsExternalUrl": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "string", + "PixelFormat": "string", + "Level": 0, + "IsAnamorphic": true + } + ], + "VideoType": "VideoFile", + "PartCount": 0, + "MediaSourceCount": 0, + "ImageTags": { + "property1": "string", + "property2": "string" + }, + "BackdropImageTags": ["string"], + "ScreenshotImageTags": ["string"], + "ParentLogoImageTag": "string", + "ParentArtItemId": "10c1875b-b82c-48e8-bae9-939a5e68dc2f", + "ParentArtImageTag": "string", + "SeriesThumbImageTag": "string", + "ImageBlurHashes": { + "Primary": { + "property1": "string", + "property2": "string" + }, + "Art": { + "property1": "string", + "property2": "string" + }, + "Backdrop": { + "property1": "string", + "property2": "string" + }, + "Banner": { + "property1": "string", + "property2": "string" + }, + "Logo": { + "property1": "string", + "property2": "string" + }, + "Thumb": { + "property1": "string", + "property2": "string" + }, + "Disc": { + "property1": "string", + "property2": "string" + }, + "Box": { + "property1": "string", + "property2": "string" + }, + "Screenshot": { + "property1": "string", + "property2": "string" + }, + "Menu": { + "property1": "string", + "property2": "string" + }, + "Chapter": { + "property1": "string", + "property2": "string" + }, + "BoxRear": { + "property1": "string", + "property2": "string" + }, + "Profile": { + "property1": "string", + "property2": "string" + } + }, + "SeriesStudio": "string", + "ParentThumbItemId": "ae6ff707-333d-4994-be6d-b83ca1b35f46", + "ParentThumbImageTag": "string", + "ParentPrimaryImageItemId": "string", + "ParentPrimaryImageTag": "string", + "Chapters": [ + { + "StartPositionTicks": 0, + "Name": "string", + "ImagePath": "string", + "ImageDateModified": "2019-08-24T14:15:22Z", + "ImageTag": "string" + } + ], + "LocationType": "FileSystem", + "IsoType": "Dvd", + "MediaType": "string", + "EndDate": "2019-08-24T14:15:22Z", + "LockedFields": ["Cast"], + "TrailerCount": 0, + "MovieCount": 0, + "SeriesCount": 0, + "ProgramCount": 0, + "EpisodeCount": 0, + "SongCount": 0, + "AlbumCount": 0, + "ArtistCount": 0, + "MusicVideoCount": 0, + "LockData": true, + "Width": 0, + "Height": 0, + "CameraMake": "string", + "CameraModel": "string", + "Software": "string", + "ExposureTime": 0, + "FocalLength": 0, + "ImageOrientation": "TopLeft", + "Aperture": 0, + "ShutterSpeed": 0, + "Latitude": 0, + "Longitude": 0, + "Altitude": 0, + "IsoSpeedRating": 0, + "SeriesTimerId": "string", + "ProgramId": "string", + "ChannelPrimaryImageTag": "string", + "StartDate": "2019-08-24T14:15:22Z", + "CompletionPercentage": 0, + "IsRepeat": true, + "EpisodeTitle": "string", + "ChannelType": "TV", + "Audio": "Mono", + "IsMovie": true, + "IsSports": true, + "IsSeries": true, + "IsLive": true, + "IsNews": true, + "IsKids": true, + "IsPremiere": true, + "TimerId": "string", + "CurrentProgram": {} +} diff --git a/tests/components/jellyfin/fixtures/get-media-folders.json b/tests/components/jellyfin/fixtures/get-media-folders.json new file mode 100644 index 00000000000..ff87751a9da --- /dev/null +++ b/tests/components/jellyfin/fixtures/get-media-folders.json @@ -0,0 +1,510 @@ +{ + "Items": [ + { + "Name": "COLLECTION FOLDER", + "OriginalTitle": "string", + "ServerId": "SERVER-UUID", + "Id": "COLLECTION-FOLDER-UUID", + "Etag": "string", + "SourceType": "string", + "PlaylistItemId": "string", + "DateCreated": "2019-08-24T14:15:22Z", + "DateLastMediaAdded": "2019-08-24T14:15:22Z", + "ExtraType": "string", + "AirsBeforeSeasonNumber": 0, + "AirsAfterSeasonNumber": 0, + "AirsBeforeEpisodeNumber": 0, + "CanDelete": true, + "CanDownload": true, + "HasSubtitles": true, + "PreferredMetadataLanguage": "string", + "PreferredMetadataCountryCode": "string", + "SupportsSync": true, + "Container": "string", + "SortName": "string", + "ForcedSortName": "string", + "Video3DFormat": "HalfSideBySide", + "PremiereDate": "2019-08-24T14:15:22Z", + "ExternalUrls": [ + { + "Name": "string", + "Url": "string" + } + ], + "MediaSources": [ + { + "Protocol": "File", + "Id": "string", + "Path": "string", + "EncoderPath": "string", + "EncoderProtocol": "File", + "Type": "Default", + "Container": "string", + "Size": 0, + "Name": "string", + "IsRemote": true, + "ETag": "string", + "RunTimeTicks": 0, + "ReadAtNativeFramerate": true, + "IgnoreDts": true, + "IgnoreIndex": true, + "GenPtsInput": true, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "IsInfiniteStream": true, + "RequiresOpening": true, + "OpenToken": "string", + "RequiresClosing": true, + "LiveStreamId": "string", + "BufferMs": 0, + "RequiresLooping": true, + "SupportsProbing": true, + "VideoType": "VideoFile", + "IsoType": "Dvd", + "Video3DFormat": "HalfSideBySide", + "MediaStreams": [ + { + "Codec": "string", + "CodecTag": "string", + "Language": "string", + "ColorRange": "string", + "ColorSpace": "string", + "ColorTransfer": "string", + "ColorPrimaries": "string", + "DvVersionMajor": 0, + "DvVersionMinor": 0, + "DvProfile": 0, + "DvLevel": 0, + "RpuPresentFlag": 0, + "ElPresentFlag": 0, + "BlPresentFlag": 0, + "DvBlSignalCompatibilityId": 0, + "Comment": "string", + "TimeBase": "string", + "CodecTimeBase": "string", + "Title": "string", + "VideoRange": "string", + "VideoRangeType": "string", + "VideoDoViTitle": "string", + "LocalizedUndefined": "string", + "LocalizedDefault": "string", + "LocalizedForced": "string", + "LocalizedExternal": "string", + "DisplayTitle": "string", + "NalLengthSize": "string", + "IsInterlaced": true, + "IsAVC": true, + "ChannelLayout": "string", + "BitRate": 0, + "BitDepth": 0, + "RefFrames": 0, + "PacketLength": 0, + "Channels": 0, + "SampleRate": 0, + "IsDefault": true, + "IsForced": true, + "Height": 0, + "Width": 0, + "AverageFrameRate": 0, + "RealFrameRate": 0, + "Profile": "string", + "Type": "Audio", + "AspectRatio": "string", + "Index": 0, + "Score": 0, + "IsExternal": true, + "DeliveryMethod": "Encode", + "DeliveryUrl": "string", + "IsExternalUrl": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "string", + "PixelFormat": "string", + "Level": 0, + "IsAnamorphic": true + } + ], + "MediaAttachments": [ + { + "Codec": "string", + "CodecTag": "string", + "Comment": "string", + "Index": 0, + "FileName": "string", + "MimeType": "string", + "DeliveryUrl": "string" + } + ], + "Formats": ["string"], + "Bitrate": 0, + "Timestamp": "None", + "RequiredHttpHeaders": { + "property1": "string", + "property2": "string" + }, + "TranscodingUrl": "string", + "TranscodingSubProtocol": "string", + "TranscodingContainer": "string", + "AnalyzeDurationMs": 0, + "DefaultAudioStreamIndex": 0, + "DefaultSubtitleStreamIndex": 0 + } + ], + "CriticRating": 0, + "ProductionLocations": ["string"], + "Path": "string", + "EnableMediaSourceDisplay": true, + "OfficialRating": "string", + "CustomRating": "string", + "ChannelId": "04b0b2a5-93cb-474d-8ea9-3df0f84eb0ff", + "ChannelName": "string", + "Overview": "string", + "Taglines": ["string"], + "Genres": ["string"], + "CommunityRating": 0, + "CumulativeRunTimeTicks": 0, + "RunTimeTicks": 0, + "PlayAccess": "Full", + "AspectRatio": "string", + "ProductionYear": 0, + "IsPlaceHolder": true, + "Number": "string", + "ChannelNumber": "string", + "IndexNumber": 0, + "IndexNumberEnd": 0, + "ParentIndexNumber": 0, + "RemoteTrailers": [ + { + "Url": "string", + "Name": "string" + } + ], + "ProviderIds": { + "property1": "string", + "property2": "string" + }, + "IsHD": true, + "IsFolder": true, + "ParentId": "c54e2d15-b5eb-48b7-9b04-53f376904b1e", + "Type": "CollectionFolder", + "People": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43", + "Role": "string", + "Type": "string", + "PrimaryImageTag": "string", + "ImageBlurHashes": { + "Primary": { + "property1": "string", + "property2": "string" + }, + "Art": { + "property1": "string", + "property2": "string" + }, + "Backdrop": { + "property1": "string", + "property2": "string" + }, + "Banner": { + "property1": "string", + "property2": "string" + }, + "Logo": { + "property1": "string", + "property2": "string" + }, + "Thumb": { + "property1": "string", + "property2": "string" + }, + "Disc": { + "property1": "string", + "property2": "string" + }, + "Box": { + "property1": "string", + "property2": "string" + }, + "Screenshot": { + "property1": "string", + "property2": "string" + }, + "Menu": { + "property1": "string", + "property2": "string" + }, + "Chapter": { + "property1": "string", + "property2": "string" + }, + "BoxRear": { + "property1": "string", + "property2": "string" + }, + "Profile": { + "property1": "string", + "property2": "string" + } + } + } + ], + "Studios": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "GenreItems": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "ParentLogoItemId": "c78d400f-de5c-421e-8714-4fb05d387233", + "ParentBackdropItemId": "c22fd826-17fc-44f4-9b04-1eb3e8fb9173", + "ParentBackdropImageTags": ["string"], + "LocalTrailerCount": 0, + "UserData": { + "Rating": 0, + "PlayedPercentage": 0, + "UnplayedItemCount": 0, + "PlaybackPositionTicks": 0, + "PlayCount": 0, + "IsFavorite": true, + "Likes": true, + "LastPlayedDate": "2019-08-24T14:15:22Z", + "Played": true, + "Key": "string", + "ItemId": "string" + }, + "RecursiveItemCount": 0, + "ChildCount": 0, + "SeriesName": "string", + "SeriesId": "SERIES-UUID", + "SeasonId": "SEASON-UUID", + "SpecialFeatureCount": 0, + "DisplayPreferencesId": "string", + "Status": "string", + "AirTime": "string", + "AirDays": ["Sunday"], + "Tags": ["string"], + "PrimaryImageAspectRatio": 0, + "Artists": ["string"], + "ArtistItems": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "Album": "string", + "CollectionType": "tvshows", + "DisplayOrder": "string", + "AlbumId": "21af9851-8e39-43a9-9c47-513d3b9e99fc", + "AlbumPrimaryImageTag": "string", + "SeriesPrimaryImageTag": "string", + "AlbumArtist": "string", + "AlbumArtists": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "SeasonName": "string", + "MediaStreams": [ + { + "Codec": "string", + "CodecTag": "string", + "Language": "string", + "ColorRange": "string", + "ColorSpace": "string", + "ColorTransfer": "string", + "ColorPrimaries": "string", + "DvVersionMajor": 0, + "DvVersionMinor": 0, + "DvProfile": 0, + "DvLevel": 0, + "RpuPresentFlag": 0, + "ElPresentFlag": 0, + "BlPresentFlag": 0, + "DvBlSignalCompatibilityId": 0, + "Comment": "string", + "TimeBase": "string", + "CodecTimeBase": "string", + "Title": "string", + "VideoRange": "string", + "VideoRangeType": "string", + "VideoDoViTitle": "string", + "LocalizedUndefined": "string", + "LocalizedDefault": "string", + "LocalizedForced": "string", + "LocalizedExternal": "string", + "DisplayTitle": "string", + "NalLengthSize": "string", + "IsInterlaced": true, + "IsAVC": true, + "ChannelLayout": "string", + "BitRate": 0, + "BitDepth": 0, + "RefFrames": 0, + "PacketLength": 0, + "Channels": 0, + "SampleRate": 0, + "IsDefault": true, + "IsForced": true, + "Height": 0, + "Width": 0, + "AverageFrameRate": 0, + "RealFrameRate": 0, + "Profile": "string", + "Type": "Audio", + "AspectRatio": "string", + "Index": 0, + "Score": 0, + "IsExternal": true, + "DeliveryMethod": "Encode", + "DeliveryUrl": "string", + "IsExternalUrl": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "string", + "PixelFormat": "string", + "Level": 0, + "IsAnamorphic": true + } + ], + "VideoType": "VideoFile", + "PartCount": 0, + "MediaSourceCount": 0, + "ImageTags": { + "property1": "string", + "property2": "string" + }, + "BackdropImageTags": ["string"], + "ScreenshotImageTags": ["string"], + "ParentLogoImageTag": "string", + "ParentArtItemId": "10c1875b-b82c-48e8-bae9-939a5e68dc2f", + "ParentArtImageTag": "string", + "SeriesThumbImageTag": "string", + "ImageBlurHashes": { + "Primary": { + "property1": "string", + "property2": "string" + }, + "Art": { + "property1": "string", + "property2": "string" + }, + "Backdrop": { + "property1": "string", + "property2": "string" + }, + "Banner": { + "property1": "string", + "property2": "string" + }, + "Logo": { + "property1": "string", + "property2": "string" + }, + "Thumb": { + "property1": "string", + "property2": "string" + }, + "Disc": { + "property1": "string", + "property2": "string" + }, + "Box": { + "property1": "string", + "property2": "string" + }, + "Screenshot": { + "property1": "string", + "property2": "string" + }, + "Menu": { + "property1": "string", + "property2": "string" + }, + "Chapter": { + "property1": "string", + "property2": "string" + }, + "BoxRear": { + "property1": "string", + "property2": "string" + }, + "Profile": { + "property1": "string", + "property2": "string" + } + }, + "SeriesStudio": "string", + "ParentThumbItemId": "ae6ff707-333d-4994-be6d-b83ca1b35f46", + "ParentThumbImageTag": "string", + "ParentPrimaryImageItemId": "string", + "ParentPrimaryImageTag": "string", + "Chapters": [ + { + "StartPositionTicks": 0, + "Name": "string", + "ImagePath": "string", + "ImageDateModified": "2019-08-24T14:15:22Z", + "ImageTag": "string" + } + ], + "LocationType": "FileSystem", + "IsoType": "Dvd", + "MediaType": "string", + "EndDate": "2019-08-24T14:15:22Z", + "LockedFields": ["Cast"], + "TrailerCount": 0, + "MovieCount": 0, + "SeriesCount": 0, + "ProgramCount": 0, + "EpisodeCount": 0, + "SongCount": 0, + "AlbumCount": 0, + "ArtistCount": 0, + "MusicVideoCount": 0, + "LockData": true, + "Width": 0, + "Height": 0, + "CameraMake": "string", + "CameraModel": "string", + "Software": "string", + "ExposureTime": 0, + "FocalLength": 0, + "ImageOrientation": "TopLeft", + "Aperture": 0, + "ShutterSpeed": 0, + "Latitude": 0, + "Longitude": 0, + "Altitude": 0, + "IsoSpeedRating": 0, + "SeriesTimerId": "string", + "ProgramId": "string", + "ChannelPrimaryImageTag": "string", + "StartDate": "2019-08-24T14:15:22Z", + "CompletionPercentage": 0, + "IsRepeat": true, + "EpisodeTitle": "string", + "ChannelType": "TV", + "Audio": "Mono", + "IsMovie": true, + "IsSports": true, + "IsSeries": true, + "IsLive": true, + "IsNews": true, + "IsKids": true, + "IsPremiere": true, + "TimerId": "string", + "CurrentProgram": {} + } + ], + "TotalRecordCount": 0, + "StartIndex": 0 +} diff --git a/tests/components/jellyfin/fixtures/user-items-parent-id.json b/tests/components/jellyfin/fixtures/user-items-parent-id.json new file mode 100644 index 00000000000..2e06c30894c --- /dev/null +++ b/tests/components/jellyfin/fixtures/user-items-parent-id.json @@ -0,0 +1,510 @@ +{ + "Items": [ + { + "Name": "EPISODE", + "OriginalTitle": "string", + "ServerId": "SERVER-UUID", + "Id": "EPISODE-UUID", + "Etag": "string", + "SourceType": "string", + "PlaylistItemId": "string", + "DateCreated": "2019-08-24T14:15:22Z", + "DateLastMediaAdded": "2019-08-24T14:15:22Z", + "ExtraType": "string", + "AirsBeforeSeasonNumber": 0, + "AirsAfterSeasonNumber": 0, + "AirsBeforeEpisodeNumber": 0, + "CanDelete": true, + "CanDownload": true, + "HasSubtitles": true, + "PreferredMetadataLanguage": "string", + "PreferredMetadataCountryCode": "string", + "SupportsSync": true, + "Container": "string", + "SortName": "string", + "ForcedSortName": "string", + "Video3DFormat": "HalfSideBySide", + "PremiereDate": "2019-08-24T14:15:22Z", + "ExternalUrls": [ + { + "Name": "string", + "Url": "string" + } + ], + "MediaSources": [ + { + "Protocol": "File", + "Id": "string", + "Path": "string", + "EncoderPath": "string", + "EncoderProtocol": "File", + "Type": "Default", + "Container": "string", + "Size": 0, + "Name": "string", + "IsRemote": true, + "ETag": "string", + "RunTimeTicks": 0, + "ReadAtNativeFramerate": true, + "IgnoreDts": true, + "IgnoreIndex": true, + "GenPtsInput": true, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "IsInfiniteStream": true, + "RequiresOpening": true, + "OpenToken": "string", + "RequiresClosing": true, + "LiveStreamId": "string", + "BufferMs": 0, + "RequiresLooping": true, + "SupportsProbing": true, + "VideoType": "VideoFile", + "IsoType": "Dvd", + "Video3DFormat": "HalfSideBySide", + "MediaStreams": [ + { + "Codec": "string", + "CodecTag": "string", + "Language": "string", + "ColorRange": "string", + "ColorSpace": "string", + "ColorTransfer": "string", + "ColorPrimaries": "string", + "DvVersionMajor": 0, + "DvVersionMinor": 0, + "DvProfile": 0, + "DvLevel": 0, + "RpuPresentFlag": 0, + "ElPresentFlag": 0, + "BlPresentFlag": 0, + "DvBlSignalCompatibilityId": 0, + "Comment": "string", + "TimeBase": "string", + "CodecTimeBase": "string", + "Title": "string", + "VideoRange": "string", + "VideoRangeType": "string", + "VideoDoViTitle": "string", + "LocalizedUndefined": "string", + "LocalizedDefault": "string", + "LocalizedForced": "string", + "LocalizedExternal": "string", + "DisplayTitle": "string", + "NalLengthSize": "string", + "IsInterlaced": true, + "IsAVC": true, + "ChannelLayout": "string", + "BitRate": 0, + "BitDepth": 0, + "RefFrames": 0, + "PacketLength": 0, + "Channels": 0, + "SampleRate": 0, + "IsDefault": true, + "IsForced": true, + "Height": 0, + "Width": 0, + "AverageFrameRate": 0, + "RealFrameRate": 0, + "Profile": "string", + "Type": "Audio", + "AspectRatio": "string", + "Index": 0, + "Score": 0, + "IsExternal": true, + "DeliveryMethod": "Encode", + "DeliveryUrl": "string", + "IsExternalUrl": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "string", + "PixelFormat": "string", + "Level": 0, + "IsAnamorphic": true + } + ], + "MediaAttachments": [ + { + "Codec": "string", + "CodecTag": "string", + "Comment": "string", + "Index": 0, + "FileName": "string", + "MimeType": "string", + "DeliveryUrl": "string" + } + ], + "Formats": ["string"], + "Bitrate": 0, + "Timestamp": "None", + "RequiredHttpHeaders": { + "property1": "string", + "property2": "string" + }, + "TranscodingUrl": "string", + "TranscodingSubProtocol": "string", + "TranscodingContainer": "string", + "AnalyzeDurationMs": 0, + "DefaultAudioStreamIndex": 0, + "DefaultSubtitleStreamIndex": 0 + } + ], + "CriticRating": 0, + "ProductionLocations": ["string"], + "Path": "string", + "EnableMediaSourceDisplay": true, + "OfficialRating": "string", + "CustomRating": "string", + "ChannelId": "04b0b2a5-93cb-474d-8ea9-3df0f84eb0ff", + "ChannelName": "string", + "Overview": "string", + "Taglines": ["string"], + "Genres": ["string"], + "CommunityRating": 0, + "CumulativeRunTimeTicks": 0, + "RunTimeTicks": 0, + "PlayAccess": "Full", + "AspectRatio": "string", + "ProductionYear": 0, + "IsPlaceHolder": true, + "Number": "string", + "ChannelNumber": "string", + "IndexNumber": 0, + "IndexNumberEnd": 0, + "ParentIndexNumber": 0, + "RemoteTrailers": [ + { + "Url": "string", + "Name": "string" + } + ], + "ProviderIds": { + "property1": "string", + "property2": "string" + }, + "IsHD": true, + "IsFolder": false, + "ParentId": "FOLDER-UUID", + "Type": "Episode", + "People": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43", + "Role": "string", + "Type": "string", + "PrimaryImageTag": "string", + "ImageBlurHashes": { + "Primary": { + "property1": "string", + "property2": "string" + }, + "Art": { + "property1": "string", + "property2": "string" + }, + "Backdrop": { + "property1": "string", + "property2": "string" + }, + "Banner": { + "property1": "string", + "property2": "string" + }, + "Logo": { + "property1": "string", + "property2": "string" + }, + "Thumb": { + "property1": "string", + "property2": "string" + }, + "Disc": { + "property1": "string", + "property2": "string" + }, + "Box": { + "property1": "string", + "property2": "string" + }, + "Screenshot": { + "property1": "string", + "property2": "string" + }, + "Menu": { + "property1": "string", + "property2": "string" + }, + "Chapter": { + "property1": "string", + "property2": "string" + }, + "BoxRear": { + "property1": "string", + "property2": "string" + }, + "Profile": { + "property1": "string", + "property2": "string" + } + } + } + ], + "Studios": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "GenreItems": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "ParentLogoItemId": "c78d400f-de5c-421e-8714-4fb05d387233", + "ParentBackdropItemId": "c22fd826-17fc-44f4-9b04-1eb3e8fb9173", + "ParentBackdropImageTags": ["string"], + "LocalTrailerCount": 0, + "UserData": { + "Rating": 0, + "PlayedPercentage": 0, + "UnplayedItemCount": 0, + "PlaybackPositionTicks": 0, + "PlayCount": 0, + "IsFavorite": true, + "Likes": true, + "LastPlayedDate": "2019-08-24T14:15:22Z", + "Played": true, + "Key": "string", + "ItemId": "string" + }, + "RecursiveItemCount": 0, + "ChildCount": 0, + "SeriesName": "string", + "SeriesId": "c7b70af4-4902-4a7e-95ab-28349b6c7afc", + "SeasonId": "badb6463-e5b7-45c5-8141-71204420ec8f", + "SpecialFeatureCount": 0, + "DisplayPreferencesId": "string", + "Status": "string", + "AirTime": "string", + "AirDays": ["Sunday"], + "Tags": ["string"], + "PrimaryImageAspectRatio": 0, + "Artists": ["string"], + "ArtistItems": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "Album": "string", + "CollectionType": "string", + "DisplayOrder": "string", + "AlbumId": "21af9851-8e39-43a9-9c47-513d3b9e99fc", + "AlbumPrimaryImageTag": "string", + "SeriesPrimaryImageTag": "string", + "AlbumArtist": "string", + "AlbumArtists": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "SeasonName": "string", + "MediaStreams": [ + { + "Codec": "string", + "CodecTag": "string", + "Language": "string", + "ColorRange": "string", + "ColorSpace": "string", + "ColorTransfer": "string", + "ColorPrimaries": "string", + "DvVersionMajor": 0, + "DvVersionMinor": 0, + "DvProfile": 0, + "DvLevel": 0, + "RpuPresentFlag": 0, + "ElPresentFlag": 0, + "BlPresentFlag": 0, + "DvBlSignalCompatibilityId": 0, + "Comment": "string", + "TimeBase": "string", + "CodecTimeBase": "string", + "Title": "string", + "VideoRange": "string", + "VideoRangeType": "string", + "VideoDoViTitle": "string", + "LocalizedUndefined": "string", + "LocalizedDefault": "string", + "LocalizedForced": "string", + "LocalizedExternal": "string", + "DisplayTitle": "string", + "NalLengthSize": "string", + "IsInterlaced": true, + "IsAVC": true, + "ChannelLayout": "string", + "BitRate": 0, + "BitDepth": 0, + "RefFrames": 0, + "PacketLength": 0, + "Channels": 0, + "SampleRate": 0, + "IsDefault": true, + "IsForced": true, + "Height": 0, + "Width": 0, + "AverageFrameRate": 0, + "RealFrameRate": 0, + "Profile": "string", + "Type": "Audio", + "AspectRatio": "string", + "Index": 0, + "Score": 0, + "IsExternal": true, + "DeliveryMethod": "Encode", + "DeliveryUrl": "string", + "IsExternalUrl": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "string", + "PixelFormat": "string", + "Level": 0, + "IsAnamorphic": true + } + ], + "VideoType": "VideoFile", + "PartCount": 0, + "MediaSourceCount": 0, + "ImageTags": { + "property1": "string", + "property2": "string" + }, + "BackdropImageTags": ["string"], + "ScreenshotImageTags": ["string"], + "ParentLogoImageTag": "string", + "ParentArtItemId": "10c1875b-b82c-48e8-bae9-939a5e68dc2f", + "ParentArtImageTag": "string", + "SeriesThumbImageTag": "string", + "ImageBlurHashes": { + "Primary": { + "property1": "string", + "property2": "string" + }, + "Art": { + "property1": "string", + "property2": "string" + }, + "Backdrop": { + "property1": "string", + "property2": "string" + }, + "Banner": { + "property1": "string", + "property2": "string" + }, + "Logo": { + "property1": "string", + "property2": "string" + }, + "Thumb": { + "property1": "string", + "property2": "string" + }, + "Disc": { + "property1": "string", + "property2": "string" + }, + "Box": { + "property1": "string", + "property2": "string" + }, + "Screenshot": { + "property1": "string", + "property2": "string" + }, + "Menu": { + "property1": "string", + "property2": "string" + }, + "Chapter": { + "property1": "string", + "property2": "string" + }, + "BoxRear": { + "property1": "string", + "property2": "string" + }, + "Profile": { + "property1": "string", + "property2": "string" + } + }, + "SeriesStudio": "string", + "ParentThumbItemId": "ae6ff707-333d-4994-be6d-b83ca1b35f46", + "ParentThumbImageTag": "string", + "ParentPrimaryImageItemId": "string", + "ParentPrimaryImageTag": "string", + "Chapters": [ + { + "StartPositionTicks": 0, + "Name": "string", + "ImagePath": "string", + "ImageDateModified": "2019-08-24T14:15:22Z", + "ImageTag": "string" + } + ], + "LocationType": "FileSystem", + "IsoType": "Dvd", + "MediaType": "string", + "EndDate": "2019-08-24T14:15:22Z", + "LockedFields": ["Cast"], + "TrailerCount": 0, + "MovieCount": 0, + "SeriesCount": 0, + "ProgramCount": 0, + "EpisodeCount": 0, + "SongCount": 0, + "AlbumCount": 0, + "ArtistCount": 0, + "MusicVideoCount": 0, + "LockData": true, + "Width": 0, + "Height": 0, + "CameraMake": "string", + "CameraModel": "string", + "Software": "string", + "ExposureTime": 0, + "FocalLength": 0, + "ImageOrientation": "TopLeft", + "Aperture": 0, + "ShutterSpeed": 0, + "Latitude": 0, + "Longitude": 0, + "Altitude": 0, + "IsoSpeedRating": 0, + "SeriesTimerId": "string", + "ProgramId": "string", + "ChannelPrimaryImageTag": "string", + "StartDate": "2019-08-24T14:15:22Z", + "CompletionPercentage": 0, + "IsRepeat": true, + "EpisodeTitle": "string", + "ChannelType": "TV", + "Audio": "Mono", + "IsMovie": true, + "IsSports": true, + "IsSeries": true, + "IsLive": true, + "IsNews": true, + "IsKids": true, + "IsPremiere": true, + "TimerId": "string", + "CurrentProgram": {} + } + ], + "TotalRecordCount": 0, + "StartIndex": 0 +} diff --git a/tests/components/jellyfin/fixtures/user-items.json b/tests/components/jellyfin/fixtures/user-items.json new file mode 100644 index 00000000000..7461626de18 --- /dev/null +++ b/tests/components/jellyfin/fixtures/user-items.json @@ -0,0 +1,510 @@ +{ + "Items": [ + { + "Name": "FOLDER", + "OriginalTitle": "string", + "ServerId": "SERVER-UUID", + "Id": "FOLDER-UUID", + "Etag": "string", + "SourceType": "string", + "PlaylistItemId": "string", + "DateCreated": "2019-08-24T14:15:22Z", + "DateLastMediaAdded": "2019-08-24T14:15:22Z", + "ExtraType": "string", + "AirsBeforeSeasonNumber": 0, + "AirsAfterSeasonNumber": 0, + "AirsBeforeEpisodeNumber": 0, + "CanDelete": true, + "CanDownload": true, + "HasSubtitles": true, + "PreferredMetadataLanguage": "string", + "PreferredMetadataCountryCode": "string", + "SupportsSync": true, + "Container": "string", + "SortName": "string", + "ForcedSortName": "string", + "Video3DFormat": "HalfSideBySide", + "PremiereDate": "2019-08-24T14:15:22Z", + "ExternalUrls": [ + { + "Name": "string", + "Url": "string" + } + ], + "MediaSources": [ + { + "Protocol": "File", + "Id": "string", + "Path": "string", + "EncoderPath": "string", + "EncoderProtocol": "File", + "Type": "Default", + "Container": "string", + "Size": 0, + "Name": "string", + "IsRemote": true, + "ETag": "string", + "RunTimeTicks": 0, + "ReadAtNativeFramerate": true, + "IgnoreDts": true, + "IgnoreIndex": true, + "GenPtsInput": true, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "IsInfiniteStream": true, + "RequiresOpening": true, + "OpenToken": "string", + "RequiresClosing": true, + "LiveStreamId": "string", + "BufferMs": 0, + "RequiresLooping": true, + "SupportsProbing": true, + "VideoType": "VideoFile", + "IsoType": "Dvd", + "Video3DFormat": "HalfSideBySide", + "MediaStreams": [ + { + "Codec": "string", + "CodecTag": "string", + "Language": "string", + "ColorRange": "string", + "ColorSpace": "string", + "ColorTransfer": "string", + "ColorPrimaries": "string", + "DvVersionMajor": 0, + "DvVersionMinor": 0, + "DvProfile": 0, + "DvLevel": 0, + "RpuPresentFlag": 0, + "ElPresentFlag": 0, + "BlPresentFlag": 0, + "DvBlSignalCompatibilityId": 0, + "Comment": "string", + "TimeBase": "string", + "CodecTimeBase": "string", + "Title": "string", + "VideoRange": "string", + "VideoRangeType": "string", + "VideoDoViTitle": "string", + "LocalizedUndefined": "string", + "LocalizedDefault": "string", + "LocalizedForced": "string", + "LocalizedExternal": "string", + "DisplayTitle": "string", + "NalLengthSize": "string", + "IsInterlaced": true, + "IsAVC": true, + "ChannelLayout": "string", + "BitRate": 0, + "BitDepth": 0, + "RefFrames": 0, + "PacketLength": 0, + "Channels": 0, + "SampleRate": 0, + "IsDefault": true, + "IsForced": true, + "Height": 0, + "Width": 0, + "AverageFrameRate": 0, + "RealFrameRate": 0, + "Profile": "string", + "Type": "Audio", + "AspectRatio": "string", + "Index": 0, + "Score": 0, + "IsExternal": true, + "DeliveryMethod": "Encode", + "DeliveryUrl": "string", + "IsExternalUrl": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "string", + "PixelFormat": "string", + "Level": 0, + "IsAnamorphic": true + } + ], + "MediaAttachments": [ + { + "Codec": "string", + "CodecTag": "string", + "Comment": "string", + "Index": 0, + "FileName": "string", + "MimeType": "string", + "DeliveryUrl": "string" + } + ], + "Formats": ["string"], + "Bitrate": 0, + "Timestamp": "None", + "RequiredHttpHeaders": { + "property1": "string", + "property2": "string" + }, + "TranscodingUrl": "string", + "TranscodingSubProtocol": "string", + "TranscodingContainer": "string", + "AnalyzeDurationMs": 0, + "DefaultAudioStreamIndex": 0, + "DefaultSubtitleStreamIndex": 0 + } + ], + "CriticRating": 0, + "ProductionLocations": ["string"], + "Path": "string", + "EnableMediaSourceDisplay": true, + "OfficialRating": "string", + "CustomRating": "string", + "ChannelId": "04b0b2a5-93cb-474d-8ea9-3df0f84eb0ff", + "ChannelName": "string", + "Overview": "string", + "Taglines": ["string"], + "Genres": ["string"], + "CommunityRating": 0, + "CumulativeRunTimeTicks": 0, + "RunTimeTicks": 0, + "PlayAccess": "Full", + "AspectRatio": "string", + "ProductionYear": 0, + "IsPlaceHolder": true, + "Number": "string", + "ChannelNumber": "string", + "IndexNumber": 0, + "IndexNumberEnd": 0, + "ParentIndexNumber": 0, + "RemoteTrailers": [ + { + "Url": "string", + "Name": "string" + } + ], + "ProviderIds": { + "property1": "string", + "property2": "string" + }, + "IsHD": true, + "IsFolder": true, + "ParentId": "c54e2d15-b5eb-48b7-9b04-53f376904b1e", + "Type": "AggregateFolder", + "People": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43", + "Role": "string", + "Type": "string", + "PrimaryImageTag": "string", + "ImageBlurHashes": { + "Primary": { + "property1": "string", + "property2": "string" + }, + "Art": { + "property1": "string", + "property2": "string" + }, + "Backdrop": { + "property1": "string", + "property2": "string" + }, + "Banner": { + "property1": "string", + "property2": "string" + }, + "Logo": { + "property1": "string", + "property2": "string" + }, + "Thumb": { + "property1": "string", + "property2": "string" + }, + "Disc": { + "property1": "string", + "property2": "string" + }, + "Box": { + "property1": "string", + "property2": "string" + }, + "Screenshot": { + "property1": "string", + "property2": "string" + }, + "Menu": { + "property1": "string", + "property2": "string" + }, + "Chapter": { + "property1": "string", + "property2": "string" + }, + "BoxRear": { + "property1": "string", + "property2": "string" + }, + "Profile": { + "property1": "string", + "property2": "string" + } + } + } + ], + "Studios": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "GenreItems": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "ParentLogoItemId": "c78d400f-de5c-421e-8714-4fb05d387233", + "ParentBackdropItemId": "c22fd826-17fc-44f4-9b04-1eb3e8fb9173", + "ParentBackdropImageTags": ["string"], + "LocalTrailerCount": 0, + "UserData": { + "Rating": 0, + "PlayedPercentage": 0, + "UnplayedItemCount": 0, + "PlaybackPositionTicks": 0, + "PlayCount": 0, + "IsFavorite": true, + "Likes": true, + "LastPlayedDate": "2019-08-24T14:15:22Z", + "Played": true, + "Key": "string", + "ItemId": "string" + }, + "RecursiveItemCount": 0, + "ChildCount": 0, + "SeriesName": "string", + "SeriesId": "c7b70af4-4902-4a7e-95ab-28349b6c7afc", + "SeasonId": "badb6463-e5b7-45c5-8141-71204420ec8f", + "SpecialFeatureCount": 0, + "DisplayPreferencesId": "string", + "Status": "string", + "AirTime": "string", + "AirDays": ["Sunday"], + "Tags": ["string"], + "PrimaryImageAspectRatio": 0, + "Artists": ["string"], + "ArtistItems": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "Album": "string", + "CollectionType": "string", + "DisplayOrder": "string", + "AlbumId": "21af9851-8e39-43a9-9c47-513d3b9e99fc", + "AlbumPrimaryImageTag": "string", + "SeriesPrimaryImageTag": "string", + "AlbumArtist": "string", + "AlbumArtists": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "SeasonName": "string", + "MediaStreams": [ + { + "Codec": "string", + "CodecTag": "string", + "Language": "string", + "ColorRange": "string", + "ColorSpace": "string", + "ColorTransfer": "string", + "ColorPrimaries": "string", + "DvVersionMajor": 0, + "DvVersionMinor": 0, + "DvProfile": 0, + "DvLevel": 0, + "RpuPresentFlag": 0, + "ElPresentFlag": 0, + "BlPresentFlag": 0, + "DvBlSignalCompatibilityId": 0, + "Comment": "string", + "TimeBase": "string", + "CodecTimeBase": "string", + "Title": "string", + "VideoRange": "string", + "VideoRangeType": "string", + "VideoDoViTitle": "string", + "LocalizedUndefined": "string", + "LocalizedDefault": "string", + "LocalizedForced": "string", + "LocalizedExternal": "string", + "DisplayTitle": "string", + "NalLengthSize": "string", + "IsInterlaced": true, + "IsAVC": true, + "ChannelLayout": "string", + "BitRate": 0, + "BitDepth": 0, + "RefFrames": 0, + "PacketLength": 0, + "Channels": 0, + "SampleRate": 0, + "IsDefault": true, + "IsForced": true, + "Height": 0, + "Width": 0, + "AverageFrameRate": 0, + "RealFrameRate": 0, + "Profile": "string", + "Type": "Audio", + "AspectRatio": "string", + "Index": 0, + "Score": 0, + "IsExternal": true, + "DeliveryMethod": "Encode", + "DeliveryUrl": "string", + "IsExternalUrl": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "string", + "PixelFormat": "string", + "Level": 0, + "IsAnamorphic": true + } + ], + "VideoType": "VideoFile", + "PartCount": 0, + "MediaSourceCount": 0, + "ImageTags": { + "property1": "string", + "property2": "string" + }, + "BackdropImageTags": ["string"], + "ScreenshotImageTags": ["string"], + "ParentLogoImageTag": "string", + "ParentArtItemId": "10c1875b-b82c-48e8-bae9-939a5e68dc2f", + "ParentArtImageTag": "string", + "SeriesThumbImageTag": "string", + "ImageBlurHashes": { + "Primary": { + "property1": "string", + "property2": "string" + }, + "Art": { + "property1": "string", + "property2": "string" + }, + "Backdrop": { + "property1": "string", + "property2": "string" + }, + "Banner": { + "property1": "string", + "property2": "string" + }, + "Logo": { + "property1": "string", + "property2": "string" + }, + "Thumb": { + "property1": "string", + "property2": "string" + }, + "Disc": { + "property1": "string", + "property2": "string" + }, + "Box": { + "property1": "string", + "property2": "string" + }, + "Screenshot": { + "property1": "string", + "property2": "string" + }, + "Menu": { + "property1": "string", + "property2": "string" + }, + "Chapter": { + "property1": "string", + "property2": "string" + }, + "BoxRear": { + "property1": "string", + "property2": "string" + }, + "Profile": { + "property1": "string", + "property2": "string" + } + }, + "SeriesStudio": "string", + "ParentThumbItemId": "ae6ff707-333d-4994-be6d-b83ca1b35f46", + "ParentThumbImageTag": "string", + "ParentPrimaryImageItemId": "string", + "ParentPrimaryImageTag": "string", + "Chapters": [ + { + "StartPositionTicks": 0, + "Name": "string", + "ImagePath": "string", + "ImageDateModified": "2019-08-24T14:15:22Z", + "ImageTag": "string" + } + ], + "LocationType": "FileSystem", + "IsoType": "Dvd", + "MediaType": "string", + "EndDate": "2019-08-24T14:15:22Z", + "LockedFields": ["Cast"], + "TrailerCount": 0, + "MovieCount": 0, + "SeriesCount": 0, + "ProgramCount": 0, + "EpisodeCount": 0, + "SongCount": 0, + "AlbumCount": 0, + "ArtistCount": 0, + "MusicVideoCount": 0, + "LockData": true, + "Width": 0, + "Height": 0, + "CameraMake": "string", + "CameraModel": "string", + "Software": "string", + "ExposureTime": 0, + "FocalLength": 0, + "ImageOrientation": "TopLeft", + "Aperture": 0, + "ShutterSpeed": 0, + "Latitude": 0, + "Longitude": 0, + "Altitude": 0, + "IsoSpeedRating": 0, + "SeriesTimerId": "string", + "ProgramId": "string", + "ChannelPrimaryImageTag": "string", + "StartDate": "2019-08-24T14:15:22Z", + "CompletionPercentage": 0, + "IsRepeat": true, + "EpisodeTitle": "string", + "ChannelType": "TV", + "Audio": "Mono", + "IsMovie": true, + "IsSports": true, + "IsSeries": true, + "IsLive": true, + "IsNews": true, + "IsKids": true, + "IsPremiere": true, + "TimerId": "string", + "CurrentProgram": {} + } + ], + "TotalRecordCount": 0, + "StartIndex": 0 +} diff --git a/tests/components/jellyfin/test_media_player.py b/tests/components/jellyfin/test_media_player.py new file mode 100644 index 00000000000..40ddfefce39 --- /dev/null +++ b/tests/components/jellyfin/test_media_player.py @@ -0,0 +1,356 @@ +"""Tests for the Jellyfin media_player platform.""" +from datetime import timedelta +from unittest.mock import MagicMock + +from aiohttp import ClientSession + +from homeassistant.components.jellyfin.const import DOMAIN +from homeassistant.components.media_player import ( + ATTR_MEDIA_ALBUM_ARTIST, + ATTR_MEDIA_ALBUM_NAME, + ATTR_MEDIA_ARTIST, + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_CONTENT_TYPE, + ATTR_MEDIA_DURATION, + ATTR_MEDIA_EPISODE, + ATTR_MEDIA_POSITION, + ATTR_MEDIA_POSITION_UPDATED_AT, + ATTR_MEDIA_SEASON, + ATTR_MEDIA_SERIES_TITLE, + ATTR_MEDIA_TRACK, + ATTR_MEDIA_VOLUME_LEVEL, + ATTR_MEDIA_VOLUME_MUTED, + DOMAIN as MP_DOMAIN, + MediaClass, + MediaPlayerState, + MediaType, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_ICON, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.util.dt import utcnow + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_media_player( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_jellyfin: MagicMock, + mock_api: MagicMock, +) -> None: + """Test the Jellyfin media player.""" + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + + state = hass.states.get("media_player.jellyfin_device") + + assert state + assert state.state == MediaPlayerState.PAUSED + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "JELLYFIN-DEVICE" + assert state.attributes.get(ATTR_ICON) is None + assert state.attributes.get(ATTR_MEDIA_VOLUME_LEVEL) == 0.0 + assert state.attributes.get(ATTR_MEDIA_VOLUME_MUTED) is True + assert state.attributes.get(ATTR_MEDIA_DURATION) == 60 + assert state.attributes.get(ATTR_MEDIA_POSITION) == 10 + assert state.attributes.get(ATTR_MEDIA_POSITION_UPDATED_AT) + assert state.attributes.get(ATTR_MEDIA_CONTENT_ID) == "EPISODE-UUID" + assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MediaType.TVSHOW + assert state.attributes.get(ATTR_MEDIA_SERIES_TITLE) == "SERIES" + assert state.attributes.get(ATTR_MEDIA_SEASON) == 1 + assert state.attributes.get(ATTR_MEDIA_EPISODE) == 3 + + entry = entity_registry.async_get(state.entity_id) + assert entry + assert entry.device_id + assert entry.entity_category is None + assert entry.unique_id == "SERVER-UUID-SESSION-UUID" + + assert len(mock_api.sessions.mock_calls) == 1 + async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert len(mock_api.sessions.mock_calls) == 2 + + mock_api.sessions.return_value = [] + async_fire_time_changed(hass, utcnow() + timedelta(seconds=20)) + await hass.async_block_till_done() + assert len(mock_api.sessions.mock_calls) == 3 + + device = device_registry.async_get(entry.device_id) + assert device + assert device.configuration_url is None + assert device.connections == set() + assert device.entry_type is None + assert device.hw_version is None + assert device.identifiers == {(DOMAIN, "DEVICE-UUID")} + assert device.manufacturer == "Jellyfin" + assert device.name == "JELLYFIN-DEVICE" + assert device.sw_version == "1.0.0" + + +async def test_media_player_music( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_jellyfin: MagicMock, + mock_api: MagicMock, +) -> None: + """Test the Jellyfin media player.""" + entity_registry = er.async_get(hass) + + state = hass.states.get("media_player.jellyfin_device_four") + + assert state + assert state.state == MediaPlayerState.PLAYING + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "JELLYFIN DEVICE FOUR" + assert state.attributes.get(ATTR_ICON) is None + assert state.attributes.get(ATTR_MEDIA_VOLUME_LEVEL) == 1.0 + assert state.attributes.get(ATTR_MEDIA_VOLUME_MUTED) is False + assert state.attributes.get(ATTR_MEDIA_DURATION) == 73 + assert state.attributes.get(ATTR_MEDIA_POSITION) == 22 + assert state.attributes.get(ATTR_MEDIA_POSITION_UPDATED_AT) + assert state.attributes.get(ATTR_MEDIA_CONTENT_ID) == "MUSIC-UUID" + assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MediaType.MUSIC + assert state.attributes.get(ATTR_MEDIA_ALBUM_NAME) == "ALBUM" + assert state.attributes.get(ATTR_MEDIA_ALBUM_ARTIST) == "Album Artist" + assert state.attributes.get(ATTR_MEDIA_ARTIST) == "Contributing Artist" + assert state.attributes.get(ATTR_MEDIA_TRACK) == 1 + assert state.attributes.get(ATTR_MEDIA_SERIES_TITLE) is None + assert state.attributes.get(ATTR_MEDIA_SEASON) is None + assert state.attributes.get(ATTR_MEDIA_EPISODE) is None + + entry = entity_registry.async_get(state.entity_id) + assert entry + assert entry.device_id is None + assert entry.entity_category is None + assert entry.unique_id == "SERVER-UUID-SESSION-UUID-FOUR" + + +async def test_services( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_jellyfin: MagicMock, + mock_api: MagicMock, +) -> None: + """Test Jellyfin media player services.""" + state = hass.states.get("media_player.jellyfin_device") + assert state + + await hass.services.async_call( + MP_DOMAIN, + "play_media", + { + ATTR_ENTITY_ID: state.entity_id, + "media_content_type": "", + "media_content_id": "ITEM-UUID", + }, + blocking=True, + ) + assert len(mock_api.remote_play_media.mock_calls) == 1 + assert mock_api.remote_play_media.mock_calls[0].args == ( + "SESSION-UUID", + ["ITEM-UUID"], + ) + + await hass.services.async_call( + MP_DOMAIN, + "media_pause", + { + ATTR_ENTITY_ID: state.entity_id, + }, + blocking=True, + ) + assert len(mock_api.remote_pause.mock_calls) == 1 + + await hass.services.async_call( + MP_DOMAIN, + "media_play", + { + ATTR_ENTITY_ID: state.entity_id, + }, + blocking=True, + ) + assert len(mock_api.remote_unpause.mock_calls) == 1 + + await hass.services.async_call( + MP_DOMAIN, + "media_play_pause", + { + ATTR_ENTITY_ID: state.entity_id, + }, + blocking=True, + ) + assert len(mock_api.remote_playpause.mock_calls) == 1 + + await hass.services.async_call( + MP_DOMAIN, + "media_seek", + { + ATTR_ENTITY_ID: state.entity_id, + "seek_position": 10, + }, + blocking=True, + ) + assert len(mock_api.remote_seek.mock_calls) == 1 + assert mock_api.remote_seek.mock_calls[0].args == ( + "SESSION-UUID", + 100000000, + ) + + await hass.services.async_call( + MP_DOMAIN, + "media_stop", + { + ATTR_ENTITY_ID: state.entity_id, + }, + blocking=True, + ) + assert len(mock_api.remote_stop.mock_calls) == 1 + + await hass.services.async_call( + MP_DOMAIN, + "volume_set", + { + ATTR_ENTITY_ID: state.entity_id, + "volume_level": 0.5, + }, + blocking=True, + ) + assert len(mock_api.remote_set_volume.mock_calls) == 1 + + await hass.services.async_call( + MP_DOMAIN, + "volume_mute", + { + ATTR_ENTITY_ID: state.entity_id, + "is_volume_muted": True, + }, + blocking=True, + ) + assert len(mock_api.remote_mute.mock_calls) == 1 + + await hass.services.async_call( + MP_DOMAIN, + "volume_mute", + { + ATTR_ENTITY_ID: state.entity_id, + "is_volume_muted": False, + }, + blocking=True, + ) + assert len(mock_api.remote_unmute.mock_calls) == 1 + + +async def test_browse_media( + hass: HomeAssistant, + hass_ws_client: ClientSession, + init_integration: MockConfigEntry, + mock_jellyfin: MagicMock, + mock_api: MagicMock, +) -> None: + """Test Jellyfin browse media.""" + client = await hass_ws_client() + + # browse root folder + await client.send_json( + { + "id": 1, + "type": "media_player/browse_media", + "entity_id": "media_player.jellyfin_device", + } + ) + response = await client.receive_json() + assert response["success"] + expected_child_item = { + "title": "COLLECTION FOLDER", + "media_class": MediaClass.DIRECTORY.value, + "media_content_type": "collection", + "media_content_id": "COLLECTION-FOLDER-UUID", + "can_play": False, + "can_expand": True, + "thumbnail": "http://localhost/Items/c22fd826-17fc-44f4-9b04-1eb3e8fb9173/Images/Backdrop.jpg", + "children_media_class": None, + } + + assert response["result"]["media_content_id"] == "" + assert response["result"]["media_content_type"] == "root" + assert response["result"]["title"] == "Jellyfin" + assert response["result"]["children"][0] == expected_child_item + + # browse collection folder + await client.send_json( + { + "id": 2, + "type": "media_player/browse_media", + "entity_id": "media_player.jellyfin_device", + "media_content_type": "collection", + "media_content_id": "COLLECTION-FOLDER-UUID", + } + ) + + response = await client.receive_json() + expected_child_item = { + "title": "EPISODE", + "media_class": MediaClass.EPISODE.value, + "media_content_type": MediaType.EPISODE.value, + "media_content_id": "EPISODE-UUID", + "can_play": True, + "can_expand": False, + "thumbnail": "http://localhost/Items/c22fd826-17fc-44f4-9b04-1eb3e8fb9173/Images/Backdrop.jpg", + "children_media_class": None, + } + + assert response["success"] + assert response["result"]["media_content_id"] == "COLLECTION-FOLDER-UUID" + assert response["result"]["title"] == "FOLDER" + assert response["result"]["children"][0] == expected_child_item + + # browse for collection without children + mock_api.user_items.side_effect = None + mock_api.user_items.return_value = {} + + await client.send_json( + { + "id": 3, + "type": "media_player/browse_media", + "entity_id": "media_player.jellyfin_device", + "media_content_type": "collection", + "media_content_id": "COLLECTION-FOLDER-UUID", + } + ) + + response = await client.receive_json() + assert response["success"] is False + assert response["error"] + assert ( + response["error"]["message"] + == "Media not found: collection / COLLECTION-FOLDER-UUID" + ) + + # browse for non-existent item + mock_api.get_item.side_effect = None + mock_api.get_item.return_value = {} + + await client.send_json( + { + "id": 4, + "type": "media_player/browse_media", + "entity_id": "media_player.jellyfin_device", + "media_content_type": "collection", + "media_content_id": "COLLECTION-UUID-404", + } + ) + + response = await client.receive_json() + assert response["success"] is False + assert response["error"] + assert ( + response["error"]["message"] + == "Media not found: collection / COLLECTION-UUID-404" + )