From 72e13f2c56393bb98ae7e1411731780c8f8f950b Mon Sep 17 00:00:00 2001 From: micha91 Date: Tue, 28 Mar 2023 18:19:48 +0200 Subject: [PATCH] Bae implementation of Plex Music Provider (#586) - Base implementation - Essential features present - Authentication is manual action with entering a token --- .../server/providers/plex/__init__.py | 484 ++++++++++++++++++ .../server/providers/plex/icon.png | Bin 0 -> 40346 bytes .../server/providers/plex/manifest.json | 10 + requirements_all.txt | 1 + 4 files changed, 495 insertions(+) create mode 100644 music_assistant/server/providers/plex/__init__.py create mode 100644 music_assistant/server/providers/plex/icon.png create mode 100644 music_assistant/server/providers/plex/manifest.json diff --git a/music_assistant/server/providers/plex/__init__.py b/music_assistant/server/providers/plex/__init__.py new file mode 100644 index 00000000..b24e27ca --- /dev/null +++ b/music_assistant/server/providers/plex/__init__.py @@ -0,0 +1,484 @@ +"""Plex musicprovider support for MusicAssistant.""" +from asyncio import TaskGroup +from collections.abc import AsyncGenerator, Callable, Coroutine + +from aiohttp import ClientTimeout +from plexapi.audio import Album as PlexAlbum +from plexapi.audio import Artist as PlexArtist +from plexapi.audio import Playlist as PlexPlaylist +from plexapi.audio import Track as PlexTrack +from plexapi.library import MusicSection as PlexMusicSection +from plexapi.media import AudioStream as PlexAudioStream +from plexapi.media import Media as PlexMedia +from plexapi.media import MediaPart as PlexMediaPart +from plexapi.myplex import MyPlexAccount +from plexapi.server import PlexServer + +from music_assistant.common.helpers.uri import create_uri +from music_assistant.common.helpers.util import create_sort_name +from music_assistant.common.models.config_entries import ConfigEntry, ProviderConfig +from music_assistant.common.models.enums import ( + ConfigEntryType, + ContentType, + ImageType, + MediaType, + ProviderFeature, +) +from music_assistant.common.models.errors import InvalidDataError, LoginFailed, MediaNotFoundError +from music_assistant.common.models.media_items import ( + Album, + Artist, + ItemMapping, + MediaItem, + MediaItemImage, + Playlist, + ProviderMapping, + SearchResults, + StreamDetails, + Track, +) +from music_assistant.common.models.provider import ProviderManifest +from music_assistant.server import MusicAssistant +from music_assistant.server.helpers.tags import parse_tags +from music_assistant.server.models import ProviderInstanceType +from music_assistant.server.models.music_provider import MusicProvider + +CONF_AUTH_TOKEN = "token" +CONF_SERVER_NAME = "server" +CONF_LIBRARY_NAME = "library" + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + if not config.get_value(CONF_AUTH_TOKEN): + raise LoginFailed("Invalid login credentials") + + prov = PlexProvider(mass, manifest, config) + await prov.handle_setup() + return prov + + +async def get_config_entries( + mass: MusicAssistant, manifest: ProviderManifest # noqa: ARG001 +) -> tuple[ConfigEntry, ...]: + """Return Config entries to setup this provider.""" + return ( + ConfigEntry( + key=CONF_SERVER_NAME, type=ConfigEntryType.STRING, label="Server", required=True + ), + ConfigEntry( + key=CONF_LIBRARY_NAME, type=ConfigEntryType.STRING, label="Library", required=True + ), + ConfigEntry( + key=CONF_AUTH_TOKEN, type=ConfigEntryType.SECURE_STRING, label="Token", required=True + ), + ) + + +class PlexProvider(MusicProvider): + """Provider for a plex music library.""" + + _plex_server: PlexServer = None + _plex_library: PlexMusicSection = None + + async def handle_setup(self) -> None: + """Set up the music provider by connecting to the server.""" + + def connect(): + plex_account = MyPlexAccount(token=self.config.get_value(CONF_AUTH_TOKEN)) + return plex_account.resource(self.config.get_value(CONF_SERVER_NAME)).connect() + + self._plex_server = await self._run_async(connect) + self._plex_library = await self._run_async( + self._plex_server.library.section, self.config.get_value(CONF_LIBRARY_NAME) + ) + + async def resolve_image(self, path: str) -> str | bytes | AsyncGenerator[bytes, None]: + """Return the full image URL including the auth token.""" + return self._plex_server.url(path, True) + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return a list of supported features.""" + return ( + ProviderFeature.LIBRARY_ARTISTS, + ProviderFeature.LIBRARY_ALBUMS, + ProviderFeature.LIBRARY_TRACKS, + ProviderFeature.LIBRARY_PLAYLISTS, + ProviderFeature.BROWSE, + ProviderFeature.SEARCH, + ProviderFeature.ARTIST_ALBUMS, + ) + + async def _run_async(self, call: Callable, *args, **kwargs): + return await self.mass.create_task(call, *args, **kwargs) + + async def _get_data(self, key, cls=None): + return await self._run_async(self._plex_library.fetchItem, key, cls) + + def _get_item_mapping(self, media_type: MediaType, key: str, name: str) -> ItemMapping: + return ItemMapping( + media_type, + key, + self.domain, + name, + create_uri(media_type, self.domain, key), + create_sort_name(self.name), + ) + + async def _parse(self, plex_media) -> MediaItem | None: + if plex_media.type == "artist": + return await self._parse_artist(plex_media) + elif plex_media.type == "album": + return await self._parse_album(plex_media) + elif plex_media.type == "track": + return await self._parse_track(plex_media) + elif plex_media.type == "playlist": + return await self._parse_playlist(plex_media) + return None + + async def _search_track(self, search_query, limit) -> list[PlexTrack]: + return await self._run_async( + self._plex_library.searchTracks, title=search_query, limit=limit + ) + + async def _search_album(self, search_query, limit) -> list[PlexAlbum]: + return await self._run_async( + self._plex_library.searchAlbums, title=search_query, limit=limit + ) + + async def _search_artist(self, search_query, limit) -> list[PlexArtist]: + return await self._run_async( + self._plex_library.searchArtists, title=search_query, limit=limit + ) + + async def _search_playlist(self, search_query, limit) -> list[PlexPlaylist]: + return await self._run_async(self._plex_library.playlists, title=search_query, limit=limit) + + async def _search_track_advanced(self, limit, **kwargs) -> list[PlexTrack]: + return await self._run_async(self._plex_library.searchTracks, filters=kwargs, limit=limit) + + async def _search_album_advanced(self, limit, **kwargs) -> list[PlexAlbum]: + return await self._run_async(self._plex_library.searchAlbums, filters=kwargs, limit=limit) + + async def _search_artist_advanced(self, limit, **kwargs) -> list[PlexArtist]: + return await self._run_async(self._plex_library.searchArtists, filters=kwargs, limit=limit) + + async def _search_playlist_advanced(self, limit, **kwargs) -> list[PlexPlaylist]: + return await self._run_async(self._plex_library.playlists, filters=kwargs, limit=limit) + + async def _search_and_parse( + self, search_coro: Coroutine, parse_coro: Callable + ) -> list[MediaItem]: + task_results = [] + async with TaskGroup() as tg: + for item in await search_coro: + task_results.append(tg.create_task(parse_coro(item))) + + results = [] + for task in task_results: + results.append(task.result()) + + return results + + async def _parse_album(self, plex_album: PlexAlbum) -> Album: + """Parse a Plex Album response to an Album model object.""" + album_id = plex_album.key + album = Album( + item_id=album_id, + provider=self.domain, + name=plex_album.title, + ) + if plex_album.year: + album.year = plex_album.year + if thumb := plex_album.firstAttr("thumb", "parentThumb", "grandparentThumb"): + album.metadata.images = [MediaItemImage(ImageType.THUMB, thumb, self.instance_id)] + if plex_album.summary: + album.metadata.description = plex_album.summary + + album.artist = self._get_item_mapping( + MediaType.ARTIST, plex_album.parentKey, plex_album.parentTitle + ) + + album.add_provider_mapping( + ProviderMapping( + item_id=str(album_id), + provider_domain=self.domain, + provider_instance=self.instance_id, + url=plex_album.getWebURL(), + ) + ) + return album + + async def _parse_artist(self, plex_artist: PlexArtist) -> Artist: + """Parse a Plex Artist response to Artist model object.""" + artist_id = plex_artist.key + if not artist_id: + raise InvalidDataError("Artist does not have a valid ID") + artist = Artist(item_id=artist_id, name=plex_artist.title, provider=self.domain) + if plex_artist.summary: + artist.metadata.description = plex_artist.summary + if thumb := plex_artist.firstAttr("thumb", "parentThumb", "grandparentThumb"): + artist.metadata.images = [MediaItemImage(ImageType.THUMB, thumb, self.instance_id)] + artist.add_provider_mapping( + ProviderMapping( + item_id=str(artist_id), + provider_domain=self.domain, + provider_instance=self.instance_id, + url=plex_artist.getWebURL(), + ) + ) + return artist + + async def _parse_playlist(self, plex_playlist: PlexPlaylist) -> Playlist: + """Parse a Plex Playlist response to a Playlist object.""" + playlist = Playlist( + item_id=plex_playlist.key, provider=self.domain, name=plex_playlist.title + ) + if plex_playlist.summary: + playlist.metadata.description = plex_playlist.summary + if thumb := plex_playlist.firstAttr("thumb", "parentThumb", "grandparentThumb"): + playlist.metadata.images = [MediaItemImage(ImageType.THUMB, thumb, self.instance_id)] + playlist.is_editable = True + playlist.add_provider_mapping( + ProviderMapping( + item_id=plex_playlist.key, + provider_domain=self.domain, + provider_instance=self.instance_id, + url=plex_playlist.getWebURL(), + ) + ) + return playlist + + async def _parse_track(self, plex_track: PlexTrack) -> Track: + """Parse a Plex Track response to a Track model object.""" + track = Track(item_id=plex_track.key, provider=self.domain, name=plex_track.title) + + if plex_track.grandparentKey: + track.artist = self._get_item_mapping( + MediaType.ARTIST, plex_track.grandparentKey, plex_track.grandparentTitle + ) + if thumb := plex_track.firstAttr("thumb", "parentThumb", "grandparentThumb"): + track.metadata.images = [MediaItemImage(ImageType.THUMB, thumb, self.instance_id)] + if plex_track.parentKey: + track.album = self._get_item_mapping( + MediaType.ALBUM, plex_track.parentKey, plex_track.parentKey + ) + if plex_track.duration: + track.duration = int(plex_track.duration / 1000) + if plex_track.trackNumber: + track.track_number = plex_track.trackNumber + if plex_track.parentIndex: + track.disc_number = plex_track.parentIndex + available = False + content = None + + if plex_track.media: + available = True + content = plex_track.media[0].container + + track.add_provider_mapping( + ProviderMapping( + item_id=plex_track.key, + provider_domain=self.domain, + provider_instance=self.instance_id, + available=available, + content_type=ContentType.try_parse(content) if content else None, + url=plex_track.getWebURL(), + ) + ) + return track + + async def search( + self, + search_query: str, + media_types: list[MediaType] | None = None, + limit: int = 20, + ) -> SearchResults: + """Perform search on the plex library. + + :param search_query: Search query. + :param media_types: A list of media_types to include. All types if None. + :param limit: Number of items to return in the search (per type). + """ + if not media_types: + media_types = [MediaType.ARTIST, MediaType.ALBUM, MediaType.TRACK, MediaType.PLAYLIST] + + tasks = {} + + async with TaskGroup() as tg: + for media_type in media_types: + if media_type == MediaType.ARTIST: + tasks[MediaType.ARTIST] = tg.create_task( + self._search_and_parse( + self._search_artist(search_query, limit), self._parse_artist + ) + ) + elif media_type == MediaType.ALBUM: + tasks[MediaType.ARTIST] = tg.create_task( + self._search_and_parse( + self._search_album(search_query, limit), self._parse_album + ) + ) + elif media_type == MediaType.TRACK: + tasks[MediaType.ARTIST] = tg.create_task( + self._search_and_parse( + self._search_track(search_query, limit), self._parse_track + ) + ) + elif media_type == MediaType.PLAYLIST: + tasks[MediaType.ARTIST] = tg.create_task( + self._search_and_parse( + self._search_playlist(search_query, limit), self._parse_playlist + ) + ) + + search_results = SearchResults() + + for media_type, task in tasks.items(): + if media_type == MediaType.ARTIST: + search_results.artists = task.result() + elif media_type == MediaType.ALBUM: + search_results.albums = task.result() + elif media_type == MediaType.TRACK: + search_results.tracks = task.result() + elif media_type == MediaType.PLAYLIST: + search_results.playlists = task.result() + + return search_results + + async def get_library_artists(self) -> AsyncGenerator[Artist, None]: + """Retrieve all library artists from Plex Music.""" + artists_obj = await self._run_async(self._plex_library.all) + for artist in artists_obj: + yield await self._parse_artist(artist) + + async def get_library_albums(self) -> AsyncGenerator[Album, None]: + """Retrieve all library albums from Plex Music.""" + albums_obj = await self._run_async(self._plex_library.albums) + for album in albums_obj: + yield await self._parse_album(album) + + async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: + """Retrieve all library playlists from the provider.""" + playlists_obj = await self._run_async(self._plex_library.playlists) + for playlist in playlists_obj: + yield await self._parse_playlist(playlist) + + async def get_library_tracks(self) -> AsyncGenerator[Track, None]: + """Retrieve library tracks from Plex Music.""" + tracks_obj = await self._search_track(None, limit=99999) + for track in tracks_obj: + yield await self._parse_track(track) + + async def get_album(self, prov_album_id) -> Album: + """Get full album details by id.""" + plex_album = await self._get_data(prov_album_id, PlexAlbum) + return await self._parse_album(plex_album) if plex_album else None + + async def get_album_tracks(self, prov_album_id: str) -> list[Track]: + """Get album tracks for given album id.""" + plex_album = await self._get_data(prov_album_id, PlexAlbum) + + tracks = [] + for plex_track in await self._run_async(plex_album.tracks): + track = await self._parse_track(plex_track) + tracks.append(track) + return tracks + + async def get_artist(self, prov_artist_id) -> Artist: + """Get full artist details by id.""" + plex_artist = await self._get_data(prov_artist_id, PlexArtist) + return await self._parse_artist(plex_artist) if plex_artist else None + + async def get_track(self, prov_track_id) -> Track: + """Get full track details by id.""" + plex_track = await self._get_data(prov_track_id, PlexTrack) + return await self._parse_track(plex_track) + + async def get_playlist(self, prov_playlist_id) -> Playlist: + """Get full playlist details by id.""" + plex_playlist = await self._get_data(prov_playlist_id, PlexPlaylist) + return await self._parse_playlist(plex_playlist) + + async def get_playlist_tracks( # type: ignore[return] + self, prov_playlist_id: str + ) -> AsyncGenerator[Track, None]: + """Get all playlist tracks for given playlist id.""" + plex_playlist = await self._get_data(prov_playlist_id, PlexPlaylist) + + playlist_items = await self._run_async(plex_playlist.items) + + if not playlist_items: + yield None + for index, plex_track in enumerate(playlist_items): + track = await self._parse_track(plex_track) + if track: + track.position = index + 1 + yield track + + async def get_artist_albums(self, prov_artist_id) -> list[Album]: + """Get a list of albums for the given artist.""" + plex_artist = await self._get_data(prov_artist_id, PlexArtist) + plex_albums = await self._run_async(plex_artist.albums) + if plex_albums: + albums = [] + for album_obj in plex_albums: + albums.append(await self._parse_album(album_obj)) + return albums + return [] + + async def get_stream_details(self, item_id: str) -> StreamDetails | None: + """Get streamdetails for a track.""" + plex_track = await self._get_data(item_id, PlexTrack) + if not plex_track or not plex_track.media: + raise MediaNotFoundError(f"track {item_id} not found") + + media: PlexMedia = plex_track.media[0] + + media_type = ContentType.try_parse(media.container) + media_part: PlexMediaPart = media.parts[0] + audio_stream: PlexAudioStream = media_part.audioStreams()[0] + + stream_details = StreamDetails( + item_id=plex_track.key, + provider=self.domain, + content_type=ContentType.try_parse(media.container), + duration=plex_track.duration, + channels=media.audioChannels, + data=plex_track, + ) + + if audio_stream.loudness: + stream_details.loudness = audio_stream.loudness + + if media_type != ContentType.M4A: + stream_details.direct = self._plex_server.url(media_part.key, True) + if audio_stream.samplingRate: + stream_details.sample_rate = audio_stream.samplingRate + if audio_stream.bitDepth: + stream_details.bit_depth = audio_stream.bitDepth + + else: + url = plex_track.getStreamURL() + media_info = await parse_tags(url) + + stream_details.channels = media_info.channels + stream_details.content_type = ContentType.try_parse(media_info.format) + stream_details.sample_rate = media_info.sample_rate + stream_details.bit_depth = media_info.bits_per_sample + + return stream_details + + async def get_audio_stream( + self, streamdetails: StreamDetails, seek_position: int = 0 + ) -> AsyncGenerator[bytes, None]: + """Return the audio stream for the provider item.""" + url = streamdetails.data.getStreamURL(offset=seek_position) + + timeout = ClientTimeout(total=0, connect=30, sock_read=600) + async with self.mass.http_session.get(url, timeout=timeout) as resp: + async for chunk in resp.content.iter_any(): + yield chunk diff --git a/music_assistant/server/providers/plex/icon.png b/music_assistant/server/providers/plex/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..85bf53fe869b841942209533c906c41d6e8cc634 GIT binary patch literal 40346 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 literal 0 HcmV?d00001 diff --git a/music_assistant/server/providers/plex/manifest.json b/music_assistant/server/providers/plex/manifest.json new file mode 100644 index 00000000..7a48bfd0 --- /dev/null +++ b/music_assistant/server/providers/plex/manifest.json @@ -0,0 +1,10 @@ +{ + "type": "music", + "domain": "plex", + "name": "Plex Media Server Library", + "description": "Support for the Plex streaming provider in Music Assistant.", + "codeowners": ["@micha91"], + "requirements": ["plexapi==4.13.2"], + "documentation": "https://github.com/music-assistant/hass-music-assistant/discussions/816", + "multi_instance": true +} diff --git a/requirements_all.txt b/requirements_all.txt index 5efd7f62..0b6f1c0b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -16,6 +16,7 @@ memory-tempfile==2.2.3 music-assistant-frontend==20230327.1 orjson==3.8.7 pillow==9.4.0 +plexapi==4.13.2 PyChromecast==13.0.5 python-slugify==8.0.1 shortuuid==1.0.11 -- 2.34.1