mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-16 02:02:58 -07:00
Update plexapi to 4.3.0
This commit is contained in:
parent
6b013da697
commit
f497c11d73
18 changed files with 2365 additions and 1061 deletions
|
@ -1,47 +1,54 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from plexapi import media, utils
|
||||
from plexapi.exceptions import BadRequest, NotFound
|
||||
from plexapi.base import Playable, PlexPartialObject
|
||||
from plexapi.compat import quote_plus, urlencode
|
||||
import os
|
||||
from urllib.parse import quote_plus, urlencode
|
||||
|
||||
from plexapi import library, media, settings, utils
|
||||
from plexapi.base import Playable, PlexPartialObject
|
||||
from plexapi.exceptions import BadRequest, NotFound
|
||||
|
||||
|
||||
class Video(PlexPartialObject):
|
||||
""" Base class for all video objects including :class:`~plexapi.video.Movie`,
|
||||
:class:`~plexapi.video.Show`, :class:`~plexapi.video.Season`,
|
||||
:class:`~plexapi.video.Episode`.
|
||||
:class:`~plexapi.video.Episode`, and :class:`~plexapi.video.Clip`.
|
||||
|
||||
Attributes:
|
||||
addedAt (datetime): Datetime this item was added to the library.
|
||||
art (str): URL to artwork image.
|
||||
addedAt (datetime): Datetime the item was added to the library.
|
||||
art (str): URL to artwork image (/library/metadata/<ratingKey>/art/<artid>).
|
||||
artBlurHash (str): BlurHash string for artwork image.
|
||||
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
|
||||
guid (str): Plex GUID for the movie, show, season, episode, or clip (plex://movie/5d776b59ad5437001f79c6f8).
|
||||
key (str): API URL (/library/metadata/<ratingkey>).
|
||||
lastViewedAt (datetime): Datetime item was last accessed.
|
||||
lastViewedAt (datetime): Datetime the item was last played.
|
||||
librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID.
|
||||
listType (str): Hardcoded as 'audio' (useful for search filters).
|
||||
ratingKey (int): Unique key identifying this item.
|
||||
summary (str): Summary of the artist, track, or album.
|
||||
thumb (str): URL to thumbnail image.
|
||||
librarySectionKey (str): :class:`~plexapi.library.LibrarySection` key.
|
||||
librarySectionTitle (str): :class:`~plexapi.library.LibrarySection` title.
|
||||
listType (str): Hardcoded as 'video' (useful for search filters).
|
||||
ratingKey (int): Unique key identifying the item.
|
||||
summary (str): Summary of the movie, show, season, episode, or clip.
|
||||
thumb (str): URL to thumbnail image (/library/metadata/<ratingKey>/thumb/<thumbid>).
|
||||
thumbBlurHash (str): BlurHash string for thumbnail image.
|
||||
title (str): Artist, Album or Track title. (Jason Mraz, We Sing, Lucky, etc.)
|
||||
title (str): Name of the movie, show, season, episode, or clip.
|
||||
titleSort (str): Title to use when sorting (defaults to title).
|
||||
type (str): 'artist', 'album', or 'track'.
|
||||
updatedAt (datatime): Datetime this item was updated.
|
||||
viewCount (int): Count of times this item was accessed.
|
||||
type (str): 'movie', 'show', 'season', 'episode', or 'clip'.
|
||||
updatedAt (datatime): Datetime the item was updated.
|
||||
viewCount (int): Count of times the item was played.
|
||||
"""
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.listType = 'video'
|
||||
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
|
||||
self.art = data.attrib.get('art')
|
||||
self.artBlurHash = data.attrib.get('artBlurHash')
|
||||
self.fields = self.findItems(data, media.Field)
|
||||
self.guid = data.attrib.get('guid')
|
||||
self.key = data.attrib.get('key', '')
|
||||
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt'))
|
||||
self.librarySectionID = data.attrib.get('librarySectionID')
|
||||
self.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||
self.listType = 'video'
|
||||
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
|
||||
self.summary = data.attrib.get('summary')
|
||||
self.thumb = data.attrib.get('thumb')
|
||||
|
@ -133,8 +140,9 @@ class Video(PlexPartialObject):
|
|||
policyValue="", policyUnwatched=0, videoQuality=None, deviceProfile=None):
|
||||
""" Optimize item
|
||||
|
||||
locationID (int): -1 in folder with orginal items
|
||||
2 library path
|
||||
locationID (int): -1 in folder with original items
|
||||
2 library path id
|
||||
library path id is found in library.locations[i].id
|
||||
|
||||
target (str): custom quality name.
|
||||
if none provided use "Custom: {deviceProfile}"
|
||||
|
@ -164,6 +172,13 @@ class Video(PlexPartialObject):
|
|||
if targetTagID not in tagIDs and (deviceProfile is None or videoQuality is None):
|
||||
raise BadRequest('Unexpected or missing quality profile.')
|
||||
|
||||
libraryLocationIDs = [location.id for location in self.section()._locations()]
|
||||
libraryLocationIDs.append(-1)
|
||||
|
||||
if locationID not in libraryLocationIDs:
|
||||
raise BadRequest('Unexpected library path ID. %s not in %s' %
|
||||
(locationID, libraryLocationIDs))
|
||||
|
||||
if isinstance(targetTagID, str):
|
||||
tagIndex = tagKeys.index(targetTagID)
|
||||
targetTagID = tagValues[tagIndex]
|
||||
|
@ -250,35 +265,33 @@ class Movie(Playable, Video):
|
|||
Attributes:
|
||||
TAG (str): 'Video'
|
||||
TYPE (str): 'movie'
|
||||
art (str): Key to movie artwork (/library/metadata/<ratingkey>/art/<artid>)
|
||||
audienceRating (float): Audience rating (usually from Rotten Tomatoes).
|
||||
audienceRatingImage (str): Key to audience rating image (rottentomatoes://image.rating.spilled)
|
||||
audienceRatingImage (str): Key to audience rating image (rottentomatoes://image.rating.spilled).
|
||||
chapters (List<:class:`~plexapi.media.Chapter`>): List of Chapter objects.
|
||||
chapterSource (str): Chapter source (agent; media; mixed).
|
||||
collections (List<:class:`~plexapi.media.Collection`>): List of collection objects.
|
||||
contentRating (str) Content rating (PG-13; NR; TV-G).
|
||||
duration (int): Duration of movie in milliseconds.
|
||||
guid: Plex GUID (com.plexapp.agents.imdb://tt4302938?lang=en).
|
||||
countries (List<:class:`~plexapi.media.Country`>): List of countries objects.
|
||||
directors (List<:class:`~plexapi.media.Director`>): List of director objects.
|
||||
duration (int): Duration of the movie in milliseconds.
|
||||
genres (List<:class:`~plexapi.media.Genre`>): List of genre objects.
|
||||
guids (List<:class:`~plexapi.media.Guid`>): List of guid objects.
|
||||
labels (List<:class:`~plexapi.media.Label`>): List of label objects.
|
||||
media (List<:class:`~plexapi.media.Media`>): List of media objects.
|
||||
originallyAvailableAt (datetime): Datetime the movie was released.
|
||||
originalTitle (str): Original title, often the foreign title (転々; 엽기적인 그녀).
|
||||
originallyAvailableAt (datetime): Datetime movie was released.
|
||||
primaryExtraKey (str) Primary extra key (/library/metadata/66351).
|
||||
rating (float): Movie rating (7.9; 9.8; 8.1).
|
||||
ratingImage (str): Key to rating image (rottentomatoes://image.rating.rotten).
|
||||
producers (List<:class:`~plexapi.media.Producer`>): List of producers objects.
|
||||
rating (float): Movie critic rating (7.9; 9.8; 8.1).
|
||||
ratingImage (str): Key to critic rating image (rottentomatoes://image.rating.rotten).
|
||||
roles (List<:class:`~plexapi.media.Role`>): List of role objects.
|
||||
similar (List<:class:`~plexapi.media.Similar`>): List of Similar objects.
|
||||
studio (str): Studio that created movie (Di Bonaventura Pictures; 21 Laps Entertainment).
|
||||
tagline (str): Movie tag line (Back 2 Work; Who says men can't change?).
|
||||
userRating (float): User rating (2.0; 8.0).
|
||||
viewOffset (int): View offset in milliseconds.
|
||||
year (int): Year movie was released.
|
||||
collections (List<:class:`~plexapi.media.Collection`>): List of collections this media belongs.
|
||||
countries (List<:class:`~plexapi.media.Country`>): List of countries objects.
|
||||
directors (List<:class:`~plexapi.media.Director`>): List of director objects.
|
||||
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
|
||||
genres (List<:class:`~plexapi.media.Genre`>): List of genre objects.
|
||||
guids (List<:class:`~plexapi.media.Guid`>): List of guid objects.
|
||||
media (List<:class:`~plexapi.media.Media`>): List of media objects.
|
||||
producers (List<:class:`~plexapi.media.Producer`>): List of producers objects.
|
||||
roles (List<:class:`~plexapi.media.Role`>): List of role objects.
|
||||
writers (List<:class:`~plexapi.media.Writer`>): List of writers objects.
|
||||
chapters (List<:class:`~plexapi.media.Chapter`>): List of Chapter objects.
|
||||
similar (List<:class:`~plexapi.media.Similar`>): List of Similar objects.
|
||||
year (int): Year movie was released.
|
||||
"""
|
||||
TAG = 'Video'
|
||||
TYPE = 'movie'
|
||||
|
@ -288,38 +301,33 @@ class Movie(Playable, Video):
|
|||
""" Load attribute values from Plex XML response. """
|
||||
Video._loadData(self, data)
|
||||
Playable._loadData(self, data)
|
||||
|
||||
self.art = data.attrib.get('art')
|
||||
self.audienceRating = utils.cast(float, data.attrib.get('audienceRating'))
|
||||
self.audienceRatingImage = data.attrib.get('audienceRatingImage')
|
||||
self.chapters = self.findItems(data, media.Chapter)
|
||||
self.chapterSource = data.attrib.get('chapterSource')
|
||||
self.collections = self.findItems(data, media.Collection)
|
||||
self.contentRating = data.attrib.get('contentRating')
|
||||
self.countries = self.findItems(data, media.Country)
|
||||
self.directors = self.findItems(data, media.Director)
|
||||
self.duration = utils.cast(int, data.attrib.get('duration'))
|
||||
self.guid = data.attrib.get('guid')
|
||||
self.genres = self.findItems(data, media.Genre)
|
||||
self.guids = self.findItems(data, media.Guid)
|
||||
self.labels = self.findItems(data, media.Label)
|
||||
self.media = self.findItems(data, media.Media)
|
||||
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
|
||||
self.originalTitle = data.attrib.get('originalTitle')
|
||||
self.originallyAvailableAt = utils.toDatetime(
|
||||
data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
|
||||
self.primaryExtraKey = data.attrib.get('primaryExtraKey')
|
||||
self.producers = self.findItems(data, media.Producer)
|
||||
self.rating = utils.cast(float, data.attrib.get('rating'))
|
||||
self.ratingImage = data.attrib.get('ratingImage')
|
||||
self.roles = self.findItems(data, media.Role)
|
||||
self.similar = self.findItems(data, media.Similar)
|
||||
self.studio = data.attrib.get('studio')
|
||||
self.tagline = data.attrib.get('tagline')
|
||||
self.userRating = utils.cast(float, data.attrib.get('userRating'))
|
||||
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
||||
self.year = utils.cast(int, data.attrib.get('year'))
|
||||
self.collections = self.findItems(data, media.Collection)
|
||||
self.countries = self.findItems(data, media.Country)
|
||||
self.directors = self.findItems(data, media.Director)
|
||||
self.fields = self.findItems(data, media.Field)
|
||||
self.genres = self.findItems(data, media.Genre)
|
||||
self.guids = self.findItems(data, media.Guid)
|
||||
self.media = self.findItems(data, media.Media)
|
||||
self.producers = self.findItems(data, media.Producer)
|
||||
self.roles = self.findItems(data, media.Role)
|
||||
self.writers = self.findItems(data, media.Writer)
|
||||
self.labels = self.findItems(data, media.Label)
|
||||
self.chapters = self.findItems(data, media.Chapter)
|
||||
self.similar = self.findItems(data, media.Similar)
|
||||
self.year = utils.cast(int, data.attrib.get('year'))
|
||||
|
||||
@property
|
||||
def actors(self):
|
||||
|
@ -329,7 +337,10 @@ class Movie(Playable, Video):
|
|||
@property
|
||||
def locations(self):
|
||||
""" This does not exist in plex xml response but is added to have a common
|
||||
interface to get the location of the Movie
|
||||
interface to get the locations of the movie.
|
||||
|
||||
Retruns:
|
||||
List<str> of file paths where the movie is found on disk.
|
||||
"""
|
||||
return [part.file for part in self.iterParts() if part]
|
||||
|
||||
|
@ -337,6 +348,15 @@ class Movie(Playable, Video):
|
|||
# This is just for compat.
|
||||
return self.title
|
||||
|
||||
def hubs(self):
|
||||
""" Returns a list of :class:`~plexapi.library.Hub` objects. """
|
||||
data = self._server.query(self._details_key)
|
||||
video = data.find('Video')
|
||||
if video:
|
||||
related = video.find('Related')
|
||||
if related:
|
||||
return self.findItems(related, library.Hub)
|
||||
|
||||
def download(self, savepath=None, keep_original_name=False, **kwargs):
|
||||
""" Download video files to specified directory.
|
||||
|
||||
|
@ -371,61 +391,56 @@ class Show(Video):
|
|||
Attributes:
|
||||
TAG (str): 'Directory'
|
||||
TYPE (str): 'show'
|
||||
art (str): Key to show artwork (/library/metadata/<ratingkey>/art/<artid>)
|
||||
banner (str): Key to banner artwork (/library/metadata/<ratingkey>/art/<artid>)
|
||||
childCount (int): Unknown.
|
||||
banner (str): Key to banner artwork (/library/metadata/<ratingkey>/banner/<bannerid>).
|
||||
childCount (int): Number of seasons in the show.
|
||||
collections (List<:class:`~plexapi.media.Collection`>): List of collection objects.
|
||||
contentRating (str) Content rating (PG-13; NR; TV-G).
|
||||
collections (List<:class:`~plexapi.media.Collection`>): List of collections this media belongs.
|
||||
duration (int): Duration of show in milliseconds.
|
||||
guid (str): Plex GUID (com.plexapp.agents.imdb://tt4302938?lang=en).
|
||||
index (int): Plex index (?)
|
||||
leafCount (int): Unknown.
|
||||
locations (list<str>): List of locations paths.
|
||||
originallyAvailableAt (datetime): Datetime show was released.
|
||||
rating (float): Show rating (7.9; 9.8; 8.1).
|
||||
studio (str): Studio that created show (Di Bonaventura Pictures; 21 Laps Entertainment).
|
||||
theme (str): Key to theme resource (/library/metadata/<ratingkey>/theme/<themeid>)
|
||||
viewedLeafCount (int): Unknown.
|
||||
year (int): Year the show was released.
|
||||
duration (int): Typical duration of the show episodes in milliseconds.
|
||||
genres (List<:class:`~plexapi.media.Genre`>): List of genre objects.
|
||||
index (int): Plex index number for the show.
|
||||
key (str): API URL (/library/metadata/<ratingkey>).
|
||||
labels (List<:class:`~plexapi.media.Label`>): List of label objects.
|
||||
leafCount (int): Number of items in the show view.
|
||||
locations (List<str>): List of folder paths where the show is found on disk.
|
||||
originallyAvailableAt (datetime): Datetime the show was released.
|
||||
rating (float): Show rating (7.9; 9.8; 8.1).
|
||||
roles (List<:class:`~plexapi.media.Role`>): List of role objects.
|
||||
similar (List<:class:`~plexapi.media.Similar`>): List of Similar objects.
|
||||
studio (str): Studio that created show (Di Bonaventura Pictures; 21 Laps Entertainment).
|
||||
theme (str): URL to theme resource (/library/metadata/<ratingkey>/theme/<themeid>).
|
||||
viewedLeafCount (int): Number of items marked as played in the show view.
|
||||
year (int): Year the show was released.
|
||||
"""
|
||||
TAG = 'Directory'
|
||||
TYPE = 'show'
|
||||
METADATA_TYPE = 'episode'
|
||||
|
||||
def __iter__(self):
|
||||
for season in self.seasons():
|
||||
yield season
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
Video._loadData(self, data)
|
||||
# fix key if loaded from search
|
||||
self.key = self.key.replace('/children', '') # FIX_BUG_50
|
||||
self.art = data.attrib.get('art')
|
||||
self.banner = data.attrib.get('banner')
|
||||
self.childCount = utils.cast(int, data.attrib.get('childCount'))
|
||||
self.contentRating = data.attrib.get('contentRating')
|
||||
self.collections = self.findItems(data, media.Collection)
|
||||
self.contentRating = data.attrib.get('contentRating')
|
||||
self.duration = utils.cast(int, data.attrib.get('duration'))
|
||||
self.guid = data.attrib.get('guid')
|
||||
self.index = data.attrib.get('index')
|
||||
self.genres = self.findItems(data, media.Genre)
|
||||
self.index = utils.cast(int, data.attrib.get('index'))
|
||||
self.key = self.key.replace('/children', '') # FIX_BUG_50
|
||||
self.labels = self.findItems(data, media.Label)
|
||||
self.leafCount = utils.cast(int, data.attrib.get('leafCount'))
|
||||
self.locations = self.listAttrs(data, 'path', etag='Location')
|
||||
self.originallyAvailableAt = utils.toDatetime(
|
||||
data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
|
||||
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
|
||||
self.rating = utils.cast(float, data.attrib.get('rating'))
|
||||
self.roles = self.findItems(data, media.Role)
|
||||
self.similar = self.findItems(data, media.Similar)
|
||||
self.studio = data.attrib.get('studio')
|
||||
self.theme = data.attrib.get('theme')
|
||||
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount'))
|
||||
self.year = utils.cast(int, data.attrib.get('year'))
|
||||
self.fields = self.findItems(data, media.Field)
|
||||
self.genres = self.findItems(data, media.Genre)
|
||||
self.roles = self.findItems(data, media.Role)
|
||||
self.labels = self.findItems(data, media.Label)
|
||||
self.similar = self.findItems(data, media.Similar)
|
||||
|
||||
def __iter__(self):
|
||||
for season in self.seasons():
|
||||
yield season
|
||||
|
||||
@property
|
||||
def actors(self):
|
||||
|
@ -434,52 +449,116 @@ class Show(Video):
|
|||
|
||||
@property
|
||||
def isWatched(self):
|
||||
""" Returns True if this show is fully watched. """
|
||||
""" Returns True if the show is fully watched. """
|
||||
return bool(self.viewedLeafCount == self.leafCount)
|
||||
|
||||
def seasons(self, **kwargs):
|
||||
""" Returns a list of :class:`~plexapi.video.Season` objects. """
|
||||
key = '/library/metadata/%s/children?excludeAllLeaves=1' % self.ratingKey
|
||||
return self.fetchItems(key, **kwargs)
|
||||
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('Preferences'):
|
||||
for elem in item:
|
||||
setting = settings.Preferences(data=elem, server=self._server)
|
||||
setting._initpath = self.key
|
||||
items.append(setting)
|
||||
|
||||
def season(self, title=None):
|
||||
return items
|
||||
|
||||
def editAdvanced(self, **kwargs):
|
||||
""" Edit a show's advanced settings. """
|
||||
data = {}
|
||||
key = '%s/prefs?' % self.key
|
||||
preferences = {pref.id: list(pref.enumValues.keys()) for pref in self.preferences()}
|
||||
for settingID, value in kwargs.items():
|
||||
enumValues = preferences.get(settingID)
|
||||
if value in enumValues:
|
||||
data[settingID] = value
|
||||
else:
|
||||
raise NotFound('%s not found in %s' % (value, enumValues))
|
||||
url = key + urlencode(data)
|
||||
self._server.query(url, method=self._server._session.put)
|
||||
|
||||
def defaultAdvanced(self):
|
||||
""" Edit all of show's advanced settings to default. """
|
||||
data = {}
|
||||
key = '%s/prefs?' % self.key
|
||||
for preference in self.preferences():
|
||||
data[preference.id] = preference.default
|
||||
url = key + urlencode(data)
|
||||
self._server.query(url, method=self._server._session.put)
|
||||
|
||||
def hubs(self):
|
||||
""" Returns a list of :class:`~plexapi.library.Hub` objects. """
|
||||
data = self._server.query(self._details_key)
|
||||
directory = data.find('Directory')
|
||||
if directory:
|
||||
related = directory.find('Related')
|
||||
if related:
|
||||
return self.findItems(related, library.Hub)
|
||||
|
||||
def onDeck(self):
|
||||
""" Returns show's On Deck :class:`~plexapi.video.Video` object or `None`.
|
||||
If show is unwatched, return will likely be the first episode.
|
||||
"""
|
||||
data = self._server.query(self._details_key)
|
||||
episode = next(data.iter('OnDeck'), None)
|
||||
if episode:
|
||||
return self.findItems(episode)[0]
|
||||
return None
|
||||
|
||||
def season(self, title=None, season=None):
|
||||
""" Returns the season with the specified title or number.
|
||||
|
||||
Parameters:
|
||||
title (str or int): Title or Number of the season to return.
|
||||
title (str): Title of the season to return.
|
||||
season (int): Season number (default: None; required if title not specified).
|
||||
|
||||
Raises:
|
||||
:exc:`~plexapi.exceptions.BadRequest`: If title or season parameter is missing.
|
||||
"""
|
||||
key = '/library/metadata/%s/children' % self.ratingKey
|
||||
if isinstance(title, int):
|
||||
return self.fetchItem(key, etag='Directory', index__iexact=str(title))
|
||||
return self.fetchItem(key, etag='Directory', title__iexact=title)
|
||||
if title is not None and not isinstance(title, int):
|
||||
return self.fetchItem(key, Season, title__iexact=title)
|
||||
elif season is not None or isinstance(title, int):
|
||||
if isinstance(title, int):
|
||||
index = title
|
||||
else:
|
||||
index = season
|
||||
return self.fetchItem(key, Season, index=index)
|
||||
raise BadRequest('Missing argument: title or season is required')
|
||||
|
||||
def episodes(self, **kwargs):
|
||||
""" Returns a list of :class:`~plexapi.video.Episode` objects. """
|
||||
key = '/library/metadata/%s/allLeaves' % self.ratingKey
|
||||
return self.fetchItems(key, **kwargs)
|
||||
def seasons(self, **kwargs):
|
||||
""" Returns a list of :class:`~plexapi.video.Season` objects in the show. """
|
||||
key = '/library/metadata/%s/children?excludeAllLeaves=1' % self.ratingKey
|
||||
return self.fetchItems(key, Season, **kwargs)
|
||||
|
||||
def episode(self, title=None, season=None, episode=None):
|
||||
""" Find a episode using a title or season and episode.
|
||||
|
||||
Parameters:
|
||||
title (str): Title of the episode to return
|
||||
season (int): Season number (default:None; required if title not specified).
|
||||
episode (int): Episode number (default:None; required if title not specified).
|
||||
season (int): Season number (default: None; required if title not specified).
|
||||
episode (int): Episode number (default: None; required if title not specified).
|
||||
|
||||
Raises:
|
||||
:exc:`plexapi.exceptions.BadRequest`: If season and episode is missing.
|
||||
:exc:`plexapi.exceptions.NotFound`: If the episode is missing.
|
||||
:exc:`~plexapi.exceptions.BadRequest`: If title or season and episode parameters are missing.
|
||||
"""
|
||||
if title:
|
||||
key = '/library/metadata/%s/allLeaves' % self.ratingKey
|
||||
return self.fetchItem(key, title__iexact=title)
|
||||
elif season is not None and episode:
|
||||
results = [i for i in self.episodes() if i.seasonNumber == season and i.index == episode]
|
||||
if results:
|
||||
return results[0]
|
||||
raise NotFound('Couldnt find %s S%s E%s' % (self.title, season, episode))
|
||||
key = '/library/metadata/%s/allLeaves' % self.ratingKey
|
||||
if title is not None:
|
||||
return self.fetchItem(key, Episode, title__iexact=title)
|
||||
elif season is not None and episode is not None:
|
||||
return self.fetchItem(key, Episode, parentIndex=season, index=episode)
|
||||
raise BadRequest('Missing argument: title or season and episode are required')
|
||||
|
||||
def episodes(self, **kwargs):
|
||||
""" Returns a list of :class:`~plexapi.video.Episode` objects in the show. """
|
||||
key = '/library/metadata/%s/allLeaves' % self.ratingKey
|
||||
return self.fetchItems(key, Episode, **kwargs)
|
||||
|
||||
def get(self, title=None, season=None, episode=None):
|
||||
""" Alias to :func:`~plexapi.video.Show.episode`. """
|
||||
return self.episode(title, season, episode)
|
||||
|
||||
def watched(self):
|
||||
""" Returns list of watched :class:`~plexapi.video.Episode` objects. """
|
||||
return self.episodes(viewCount__gt=0)
|
||||
|
@ -488,10 +567,6 @@ class Show(Video):
|
|||
""" Returns list of unwatched :class:`~plexapi.video.Episode` objects. """
|
||||
return self.episodes(viewCount=0)
|
||||
|
||||
def get(self, title=None, season=None, episode=None):
|
||||
""" Alias to :func:`~plexapi.video.Show.episode`. """
|
||||
return self.episode(title, season, episode)
|
||||
|
||||
def download(self, savepath=None, keep_original_name=False, **kwargs):
|
||||
""" Download video files to specified directory.
|
||||
|
||||
|
@ -514,31 +589,28 @@ class Season(Video):
|
|||
Attributes:
|
||||
TAG (str): 'Directory'
|
||||
TYPE (str): 'season'
|
||||
leafCount (int): Number of episodes in season.
|
||||
index (int): Season number.
|
||||
parentKey (str): Key to this seasons :class:`~plexapi.video.Show`.
|
||||
parentRatingKey (int): Unique key for this seasons :class:`~plexapi.video.Show`.
|
||||
parentTitle (str): Title of this seasons :class:`~plexapi.video.Show`.
|
||||
viewedLeafCount (int): Number of watched episodes in season.
|
||||
key (str): API URL (/library/metadata/<ratingkey>).
|
||||
leafCount (int): Number of items in the season view.
|
||||
parentGuid (str): Plex GUID for the show (plex://show/5d9c086fe9d5a1001f4d9fe6).
|
||||
parentIndex (int): Plex index number for the show.
|
||||
parentKey (str): API URL of the show (/library/metadata/<parentRatingKey>).
|
||||
parentRatingKey (int): Unique key identifying the show.
|
||||
parentTheme (str): URL to show theme resource (/library/metadata/<parentRatingkey>/theme/<themeid>).
|
||||
parentThumb (str): URL to show thumbnail image (/library/metadata/<parentRatingKey>/thumb/<thumbid>).
|
||||
parentTitle (str): Name of the show for the season.
|
||||
viewedLeafCount (int): Number of items marked as played in the season view.
|
||||
"""
|
||||
TAG = 'Directory'
|
||||
TYPE = 'season'
|
||||
METADATA_TYPE = 'episode'
|
||||
|
||||
def __iter__(self):
|
||||
for episode in self.episodes():
|
||||
yield episode
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
Video._loadData(self, data)
|
||||
# fix key if loaded from search
|
||||
self.key = self.key.replace('/children', '')
|
||||
art = data.attrib.get('art')
|
||||
self.art = art if art and str(self.ratingKey) in art else None
|
||||
self.guid = data.attrib.get('guid')
|
||||
self.leafCount = utils.cast(int, data.attrib.get('leafCount'))
|
||||
self.index = utils.cast(int, data.attrib.get('index'))
|
||||
self.key = self.key.replace('/children', '') # FIX_BUG_50
|
||||
self.leafCount = utils.cast(int, data.attrib.get('leafCount'))
|
||||
self.parentGuid = data.attrib.get('parentGuid')
|
||||
self.parentIndex = data.attrib.get('parentIndex')
|
||||
self.parentKey = data.attrib.get('parentKey')
|
||||
|
@ -547,7 +619,10 @@ class Season(Video):
|
|||
self.parentThumb = data.attrib.get('parentThumb')
|
||||
self.parentTitle = data.attrib.get('parentTitle')
|
||||
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount'))
|
||||
self.fields = self.findItems(data, media.Field)
|
||||
|
||||
def __iter__(self):
|
||||
for episode in self.episodes():
|
||||
yield episode
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s>' % ':'.join([p for p in [
|
||||
|
@ -558,7 +633,7 @@ class Season(Video):
|
|||
|
||||
@property
|
||||
def isWatched(self):
|
||||
""" Returns True if this season is fully watched. """
|
||||
""" Returns True if the season is fully watched. """
|
||||
return bool(self.viewedLeafCount == self.leafCount)
|
||||
|
||||
@property
|
||||
|
@ -567,31 +642,44 @@ class Season(Video):
|
|||
return self.index
|
||||
|
||||
def episodes(self, **kwargs):
|
||||
""" Returns a list of :class:`~plexapi.video.Episode` objects. """
|
||||
""" Returns a list of :class:`~plexapi.video.Episode` objects in the season. """
|
||||
key = '/library/metadata/%s/children' % self.ratingKey
|
||||
return self.fetchItems(key, **kwargs)
|
||||
return self.fetchItems(key, Episode, **kwargs)
|
||||
|
||||
def episode(self, title=None, episode=None):
|
||||
""" Returns the episode with the given title or number.
|
||||
|
||||
Parameters:
|
||||
title (str): Title of the episode to return.
|
||||
episode (int): Episode number (default:None; required if title not specified).
|
||||
episode (int): Episode number (default: None; required if title not specified).
|
||||
|
||||
Raises:
|
||||
:exc:`~plexapi.exceptions.BadRequest`: If title or episode parameter is missing.
|
||||
"""
|
||||
if not title and not episode:
|
||||
raise BadRequest('Missing argument, you need to use title or episode.')
|
||||
key = '/library/metadata/%s/children' % self.ratingKey
|
||||
if title:
|
||||
return self.fetchItem(key, title=title)
|
||||
return self.fetchItem(key, parentIndex=self.index, index=episode)
|
||||
if title is not None:
|
||||
return self.fetchItem(key, Episode, title__iexact=title)
|
||||
elif episode is not None:
|
||||
return self.fetchItem(key, Episode, parentIndex=self.index, index=episode)
|
||||
raise BadRequest('Missing argument: title or episode is required')
|
||||
|
||||
def get(self, title=None, episode=None):
|
||||
""" Alias to :func:`~plexapi.video.Season.episode`. """
|
||||
return self.episode(title, episode)
|
||||
|
||||
def onDeck(self):
|
||||
""" Returns season's On Deck :class:`~plexapi.video.Video` object or `None`.
|
||||
Will only return a match if the show's On Deck episode is in this season.
|
||||
"""
|
||||
data = self._server.query(self._details_key)
|
||||
episode = next(data.iter('OnDeck'), None)
|
||||
if episode:
|
||||
return self.findItems(episode)[0]
|
||||
return None
|
||||
|
||||
def show(self):
|
||||
""" Return this seasons :func:`~plexapi.video.Show`.. """
|
||||
return self.fetchItem(int(self.parentRatingKey))
|
||||
""" Return the season's :class:`~plexapi.video.Show`. """
|
||||
return self.fetchItem(self.parentRatingKey)
|
||||
|
||||
def watched(self):
|
||||
""" Returns list of watched :class:`~plexapi.video.Episode` objects. """
|
||||
|
@ -627,31 +715,32 @@ class Episode(Playable, Video):
|
|||
Attributes:
|
||||
TAG (str): 'Video'
|
||||
TYPE (str): 'episode'
|
||||
art (str): Key to episode artwork (/library/metadata/<ratingkey>/art/<artid>)
|
||||
chapterSource (str): Unknown (media).
|
||||
chapters (List<:class:`~plexapi.media.Chapter`>): List of Chapter objects.
|
||||
chapterSource (str): Chapter source (agent; media; mixed).
|
||||
contentRating (str) Content rating (PG-13; NR; TV-G).
|
||||
duration (int): Duration of episode in milliseconds.
|
||||
grandparentArt (str): Key to this episodes :class:`~plexapi.video.Show` artwork.
|
||||
grandparentKey (str): Key to this episodes :class:`~plexapi.video.Show`.
|
||||
grandparentRatingKey (str): Unique key for this episodes :class:`~plexapi.video.Show`.
|
||||
grandparentTheme (str): Key to this episodes :class:`~plexapi.video.Show` theme.
|
||||
grandparentThumb (str): Key to this episodes :class:`~plexapi.video.Show` thumb.
|
||||
grandparentTitle (str): Title of this episodes :class:`~plexapi.video.Show`.
|
||||
guid (str): Plex GUID (com.plexapp.agents.imdb://tt4302938?lang=en).
|
||||
index (int): Episode number.
|
||||
originallyAvailableAt (datetime): Datetime episode was released.
|
||||
parentIndex (str): Season number of episode.
|
||||
parentKey (str): Key to this episodes :class:`~plexapi.video.Season`.
|
||||
parentRatingKey (int): Unique key for this episodes :class:`~plexapi.video.Season`.
|
||||
parentThumb (str): Key to this episodes thumbnail.
|
||||
parentTitle (str): Name of this episode's season
|
||||
title (str): Name of this Episode
|
||||
rating (float): Movie rating (7.9; 9.8; 8.1).
|
||||
viewOffset (int): View offset in milliseconds.
|
||||
year (int): Year episode was released.
|
||||
directors (List<:class:`~plexapi.media.Director`>): List of director objects.
|
||||
duration (int): Duration of the episode in milliseconds.
|
||||
grandparentArt (str): URL to show artwork (/library/metadata/<grandparentRatingKey>/art/<artid>).
|
||||
grandparentGuid (str): Plex GUID for the show (plex://show/5d9c086fe9d5a1001f4d9fe6).
|
||||
grandparentKey (str): API URL of the show (/library/metadata/<grandparentRatingKey>).
|
||||
grandparentRatingKey (int): Unique key identifying the show.
|
||||
grandparentTheme (str): URL to show theme resource (/library/metadata/<grandparentRatingkey>/theme/<themeid>).
|
||||
grandparentThumb (str): URL to show thumbnail image (/library/metadata/<grandparentRatingKey>/thumb/<thumbid>).
|
||||
grandparentTitle (str): Name of the show for the episode.
|
||||
index (int): Episode number.
|
||||
markers (List<:class:`~plexapi.media.Marker`>): List of marker objects.
|
||||
media (List<:class:`~plexapi.media.Media`>): List of media objects.
|
||||
originallyAvailableAt (datetime): Datetime the episode was released.
|
||||
parentGuid (str): Plex GUID for the season (plex://season/5d9c09e42df347001e3c2a72).
|
||||
parentIndex (int): Season number of episode.
|
||||
parentKey (str): API URL of the season (/library/metadata/<parentRatingKey>).
|
||||
parentRatingKey (int): Unique key identifying the season.
|
||||
parentThumb (str): URL to season thumbnail image (/library/metadata/<parentRatingKey>/thumb/<thumbid>).
|
||||
parentTitle (str): Name of the season for the episode.
|
||||
rating (float): Episode rating (7.9; 9.8; 8.1).
|
||||
viewOffset (int): View offset in milliseconds.
|
||||
writers (List<:class:`~plexapi.media.Writer`>): List of writers objects.
|
||||
year (int): Year episode was released.
|
||||
"""
|
||||
TAG = 'Video'
|
||||
TYPE = 'episode'
|
||||
|
@ -662,10 +751,10 @@ class Episode(Playable, Video):
|
|||
Video._loadData(self, data)
|
||||
Playable._loadData(self, data)
|
||||
self._seasonNumber = None # cached season number
|
||||
art = data.attrib.get('art')
|
||||
self.art = art if art and str(self.ratingKey) in art else None
|
||||
self.chapters = self.findItems(data, media.Chapter)
|
||||
self.chapterSource = data.attrib.get('chapterSource')
|
||||
self.contentRating = data.attrib.get('contentRating')
|
||||
self.directors = self.findItems(data, media.Director)
|
||||
self.duration = utils.cast(int, data.attrib.get('duration'))
|
||||
self.grandparentArt = data.attrib.get('grandparentArt')
|
||||
self.grandparentGuid = data.attrib.get('grandparentGuid')
|
||||
|
@ -674,27 +763,20 @@ class Episode(Playable, Video):
|
|||
self.grandparentTheme = data.attrib.get('grandparentTheme')
|
||||
self.grandparentThumb = data.attrib.get('grandparentThumb')
|
||||
self.grandparentTitle = data.attrib.get('grandparentTitle')
|
||||
self.guid = data.attrib.get('guid')
|
||||
self.index = utils.cast(int, data.attrib.get('index'))
|
||||
self.markers = self.findItems(data, media.Marker)
|
||||
self.media = self.findItems(data, media.Media)
|
||||
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
|
||||
self.parentGuid = data.attrib.get('parentGuid')
|
||||
self.parentIndex = data.attrib.get('parentIndex')
|
||||
self.parentIndex = utils.cast(int, data.attrib.get('parentIndex'))
|
||||
self.parentKey = data.attrib.get('parentKey')
|
||||
self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey'))
|
||||
self.parentThumb = data.attrib.get('parentThumb')
|
||||
self.parentTitle = data.attrib.get('parentTitle')
|
||||
self.title = data.attrib.get('title')
|
||||
self.rating = utils.cast(float, data.attrib.get('rating'))
|
||||
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
||||
self.year = utils.cast(int, data.attrib.get('year'))
|
||||
self.directors = self.findItems(data, media.Director)
|
||||
self.fields = self.findItems(data, media.Field)
|
||||
self.media = self.findItems(data, media.Media)
|
||||
self.writers = self.findItems(data, media.Writer)
|
||||
self.labels = self.findItems(data, media.Label)
|
||||
self.collections = self.findItems(data, media.Collection)
|
||||
self.chapters = self.findItems(data, media.Chapter)
|
||||
self.markers = self.findItems(data, media.Marker)
|
||||
self.year = utils.cast(int, data.attrib.get('year'))
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s>' % ':'.join([p for p in [
|
||||
|
@ -710,13 +792,16 @@ class Episode(Playable, Video):
|
|||
@property
|
||||
def locations(self):
|
||||
""" This does not exist in plex xml response but is added to have a common
|
||||
interface to get the location of the Episode
|
||||
interface to get the locations of the episode.
|
||||
|
||||
Retruns:
|
||||
List<str> of file paths where the episode is found on disk.
|
||||
"""
|
||||
return [part.file for part in self.iterParts() if part]
|
||||
|
||||
@property
|
||||
def seasonNumber(self):
|
||||
""" Returns this episodes season number. """
|
||||
""" Returns the episodes season number. """
|
||||
if self._seasonNumber is None:
|
||||
self._seasonNumber = self.parentIndex if self.parentIndex else self.season().seasonNumber
|
||||
return utils.cast(int, self._seasonNumber)
|
||||
|
@ -728,18 +813,18 @@ class Episode(Playable, Video):
|
|||
|
||||
@property
|
||||
def hasIntroMarker(self):
|
||||
""" Returns True if this episode has an intro marker in the xml. """
|
||||
""" Returns True if the episode has an intro marker in the xml. """
|
||||
if not self.isFullObject():
|
||||
self.reload()
|
||||
return any(marker.type == 'intro' for marker in self.markers)
|
||||
|
||||
def season(self):
|
||||
"""" Return this episodes :func:`~plexapi.video.Season`.. """
|
||||
"""" Return the episode's :class:`~plexapi.video.Season`. """
|
||||
return self.fetchItem(self.parentKey)
|
||||
|
||||
def show(self):
|
||||
"""" Return this episodes :func:`~plexapi.video.Show`.. """
|
||||
return self.fetchItem(int(self.grandparentRatingKey))
|
||||
"""" Return the episode's :class:`~plexapi.video.Show`. """
|
||||
return self.fetchItem(self.grandparentRatingKey)
|
||||
|
||||
def _defaultSyncTitle(self):
|
||||
""" Returns str, default title for a new syncItem. """
|
||||
|
@ -748,35 +833,49 @@ class Episode(Playable, Video):
|
|||
|
||||
@utils.registerPlexObject
|
||||
class Clip(Playable, Video):
|
||||
""" Represents a single Clip."""
|
||||
"""Represents a single Clip.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Video'
|
||||
TYPE (str): 'clip'
|
||||
duration (int): Duration of the clip in milliseconds.
|
||||
extraType (int): Unknown.
|
||||
index (int): Plex index number for the clip.
|
||||
media (List<:class:`~plexapi.media.Media`>): List of media objects.
|
||||
originallyAvailableAt (datetime): Datetime the clip was released.
|
||||
skipDetails (int): Unknown.
|
||||
subtype (str): Type of clip (trailer, behindTheScenes, sceneOrSample, etc.).
|
||||
thumbAspectRatio (str): Aspect ratio of the thumbnail image.
|
||||
viewOffset (int): View offset in milliseconds.
|
||||
year (int): Year clip was released.
|
||||
"""
|
||||
|
||||
TAG = 'Video'
|
||||
TYPE = 'clip'
|
||||
METADATA_TYPE = 'clip'
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
"""Load attribute values from Plex XML response."""
|
||||
Video._loadData(self, data)
|
||||
Playable._loadData(self, data)
|
||||
self._data = data
|
||||
self.addedAt = data.attrib.get('addedAt')
|
||||
self.duration = utils.cast(int, data.attrib.get('duration'))
|
||||
self.guid = data.attrib.get('guid')
|
||||
self.key = data.attrib.get('key')
|
||||
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
|
||||
self.ratingKey = data.attrib.get('ratingKey')
|
||||
self.extraType = utils.cast(int, data.attrib.get('extraType'))
|
||||
self.index = utils.cast(int, data.attrib.get('index'))
|
||||
self.media = self.findItems(data, media.Media)
|
||||
self.originallyAvailableAt = data.attrib.get('originallyAvailableAt')
|
||||
self.skipDetails = utils.cast(int, data.attrib.get('skipDetails'))
|
||||
self.subtype = data.attrib.get('subtype')
|
||||
self.thumb = data.attrib.get('thumb')
|
||||
self.thumbAspectRatio = data.attrib.get('thumbAspectRatio')
|
||||
self.title = data.attrib.get('title')
|
||||
self.type = data.attrib.get('type')
|
||||
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
||||
self.year = utils.cast(int, data.attrib.get('year'))
|
||||
self.media = self.findItems(data, media.Media)
|
||||
|
||||
@property
|
||||
def locations(self):
|
||||
""" This does not exist in plex xml response but is added to have a common
|
||||
interface to get the location of the Clip
|
||||
interface to get the locations of the clip.
|
||||
|
||||
Retruns:
|
||||
List<str> of file paths where the clip is found on disk.
|
||||
"""
|
||||
return [part.file for part in self.iterParts() if part]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue