Add media_player platform to Jellyfin (#76801)
parent
9b331abe91
commit
3759be09df
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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
|
|
@ -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."""
|
||||
|
||||
|
|
|
@ -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__)
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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,
|
||||
)
|
|
@ -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]
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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": {}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
)
|
Loading…
Reference in New Issue