ADD: service, sensor, media browser

This commit is contained in:
Chris Browet 2021-05-06 17:54:28 +02:00
parent e73f03f697
commit d9e5e3d5a2
7 changed files with 493 additions and 75 deletions

View file

@ -4,7 +4,9 @@ import time
import re
import traceback
import collections.abc
from typing import Mapping, MutableMapping, Sequence, Iterable, List
from typing import Mapping, MutableMapping, Optional, Sequence, Iterable, List, Tuple
import voluptuous as vol
from homeassistant.exceptions import ConfigEntryNotReady
from jellyfin_apiclient_python import JellyfinClient
@ -13,14 +15,13 @@ from jellyfin_apiclient_python.connection_manager import CONNECTION_STATE
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( # pylint: disable=import-error
CONF_HOST,
CONF_PORT,
CONF_SSL,
ATTR_ENTITY_ID,
CONF_URL,
CONF_USERNAME,
CONF_PASSWORD,
CONF_VERIFY_SSL,
CONF_CLIENT_ID,
EVENT_HOMEASSISTANT_STOP,
)
import homeassistant.helpers.config_validation as cv # pylint: disable=import-error
@ -31,6 +32,7 @@ from homeassistant.helpers.dispatcher import ( # pylint: disable=import-error
from .const import (
DOMAIN,
SIGNAL_STATE_UPDATED,
SERVICE_SCAN,
STATE_OFF,
STATE_IDLE,
STATE_PAUSED,
@ -40,13 +42,19 @@ from .device import JellyfinDevice
_LOGGER = logging.getLogger(__name__)
PLATFORMS = ["media_player"]
PLATFORMS = ["sensor", "media_player"]
UPDATE_UNLISTENER = None
USER_APP_NAME = "Home Assistant"
CLIENT_VERSION = "1.0"
PATH_REGEX = re.compile("^(https?://)?([^/:]+)(:[0-9]+)?(/.*)?$")
SCAN_SERVICE_SCHEMA = vol.Schema(
{
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
}
)
def autolog(message):
"Automatically log the current function details."
import inspect
@ -88,21 +96,45 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
UPDATE_UNLISTENER = config_entry.add_update_listener(_update_listener)
hass.data[DOMAIN][config.get(CONF_URL)] = {}
_jelly = JellyfinClientManager(hass, config)
try:
await _jelly.connect()
hass.data[DOMAIN][config.get(CONF_HOST)] = _jelly
hass.data[DOMAIN][config.get(CONF_URL)]["manager"] = _jelly
except:
_LOGGER.error("Cannot connect to Jellyfin server.")
raise
async def service_trigger_scan(service):
entity_id = service.data.get(ATTR_ENTITY_ID)
for sensor in hass.data[DOMAIN][config.get(CONF_URL)]["sensor"]["entities"]:
if sensor.entity_id == entity_id:
await sensor.async_trigger_scan()
hass.services.async_register(
DOMAIN,
SERVICE_SCAN,
service_trigger_scan,
schema=SCAN_SERVICE_SCHEMA,
)
for component in PLATFORMS:
hass.data[DOMAIN][config.get(CONF_URL)][component] = {}
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, component)
)
async_dispatcher_send(hass, SIGNAL_STATE_UPDATED)
async def stop_jellyfin(event):
"""Stop Jellyfin connection."""
await _jelly.stop()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_jellyfin)
await _jelly.start()
return True
@ -115,9 +147,13 @@ class JellyfinClientManager(object):
def __init__(self, hass: HomeAssistant, config_entry):
self.hass = hass
self.callback = lambda client, event_name, data: None
self.client: JellyfinClient = None
self.jf_client: JellyfinClient = None
self.is_stopping = True
self._event_loop = hass.loop
self.host = config_entry[CONF_URL]
self._info = None
self.config_entry = config_entry
self.server_url = ""
@ -227,14 +263,14 @@ class JellyfinClientManager(object):
self.server_url = "".join(filter(bool, (protocol, host, port, path)))
self.client = self.client_factory(self.config_entry)
self.client.auth.connect_to_address(self.server_url)
result = self.client.auth.login(self.server_url, self.config_entry[CONF_USERNAME], self.config_entry[CONF_PASSWORD])
self.jf_client = self.client_factory(self.config_entry)
self.jf_client.auth.connect_to_address(self.server_url)
result = self.jf_client.auth.login(self.server_url, self.config_entry[CONF_USERNAME], self.config_entry[CONF_PASSWORD])
if "AccessToken" not in result:
return False
credentials = self.client.auth.credentials.get_credentials()
self.client.authenticate(credentials)
credentials = self.jf_client.auth.credentials.get_credentials()
self.jf_client.authenticate(credentials)
return True
async def start(self):
@ -243,7 +279,7 @@ class JellyfinClientManager(object):
def event(event_name, data):
_LOGGER.debug("Event: %s", event_name)
if event_name == "WebSocketConnect":
self.client.wsc.send("SessionsStart", "0,1500")
self.jf_client.wsc.send("SessionsStart", "0,1500")
elif event_name == "WebSocketDisconnect":
timeout_gen = self.expo(100)
while not self.is_stopping:
@ -253,37 +289,36 @@ class JellyfinClientManager(object):
timeout
)
)
self.client.stop()
self.jf_client.stop()
time.sleep(timeout)
if self.login():
break
elif event_name == "Sessions":
self._sessions = self.clean_none_dict_values(data)["value"]
self.update_device_list(self._sessions)
self.update_device_list()
else:
self.callback(self.client, event_name, data)
self.callback(self.jf_client, event_name, data)
self.client.callback = event
self.client.callback_ws = event
await self.hass.async_add_executor_job(self.client.start, True)
self.jf_client.callback = event
self.jf_client.callback_ws = event
await self.hass.async_add_executor_job(self.jf_client.start, True)
self.is_stopping = False
self._sessions = self.clean_none_dict_values(await self.hass.async_add_executor_job(self.client.jellyfin.get_sessions))
self.update_device_list(self._sessions)
self._info = await self.hass.async_add_executor_job(self.jf_client.jellyfin._get, "System/Info")
self._sessions = self.clean_none_dict_values(await self.hass.async_add_executor_job(self.jf_client.jellyfin.get_sessions))
async def stop(self):
autolog(">>>")
await self.hass.async_add_executor_job(self.client.stop)
await self.hass.async_add_executor_job(self.jf_client.stop)
self.is_stopping = True
def update_device_list(self, sessions):
def update_device_list(self):
""" Update device list. """
autolog(">>>")
_LOGGER.debug("sessions: %s", str(sessions))
if sessions is None:
# _LOGGER.debug("sessions: %s", str(sessions))
if self._sessions is None:
_LOGGER.error('Error updating Jellyfin devices.')
return
@ -291,8 +326,8 @@ class JellyfinClientManager(object):
new_devices = []
active_devices = []
dev_update = False
for device in sessions:
_LOGGER.debug("device: %s", str(device))
for device in self._sessions:
# _LOGGER.debug("device: %s", str(device))
dev_name = '{}.{}'.format(device['DeviceId'], device['Client'])
try:
@ -385,19 +420,63 @@ class JellyfinClientManager(object):
else:
return False
@property
def info(self):
if self.is_stopping:
return None
return self._info
async def trigger_scan(self):
await self.hass.async_add_executor_job(self.jf_client.jellyfin._post, "Library/Refresh")
async def get_item(self, id):
return await self.hass.async_add_executor_job(self.jf_client.jellyfin.get_item, id)
async def get_items(self, query=None):
response = await self.hass.async_add_executor_job(self.jf_client.jellyfin.users, "/Items", "GET", query)
#_LOGGER.debug("get_items: %s | %s", str(query), str(response))
return response["Items"]
async def set_playstate(self, session_id, state, params):
await self.hass.async_add_executor_job(self.client.jellyfin.post_session, session_id, "Playing/%s" % state, params)
await self.hass.async_add_executor_job(self.jf_client.jellyfin.post_session, session_id, "Playing/%s" % state, params)
async def play_media(self, session_id, media_id):
params = {
"playCommand": "PlayNow",
"itemIds": media_id
}
await self.hass.async_add_executor_job(self.jf_client.jellyfin.post_session, session_id, "Playing", params)
async def get_artwork(self, media_id) -> Tuple[Optional[str], Optional[str]]:
query = {
"format": "PNG",
"maxWidth": 500,
"maxHeight": 500
}
image = await self.hass.async_add_executor_job(self.jf_client.jellyfin.items, "GET", "%s/Images/Primary" % media_id, query)
if image is not None:
return (image, "image/png")
return (None, None)
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)
@property
def api(self):
""" Return the api. """
return self.client.jellyfin
return self.jf_client.jellyfin
@property
def devices(self) -> Mapping[str, JellyfinDevice]:
""" Return devices dictionary. """
return self._devices
@property
def is_available(self):
return not self.is_stopping
# Callbacks
def add_new_devices_callback(self, callback):

View file

@ -2,6 +2,7 @@
DOMAIN = "jellyfin"
SIGNAL_STATE_UPDATED = "{}.updated".format(DOMAIN)
SERVICE_SCAN = "trigger_scan"
DEFAULT_SSL = False
DEFAULT_VERIFY_SSL = True

View file

@ -1,4 +1,5 @@
import logging
from typing import Optional, Tuple
from .const import (
STATE_OFF,
STATE_IDLE,
@ -9,10 +10,11 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
class JellyfinDevice(object):
""" Represents properties of an Emby Device. """
def __init__(self, session, server):
""" Represents properties of an Jellyfin Device. """
def __init__(self, session, jf_manager):
"""Initialize Emby device object."""
self.server = server
self.jf_manager = jf_manager
self.is_active = True
self.update_data(session)
@ -159,7 +161,7 @@ class JellyfinDevice(object):
image_type = 'Primary'
except KeyError:
return None
url = self.server.api.artwork(self.media_id, image_type, 500)
url = self.jf_manager.api.artwork(self.media_id, image_type, 500)
return url
else:
return None
@ -216,15 +218,26 @@ class JellyfinDevice(object):
""" 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.server.set_playstate(self.session_id, state, params)
await self.jf_manager.set_playstate(self.session_id, state, params)
def media_play(self):
""" Send play command to device. """
@ -248,4 +261,7 @@ class JellyfinDevice(object):
def media_seek(self, position):
""" Send seek command to device. """
return self.set_playstate('Seek', position)
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 Normal file
View file

@ -0,0 +1,197 @@
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

View file

@ -1,10 +1,6 @@
"""Support to interface with the Jellyfin API."""
import logging
from typing import Mapping, MutableMapping, Sequence, Iterable, List
from jellyfin_apiclient_python import JellyfinClient
from jellyfin_apiclient_python.connection_manager import CONNECTION_STATE
import voluptuous as vol
from typing import Mapping, MutableMapping, Optional, Sequence, Iterable, List, Tuple, Union
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity
from homeassistant.components.media_player.const import (
@ -12,18 +8,17 @@ from homeassistant.components.media_player.const import (
MEDIA_TYPE_MOVIE,
MEDIA_TYPE_MUSIC,
MEDIA_TYPE_TVSHOW,
SUPPORT_PLAY_MEDIA,
SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE,
SUPPORT_PLAY,
SUPPORT_PREVIOUS_TRACK,
SUPPORT_SEEK,
SUPPORT_STOP,
SUPPORT_BROWSE_MEDIA,
)
from homeassistant.const import (
CONF_API_KEY,
CONF_HOST,
CONF_PORT,
CONF_SSL,
CONF_URL,
DEVICE_DEFAULT_NAME,
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP,
@ -33,15 +28,16 @@ from homeassistant.const import (
STATE_PLAYING,
)
from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
from . import JellyfinClientManager
from . import JellyfinClientManager, autolog
from .media_browser import library_items
from .const import (
DOMAIN,
SIGNAL_STATE_UPDATED,
)
PLATFORM = "media_player"
_LOGGER = logging.getLogger(__name__)
@ -53,8 +49,10 @@ DEFAULT_PORT = 8096
DEFAULT_SSL_PORT = 8920
DEFAULT_SSL = False
SUPPORT_EMBY = (
SUPPORT_PAUSE
SUPPORT_JELLYFIN = (
SUPPORT_BROWSE_MEDIA
| SUPPORT_PLAY_MEDIA
| SUPPORT_PAUSE
| SUPPORT_PREVIOUS_TRACK
| SUPPORT_NEXT_TRACK
| SUPPORT_STOP
@ -62,22 +60,15 @@ SUPPORT_EMBY = (
| SUPPORT_PLAY
)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT): cv.port,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
}
)
async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities):
"""Set up switches dynamically."""
"""Set up media players dynamically."""
active_jellyfin_devices: List[JellyfinMediaPlayer] = {}
inactive_jellyfin_devices: List[JellyfinMediaPlayer] = {}
_jelly: JellyfinClientManager = hass.data[DOMAIN][config_entry.data.get(CONF_HOST)]
_jelly: JellyfinClientManager = hass.data[DOMAIN][config_entry.data.get(CONF_URL)]["manager"]
hass.data[DOMAIN][_jelly.host][PLATFORM]["entities"] = []
@callback
def device_update_callback(data):
@ -115,25 +106,19 @@ async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entitie
_LOGGER.debug("Inactive %s, item: %s", data, rem)
rem.set_available(False)
async def stop_jellyfin(event):
"""Stop Jellyfin connection."""
await _jelly.stop()
_jelly.add_new_devices_callback(device_update_callback)
_jelly.add_stale_devices_callback(device_removal_callback)
await _jelly.start()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_jellyfin)
_jelly.update_device_list()
class JellyfinMediaPlayer(MediaPlayerEntity):
"""Representation of an Jellyfin device."""
def __init__(self, jellyfin: JellyfinClientManager, device_id):
def __init__(self, jelly_cm: JellyfinClientManager, device_id):
"""Initialize the Jellyfin device."""
_LOGGER.debug("New Jellyfin Device initialized with ID: %s", device_id)
self.jellyfin = jellyfin
self.jelly_cm = jelly_cm
self.device_id = device_id
self.device = self.jellyfin.devices[self.device_id]
self.device = self.jelly_cm.devices[self.device_id]
self._available = True
@ -141,8 +126,12 @@ class JellyfinMediaPlayer(MediaPlayerEntity):
self.media_status_received = None
async def async_added_to_hass(self):
"""Register callback."""
self.jellyfin.add_update_callback(self.async_update_callback, self.device_id)
self.hass.data[DOMAIN][self.jelly_cm.host][PLATFORM]["entities"].append(self)
self.jelly_cm.add_update_callback(self.async_update_callback, self.device_id)
async def async_will_remove_from_hass(self):
self.hass.data[DOMAIN][self.jelly_cm.host][PLATFORM]["entities"].remove(self)
self.jelly_cm.remove_update_callback(self.async_update_callback, self.device_id)
@callback
def async_update_callback(self, msg):
@ -159,6 +148,28 @@ class JellyfinMediaPlayer(MediaPlayerEntity):
self.async_write_ha_state()
async def async_get_browse_image(
self,
media_content_type: str,
media_content_id: str,
media_image_id: Optional[str] = None,
) -> Tuple[Optional[str], Optional[str]]:
"""
fetch internally accessible image for media browser.
"""
autolog("<<<")
if media_content_id:
return self.device.get_artwork(media_content_id)
return (None, None)
async def async_browse_media(self, media_content_type=None, media_content_id=None):
"""Implement the media source."""
_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)
@property
def available(self):
"""Return True if entity is available."""
@ -295,7 +306,7 @@ class JellyfinMediaPlayer(MediaPlayerEntity):
def supported_features(self):
"""Flag media player features that are supported."""
if self.supports_remote_control:
return SUPPORT_EMBY
return SUPPORT_JELLYFIN
return 0
async def async_media_play(self):
@ -320,4 +331,8 @@ class JellyfinMediaPlayer(MediaPlayerEntity):
async def async_media_seek(self, position):
"""Send seek command."""
await self.device.media_seek(position)
await self.device.media_seek(position)
async def async_play_media(self, media_type: str, media_id: str, **kwargs) -> None:
_LOGGER.debug("Play media requested: %s / %s", media_type, media_id)
await self.device.play_media(media_id)

96
sensor.py Normal file
View file

@ -0,0 +1,96 @@
"""Support to interface with the Jellyfin API."""
import logging
from homeassistant.helpers.entity import Entity
from homeassistant.const import (
CONF_URL,
DEVICE_DEFAULT_NAME,
STATE_ON,
STATE_OFF,
)
from homeassistant.core import HomeAssistant
import homeassistant.util.dt as dt_util
from . import JellyfinClientManager, autolog
from .const import (
DOMAIN,
)
PLATFORM = "sensor"
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities):
_jelly: JellyfinClientManager = hass.data[DOMAIN][config_entry.data.get(CONF_URL)]["manager"]
hass.data[DOMAIN][_jelly.host][PLATFORM]["entities"] = []
async_add_entities([JellyfinSensor(_jelly)], True)
class JellyfinSensor(Entity):
"""Representation of an Jellyfin device."""
def __init__(self, jelly_cm: JellyfinClientManager):
"""Initialize the Jellyfin device."""
_LOGGER.debug("New Jellyfin Sensor initialized")
self.jelly_cm = jelly_cm
self._available = True
async def async_added_to_hass(self):
self.hass.data[DOMAIN][self.jelly_cm.host][PLATFORM]["entities"].append(self)
async def async_will_remove_from_hass(self):
self.hass.data[DOMAIN][self.jelly_cm.host][PLATFORM]["entities"].remove(self)
@property
def available(self):
"""Return True if entity is available."""
return self.jelly_cm.is_available
@property
def unique_id(self):
"""Return the id of this jellyfin server."""
return self.jelly_cm.info["Id"]
@property
def device_info(self):
"""Return device information about this entity."""
return {
"identifiers": {
# Unique identifiers within a specific domain
(DOMAIN, self.jelly_cm.server_url)
},
"manufacturer": "Jellyfin",
"model": f"Jellyfin {self.jelly_cm.info['Version']}".rstrip(),
"name": self.jelly_cm.info['ServerName'],
}
@property
def name(self):
"""Return the name of the device."""
return f"Jellyfin {self.jelly_cm.info['ServerName']}" or DEVICE_DEFAULT_NAME
@property
def should_poll(self):
"""Return True if entity has to be polled for state."""
return False
@property
def state(self):
"""Return the state of the device."""
return STATE_ON if self.jelly_cm.is_available else STATE_OFF
@property
def extra_state_attributes(self):
"""Return the state attributes."""
return {
"os": self.jelly_cm.info["OperatingSystem"],
"update_available": self.jelly_cm.info["HasUpdateAvailable"],
"version": self.jelly_cm.info["Version"],
}
async def async_trigger_scan(self):
_LOGGER.info("Library scan triggered")
await self.jelly_cm.trigger_scan()

14
services.yaml Normal file
View file

@ -0,0 +1,14 @@
trigger_scan:
# Description of the service
name: Trigger library scan
description: "Trigger a scan on Jellyfin"
fields:
entity_id:
name: Entity
description: Name of entity to send command.
required: true
example: "media_player.jellyfin_firefox"
selector:
entity:
integration: jellyfin
domain: sensor