1 """The Media Source implementation for the Jellyfin integration."""
3 from __future__
import annotations
10 from jellyfin_apiclient_python.api
import jellyfin_url
11 from jellyfin_apiclient_python.client
import JellyfinClient
22 from .
import JellyfinConfigEntry
24 COLLECTION_TYPE_MOVIES,
25 COLLECTION_TYPE_MUSIC,
28 ITEM_KEY_COLLECTION_TYPE,
31 ITEM_KEY_INDEX_NUMBER,
32 ITEM_KEY_MEDIA_SOURCES,
44 MEDIA_SOURCE_KEY_PATH,
49 SUPPORTED_COLLECTION_TYPES,
52 _LOGGER = logging.getLogger(__name__)
56 """Set up Jellyfin media source."""
58 entry: JellyfinConfigEntry = hass.config_entries.async_entries(DOMAIN)[0]
59 coordinator = entry.runtime_data
65 """Represents a Jellyfin server."""
67 name: str =
"Jellyfin"
70 self, hass: HomeAssistant, client: JellyfinClient, entry: JellyfinConfigEntry
72 """Initialize the Jellyfin media source."""
79 self.
apiapi = client.jellyfin
80 self.
urlurl = jellyfin_url(client,
"")
83 """Return a streamable URL and associated mime type."""
84 media_item = await self.
hasshass.async_add_executor_job(
85 self.
apiapi.get_item, item.identifier
92 assert mime_type
is not None
94 return PlayMedia(stream_url, mime_type)
97 """Return a browsable Jellyfin media source."""
98 if not item.identifier:
101 media_item = await self.
hasshass.async_add_executor_job(
102 self.
apiapi.get_item, item.identifier
105 item_type = media_item[
"Type"]
106 if item_type == ITEM_TYPE_LIBRARY:
108 if item_type == ITEM_TYPE_ARTIST:
109 return await self.
_build_artist_build_artist(media_item,
True)
110 if item_type == ITEM_TYPE_ALBUM:
111 return await self.
_build_album_build_album(media_item,
True)
112 if item_type == ITEM_TYPE_SERIES:
113 return await self.
_build_series_build_series(media_item,
True)
114 if item_type == ITEM_TYPE_SEASON:
115 return await self.
_build_season_build_season(media_item,
True)
117 raise BrowseError(f
"Unsupported item type {item_type}")
120 """Return all supported libraries the user has access to as media sources."""
121 base = BrowseMediaSource(
124 media_class=MediaClass.DIRECTORY,
125 media_content_type=MEDIA_TYPE_NONE,
129 children_media_class=MediaClass.DIRECTORY,
136 for library
in libraries:
137 base.children.append(await self.
_build_library_build_library(library,
False))
142 """Return all supported libraries a user has access to."""
143 response = await self.
hasshass.async_add_executor_job(self.
apiapi.get_media_folders)
144 libraries = response[
"Items"]
146 for library
in libraries:
147 if ITEM_KEY_COLLECTION_TYPE
in library:
148 if library[ITEM_KEY_COLLECTION_TYPE]
in SUPPORTED_COLLECTION_TYPES:
149 result.append(library)
153 self, library: dict[str, Any], include_children: bool
154 ) -> BrowseMediaSource:
155 """Return a single library as a browsable media source."""
156 collection_type = library[ITEM_KEY_COLLECTION_TYPE]
158 if collection_type == COLLECTION_TYPE_MUSIC:
160 if collection_type == COLLECTION_TYPE_MOVIES:
165 self, library: dict[str, Any], include_children: bool
166 ) -> BrowseMediaSource:
167 """Return a single music library as a browsable media source."""
168 library_id = library[ITEM_KEY_ID]
169 library_name = library[ITEM_KEY_NAME]
171 result = BrowseMediaSource(
173 identifier=library_id,
174 media_class=MediaClass.DIRECTORY,
175 media_content_type=MEDIA_TYPE_NONE,
182 result.children_media_class = MediaClass.ARTIST
183 result.children = await self.
_build_artists_build_artists(library_id)
184 if not result.children:
185 result.children_media_class = MediaClass.ALBUM
186 result.children = await self.
_build_albums_build_albums(library_id)
191 """Return all artists in the music library."""
192 artists = await self.
_get_children_get_children(library_id, ITEM_TYPE_ARTIST)
198 ITEM_KEY_NAME
not in k,
199 k.get(ITEM_KEY_NAME),
202 return [await self.
_build_artist_build_artist(artist,
False)
for artist
in artists]
205 self, artist: dict[str, Any], include_children: bool
206 ) -> BrowseMediaSource:
207 """Return a single artist as a browsable media source."""
208 artist_id = artist[ITEM_KEY_ID]
209 artist_name = artist[ITEM_KEY_NAME]
212 result = BrowseMediaSource(
214 identifier=artist_id,
215 media_class=MediaClass.ARTIST,
216 media_content_type=MEDIA_TYPE_NONE,
220 thumbnail=thumbnail_url,
224 result.children_media_class = MediaClass.ALBUM
225 result.children = await self.
_build_albums_build_albums(artist_id)
230 """Return all albums of a single artist as browsable media sources."""
231 albums = await self.
_get_children_get_children(parent_id, ITEM_TYPE_ALBUM)
237 ITEM_KEY_NAME
not in k,
238 k.get(ITEM_KEY_NAME),
241 return [await self.
_build_album_build_album(album,
False)
for album
in albums]
244 self, album: dict[str, Any], include_children: bool
245 ) -> BrowseMediaSource:
246 """Return a single album as a browsable media source."""
247 album_id = album[ITEM_KEY_ID]
248 album_title = album[ITEM_KEY_NAME]
251 result = BrowseMediaSource(
254 media_class=MediaClass.ALBUM,
255 media_content_type=MEDIA_TYPE_NONE,
259 thumbnail=thumbnail_url,
263 result.children_media_class = MediaClass.TRACK
264 result.children = await self.
_build_tracks_build_tracks(album_id)
269 """Return all tracks of a single album as browsable media sources."""
270 tracks = await self.
_get_children_get_children(album_id, ITEM_TYPE_AUDIO)
276 ITEM_KEY_INDEX_NUMBER
not in k,
277 k.get(ITEM_KEY_INDEX_NUMBER),
287 """Return a single track as a browsable media source."""
288 track_id = track[ITEM_KEY_ID]
289 track_title = track[ITEM_KEY_NAME]
293 return BrowseMediaSource(
296 media_class=MediaClass.TRACK,
297 media_content_type=mime_type,
301 thumbnail=thumbnail_url,
305 self, library: dict[str, Any], include_children: bool
306 ) -> BrowseMediaSource:
307 """Return a single movie library as a browsable media source."""
308 library_id = library[ITEM_KEY_ID]
309 library_name = library[ITEM_KEY_NAME]
311 result = BrowseMediaSource(
313 identifier=library_id,
314 media_class=MediaClass.DIRECTORY,
315 media_content_type=MEDIA_TYPE_NONE,
322 result.children_media_class = MediaClass.MOVIE
323 result.children = await self.
_build_movies_build_movies(library_id)
328 """Return all movies in the movie library."""
329 movies = await self.
_get_children_get_children(library_id, ITEM_TYPE_MOVIE)
335 ITEM_KEY_NAME
not in k,
336 k.get(ITEM_KEY_NAME),
346 """Return a single movie as a browsable media source."""
347 movie_id = movie[ITEM_KEY_ID]
348 movie_title = movie[ITEM_KEY_NAME]
352 return BrowseMediaSource(
355 media_class=MediaClass.MOVIE,
356 media_content_type=mime_type,
360 thumbnail=thumbnail_url,
364 self, library: dict[str, Any], include_children: bool
365 ) -> BrowseMediaSource:
366 """Return a single tv show library as a browsable media source."""
367 library_id = library[ITEM_KEY_ID]
368 library_name = library[ITEM_KEY_NAME]
370 result = BrowseMediaSource(
372 identifier=library_id,
373 media_class=MediaClass.DIRECTORY,
374 media_content_type=MEDIA_TYPE_NONE,
381 result.children_media_class = MediaClass.TV_SHOW
382 result.children = await self.
_build_tvshow_build_tvshow(library_id)
387 """Return all series in the tv library."""
388 series = await self.
_get_children_get_children(library_id, ITEM_TYPE_SERIES)
394 ITEM_KEY_NAME
not in k,
395 k.get(ITEM_KEY_NAME),
398 return [await self.
_build_series_build_series(s,
False)
for s
in series]
401 self, series: dict[str, Any], include_children: bool
402 ) -> BrowseMediaSource:
403 """Return a single series as a browsable media source."""
404 series_id = series[ITEM_KEY_ID]
405 series_title = series[ITEM_KEY_NAME]
408 result = BrowseMediaSource(
410 identifier=series_id,
411 media_class=MediaClass.TV_SHOW,
412 media_content_type=MEDIA_TYPE_NONE,
416 thumbnail=thumbnail_url,
420 result.children_media_class = MediaClass.SEASON
421 result.children = await self.
_build_seasons_build_seasons(series_id)
426 """Return all seasons in the series."""
427 seasons = await self.
_get_children_get_children(series_id, ITEM_TYPE_SEASON)
433 ITEM_KEY_INDEX_NUMBER
not in k,
434 k.get(ITEM_KEY_INDEX_NUMBER),
437 return [await self.
_build_season_build_season(season,
False)
for season
in seasons]
440 self, season: dict[str, Any], include_children: bool
441 ) -> BrowseMediaSource:
442 """Return a single series as a browsable media source."""
443 season_id = season[ITEM_KEY_ID]
444 season_title = season[ITEM_KEY_NAME]
447 result = BrowseMediaSource(
449 identifier=season_id,
450 media_class=MediaClass.TV_SHOW,
451 media_content_type=MEDIA_TYPE_NONE,
455 thumbnail=thumbnail_url,
459 result.children_media_class = MediaClass.EPISODE
465 """Return all episode in the season."""
466 episodes = await self.
_get_children_get_children(season_id, ITEM_TYPE_EPISODE)
472 ITEM_KEY_INDEX_NUMBER
not in k,
473 k.get(ITEM_KEY_INDEX_NUMBER),
478 for episode
in episodes
483 """Return a single episode as a browsable media source."""
484 episode_id = episode[ITEM_KEY_ID]
485 episode_title = episode[ITEM_KEY_NAME]
489 return BrowseMediaSource(
491 identifier=episode_id,
492 media_class=MediaClass.EPISODE,
493 media_content_type=mime_type,
497 thumbnail=thumbnail_url,
501 self, parent_id: str, item_type: str
502 ) -> list[dict[str, Any]]:
503 """Return all children for the parent_id whose item type is item_type."""
506 "ParentId": parent_id,
507 "IncludeItemTypes": item_type,
509 if item_type
in PLAYABLE_ITEM_TYPES:
510 params[
"Fields"] = ITEM_KEY_MEDIA_SOURCES
512 result = await self.
hasshass.async_add_executor_job(self.
apiapi.user_items,
"", params)
513 return result[
"Items"]
516 """Return the URL for the primary image of a media item if available."""
517 image_tags = media_item[ITEM_KEY_IMAGE_TAGS]
519 if "Primary" not in image_tags:
522 item_id = media_item[ITEM_KEY_ID]
523 return str(self.
apiapi.artwork(item_id,
"Primary", MAX_IMAGE_WIDTH))
526 """Return the stream URL for a media item."""
527 media_type = media_item[ITEM_KEY_MEDIA_TYPE]
528 item_id = media_item[ITEM_KEY_ID]
530 if media_type == MEDIA_TYPE_AUDIO:
531 if audio_codec := self.
entryentry.options.get(CONF_AUDIO_CODEC):
532 return self.
apiapi.audio_url(item_id, audio_codec=audio_codec)
533 return self.
apiapi.audio_url(item_id)
534 if media_type == MEDIA_TYPE_VIDEO:
535 return self.
apiapi.video_url(item_id)
537 raise BrowseError(f
"Unsupported media type {media_type}")
541 """Return the mime type of a media item."""
542 if not media_item.get(ITEM_KEY_MEDIA_SOURCES):
543 _LOGGER.debug(
"Unable to determine mime type for item without media source")
546 media_source = media_item[ITEM_KEY_MEDIA_SOURCES][0]
548 if MEDIA_SOURCE_KEY_PATH
not in media_source:
549 _LOGGER.debug(
"Unable to determine mime type for media source without path")
552 path = media_source[MEDIA_SOURCE_KEY_PATH]
553 mime_type, _ = mimetypes.guess_type(path)
555 if mime_type
is None:
557 "Unable to determine mime type for path %s", os.path.basename(path)