2016-03-21 04:26:02 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
2016-03-17 04:51:20 +00:00
|
|
|
from plexapi import media, utils
|
2017-02-07 06:20:49 +00:00
|
|
|
from plexapi.exceptions import BadRequest, NotFound
|
2017-02-04 19:46:51 +00:00
|
|
|
from plexapi.base import Playable, PlexPartialObject
|
2017-01-02 21:06:40 +00:00
|
|
|
|
2014-12-29 03:21:58 +00:00
|
|
|
|
2016-04-04 03:55:29 +00:00
|
|
|
class Video(PlexPartialObject):
|
2017-02-09 20:01:23 +00:00
|
|
|
""" Base class for all video objects including :class:`~plexapi.video.Movie`,
|
|
|
|
:class:`~plexapi.video.Show`, :class:`~plexapi.video.Season`,
|
|
|
|
:class:`~plexapi.video.Episode`.
|
|
|
|
|
|
|
|
Attributes:
|
|
|
|
addedAt (datetime): Datetime this item was added to the library.
|
|
|
|
key (str): API URL (/library/metadata/<ratingkey>).
|
|
|
|
lastViewedAt (datetime): Datetime item was last accessed.
|
|
|
|
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.
|
|
|
|
title (str): Artist, Album or Track title. (Jason Mraz, We Sing, Lucky, etc.)
|
|
|
|
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.
|
|
|
|
"""
|
2014-12-29 03:21:58 +00:00
|
|
|
def _loadData(self, data):
|
2017-02-13 19:38:40 +00:00
|
|
|
""" Load attribute values from Plex XML response. """
|
2017-02-06 04:52:10 +00:00
|
|
|
self._data = data
|
2016-04-12 02:43:21 +00:00
|
|
|
self.listType = 'video'
|
2017-02-04 17:43:50 +00:00
|
|
|
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
|
|
|
|
self.key = data.attrib.get('key')
|
|
|
|
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt'))
|
|
|
|
self.librarySectionID = data.attrib.get('librarySectionID')
|
|
|
|
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
|
|
|
|
self.summary = data.attrib.get('summary')
|
|
|
|
self.thumb = data.attrib.get('thumb')
|
|
|
|
self.title = data.attrib.get('title')
|
2016-03-21 04:26:02 +00:00
|
|
|
self.titleSort = data.attrib.get('titleSort', self.title)
|
2017-02-04 17:43:50 +00:00
|
|
|
self.type = data.attrib.get('type')
|
|
|
|
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
|
2016-03-21 04:26:02 +00:00
|
|
|
self.viewCount = utils.cast(int, data.attrib.get('viewCount', 0))
|
2014-12-29 03:21:58 +00:00
|
|
|
|
2017-02-13 19:38:40 +00:00
|
|
|
@property
|
|
|
|
def isWatched(self):
|
|
|
|
""" Returns True if this video is watched. """
|
|
|
|
return bool(self.viewCount > 0)
|
|
|
|
|
2016-04-04 03:55:29 +00:00
|
|
|
@property
|
|
|
|
def thumbUrl(self):
|
2017-02-09 20:01:23 +00:00
|
|
|
""" Return url to for the thumbnail image. """
|
2017-01-02 21:19:07 +00:00
|
|
|
if self.thumb:
|
2017-02-09 04:29:17 +00:00
|
|
|
return self._server.url(self.thumb)
|
2016-04-04 03:55:29 +00:00
|
|
|
|
2014-12-29 03:21:58 +00:00
|
|
|
def markWatched(self):
|
2017-02-09 20:01:23 +00:00
|
|
|
""" Mark video as watched. """
|
2017-02-07 06:20:49 +00:00
|
|
|
key = '/:/scrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey
|
2017-02-09 04:29:17 +00:00
|
|
|
self._server.query(key)
|
2014-12-29 03:21:58 +00:00
|
|
|
self.reload()
|
|
|
|
|
|
|
|
def markUnwatched(self):
|
2017-02-09 20:01:23 +00:00
|
|
|
""" Mark video unwatched. """
|
2017-02-07 06:20:49 +00:00
|
|
|
key = '/:/unscrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey
|
2017-02-09 04:29:17 +00:00
|
|
|
self._server.query(key)
|
2014-12-29 03:21:58 +00:00
|
|
|
self.reload()
|
|
|
|
|
|
|
|
|
2017-02-13 02:55:55 +00:00
|
|
|
@utils.registerPlexObject
|
2016-04-04 03:55:29 +00:00
|
|
|
class Movie(Video, Playable):
|
2017-02-09 20:01:23 +00:00
|
|
|
""" Represents a single Movie.
|
|
|
|
|
|
|
|
Attributes:
|
2017-02-13 19:38:40 +00:00
|
|
|
TAG (str): 'Diectory'
|
|
|
|
TYPE (str): 'movie'
|
2017-02-09 20:01:23 +00:00
|
|
|
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)
|
|
|
|
chapterSource (str): Chapter source (agent; media; mixed).
|
|
|
|
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).
|
|
|
|
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).
|
|
|
|
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.
|
2017-02-13 19:38:40 +00:00
|
|
|
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.
|
|
|
|
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.
|
2017-02-09 20:01:23 +00:00
|
|
|
"""
|
2017-02-13 02:55:55 +00:00
|
|
|
TAG = 'Video'
|
2014-12-29 03:21:58 +00:00
|
|
|
TYPE = 'movie'
|
|
|
|
|
|
|
|
def _loadData(self, data):
|
2017-02-13 19:38:40 +00:00
|
|
|
""" Load attribute values from Plex XML response. """
|
2016-04-07 05:39:04 +00:00
|
|
|
Video._loadData(self, data)
|
|
|
|
Playable._loadData(self, data)
|
2017-02-04 17:43:50 +00:00
|
|
|
self.art = data.attrib.get('art')
|
2017-02-07 06:20:49 +00:00
|
|
|
self.audienceRating = utils.cast(float, data.attrib.get('audienceRating'))
|
2017-02-04 17:43:50 +00:00
|
|
|
self.audienceRatingImage = data.attrib.get('audienceRatingImage')
|
|
|
|
self.chapterSource = data.attrib.get('chapterSource')
|
|
|
|
self.contentRating = data.attrib.get('contentRating')
|
|
|
|
self.duration = utils.cast(int, data.attrib.get('duration'))
|
|
|
|
self.guid = data.attrib.get('guid')
|
|
|
|
self.originalTitle = data.attrib.get('originalTitle')
|
2016-12-16 23:38:08 +00:00
|
|
|
self.originallyAvailableAt = utils.toDatetime(
|
2017-02-04 17:43:50 +00:00
|
|
|
data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
|
|
|
|
self.primaryExtraKey = data.attrib.get('primaryExtraKey')
|
|
|
|
self.rating = data.attrib.get('rating')
|
|
|
|
self.ratingImage = data.attrib.get('ratingImage')
|
|
|
|
self.studio = data.attrib.get('studio')
|
|
|
|
self.tagline = data.attrib.get('tagline')
|
|
|
|
self.userRating = utils.cast(float, data.attrib.get('userRating'))
|
2016-03-21 04:26:02 +00:00
|
|
|
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
2017-02-04 17:43:50 +00:00
|
|
|
self.year = utils.cast(int, data.attrib.get('year'))
|
2017-02-13 02:55:55 +00:00
|
|
|
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.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)
|
2016-12-16 23:38:08 +00:00
|
|
|
|
2016-03-21 04:26:02 +00:00
|
|
|
@property
|
|
|
|
def actors(self):
|
2017-02-13 19:38:40 +00:00
|
|
|
""" Alias to self.roles. """
|
2016-03-21 04:26:02 +00:00
|
|
|
return self.roles
|
2016-12-16 23:38:08 +00:00
|
|
|
|
2017-01-09 14:21:54 +00:00
|
|
|
@property
|
2017-02-13 03:33:38 +00:00
|
|
|
def locations(self):
|
2017-01-09 14:21:54 +00:00
|
|
|
""" This does not exist in plex xml response but is added to have a common
|
|
|
|
interface to get the location of the Movie/Show/Episode
|
|
|
|
"""
|
2017-02-13 03:33:38 +00:00
|
|
|
return [p.file for p in self.iterParts() if p]
|
2017-01-09 14:21:54 +00:00
|
|
|
|
|
|
|
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
|
2017-02-13 19:38:40 +00:00
|
|
|
""" Download video files to specified directory.
|
|
|
|
|
|
|
|
Parameters:
|
|
|
|
savepath (str): Defaults to current working dir.
|
|
|
|
keep_orginal_name (bool): True to keep the original file name otherwise
|
|
|
|
a friendlier is generated.
|
|
|
|
**kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL()`.
|
|
|
|
"""
|
2017-01-09 14:21:54 +00:00
|
|
|
downloaded = []
|
|
|
|
locs = [i for i in self.iterParts() if i]
|
|
|
|
for loc in locs:
|
|
|
|
if keep_orginal_name is False:
|
|
|
|
name = '%s.%s' % (self.title.replace(' ', '.'), loc.container)
|
|
|
|
else:
|
|
|
|
name = loc.file
|
|
|
|
# So this seems to be a alot slower but allows transcode.
|
|
|
|
if kwargs:
|
|
|
|
download_url = self.getStreamURL(**kwargs)
|
|
|
|
else:
|
2017-02-09 04:29:17 +00:00
|
|
|
download_url = self._server.url('%s?download=1' % loc.key)
|
2017-02-09 04:13:54 +00:00
|
|
|
dl = utils.download(download_url, filename=name, savepath=savepath, session=self._server._session)
|
2017-01-09 14:21:54 +00:00
|
|
|
if dl:
|
|
|
|
downloaded.append(dl)
|
|
|
|
return downloaded
|
|
|
|
|
2014-12-29 03:21:58 +00:00
|
|
|
|
2017-02-13 02:55:55 +00:00
|
|
|
@utils.registerPlexObject
|
2014-12-29 03:21:58 +00:00
|
|
|
class Show(Video):
|
2017-02-13 19:38:40 +00:00
|
|
|
""" Represents a single Show (including all seasons and episodes).
|
|
|
|
|
|
|
|
Attributes:
|
|
|
|
TAG (str): 'Diectory'
|
|
|
|
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.
|
|
|
|
contentRating (str) Content rating (PG-13; NR; TV-G).
|
|
|
|
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.
|
|
|
|
genres (List<:class:`~plexapi.media.Genre`>): List of genre objects.
|
|
|
|
roles (List<:class:`~plexapi.media.Role`>): List of role objects.
|
|
|
|
"""
|
2017-02-13 02:55:55 +00:00
|
|
|
TAG = 'Directory'
|
2014-12-29 03:21:58 +00:00
|
|
|
TYPE = 'show'
|
2015-02-24 03:42:29 +00:00
|
|
|
|
2014-12-29 03:21:58 +00:00
|
|
|
def _loadData(self, data):
|
2017-02-13 19:38:40 +00:00
|
|
|
""" Load attribute values from Plex XML response. """
|
2016-04-07 05:39:04 +00:00
|
|
|
Video._loadData(self, data)
|
2017-02-13 19:38:40 +00:00
|
|
|
# fix key if loaded from search
|
2017-01-09 14:21:54 +00:00
|
|
|
self.key = self.key.replace('/children', '')
|
2017-02-04 17:43:50 +00:00
|
|
|
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.duration = utils.cast(int, data.attrib.get('duration'))
|
|
|
|
self.guid = data.attrib.get('guid')
|
|
|
|
self.index = data.attrib.get('index')
|
|
|
|
self.leafCount = utils.cast(int, data.attrib.get('leafCount'))
|
2017-02-13 03:15:47 +00:00
|
|
|
self.locations = self.listAttrs(data, 'path', etag='Location')
|
2016-12-16 23:38:08 +00:00
|
|
|
self.originallyAvailableAt = utils.toDatetime(
|
2017-02-04 17:43:50 +00:00
|
|
|
data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
|
|
|
|
self.rating = utils.cast(float, data.attrib.get('rating'))
|
|
|
|
self.studio = data.attrib.get('studio')
|
|
|
|
self.theme = data.attrib.get('theme')
|
2017-02-06 04:52:10 +00:00
|
|
|
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount'))
|
2017-02-04 17:43:50 +00:00
|
|
|
self.year = utils.cast(int, data.attrib.get('year'))
|
2017-02-13 02:55:55 +00:00
|
|
|
self.genres = self.findItems(data, media.Genre)
|
|
|
|
self.roles = self.findItems(data, media.Role)
|
2016-03-21 04:26:02 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def actors(self):
|
2017-02-13 19:38:40 +00:00
|
|
|
""" Alias to self.roles. """
|
2016-03-21 04:26:02 +00:00
|
|
|
return self.roles
|
2016-12-16 23:38:08 +00:00
|
|
|
|
2016-03-21 04:26:02 +00:00
|
|
|
@property
|
2016-03-22 03:12:12 +00:00
|
|
|
def isWatched(self):
|
2017-02-13 19:38:40 +00:00
|
|
|
""" Returns True if this show is fully watched. """
|
2016-03-21 04:26:02 +00:00
|
|
|
return bool(self.viewedLeafCount == self.leafCount)
|
2014-12-29 03:21:58 +00:00
|
|
|
|
2017-02-09 04:08:25 +00:00
|
|
|
def seasons(self, **kwargs):
|
2017-02-13 19:38:40 +00:00
|
|
|
""" Returns a list of :class:`~plexapi.video.Season` objects. """
|
2017-02-06 04:52:10 +00:00
|
|
|
key = '/library/metadata/%s/children' % self.ratingKey
|
2017-02-13 02:55:55 +00:00
|
|
|
return self.fetchItems(key, **kwargs)
|
2014-12-29 03:21:58 +00:00
|
|
|
|
2017-01-04 20:38:04 +00:00
|
|
|
def season(self, title=None):
|
2017-02-06 04:52:10 +00:00
|
|
|
""" Returns the season with the specified title or number.
|
2017-01-02 21:06:40 +00:00
|
|
|
|
2017-02-06 04:52:10 +00:00
|
|
|
Parameters:
|
|
|
|
title (str or int): Title or Number of the season to return.
|
2017-01-02 21:06:40 +00:00
|
|
|
"""
|
2017-01-05 21:58:43 +00:00
|
|
|
if isinstance(title, int):
|
|
|
|
title = 'Season %s' % title
|
2017-02-06 04:52:10 +00:00
|
|
|
key = '/library/metadata/%s/children' % self.ratingKey
|
2017-02-13 02:55:55 +00:00
|
|
|
return self.fetchItem(key, etag='Directory', title__iexact=title)
|
2014-12-29 03:21:58 +00:00
|
|
|
|
2017-02-09 04:08:25 +00:00
|
|
|
def episodes(self, **kwargs):
|
2017-02-13 19:38:40 +00:00
|
|
|
""" Returns a list of :class:`~plexapi.video.Episode` objects. """
|
2017-02-07 06:20:49 +00:00
|
|
|
key = '/library/metadata/%s/allLeaves' % self.ratingKey
|
2017-02-09 04:08:25 +00:00
|
|
|
return self.fetchItems(key, **kwargs)
|
2014-12-29 03:21:58 +00:00
|
|
|
|
2017-01-04 20:38:04 +00:00
|
|
|
def episode(self, title=None, season=None, episode=None):
|
2017-02-13 19:38:40 +00:00
|
|
|
""" Find a episode using a title or season and episode.
|
2017-01-05 21:58:43 +00:00
|
|
|
|
2017-02-13 19:38:40 +00:00
|
|
|
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).
|
2017-01-05 21:58:43 +00:00
|
|
|
|
|
|
|
Raises:
|
|
|
|
ValueError: If season and episode is missing.
|
|
|
|
NotFound: If the episode is missing.
|
|
|
|
"""
|
2017-01-04 20:38:04 +00:00
|
|
|
if title:
|
2017-02-07 06:20:49 +00:00
|
|
|
key = '/library/metadata/%s/allLeaves' % self.ratingKey
|
2017-02-09 06:54:38 +00:00
|
|
|
return self.fetchItem(key, title__iexact=title)
|
2017-01-04 20:38:04 +00:00
|
|
|
elif season and episode:
|
2017-02-06 04:52:10 +00:00
|
|
|
results = [i for i in self.episodes() if i.seasonNumber == season and i.index == episode]
|
2017-01-04 20:38:04 +00:00
|
|
|
if results:
|
|
|
|
return results[0]
|
2017-02-06 04:52:10 +00:00
|
|
|
raise NotFound('Couldnt find %s S%s E%s' % (self.title, season, episode))
|
2017-02-09 20:01:23 +00:00
|
|
|
raise TypeError('Missing argument: title or season and episode are required')
|
2014-12-29 03:21:58 +00:00
|
|
|
|
2015-06-02 02:55:20 +00:00
|
|
|
def watched(self):
|
2017-02-13 19:38:40 +00:00
|
|
|
""" Returns list of watched :class:`~plexapi.video.Episode` objects. """
|
2017-02-09 06:54:38 +00:00
|
|
|
return self.episodes(viewCount__gt=0)
|
2015-06-02 02:55:20 +00:00
|
|
|
|
|
|
|
def unwatched(self):
|
2017-02-13 19:38:40 +00:00
|
|
|
""" Returns list of unwatched :class:`~plexapi.video.Episode` objects. """
|
2017-02-09 06:54:38 +00:00
|
|
|
return self.episodes(viewCount=0)
|
2015-06-02 02:55:20 +00:00
|
|
|
|
2017-02-13 19:38:40 +00:00
|
|
|
def get(self, title=None, season=None, episode=None):
|
|
|
|
""" Alias to :func:`~plexapi.video.Show.episode()`. """
|
|
|
|
return self.episode(title, season, episode)
|
2014-12-29 03:21:58 +00:00
|
|
|
|
2017-01-09 14:21:54 +00:00
|
|
|
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
|
2017-02-13 19:38:40 +00:00
|
|
|
""" Download video files to specified directory.
|
|
|
|
|
|
|
|
Parameters:
|
|
|
|
savepath (str): Defaults to current working dir.
|
|
|
|
keep_orginal_name (bool): True to keep the original file name otherwise
|
|
|
|
a friendlier is generated.
|
|
|
|
**kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL()`.
|
|
|
|
"""
|
2017-01-09 14:21:54 +00:00
|
|
|
downloaded = []
|
|
|
|
for ep in self.episodes():
|
|
|
|
dl = ep.download(savepath=savepath, keep_orginal_name=keep_orginal_name, **kwargs)
|
|
|
|
if dl:
|
|
|
|
downloaded.extend(dl)
|
|
|
|
return downloaded
|
2015-06-02 02:27:43 +00:00
|
|
|
|
2014-12-29 03:21:58 +00:00
|
|
|
|
2017-02-13 02:55:55 +00:00
|
|
|
@utils.registerPlexObject
|
2014-12-29 03:21:58 +00:00
|
|
|
class Season(Video):
|
2017-02-13 19:38:40 +00:00
|
|
|
""" Represents a single Show Season (including all episodes).
|
|
|
|
|
|
|
|
Attributes:
|
|
|
|
TAG (str): 'Diectory'
|
|
|
|
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.
|
|
|
|
"""
|
2017-02-13 02:55:55 +00:00
|
|
|
TAG = 'Directory'
|
2014-12-29 03:21:58 +00:00
|
|
|
TYPE = 'season'
|
|
|
|
|
|
|
|
def _loadData(self, data):
|
2017-02-13 19:38:40 +00:00
|
|
|
""" Load attribute values from Plex XML response. """
|
2016-04-07 05:39:04 +00:00
|
|
|
Video._loadData(self, data)
|
2017-02-13 19:38:40 +00:00
|
|
|
# fix key if loaded from search
|
2017-01-09 14:21:54 +00:00
|
|
|
self.key = self.key.replace('/children', '')
|
2017-02-04 17:43:50 +00:00
|
|
|
self.leafCount = utils.cast(int, data.attrib.get('leafCount'))
|
|
|
|
self.index = utils.cast(int, data.attrib.get('index'))
|
|
|
|
self.parentKey = data.attrib.get('parentKey')
|
|
|
|
self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey'))
|
|
|
|
self.parentTitle = data.attrib.get('parentTitle')
|
2017-02-06 04:52:10 +00:00
|
|
|
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount'))
|
2016-12-16 23:38:08 +00:00
|
|
|
|
2017-02-07 06:20:49 +00:00
|
|
|
def __repr__(self):
|
|
|
|
return '<%s>' % ':'.join([p for p in [
|
|
|
|
self.__class__.__name__,
|
|
|
|
self.key.replace('/library/metadata/', '').replace('/children', ''),
|
|
|
|
'%s-s%s' % (self.parentTitle.replace(' ','-')[:20], self.seasonNumber),
|
|
|
|
] if p])
|
|
|
|
|
2016-03-21 04:26:02 +00:00
|
|
|
@property
|
2016-03-22 03:12:12 +00:00
|
|
|
def isWatched(self):
|
2017-02-13 19:38:40 +00:00
|
|
|
""" Returns True if this season is fully watched. """
|
2016-03-21 04:26:02 +00:00
|
|
|
return bool(self.viewedLeafCount == self.leafCount)
|
2014-12-29 03:21:58 +00:00
|
|
|
|
2016-05-20 03:42:06 +00:00
|
|
|
@property
|
|
|
|
def seasonNumber(self):
|
2017-02-13 19:38:40 +00:00
|
|
|
""" Returns season number. """
|
2016-05-20 03:42:06 +00:00
|
|
|
return self.index
|
|
|
|
|
2017-02-09 04:08:25 +00:00
|
|
|
def episodes(self, **kwargs):
|
2017-02-13 19:38:40 +00:00
|
|
|
""" Returns a list of :class:`~plexapi.video.Episode` objects. """
|
2017-02-06 04:52:10 +00:00
|
|
|
key = '/library/metadata/%s/children' % self.ratingKey
|
2017-02-13 02:55:55 +00:00
|
|
|
return self.fetchItems(key, **kwargs)
|
2014-12-29 03:21:58 +00:00
|
|
|
|
2017-02-13 19:38:40 +00:00
|
|
|
def episode(self, title=None, episode=None):
|
2017-02-07 06:20:49 +00:00
|
|
|
""" Returns the episode with the given title or number.
|
2017-01-02 21:06:40 +00:00
|
|
|
|
2017-02-07 06:20:49 +00:00
|
|
|
Parameters:
|
|
|
|
title (str): Title of the episode to return.
|
2017-02-13 19:38:40 +00:00
|
|
|
episode (int): Episode number (default:None; required if title not specified).
|
2017-01-05 21:58:43 +00:00
|
|
|
|
|
|
|
Raises:
|
|
|
|
TypeError: If title and episode is missing.
|
|
|
|
NotFound: If that episode cant be found.
|
2016-12-16 23:38:08 +00:00
|
|
|
"""
|
2017-02-13 19:38:40 +00:00
|
|
|
if not title and not episode:
|
2017-02-07 06:20:49 +00:00
|
|
|
raise BadRequest('Missing argument, you need to use title or episode.')
|
|
|
|
key = '/library/metadata/%s/children' % self.ratingKey
|
2017-01-04 20:38:04 +00:00
|
|
|
if title:
|
2017-02-08 07:00:43 +00:00
|
|
|
return self.fetchItem(key, title=title)
|
2017-02-13 19:38:40 +00:00
|
|
|
return self.fetchItem(key, seasonNumber=self.index, index=episode)
|
2017-01-04 20:38:04 +00:00
|
|
|
|
2017-02-13 19:38:40 +00:00
|
|
|
def get(self, title=None, episode=None):
|
|
|
|
""" Alias to :func:`~plexapi.video.Season.episode()`. """
|
|
|
|
return self.episode(title, episode)
|
2014-12-29 03:21:58 +00:00
|
|
|
|
|
|
|
def show(self):
|
2017-02-13 19:38:40 +00:00
|
|
|
""" Return this seasons :func:`~plexapi.video.Show`.. """
|
2017-02-07 06:20:49 +00:00
|
|
|
return self.fetchItem(self.parentKey)
|
2014-12-29 03:21:58 +00:00
|
|
|
|
2015-06-02 02:55:20 +00:00
|
|
|
def watched(self):
|
2017-02-13 19:38:40 +00:00
|
|
|
""" Returns list of watched :class:`~plexapi.video.Episode` objects. """
|
2015-06-02 02:55:20 +00:00
|
|
|
return self.episodes(watched=True)
|
|
|
|
|
|
|
|
def unwatched(self):
|
2017-02-13 19:38:40 +00:00
|
|
|
""" Returns list of unwatched :class:`~plexapi.video.Episode` objects. """
|
2015-06-02 02:55:20 +00:00
|
|
|
return self.episodes(watched=False)
|
|
|
|
|
2017-01-09 14:21:54 +00:00
|
|
|
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
|
2017-02-13 19:38:40 +00:00
|
|
|
""" Download video files to specified directory.
|
|
|
|
|
|
|
|
Parameters:
|
|
|
|
savepath (str): Defaults to current working dir.
|
|
|
|
keep_orginal_name (bool): True to keep the original file name otherwise
|
|
|
|
a friendlier is generated.
|
|
|
|
**kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL()`.
|
|
|
|
"""
|
2017-01-09 14:21:54 +00:00
|
|
|
downloaded = []
|
|
|
|
for ep in self.episodes():
|
|
|
|
dl = ep.download(savepath=savepath, keep_orginal_name=keep_orginal_name, **kwargs)
|
|
|
|
if dl:
|
|
|
|
downloaded.extend(dl)
|
|
|
|
return downloaded
|
2017-01-05 21:58:43 +00:00
|
|
|
|
2014-12-29 03:21:58 +00:00
|
|
|
|
2017-02-13 02:55:55 +00:00
|
|
|
@utils.registerPlexObject
|
2016-04-04 03:55:29 +00:00
|
|
|
class Episode(Video, Playable):
|
2017-02-13 19:38:40 +00:00
|
|
|
""" Represents a single Shows Episode.
|
|
|
|
|
|
|
|
Attributes:
|
|
|
|
TAG (str): 'Diectory'
|
|
|
|
TYPE (str): 'episode'
|
|
|
|
art (str): Key to episode artwork (/library/metadata/<ratingkey>/art/<artid>)
|
|
|
|
chapterSource (str): Unknown (media).
|
|
|
|
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.
|
|
|
|
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.
|
|
|
|
media (List<:class:`~plexapi.media.Media`>): List of media objects.
|
|
|
|
writers (List<:class:`~plexapi.media.Writer`>): List of writers objects.
|
|
|
|
"""
|
2017-02-13 02:55:55 +00:00
|
|
|
TAG = 'Video'
|
2014-12-29 03:21:58 +00:00
|
|
|
TYPE = 'episode'
|
|
|
|
|
|
|
|
def _loadData(self, data):
|
2017-02-13 19:38:40 +00:00
|
|
|
""" Load attribute values from Plex XML response. """
|
2016-04-07 05:39:04 +00:00
|
|
|
Video._loadData(self, data)
|
|
|
|
Playable._loadData(self, data)
|
2017-02-06 04:52:10 +00:00
|
|
|
self._seasonNumber = None # cached season number
|
2017-02-04 17:43:50 +00:00
|
|
|
self.art = data.attrib.get('art')
|
|
|
|
self.chapterSource = data.attrib.get('chapterSource')
|
|
|
|
self.contentRating = data.attrib.get('contentRating')
|
|
|
|
self.duration = utils.cast(int, data.attrib.get('duration'))
|
|
|
|
self.grandparentArt = data.attrib.get('grandparentArt')
|
|
|
|
self.grandparentKey = data.attrib.get('grandparentKey')
|
|
|
|
self.grandparentRatingKey = utils.cast(int, data.attrib.get('grandparentRatingKey'))
|
|
|
|
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.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
|
|
|
|
self.parentIndex = 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.rating = utils.cast(float, data.attrib.get('rating'))
|
2016-03-17 04:51:20 +00:00
|
|
|
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
2017-02-04 17:43:50 +00:00
|
|
|
self.year = utils.cast(int, data.attrib.get('year'))
|
2017-02-13 02:55:55 +00:00
|
|
|
self.directors = self.findItems(data, media.Director)
|
|
|
|
self.media = self.findItems(data, media.Media)
|
|
|
|
self.writers = self.findItems(data, media.Writer)
|
2016-03-21 04:26:02 +00:00
|
|
|
|
2017-01-05 21:58:43 +00:00
|
|
|
def __repr__(self):
|
2017-02-07 06:20:49 +00:00
|
|
|
return '<%s>' % ':'.join([p for p in [
|
|
|
|
self.__class__.__name__,
|
|
|
|
self.key.replace('/library/metadata/', '').replace('/children', ''),
|
|
|
|
'%s-s%se%s' % (self.grandparentTitle.replace(' ','-')[:20], self.seasonNumber, self.index),
|
|
|
|
] if p])
|
2017-01-05 21:58:43 +00:00
|
|
|
|
2017-02-13 19:38:40 +00:00
|
|
|
def _prettyfilename(self):
|
|
|
|
""" Returns a human friendly filename. """
|
|
|
|
return '%s.S%sE%s' % (self.grandparentTitle.replace(' ', '.'),
|
|
|
|
str(self.seasonNumber).zfill(2), str(self.index).zfill(2))
|
|
|
|
|
2016-03-21 04:26:02 +00:00
|
|
|
@property
|
2017-02-13 19:38:40 +00:00
|
|
|
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/Show
|
|
|
|
"""
|
|
|
|
return [part.file for part in self.iterParts() if part]
|
2014-12-29 03:21:58 +00:00
|
|
|
|
2016-05-20 03:42:06 +00:00
|
|
|
@property
|
|
|
|
def seasonNumber(self):
|
2017-02-13 19:38:40 +00:00
|
|
|
""" Returns this episodes season number. """
|
2016-05-20 03:42:06 +00:00
|
|
|
if self._seasonNumber is None:
|
|
|
|
self._seasonNumber = self.parentIndex if self.parentIndex else self.season().seasonNumber
|
2017-01-04 20:38:04 +00:00
|
|
|
return utils.cast(int, self._seasonNumber)
|
2016-05-20 03:42:06 +00:00
|
|
|
|
2015-02-24 03:42:29 +00:00
|
|
|
@property
|
|
|
|
def thumbUrl(self):
|
2017-02-09 20:01:23 +00:00
|
|
|
""" Return url to for the thumbnail image. """
|
2017-01-02 21:19:07 +00:00
|
|
|
if self.grandparentThumb:
|
2017-02-09 04:29:17 +00:00
|
|
|
return self._server.url(self.grandparentThumb)
|
2015-02-24 03:42:29 +00:00
|
|
|
|
2014-12-29 03:21:58 +00:00
|
|
|
def season(self):
|
2017-02-13 19:38:40 +00:00
|
|
|
"""" Return this episodes :func:`~plexapi.video.Season`.. """
|
2017-02-07 06:20:49 +00:00
|
|
|
return self.fetchItem(self.parentKey)
|
2014-12-29 03:21:58 +00:00
|
|
|
|
|
|
|
def show(self):
|
2017-02-13 19:38:40 +00:00
|
|
|
"""" Return this episodes :func:`~plexapi.video.Show`.. """
|
2017-02-07 06:20:49 +00:00
|
|
|
return self.fetchItem(self.grandparentKey)
|