Ejemplo n.º 1
0
 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
Ejemplo n.º 2
0
 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)
Ejemplo n.º 3
0
    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)
Ejemplo n.º 4
0
 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']
Ejemplo n.º 5
0
 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
Ejemplo n.º 6
0
    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))
Ejemplo n.º 7
0
    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)
Ejemplo n.º 8
0
 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)
Ejemplo n.º 9
0
    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()
Ejemplo n.º 10
0
    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)
Ejemplo n.º 11
0
 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)
Ejemplo n.º 12
0
    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')
Ejemplo n.º 13
0
 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)
Ejemplo n.º 14
0
 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
Ejemplo n.º 15
0
    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)
Ejemplo n.º 16
0
 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()
Ejemplo n.º 17
0
 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)
Ejemplo n.º 18
0
    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
            )
        )
Ejemplo n.º 19
0
    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')
Ejemplo n.º 20
0
 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
Ejemplo n.º 21
0
 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)
Ejemplo n.º 22
0
    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')
Ejemplo n.º 23
0
    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')
Ejemplo n.º 24
0
    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')
Ejemplo n.º 25
0
 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
Ejemplo n.º 26
0
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
Ejemplo n.º 27
0
 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)
Ejemplo n.º 28
0
 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
Ejemplo n.º 29
0
    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)
Ejemplo n.º 30
0
    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')