mirror of
https://github.com/pkkid/python-plexapi
synced 2024-11-29 23:20:23 +00:00
716 lines
37 KiB
Python
716 lines
37 KiB
Python
# -*- 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
|