upcoming and yamc contd

This commit is contained in:
Chris Browet 2021-05-10 18:25:44 +02:00
parent 78fb20fd66
commit b99157c12a
9 changed files with 255 additions and 29 deletions

View file

@ -14,6 +14,7 @@ Jellyfin integration for Home Assistant
- 1 media_player entity per device
- 1 sensor per server
- Supports the "upcoming-media-card" custom card
### Media Browser

View file

@ -1,4 +1,5 @@
"""The jellyfin component."""
import json
import logging
import time
import re
@ -19,6 +20,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( # pylint: disable=import-error
ATTR_ENTITY_ID,
ATTR_ID,
CONF_URL,
CONF_USERNAME,
CONF_PASSWORD,
@ -38,10 +40,17 @@ from .const import (
CLIENT_VERSION,
SIGNAL_STATE_UPDATED,
SERVICE_SCAN,
SERVICE_BROWSE,
SERVICE_DELETE,
SERVICE_YAMC_SETPAGE,
ATTR_PAGE,
STATE_OFF,
STATE_IDLE,
STATE_PAUSED,
STATE_PLAYING,
CONF_GENERATE_UPCOMING,
CONF_GENERATE_YAMC,
YAMC_PAGE_SIZE,
)
_LOGGER = logging.getLogger(__name__)
@ -52,11 +61,39 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30)
PATH_REGEX = re.compile("^(https?://)?([^/:]+)(:[0-9]+)?(/.*)?$")
SCAN_SERVICE_SCHEMA = vol.Schema(
SERVICE_SCHEMA = vol.Schema({
})
SCAN_SERVICE_SCHEMA = SERVICE_SCHEMA.extend(
{
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
}
)
YAMC_SETPAGE_SERVICE_SCHEMA = SERVICE_SCHEMA.extend(
{
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
vol.Required(ATTR_PAGE): vol.All(vol.Coerce(int))
}
)
DELETE_SERVICE_SCHEMA = SERVICE_SCHEMA.extend(
{
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
vol.Required(ATTR_ID): cv.string
}
)
BROWSE_SERVICE_SCHEMA = SERVICE_SCHEMA.extend(
{
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
vol.Required(ATTR_ID): cv.string
}
)
SERVICE_TO_METHOD = {
SERVICE_SCAN: {'method': 'async_trigger_scan', 'schema': SCAN_SERVICE_SCHEMA},
SERVICE_BROWSE: {'method': 'async_browse_item', 'schema': BROWSE_SERVICE_SCHEMA},
SERVICE_DELETE: {'method': 'async_delete_item', 'schema': DELETE_SERVICE_SCHEMA},
SERVICE_YAMC_SETPAGE: {'method': 'async_yamc_setpage', 'schema': YAMC_SETPAGE_SERVICE_SCHEMA},
}
def autolog(message):
"Automatically log the current function details."
@ -108,19 +145,25 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
_LOGGER.error("Cannot connect to Jellyfin server.")
raise
async def service_trigger_scan(service):
async def async_service_handler(service):
"""Map services to methods"""
method = SERVICE_TO_METHOD.get(service.service)
params = {key: value for key, value in service.data.items() if key != "entity_id"}
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()
await getattr(sensor, method['method'])(**params)
hass.services.async_register(
DOMAIN,
SERVICE_SCAN,
service_trigger_scan,
schema=SCAN_SERVICE_SCHEMA,
)
for media_player in hass.data[DOMAIN][config.get(CONF_URL)]["media_player"]["entities"]:
if media_player.entity_id == entity_id:
await getattr(media_player, method['method'])(**params)
for my_service in SERVICE_TO_METHOD:
schema = SERVICE_TO_METHOD[my_service].get('schema', SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, my_service, async_service_handler, schema=schema)
for component in PLATFORMS:
hass.data[DOMAIN][config.get(CONF_URL)][component] = {}
@ -143,6 +186,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
async def _update_listener(hass, config_entry):
"""Update listener."""
_LOGGER.debug("reload triggered")
await hass.config_entries.async_reload(config_entry.entry_id)
@ -403,6 +447,9 @@ class JellyfinDevice(object):
async def play_media(self, media_id):
await self.jf_manager.play_media(self.session_id, media_id)
async def browse_item(self, media_id):
await self.jf_manager.view_media(self.session_id, media_id)
class JellyfinClientManager(object):
def __init__(self, hass: HomeAssistant, config_entry):
self.hass = hass
@ -414,6 +461,8 @@ class JellyfinClientManager(object):
self.host = config_entry[CONF_URL]
self._info = None
self._data = None
self._yamc = None
self._yamc_cur_page = 1
self.config_entry = config_entry
self.server_url = ""
@ -582,12 +631,27 @@ class JellyfinClientManager(object):
@util.Throttle(MIN_TIME_BETWEEN_UPDATES)
async def update_data(self):
self._data = await self.hass.async_add_executor_job(self.jf_client.jellyfin.shows, "/NextUp", {
'Limit': 10,
'UserId': "{UserId}",
"fields": "DateCreated,Studios,Genres"
})
_LOGGER.debug("update_data: %s", str(self._data))
if self.config_entry[CONF_GENERATE_UPCOMING]:
self._data = await self.hass.async_add_executor_job(self.jf_client.jellyfin.shows, "/NextUp", {
'Limit': YAMC_PAGE_SIZE,
'UserId': "{UserId}",
"fields": "DateCreated,Studios,Genres"
})
#_LOGGER.debug("update data: %s", str(self._data))
if self.config_entry[CONF_GENERATE_YAMC]:
self._yamc = await self.hass.async_add_executor_job(self.jf_client.jellyfin.items, "", "GET", {
'startIndex': (self._yamc_cur_page - 1) * YAMC_PAGE_SIZE,
'limit': YAMC_PAGE_SIZE,
'userId': "{UserId}",
'includeItemTypes': "Movie",
'sortBy': 'DateCreated',
'sortOrder': 'Descending',
'recursive': 'true',
"fields": "DateCreated,Studios,Genres,Taglines,ProviderIds",
'collapseBoxSetItems': 'false',
})
_LOGGER.debug("update yamc: %s", str(self._yamc))
def update_device_list(self):
""" Update device list. """
@ -705,7 +769,7 @@ class JellyfinClientManager(object):
@property
def data(self):
"""Upcoming card data"""
if self.is_stopping:
if self.config_entry[CONF_GENERATE_UPCOMING] == False or self.is_stopping:
return None
data = []
@ -734,13 +798,105 @@ class JellyfinClientManager(object):
"poster": self.get_artwork_url(item["Id"]),
"fanart": self.get_artwork_url(item["Id"], "Backdrop"),
"genres": ",".join(item["Genres"]),
"rating": None,
"stream_url": None,
"trakt_url": None,
})
return data
@property
def yamc(self):
"""Upcoming card data"""
if self.config_entry[CONF_GENERATE_YAMC] == False or self.is_stopping:
return None
data = []
data.append({
'title_default': '$title',
'line1_default': '$tagline',
'line2_default': '$empty',
'line3_default': '$release - $genres',
'line4_default': '$runtime - $rating - $info',
'line5_default': '$date',
'text_link_default': '$trakt_url',
'link_default': '$stream_url',
})
if self._yamc is None or "Items" not in self._yamc:
return data
for item in self._yamc["Items"]:
imdbid = None
if item["Type"] == "Movie":
if "ProviderIds" in item and "Imdb" in item["ProviderIds"]:
imdbid = item["ProviderIds"]["Imdb"]
data.append({
"id": item["Id"],
"type": item["Type"],
"title": item["Name"],
"tagline": item["Taglines"][0] if "Taglines" in item and len(item["Taglines"]) > 0 else "",
"flag": False,
"airdate": item["DateCreated"],
#"number": f'S{item["ParentIndexNumber"]}E{item["IndexNumber"]}',
"runtime": int(item["RunTimeTicks"] / 10000000 / 60) if "RunTimeTicks" in item else None,
"studio": ",".join(o["Name"] for o in item["Studios"]),
"release": dt.parse(item["PremiereDate"]).__format__("%Y") if "PremiereDate" in item else None,
"poster": self.get_artwork_url(item["Id"]),
"fanart": self.get_artwork_url(item["Id"], "Backdrop"),
"genres": ",".join(item["Genres"]),
"progress": 0,
"rating": None,
"stream_url": None,
'trakt_url': f"https://trakt.tv/search/imdb/{imdbid}?id_type=movie" if imdbid else "",
})
elif item["Type"] == "Episode":
if "ProviderIds" in item and "Imdb" in item["ProviderIds"]:
imdbid = item["ProviderIds"]["Imdb"]
data.append({
"id": item["Id"],
"type": item["Type"],
"title": item["SeriesName"] if "SeriesName" in item else item["Name"],
"episode": item["Name"],
"tagline": item["Taglines"][0] if "Taglines" in item else "",
"flag": False,
"airdate": item["DateCreated"],
"number": f'S{item["ParentIndexNumber"]}E{item["IndexNumber"]}',
"runtime": int(item["RunTimeTicks"] / 10000000 / 60) if "RunTimeTicks" in item else None,
"studio": ",".join(o["Name"] for o in item["Studios"]),
"release": dt.parse(item["PremiereDate"]).__format__("%d/%m/%Y") if "PremiereDate" in item else None,
"poster": self.get_artwork_url(item["Id"]),
"fanart": self.get_artwork_url(item["Id"], "Backdrop"),
"genres": ",".join(item["Genres"]),
"progress": 0,
"rating": None,
"stream_url": None,
'trakt_url': f"https://trakt.tv/search/imdb/{imdbid}?id_type=movie" if imdbid else "",
})
attrs = {}
attrs["last_search"] = None
attrs["playlists"] = json.dumps([])
attrs['total_items'] = min(50, self._yamc["TotalRecordCount"])
attrs["page"] = self._yamc_cur_page
attrs["page_size"] = YAMC_PAGE_SIZE
attrs['data'] = json.dumps(data)
return attrs
async def trigger_scan(self):
await self.hass.async_add_executor_job(self.jf_client.jellyfin._post, "Library/Refresh")
async def delete_item(self, id):
await self.hass.async_add_executor_job(self.jf_client.jellyfin.items, f"/{id}", "DELETE")
await self.update_data(no_throttle=True)
async def yamc_set_page(self, page):
self._yamc_cur_page = page
await self.update_data(no_throttle=True)
def get_server_url(self) -> str:
return self.jf_client.config.data["auth.server"]
@ -765,6 +921,17 @@ class JellyfinClientManager(object):
}
await self.hass.async_add_executor_job(self.jf_client.jellyfin.post_session, session_id, "Playing", params)
async def view_media(self, session_id, media_id):
item = await self.hass.async_add_executor_job(self.jf_client.jellyfin.get_item, media_id)
_LOGGER.debug(f'view_media: {str(item)}')
params = {
"itemId": media_id,
"itemType": item["Type"],
"itemName": item["Name"]
}
await self.hass.async_add_executor_job(self.jf_client.jellyfin.post_session, session_id, "Viewing", params)
async def get_artwork(self, media_id, type="Primary") -> Tuple[Optional[str], Optional[str]]:
query = {
"format": "PNG",

View file

@ -19,8 +19,8 @@ from .const import (
DOMAIN,
DEFAULT_SSL,
DEFAULT_VERIFY_SSL,
DEFAULT_PORT,
CONN_TIMEOUT,
CONF_GENERATE_UPCOMING,
CONF_GENERATE_YAMC,
)
_LOGGER = logging.getLogger(__name__)
@ -63,6 +63,8 @@ class JellyfinFlowHandler(config_entries.ConfigFlow):
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool,
vol.Optional(CONF_GENERATE_UPCOMING, default=False): bool,
vol.Optional(CONF_GENERATE_YAMC, default=False): bool,
}
if user_input is not None:
@ -70,6 +72,8 @@ class JellyfinFlowHandler(config_entries.ConfigFlow):
self._username = user_input[CONF_USERNAME]
self._password = user_input[CONF_PASSWORD]
self._verify_ssl = user_input[CONF_VERIFY_SSL]
self._generate_upcoming = user_input[CONF_GENERATE_UPCOMING]
self._generate_yamc = user_input[CONF_GENERATE_YAMC]
try:
await self.async_set_unique_id(DOMAIN)
@ -83,6 +87,8 @@ class JellyfinFlowHandler(config_entries.ConfigFlow):
CONF_PASSWORD: self._password,
CONF_VERIFY_SSL: self._verify_ssl,
CONF_CLIENT_ID: str(uuid.uuid4()),
CONF_GENERATE_UPCOMING: self._generate_upcoming,
CONF_GENERATE_YAMC: self._generate_yamc,
},
)
@ -115,6 +121,8 @@ class JellyfinOptionsFlowHandler(config_entries.OptionsFlow):
self._username = config_entry.data[CONF_USERNAME] if CONF_USERNAME in config_entry.data else None
self._password = config_entry.data[CONF_PASSWORD] if CONF_PASSWORD in config_entry.data else None
self._verify_ssl = config_entry.data[CONF_VERIFY_SSL] if CONF_VERIFY_SSL in config_entry.options else DEFAULT_VERIFY_SSL
self._generate_upcoming = config_entry.data[CONF_GENERATE_UPCOMING] if CONF_GENERATE_UPCOMING in config_entry.options else False
self._generate_yamc = config_entry.data[CONF_GENERATE_YAMC] if CONF_GENERATE_YAMC in config_entry.options else False
async def async_step_init(self, user_input=None):
"""Manage the options."""
@ -128,12 +136,16 @@ class JellyfinOptionsFlowHandler(config_entries.OptionsFlow):
self._username = user_input[CONF_USERNAME]
self._password = user_input[CONF_PASSWORD]
self._verify_ssl = user_input[CONF_VERIFY_SSL]
self._generate_upcoming = user_input[CONF_GENERATE_UPCOMING]
self._generate_yamc = user_input[CONF_GENERATE_YAMC]
data_schema = {
vol.Required(CONF_URL, default=self._url): str,
vol.Required(CONF_USERNAME, default=self._username): str,
vol.Required(CONF_PASSWORD, default=self._password): str,
vol.Optional(CONF_VERIFY_SSL, default=self._verify_ssl): bool,
vol.Optional(CONF_GENERATE_UPCOMING, default=self._generate_upcoming): bool,
vol.Optional(CONF_GENERATE_YAMC, default=self._generate_yamc): bool,
}
if user_input is not None:
@ -145,6 +157,8 @@ class JellyfinOptionsFlowHandler(config_entries.OptionsFlow):
CONF_USERNAME: self._username,
CONF_PASSWORD: self._password,
CONF_VERIFY_SSL: self._verify_ssl,
CONF_GENERATE_UPCOMING: self._generate_upcoming,
CONF_GENERATE_YAMC: self._generate_yamc,
},
)

View file

@ -2,7 +2,13 @@
DOMAIN = "jellyfin"
SIGNAL_STATE_UPDATED = "{}.updated".format(DOMAIN)
SERVICE_SCAN = "trigger_scan"
SERVICE_YAMC_SETPAGE = "yamc_setpage"
SERVICE_BROWSE = "browse"
SERVICE_DELETE = "delete"
ATTR_PAGE = 'page'
USER_APP_NAME = "Home Assistant"
CLIENT_VERSION = "1.0"
@ -16,4 +22,9 @@ CONN_TIMEOUT = 5.0
STATE_PLAYING = 'Playing'
STATE_PAUSED = 'Paused'
STATE_IDLE = 'Idle'
STATE_OFF = 'Off'
STATE_OFF = 'Off'
CONF_GENERATE_UPCOMING = "generate_upcoming"
CONF_GENERATE_YAMC = "generate_yamc"
YAMC_PAGE_SIZE=7

View file

@ -336,3 +336,7 @@ class JellyfinMediaPlayer(MediaPlayerEntity):
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)
async def async_browse_item(self, id):
_LOGGER.debug(f"async_browse_item triggered {id}")
await self.device.browse_item(id)

View file

@ -85,13 +85,30 @@ class JellyfinSensor(Entity):
@property
def extra_state_attributes(self):
"""Return the state attributes."""
return {
extra_attr = {
"os": self.jelly_cm.info["OperatingSystem"],
"update_available": self.jelly_cm.info["HasUpdateAvailable"],
"version": self.jelly_cm.info["Version"],
"data": self.jelly_cm.data,
}
if self.jelly_cm.data:
extra_attr["data"] = self.jelly_cm.data
if self.jelly_cm.yamc:
extra_attr["yamc"] = self.jelly_cm.yamc
return extra_attr
async def async_trigger_scan(self):
_LOGGER.info("Library scan triggered")
await self.jelly_cm.trigger_scan()
async def async_delete_item(self, id):
_LOGGER.debug("async_delete_item triggered")
await self.jelly_cm.delete_item(id)
self.async_schedule_update_ha_state()
async def async_yamc_setpage(self, page):
_LOGGER.debug("YAMC setpage: %d", page)
await self.jelly_cm.yamc_set_page(page)
self.async_schedule_update_ha_state()

View file

@ -4,13 +4,15 @@
"flow_title": "Jellyfin Configuration",
"step": {
"user": {
"title": "Luci Config",
"description": "Configure the connection details.",
"title": "Jellyfin Config",
"description": "Configure the integration.",
"data": {
"url": "[%key:common::config_flow::data::url%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]",
"generate_upcoming": "Generate Upcoming card data",
"generate_yamc": "Generate YAMC card data"
}
}
},
@ -33,7 +35,9 @@
"url": "[%key:common::config_flow::data::url%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]",
"generate_upcoming": "Generate Upcoming card data",
"generate_yamc": "Generate YAMC card data"
}
}
},

View file

@ -15,7 +15,9 @@
"url": "Host URL",
"username": "Username",
"password": "Password",
"verify_ssl": "Verify SSL host"
"verify_ssl": "Verify SSL host",
"generate_upcoming": "Generate Upcoming card data",
"generate_yamc": "Generate YAMC card data"
},
"description": "Configure the connection details.",
"title": "Jellyfin"
@ -38,7 +40,9 @@
"url": "Host URL",
"username": "Username",
"password": "Password",
"verify_ssl": "Verify SSL host"
"verify_ssl": "Verify SSL host",
"generate_upcoming": "Generate Upcoming card data",
"generate_yamc": "Generate YAMC card data"
},
"description": "Configure the connection details.",
"title": "Jellyfin"

View file

@ -15,7 +15,9 @@
"url": "URL Serveur",
"username": "Utilisateur",
"password": "Mot de passe",
"verify_ssl": "Vérification SSL"
"verify_ssl": "Vérification SSL",
"generate_upcoming": "Générer les données pour la carte Upcoming",
"generate_yamc": "Générer les données pour la carte YAMC"
},
"description": "Configuration des paramètres de connexion.",
"title": "Jellyfin"
@ -38,7 +40,9 @@
"url": "URL Serveur",
"username": "Utilisateur",
"password": "Mot de passe",
"verify_ssl": "Vérification SSL"
"verify_ssl": "Vérification SSL",
"generate_upcoming": "Générer les données pour la carte Upcoming",
"generate_yamc": "Générer les données pour la carte YAMC"
},
"description": "Configuration des paramètres de connexion.",
"title": "Jellyfin"