def sendCommand(self, command, proxy=None, **params): """Send a command to the client Args: command (str): See the commands listed below proxy (None, optional): Description **params (dict): Description Returns: Element Raises: Unsupported: Unsupported clients """ command = command.strip('/') controller = command.split('/')[0] if controller not in self.protocolCapabilities: raise Unsupported('Client %s does not support the %s controller.' % (self.title, controller)) path = '/player/%s%s' % (command, utils.joinArgs(params)) headers = {'X-Plex-Target-Client-Identifier': self.machineIdentifier} self._commandId += 1 params['commandID'] = self._commandId proxy = self._proxyThroughServer if proxy is None else proxy if proxy: return self.server.query(path, headers=headers) path = '/player/%s%s' % (command, utils.joinArgs(params)) return self.query(path, headers=headers)
def getStreamURL(self, **params): """ Returns a stream url that may be used by external applications such as VLC. Parameters: **params (dict): optional parameters to manipulate the playback when accessing the stream. A few known parameters include: maxVideoBitrate, videoResolution offset, copyts, protocol, mediaIndex, platform. Raises: :class:`plexapi.exceptions.Unsupported`: When the item doesn't support fetching a stream URL. """ if self.TYPE not in ('movie', 'episode', 'track'): raise Unsupported('Fetching stream URL for %s is unsupported.' % self.TYPE) mvb = params.get('maxVideoBitrate') vr = params.get('videoResolution', '') params = { 'path': self.key, 'offset': params.get('offset', 0), 'copyts': params.get('copyts', 1), 'protocol': params.get('protocol'), 'mediaIndex': params.get('mediaIndex', 0), 'X-Plex-Platform': params.get('platform', 'Chrome'), 'maxVideoBitrate': max(mvb, 64) if mvb else None, 'videoResolution': vr if re.match('^\d+x\d+$', vr) else None } # remove None values params = {k: v for k, v in params.items() if v is not None} streamtype = 'audio' if self.TYPE in ('track', 'album') else 'video' # sort the keys since the randomness f***s with my tests.. sorted_params = sorted(params.items(), key=lambda val: val[0]) return self._server.url('/%s/:/transcode/universal/start.m3u8?%s' % (streamtype, urlencode(sorted_params)), includeToken=True)
def getStreamURL(self, **params): """Make a stream url that can be used by vlc. Args: **params (dict): Description Returns: string: '' Raises: Unsupported: Raises a error is the type is wrong. """ if self.TYPE not in ('movie', 'episode', 'track'): raise Unsupported( 'Fetching stream URL for %s is unsupported.' % self.TYPE) mvb = params.get('maxVideoBitrate') vr = params.get('videoResolution', '') params = { 'path': self.key, 'offset': params.get('offset', 0), 'copyts': params.get('copyts', 1), 'protocol': params.get('protocol'), 'mediaIndex': params.get('mediaIndex', 0), 'X-Plex-Platform': params.get('platform', 'Chrome'), 'maxVideoBitrate': max(mvb, 64) if mvb else None, 'videoResolution': vr if re.match('^\d+x\d+$', vr) else None } # remove None values params = {k: v for k, v in params.items() if v is not None} streamtype = 'audio' if self.TYPE in ('track', 'album') else 'video' return self.server.url('/%s/:/transcode/universal/start.m3u8?%s' % (streamtype, urlencode(params)))
def connect(self, timeout=None): """ Alias of reload as any subsequent requests to this client will be made directly to the device even if the object attributes were initially populated from a PlexServer. """ if not self.key: raise Unsupported('Cannot reload an object not built from a URL.') self._initpath = self.key data = self.query(self.key, timeout=timeout) if not data: raise NotFound("Client not found at %s" % self._baseurl) if self._clientIdentifier: client = next( (x for x in data if x.attrib.get("machineIdentifier") == self._clientIdentifier ), None, ) if client is None: raise NotFound("Client with identifier %s not found at %s" % (self._clientIdentifier, self._baseurl)) else: client = data[0] self._loadData(client) return self
def getStreamUrl(self, offset=0, maxVideoBitrate=None, videoResolution=None, **kwargs): """ Fetch URL to stream video directly. offset: Start time (in seconds) video will initiate from (ex: 300). maxVideoBitrate: Max bitrate video and audio stream (ex: 64). videoResolution: Max resolution of a video stream (ex: 1280x720). params: Dict of additional parameters to include in URL. """ if self.TYPE not in [Movie.TYPE, Episode.TYPE]: raise Unsupported('Cannot get stream URL for %s.' % self.TYPE) params = {} params['path'] = self.key params['offset'] = offset params['copyts'] = kwargs.get('copyts', 1) params['mediaIndex'] = kwargs.get('mediaIndex', 0) params['X-Plex-Platform'] = kwargs.get('platform', 'Chrome') if maxVideoBitrate: params['maxVideoBitrate'] = max(maxVideoBitrate, 64) if videoResolution and re.match('^\d+x\d+$', videoResolution): params['videoResolution'] = videoResolution return self.server.url('/video/:/transcode/universal/start?%s' % urllib.urlencode(params))
def playMedia(self, media, offset=0, **params): """ Start playback of the specified media item. See also: Parameters: media (:class:`~plexapi.media.Media`): Media item to be played back (movie, music, photo). offset (int): Number of milliseconds at which to start playing with zero representing the beginning (default 0). **params (dict): Optional additional parameters to include in the playback request. See also: https://github.com/plexinc/plex-media-player/wiki/Remote-control-API#modified-commands Raises: :class:`~plexapi.exceptions.Unsupported`: When no PlexServer specified in this object. """ if not self.server: raise Unsupported('A server must be specified before using this command.') server_url = media.server.baseurl.split(':') playqueue = self.server.createPlayQueue(media) self.sendCommand('playback/playMedia', **dict({ 'machineIdentifier': self.server.machineIdentifier, 'address': server_url[1].strip('/'), 'port': server_url[-1], 'offset': offset, 'key': media.key, 'containerKey': '/playQueues/%s?window=100&own=1' % playqueue.playQueueID, }, **params))
def playMedia(self, media, offset=0, **params): """ Start playback of the specified media item. See also: Parameters: media (:class:`~plexapi.media.Media`): Media item to be played back (movie, music, photo, playlist, playqueue). offset (int): Number of milliseconds at which to start playing with zero representing the beginning (default 0). **params (dict): Optional additional parameters to include in the playback request. See also: https://github.com/plexinc/plex-media-player/wiki/Remote-control-API#modified-commands Raises: :class:`~plexapi.exceptions.Unsupported`: When no PlexServer specified in this object. """ if not self._server: raise Unsupported('A server must be specified before using this command.') server_url = media._server._baseurl.split(':') try: self.sendCommand('timeline/subscribe', port=server_url[1].strip('/'), protocol='http') except: # some clients dont need or like this and raises http 400. # We want to include the exception in the log, but it might still work # so we swallow it. log.exception('%s failed to subscribe ' % self.title) playqueue = media if isinstance(media, PlayQueue) else self._server.createPlayQueue(media) self.sendCommand('playback/playMedia', **dict({ 'machineIdentifier': self._server.machineIdentifier, 'address': server_url[1].strip('/'), 'port': server_url[-1], 'offset': offset, 'key': media.key, 'token': media._server._token, 'containerKey': '/playQueues/%s?window=100&own=1' % playqueue.playQueueID, }, **params))
def playMedia(self, media, **params): """Start playback on a media item. Args: media (str): movie, music, photo **params (TYPE): Description Raises: Unsupported: Description """ if not self.server: raise Unsupported( 'A server must be specified before using this command.') server_url = media.server.baseurl.split(':') playqueue = self.server.createPlayQueue(media) self.sendCommand( 'playback/playMedia', **dict( { 'machineIdentifier': self.server.machineIdentifier, 'address': server_url[1].strip('/'), 'port': server_url[-1], 'key': media.key, 'containerKey': '/playQueues/%s?window=100&own=1' % playqueue.playQueueID, }, **params))
def section(self): """ Returns the :class:`~plexapi.library.LibrarySection` this smart playlist belongs to. Raises: :class:`plexapi.exceptions.BadRequest`: When trying to get the section for a regular playlist. :class:`plexapi.exceptions.Unsupported`: When unable to determine the library section. """ if not self.smart: raise BadRequest( 'Regular playlists are not associated with a library.') if self._section is None: # Try to parse the library section from the content URI string match = re.search(r'/library/sections/(\d+)/all', unquote(self.content or '')) if match: sectionKey = int(match.group(1)) self._section = self._server.library.sectionByID(sectionKey) return self._section # Try to get the library section from the first item in the playlist if self.items(): self._section = self.items()[0].section() return self._section raise Unsupported('Unable to determine the library section') return self._section
def addItem(self, item, playNext=False, refresh=True): """ Append the provided item to the "Up Next" section of the PlayQueue. Items can only be added to the section immediately following the current playing item. Parameters: item (:class:`~plexapi.media.Media` or :class:`~plexapi.playlist.Playlist`): Single media item or Playlist. playNext (bool, optional): If True, add this item to the front of the "Up Next" section. If False, the item will be appended to the end of the "Up Next" section. Only has an effect if an item has already been added to the "Up Next" section. See https://support.plex.tv/articles/202188298-play-queues/ for more details. refresh (bool, optional): Refresh the PlayQueue from the server before updating. """ if refresh: self.refresh() args = {} if item.type == "playlist": args["playlistID"] = item.ratingKey itemType = item.playlistType else: uuid = item.section().uuid itemType = item.listType args["uri"] = f"library://{uuid}/item{item.key}" if itemType != self.playQueueType: raise Unsupported("Item type does not match PlayQueue type") if playNext: args["next"] = 1 path = f"/playQueues/{self.playQueueID}{utils.joinArgs(args)}" data = self._server.query(path, method=self._server._session.put) self._loadData(data)
def goToMedia(self, media, **params): """ Navigate directly to the specified media page. Parameters: media (:class:`~plexapi.media.Media`): Media object to navigate to. **params (dict): Additional GET parameters to include with the command. Raises: :exc:`plexapi.exceptions.Unsupported`: When no PlexServer specified in this object. """ if not self._server: raise Unsupported( 'A server must be specified before using this command.') server_url = media._server._baseurl.split(':') self.sendCommand( 'mirror/details', **dict( { 'machineIdentifier': self._server.machineIdentifier, 'address': server_url[1].strip('/'), 'port': server_url[-1], 'key': media.key, 'protocol': server_url[0], 'token': media._server.createToken() }, **params))
def sendCommand(self, command, proxy=None, **params): """ Convenience wrapper around :func:`~plexapi.client.PlexClient.query()` to more easily send simple commands to the client. Returns an ElementTree object containing the response. Parameters: command (str): Command to be sent in for format '<controller>/<command>'. proxy (bool): Set True to proxy this command through the PlexServer. **params (dict): Additional GET parameters to include with the command. Raises: :class:`~plexapi.exceptions.Unsupported`: When we detect the client doesn't support this capability. """ command = command.strip('/') controller = command.split('/')[0] if controller not in self.protocolCapabilities: raise Unsupported('Client %s does not support the %s controller.' % (self.title, controller)) path = '/player/%s%s' % (command, utils.joinArgs(params)) headers = {'X-Plex-Target-Client-Identifier': self.machineIdentifier} self._commandId += 1 params['commandID'] = self._commandId proxy = self._proxyThroughServer if proxy is None else proxy if proxy: return self.server.query(path, headers=headers) path = '/player/%s%s' % (command, utils.joinArgs(params)) return self.query(path, headers=headers)
def metadataType(self): if self.isVideo: return 'movie' elif self.isAudio: return 'track' elif self.isPhoto: return 'photo' else: raise Unsupported('Unexpected playlist type')
def reload(self, key=None): """ Reload the data for this object from self.key. """ key = key or self._details_key or self.key if not key: raise Unsupported('Cannot reload an object not built from a URL.') self._initpath = key data = self._server.query(key) self._loadData(data[0]) return self
def playMedia(self, media, offset=0, **params): """ Start playback of the specified media item. See also: Parameters: media (:class:`~plexapi.media.Media`): Media item to be played back (movie, music, photo, playlist, playqueue). offset (int): Number of milliseconds at which to start playing with zero representing the beginning (default 0). **params (dict): Optional additional parameters to include in the playback request. See also: https://github.com/plexinc/plex-media-player/wiki/Remote-control-API#modified-commands Raises: :class:`plexapi.exceptions.Unsupported`: When no PlexServer specified in this object. """ if not self._server: raise Unsupported( 'A server must be specified before using this command.') server_url = media._server._baseurl.split(':') server_port = server_url[-1].strip('/') if hasattr(media, "playlistType"): mediatype = media.playlistType else: if isinstance(media, PlayQueue): mediatype = media.items[0].listType else: mediatype = media.listType # mediatype must be in ["video", "music", "photo"] if mediatype == "audio": mediatype = "music" playqueue = media if isinstance( media, PlayQueue) else self._server.createPlayQueue(media) self.sendCommand( 'playback/playMedia', **dict( { 'machineIdentifier': self._server.machineIdentifier, 'address': server_url[1].strip('/'), 'port': server_port, 'offset': offset, 'key': media.key, 'token': media._server.createToken(), 'type': mediatype, 'containerKey': '/playQueues/%s&&window=100&&own=1' % playqueue.playQueueID, }, **params))
def metadataType(self): """ Returns the type of metadata in the playlist (movie, track, or photo). """ if self.isVideo: return 'movie' elif self.isAudio: return 'track' elif self.isPhoto: return 'photo' else: raise Unsupported('Unexpected playlist type')
def listType(self): """ Returns the listType for the collection. """ if self.isVideo: return 'video' elif self.isAudio: return 'audio' elif self.isPhoto: return 'photo' else: raise Unsupported('Unexpected collection type')
def sync(self, videoQuality=None, photoResolution=None, audioBitrate=None, client=None, clientId=None, limit=None, unwatched=False, title=None): """ Add the collection as sync item for the specified device. See :func:`~plexapi.myplex.MyPlexAccount.sync` for possible exceptions. Parameters: videoQuality (int): idx of quality of the video, one of VIDEO_QUALITY_* values defined in :mod:`~plexapi.sync` module. Used only when collection contains video. photoResolution (str): maximum allowed resolution for synchronized photos, see PHOTO_QUALITY_* values in the module :mod:`~plexapi.sync`. Used only when collection contains photos. audioBitrate (int): maximum bitrate for synchronized music, better use one of MUSIC_BITRATE_* values from the module :mod:`~plexapi.sync`. Used only when collection contains audio. client (:class:`~plexapi.myplex.MyPlexDevice`): sync destination, see :func:`~plexapi.myplex.MyPlexAccount.sync`. clientId (str): sync destination, see :func:`~plexapi.myplex.MyPlexAccount.sync`. limit (int): maximum count of items to sync, unlimited if `None`. unwatched (bool): if `True` watched videos wouldn't be synced. title (str): descriptive title for the new :class:`~plexapi.sync.SyncItem`, if empty the value would be generated from metadata of current photo. Raises: :exc:`~plexapi.exceptions.BadRequest`: When collection is not allowed to sync. :exc:`~plexapi.exceptions.Unsupported`: When collection content is unsupported. Returns: :class:`~plexapi.sync.SyncItem`: A new instance of the created sync item. """ if not self.section().allowSync: raise BadRequest('The collection is not allowed to sync') from plexapi.sync import SyncItem, Policy, MediaSettings myplex = self._server.myPlexAccount() sync_item = SyncItem(self._server, None) sync_item.title = title if title else self.title sync_item.rootTitle = self.title sync_item.contentType = self.listType sync_item.metadataType = self.metadataType sync_item.machineIdentifier = self._server.machineIdentifier sync_item.location = 'library:///directory/%s' % quote_plus( '%s/children?excludeAllLeaves=1' % (self.key) ) sync_item.policy = Policy.create(limit, unwatched) if self.isVideo: sync_item.mediaSettings = MediaSettings.createVideo(videoQuality) elif self.isAudio: sync_item.mediaSettings = MediaSettings.createMusic(audioBitrate) elif self.isPhoto: sync_item.mediaSettings = MediaSettings.createPhoto(photoResolution) else: raise Unsupported('Unsupported collection content') return myplex.sync(sync_item, client=client, clientId=clientId)
def connect(self, timeout=None): """ Alias of reload as any subsequent requests to this client will be made directly to the device even if the object attributes were initially populated from a PlexServer. """ if not self.key: raise Unsupported('Cannot reload an object not built from a URL.') self._initpath = self.key data = self.query(self.key, timeout=timeout) self._loadData(data[0]) return self
def proxyThroughServer(self, value=True): """Connect to the client via the server. Args: value (bool, optional): Description Raises: Unsupported: Cannot use client proxy with unknown server. """ if value is True and not self.server: raise Unsupported('Cannot use client proxy with unknown server.') self._proxyThroughServer = value
def _reload(self, key=None, _overwriteNone=True, **kwargs): """ Perform the actual reload. """ details_key = self._buildDetailsKey( **kwargs) if kwargs else self._details_key key = key or details_key or self.key if not key: raise Unsupported('Cannot reload an object not built from a URL.') self._initpath = key data = self._server.query(key) self._overwriteNone = _overwriteNone self._loadData(data[0]) self._overwriteNone = True return self
def proxyThroughServer(self, value=True): """ Tells this PlexClient instance to proxy all future commands through the PlexServer. Useful if you do not wish to connect directly to the Client device itself. Parameters: value (bool): Enable or disable proxying (optional, default True). Raises: :class:`~plexapi.exceptions.Unsupported`: Cannot use client proxy with unknown server. """ if value is True and not self.server: raise Unsupported('Cannot use client proxy with unknown server.') self._proxyThroughServer = value
def goToMedia(self, media, **params): if not self.server: raise Unsupported( 'A server must be specified before using this command.') server_url = media.server.baseurl.split(':') self.sendCommand( 'mirror/details', **dict( { 'machineIdentifier': self.server.machineIdentifier, 'address': server_url[1].strip('/'), 'port': server_url[-1], 'key': media.key, }, **params))
def sendCommand(self, command, proxy=None, **params): command = command.strip('/') controller = command.split('/')[0] if controller not in self.protocolCapabilities: raise Unsupported('Client %s does not support the %s controller.' % (self.title, controller)) path = '/player/%s%s' % (command, utils.joinArgs(params)) headers = {'X-Plex-Target-Client-Identifier': self.machineIdentifier} self._commandId += 1 params['commandID'] = self._commandId proxy = self._proxyThroughServer if proxy is None else proxy if proxy: return self.server.query(path, headers=headers) path = '/player/%s%s' % (command, utils.joinArgs(params)) return self.query(path, headers=headers)
def playMedia(self, media, **params): if not self.server: raise Unsupported( 'A server must be specified before using this command.') server_url = media.server.baseurl.split(':') playqueue = self.server.createPlayQueue(media) self.sendCommand( 'playback/playMedia', **dict( { 'machineIdentifier': self.server.machineIdentifier, 'address': server_url[1].strip('/'), 'port': server_url[-1], 'key': media.key, 'containerKey': '/playQueues/%s?window=100&own=1' % playqueue.playQueueID, }, **params))
def reload(self, key=None, **kwargs): """ Reload the data for this object from self.key. Parameters: key (string, optional): Override the key to reload. **kwargs (dict): A dictionary of XML include parameters to exclude or override. All parameters are included by default with the option to override each parameter or disable each parameter individually by setting it to False or 0. See :class:`~plexapi.base.PlexPartialObject` for all the available include parameters. Example: .. code-block:: python from plexapi.server import PlexServer plex = PlexServer('http://localhost:32400', token='xxxxxxxxxxxxxxxxxxxx') movie = plex.library.section('Movies').get('Cars') # Partial reload of the movie without the `checkFiles` parameter. # Excluding `checkFiles` will prevent the Plex server from reading the # file to check if the file still exists and is accessible. # The movie object will remain as a partial object. movie.reload(checkFiles=False) movie.isPartialObject() # Returns True # Full reload of the movie with all include parameters. # The movie object will be a full object. movie.reload() movie.isFullObject() # Returns True """ details_key = self._buildDetailsKey( **kwargs) if kwargs else self._details_key key = key or details_key or self.key if not key: raise Unsupported('Cannot reload an object not built from a URL.') self._initpath = key data = self._server.query(key) self._loadData(data[0]) return self
def goToMedia(self, media, **params): """Go to a media on the client. Args: media (str): movie, music, photo **params (TYPE): Description # todo Raises: Unsupported: Description """ if not self.server: raise Unsupported( 'A server must be specified before using this command.') server_url = media.server.baseurl.split(':') self.sendCommand( 'mirror/details', **dict( { 'machineIdentifier': self.server.machineIdentifier, 'address': server_url[1].strip('/'), 'port': server_url[-1], 'key': media.key, }, **params))
def proxyThroughServer(self, value=True): if value is True and not self.server: raise Unsupported('Cannot use client proxy with unknown server.') self._proxyThroughServer = value