python-plexapi/plexapi/library.py

1635 lines
73 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
import warnings
2020-05-12 21:15:16 +00:00
from urllib.parse import quote, quote_plus, unquote, urlencode
from plexapi import X_PLEX_CONTAINER_SIZE, log, utils
from plexapi.base import PlexObject, PlexPartialObject
from plexapi.exceptions import BadRequest, NotFound
2020-04-15 18:41:15 +00:00
from plexapi.media import MediaTag
from plexapi.settings import Setting
2014-12-29 03:21:58 +00:00
2020-12-05 07:54:43 +00:00
warnings.simplefilter('default')
2014-12-29 03:21:58 +00:00
class Library(PlexObject):
2017-01-26 04:21:13 +00:00
""" Represents a PlexServer library. This contains all sections of media defined
in your Plex server including video, shows and audio.
Attributes:
key (str): '/library'
2017-01-26 04:21:13 +00:00
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
2014-12-29 03:21:58 +00:00
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):
2017-01-26 04:21:13 +00:00
""" 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
2014-12-29 03:21:58 +00:00
def section(self, title=None):
2017-01-31 04:48:21 +00:00
""" Returns the :class:`~plexapi.library.LibrarySection` that matches the specified title.
2017-01-26 04:21:13 +00:00
Parameters:
title (str): Title of the section to return.
"""
for section in self.sections():
if section.title.lower() == title.lower():
return section
2014-12-29 03:21:58 +00:00
raise NotFound('Invalid library section: %s' % title)
2016-12-15 23:06:12 +00:00
def sectionByID(self, sectionID):
2017-01-31 04:48:21 +00:00
""" Returns the :class:`~plexapi.library.LibrarySection` that matches the specified sectionID.
2017-01-31 00:02:22 +00:00
2017-01-26 04:21:13 +00:00
Parameters:
sectionID (str): ID of the section to return.
2017-01-26 04:21:13 +00:00
"""
if not self._sectionsByID or sectionID not in self._sectionsByID:
self.sections()
return self._sectionsByID[sectionID]
2014-12-29 03:21:58 +00:00
2017-02-09 04:08:25 +00:00
def all(self, **kwargs):
2017-01-26 04:21:13 +00:00
""" 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
2014-12-29 03:21:58 +00:00
def onDeck(self):
2017-01-26 04:21:13 +00:00
""" Returns a list of all media items on deck. """
return self.fetchItems('/library/onDeck')
2014-12-29 03:21:58 +00:00
def recentlyAdded(self):
2017-01-26 04:21:13 +00:00
""" Returns a list of all media items recently added. """
return self.fetchItems('/library/recentlyAdded')
2016-12-15 23:06:12 +00:00
def search(self, title=None, libtype=None, **kwargs):
2017-01-26 04:21:13 +00:00
""" 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.
2016-12-15 23:06:12 +00:00
Example: "studio=Comedy%20Central" or "year=1999" "title=Kung Fu" all work. Other items
2017-01-26 04:21:13 +00:00
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 = {}
2016-12-15 23:06:12 +00:00
if title:
2020-05-11 21:12:15 +00:00
args['title'] = title
2016-12-15 23:06:12 +00:00
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)
2016-12-15 23:06:12 +00:00
2014-12-29 03:21:58 +00:00
def cleanBundles(self):
2017-01-26 04:21:13 +00:00
""" 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.
"""
2017-02-02 03:53:05 +00:00
# TODO: Should this check the response for success or the correct mediaprefix?
self._server.query('/library/clean/bundles?async=1', method=self._server._session.put)
2014-12-29 03:21:58 +00:00
def emptyTrash(self):
2017-01-26 04:21:13 +00:00
""" If a library has items in the Library Trash, use this option to empty the Trash. """
2014-12-29 03:21:58 +00:00
for section in self.sections():
section.emptyTrash()
def optimize(self):
2017-01-26 04:21:13 +00:00
""" 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?async=1', method=self._server._session.put)
2014-12-29 03:21:58 +00:00
def update(self):
""" Scan this library for new items."""
self._server.query('/library/sections/all/refresh')
2017-01-03 22:58:35 +00:00
def cancelUpdate(self):
2017-02-15 04:22:57 +00:00
""" Cancel a library update. """
key = '/library/sections/all/refresh'
self._server.query(key, method=self._server._session.delete)
def refresh(self):
2017-02-15 04:22:57 +00:00
""" 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.
2017-02-18 21:12:43 +00:00
"""
for section in self.sections():
section.deleteMediaPreviews()
2017-02-18 21:12:43 +00:00
2017-02-27 22:16:02 +00:00
def add(self, name='', type='', agent='', scanner='', location='', language='en', *args, **kwargs):
2017-02-27 12:23:35 +00:00
""" Simplified add for the most common options.
2017-03-22 03:04:23 +00:00
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.
2017-04-29 06:21:20 +00:00
* **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
2017-04-29 06:21:20 +00:00
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.
2017-04-29 06:21:20 +00:00
* **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.
2017-04-29 06:21:20 +00:00
* **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.
2017-04-29 06:21:20 +00:00
* **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
2017-04-29 06:21:20 +00:00
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.
2017-02-27 12:23:35 +00:00
"""
2017-02-27 21:04:37 +00:00
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
2017-02-27 21:04:37 +00:00
if kwargs:
2017-02-27 12:23:35 +00:00
part += urlencode(kwargs)
2017-02-27 21:04:37 +00:00
return self._server.query(part, method=self._server._session.post)
2017-02-27 12:23:35 +00:00
def history(self, maxresults=9999999, mindate=None):
""" Get Play History for all library Sections for the owner.
Parameters:
maxresults (int): Only return the specified number of results (optional).
mindate (datetime): Min datetime to return results from.
"""
2019-11-16 21:35:20 +00:00
hist = []
for section in self.sections():
hist.extend(section.history(maxresults=maxresults, mindate=mindate))
2019-11-16 21:35:20 +00:00
return hist
2017-02-27 12:23:35 +00:00
class LibrarySection(PlexObject):
2017-01-26 04:21:13 +00:00
""" Base class for a single library section.
Attributes:
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)
totalSize (int): Total number of item in the library
2017-01-26 04:21:13 +00:00
"""
2014-12-29 03:21:58 +00:00
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
2014-12-29 03:21:58 +00:00
self.language = data.attrib.get('language')
2017-02-13 03:15:47 +00:00
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')
# Private attrs as we dont want a reload.
self._total_size = None
2020-04-27 16:22:10 +00:00
def fetchItems(self, ekey, cls=None, container_start=None, container_size=None, **kwargs):
""" Load the specified key to find and build all items with the specified tag
and attrs. See :func:`~plexapi.base.PlexObject.fetchItem` for more details
on how this is used.
2020-04-27 16:22:10 +00:00
Parameters:
container_start (None, int): offset to get a subset of the data
container_size (None, int): How many items in data
"""
url_kw = {}
2020-04-27 16:22:10 +00:00
if container_start is not None:
url_kw["X-Plex-Container-Start"] = container_start
if container_size is not None:
url_kw["X-Plex-Container-Size"] = container_size
if ekey is None:
raise BadRequest('ekey was not provided')
data = self._server.query(ekey, params=url_kw)
2020-04-26 21:14:00 +00:00
if '/all' in ekey:
# totalSize is only included in the xml response
# if container size is used.
total_size = data.attrib.get("totalSize") or data.attrib.get("size")
self._total_size = utils.cast(int, total_size)
2020-04-26 21:14:00 +00:00
items = self.findItems(data, cls, ekey, **kwargs)
librarySectionID = data.attrib.get('librarySectionID')
if librarySectionID:
for item in items:
item.librarySectionID = librarySectionID
return items
@property
def totalSize(self):
if self._total_size is None:
part = '/library/sections/%s/all?X-Plex-Container-Start=0&X-Plex-Container-Size=1' % self.key
data = self._server.query(part)
self._total_size = int(data.attrib.get("totalSize"))
return self._total_size
2014-12-29 03:21:58 +00:00
def delete(self):
2017-03-22 03:04:23 +00:00
""" Delete a library section. """
try:
return self._server.query('/library/sections/%s' % self.key, method=self._server._session.delete)
2017-02-10 23:16:34 +00:00
except BadRequest: # pragma: no cover
2017-02-13 03:38:56 +00:00
msg = 'Failed to delete library %s' % self.key
msg += 'You may need to allow this permission in your Plex settings.'
log.error(msg)
raise
2020-04-15 18:41:15 +00:00
def reload(self, key=None):
2020-04-15 23:01:45 +00:00
return self._server.library.section(self.title)
2020-04-15 18:41:15 +00:00
def edit(self, agent=None, **kwargs):
""" Edit a library (Note: agent is required). See :class:`~plexapi.library.Library` for example usage.
2017-10-03 18:39:28 +00:00
2017-03-22 03:04:23 +00:00
Parameters:
kwargs (dict): Dict of settings to edit.
2017-02-27 21:04:37 +00:00
"""
if not agent:
agent = self.agent
part = '/library/sections/%s?agent=%s&%s' % (self.key, agent, urlencode(kwargs))
2017-02-27 22:16:02 +00:00
self._server.query(part, method=self._server._session.put)
2017-02-27 22:20:10 +00:00
2017-02-27 22:16:02 +00:00
# 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
2014-12-29 03:21:58 +00:00
def get(self, title):
2017-01-26 04:21:13 +00:00
""" Returns the media item with the specified title.
Parameters:
title (str): Title of the item to return.
"""
2020-05-11 21:12:15 +00:00
key = '/library/sections/%s/all?title=%s' % (self.key, quote(title, safe=''))
return self.fetchItem(key, title__iexact=title)
2014-12-29 03:21:58 +00:00
2018-12-21 19:51:39 +00:00
def all(self, sort=None, **kwargs):
2019-02-07 00:04:26 +00:00
""" Returns a list of media from this library section.
2018-12-21 19:51:39 +00:00
Parameters:
sort (string): The sort string
"""
sortStr = ''
2019-02-07 00:04:26 +00:00
if sort is not None:
2018-12-21 19:51:39 +00:00
sortStr = '?sort=' + sort
2019-09-21 20:11:57 +00:00
2018-12-21 19:51:39 +00:00
key = '/library/sections/%s/all%s' % (self.key, sortStr)
2017-02-09 04:08:25 +00:00
return self.fetchItems(key, **kwargs)
2016-12-15 23:06:12 +00:00
2020-07-02 05:33:36 +00:00
def folders(self):
2020-11-23 03:06:30 +00:00
""" Returns a list of available :class:`~plexapi.library.Folder` for this library section.
2020-07-02 05:33:36 +00:00
"""
key = '/library/sections/%s/folder' % self.key
return self.fetchItems(key, Folder)
def hubs(self):
2020-11-23 03:06:30 +00:00
""" Returns a list of available :class:`~plexapi.library.Hub` for this library section.
"""
key = '/hubs/sections/%s' % self.key
return self.fetchItems(key)
def _filters(self):
""" Returns a list of :class:`~plexapi.library.Filter` from this library section. """
key = '/library/sections/%s/filters' % self.key
return self.fetchItems(key, cls=Filter)
2020-06-30 13:19:53 +00:00
def _sorts(self, mediaType=None):
2020-11-23 03:06:30 +00:00
""" Returns a list of available :class:`~plexapi.library.Sort` for this library section.
"""
2020-06-30 13:19:53 +00:00
items = []
for data in self.listChoices('sorts', mediaType):
sort = Sort(server=self._server, data=data._data)
sort._initpath = data._initpath
items.append(sort)
return items
def filterFields(self, mediaType=None):
2020-11-23 03:06:30 +00:00
""" Returns a list of available :class:`~plexapi.library.FilterField` for this library section.
"""
items = []
key = '/library/sections/%s/filters?includeMeta=1' % self.key
data = self._server.query(key)
for meta in data.iter('Meta'):
for metaType in meta.iter('Type'):
if not mediaType or metaType.attrib.get('type') == mediaType:
fields = self.findItems(metaType, FilterField)
for field in fields:
field._initpath = metaType.attrib.get('key')
fieldType = [_ for _ in self.findItems(meta, FieldType) if _.type == field.type]
field.operators = fieldType[0].operators
items += fields
if not items and mediaType:
raise BadRequest('mediaType (%s) not found.' % mediaType)
2020-06-30 13:19:53 +00:00
return items
def agents(self):
2020-11-23 03:06:30 +00:00
""" Returns a list of available :class:`~plexapi.media.Agent` for this library section.
"""
return self._server.agents(utils.searchType(self.type))
def settings(self):
""" Returns a list of all library settings. """
key = '/library/sections/%s/prefs' % self.key
data = self._server.query(key)
return self.findItems(data, cls=Setting)
def editAdvanced(self, **kwargs):
""" Edit a library's advanced settings. """
data = {}
idEnums = {}
key = 'prefs[%s]'
for setting in self.settings():
if setting.type != 'bool':
idEnums[setting.id] = setting.enumValues
else:
idEnums[setting.id] = {0: False, 1: True}
for settingID, value in kwargs.items():
try:
enums = idEnums.get(settingID)
enumValues = [int(x) for x in enums]
except TypeError:
raise NotFound('%s not found in %s' % (value, list(idEnums.keys())))
if value in enumValues:
data[key % settingID] = value
else:
raise NotFound('%s not found in %s' % (value, enums))
self.edit(**data)
def defaultAdvanced(self):
""" Edit all of library's advanced settings to default. """
data = {}
key = 'prefs[%s]'
for setting in self.settings():
if setting.type == 'bool':
data[key % setting.id] = int(setting.default)
else:
data[key % setting.id] = setting.default
self.edit(**data)
def timeline(self):
""" Returns a timeline query for this library section. """
key = '/library/sections/%s/timeline' % self.key
data = self._server.query(key)
return LibraryTimeline(self, data)
def onDeck(self):
2017-01-26 04:21:13 +00:00
""" 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):
2017-01-26 04:21:13 +00:00
""" Returns a list of media items recently added from this library section.
2017-01-31 00:02:22 +00:00
2017-01-26 04:21:13 +00:00
Parameters:
maxresults (int): Max number of items to return (default 50).
"""
return self.search(sort='addedAt:desc', maxresults=maxresults)
2016-12-15 23:06:12 +00:00
def firstCharacter(self):
key = '/library/sections/%s/firstCharacter' % self.key
return self.fetchItems(key, cls=FirstCharacter)
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):
2017-01-26 04:21:13 +00:00
""" If a section has items in the Trash, use this option to empty the Trash. """
key = '/library/sections/%s/emptyTrash' % self.key
2017-11-24 23:48:32 +00:00
self._server.query(key, method=self._server._session.put)
2020-11-21 01:05:57 +00:00
def update(self, path=None):
""" Scan this section for new media.
Parameters:
path (str, optional): Full path to folder to scan.
"""
key = '/library/sections/%s/refresh' % self.key
2020-11-21 01:05:57 +00:00
if path is not None:
key += '?path=%s' % quote_plus(path)
self._server.query(key)
def cancelUpdate(self):
2017-02-15 04:22:57 +00:00
""" 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):
2017-02-15 04:22:57 +00:00
""" 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)
2016-12-15 23:06:12 +00:00
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.
2017-02-18 21:12:43 +00:00
"""
key = '/library/sections/%s/indexes' % self.key
self._server.query(key, method=self._server._session.delete)
def listChoices(self, category, libtype=None, **kwargs):
2017-01-26 04:21:13 +00:00
""" Returns a list of :class:`~plexapi.library.FilterChoice` objects for the
specified category and libtype. kwargs can be any of the same kwargs in
2020-11-23 03:06:30 +00:00
:func:`~plexapi.library.LibraySection.search` to help narrow down the choices
2017-01-26 04:21:13 +00:00
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:
2020-11-23 20:20:56 +00:00
:exc:`~plexapi.exceptions.BadRequest`: Cannot include kwarg equal to specified category.
2014-12-29 03:21:58 +00:00
"""
# TODO: Should this be moved to base?
if category in kwargs:
2017-01-26 04:21:13 +00:00
raise BadRequest('Cannot include kwarg equal to specified category: %s' % category)
2014-12-29 03:21:58 +00:00
args = {}
for subcategory, value in kwargs.items():
args[category] = self._cleanSearchFilter(subcategory, value)
2016-12-15 23:06:12 +00:00
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)
2014-12-29 03:21:58 +00:00
2020-04-27 16:22:10 +00:00
def search(self, title=None, sort=None, maxresults=None,
libtype=None, container_start=0, container_size=X_PLEX_CONTAINER_SIZE, **kwargs):
""" Search the library. The http requests will be batched in container_size. 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.
2016-12-15 23:06:12 +00:00
2017-01-26 04:21:13 +00:00
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).
2017-02-20 05:37:00 +00:00
libtype (str): Filter results to a spcifiec libtype (movie, show, episode, artist,
album, track; optional).
2020-04-27 16:22:10 +00:00
container_start (int): default 0
2020-04-26 21:29:28 +00:00
container_size (int): default X_PLEX_CONTAINER_SIZE in your config file.
2017-01-26 04:21:13 +00:00
**kwargs (dict): Any of the available filters for the current library section. Partial string
Improvements in tests process (#297) * lets begin * skip plexpass tests if there is not plexpass on account * test new myplex attrubutes * bootstrap: proper photos organisation * fix rest of photos tests * fix myplex new attributes test * fix music bootstrap by setting agent to lastfm * fix sync tests * increase bootstrap timeout * remove timeout from .travis.yml * do not create playlist-style photoalbums in plex-bootstraptest.py * allow negative filtering in LibrarySection.search() * fix sync tests once again * use sendCrashReports in test_settings * fix test_settings * fix test_video * do not accept eula in bootstrap * fix PlexServer.isLatest() * add test against old version of PlexServer * fix MyPlexAccount.OutOut * add flag for one-time testing in Travis * fix test_library onDeck tests * fix more tests * use tqdm in plex-bootstraptest for media scanning progress * create sections one-by-one * update docs on AlertListener for timeline entries * fix plex-bootstraptest for server version 1.3.2 * display skip/xpass/xfail reasons * fix tests on 1.3 * wait for music to be fully processed in plex-bootstraptest * fix misplaced TEST_ACCOUNT_ONCE * fix test_myplex_users, not sure if in proper-way * add pytest-rerunfailures; mark test_myplex_optout as flaky * fix comment * Revert "add pytest-rerunfailures; mark test_myplex_optout as flaky" This reverts commit 580e4c95a758c92329d757eb2f3fc3bf44b26f09. * restart plex container on failure * add conftest.wait_until() and used where some retries are required * add more wait_until() usage in test_sync * fix managed user search * fix updating managed users in myplex * allow to add new servers to existent users * add new server to a shared user while bootstrapping * add some docs on testing process * perform few attemps when unable to get the claim token * unlock websocket-client in requirements_dev * fix docblock in tools/plex-teardowntest * do not hardcode mediapart size in test_video * remove cache:pip from travis * Revert "unlock websocket-client in requirements_dev" This reverts commit 0d536bd06dbdc4a4b869a1686f8cd008898859fe. * remove debug from server.py * improve webhook tests * fix type() check to isinstance() * remove excessive `else` branch due to Hellowlol advice * add `unknown` as allowed `myPlexMappingState` in test_server
2018-09-14 18:03:23 +00:00
matches allowed. Multiple matches OR together. Negative filtering also possible, just add an
exclamation mark to the end of filter name, e.g. `resolution!=1x1`.
2016-12-15 23:06:12 +00:00
2017-01-26 04:21:13 +00:00
* 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]
Improvements in tests process (#297) * lets begin * skip plexpass tests if there is not plexpass on account * test new myplex attrubutes * bootstrap: proper photos organisation * fix rest of photos tests * fix myplex new attributes test * fix music bootstrap by setting agent to lastfm * fix sync tests * increase bootstrap timeout * remove timeout from .travis.yml * do not create playlist-style photoalbums in plex-bootstraptest.py * allow negative filtering in LibrarySection.search() * fix sync tests once again * use sendCrashReports in test_settings * fix test_settings * fix test_video * do not accept eula in bootstrap * fix PlexServer.isLatest() * add test against old version of PlexServer * fix MyPlexAccount.OutOut * add flag for one-time testing in Travis * fix test_library onDeck tests * fix more tests * use tqdm in plex-bootstraptest for media scanning progress * create sections one-by-one * update docs on AlertListener for timeline entries * fix plex-bootstraptest for server version 1.3.2 * display skip/xpass/xfail reasons * fix tests on 1.3 * wait for music to be fully processed in plex-bootstraptest * fix misplaced TEST_ACCOUNT_ONCE * fix test_myplex_users, not sure if in proper-way * add pytest-rerunfailures; mark test_myplex_optout as flaky * fix comment * Revert "add pytest-rerunfailures; mark test_myplex_optout as flaky" This reverts commit 580e4c95a758c92329d757eb2f3fc3bf44b26f09. * restart plex container on failure * add conftest.wait_until() and used where some retries are required * add more wait_until() usage in test_sync * fix managed user search * fix updating managed users in myplex * allow to add new servers to existent users * add new server to a shared user while bootstrapping * add some docs on testing process * perform few attemps when unable to get the claim token * unlock websocket-client in requirements_dev * fix docblock in tools/plex-teardowntest * do not hardcode mediapart size in test_video * remove cache:pip from travis * Revert "unlock websocket-client in requirements_dev" This reverts commit 0d536bd06dbdc4a4b869a1686f8cd008898859fe. * remove debug from server.py * improve webhook tests * fix type() check to isinstance() * remove excessive `else` branch due to Hellowlol advice * add `unknown` as allowed `myPlexMappingState` in test_server
2018-09-14 18:03:23 +00:00
Raises:
2020-11-23 20:20:56 +00:00
:exc:`~plexapi.exceptions.BadRequest`: when applying unknown filter
"""
# cleanup the core arguments
args = {}
for category, value in kwargs.items():
args[category] = self._cleanSearchFilter(category, value, libtype)
2016-12-15 23:06:12 +00:00
if title is not None:
2020-05-11 21:12:15 +00:00
args['title'] = title
2016-12-15 23:06:12 +00:00
if sort is not None:
args['sort'] = self._cleanSearchSort(sort)
if libtype is not None:
args['type'] = utils.searchType(libtype)
results = []
subresults = []
offset = container_start
2020-04-27 16:22:10 +00:00
if maxresults is not None:
container_size = min(container_size, maxresults)
while True:
key = '/library/sections/%s/all%s' % (self.key, utils.joinArgs(args))
2020-04-27 16:22:10 +00:00
subresults = self.fetchItems(key, container_start=container_start,
container_size=container_size)
if not len(subresults):
if offset > self.totalSize:
log.info("container_start is higher then the number of items in the library")
break
results.extend(subresults)
2020-04-26 21:29:28 +00:00
# self.totalSize is not used as a condition in the while loop as
# this require a additional http request.
2020-04-26 21:29:28 +00:00
# self.totalSize is updated from .fetchItems
wanted_number_of_items = self.totalSize - offset
2020-04-27 16:22:10 +00:00
if maxresults is not None:
wanted_number_of_items = min(maxresults, wanted_number_of_items)
2020-04-27 16:22:10 +00:00
container_size = min(container_size, maxresults - len(results))
if wanted_number_of_items <= len(results):
break
2020-04-27 16:22:10 +00:00
container_start += container_size
2020-04-26 21:29:28 +00:00
return results
2014-12-29 03:21:58 +00:00
def _cleanSearchFilter(self, category, value, libtype=None):
# check a few things before we begin
categories = [x.key for x in self.filterFields()]
booleanFilters = [x.key for x in self.filterFields() if x.type == 'boolean']
Improvements in tests process (#297) * lets begin * skip plexpass tests if there is not plexpass on account * test new myplex attrubutes * bootstrap: proper photos organisation * fix rest of photos tests * fix myplex new attributes test * fix music bootstrap by setting agent to lastfm * fix sync tests * increase bootstrap timeout * remove timeout from .travis.yml * do not create playlist-style photoalbums in plex-bootstraptest.py * allow negative filtering in LibrarySection.search() * fix sync tests once again * use sendCrashReports in test_settings * fix test_settings * fix test_video * do not accept eula in bootstrap * fix PlexServer.isLatest() * add test against old version of PlexServer * fix MyPlexAccount.OutOut * add flag for one-time testing in Travis * fix test_library onDeck tests * fix more tests * use tqdm in plex-bootstraptest for media scanning progress * create sections one-by-one * update docs on AlertListener for timeline entries * fix plex-bootstraptest for server version 1.3.2 * display skip/xpass/xfail reasons * fix tests on 1.3 * wait for music to be fully processed in plex-bootstraptest * fix misplaced TEST_ACCOUNT_ONCE * fix test_myplex_users, not sure if in proper-way * add pytest-rerunfailures; mark test_myplex_optout as flaky * fix comment * Revert "add pytest-rerunfailures; mark test_myplex_optout as flaky" This reverts commit 580e4c95a758c92329d757eb2f3fc3bf44b26f09. * restart plex container on failure * add conftest.wait_until() and used where some retries are required * add more wait_until() usage in test_sync * fix managed user search * fix updating managed users in myplex * allow to add new servers to existent users * add new server to a shared user while bootstrapping * add some docs on testing process * perform few attemps when unable to get the claim token * unlock websocket-client in requirements_dev * fix docblock in tools/plex-teardowntest * do not hardcode mediapart size in test_video * remove cache:pip from travis * Revert "unlock websocket-client in requirements_dev" This reverts commit 0d536bd06dbdc4a4b869a1686f8cd008898859fe. * remove debug from server.py * improve webhook tests * fix type() check to isinstance() * remove excessive `else` branch due to Hellowlol advice * add `unknown` as allowed `myPlexMappingState` in test_server
2018-09-14 18:03:23 +00:00
if category.endswith('!'):
if category[:-1] not in categories:
Improvements in tests process (#297) * lets begin * skip plexpass tests if there is not plexpass on account * test new myplex attrubutes * bootstrap: proper photos organisation * fix rest of photos tests * fix myplex new attributes test * fix music bootstrap by setting agent to lastfm * fix sync tests * increase bootstrap timeout * remove timeout from .travis.yml * do not create playlist-style photoalbums in plex-bootstraptest.py * allow negative filtering in LibrarySection.search() * fix sync tests once again * use sendCrashReports in test_settings * fix test_settings * fix test_video * do not accept eula in bootstrap * fix PlexServer.isLatest() * add test against old version of PlexServer * fix MyPlexAccount.OutOut * add flag for one-time testing in Travis * fix test_library onDeck tests * fix more tests * use tqdm in plex-bootstraptest for media scanning progress * create sections one-by-one * update docs on AlertListener for timeline entries * fix plex-bootstraptest for server version 1.3.2 * display skip/xpass/xfail reasons * fix tests on 1.3 * wait for music to be fully processed in plex-bootstraptest * fix misplaced TEST_ACCOUNT_ONCE * fix test_myplex_users, not sure if in proper-way * add pytest-rerunfailures; mark test_myplex_optout as flaky * fix comment * Revert "add pytest-rerunfailures; mark test_myplex_optout as flaky" This reverts commit 580e4c95a758c92329d757eb2f3fc3bf44b26f09. * restart plex container on failure * add conftest.wait_until() and used where some retries are required * add more wait_until() usage in test_sync * fix managed user search * fix updating managed users in myplex * allow to add new servers to existent users * add new server to a shared user while bootstrapping * add some docs on testing process * perform few attemps when unable to get the claim token * unlock websocket-client in requirements_dev * fix docblock in tools/plex-teardowntest * do not hardcode mediapart size in test_video * remove cache:pip from travis * Revert "unlock websocket-client in requirements_dev" This reverts commit 0d536bd06dbdc4a4b869a1686f8cd008898859fe. * remove debug from server.py * improve webhook tests * fix type() check to isinstance() * remove excessive `else` branch due to Hellowlol advice * add `unknown` as allowed `myPlexMappingState` in test_server
2018-09-14 18:03:23 +00:00
raise BadRequest('Unknown filter category: %s' % category[:-1])
elif category not in categories:
raise BadRequest('Unknown filter category: %s' % category)
if category in booleanFilters:
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)
2016-12-15 23:06:12 +00:00
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
2016-12-15 23:06:12 +00:00
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
2020-04-26 19:55:54 +00:00
log.debug('Filter value not listed, using raw item value: %s' % item)
result.add(item)
return ','.join(result)
2016-12-15 23:06:12 +00:00
def _cleanSearchSort(self, sort):
sort = '%s:asc' % sort if ':' not in sort else sort
scol, sdir = sort.lower().split(':')
allowedSort = [sort.key for sort in self._sorts()]
lookup = {s.lower(): s for s in allowedSort}
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)
def _locations(self):
""" Returns a list of :class:`~plexapi.library.Location` objects
"""
return self.findItems(self._data, etag='Location')
2018-09-08 15:25:16 +00:00
def sync(self, policy, mediaSettings, client=None, clientId=None, title=None, sort=None, libtype=None,
**kwargs):
""" Add current library section as sync item for specified device.
2020-11-23 03:06:30 +00:00
See description of :func:`~plexapi.library.LibrarySection.search` for details about filtering / sorting
and :func:`~plexapi.myplex.MyPlexAccount.sync` for possible exceptions.
2018-09-08 15:25:16 +00:00
Parameters:
2020-11-23 03:06:30 +00:00
policy (:class:`~plexapi.sync.Policy`): policy of syncing the media (how many items to sync and process
2018-09-08 15:25:16 +00:00
watched media or not), generated automatically when method
called on specific LibrarySection object.
2020-11-23 03:06:30 +00:00
mediaSettings (:class:`~plexapi.sync.MediaSettings`): Transcoding settings used for the media, generated
2018-09-08 15:25:16 +00:00
automatically when method called on specific
LibrarySection object.
2020-11-23 03:06:30 +00:00
client (:class:`~plexapi.myplex.MyPlexDevice`): sync destination, see
:func:`~plexapi.myplex.MyPlexAccount.sync`.
clientId (str): sync destination, see :func:`~plexapi.myplex.MyPlexAccount.sync`.
title (str): descriptive title for the new :class:`~plexapi.sync.SyncItem`, if empty the value would be
2018-09-08 15:25:16 +00:00
generated from metadata of current media.
sort (str): formatted as `column:dir`; column can be any of {`addedAt`, `originallyAvailableAt`,
`lastViewedAt`, `titleSort`, `rating`, `mediaHeight`, `duration`}. dir can be `asc` or
`desc`.
libtype (str): Filter results to a specific libtype (`movie`, `show`, `episode`, `artist`, `album`,
`track`).
Returns:
2020-11-23 03:06:30 +00:00
:class:`~plexapi.sync.SyncItem`: an instance of created syncItem.
2018-09-08 15:25:16 +00:00
Raises:
2020-11-23 20:20:56 +00:00
:exc:`~plexapi.exceptions.BadRequest`: when the library is not allowed to sync
2018-09-08 15:25:16 +00:00
Example:
.. code-block:: python
from plexapi import myplex
from plexapi.sync import Policy, MediaSettings, VIDEO_QUALITY_3_MBPS_720p
c = myplex.MyPlexAccount()
target = c.device('Plex Client')
sync_items_wd = c.syncItems(target.clientIdentifier)
srv = c.resource('Server Name').connect()
section = srv.library.section('Movies')
policy = Policy('count', unwatched=True, value=1)
media_settings = MediaSettings.create(VIDEO_QUALITY_3_MBPS_720p)
section.sync(target, policy, media_settings, title='Next best movie', sort='rating:desc')
"""
from plexapi.sync import SyncItem
if not self.allowSync:
raise BadRequest('The requested library is not allowed to sync')
args = {}
for category, value in kwargs.items():
args[category] = self._cleanSearchFilter(category, value, libtype)
if sort is not None:
args['sort'] = self._cleanSearchSort(sort)
if libtype is not None:
args['type'] = utils.searchType(libtype)
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.CONTENT_TYPE
sync_item.metadataType = self.METADATA_TYPE
sync_item.machineIdentifier = self._server.machineIdentifier
key = '/library/sections/%s/all' % self.key
sync_item.location = 'library://%s/directory/%s' % (self.uuid, quote_plus(key + utils.joinArgs(args)))
sync_item.policy = policy
sync_item.mediaSettings = mediaSettings
return myplex.sync(client=client, clientId=clientId, sync_item=sync_item)
def history(self, maxresults=9999999, mindate=None):
""" Get Play History for this library Section for the owner.
Parameters:
maxresults (int): Only return the specified number of results (optional).
mindate (datetime): Min datetime to return results from.
"""
return self._server.history(maxresults=maxresults, mindate=mindate, librarySectionID=self.key, accountID=1)
2019-11-16 21:35:20 +00:00
def collection(self, **kwargs):
2020-12-05 07:54:43 +00:00
msg = 'LibrarySection.collection() will be deprecated in the future, use collections() (plural) instead.'
warnings.warn(msg, DeprecationWarning)
log.warning(msg)
return self.collections()
def collections(self, **kwargs):
""" Returns a list of collections from this library section.
See description of :func:`plexapi.library.LibrarySection.search` for details about filtering / sorting.
"""
return self.search(libtype='collection', **kwargs)
2020-12-04 20:28:11 +00:00
def playlists(self, **kwargs):
""" Returns a list of playlists from this library section. """
key = '/playlists?type=15&playlistType=%s&sectionID=%s' % (self.CONTENT_TYPE, self.key)
return self.fetchItems(key, **kwargs)
2014-12-29 03:21:58 +00:00
class MovieSection(LibrarySection):
2017-01-26 04:21:13 +00:00
""" Represents a :class:`~plexapi.library.LibrarySection` section containing movies.
2017-01-31 00:02:22 +00:00
2017-01-26 04:21:13 +00:00
Attributes:
TAG (str): 'Directory'
2017-01-26 04:21:13 +00:00
TYPE (str): 'movie'
"""
TAG = 'Directory'
2014-12-29 03:21:58 +00:00
TYPE = 'movie'
2018-09-08 15:25:16 +00:00
METADATA_TYPE = 'movie'
CONTENT_TYPE = 'video'
2014-12-29 03:21:58 +00:00
2018-09-08 15:25:16 +00:00
def sync(self, videoQuality, limit=None, unwatched=False, **kwargs):
""" Add current Movie library section as sync item for specified device.
2020-11-23 03:06:30 +00:00
See description of :func:`~plexapi.library.LibrarySection.search` for details about filtering / sorting and
:func:`~plexapi.library.LibrarySection.sync` for details on syncing libraries and possible exceptions.
2018-09-08 15:25:16 +00:00
Parameters:
videoQuality (int): idx of quality of the video, one of VIDEO_QUALITY_* values defined in
2020-11-23 03:06:30 +00:00
:mod:`~plexapi.sync` module.
2018-09-08 15:25:16 +00:00
limit (int): maximum count of movies to sync, unlimited if `None`.
unwatched (bool): if `True` watched videos wouldn't be synced.
Returns:
2020-11-23 03:06:30 +00:00
:class:`~plexapi.sync.SyncItem`: an instance of created syncItem.
2018-09-08 15:25:16 +00:00
Example:
.. code-block:: python
from plexapi import myplex
from plexapi.sync import VIDEO_QUALITY_3_MBPS_720p
c = myplex.MyPlexAccount()
target = c.device('Plex Client')
sync_items_wd = c.syncItems(target.clientIdentifier)
srv = c.resource('Server Name').connect()
section = srv.library.section('Movies')
section.sync(VIDEO_QUALITY_3_MBPS_720p, client=target, limit=1, unwatched=True,
title='Next best movie', sort='rating:desc')
"""
from plexapi.sync import Policy, MediaSettings
kwargs['mediaSettings'] = MediaSettings.createVideo(videoQuality)
kwargs['policy'] = Policy.create(limit, unwatched)
return super(MovieSection, self).sync(**kwargs)
2014-12-29 03:21:58 +00:00
class ShowSection(LibrarySection):
2017-01-26 04:21:13 +00:00
""" Represents a :class:`~plexapi.library.LibrarySection` section containing tv shows.
2017-01-31 00:02:22 +00:00
2017-01-26 04:21:13 +00:00
Attributes:
TAG (str): 'Directory'
2017-01-26 04:21:13 +00:00
TYPE (str): 'show'
"""
TAG = 'Directory'
2014-12-29 03:21:58 +00:00
TYPE = 'show'
2018-09-08 15:25:16 +00:00
METADATA_TYPE = 'episode'
CONTENT_TYPE = 'video'
2015-06-08 16:41:47 +00:00
def searchShows(self, **kwargs):
2020-11-23 03:06:30 +00:00
""" Search for a show. See :func:`~plexapi.library.LibrarySection.search` for usage. """
return self.search(libtype='show', **kwargs)
2014-12-29 03:21:58 +00:00
def searchEpisodes(self, **kwargs):
2020-11-23 03:06:30 +00:00
""" Search for an episode. See :func:`~plexapi.library.LibrarySection.search` for usage. """
return self.search(libtype='episode', **kwargs)
2014-12-29 03:21:58 +00:00
def recentlyAdded(self, libtype='episode', maxresults=50):
2017-01-26 04:21:13 +00:00
""" Returns a list of recently added episodes from this library section.
2017-01-31 00:02:22 +00:00
2017-01-26 04:21:13 +00:00
Parameters:
maxresults (int): Max number of items to return (default 50).
"""
return self.search(sort='episode.addedAt:desc', libtype=libtype, maxresults=maxresults)
2018-09-08 15:25:16 +00:00
def sync(self, videoQuality, limit=None, unwatched=False, **kwargs):
""" Add current Show library section as sync item for specified device.
2020-11-23 03:06:30 +00:00
See description of :func:`~plexapi.library.LibrarySection.search` for details about filtering / sorting and
:func:`~plexapi.library.LibrarySection.sync` for details on syncing libraries and possible exceptions.
2018-09-08 15:25:16 +00:00
Parameters:
videoQuality (int): idx of quality of the video, one of VIDEO_QUALITY_* values defined in
2020-11-23 03:06:30 +00:00
:mod:`~plexapi.sync` module.
2018-09-08 15:25:16 +00:00
limit (int): maximum count of episodes to sync, unlimited if `None`.
unwatched (bool): if `True` watched videos wouldn't be synced.
Returns:
2020-11-23 03:06:30 +00:00
:class:`~plexapi.sync.SyncItem`: an instance of created syncItem.
2018-09-08 15:25:16 +00:00
Example:
.. code-block:: python
from plexapi import myplex
from plexapi.sync import VIDEO_QUALITY_3_MBPS_720p
c = myplex.MyPlexAccount()
target = c.device('Plex Client')
sync_items_wd = c.syncItems(target.clientIdentifier)
srv = c.resource('Server Name').connect()
section = srv.library.section('TV-Shows')
section.sync(VIDEO_QUALITY_3_MBPS_720p, client=target, limit=1, unwatched=True,
title='Next unwatched episode')
"""
from plexapi.sync import Policy, MediaSettings
kwargs['mediaSettings'] = MediaSettings.createVideo(videoQuality)
kwargs['policy'] = Policy.create(limit, unwatched)
return super(ShowSection, self).sync(**kwargs)
2014-12-29 03:21:58 +00:00
class MusicSection(LibrarySection):
2017-01-26 04:21:13 +00:00
""" Represents a :class:`~plexapi.library.LibrarySection` section containing music artists.
2017-01-31 00:02:22 +00:00
2017-01-26 04:21:13 +00:00
Attributes:
TAG (str): 'Directory'
2017-01-26 04:21:13 +00:00
TYPE (str): 'artist'
"""
TAG = 'Directory'
TYPE = 'artist'
2016-12-15 23:06:12 +00:00
2018-09-08 15:25:16 +00:00
CONTENT_TYPE = 'audio'
METADATA_TYPE = 'track'
def albums(self):
2017-01-26 04:21:13 +00:00
""" Returns a list of :class:`~plexapi.audio.Album` objects in this section. """
key = '/library/sections/%s/albums' % self.key
return self.fetchItems(key)
def stations(self):
""" Returns a list of :class:`~plexapi.audio.Album` objects in this section. """
key = '/hubs/sections/%s?includeStations=1' % self.key
return self.fetchItems(key, cls=Station)
2016-04-10 03:59:47 +00:00
def searchArtists(self, **kwargs):
2020-11-23 03:06:30 +00:00
""" Search for an artist. See :func:`~plexapi.library.LibrarySection.search` for usage. """
return self.search(libtype='artist', **kwargs)
2016-04-10 03:59:47 +00:00
def searchAlbums(self, **kwargs):
2020-11-23 03:06:30 +00:00
""" Search for an album. See :func:`~plexapi.library.LibrarySection.search` for usage. """
return self.search(libtype='album', **kwargs)
2016-12-15 23:06:12 +00:00
def searchTracks(self, **kwargs):
2020-11-23 03:06:30 +00:00
""" Search for a track. See :func:`~plexapi.library.LibrarySection.search` for usage. """
return self.search(libtype='track', **kwargs)
2018-09-08 15:25:16 +00:00
def sync(self, bitrate, limit=None, **kwargs):
""" Add current Music library section as sync item for specified device.
2020-11-23 03:06:30 +00:00
See description of :func:`~plexapi.library.LibrarySection.search` for details about filtering / sorting and
:func:`~plexapi.library.LibrarySection.sync` for details on syncing libraries and possible exceptions.
2018-09-08 15:25:16 +00:00
Parameters:
bitrate (int): maximum bitrate for synchronized music, better use one of MUSIC_BITRATE_* values from the
2020-11-23 03:06:30 +00:00
module :mod:`~plexapi.sync`.
2018-09-08 15:25:16 +00:00
limit (int): maximum count of tracks to sync, unlimited if `None`.
Returns:
2020-11-23 03:06:30 +00:00
:class:`~plexapi.sync.SyncItem`: an instance of created syncItem.
2018-09-08 15:25:16 +00:00
Example:
.. code-block:: python
from plexapi import myplex
from plexapi.sync import AUDIO_BITRATE_320_KBPS
c = myplex.MyPlexAccount()
target = c.device('Plex Client')
sync_items_wd = c.syncItems(target.clientIdentifier)
srv = c.resource('Server Name').connect()
section = srv.library.section('Music')
section.sync(AUDIO_BITRATE_320_KBPS, client=target, limit=100, sort='addedAt:desc',
title='New music')
"""
from plexapi.sync import Policy, MediaSettings
kwargs['mediaSettings'] = MediaSettings.createMusic(bitrate)
kwargs['policy'] = Policy.create(limit)
return super(MusicSection, self).sync(**kwargs)
2016-04-10 03:59:47 +00:00
class PhotoSection(LibrarySection):
2017-01-26 04:21:13 +00:00
""" Represents a :class:`~plexapi.library.LibrarySection` section containing photos.
2017-01-31 00:02:22 +00:00
2017-01-26 04:21:13 +00:00
Attributes:
TAG (str): 'Directory'
2017-01-26 04:21:13 +00:00
TYPE (str): 'photo'
"""
TAG = 'Directory'
2016-04-10 03:59:47 +00:00
TYPE = 'photo'
2018-09-08 15:25:16 +00:00
CONTENT_TYPE = 'photo'
METADATA_TYPE = 'photo'
2016-12-15 23:06:12 +00:00
def collections(self, **kwargs):
raise NotImplementedError('Collections are not available for a Photo library.')
def searchAlbums(self, title, **kwargs):
2020-11-23 03:06:30 +00:00
""" Search for an album. See :func:`~plexapi.library.LibrarySection.search` for usage. """
Improvements in tests process (#297) * lets begin * skip plexpass tests if there is not plexpass on account * test new myplex attrubutes * bootstrap: proper photos organisation * fix rest of photos tests * fix myplex new attributes test * fix music bootstrap by setting agent to lastfm * fix sync tests * increase bootstrap timeout * remove timeout from .travis.yml * do not create playlist-style photoalbums in plex-bootstraptest.py * allow negative filtering in LibrarySection.search() * fix sync tests once again * use sendCrashReports in test_settings * fix test_settings * fix test_video * do not accept eula in bootstrap * fix PlexServer.isLatest() * add test against old version of PlexServer * fix MyPlexAccount.OutOut * add flag for one-time testing in Travis * fix test_library onDeck tests * fix more tests * use tqdm in plex-bootstraptest for media scanning progress * create sections one-by-one * update docs on AlertListener for timeline entries * fix plex-bootstraptest for server version 1.3.2 * display skip/xpass/xfail reasons * fix tests on 1.3 * wait for music to be fully processed in plex-bootstraptest * fix misplaced TEST_ACCOUNT_ONCE * fix test_myplex_users, not sure if in proper-way * add pytest-rerunfailures; mark test_myplex_optout as flaky * fix comment * Revert "add pytest-rerunfailures; mark test_myplex_optout as flaky" This reverts commit 580e4c95a758c92329d757eb2f3fc3bf44b26f09. * restart plex container on failure * add conftest.wait_until() and used where some retries are required * add more wait_until() usage in test_sync * fix managed user search * fix updating managed users in myplex * allow to add new servers to existent users * add new server to a shared user while bootstrapping * add some docs on testing process * perform few attemps when unable to get the claim token * unlock websocket-client in requirements_dev * fix docblock in tools/plex-teardowntest * do not hardcode mediapart size in test_video * remove cache:pip from travis * Revert "unlock websocket-client in requirements_dev" This reverts commit 0d536bd06dbdc4a4b869a1686f8cd008898859fe. * remove debug from server.py * improve webhook tests * fix type() check to isinstance() * remove excessive `else` branch due to Hellowlol advice * add `unknown` as allowed `myPlexMappingState` in test_server
2018-09-14 18:03:23 +00:00
return self.search(libtype='photoalbum', title=title, **kwargs)
2016-12-15 23:06:12 +00:00
2017-01-31 00:02:22 +00:00
def searchPhotos(self, title, **kwargs):
2020-11-23 03:06:30 +00:00
""" Search for a photo. See :func:`~plexapi.library.LibrarySection.search` for usage. """
Improvements in tests process (#297) * lets begin * skip plexpass tests if there is not plexpass on account * test new myplex attrubutes * bootstrap: proper photos organisation * fix rest of photos tests * fix myplex new attributes test * fix music bootstrap by setting agent to lastfm * fix sync tests * increase bootstrap timeout * remove timeout from .travis.yml * do not create playlist-style photoalbums in plex-bootstraptest.py * allow negative filtering in LibrarySection.search() * fix sync tests once again * use sendCrashReports in test_settings * fix test_settings * fix test_video * do not accept eula in bootstrap * fix PlexServer.isLatest() * add test against old version of PlexServer * fix MyPlexAccount.OutOut * add flag for one-time testing in Travis * fix test_library onDeck tests * fix more tests * use tqdm in plex-bootstraptest for media scanning progress * create sections one-by-one * update docs on AlertListener for timeline entries * fix plex-bootstraptest for server version 1.3.2 * display skip/xpass/xfail reasons * fix tests on 1.3 * wait for music to be fully processed in plex-bootstraptest * fix misplaced TEST_ACCOUNT_ONCE * fix test_myplex_users, not sure if in proper-way * add pytest-rerunfailures; mark test_myplex_optout as flaky * fix comment * Revert "add pytest-rerunfailures; mark test_myplex_optout as flaky" This reverts commit 580e4c95a758c92329d757eb2f3fc3bf44b26f09. * restart plex container on failure * add conftest.wait_until() and used where some retries are required * add more wait_until() usage in test_sync * fix managed user search * fix updating managed users in myplex * allow to add new servers to existent users * add new server to a shared user while bootstrapping * add some docs on testing process * perform few attemps when unable to get the claim token * unlock websocket-client in requirements_dev * fix docblock in tools/plex-teardowntest * do not hardcode mediapart size in test_video * remove cache:pip from travis * Revert "unlock websocket-client in requirements_dev" This reverts commit 0d536bd06dbdc4a4b869a1686f8cd008898859fe. * remove debug from server.py * improve webhook tests * fix type() check to isinstance() * remove excessive `else` branch due to Hellowlol advice * add `unknown` as allowed `myPlexMappingState` in test_server
2018-09-14 18:03:23 +00:00
return self.search(libtype='photo', title=title, **kwargs)
2017-01-03 22:58:35 +00:00
2018-09-08 15:25:16 +00:00
def sync(self, resolution, limit=None, **kwargs):
""" Add current Music library section as sync item for specified device.
2020-11-23 03:06:30 +00:00
See description of :func:`~plexapi.library.LibrarySection.search` for details about filtering / sorting and
:func:`~plexapi.library.LibrarySection.sync` for details on syncing libraries and possible exceptions.
2018-09-08 15:25:16 +00:00
Parameters:
resolution (str): maximum allowed resolution for synchronized photos, see PHOTO_QUALITY_* values in the
2020-11-23 03:06:30 +00:00
module :mod:`~plexapi.sync`.
2018-09-08 15:25:16 +00:00
limit (int): maximum count of tracks to sync, unlimited if `None`.
Returns:
2020-11-23 03:06:30 +00:00
:class:`~plexapi.sync.SyncItem`: an instance of created syncItem.
2018-09-08 15:25:16 +00:00
Example:
.. code-block:: python
from plexapi import myplex
from plexapi.sync import PHOTO_QUALITY_HIGH
c = myplex.MyPlexAccount()
target = c.device('Plex Client')
sync_items_wd = c.syncItems(target.clientIdentifier)
srv = c.resource('Server Name').connect()
section = srv.library.section('Photos')
section.sync(PHOTO_QUALITY_HIGH, client=target, limit=100, sort='addedAt:desc',
title='Fresh photos')
"""
from plexapi.sync import Policy, MediaSettings
kwargs['mediaSettings'] = MediaSettings.createPhoto(resolution)
kwargs['policy'] = Policy.create(limit)
return super(PhotoSection, self).sync(**kwargs)
2017-01-03 22:58:35 +00:00
class FilterChoice(PlexObject):
2017-01-26 04:21:13 +00:00
""" 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
2020-11-23 03:06:30 +00:00
:func:`~plexapi.library.LibrarySection.listChoices`.
2017-01-26 04:21:13 +00:00
Attributes:
TAG (str): 'Directory'
2017-01-26 04:21:13 +00:00
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).
2017-01-31 00:02:22 +00:00
thumb (str): Thumbnail used to represent this filter option.
2017-01-26 04:21:13 +00:00
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')
2020-09-09 19:25:38 +00:00
@utils.registerPlexObject
class LibraryTimeline(PlexObject):
"""Represents a LibrarySection timeline.
Attributes:
TAG (str): 'LibraryTimeline'
size (int): Unknown
allowSync (bool): Unknown
art (str): Relative path to art image.
content (str): "secondary"
identifier (str): "com.plexapp.plugins.library"
latestEntryTime (int): Epoch timestamp
mediaTagPrefix (str): "/system/bundle/media/flags/"
mediaTagVersion (int): Unknown
thumb (str): Relative path to library thumb image.
title1 (str): Name of library section.
updateQueueSize (int): Number of items queued to update.
viewGroup (str): "secondary"
viewMode (int): Unknown
"""
TAG = 'LibraryTimeline'
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
self._data = data
self.size = utils.cast(int, data.attrib.get('size'))
self.allowSync = utils.cast(bool, data.attrib.get('allowSync'))
self.art = data.attrib.get('art')
self.content = data.attrib.get('content')
self.identifier = data.attrib.get('identifier')
self.latestEntryTime = utils.cast(int, data.attrib.get('latestEntryTime'))
self.mediaTagPrefix = data.attrib.get('mediaTagPrefix')
self.mediaTagVersion = utils.cast(int, data.attrib.get('mediaTagVersion'))
self.thumb = data.attrib.get('thumb')
self.title1 = data.attrib.get('title1')
self.updateQueueSize = utils.cast(int, data.attrib.get('updateQueueSize'))
self.viewGroup = data.attrib.get('viewGroup')
self.viewMode = utils.cast(int, data.attrib.get('viewMode'))
2020-06-09 19:56:21 +00:00
@utils.registerPlexObject
class Location(PlexObject):
""" Represents a single library Location.
Attributes:
TAG (str): 'Location'
id (int): Location path ID.
path (str): Path used for library..
"""
TAG = 'Location'
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
self._data = data
self.id = utils.cast(int, data.attrib.get('id'))
self.path = data.attrib.get('path')
class Filter(PlexObject):
""" Represents a single Filter.
Attributes:
TAG (str): 'Directory'
TYPE (str): 'filter'
"""
TAG = 'Directory'
TYPE = 'filter'
def _loadData(self, data):
self._data = data
self.filter = data.attrib.get('filter')
self.filterType = data.attrib.get('filterType')
self.key = data.attrib.get('key')
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')
2020-02-06 18:02:14 +00:00
self.key = data.attrib.get('key')
self.items = self.findItems(data)
def __len__(self):
return self.size
2018-04-10 17:24:39 +00:00
@utils.registerPlexObject
class Station(PlexObject):
2020-06-22 19:27:45 +00:00
""" Represents the Station area in the MusicSection.
Attributes:
2020-06-22 19:27:45 +00:00
TITLE (str): 'Stations'
TYPE (str): 'station'
hubIdentifier (str): Unknown.
size (int): Number of items found.
title (str): Title of this Hub.
type (str): Type of items in the Hub.
2020-06-22 19:27:45 +00:00
more (str): Unknown.
style (str): Unknown
items (str): List of items in the Hub.
"""
TITLE = 'Stations'
TYPE = 'station'
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.more = data.attrib.get('more')
self.style = data.attrib.get('style')
self.items = self.findItems(data)
def __len__(self):
return self.size
2020-06-30 13:06:26 +00:00
class Sort(PlexObject):
2020-07-02 04:40:12 +00:00
""" Represents a Sort element found in library.
2020-06-30 13:06:26 +00:00
2020-07-02 04:40:12 +00:00
Attributes:
TAG (str): 'Sort'
defaultDirection (str): Default sorting direction.
descKey (str): Url key for sorting with desc.
key (str): Url key for sorting,
title (str): Title of sorting,
firstCharacterKey (str): Url path for first character endpoint.
2020-06-30 13:06:26 +00:00
"""
TAG = 'Sort'
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
self._data = data
self.defaultDirection = data.attrib.get('defaultDirection')
self.descKey = data.attrib.get('descKey')
self.key = data.attrib.get('key')
self.title = data.attrib.get('title')
self.firstCharacterKey = data.attrib.get('firstCharacterKey')
2020-06-30 13:06:26 +00:00
class FilterField(PlexObject):
""" Represents a Filters Field element found in library.
2020-07-02 04:40:51 +00:00
Attributes:
TAG (str): 'Field'
key (str): Url key for filter,
title (str): Title of filter.
type (str): Type of filter (string, boolean, integer, date, etc).
subType (str): Subtype of filter (decade, rating, etc).
operators (str): Operators available for this filter.
"""
TAG = 'Field'
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
self._data = data
self.key = data.attrib.get('key')
self.title = data.attrib.get('title')
self.type = data.attrib.get('type')
self.subType = data.attrib.get('subType')
self.operators = []
@utils.registerPlexObject
class Operator(PlexObject):
2020-07-02 04:42:28 +00:00
""" Represents an Operator available for filter.
2020-07-02 04:42:28 +00:00
Attributes:
TAG (str): 'Operator'
key (str): Url key for operator.
title (str): Title of operator.
"""
TAG = 'Operator'
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
self.key = data.attrib.get('key')
self.title = data.attrib.get('title')
2020-07-02 05:33:18 +00:00
class Folder(PlexObject):
""" Represents a Folder inside a library.
Attributes:
key (str): Url key for folder.
title (str): Title of folder.
"""
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
self.key = data.attrib.get('key')
self.title = data.attrib.get('title')
def subfolders(self):
2020-11-23 03:06:30 +00:00
""" Returns a list of available :class:`~plexapi.library.Folder` for this folder.
2020-07-02 05:33:18 +00:00
Continue down subfolders until a mediaType is found.
"""
if self.key.startswith('/library/metadata'):
return self.fetchItems(self.key)
else:
return self.fetchItems(self.key, Folder)
def allSubfolders(self):
2020-11-23 03:06:30 +00:00
""" Returns a list of all available :class:`~plexapi.library.Folder` for this folder.
Only returns :class:`~plexapi.library.Folder`.
"""
2020-09-09 19:23:56 +00:00
folders = []
for folder in self.subfolders():
if not folder.key.startswith('/library/metadata'):
folders.append(folder)
while True:
for subfolder in folder.subfolders():
if not subfolder.key.startswith('/library/metadata'):
folders.append(subfolder)
continue
break
return folders
2020-07-02 05:33:18 +00:00
@utils.registerPlexObject
class FieldType(PlexObject):
2020-07-02 04:42:51 +00:00
""" Represents a FieldType for filter.
2020-07-02 04:42:51 +00:00
Attributes:
TAG (str): 'Operator'
type (str): Type of filter (string, boolean, integer, date, etc),
operators (str): Operators available for this filter.
"""
TAG = 'FieldType'
def __repr__(self):
_type = self._clean(self.firstAttr('type'))
return '<%s>' % ':'.join([p for p in [self.__class__.__name__, _type] if p])
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
self._data = data
self.type = data.attrib.get('type')
self.operators = self.findItems(data, Operator)
2020-06-30 12:59:30 +00:00
class FirstCharacter(PlexObject):
2020-07-02 04:43:21 +00:00
""" Represents a First Character element from a library.
Attributes:
key (str): Url key for character.
size (str): Total amount of library items starting with this character.
title (str): Character (#, !, A, B, C, ...).
"""
2020-06-30 12:59:30 +00:00
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
self._data = data
self.key = data.attrib.get('key')
self.size = data.attrib.get('size')
self.title = data.attrib.get('title')
@utils.registerPlexObject
class Collections(PlexPartialObject):
""" Represents a single Collection.
Attributes:
TAG (str): 'Directory'
TYPE (str): 'collection'
ratingKey (int): Unique key identifying this item.
addedAt (datetime): Datetime this item was added to the library.
art (str): URL to artwork image.
artBlurHash (str): BlurHash string for artwork image.
childCount (int): Count of child object(s)
collectionMode (str): How the items in the collection are displayed.
collectionSort (str): How to sort the items in the collection.
2020-06-06 18:01:16 +00:00
contentRating (str) Content rating (PG-13; NR; TV-G).
fields (list): List of :class:`~plexapi.media.Field`.
2020-06-06 18:01:16 +00:00
guid (str): Plex GUID (collection://XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX).
index (int): Unknown
2020-06-06 18:01:16 +00:00
key (str): API URL (/library/metadata/<ratingkey>).
labels (List<:class:`~plexapi.media.Label`>): List of field objects.
librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID.
librarySectionKey (str): API URL (/library/sections/<sectionkey>).
librarySectionTitle (str): Section Title
maxYear (int): Year
2020-06-06 18:01:16 +00:00
minYear (int): Year
subtype (str): Media type
summary (str): Summary of the collection
thumb (str): URL to thumbnail image.
thumbBlurHash (str): BlurHash string for thumbnail image.
2020-06-06 18:01:16 +00:00
title (str): Collection Title
titleSort (str): Title to use when sorting (defaults to title).
type (str): Hardcoded 'collection'
2020-06-06 18:01:16 +00:00
updatedAt (datatime): Datetime this item was updated.
"""
TAG = 'Directory'
TYPE = 'collection'
def _loadData(self, data):
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
self.art = data.attrib.get('art')
self.artBlurHash = data.attrib.get('artBlurHash')
self.childCount = utils.cast(int, data.attrib.get('childCount'))
self.collectionMode = data.attrib.get('collectionMode')
self.collectionSort = data.attrib.get('collectionSort')
self.contentRating = data.attrib.get('contentRating')
self.fields = self.findItems(data, etag='Field')
self.guid = data.attrib.get('guid')
self.index = utils.cast(int, data.attrib.get('index'))
self.key = data.attrib.get('key').replace('/children', '') # FIX_BUG_50
self.labels = self.findItems(data, etag='Label')
self.librarySectionID = data.attrib.get('librarySectionID')
self.librarySectionKey = data.attrib.get('librarySectionKey')
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
self.maxYear = utils.cast(int, data.attrib.get('maxYear'))
self.minYear = utils.cast(int, data.attrib.get('minYear'))
self.subtype = data.attrib.get('subtype')
self.summary = data.attrib.get('summary')
self.thumb = data.attrib.get('thumb')
self.thumbBlurHash = data.attrib.get('thumbBlurHash')
self.title = data.attrib.get('title')
self.titleSort = data.attrib.get('titleSort')
self.type = data.attrib.get('type')
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
2018-04-15 19:05:05 +00:00
@property
def children(self):
return self.fetchItems(self.key)
def __len__(self):
return self.childCount
def _preferences(self):
""" Returns a list of :class:`~plexapi.settings.Preferences` objects. """
items = []
data = self._server.query(self._details_key)
for item in data.iter('Setting'):
items.append(Setting(data=item, server=self._server))
return items
2018-04-15 19:05:05 +00:00
def delete(self):
part = '/library/metadata/%s' % self.ratingKey
return self._server.query(part, method=self._server._session.delete)
2019-09-21 20:11:57 +00:00
def modeUpdate(self, mode=None):
""" Update Collection Mode
Parameters:
2019-09-21 20:11:57 +00:00
mode: default (Library default)
hide (Hide Collection)
hideItems (Hide Items in this Collection)
showItems (Show this Collection and its Items)
Example:
2019-09-21 20:11:57 +00:00
collection = 'plexapi.library.Collections'
collection.updateMode(mode="hide")
"""
2020-11-22 01:35:33 +00:00
mode_dict = {'default': '-1',
'hide': '0',
'hideItems': '1',
'showItems': '2'}
key = mode_dict.get(mode)
if key is None:
raise BadRequest('Unknown collection mode : %s. Options %s' % (mode, list(mode_dict)))
part = '/library/metadata/%s/prefs?collectionMode=%s' % (self.ratingKey, key)
return self._server.query(part, method=self._server._session.put)
2018-04-15 19:05:05 +00:00
def sortUpdate(self, sort=None):
""" Update Collection Sorting
Parameters:
2019-09-21 20:11:57 +00:00
sort: realease (Order Collection by realease dates)
alpha (Order Collection Alphabetically)
Example:
2019-09-21 20:11:57 +00:00
colleciton = 'plexapi.library.Collections'
collection.updateSort(mode="alpha")
"""
sort_dict = {'release': '0',
'alpha': '1'}
key = sort_dict.get(sort)
if key is None:
raise BadRequest('Unknown sort dir: %s. Options: %s' % (sort, list(sort_dict)))
part = '/library/metadata/%s/prefs?collectionSort=%s' % (self.ratingKey, key)
return self._server.query(part, method=self._server._session.put)
2020-03-18 04:55:38 +00:00
def posters(self):
""" Returns list of available poster objects. :class:`~plexapi.media.Poster`. """
2020-03-18 13:39:44 +00:00
return self.fetchItems('/library/metadata/%s/posters' % self.ratingKey)
2020-03-18 04:55:38 +00:00
def uploadPoster(self, url=None, filepath=None):
""" Upload poster from url or filepath. :class:`~plexapi.media.Poster` to :class:`~plexapi.video.Video`. """
if url:
key = '/library/metadata/%s/posters?url=%s' % (self.ratingKey, quote_plus(url))
self._server.query(key, method=self._server._session.post)
elif filepath:
key = '/library/metadata/%s/posters?' % self.ratingKey
2020-03-18 04:55:38 +00:00
data = open(filepath, 'rb').read()
self._server.query(key, method=self._server._session.post, data=data)
def setPoster(self, poster):
""" Set . :class:`~plexapi.media.Poster` to :class:`~plexapi.video.Video` """
poster.select()
2020-03-18 04:55:38 +00:00
def arts(self):
""" Returns list of available art objects. :class:`~plexapi.media.Poster`. """
return self.fetchItems('/library/metadata/%s/arts' % self.ratingKey)
def uploadArt(self, url=None, filepath=None):
""" Upload art from url or filepath. :class:`~plexapi.media.Poster` to :class:`~plexapi.video.Video`. """
if url:
key = '/library/metadata/%s/arts?url=%s' % (self.ratingKey, quote_plus(url))
self._server.query(key, method=self._server._session.post)
elif filepath:
key = '/library/metadata/%s/arts?' % self.ratingKey
data = open(filepath, 'rb').read()
self._server.query(key, method=self._server._session.post, data=data)
def setArt(self, art):
""" Set :class:`~plexapi.media.Poster` to :class:`~plexapi.video.Video` """
art.select()
2018-04-15 19:05:05 +00:00
# def edit(self, **kwargs):
# TODO
@utils.registerPlexObject
class Path(PlexObject):
""" Represents a single directory Path.
Attributes:
TAG (str): 'Path'
home (bool): True if the path is the home directory
key (str): API URL (/services/browse/<base64path>)
network (bool): True if path is a network location
2020-11-16 05:28:58 +00:00
path (str): Full path to folder
title (str): Folder name
"""
TAG = 'Path'
def _loadData(self, data):
self.home = utils.cast(bool, data.attrib.get('home'))
self.key = data.attrib.get('key')
self.network = utils.cast(bool, data.attrib.get('network'))
self.path = data.attrib.get('path')
self.title = data.attrib.get('title')
def browse(self, includeFiles=True):
2020-11-23 03:04:14 +00:00
""" Alias for :func:`~plexapi.server.PlexServer.browse`. """
return self._server.browse(self, includeFiles)
def walk(self):
2020-11-23 03:04:14 +00:00
""" Alias for :func:`~plexapi.server.PlexServer.walk`. """
for path, paths, files in self._server.walk(self):
yield path, paths, files
@utils.registerPlexObject
class File(PlexObject):
""" Represents a single File.
Attributes:
TAG (str): 'File'
key (str): API URL (/services/browse/<base64path>)
2020-11-16 05:28:58 +00:00
path (str): Full path to file
title (str): File name
"""
TAG = 'File'
def _loadData(self, data):
self.key = data.attrib.get('key')
self.path = data.attrib.get('path')
self.title = data.attrib.get('title')