mirror of
https://github.com/sudoxnym/fin-assistant.git
synced 2026-04-14 19:46:26 +00:00
Baseline media_source
This commit is contained in:
parent
7337601293
commit
a9e7a24af2
7 changed files with 655 additions and 471 deletions
26
README.md
26
README.md
|
|
@ -1,2 +1,28 @@
|
||||||
# jellyfin_ha
|
# jellyfin_ha
|
||||||
|
|
||||||
Jellyfin integration for Home Assistant
|
Jellyfin integration for Home Assistant
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
### 1.0
|
||||||
|
|
||||||
|
- Initial public release
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Entities
|
||||||
|
|
||||||
|
- 1 media_player entity per device
|
||||||
|
- 1 sensor per server
|
||||||
|
|
||||||
|
### Media Browser
|
||||||
|
|
||||||
|
- Browse medias and start playback from within Home Assistant
|
||||||
|
|
||||||
|
### Media Source
|
||||||
|
|
||||||
|
- Browse and stream to device (e.g. Chromecast)
|
||||||
|
|
||||||
|
### Services
|
||||||
|
|
||||||
|
- `trigger_scan`: Trigger a server media scan
|
||||||
|
|
|
||||||
270
__init__.py
270
__init__.py
|
|
@ -31,6 +31,8 @@ from homeassistant.helpers.dispatcher import ( # pylint: disable=import-error
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
USER_APP_NAME,
|
||||||
|
CLIENT_VERSION,
|
||||||
SIGNAL_STATE_UPDATED,
|
SIGNAL_STATE_UPDATED,
|
||||||
SERVICE_SCAN,
|
SERVICE_SCAN,
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
|
|
@ -38,15 +40,12 @@ from .const import (
|
||||||
STATE_PAUSED,
|
STATE_PAUSED,
|
||||||
STATE_PLAYING,
|
STATE_PLAYING,
|
||||||
)
|
)
|
||||||
from .device import JellyfinDevice
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
PLATFORMS = ["sensor", "media_player"]
|
PLATFORMS = ["sensor", "media_player"]
|
||||||
UPDATE_UNLISTENER = None
|
UPDATE_UNLISTENER = None
|
||||||
|
|
||||||
USER_APP_NAME = "Home Assistant"
|
|
||||||
CLIENT_VERSION = "1.0"
|
|
||||||
PATH_REGEX = re.compile("^(https?://)?([^/:]+)(:[0-9]+)?(/.*)?$")
|
PATH_REGEX = re.compile("^(https?://)?([^/:]+)(:[0-9]+)?(/.*)?$")
|
||||||
|
|
||||||
SCAN_SERVICE_SCHEMA = vol.Schema(
|
SCAN_SERVICE_SCHEMA = vol.Schema(
|
||||||
|
|
@ -143,6 +142,263 @@ async def _update_listener(hass, config_entry):
|
||||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
|
class JellyfinDevice(object):
|
||||||
|
""" Represents properties of an Jellyfin Device. """
|
||||||
|
|
||||||
|
def __init__(self, session, jf_manager):
|
||||||
|
"""Initialize Emby device object."""
|
||||||
|
self.jf_manager = jf_manager
|
||||||
|
self.is_active = True
|
||||||
|
self.update_data(session)
|
||||||
|
|
||||||
|
def update_data(self, session):
|
||||||
|
""" Update session object. """
|
||||||
|
self.session = session
|
||||||
|
|
||||||
|
def set_active(self, active):
|
||||||
|
""" Mark device as on/off. """
|
||||||
|
self.is_active = active
|
||||||
|
|
||||||
|
@property
|
||||||
|
def session_raw(self):
|
||||||
|
""" Return raw session data. """
|
||||||
|
return self.session
|
||||||
|
|
||||||
|
@property
|
||||||
|
def session_id(self):
|
||||||
|
""" Return current session Id. """
|
||||||
|
try:
|
||||||
|
return self.session['Id']
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
""" Return device id."""
|
||||||
|
try:
|
||||||
|
return self.session['DeviceId']
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
""" Return device name."""
|
||||||
|
try:
|
||||||
|
return self.session['DeviceName']
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client(self):
|
||||||
|
""" Return client name. """
|
||||||
|
try:
|
||||||
|
return self.session['Client']
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def username(self):
|
||||||
|
""" Return device name."""
|
||||||
|
try:
|
||||||
|
return self.session['UserName']
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_title(self):
|
||||||
|
""" Return title currently playing."""
|
||||||
|
try:
|
||||||
|
return self.session['NowPlayingItem']['Name']
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_season(self):
|
||||||
|
"""Season of curent playing media (TV Show only)."""
|
||||||
|
try:
|
||||||
|
return self.session['NowPlayingItem']['ParentIndexNumber']
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_series_title(self):
|
||||||
|
"""The title of the series of current playing media (TV Show only)."""
|
||||||
|
try:
|
||||||
|
return self.session['NowPlayingItem']['SeriesName']
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_episode(self):
|
||||||
|
"""Episode of current playing media (TV Show only)."""
|
||||||
|
try:
|
||||||
|
return self.session['NowPlayingItem']['IndexNumber']
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_album_name(self):
|
||||||
|
"""Album name of current playing media (Music track only)."""
|
||||||
|
try:
|
||||||
|
return self.session['NowPlayingItem']['Album']
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_artist(self):
|
||||||
|
"""Artist of current playing media (Music track only)."""
|
||||||
|
try:
|
||||||
|
artists = self.session['NowPlayingItem']['Artists']
|
||||||
|
if len(artists) > 1:
|
||||||
|
return artists[0]
|
||||||
|
else:
|
||||||
|
return artists
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_album_artist(self):
|
||||||
|
"""Album artist of current playing media (Music track only)."""
|
||||||
|
try:
|
||||||
|
return self.session['NowPlayingItem']['AlbumArtist']
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_id(self):
|
||||||
|
""" Return title currently playing."""
|
||||||
|
try:
|
||||||
|
return self.session['NowPlayingItem']['Id']
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_type(self):
|
||||||
|
""" Return type currently playing."""
|
||||||
|
try:
|
||||||
|
return self.session['NowPlayingItem']['Type']
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_image_url(self):
|
||||||
|
"""Image url of current playing media."""
|
||||||
|
if self.is_nowplaying:
|
||||||
|
try:
|
||||||
|
image_id = self.session['NowPlayingItem']['ImageTags']['Thumb']
|
||||||
|
image_type = 'Thumb'
|
||||||
|
except KeyError:
|
||||||
|
try:
|
||||||
|
image_id = self.session[
|
||||||
|
'NowPlayingItem']['ImageTags']['Primary']
|
||||||
|
image_type = 'Primary'
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
url = self.jf_manager.api.artwork(self.media_id, image_type, 500)
|
||||||
|
return url
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_position(self):
|
||||||
|
""" Return position currently playing."""
|
||||||
|
try:
|
||||||
|
return int(self.session['PlayState']['PositionTicks']) / 10000000
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_runtime(self):
|
||||||
|
""" Return total runtime length."""
|
||||||
|
try:
|
||||||
|
return int(
|
||||||
|
self.session['NowPlayingItem']['RunTimeTicks']) / 10000000
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_percent_played(self):
|
||||||
|
""" Return media percent played. """
|
||||||
|
try:
|
||||||
|
return (self.media_position / self.media_runtime) * 100
|
||||||
|
except TypeError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
""" Return current playstate of the device. """
|
||||||
|
if self.is_active:
|
||||||
|
if 'NowPlayingItem' in self.session:
|
||||||
|
if self.session['PlayState']['IsPaused']:
|
||||||
|
return STATE_PAUSED
|
||||||
|
else:
|
||||||
|
return STATE_PLAYING
|
||||||
|
else:
|
||||||
|
return STATE_IDLE
|
||||||
|
else:
|
||||||
|
return STATE_OFF
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_nowplaying(self):
|
||||||
|
""" Return true if an item is currently active. """
|
||||||
|
if self.state == 'Idle' or self.state == 'Off':
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supports_remote_control(self):
|
||||||
|
""" Return remote control status. """
|
||||||
|
return self.session['SupportsRemoteControl']
|
||||||
|
|
||||||
|
async def get_item(self, id):
|
||||||
|
return await self.jf_manager.get_item(id)
|
||||||
|
|
||||||
|
async def get_items(self, query=None):
|
||||||
|
return await self.jf_manager.get_items(query)
|
||||||
|
|
||||||
|
async def get_artwork(self, media_id) -> Tuple[Optional[str], Optional[str]]:
|
||||||
|
return await self.jf_manager.get_artwork(media_id)
|
||||||
|
|
||||||
|
async def get_artwork_url(self, media_id) -> str:
|
||||||
|
return await self.jf_manager.get_artwork_url(media_id)
|
||||||
|
|
||||||
|
async def set_playstate(self, state, pos=0):
|
||||||
|
""" Send media commands to server. """
|
||||||
|
params = {}
|
||||||
|
if state == 'Seek':
|
||||||
|
params['SeekPositionTicks'] = int(pos * 10000000)
|
||||||
|
params['static'] = 'true'
|
||||||
|
|
||||||
|
await self.jf_manager.set_playstate(self.session_id, state, params)
|
||||||
|
|
||||||
|
def media_play(self):
|
||||||
|
""" Send play command to device. """
|
||||||
|
return self.set_playstate('Unpause')
|
||||||
|
|
||||||
|
def media_pause(self):
|
||||||
|
""" Send pause command to device. """
|
||||||
|
return self.set_playstate('Pause')
|
||||||
|
|
||||||
|
def media_stop(self):
|
||||||
|
""" Send stop command to device. """
|
||||||
|
return self.set_playstate('Stop')
|
||||||
|
|
||||||
|
def media_next(self):
|
||||||
|
""" Send next track command to device. """
|
||||||
|
return self.set_playstate('NextTrack')
|
||||||
|
|
||||||
|
def media_previous(self):
|
||||||
|
""" Send previous track command to device. """
|
||||||
|
return self.set_playstate('PreviousTrack')
|
||||||
|
|
||||||
|
def media_seek(self, position):
|
||||||
|
""" Send seek command to device. """
|
||||||
|
return self.set_playstate('Seek', position)
|
||||||
|
|
||||||
|
async def play_media(self, media_id):
|
||||||
|
await self.jf_manager.play_media(self.session_id, media_id)
|
||||||
|
|
||||||
class JellyfinClientManager(object):
|
class JellyfinClientManager(object):
|
||||||
def __init__(self, hass: HomeAssistant, config_entry):
|
def __init__(self, hass: HomeAssistant, config_entry):
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
|
|
@ -430,6 +686,9 @@ class JellyfinClientManager(object):
|
||||||
async def trigger_scan(self):
|
async def trigger_scan(self):
|
||||||
await self.hass.async_add_executor_job(self.jf_client.jellyfin._post, "Library/Refresh")
|
await self.hass.async_add_executor_job(self.jf_client.jellyfin._post, "Library/Refresh")
|
||||||
|
|
||||||
|
def get_server_url(self) -> str:
|
||||||
|
return self.jf_client.config.data["auth.server"]
|
||||||
|
|
||||||
async def get_item(self, id):
|
async def get_item(self, id):
|
||||||
return await self.hass.async_add_executor_job(self.jf_client.jellyfin.get_item, id)
|
return await self.hass.async_add_executor_job(self.jf_client.jellyfin.get_item, id)
|
||||||
|
|
||||||
|
|
@ -460,6 +719,9 @@ class JellyfinClientManager(object):
|
||||||
|
|
||||||
return (None, None)
|
return (None, None)
|
||||||
|
|
||||||
|
async def get_play_info(self, media_id, profile):
|
||||||
|
return await self.hass.async_add_executor_job(self.jf_client.jellyfin.get_play_info, media_id, profile)
|
||||||
|
|
||||||
async def get_artwork_url(self, media_id) -> str:
|
async def get_artwork_url(self, media_id) -> str:
|
||||||
return await self.hass.async_add_executor_job(self.jf_client.jellyfin.artwork, media_id, "Primary", 500)
|
return await self.hass.async_add_executor_job(self.jf_client.jellyfin.artwork, media_id, "Primary", 500)
|
||||||
|
|
||||||
|
|
@ -520,5 +782,3 @@ class JellyfinClientManager(object):
|
||||||
_LOGGER.debug('Update callback %s for device %s by %s',
|
_LOGGER.debug('Update callback %s for device %s by %s',
|
||||||
callback, device, msg)
|
callback, device, msg)
|
||||||
self._event_loop.call_soon(callback, msg)
|
self._event_loop.call_soon(callback, msg)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
3
const.py
3
const.py
|
|
@ -4,6 +4,9 @@ DOMAIN = "jellyfin"
|
||||||
SIGNAL_STATE_UPDATED = "{}.updated".format(DOMAIN)
|
SIGNAL_STATE_UPDATED = "{}.updated".format(DOMAIN)
|
||||||
SERVICE_SCAN = "trigger_scan"
|
SERVICE_SCAN = "trigger_scan"
|
||||||
|
|
||||||
|
USER_APP_NAME = "Home Assistant"
|
||||||
|
CLIENT_VERSION = "1.0"
|
||||||
|
|
||||||
DEFAULT_SSL = False
|
DEFAULT_SSL = False
|
||||||
DEFAULT_VERIFY_SSL = True
|
DEFAULT_VERIFY_SSL = True
|
||||||
DEFAULT_PORT = 8096
|
DEFAULT_PORT = 8096
|
||||||
|
|
|
||||||
267
device.py
267
device.py
|
|
@ -1,267 +0,0 @@
|
||||||
import logging
|
|
||||||
from typing import Optional, Tuple
|
|
||||||
from .const import (
|
|
||||||
STATE_OFF,
|
|
||||||
STATE_IDLE,
|
|
||||||
STATE_PAUSED,
|
|
||||||
STATE_PLAYING,
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
class JellyfinDevice(object):
|
|
||||||
""" Represents properties of an Jellyfin Device. """
|
|
||||||
|
|
||||||
def __init__(self, session, jf_manager):
|
|
||||||
"""Initialize Emby device object."""
|
|
||||||
self.jf_manager = jf_manager
|
|
||||||
self.is_active = True
|
|
||||||
self.update_data(session)
|
|
||||||
|
|
||||||
def update_data(self, session):
|
|
||||||
""" Update session object. """
|
|
||||||
self.session = session
|
|
||||||
|
|
||||||
def set_active(self, active):
|
|
||||||
""" Mark device as on/off. """
|
|
||||||
self.is_active = active
|
|
||||||
|
|
||||||
@property
|
|
||||||
def session_raw(self):
|
|
||||||
""" Return raw session data. """
|
|
||||||
return self.session
|
|
||||||
|
|
||||||
@property
|
|
||||||
def session_id(self):
|
|
||||||
""" Return current session Id. """
|
|
||||||
try:
|
|
||||||
return self.session['Id']
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def unique_id(self):
|
|
||||||
""" Return device id."""
|
|
||||||
try:
|
|
||||||
return self.session['DeviceId']
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
""" Return device name."""
|
|
||||||
try:
|
|
||||||
return self.session['DeviceName']
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def client(self):
|
|
||||||
""" Return client name. """
|
|
||||||
try:
|
|
||||||
return self.session['Client']
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def username(self):
|
|
||||||
""" Return device name."""
|
|
||||||
try:
|
|
||||||
return self.session['UserName']
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def media_title(self):
|
|
||||||
""" Return title currently playing."""
|
|
||||||
try:
|
|
||||||
return self.session['NowPlayingItem']['Name']
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def media_season(self):
|
|
||||||
"""Season of curent playing media (TV Show only)."""
|
|
||||||
try:
|
|
||||||
return self.session['NowPlayingItem']['ParentIndexNumber']
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def media_series_title(self):
|
|
||||||
"""The title of the series of current playing media (TV Show only)."""
|
|
||||||
try:
|
|
||||||
return self.session['NowPlayingItem']['SeriesName']
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def media_episode(self):
|
|
||||||
"""Episode of current playing media (TV Show only)."""
|
|
||||||
try:
|
|
||||||
return self.session['NowPlayingItem']['IndexNumber']
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def media_album_name(self):
|
|
||||||
"""Album name of current playing media (Music track only)."""
|
|
||||||
try:
|
|
||||||
return self.session['NowPlayingItem']['Album']
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def media_artist(self):
|
|
||||||
"""Artist of current playing media (Music track only)."""
|
|
||||||
try:
|
|
||||||
artists = self.session['NowPlayingItem']['Artists']
|
|
||||||
if len(artists) > 1:
|
|
||||||
return artists[0]
|
|
||||||
else:
|
|
||||||
return artists
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def media_album_artist(self):
|
|
||||||
"""Album artist of current playing media (Music track only)."""
|
|
||||||
try:
|
|
||||||
return self.session['NowPlayingItem']['AlbumArtist']
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def media_id(self):
|
|
||||||
""" Return title currently playing."""
|
|
||||||
try:
|
|
||||||
return self.session['NowPlayingItem']['Id']
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def media_type(self):
|
|
||||||
""" Return type currently playing."""
|
|
||||||
try:
|
|
||||||
return self.session['NowPlayingItem']['Type']
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def media_image_url(self):
|
|
||||||
"""Image url of current playing media."""
|
|
||||||
if self.is_nowplaying:
|
|
||||||
try:
|
|
||||||
image_id = self.session['NowPlayingItem']['ImageTags']['Thumb']
|
|
||||||
image_type = 'Thumb'
|
|
||||||
except KeyError:
|
|
||||||
try:
|
|
||||||
image_id = self.session[
|
|
||||||
'NowPlayingItem']['ImageTags']['Primary']
|
|
||||||
image_type = 'Primary'
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
url = self.jf_manager.api.artwork(self.media_id, image_type, 500)
|
|
||||||
return url
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def media_position(self):
|
|
||||||
""" Return position currently playing."""
|
|
||||||
try:
|
|
||||||
return int(self.session['PlayState']['PositionTicks']) / 10000000
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def media_runtime(self):
|
|
||||||
""" Return total runtime length."""
|
|
||||||
try:
|
|
||||||
return int(
|
|
||||||
self.session['NowPlayingItem']['RunTimeTicks']) / 10000000
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def media_percent_played(self):
|
|
||||||
""" Return media percent played. """
|
|
||||||
try:
|
|
||||||
return (self.media_position / self.media_runtime) * 100
|
|
||||||
except TypeError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def state(self):
|
|
||||||
""" Return current playstate of the device. """
|
|
||||||
if self.is_active:
|
|
||||||
if 'NowPlayingItem' in self.session:
|
|
||||||
if self.session['PlayState']['IsPaused']:
|
|
||||||
return STATE_PAUSED
|
|
||||||
else:
|
|
||||||
return STATE_PLAYING
|
|
||||||
else:
|
|
||||||
return STATE_IDLE
|
|
||||||
else:
|
|
||||||
return STATE_OFF
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_nowplaying(self):
|
|
||||||
""" Return true if an item is currently active. """
|
|
||||||
if self.state == 'Idle' or self.state == 'Off':
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def supports_remote_control(self):
|
|
||||||
""" Return remote control status. """
|
|
||||||
return self.session['SupportsRemoteControl']
|
|
||||||
|
|
||||||
async def get_item(self, id):
|
|
||||||
return await self.jf_manager.get_item(id)
|
|
||||||
|
|
||||||
async def get_items(self, query=None):
|
|
||||||
return await self.jf_manager.get_items(query)
|
|
||||||
|
|
||||||
async def get_artwork(self, media_id) -> Tuple[Optional[str], Optional[str]]:
|
|
||||||
return await self.jf_manager.get_artwork(media_id)
|
|
||||||
|
|
||||||
async def get_artwork_url(self, media_id) -> str:
|
|
||||||
return await self.jf_manager.get_artwork_url(media_id)
|
|
||||||
|
|
||||||
async def set_playstate(self, state, pos=0):
|
|
||||||
""" Send media commands to server. """
|
|
||||||
params = {}
|
|
||||||
if state == 'Seek':
|
|
||||||
params['SeekPositionTicks'] = int(pos * 10000000)
|
|
||||||
params['static'] = 'true'
|
|
||||||
|
|
||||||
await self.jf_manager.set_playstate(self.session_id, state, params)
|
|
||||||
|
|
||||||
def media_play(self):
|
|
||||||
""" Send play command to device. """
|
|
||||||
return self.set_playstate('Unpause')
|
|
||||||
|
|
||||||
def media_pause(self):
|
|
||||||
""" Send pause command to device. """
|
|
||||||
return self.set_playstate('Pause')
|
|
||||||
|
|
||||||
def media_stop(self):
|
|
||||||
""" Send stop command to device. """
|
|
||||||
return self.set_playstate('Stop')
|
|
||||||
|
|
||||||
def media_next(self):
|
|
||||||
""" Send next track command to device. """
|
|
||||||
return self.set_playstate('NextTrack')
|
|
||||||
|
|
||||||
def media_previous(self):
|
|
||||||
""" Send previous track command to device. """
|
|
||||||
return self.set_playstate('PreviousTrack')
|
|
||||||
|
|
||||||
def media_seek(self, position):
|
|
||||||
""" Send seek command to device. """
|
|
||||||
return self.set_playstate('Seek', position)
|
|
||||||
|
|
||||||
async def play_media(self, media_id):
|
|
||||||
await self.jf_manager.play_media(self.session_id, media_id)
|
|
||||||
197
media_browser.py
197
media_browser.py
|
|
@ -1,197 +0,0 @@
|
||||||
import logging
|
|
||||||
|
|
||||||
from homeassistant.components.media_player import BrowseError, BrowseMedia
|
|
||||||
from homeassistant.components.media_player.const import (
|
|
||||||
MEDIA_CLASS_ALBUM,
|
|
||||||
MEDIA_CLASS_ARTIST,
|
|
||||||
MEDIA_CLASS_CHANNEL,
|
|
||||||
MEDIA_CLASS_DIRECTORY,
|
|
||||||
MEDIA_CLASS_EPISODE,
|
|
||||||
MEDIA_CLASS_MOVIE,
|
|
||||||
MEDIA_CLASS_MUSIC,
|
|
||||||
MEDIA_CLASS_PLAYLIST,
|
|
||||||
MEDIA_CLASS_SEASON,
|
|
||||||
MEDIA_CLASS_TRACK,
|
|
||||||
MEDIA_CLASS_TV_SHOW,
|
|
||||||
MEDIA_TYPE_ALBUM,
|
|
||||||
MEDIA_TYPE_ARTIST,
|
|
||||||
MEDIA_TYPE_CHANNEL,
|
|
||||||
MEDIA_TYPE_EPISODE,
|
|
||||||
MEDIA_TYPE_MOVIE,
|
|
||||||
MEDIA_TYPE_PLAYLIST,
|
|
||||||
MEDIA_TYPE_SEASON,
|
|
||||||
MEDIA_TYPE_TRACK,
|
|
||||||
MEDIA_TYPE_TVSHOW,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .device import JellyfinDevice
|
|
||||||
|
|
||||||
PLAYABLE_MEDIA_TYPES = [
|
|
||||||
MEDIA_TYPE_ALBUM,
|
|
||||||
MEDIA_TYPE_ARTIST,
|
|
||||||
MEDIA_TYPE_TRACK,
|
|
||||||
]
|
|
||||||
|
|
||||||
CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS = {
|
|
||||||
MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM,
|
|
||||||
MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST,
|
|
||||||
MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST,
|
|
||||||
MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON,
|
|
||||||
MEDIA_TYPE_TVSHOW: MEDIA_CLASS_TV_SHOW,
|
|
||||||
}
|
|
||||||
|
|
||||||
CHILD_TYPE_MEDIA_CLASS = {
|
|
||||||
MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON,
|
|
||||||
MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM,
|
|
||||||
MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST,
|
|
||||||
MEDIA_TYPE_MOVIE: MEDIA_CLASS_MOVIE,
|
|
||||||
MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST,
|
|
||||||
MEDIA_TYPE_TRACK: MEDIA_CLASS_TRACK,
|
|
||||||
MEDIA_TYPE_TVSHOW: MEDIA_CLASS_TV_SHOW,
|
|
||||||
MEDIA_TYPE_CHANNEL: MEDIA_CLASS_CHANNEL,
|
|
||||||
MEDIA_TYPE_EPISODE: MEDIA_CLASS_EPISODE,
|
|
||||||
}
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
class UnknownMediaType(BrowseError):
|
|
||||||
"""Unknown media type."""
|
|
||||||
|
|
||||||
def Type2Mediatype(type):
|
|
||||||
switcher = {
|
|
||||||
"Movie": MEDIA_TYPE_MOVIE,
|
|
||||||
"Series": MEDIA_TYPE_TVSHOW,
|
|
||||||
"Season": MEDIA_TYPE_SEASON,
|
|
||||||
"Episode": MEDIA_TYPE_EPISODE,
|
|
||||||
"Music": MEDIA_TYPE_ALBUM,
|
|
||||||
"Audio": MEDIA_TYPE_TRACK,
|
|
||||||
"BoxSet": MEDIA_CLASS_DIRECTORY,
|
|
||||||
"Folder": MEDIA_CLASS_DIRECTORY,
|
|
||||||
"CollectionFolder": MEDIA_CLASS_DIRECTORY,
|
|
||||||
"Playlist": MEDIA_CLASS_DIRECTORY,
|
|
||||||
"MusicArtist": MEDIA_TYPE_ARTIST,
|
|
||||||
"MusicAlbum": MEDIA_TYPE_ALBUM,
|
|
||||||
"Audio": MEDIA_TYPE_TRACK,
|
|
||||||
}
|
|
||||||
return switcher[type]
|
|
||||||
|
|
||||||
def Type2Mediaclass(type):
|
|
||||||
switcher = {
|
|
||||||
"Movie": MEDIA_CLASS_MOVIE,
|
|
||||||
"Series": MEDIA_CLASS_TV_SHOW,
|
|
||||||
"Season": MEDIA_CLASS_SEASON,
|
|
||||||
"Episode": MEDIA_CLASS_EPISODE,
|
|
||||||
"Music": MEDIA_CLASS_DIRECTORY,
|
|
||||||
"BoxSet": MEDIA_CLASS_DIRECTORY,
|
|
||||||
"Folder": MEDIA_CLASS_DIRECTORY,
|
|
||||||
"CollectionFolder": MEDIA_CLASS_DIRECTORY,
|
|
||||||
"Playlist": MEDIA_CLASS_DIRECTORY,
|
|
||||||
"MusicArtist": MEDIA_CLASS_ARTIST,
|
|
||||||
"MusicAlbum": MEDIA_CLASS_ALBUM,
|
|
||||||
"Audio": MEDIA_CLASS_TRACK,
|
|
||||||
}
|
|
||||||
return switcher[type]
|
|
||||||
|
|
||||||
def IsPlayable(type):
|
|
||||||
switcher = {
|
|
||||||
"Movie": True,
|
|
||||||
"Series": True,
|
|
||||||
"Season": True,
|
|
||||||
"Episode": True,
|
|
||||||
"Music": False,
|
|
||||||
"BoxSet": True,
|
|
||||||
"Folder": False,
|
|
||||||
"CollectionFolder": False,
|
|
||||||
"Playlist": True,
|
|
||||||
"MusicArtist": True,
|
|
||||||
"MusicAlbum": True,
|
|
||||||
"Audio": True,
|
|
||||||
}
|
|
||||||
return switcher[type]
|
|
||||||
|
|
||||||
async def library_items(device: JellyfinDevice, media_content_type=None, media_content_id=None):
|
|
||||||
"""
|
|
||||||
Create response payload to describe contents of a specific library.
|
|
||||||
|
|
||||||
Used by async_browse_media.
|
|
||||||
"""
|
|
||||||
|
|
||||||
library_info = None
|
|
||||||
query = None
|
|
||||||
|
|
||||||
if media_content_type in [None, "library"]:
|
|
||||||
library_info = BrowseMedia(
|
|
||||||
media_class=MEDIA_CLASS_DIRECTORY,
|
|
||||||
media_content_id="library",
|
|
||||||
media_content_type="library",
|
|
||||||
title="Media Library",
|
|
||||||
can_play=False,
|
|
||||||
can_expand=True,
|
|
||||||
children=[],
|
|
||||||
)
|
|
||||||
elif media_content_type in [MEDIA_CLASS_DIRECTORY, MEDIA_TYPE_ARTIST, MEDIA_TYPE_ALBUM, MEDIA_TYPE_PLAYLIST, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_SEASON]:
|
|
||||||
query = {
|
|
||||||
"ParentId": media_content_id
|
|
||||||
}
|
|
||||||
|
|
||||||
parent_item = await device.get_item(media_content_id)
|
|
||||||
library_info = BrowseMedia(
|
|
||||||
media_class=media_content_type,
|
|
||||||
media_content_id=media_content_id,
|
|
||||||
media_content_type=media_content_type,
|
|
||||||
title=parent_item["Name"],
|
|
||||||
can_play=IsPlayable(parent_item["Type"]),
|
|
||||||
can_expand=True,
|
|
||||||
thumbnail=await device.get_artwork_url(media_content_id),
|
|
||||||
children=[],
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
query = {
|
|
||||||
"Id": media_content_id
|
|
||||||
}
|
|
||||||
library_info = BrowseMedia(
|
|
||||||
media_class=MEDIA_CLASS_DIRECTORY,
|
|
||||||
media_content_id=media_content_id,
|
|
||||||
media_content_type=media_content_type,
|
|
||||||
title="",
|
|
||||||
can_play=True,
|
|
||||||
can_expand=False,
|
|
||||||
thumbnail=await device.get_artwork_url(media_content_id),
|
|
||||||
children=[],
|
|
||||||
)
|
|
||||||
|
|
||||||
items = await device.get_items(query)
|
|
||||||
for item in items:
|
|
||||||
if media_content_type in [None, "library", MEDIA_CLASS_DIRECTORY, MEDIA_TYPE_ARTIST, MEDIA_TYPE_ALBUM, MEDIA_TYPE_PLAYLIST, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_SEASON]:
|
|
||||||
if item["IsFolder"]:
|
|
||||||
library_info.children.append(BrowseMedia(
|
|
||||||
media_class=Type2Mediaclass(item["Type"]),
|
|
||||||
media_content_id=item["Id"],
|
|
||||||
media_content_type=Type2Mediatype(item["Type"]),
|
|
||||||
title=item["Name"],
|
|
||||||
can_play=IsPlayable(item["Type"]),
|
|
||||||
can_expand=True,
|
|
||||||
children=[],
|
|
||||||
thumbnail=await device.get_artwork_url(item["Id"])
|
|
||||||
))
|
|
||||||
else:
|
|
||||||
library_info.children.append(BrowseMedia(
|
|
||||||
media_class=Type2Mediaclass(item["Type"]),
|
|
||||||
media_content_id=item["Id"],
|
|
||||||
media_content_type=Type2Mediatype(item["Type"]),
|
|
||||||
title=item["Name"],
|
|
||||||
can_play=IsPlayable(item["Type"]),
|
|
||||||
can_expand=False,
|
|
||||||
children=[],
|
|
||||||
thumbnail=await device.get_artwork_url(item["Id"])
|
|
||||||
))
|
|
||||||
else:
|
|
||||||
library_info.title = item["Name"]
|
|
||||||
library_info.media_content_id = item["Id"],
|
|
||||||
library_info.media_content_type = Type2Mediatype(item["Type"])
|
|
||||||
library_info.media_class = Type2Mediaclass(item["Type"])
|
|
||||||
library_info.can_expand = False
|
|
||||||
library_info.can_play=IsPlayable(item["Type"]),
|
|
||||||
break
|
|
||||||
|
|
||||||
return library_info
|
|
||||||
|
|
@ -31,7 +31,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
from . import JellyfinClientManager, autolog
|
from . import JellyfinClientManager, autolog
|
||||||
from .media_browser import library_items
|
from .media_source import async_library_items
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
|
@ -167,7 +167,7 @@ class JellyfinMediaPlayer(MediaPlayerEntity):
|
||||||
async def async_browse_media(self, media_content_type=None, media_content_id=None):
|
async def async_browse_media(self, media_content_type=None, media_content_id=None):
|
||||||
"""Implement the media source."""
|
"""Implement the media source."""
|
||||||
_LOGGER.debug("-- async_browse_media: %s / %s", media_content_type, media_content_id)
|
_LOGGER.debug("-- async_browse_media: %s / %s", media_content_type, media_content_id)
|
||||||
return await library_items(self.device, media_content_type, media_content_id)
|
return await async_library_items(self.jelly_cm, media_content_type, media_content_id)
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
||||||
359
media_source.py
Normal file
359
media_source.py
Normal file
|
|
@ -0,0 +1,359 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
from homeassistant.components.media_source.error import MediaSourceError, Unresolvable
|
||||||
|
from homeassistant.components.media_source.models import (
|
||||||
|
BrowseMediaSource,
|
||||||
|
MediaSource,
|
||||||
|
MediaSourceItem,
|
||||||
|
PlayMedia,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
|
||||||
|
from homeassistant.components.media_player import BrowseError, BrowseMedia
|
||||||
|
from homeassistant.components.media_source.const import MEDIA_MIME_TYPES, URI_SCHEME
|
||||||
|
|
||||||
|
from homeassistant.const import ( # pylint: disable=import-error
|
||||||
|
CONF_URL,
|
||||||
|
)
|
||||||
|
from homeassistant.components.media_player.const import (
|
||||||
|
MEDIA_CLASS_ALBUM,
|
||||||
|
MEDIA_CLASS_ARTIST,
|
||||||
|
MEDIA_CLASS_CHANNEL,
|
||||||
|
MEDIA_CLASS_DIRECTORY,
|
||||||
|
MEDIA_CLASS_EPISODE,
|
||||||
|
MEDIA_CLASS_MOVIE,
|
||||||
|
MEDIA_CLASS_MUSIC,
|
||||||
|
MEDIA_CLASS_PLAYLIST,
|
||||||
|
MEDIA_CLASS_SEASON,
|
||||||
|
MEDIA_CLASS_TRACK,
|
||||||
|
MEDIA_CLASS_TV_SHOW,
|
||||||
|
MEDIA_TYPE_ALBUM,
|
||||||
|
MEDIA_TYPE_ARTIST,
|
||||||
|
MEDIA_TYPE_CHANNEL,
|
||||||
|
MEDIA_TYPE_EPISODE,
|
||||||
|
MEDIA_TYPE_MOVIE,
|
||||||
|
MEDIA_TYPE_PLAYLIST,
|
||||||
|
MEDIA_TYPE_SEASON,
|
||||||
|
MEDIA_TYPE_TRACK,
|
||||||
|
MEDIA_TYPE_TVSHOW,
|
||||||
|
)
|
||||||
|
|
||||||
|
from . import JellyfinClientManager, JellyfinDevice, autolog
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
DOMAIN,
|
||||||
|
USER_APP_NAME,
|
||||||
|
)
|
||||||
|
|
||||||
|
PLAYABLE_MEDIA_TYPES = [
|
||||||
|
MEDIA_TYPE_ALBUM,
|
||||||
|
MEDIA_TYPE_ARTIST,
|
||||||
|
MEDIA_TYPE_TRACK,
|
||||||
|
]
|
||||||
|
|
||||||
|
CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS = {
|
||||||
|
MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM,
|
||||||
|
MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST,
|
||||||
|
MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST,
|
||||||
|
MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON,
|
||||||
|
MEDIA_TYPE_TVSHOW: MEDIA_CLASS_TV_SHOW,
|
||||||
|
}
|
||||||
|
|
||||||
|
CHILD_TYPE_MEDIA_CLASS = {
|
||||||
|
MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON,
|
||||||
|
MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM,
|
||||||
|
MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST,
|
||||||
|
MEDIA_TYPE_MOVIE: MEDIA_CLASS_MOVIE,
|
||||||
|
MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST,
|
||||||
|
MEDIA_TYPE_TRACK: MEDIA_CLASS_TRACK,
|
||||||
|
MEDIA_TYPE_TVSHOW: MEDIA_CLASS_TV_SHOW,
|
||||||
|
MEDIA_TYPE_CHANNEL: MEDIA_CLASS_CHANNEL,
|
||||||
|
MEDIA_TYPE_EPISODE: MEDIA_CLASS_EPISODE,
|
||||||
|
}
|
||||||
|
|
||||||
|
IDENTIFIER_SPLIT = "~~"
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class UnknownMediaType(BrowseError):
|
||||||
|
"""Unknown media type."""
|
||||||
|
|
||||||
|
async def async_get_media_source(hass: HomeAssistant):
|
||||||
|
"""Set up Netatmo media source."""
|
||||||
|
entry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||||
|
jelly_cm: JellyfinClientManager = hass.data[DOMAIN][entry.data[CONF_URL]]["manager"]
|
||||||
|
return JellyfinSource(hass, jelly_cm)
|
||||||
|
|
||||||
|
class JellyfinSource(MediaSource):
|
||||||
|
"""Media source for Jellyfin"""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, manager: JellyfinClientManager):
|
||||||
|
"""Initialize Netatmo source."""
|
||||||
|
super().__init__(DOMAIN)
|
||||||
|
self.hass = hass
|
||||||
|
self.jelly_cm = manager
|
||||||
|
|
||||||
|
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
|
||||||
|
"""Resolve a media item to a playable item."""
|
||||||
|
autolog("<<<")
|
||||||
|
|
||||||
|
if not item or not item.identifier:
|
||||||
|
return None
|
||||||
|
|
||||||
|
_, media_content_id = parse_mediasource_identifier(item.identifier)
|
||||||
|
|
||||||
|
profile = {
|
||||||
|
"Name": USER_APP_NAME,
|
||||||
|
"MaxStreamingBitrate": 25000 * 1000,
|
||||||
|
"MusicStreamingTranscodingBitrate": 1280000,
|
||||||
|
"TimelineOffsetSeconds": 5,
|
||||||
|
"TranscodingProfiles": [
|
||||||
|
{"Type": "Audio"},
|
||||||
|
{
|
||||||
|
"Container": "mp4",
|
||||||
|
"Type": "Video",
|
||||||
|
"Protocol": "http",
|
||||||
|
"AudioCodec": "aac,mp3,opus,flac,vorbis",
|
||||||
|
"VideoCodec": "h264,mpeg4,mpeg2video",
|
||||||
|
"MaxAudioChannels": "6",
|
||||||
|
},
|
||||||
|
{"Container": "jpeg", "Type": "Photo"},
|
||||||
|
],
|
||||||
|
"DirectPlayProfiles": [],
|
||||||
|
"ResponseProfiles": [],
|
||||||
|
"ContainerProfiles": [],
|
||||||
|
"CodecProfiles": [],
|
||||||
|
"SubtitleProfiles": [
|
||||||
|
{"Format": "srt", "Method": "External"},
|
||||||
|
{"Format": "srt", "Method": "Embed"},
|
||||||
|
{"Format": "ass", "Method": "External"},
|
||||||
|
{"Format": "ass", "Method": "Embed"},
|
||||||
|
{"Format": "sub", "Method": "Embed"},
|
||||||
|
{"Format": "sub", "Method": "External"},
|
||||||
|
{"Format": "ssa", "Method": "Embed"},
|
||||||
|
{"Format": "ssa", "Method": "External"},
|
||||||
|
{"Format": "smi", "Method": "Embed"},
|
||||||
|
{"Format": "smi", "Method": "External"},
|
||||||
|
# Jellyfin currently refuses to serve these subtitle types as external.
|
||||||
|
{"Format": "pgssub", "Method": "Embed"},
|
||||||
|
# {
|
||||||
|
# "Format": "pgssub",
|
||||||
|
# "Method": "External"
|
||||||
|
# },
|
||||||
|
{"Format": "dvdsub", "Method": "Embed"},
|
||||||
|
# {
|
||||||
|
# "Format": "dvdsub",
|
||||||
|
# "Method": "External"
|
||||||
|
# },
|
||||||
|
{"Format": "pgs", "Method": "Embed"},
|
||||||
|
# {
|
||||||
|
# "Format": "pgs",
|
||||||
|
# "Method": "External"
|
||||||
|
# }
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
playback_info = await self.jelly_cm.get_play_info(media_content_id, profile)
|
||||||
|
_LOGGER.debug("playbackinfo: %s", str(playback_info))
|
||||||
|
|
||||||
|
selected = None
|
||||||
|
weight_selected = 0
|
||||||
|
for media_source in playback_info["MediaSources"]:
|
||||||
|
weight = (media_source.get("SupportsDirectPlay") or 0) * 50000 + (
|
||||||
|
media_source.get("Bitrate") or 0
|
||||||
|
) / 1000
|
||||||
|
if weight > weight_selected:
|
||||||
|
weight_selected = weight
|
||||||
|
selected = media_source
|
||||||
|
|
||||||
|
if selected["SupportsTranscoding"]:
|
||||||
|
url = self.jelly_cm.get_server_url() + selected.get("TranscodingUrl")
|
||||||
|
_LOGGER.debug("cast url: %s", url)
|
||||||
|
return PlayMedia(url, "video/mp4")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def async_browse_media(
|
||||||
|
self, item: MediaSourceItem, media_types: Tuple[str] = MEDIA_MIME_TYPES
|
||||||
|
) -> BrowseMediaSource:
|
||||||
|
"""Browse media."""
|
||||||
|
autolog("<<<")
|
||||||
|
|
||||||
|
media_contant_type, media_content_id = async_parse_identifier(item)
|
||||||
|
return await async_library_items(self.jelly_cm, media_contant_type, media_content_id)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_parse_identifier(
|
||||||
|
item: MediaSourceItem,
|
||||||
|
) -> tuple[str | None, str | None]:
|
||||||
|
"""Parse identifier."""
|
||||||
|
if not item.identifier:
|
||||||
|
# Empty source_dir_id and location
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
return item.identifier, item.identifier
|
||||||
|
|
||||||
|
def Type2Mediatype(type):
|
||||||
|
switcher = {
|
||||||
|
"Movie": MEDIA_TYPE_MOVIE,
|
||||||
|
"Series": MEDIA_TYPE_TVSHOW,
|
||||||
|
"Season": MEDIA_TYPE_SEASON,
|
||||||
|
"Episode": MEDIA_TYPE_EPISODE,
|
||||||
|
"Music": MEDIA_TYPE_ALBUM,
|
||||||
|
"Audio": MEDIA_TYPE_TRACK,
|
||||||
|
"BoxSet": MEDIA_CLASS_DIRECTORY,
|
||||||
|
"Folder": MEDIA_CLASS_DIRECTORY,
|
||||||
|
"CollectionFolder": MEDIA_CLASS_DIRECTORY,
|
||||||
|
"Playlist": MEDIA_CLASS_DIRECTORY,
|
||||||
|
"MusicArtist": MEDIA_TYPE_ARTIST,
|
||||||
|
"MusicAlbum": MEDIA_TYPE_ALBUM,
|
||||||
|
"Audio": MEDIA_TYPE_TRACK,
|
||||||
|
}
|
||||||
|
return switcher[type]
|
||||||
|
|
||||||
|
def Type2Mediaclass(type):
|
||||||
|
switcher = {
|
||||||
|
"Movie": MEDIA_CLASS_MOVIE,
|
||||||
|
"Series": MEDIA_CLASS_TV_SHOW,
|
||||||
|
"Season": MEDIA_CLASS_SEASON,
|
||||||
|
"Episode": MEDIA_CLASS_EPISODE,
|
||||||
|
"Music": MEDIA_CLASS_DIRECTORY,
|
||||||
|
"BoxSet": MEDIA_CLASS_DIRECTORY,
|
||||||
|
"Folder": MEDIA_CLASS_DIRECTORY,
|
||||||
|
"CollectionFolder": MEDIA_CLASS_DIRECTORY,
|
||||||
|
"Playlist": MEDIA_CLASS_DIRECTORY,
|
||||||
|
"MusicArtist": MEDIA_CLASS_ARTIST,
|
||||||
|
"MusicAlbum": MEDIA_CLASS_ALBUM,
|
||||||
|
"Audio": MEDIA_CLASS_TRACK,
|
||||||
|
}
|
||||||
|
return switcher[type]
|
||||||
|
|
||||||
|
def IsPlayable(type):
|
||||||
|
switcher = {
|
||||||
|
"Movie": True,
|
||||||
|
"Series": True,
|
||||||
|
"Season": True,
|
||||||
|
"Episode": True,
|
||||||
|
"Music": False,
|
||||||
|
"BoxSet": True,
|
||||||
|
"Folder": False,
|
||||||
|
"CollectionFolder": False,
|
||||||
|
"Playlist": True,
|
||||||
|
"MusicArtist": True,
|
||||||
|
"MusicAlbum": True,
|
||||||
|
"Audio": True,
|
||||||
|
}
|
||||||
|
return switcher[type]
|
||||||
|
|
||||||
|
def parse_mediasource_identifier(identifier: str):
|
||||||
|
prefix = f"{URI_SCHEME}{DOMAIN}/"
|
||||||
|
text = identifier
|
||||||
|
if identifier.startswith(prefix):
|
||||||
|
text = identifier[len(prefix):]
|
||||||
|
|
||||||
|
return text.split(IDENTIFIER_SPLIT, 2)
|
||||||
|
|
||||||
|
async def async_library_items(jelly_cm: JellyfinClientManager, media_content_type_in=None, media_content_id_in=None) -> BrowseMediaSource:
|
||||||
|
"""
|
||||||
|
Create response payload to describe contents of a specific library.
|
||||||
|
|
||||||
|
Used by async_browse_media.
|
||||||
|
"""
|
||||||
|
_LOGGER.debug(f'>> async_library_items: {media_content_id_in}')
|
||||||
|
|
||||||
|
library_info = None
|
||||||
|
query = None
|
||||||
|
|
||||||
|
if (media_content_type_in is None):
|
||||||
|
media_content_type = None
|
||||||
|
media_content_id = None
|
||||||
|
else:
|
||||||
|
media_content_type, media_content_id = parse_mediasource_identifier(media_content_id_in)
|
||||||
|
_LOGGER.debug(f'>> {media_content_type} / {media_content_id}')
|
||||||
|
|
||||||
|
if media_content_type in [None, "library"]:
|
||||||
|
library_info = BrowseMediaSource(
|
||||||
|
domain=DOMAIN,
|
||||||
|
identifier=f'library{IDENTIFIER_SPLIT}library',
|
||||||
|
media_class=MEDIA_CLASS_DIRECTORY,
|
||||||
|
media_content_type="library",
|
||||||
|
title="Media Library",
|
||||||
|
can_play=False,
|
||||||
|
can_expand=True,
|
||||||
|
children=[],
|
||||||
|
)
|
||||||
|
elif media_content_type in [MEDIA_CLASS_DIRECTORY, MEDIA_TYPE_ARTIST, MEDIA_TYPE_ALBUM, MEDIA_TYPE_PLAYLIST, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_SEASON]:
|
||||||
|
query = {
|
||||||
|
"ParentId": media_content_id
|
||||||
|
}
|
||||||
|
|
||||||
|
parent_item = await jelly_cm.get_item(media_content_id)
|
||||||
|
library_info = BrowseMediaSource(
|
||||||
|
domain=DOMAIN,
|
||||||
|
identifier=f'{media_content_type}{IDENTIFIER_SPLIT}{media_content_id}',
|
||||||
|
media_class=media_content_type,
|
||||||
|
media_content_type=media_content_type,
|
||||||
|
title=parent_item["Name"],
|
||||||
|
can_play=IsPlayable(parent_item["Type"]),
|
||||||
|
can_expand=True,
|
||||||
|
thumbnail=await jelly_cm.get_artwork_url(media_content_id),
|
||||||
|
children=[],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
query = {
|
||||||
|
"Id": media_content_id
|
||||||
|
}
|
||||||
|
library_info = BrowseMediaSource(
|
||||||
|
domain=DOMAIN,
|
||||||
|
identifier=f'{media_content_type}{IDENTIFIER_SPLIT}{media_content_id}',
|
||||||
|
media_class=MEDIA_CLASS_DIRECTORY,
|
||||||
|
media_content_type=media_content_type,
|
||||||
|
title="",
|
||||||
|
can_play=True,
|
||||||
|
can_expand=False,
|
||||||
|
thumbnail=await jelly_cm.get_artwork_url(media_content_id),
|
||||||
|
children=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
items = await jelly_cm.get_items(query)
|
||||||
|
for item in items:
|
||||||
|
if media_content_type in [None, "library", MEDIA_CLASS_DIRECTORY, MEDIA_TYPE_ARTIST, MEDIA_TYPE_ALBUM, MEDIA_TYPE_PLAYLIST, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_SEASON]:
|
||||||
|
if item["IsFolder"]:
|
||||||
|
library_info.children.append(BrowseMediaSource(
|
||||||
|
domain=DOMAIN,
|
||||||
|
identifier=f'{Type2Mediatype(item["Type"])}{IDENTIFIER_SPLIT}{item["Id"]}',
|
||||||
|
media_class=Type2Mediaclass(item["Type"]),
|
||||||
|
media_content_type=Type2Mediatype(item["Type"]),
|
||||||
|
title=item["Name"],
|
||||||
|
can_play=IsPlayable(item["Type"]),
|
||||||
|
can_expand=True,
|
||||||
|
children=[],
|
||||||
|
thumbnail=await jelly_cm.get_artwork_url(item["Id"])
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
library_info.children.append(BrowseMediaSource(
|
||||||
|
domain=DOMAIN,
|
||||||
|
identifier=f'{Type2Mediatype(item["Type"])}{IDENTIFIER_SPLIT}{item["Id"]}',
|
||||||
|
media_class=Type2Mediaclass(item["Type"]),
|
||||||
|
media_content_type=Type2Mediatype(item["Type"]),
|
||||||
|
title=item["Name"],
|
||||||
|
can_play=IsPlayable(item["Type"]),
|
||||||
|
can_expand=False,
|
||||||
|
children=[],
|
||||||
|
thumbnail=await jelly_cm.get_artwork_url(item["Id"])
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
library_info.domain=DOMAIN
|
||||||
|
library_info.identifier=f'{Type2Mediatype(item["Type"])}{IDENTIFIER_SPLIT}{item["Id"]}',
|
||||||
|
library_info.title = item["Name"]
|
||||||
|
library_info.media_content_type = Type2Mediatype(item["Type"])
|
||||||
|
library_info.media_class = Type2Mediaclass(item["Type"])
|
||||||
|
library_info.can_expand = False
|
||||||
|
library_info.can_play=IsPlayable(item["Type"]),
|
||||||
|
break
|
||||||
|
|
||||||
|
return library_info
|
||||||
|
|
||||||
Loading…
Reference in a new issue