def query(self, path, method=requests.get): global TOTAL_QUERIES TOTAL_QUERIES += 1 url = self.url(path) log.info('%s %s', method.__name__.upper(), url) response = method(url, headers=self.headers(), timeout=TIMEOUT) if response.status_code not in [200, 201]: codename = codes.get(response.status_code)[0] raise BadRequest('(%s) %s' % (response.status_code, codename)) data = response.text.encode('utf8') return ElementTree.fromstring(data) if data else None
def url(self, key, includeToken=False): """ Build a URL string with proper token argument. Token will be appended to the URL if either includeToken is True or CONFIG.log.show_secrets is 'true'. """ if not self._baseurl: raise BadRequest('PlexClient object missing baseurl.') if self._token and (includeToken or self._showSecrets): delim = '&' if '?' in key else '?' return '%s%s%sX-Plex-Token=%s' % (self._baseurl, key, delim, self._token) return '%s%s' % (self._baseurl, key)
def sync(self, videoQuality=None, photoResolution=None, audioBitrate=None, client=None, clientId=None, limit=None, unwatched=False, title=None): """ Add current playlist as sync item for 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 playlist contains video. photoResolution (str): maximum allowed resolution for synchronized photos, see PHOTO_QUALITY_* values in the module :mod:`plexapi.sync`. Used only when playlist 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 playlist 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: :class:`plexapi.exceptions.BadRequest`: when playlist is not allowed to sync. :class:`plexapi.exceptions.Unsupported`: when playlist content is unsupported. Returns: :class:`plexapi.sync.SyncItem`: an instance of created syncItem. """ if not self.allowSync: raise BadRequest('The playlist 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.playlistType sync_item.metadataType = self.metadataType sync_item.machineIdentifier = self._server.machineIdentifier sync_item.location = 'playlist:///%s' % quote_plus(self.guid) 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 playlist content') return myplex.sync(sync_item, client=client, clientId=clientId)
def claimToken(self): """ Returns a str, a new "claim-token", which you can use to register your new Plex Server instance to your account. See: https://hub.docker.com/r/plexinc/pms-docker/, https://www.plex.tv/claim/ """ response = self._session.get('https://plex.tv/api/claim/token.json', headers=self._headers(), timeout=TIMEOUT) if response.status_code not in (200, 201, 204): # pragma: no cover codename = codes.get(response.status_code)[0] errtext = response.text.replace('\n', ' ') raise BadRequest('(%s) %s %s; %s' % (response.status_code, codename, response.url, errtext)) return response.json()['token']
def query(self, url, method=None, headers=None, timeout=None, **kwargs): method = method or self._session.get timeout = timeout or TIMEOUT log.debug('%s %s %s', method.__name__.upper(), url, kwargs.get('json', '')) headers = self._headers(**headers or {}) response = method(url, headers=headers, timeout=timeout, **kwargs) if response.status_code not in (200, 201, 204): # pragma: no cover codename = codes.get(response.status_code)[0] errtext = response.text.replace('\n', ' ') raise BadRequest('(%s) %s %s; %s' % (response.status_code, codename, response.url, errtext)) data = response.text.encode('utf8') return ElementTree.fromstring(data) if data.strip() else None
def fetchItem(self, ekey, cls=None, **kwargs): """ Load the specified key to find and build the first item with the specified tag and attrs. If no tag or attrs are specified then the first item in the result set is returned. Parameters: ekey (str or int): Path in Plex to fetch items from. If an int is passed in, the key will be translated to /library/metadata/<key>. This allows fetching an item only knowing its key-id. cls (:class:`~plexapi.base.PlexObject`): If you know the class of the items to be fetched, passing this in will help the parser ensure it only returns those items. By default we convert the xml elements with the best guess PlexObjects based on tag and type attrs. etag (str): Only fetch items with the specified tag. **kwargs (dict): Optionally add attribute filters on the items to fetch. For example, passing in viewCount=0 will only return matching items. Filtering is done before the Python objects are built to help keep things speedy. Note: Because some attribute names are already used as arguments to this function, such as 'tag', you may still reference the attr tag byappending an underscore. For example, passing in _tag='foobar' will return all items where tag='foobar'. Also Note: Case very much matters when specifying kwargs -- Optionally, operators can be specified by append it to the end of the attribute name for more complex lookups. For example, passing in viewCount__gte=0 will return all items where viewCount >= 0. Available operations include: * __contains: Value contains specified arg. * __endswith: Value ends with specified arg. * __exact: Value matches specified arg. * __exists (bool): Value is or is not present in the attrs. * __gt: Value is greater than specified arg. * __gte: Value is greater than or equal to specified arg. * __icontains: Case insensative value contains specified arg. * __iendswith: Case insensative value ends with specified arg. * __iexact: Case insensative value matches specified arg. * __in: Value is in a specified list or tuple. * __iregex: Case insensative value matches the specified regular expression. * __istartswith: Case insensative value starts with specified arg. * __lt: Value is less than specified arg. * __lte: Value is less than or equal to specified arg. * __regex: Value matches the specified regular expression. * __startswith: Value starts with specified arg. """ if ekey is None: raise BadRequest('ekey was not provided') if isinstance(ekey, int): ekey = '/library/metadata/%s' % ekey for elem in self._server.query(ekey): if self._checkAttrs(elem, **kwargs): return self._buildItem(elem, cls, ekey) clsname = cls.__name__ if cls else 'None' raise NotFound('Unable to find elem: cls=%s, attrs=%s' % (clsname, kwargs))
def syncItems(self): """ Returns an instance of :class:`plexapi.sync.SyncList` for current device. Raises: :class:`plexapi.exceptions.BadRequest`: when the device doesn`t provides `sync-target`. """ if 'sync-target' not in self.provides: raise BadRequest( 'Requested syncList for device which do not provides sync-target' ) return self._server.syncItems(client=self)
def _signin(self, username, password): auth = (username, password) log.info('POST %s', self.SIGNIN) response = requests.post(self.SIGNIN, headers=plexapi.BASE_HEADERS, auth=auth, timeout=TIMEOUT) if response.status_code != requests.codes.created: codename = codes.get(response.status_code)[0] raise BadRequest('(%s) %s' % (response.status_code, codename)) self.response = response data = response.text.encode('utf8') return ElementTree.fromstring(data)
def saveEdits(self): """ Save all the batch edits and automatically reload the object. See :func:`~plexapi.base.PlexPartialObject.batchEdits` for details. """ if not isinstance(self._edits, dict): raise BadRequest( 'Batch editing mode not enabled. Must call `batchEdits()` first.' ) edits = self._edits self._edits = None self._edit(**edits) return self.reload()
def episode(self, title=None, episode=None): """ Returns the episode with the given title or number. Parameters: title (str): Title of the episode to return. episode (int): Episode number (default:None; required if title not specified). """ if not title and not episode: raise BadRequest('Missing argument, you need to use title or episode.') key = '/library/metadata/%s/children' % self.ratingKey if title: return self.fetchItem(key, title=title) return self.fetchItem(key, seasonNumber=self.index, index=episode)
def signin(cls, username, password): if 'X-Plex-Token' in plexapi.BASE_HEADERS: del plexapi.BASE_HEADERS['X-Plex-Token'] auth = (username, password) log.info('POST %s', cls.SIGNIN) response = requests.post(cls.SIGNIN, headers=plexapi.BASE_HEADERS, auth=auth, timeout=TIMEOUT) if response.status_code != requests.codes.created: codename = codes.get(response.status_code)[0] if response.status_code == 401: raise Unauthorized('(%s) %s' % (response.status_code, codename)) raise BadRequest('(%s) %s' % (response.status_code, codename)) data = ElementTree.fromstring(response.text.encode('utf8')) return cls(data, cls.SIGNIN)
def createPhoto(resolution): """ Returns a :class:`~plexapi.sync.MediaSettings` object, based on provided photo quality value. Parameters: resolution (str): maximum allowed resolution for synchronized photos, see PHOTO_QUALITY_* values in the module. Raises: :exc:`~plexapi.exceptions.BadRequest`: When provided unknown video quality. """ if resolution in PHOTO_QUALITIES: return MediaSettings(photoQuality=PHOTO_QUALITIES[resolution], photoResolution=resolution) else: raise BadRequest('Unexpected photo quality')
def addItems(self, items): if not isinstance(items, (list, tuple)): items = [items] ratingKeys = [] for item in items: if item.listType != self.playlistType: raise BadRequest('Can not mix media types when building a playlist: %s and %s' % (self.playlistType, item.listType)) ratingKeys.append(item.ratingKey) uuid = items[0].section().uuid ratingKeys = ','.join(ratingKeys) path = '%s/items%s' % (self.key, utils.joinArgs({ 'uri': 'library://%s/directory//library/metadata/%s' % (uuid, ratingKeys), })) return self.server.query(path, method=self.server.session.put)
def query(self, url, method=None, headers=None, **kwargs): method = method or self._session.get delim = '&' if '?' in url else '?' url = '%s%sX-Plex-Token=%s' % (url, delim, self._token) log.debug('%s %s', method.__name__.upper(), url) allheaders = BASE_HEADERS.copy() allheaders.update(headers or {}) response = method(url, headers=allheaders, timeout=TIMEOUT, **kwargs) if response.status_code not in (200, 201): codename = codes.get(response.status_code)[0] log.warn('BadRequest (%s) %s %s' % (response.status_code, codename, response.url)) raise BadRequest('(%s) %s' % (response.status_code, codename)) data = response.text.encode('utf8') return ElementTree.fromstring(data) if data.strip() else None
def rate(self, rating=None): """ Rate the Plex object. Note: Plex ratings are displayed out of 5 stars (e.g. rating 7.0 = 3.5 stars). Parameters: rating (float, optional): Rating from 0 to 10. Exclude to reset the rating. Raises: :exc:`~plexapi.exceptions.BadRequest`: If the rating is invalid. """ if rating is None: rating = -1 elif not isinstance(rating, (int, float)) or rating < 0 or rating > 10: raise BadRequest('Rating must be between 0 to 10.') key = '/:/rate?key=%s&identifier=com.plexapp.plugins.library&rating=%s' % (self.ratingKey, rating) self._server.query(key, method=self._server._session.put)
def save(self): """ Save any outstanding settnig changes to the :class:`~plexapi.server.PlexServer`. This performs a full reload() of Settings after complete. """ params = {} for setting in self.all(): if setting._setValue: log.info('Saving PlexServer setting %s = %s' % (setting.id, setting._setValue)) params[setting.id] = quote(setting._setValue) if not params: raise BadRequest('No setting have been modified.') querystr = '&'.join(['%s=%s' % (k, v) for k, v in params.items()]) url = '%s?%s' % (self.key, querystr) self._server.query(url, self._server._session.put) self.reload()
def augmentation(self): """ Returns a list of :class:`~plexapi.library.Hub` objects. Augmentation returns hub items relating to online media sources such as Tidal Music "Track from {item}" or "Soundtrack of {item}". Plex Pass and linked Tidal account are required. """ account = self._server.myPlexAccount() tidalOptOut = next((service.value for service in account.onlineMediaSources() if service.key == 'tv.plex.provider.music'), None) if account.subscriptionStatus != 'Active' or tidalOptOut == 'opt_out': raise BadRequest('Requires Plex Pass and Tidal Music enabled.') data = self._server.query(self.key + '?asyncAugmentMetadata=1') augmentationKey = data.attrib.get('augmentationKey') return self.fetchItems(augmentationKey)
def playMedia(self, media, offset=0, **params): if hasattr(media, "playlistType"): mediatype = media.playlistType else: if isinstance(media, PlayQueue): mediatype = media.items[0].listType else: mediatype = media.listType if mediatype == "audio": mediatype = "music" else: raise BadRequest("Sonos currently only supports music for playback") server_protocol, server_address, server_port = media._server._baseurl.split(":") server_address = server_address.strip("/") server_port = server_port.strip("/") playqueue = ( media if isinstance(media, PlayQueue) else media._server.createPlayQueue(media) ) self.sendCommand( "playback/playMedia", **dict( { "type": "music", "providerIdentifier": "com.plexapp.plugins.library", "containerKey": "/playQueues/{}?own=1".format( playqueue.playQueueID ), "key": media.key, "offset": offset, "machineIdentifier": media._server.machineIdentifier, "protocol": server_protocol, "address": server_address, "port": server_port, "token": media._server.createToken(), "commandID": self._nextCommandId(), "X-Plex-Client-Identifier": X_PLEX_IDENTIFIER, "X-Plex-Token": media._server._token, "X-Plex-Target-Client-Identifier": self.machineIdentifier, }, **params ) )
def episode(self, title=None, episode=None): """ Returns the episode with the given title or number. Parameters: title (str): Title of the episode to return. episode (int): Episode number (default: None; required if title not specified). Raises: :exc:`~plexapi.exceptions.BadRequest`: If title or episode parameter is missing. """ key = '/library/metadata/%s/children' % self.ratingKey if title is not None: return self.fetchItem(key, Episode, title__iexact=title) elif episode is not None: return self.fetchItem(key, Episode, parentIndex=self.index, index=episode) raise BadRequest('Missing argument: title or episode is required')
def sendCommand(self, command, args=None): url = '%s%s' % (self.url(command), utils.joinArgs(args)) log.info('GET %s', url) headers = plexapi.BASE_HEADERS headers['X-Plex-Target-Client-Identifier'] = self.clientIdentifier response = requests.get(url, headers=headers, timeout=TIMEOUT) if response.status_code != requests.codes.ok: codename = codes.get(response.status_code)[0] raise BadRequest('(%s) %s' % (response.status_code, codename)) data = response.text.encode('utf8') if data: try: return ElementTree.fromstring(data) except: pass return None
def listChoices(self, category, libtype=None, **kwargs): """ List choices for the specified filter category. kwargs can be any of the same kwargs in self.search() to help narrow down the choices to only those that matter in your current context. """ if category in kwargs: raise BadRequest( 'Cannot include kwarg equal to specified category: %s' % category) args = {} for subcategory, value in kwargs.items(): args[category] = self._cleanSearchFilter(subcategory, value) if libtype is not None: args['type'] = utils.searchType(libtype) query = '/library/sections/%s/%s%s' % (self.key, category, utils.joinArgs(args)) return utils.listItems(self.server, query, bytag=True)
def track(self, title=None, track=None): """ Returns the :class:`~plexapi.audio.Track` that matches the specified title. Parameters: title (str): Title of the track to return. track (int): Track number (default: None; required if title not specified). Raises: :exc:`~plexapi.exceptions.BadRequest`: If title or track parameter is missing. """ key = '/library/metadata/%s/children' % self.ratingKey if title is not None: return self.fetchItem(key, Track, title__iexact=title) elif track is not None: return self.fetchItem(key, Track, parentTitle__iexact=self.title, index=track) raise BadRequest('Missing argument: title or track is required')
def episode(self, title=None, season=None, episode=None): """ Find a episode using a title or season and episode. Parameters: title (str): Title of the episode to return season (int): Season number (default: None; required if title not specified). episode (int): Episode number (default: None; required if title not specified). Raises: :exc:`~plexapi.exceptions.BadRequest`: If title or season and episode parameters are missing. """ key = '/library/metadata/%s/allLeaves' % self.ratingKey if title is not None: return self.fetchItem(key, Episode, title__iexact=title) elif season is not None and episode is not None: return self.fetchItem(key, Episode, parentIndex=season, index=episode) raise BadRequest('Missing argument: title or season and episode are required')
def createVideo(videoQuality): """ Returns a :class:`~plexapi.sync.MediaSettings` object, based on provided video quality value. Parameters: videoQuality (int): idx of quality of the video, one of VIDEO_QUALITY_* values defined in this module. Raises: :exc:`~plexapi.exceptions.BadRequest`: When provided unknown video quality. """ if videoQuality == VIDEO_QUALITY_ORIGINAL: return MediaSettings('', '', '') elif videoQuality < len(VIDEO_QUALITIES['bitrate']): return MediaSettings(VIDEO_QUALITIES['bitrate'][videoQuality], VIDEO_QUALITIES['videoQuality'][videoQuality], VIDEO_QUALITIES['videoResolution'][videoQuality]) else: raise BadRequest('Unexpected video quality')
def query(self, path, method=None, headers=None, timeout=None, **kwargs): """ Main method used to handle HTTPS requests to the Plex client. This method helps by encoding the response to utf-8 and parsing the returned XML into and ElementTree object. Returns None if no data exists in the response. """ url = self.url(path) method = method or self._session.get timeout = timeout or TIMEOUT log.debug('%s %s', method.__name__.upper(), url) headers = self._headers(**headers or {}) response = method(url, headers=headers, timeout=timeout, **kwargs) if response.status_code not in (200, 201): codename = codes.get(response.status_code)[0] log.warn('BadRequest (%s) %s %s' % (response.status_code, codename, response.url)) raise BadRequest('(%s) %s' % (response.status_code, codename)) data = response.text.encode('utf8') return ElementTree.fromstring(data) if data.strip() else None
def handle_playlists(): if len(PLAYLISTS_GOOD) != len(PLAYLISTS_GOOD_ITEMS): print("Number of playlists do not match number of items, stopping.") return True # Remove the bad playlists. for x in PLAYLISTS_BAD: user = x.split(PLAYLIST_DELIMITER)[0].strip(SYNC_CHARACTER) playlist = x.split(PLAYLIST_DELIMITER)[1] print("Removing '" + playlist + "' from " + user) if not ARG_DRYRUN: # We grab a potential exception to see if it is actually alright. if PLEXAPI_CHECK_204: try: USER_SERVER[USERS.index(user)].playlist(playlist).delete() except BadRequest as e: message = getattr(e, 'message', str(e)) if message.startswith("(204)"): pass else: raise BadRequest(message) else: USER_SERVER[USERS.index(user)].playlist(playlist).delete() if ARG_CLEAN: return False print ("------------------------------") # Recreate the good playlists on each user. for playlist in PLAYLISTS_GOOD: owner = playlist.split(PLAYLIST_DELIMITER)[0].strip(SYNC_CHARACTER) playlist_name = playlist.split(PLAYLIST_DELIMITER)[1] for user in USERS: if not playlist.startswith(user): playlist_display_name = ( SYNC_CHARACTER + NAMES[USERS.index(owner)] + ": " + playlist_name ) print ("Creating '" + playlist_display_name + "' for " + user) if not ARG_DRYRUN: USER_SERVER[USERS.index(user)].createPlaylist( playlist_display_name, PLAYLISTS_GOOD_ITEMS[PLAYLISTS_GOOD.index(playlist)]) return False
def create(cls, server, title, items): if not isinstance(items, (list, tuple)): items = [items] ratingKeys = [] for item in items: if item.listType != items[0].listType: raise BadRequest('Can not mix media types when building a playlist') ratingKeys.append(item.ratingKey) ratingKeys = ','.join(ratingKeys) uuid = items[0].section().uuid path = '/playlists%s' % utils.joinArgs({ 'uri': 'library://%s/directory//library/metadata/%s' % (uuid, ratingKeys), 'type': items[0].listType, 'title': title, 'smart': 0 }) data = server.query(path, method=server.session.post)[0] return cls(server, data, initpath=path)
def addItems(self, items): """ Add items to a playlist. """ if not isinstance(items, (list, tuple)): items = [items] ratingKeys = [] for item in items: if item.listType != self.playlistType: # pragma: no cover raise BadRequest('Can not mix media types when building a playlist: %s and %s' % (self.playlistType, item.listType)) ratingKeys.append(str(item.ratingKey)) uuid = items[0].section().uuid ratingKeys = ','.join(ratingKeys) key = '%s/items%s' % (self.key, utils.joinArgs({ 'uri': 'library://%s/directory//library/metadata/%s' % (uuid, ratingKeys) })) result = self._server.query(key, method=self._server._session.put) self.reload() return result
def removeItems(self, items): """ Remove items from the collection. Parameters: items (List): List of :class:`~plexapi.audio.Audio`, :class:`~plexapi.video.Video`, or :class:`~plexapi.photo.Photo` objects to be removed from the collection. Raises: :class:`plexapi.exceptions.BadRequest`: When trying to remove items from a smart collection. """ if self.smart: raise BadRequest('Cannot remove items from a smart collection.') if items and not isinstance(items, (list, tuple)): items = [items] for item in items: key = '%s/items/%s' % (self.key, item.ratingKey) self._server.query(key, method=self._server._session.delete)
def season(self, title=None, season=None): """ Returns the season with the specified title or number. Parameters: title (str): Title of the season to return. season (int): Season number (default: None; required if title not specified). Raises: :exc:`~plexapi.exceptions.BadRequest`: If title or season parameter is missing. """ key = '/library/metadata/%s/children' % self.ratingKey if title is not None and not isinstance(title, int): return self.fetchItem(key, Season, title__iexact=title) elif season is not None or isinstance(title, int): if isinstance(title, int): index = title else: index = season return self.fetchItem(key, Season, index=index) raise BadRequest('Missing argument: title or season is required')