# -*- coding: utf-8 -*-
from plexapi import X_PLEX_CONTAINER_SIZE, log, utils
from plexapi.base import PlexObject
from plexapi.compat import unquote, urlencode, quote_plus
from plexapi.media import MediaTag
from plexapi.exceptions import BadRequest, NotFound


class Library(PlexObject):
    """ Represents a PlexServer library. This contains all sections of media defined
        in your Plex server including video, shows and audio.

        Attributes:
            key (str): '/library'
            identifier (str): Unknown ('com.plexapp.plugins.library').
            mediaTagVersion (str): Unknown (/system/bundle/media/flags/)
            server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to.
            title1 (str): 'Plex Library' (not sure how useful this is).
            title2 (str): Second title (this is blank on my setup).
    """
    key = '/library'

    def _loadData(self, data):
        self._data = data
        self._sectionsByID = {}  # cached Section UUIDs
        self.identifier = data.attrib.get('identifier')
        self.mediaTagVersion = data.attrib.get('mediaTagVersion')
        self.title1 = data.attrib.get('title1')
        self.title2 = data.attrib.get('title2')

    def sections(self):
        """ Returns a list of all media sections in this library. Library sections may be any of
            :class:`~plexapi.library.MovieSection`, :class:`~plexapi.library.ShowSection`,
            :class:`~plexapi.library.MusicSection`, :class:`~plexapi.library.PhotoSection`.
        """
        key = '/library/sections'
        sections = []
        for elem in self._server.query(key):
            for cls in (MovieSection, ShowSection, MusicSection, PhotoSection):
                if elem.attrib.get('type') == cls.TYPE:
                    section = cls(self._server, elem, key)
                    self._sectionsByID[section.key] = section
                    sections.append(section)
        return sections

    def section(self, title=None):
        """ Returns the :class:`~plexapi.library.LibrarySection` that matches the specified title.

            Parameters:
                title (str): Title of the section to return.
        """
        for section in self.sections():
            if section.title.lower() == title.lower():
                return section
        raise NotFound('Invalid library section: %s' % title)

    def sectionByID(self, sectionID):
        """ Returns the :class:`~plexapi.library.LibrarySection` that matches the specified sectionID.

            Parameters:
                sectionID (str): ID of the section to return.
        """
        if not self._sectionsByID or sectionID not in self._sectionsByID:
            self.sections()
        return self._sectionsByID[sectionID]

    def all(self, **kwargs):
        """ Returns a list of all media from all library sections.
            This may be a very large dataset to retrieve.
        """
        items = []
        for section in self.sections():
            for item in section.all(**kwargs):
                items.append(item)
        return items

    def onDeck(self):
        """ Returns a list of all media items on deck. """
        return self.fetchItems('/library/onDeck')

    def recentlyAdded(self):
        """ Returns a list of all media items recently added. """
        return self.fetchItems('/library/recentlyAdded')

    def search(self, title=None, libtype=None, **kwargs):
        """ Searching within a library section is much more powerful. It seems certain
            attributes on the media objects can be targeted to filter this search down
            a bit, but I havent found the documentation for it.

            Example: "studio=Comedy%20Central" or "year=1999" "title=Kung Fu" all work. Other items
            such as actor=<id> seem to work, but require you already know the id of the actor.
            TLDR: This is untested but seems to work. Use library section search when you can.
        """
        args = {}
        if title:
            args['title'] = title
        if libtype:
            args['type'] = utils.searchType(libtype)
        for attr, value in kwargs.items():
            args[attr] = value
        key = '/library/all%s' % utils.joinArgs(args)
        return self.fetchItems(key)

    def cleanBundles(self):
        """ Poster images and other metadata for items in your library are kept in "bundle"
            packages. When you remove items from your library, these bundles aren't immediately
            removed. Removing these old bundles can reduce the size of your install. By default, your
            server will automatically clean up old bundles once a week as part of Scheduled Tasks.
        """
        # TODO: Should this check the response for success or the correct mediaprefix?
        self._server.query('/library/clean/bundles')

    def emptyTrash(self):
        """ If a library has items in the Library Trash, use this option to empty the Trash. """
        for section in self.sections():
            section.emptyTrash()

    def optimize(self):
        """ The Optimize option cleans up the server database from unused or fragmented data.
            For example, if you have deleted or added an entire library or many items in a
            library, you may like to optimize the database.
        """
        self._server.query('/library/optimize')

    def update(self):
        """ Scan this library for new items."""
        self._server.query('/library/sections/all/refresh')

    def cancelUpdate(self):
        """ Cancel a library update. """
        key = '/library/sections/all/refresh'
        self._server.query(key, method=self._server._session.delete)

    def refresh(self):
        """ Forces a download of fresh media information from the internet.
            This can take a long time. Any locked fields are not modified.
        """
        self._server.query('/library/sections/all/refresh?force=1')

    def deleteMediaPreviews(self):
        """ Delete the preview thumbnails for the all sections. This cannot be
            undone. Recreating media preview files can take hours or even days.
        """
        for section in self.sections():
            section.deleteMediaPreviews()

    def add(self, name='', type='', agent='', scanner='', location='', language='en', *args, **kwargs):
        """ Simplified add for the most common options.

            Parameters:
                name (str): Name of the library
                agent (str): Example com.plexapp.agents.imdb
                type (str): movie, show, # check me
                location (str): /path/to/files
                language (str): Two letter language fx en
                kwargs (dict): Advanced options should be passed as a dict. where the id is the key.

            **Photo Preferences**

                * **agent** (str): com.plexapp.agents.none
                * **enableAutoPhotoTags** (bool): Tag photos. Default value false.
                * **enableBIFGeneration** (bool): Enable video preview thumbnails. Default value true.
                * **includeInGlobal** (bool): Include in dashboard. Default value true.
                * **scanner** (str): Plex Photo Scanner

            **Movie Preferences**

                * **agent** (str): com.plexapp.agents.none, com.plexapp.agents.imdb, com.plexapp.agents.themoviedb
                * **enableBIFGeneration** (bool): Enable video preview thumbnails. Default value true.
                * **enableCinemaTrailers** (bool): Enable Cinema Trailers. Default value true.
                * **includeInGlobal** (bool): Include in dashboard. Default value true.
                * **scanner** (str): Plex Movie Scanner, Plex Video Files Scanner

            **IMDB Movie Options** (com.plexapp.agents.imdb)

                * **title** (bool): Localized titles. Default value false.
                * **extras** (bool): Find trailers and extras automatically (Plex Pass required). Default value true.
                * **only_trailers** (bool): Skip extras which aren't trailers. Default value false.
                * **redband** (bool): Use red band (restricted audiences) trailers when available. Default value false.
                * **native_subs** (bool): Include extras with subtitles in Library language. Default value false.
                * **cast_list** (int): Cast List Source: Default value 1 Possible options: 0:IMDb,1:The Movie Database.
                * **ratings** (int): Ratings Source, Default value 0 Possible options:
                  0:Rotten Tomatoes, 1:IMDb, 2:The Movie Database.
                * **summary** (int): Plot Summary Source: Default value 1 Possible options: 0:IMDb,1:The Movie Database.
                * **country** (int): Default value 46 Possible options 0:Argentina, 1:Australia, 2:Austria,
                  3:Belgium, 4:Belize, 5:Bolivia, 6:Brazil, 7:Canada, 8:Chile, 9:Colombia, 10:Costa Rica,
                  11:Czech Republic, 12:Denmark, 13:Dominican Republic, 14:Ecuador, 15:El Salvador,
                  16:France, 17:Germany, 18:Guatemala, 19:Honduras, 20:Hong Kong SAR, 21:Ireland,
                  22:Italy, 23:Jamaica, 24:Korea, 25:Liechtenstein, 26:Luxembourg, 27:Mexico, 28:Netherlands,
                  29:New Zealand, 30:Nicaragua, 31:Panama, 32:Paraguay, 33:Peru, 34:Portugal,
                  35:Peoples Republic of China, 36:Puerto Rico, 37:Russia, 38:Singapore, 39:South Africa,
                  40:Spain, 41:Sweden, 42:Switzerland, 43:Taiwan, 44:Trinidad, 45:United Kingdom,
                  46:United States, 47:Uruguay, 48:Venezuela.
                * **collections** (bool): Use collection info from The Movie Database. Default value false.
                * **localart** (bool): Prefer artwork based on library language. Default value true.
                * **adult** (bool): Include adult content. Default value false.
                * **usage** (bool): Send anonymous usage data to Plex. Default value true.

            **TheMovieDB Movie Options** (com.plexapp.agents.themoviedb)

                * **collections** (bool): Use collection info from The Movie Database. Default value false.
                * **localart** (bool): Prefer artwork based on library language. Default value true.
                * **adult** (bool): Include adult content. Default value false.
                * **country** (int): Country (used for release date and content rating). Default value 47 Possible
                  options 0:, 1:Argentina, 2:Australia, 3:Austria, 4:Belgium, 5:Belize, 6:Bolivia, 7:Brazil, 8:Canada,
                  9:Chile, 10:Colombia, 11:Costa Rica, 12:Czech Republic, 13:Denmark, 14:Dominican Republic, 15:Ecuador,
                  16:El Salvador, 17:France, 18:Germany, 19:Guatemala, 20:Honduras, 21:Hong Kong SAR, 22:Ireland,
                  23:Italy, 24:Jamaica, 25:Korea, 26:Liechtenstein, 27:Luxembourg, 28:Mexico, 29:Netherlands,
                  30:New Zealand, 31:Nicaragua, 32:Panama, 33:Paraguay, 34:Peru, 35:Portugal,
                  36:Peoples Republic of China, 37:Puerto Rico, 38:Russia, 39:Singapore, 40:South Africa, 41:Spain,
                  42:Sweden, 43:Switzerland, 44:Taiwan, 45:Trinidad, 46:United Kingdom, 47:United States, 48:Uruguay,
                  49:Venezuela.

            **Show Preferences**

                * **agent** (str): com.plexapp.agents.none, com.plexapp.agents.thetvdb, com.plexapp.agents.themoviedb
                * **enableBIFGeneration** (bool): Enable video preview thumbnails. Default value true.
                * **episodeSort** (int): Episode order. Default -1 Possible options: 0:Oldest first, 1:Newest first.
                * **flattenSeasons** (int): Seasons. Default value 0 Possible options: 0:Show,1:Hide.
                * **includeInGlobal** (bool): Include in dashboard. Default value true.
                * **scanner** (str): Plex Series Scanner

            **TheTVDB Show Options** (com.plexapp.agents.thetvdb)

                * **extras** (bool): Find trailers and extras automatically (Plex Pass required). Default value true.
                * **native_subs** (bool): Include extras with subtitles in Library language. Default value false.

            **TheMovieDB Show Options** (com.plexapp.agents.themoviedb)

                * **collections** (bool): Use collection info from The Movie Database. Default value false.
                * **localart** (bool): Prefer artwork based on library language. Default value true.
                * **adult** (bool): Include adult content. Default value false.
                * **country** (int): Country (used for release date and content rating). Default value 47 options
                  0:, 1:Argentina, 2:Australia, 3:Austria, 4:Belgium, 5:Belize, 6:Bolivia, 7:Brazil, 8:Canada, 9:Chile,
                  10:Colombia, 11:Costa Rica, 12:Czech Republic, 13:Denmark, 14:Dominican Republic, 15:Ecuador,
                  16:El Salvador, 17:France, 18:Germany, 19:Guatemala, 20:Honduras, 21:Hong Kong SAR, 22:Ireland,
                  23:Italy, 24:Jamaica, 25:Korea, 26:Liechtenstein, 27:Luxembourg, 28:Mexico, 29:Netherlands,
                  30:New Zealand, 31:Nicaragua, 32:Panama, 33:Paraguay, 34:Peru, 35:Portugal,
                  36:Peoples Republic of China, 37:Puerto Rico, 38:Russia, 39:Singapore, 40:South Africa,
                  41:Spain, 42:Sweden, 43:Switzerland, 44:Taiwan, 45:Trinidad, 46:United Kingdom, 47:United States,
                  48:Uruguay, 49:Venezuela.

            **Other Video Preferences**

                * **agent** (str): com.plexapp.agents.none, com.plexapp.agents.imdb, com.plexapp.agents.themoviedb
                * **enableBIFGeneration** (bool): Enable video preview thumbnails. Default value true.
                * **enableCinemaTrailers** (bool): Enable Cinema Trailers. Default value true.
                * **includeInGlobal** (bool): Include in dashboard. Default value true.
                * **scanner** (str): Plex Movie Scanner, Plex Video Files Scanner

            **IMDB Other Video Options** (com.plexapp.agents.imdb)

                * **title** (bool): Localized titles. Default value false.
                * **extras** (bool): Find trailers and extras automatically (Plex Pass required). Default value true.
                * **only_trailers** (bool): Skip extras which aren't trailers. Default value false.
                * **redband** (bool): Use red band (restricted audiences) trailers when available. Default value false.
                * **native_subs** (bool): Include extras with subtitles in Library language. Default value false.
                * **cast_list** (int): Cast List Source: Default value 1 Possible options: 0:IMDb,1:The Movie Database.
                * **ratings** (int): Ratings Source Default value 0 Possible options:
                  0:Rotten Tomatoes,1:IMDb,2:The Movie Database.
                * **summary** (int): Plot Summary Source: Default value 1 Possible options: 0:IMDb,1:The Movie Database.
                * **country** (int): Country: Default value 46 Possible options: 0:Argentina, 1:Australia, 2:Austria,
                  3:Belgium, 4:Belize, 5:Bolivia, 6:Brazil, 7:Canada, 8:Chile, 9:Colombia, 10:Costa Rica,
                  11:Czech Republic, 12:Denmark, 13:Dominican Republic, 14:Ecuador, 15:El Salvador, 16:France,
                  17:Germany, 18:Guatemala, 19:Honduras, 20:Hong Kong SAR, 21:Ireland, 22:Italy, 23:Jamaica,
                  24:Korea, 25:Liechtenstein, 26:Luxembourg, 27:Mexico, 28:Netherlands, 29:New Zealand, 30:Nicaragua,
                  31:Panama, 32:Paraguay, 33:Peru, 34:Portugal, 35:Peoples Republic of China, 36:Puerto Rico,
                  37:Russia, 38:Singapore, 39:South Africa, 40:Spain, 41:Sweden, 42:Switzerland, 43:Taiwan, 44:Trinidad,
                  45:United Kingdom, 46:United States, 47:Uruguay, 48:Venezuela.
                * **collections** (bool): Use collection info from The Movie Database. Default value false.
                * **localart** (bool): Prefer artwork based on library language. Default value true.
                * **adult** (bool): Include adult content. Default value false.
                * **usage** (bool): Send anonymous usage data to Plex. Default value true.

            **TheMovieDB Other Video Options** (com.plexapp.agents.themoviedb)

                * **collections** (bool): Use collection info from The Movie Database. Default value false.
                * **localart** (bool): Prefer artwork based on library language. Default value true.
                * **adult** (bool): Include adult content. Default value false.
                * **country** (int): Country (used for release date and content rating). Default
                  value 47 Possible options 0:, 1:Argentina, 2:Australia, 3:Austria, 4:Belgium, 5:Belize,
                  6:Bolivia, 7:Brazil, 8:Canada, 9:Chile, 10:Colombia, 11:Costa Rica, 12:Czech Republic,
                  13:Denmark, 14:Dominican Republic, 15:Ecuador, 16:El Salvador, 17:France, 18:Germany,
                  19:Guatemala, 20:Honduras, 21:Hong Kong SAR, 22:Ireland, 23:Italy, 24:Jamaica,
                  25:Korea, 26:Liechtenstein, 27:Luxembourg, 28:Mexico, 29:Netherlands, 30:New Zealand,
                  31:Nicaragua, 32:Panama, 33:Paraguay, 34:Peru, 35:Portugal,
                  36:Peoples Republic of China, 37:Puerto Rico, 38:Russia, 39:Singapore,
                  40:South Africa, 41:Spain, 42:Sweden, 43:Switzerland, 44:Taiwan, 45:Trinidad,
                  46:United Kingdom, 47:United States, 48:Uruguay, 49:Venezuela.
        """
        part = '/library/sections?name=%s&type=%s&agent=%s&scanner=%s&language=%s&location=%s' % (
            quote_plus(name), type, agent, quote_plus(scanner), language, quote_plus(location))  # noqa E126
        if kwargs:
            part += urlencode(kwargs)
        return self._server.query(part, method=self._server._session.post)


class LibrarySection(PlexObject):
    """ Base class for a single library section.

        Attributes:
            ALLOWED_FILTERS (tuple): ()
            ALLOWED_SORT (tuple): ()
            BOOLEAN_FILTERS (tuple<str>): ('unwatched', 'duplicate')
            server (:class:`~plexapi.server.PlexServer`): Server this client is connected to.
            initpath (str): Path requested when building this object.
            agent (str): Unknown (com.plexapp.agents.imdb, etc)
            allowSync (bool): True if you allow syncing content from this section.
            art (str): Wallpaper artwork used to respresent this section.
            composite (str): Composit image used to represent this section.
            createdAt (datetime): Datetime this library section was created.
            filters (str): Unknown
            key (str): Key (or ID) of this library section.
            language (str): Language represented in this section (en, xn, etc).
            locations (str): Paths on disk where section content is stored.
            refreshing (str): True if this section is currently being refreshed.
            scanner (str): Internal scanner used to find media (Plex Movie Scanner, Plex Premium Music Scanner, etc.)
            thumb (str): Thumbnail image used to represent this section.
            title (str): Title of this section.
            type (str): Type of content section represents (movie, artist, photo, show).
            updatedAt (datetime): Datetime this library section was last updated.
            uuid (str): Unique id for this section (32258d7c-3e6c-4ac5-98ad-bad7a3b78c63)
    """
    ALLOWED_FILTERS = ()
    ALLOWED_SORT = ()
    BOOLEAN_FILTERS = ('unwatched', 'duplicate')

    def _loadData(self, data):
        self._data = data
        self.agent = data.attrib.get('agent')
        self.allowSync = utils.cast(bool, data.attrib.get('allowSync'))
        self.art = data.attrib.get('art')
        self.composite = data.attrib.get('composite')
        self.createdAt = utils.toDatetime(data.attrib.get('createdAt'))
        self.filters = data.attrib.get('filters')
        self.key = data.attrib.get('key')  # invalid key from plex
        self.language = data.attrib.get('language')
        self.locations = self.listAttrs(data, 'path', etag='Location')
        self.refreshing = utils.cast(bool, data.attrib.get('refreshing'))
        self.scanner = data.attrib.get('scanner')
        self.thumb = data.attrib.get('thumb')
        self.title = data.attrib.get('title')
        self.type = data.attrib.get('type')
        self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
        self.uuid = data.attrib.get('uuid')

    def delete(self):
        """ Delete a library section. """
        try:
            return self._server.query('/library/sections/%s' % self.key, method=self._server._session.delete)
        except BadRequest:  # pragma: no cover
            msg = 'Failed to delete library %s' % self.key
            msg += 'You may need to allow this permission in your Plex settings.'
            log.error(msg)
            raise

    def edit(self, **kwargs):
        """ Edit a library (Note: agent is required). See :class:`~plexapi.library.Library` for example usage.

            Parameters:
                kwargs (dict): Dict of settings to edit.
        """
        part = '/library/sections/%s?%s' % (self.key, urlencode(kwargs))
        self._server.query(part, method=self._server._session.put)

        # Reload this way since the self.key dont have a full path, but is simply a id.
        for s in self._server.library.sections():
            if s.key == self.key:
                return s

    def get(self, title):
        """ Returns the media item with the specified title.

            Parameters:
                title (str): Title of the item to return.
        """
        key = '/library/sections/%s/all' % self.key
        return self.fetchItem(key, title__iexact=title)

    def all(self, **kwargs):
        """ Returns a list of media from this library section. """
        key = '/library/sections/%s/all' % self.key
        return self.fetchItems(key, **kwargs)

    def onDeck(self):
        """ Returns a list of media items on deck from this library section. """
        key = '/library/sections/%s/onDeck' % self.key
        return self.fetchItems(key)

    def recentlyAdded(self, maxresults=50):
        """ Returns a list of media items recently added from this library section.

            Parameters:
                maxresults (int): Max number of items to return (default 50).
        """
        return self.search(sort='addedAt:desc', maxresults=maxresults)

    def analyze(self):
        """ Run an analysis on all of the items in this library section. See
            See :func:`~plexapi.base.PlexPartialObject.analyze` for more details.
        """
        key = '/library/sections/%s/analyze' % self.key
        self._server.query(key, method=self._server._session.put)

    def emptyTrash(self):
        """ If a section has items in the Trash, use this option to empty the Trash. """
        key = '/library/sections/%s/emptyTrash' % self.key
        self._server.query(key, method=self._server._session.put)

    def update(self):
        """ Scan this section for new media. """
        key = '/library/sections/%s/refresh' % self.key
        self._server.query(key)

    def cancelUpdate(self):
        """ Cancel update of this Library Section. """
        key = '/library/sections/%s/refresh' % self.key
        self._server.query(key, method=self._server._session.delete)

    def refresh(self):
        """ Forces a download of fresh media information from the internet.
            This can take a long time. Any locked fields are not modified.
        """
        key = '/library/sections/%s/refresh?force=1' % self.key
        self._server.query(key)

    def deleteMediaPreviews(self):
        """ Delete the preview thumbnails for items in this library. This cannot
            be undone. Recreating media preview files can take hours or even days.
        """
        key = '/library/sections/%s/indexes' % self.key
        self._server.query(key, method=self._server._session.delete)

    def listChoices(self, category, libtype=None, **kwargs):
        """ Returns a list of :class:`~plexapi.library.FilterChoice` objects for the
            specified category and libtype. kwargs can be any of the same kwargs in
            :func:`plexapi.library.LibraySection.search()` to help narrow down the choices
            to only those that matter in your current context.

            Parameters:
                category (str): Category to list choices for (genre, contentRating, etc).
                libtype (int): Library type of item filter.
                **kwargs (dict): Additional kwargs to narrow down the choices.

            Raises:
                :class:`~plexapi.exceptions.BadRequest`: Cannot include kwarg equal to specified category.
        """
        # TODO: Should this be moved to base?
        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)
        key = '/library/sections/%s/%s%s' % (self.key, category, utils.joinArgs(args))
        return self.fetchItems(key, cls=FilterChoice)

    def search(self, title=None, sort=None, maxresults=999999, libtype=None, **kwargs):
        """ Search the library. If there are many results, they will be fetched from the server
            in batches of X_PLEX_CONTAINER_SIZE amounts. If you're only looking for the first <num>
            results, it would be wise to set the maxresults option to that amount so this functions
            doesn't iterate over all results on the server.

            Parameters:
                title (str): General string query to search for (optional).
                sort (str): column:dir; column can be any of {addedAt, originallyAvailableAt, lastViewedAt,
                      titleSort, rating, mediaHeight, duration}. dir can be asc or desc (optional).
                maxresults (int): Only return the specified number of results (optional).
                libtype (str): Filter results to a spcifiec libtype (movie, show, episode, artist,
                    album, track; optional).
                **kwargs (dict): Any of the available filters for the current library section. Partial string
                        matches allowed. Multiple matches OR together. All inputs will be compared with the
                        available options and a warning logged if the option does not appear valid.

                        * unwatched: Display or hide unwatched content (True, False). [all]
                        * duplicate: Display or hide duplicate items (True, False). [movie]
                        * actor: List of actors to search ([actor_or_id, ...]). [movie]
                        * collection: List of collections to search within ([collection_or_id, ...]). [all]
                        * contentRating: List of content ratings to search within ([rating_or_key, ...]). [movie,tv]
                        * country: List of countries to search within ([country_or_key, ...]). [movie,music]
                        * decade: List of decades to search within ([yyy0, ...]). [movie]
                        * director: List of directors to search ([director_or_id, ...]). [movie]
                        * genre: List Genres to search within ([genere_or_id, ...]). [all]
                        * network: List of TV networks to search within ([resolution_or_key, ...]). [tv]
                        * resolution: List of video resolutions to search within ([resolution_or_key, ...]). [movie]
                        * studio: List of studios to search within ([studio_or_key, ...]). [music]
                        * year: List of years to search within ([yyyy, ...]). [all]
        """
        # cleanup the core arguments
        args = {}
        for category, value in kwargs.items():
            args[category] = self._cleanSearchFilter(category, value, libtype)
        if title is not None:
            args['title'] = title
        if sort is not None:
            args['sort'] = self._cleanSearchSort(sort)
        if libtype is not None:
            args['type'] = utils.searchType(libtype)
        # iterate over the results
        results, subresults = [], '_init'
        args['X-Plex-Container-Start'] = 0
        args['X-Plex-Container-Size'] = min(X_PLEX_CONTAINER_SIZE, maxresults)
        while subresults and maxresults > len(results):
            key = '/library/sections/%s/all%s' % (self.key, utils.joinArgs(args))
            subresults = self.fetchItems(key)
            results += subresults[:maxresults - len(results)]
            args['X-Plex-Container-Start'] += args['X-Plex-Container-Size']
        return results

    def _cleanSearchFilter(self, category, value, libtype=None):
        # check a few things before we begin
        if category not in self.ALLOWED_FILTERS:
            raise BadRequest('Unknown filter category: %s' % category)
        if category in self.BOOLEAN_FILTERS:
            return '1' if value else '0'
        if not isinstance(value, (list, tuple)):
            value = [value]
        # convert list of values to list of keys or ids
        result = set()
        choices = self.listChoices(category, libtype)
        lookup = {c.title.lower(): unquote(unquote(c.key)) for c in choices}
        allowed = set(c.key for c in choices)
        for item in value:
            item = str((item.id or item.tag) if isinstance(item, MediaTag) else item).lower()
            # find most logical choice(s) to use in url
            if item in allowed: result.add(item); continue
            if item in lookup: result.add(lookup[item]); continue
            matches = [k for t, k in lookup.items() if item in t]
            if matches: map(result.add, matches); continue
            # nothing matched; use raw item value
            log.warning('Filter value not listed, using raw item value: %s' % item)
            result.add(item)
        return ','.join(result)

    def _cleanSearchSort(self, sort):
        sort = '%s:asc' % sort if ':' not in sort else sort
        scol, sdir = sort.lower().split(':')
        lookup = {s.lower(): s for s in self.ALLOWED_SORT}
        if scol not in lookup:
            raise BadRequest('Unknown sort column: %s' % scol)
        if sdir not in ('asc', 'desc'):
            raise BadRequest('Unknown sort dir: %s' % sdir)
        return '%s:%s' % (lookup[scol], sdir)


class MovieSection(LibrarySection):
    """ Represents a :class:`~plexapi.library.LibrarySection` section containing movies.

        Attributes:
            ALLOWED_FILTERS (list<str>): List of allowed search filters. ('unwatched',
                'duplicate', 'year', 'decade', 'genre', 'contentRating', 'collection',
                'director', 'actor', 'country', 'studio', 'resolution', 'guid', 'label')
            ALLOWED_SORT (list<str>): List of allowed sorting keys. ('addedAt',
                'originallyAvailableAt', 'lastViewedAt', 'titleSort', 'rating',
                'mediaHeight', 'duration')
            TAG (str): 'Directory'
            TYPE (str): 'movie'
    """
    ALLOWED_FILTERS = ('unwatched', 'duplicate', 'year', 'decade', 'genre', 'contentRating',
                       'collection', 'director', 'actor', 'country', 'studio', 'resolution',
                       'guid', 'label')
    ALLOWED_SORT = ('addedAt', 'originallyAvailableAt', 'lastViewedAt', 'titleSort', 'rating',
                    'mediaHeight', 'duration')
    TAG = 'Directory'
    TYPE = 'movie'


class ShowSection(LibrarySection):
    """ Represents a :class:`~plexapi.library.LibrarySection` section containing tv shows.

        Attributes:
            ALLOWED_FILTERS (list<str>): List of allowed search filters. ('unwatched',
                'year', 'genre', 'contentRating', 'network', 'collection', 'guid', 'label')
            ALLOWED_SORT (list<str>): List of allowed sorting keys. ('addedAt', 'lastViewedAt',
                'originallyAvailableAt', 'titleSort', 'rating', 'unwatched')
            TAG (str): 'Directory'
            TYPE (str): 'show'
    """
    ALLOWED_FILTERS = ('unwatched', 'year', 'genre', 'contentRating', 'network', 'collection',
                       'guid', 'duplicate', 'label')
    ALLOWED_SORT = ('addedAt', 'lastViewedAt', 'originallyAvailableAt', 'titleSort',
                    'rating', 'unwatched')
    TAG = 'Directory'
    TYPE = 'show'

    def searchShows(self, **kwargs):
        """ Search for a show. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
        return self.search(libtype='show', **kwargs)

    def searchEpisodes(self, **kwargs):
        """ Search for an episode. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
        return self.search(libtype='episode', **kwargs)

    def recentlyAdded(self, libtype='episode', maxresults=50):
        """ Returns a list of recently added episodes from this library section.

            Parameters:
                maxresults (int): Max number of items to return (default 50).
        """
        return self.search(sort='addedAt:desc', libtype=libtype, maxresults=maxresults)


class MusicSection(LibrarySection):
    """ Represents a :class:`~plexapi.library.LibrarySection` section containing music artists.

        Attributes:
            ALLOWED_FILTERS (list<str>): List of allowed search filters. ('genre',
                'country', 'collection')
            ALLOWED_SORT (list<str>): List of allowed sorting keys. ('addedAt',
                'lastViewedAt', 'viewCount', 'titleSort')
            TAG (str): 'Directory'
            TYPE (str): 'artist'
    """
    ALLOWED_FILTERS = ('genre', 'country', 'collection', 'mood')
    ALLOWED_SORT = ('addedAt', 'lastViewedAt', 'viewCount', 'titleSort')
    TAG = 'Directory'
    TYPE = 'artist'

    def albums(self):
        """ Returns a list of :class:`~plexapi.audio.Album` objects in this section. """
        key = '/library/sections/%s/albums' % self.key
        return self.fetchItems(key)

    def searchArtists(self, **kwargs):
        """ Search for an artist. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
        return self.search(libtype='artist', **kwargs)

    def searchAlbums(self, **kwargs):
        """ Search for an album. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
        return self.search(libtype='album', **kwargs)

    def searchTracks(self, **kwargs):
        """ Search for a track. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
        return self.search(libtype='track', **kwargs)


class PhotoSection(LibrarySection):
    """ Represents a :class:`~plexapi.library.LibrarySection` section containing photos.

        Attributes:
            ALLOWED_FILTERS (list<str>): List of allowed search filters. ('all', 'iso',
                'make', 'lens', 'aperture', 'exposure')
            ALLOWED_SORT (list<str>): List of allowed sorting keys. ('addedAt')
            TAG (str): 'Directory'
            TYPE (str): 'photo'
    """
    ALLOWED_FILTERS = ('all', 'iso', 'make', 'lens', 'aperture', 'exposure')
    ALLOWED_SORT = ('addedAt',)
    TAG = 'Directory'
    TYPE = 'photo'

    def searchAlbums(self, title, **kwargs):
        """ Search for an album. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
        key = '/library/sections/%s/all?type=14' % self.key
        return self.fetchItems(key, title=title)

    def searchPhotos(self, title, **kwargs):
        """ Search for a photo. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
        key = '/library/sections/%s/all?type=13' % self.key
        return self.fetchItems(key, title=title)


class FilterChoice(PlexObject):
    """ Represents a single filter choice. These objects are gathered when using filters
        while searching for library items and is the object returned in the result set of
        :func:`~plexapi.library.LibrarySection.listChoices()`.

        Attributes:
            TAG (str): 'Directory'
            server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to.
            initpath (str): Relative path requested when retrieving specified `data` (optional).
            fastKey (str): API path to quickly list all items in this filter
                (/library/sections/<section>/all?genre=<key>)
            key (str): Short key (id) of this filter option (used ad <key> in fastKey above).
            thumb (str): Thumbnail used to represent this filter option.
            title (str): Human readable name for this filter option.
            type (str): Filter type (genre, contentRating, etc).
    """
    TAG = 'Directory'

    def _loadData(self, data):
        """ Load attribute values from Plex XML response. """
        self._data = data
        self.fastKey = data.attrib.get('fastKey')
        self.key = data.attrib.get('key')
        self.thumb = data.attrib.get('thumb')
        self.title = data.attrib.get('title')
        self.type = data.attrib.get('type')


@utils.registerPlexObject
class Hub(PlexObject):
    """ Represents a single Hub (or category) in the PlexServer search.

        Attributes:
            TAG (str): 'Hub'
            hubIdentifier (str): Unknown.
            size (int): Number of items found.
            title (str): Title of this Hub.
            type (str): Type of items in the Hub.
            items (str): List of items in the Hub.
    """
    TAG = 'Hub'

    def _loadData(self, data):
        """ Load attribute values from Plex XML response. """
        self._data = data
        self.hubIdentifier = data.attrib.get('hubIdentifier')
        self.size = utils.cast(int, data.attrib.get('size'))
        self.title = data.attrib.get('title')
        self.type = data.attrib.get('type')
        self.items = self.findItems(data)

    def __len__(self):
        return self.size
