Source code for soco.plugins.plex

"""This plugin supports playback from a linked Plex music service.
See: https://support.plex.tv/articles/218168898-installing-plex-for-sonos/

Requires:
    * Plex music service must be linked in the Sonos app
    * Use of 'plexapi' library (https://github.com/pkkid/python-plexapi)
    * Plex server URI used in 'plexapi' must be reachable from Sonos speakers

    Example usage:

        >>> from plexapi.server import PlexServer
        >>> from soco import SoCo
        >>> from soco.plugins.plex import PlexPlugin
        >>>
        >>> s = SoCo("<SPEAKER_IP>")
        >>> plugin = PlexPlugin(s)
        >>>
        >>> plex_uri = "http://1.2.3.4:32400"
        >>> plex_token = "<YOUR_PLEX_TOKEN>"
        >>> plex = PlexServer(plex_uri, token=plex_token)
        >>> music = plex.library.section("Music")
        >>> artist = music.get("Stevie Wonder")
        >>> album = artist.album("Innervisions")
        >>> track = album.tracks()[4]
        >>> playlist = plex.playlist("My Playlist")
        >>>
        >>> plugin.play_now(artist)     # Play all tracks from an artist
        >>> plugin.add_to_queue(track)  # Add track to the end of queue
        >>> pos = plugin.add_to_queue([album, playlist])  # Enqueue multiple
        >>> s.play_from_queue(pos)      # Play items just enqueued
"""

from urllib.parse import quote

from ..core import to_didl_string
from ..data_structures import (
    DidlMusicAlbum,
    DidlMusicArtist,
    DidlMusicTrack,
    DidlPlaylistContainer,
)
from ..exceptions import SoCoException
from ..music_services import MusicService
from ..plugins import SoCoPlugin


PREFIX_LOOKUP = {
    "album": "1004206c",
    "artist": "1005004c",
    "playlist": "1006206c",
    "track": "10036020",
    "albums:directory": "100d2066",
    "artists:directory": "10fe2066",
    "playlists:directory": "10fe2064",
}

PARENT_TYPE = {
    "album": "artist",
    "artist": "artists:directory",
    "playlist": "playlists:directory",
    "track": "album",
}

CLASS_MAPPING = {
    "album": DidlMusicAlbum,
    "artist": DidlMusicArtist,
    "playlist": DidlPlaylistContainer,
    "track": DidlMusicTrack,
}


[docs] class PlexPlugin(SoCoPlugin): """A SoCo plugin for playing Plex media using the plexapi library.""" def __init__(self, soco): """Initialize the plugin.""" super().__init__(soco) self._service_info = None @property def name(self): """Return the name of the plugin.""" return "Plex Plugin" @property def service_name(self): """Return the service name of the Plex music service.""" return "Plex" @property def service_info(self): """Cache and return the service info of the Plex music service.""" if not self._service_info: self._service_info = MusicService.get_data_for_name(self.service_name) return self._service_info @property def service_id(self): """Return the service ID of the Plex music service.""" return self.service_info["ServiceID"] @property def service_type(self): """Return the service type of the Plex music service.""" return self.service_info["ServiceType"]
[docs] def play_now(self, plex_media): """Add the media to the end of the queue and immediately begin playback.""" position = self.add_to_queue(plex_media) self.soco.play_from_queue(position - 1)
[docs] def add_to_queue(self, plex_media, position=0, as_next=False, **kwargs): """Add the provided media to the speaker's playback queue. Args: plex_media (plexapi): The plexapi object representing the Plex media to be enqueued. Can be one of plexapi.audio.Track, plexapi.audio.Album, plexapi.audio.Artist or plexapi.playlist.Playlist. Can also be a list of the above items. position (int): The index (1-based) at which the media should be added. Default is 0 (append to the end of the queue). as_next (bool): Whether this media should be played as the next track in shuffle mode. This only works if "play_mode=SHUFFLE". Note: Enqueuing multi-track items like albums or playlists will select one track randomly as the next item and shuffle the remaining tracks throughout the queue. Returns: int: The index of the first item added to the queue. """ # Handle a list of Plex media items if isinstance(plex_media, list): position_result = first_added_position = None # If inserting into the queue, repeatedly insert the items in reverse order media_items = reversed(plex_media) if (as_next or position) else plex_media for media_item in media_items: if as_next or position: # Insert each item at the initial queue position in reverse order position_result = self.add_to_queue( media_item, as_next=as_next, position=(first_added_position or position), ) else: # Append each item to the end of the queue in order position_result = self.add_to_queue(media_item) first_added_position = first_added_position or position_result if not as_next: return first_added_position return position_result plex_server = plex_media._server # pylint: disable=protected-access try: base_id = "{}:{}".format( plex_server.machineIdentifier, plex_media.librarySectionID ) except AttributeError: base_id = "{}:".format(plex_server.machineIdentifier) item_type = plex_media.TYPE parent_type = PARENT_TYPE[item_type] didl_class = CLASS_MAPPING[item_type] item_uri = "{}:{}:{}".format(base_id, plex_media.ratingKey, item_type) desc = "SA_RINCON{st}_X_#Svc{st}-0-Token".format(st=self.service_type) if item_type == "track": parent_uri = "{}:{}:{}".format( base_id, plex_media.album().ratingKey, parent_type ) elif item_type == "album": parent_uri = "{}:{}:{}".format( base_id, plex_media.artist().ratingKey, parent_type ) elif item_type == "artist": parent_uri = "{}:{}".format( "00020000artist", plex_media.title.split(" ")[0] ) elif item_type == "playlist": if not plex_media.isAudio: raise SoCoException("Non-audio playlists are not supported") parent_uri = "{}:{}".format(base_id, parent_type) item_didl = didl_class( plex_media.title, PREFIX_LOOKUP[parent_type] + quote(parent_uri), PREFIX_LOOKUP[item_type] + quote(item_uri), desc=desc, ) metadata = to_didl_string(item_didl) enqueued_uri = "x-rincon-cpcontainer:{}?sid={}&flags=8300&sn=9".format( item_didl.item_id, self.service_id ) response = self.soco.avTransport.AddURIToQueue( [ ("InstanceID", 0), ("EnqueuedURI", enqueued_uri), ("EnqueuedURIMetaData", metadata), ("DesiredFirstTrackNumberEnqueued", position), ("EnqueueAsNext", int(as_next)), ], **kwargs, ) qnumber = response["FirstTrackNumberEnqueued"] return int(qnumber)