mirror of
https://github.com/sudoxnym/fin-assistant.git
synced 2026-04-14 19:46:26 +00:00
ADD: service, sensor, media browser
This commit is contained in:
parent
e73f03f697
commit
d9e5e3d5a2
7 changed files with 493 additions and 75 deletions
139
__init__.py
139
__init__.py
|
|
@ -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):
|
||||
|
|
|
|||
1
const.py
1
const.py
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
DOMAIN = "jellyfin"
|
||||
SIGNAL_STATE_UPDATED = "{}.updated".format(DOMAIN)
|
||||
SERVICE_SCAN = "trigger_scan"
|
||||
|
||||
DEFAULT_SSL = False
|
||||
DEFAULT_VERIFY_SSL = True
|
||||
|
|
|
|||
30
device.py
30
device.py
|
|
@ -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
197
media_browser.py
Normal 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
|
||||
|
|
@ -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
96
sensor.py
Normal 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
14
services.yaml
Normal 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
|
||||
Loading…
Reference in a new issue