python-plexapi/plexapi/video.py

448 lines
17 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
from plexapi import media, utils
from plexapi.exceptions import BadRequest, NotFound
from plexapi.base import Playable, PlexPartialObject
2017-01-02 21:06:40 +00:00
2014-12-29 03:21:58 +00:00
class Video(PlexPartialObject):
2014-12-29 03:21:58 +00:00
TYPE = None
def _loadData(self, data):
""" Used to set the attributes. """
self._data = data
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')
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'))
self.viewCount = utils.cast(int, data.attrib.get('viewCount', 0))
2014-12-29 03:21:58 +00:00
@property
def thumbUrl(self):
2017-01-02 21:06:40 +00:00
"""Return url to thumb image."""
2017-01-02 21:19:07 +00:00
if self.thumb:
return self._server.url(self.thumb)
2014-12-29 03:21:58 +00:00
def analyze(self):
2016-12-16 23:38:08 +00:00
"""The primary purpose of media analysis is to gather information about
that mediaitem. All of the media you add to a Library has properties
that are useful to knowwhether it's a video file,
a music track, or one of your photos.
"""
key = '/%s/analyze' % self.key.lstrip('/')
self._server.query(key, method=self._server._session.put)
2014-12-29 03:21:58 +00:00
def markWatched(self):
2017-01-02 21:06:40 +00:00
"""Mark a items as watched."""
key = '/:/scrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey
self._server.query(key)
2014-12-29 03:21:58 +00:00
self.reload()
def markUnwatched(self):
2017-01-02 21:06:40 +00:00
"""Mark a item as unwatched."""
key = '/:/unscrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey
self._server.query(key)
2014-12-29 03:21:58 +00:00
self.reload()
def refresh(self):
2017-01-02 21:06:40 +00:00
"""Refresh a item."""
key = '%s/refresh' % self.key
self._server.query(key, method=self._server._session.put)
2016-12-16 23:38:08 +00:00
def section(self):
2017-01-02 21:06:40 +00:00
"""Library section."""
return self._server.library.sectionByID(self.librarySectionID)
2014-12-29 03:21:58 +00:00
@utils.register_libtype
class Movie(Video, Playable):
2014-12-29 03:21:58 +00:00
TYPE = 'movie'
def _loadData(self, data):
2016-12-16 23:38:08 +00:00
"""Used to set the attributes
2017-01-02 21:06:40 +00:00
2016-12-16 23:38:08 +00:00
Args:
2017-01-02 21:06:40 +00:00
data (Element): XML reponse from PMS as Element
normally built from server.query
2016-12-16 23:38:08 +00:00
"""
Video._loadData(self, data)
Playable._loadData(self, data)
2017-02-04 17:43:50 +00:00
self.art = data.attrib.get('art')
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'))
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'))
self.collections = self._buildItems(data, media.Collection)
2017-02-08 05:36:22 +00:00
self.countries = self._buildItems(data, media.Country, bytag=True)
self.directors = self._buildItems(data, media.Director, bytag=True)
self.fields = self._buildItems(data, media.Field, bytag=True)
self.genres = self._buildItems(data, media.Genre, bytag=True)
self.media = self._buildItems(data, media.Media, bytag=True)
self.producers = self._buildItems(data, media.Producer, bytag=True)
self.roles = self._buildItems(data, media.Role, bytag=True)
self.writers = self._buildItems(data, media.Writer, bytag=True)
2016-12-16 23:38:08 +00:00
@property
def actors(self):
return self.roles
2016-12-16 23:38:08 +00:00
@property
def isWatched(self):
return bool(self.viewCount > 0)
2014-12-29 03:21:58 +00:00
2017-01-09 14:21:54 +00:00
@property
def location(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/Episode
"""
files = [i.file for i in self.iterParts() if i]
if len(files) == 1:
files = files[0]
return files
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
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:
download_url = self._server.url('%s?download=1' % loc.key)
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
@utils.register_libtype
2014-12-29 03:21:58 +00:00
class Show(Video):
TYPE = 'show'
2015-02-24 03:42:29 +00:00
2014-12-29 03:21:58 +00:00
def _loadData(self, data):
Video._loadData(self, data)
# fix the key if this was 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'))
self.location = utils.findLocations(data, single=True) or None
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')
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-08 05:36:22 +00:00
self.genres = self._buildItems(data, media.Genre, bytag=True)
self.roles = self._buildItems(data, media.Role, bytag=True)
@property
def actors(self):
return self.roles
2016-12-16 23:38:08 +00:00
@property
def isWatched(self):
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-01-02 21:06:40 +00:00
"""Returns a list of Season."""
key = '/library/metadata/%s/children' % self.ratingKey
2017-02-09 04:08:25 +00:00
return self.fetchItems(key, type=Season.TYPE, **kwargs)
2014-12-29 03:21:58 +00:00
2017-01-04 20:38:04 +00:00
def season(self, title=None):
""" Returns the season with the specified title or number.
2017-01-02 21:06:40 +00:00
Parameters:
title (str or int): Title or Number of the season to return.
2017-01-02 21:06:40 +00:00
"""
if isinstance(title, int):
title = 'Season %s' % title
key = '/library/metadata/%s/children' % self.ratingKey
return self.fetchItem(key, tag='Directory', title__iexact=title)
2014-12-29 03:21:58 +00:00
2017-02-09 04:08:25 +00:00
def episodes(self, **kwargs):
""" Returs a list of Episode """
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):
"""Find a episode using a title or season and episode.
Note:
Both season and episode is required if title is missing.
Args:
title (str): Default None
season (int): Season number, default None
episode (int): Episode number, default None
Raises:
ValueError: If season and episode is missing.
NotFound: If the episode is missing.
Returns:
Episode
Examples:
>>> plex.search('The blacklist')[0].episode(season=1, episode=1)
<Episode:116263:The.Freelancer>
>>> plex.search('The blacklist')[0].episode('The Freelancer')
<Episode:116263:The.Freelancer>
"""
if not title and (not season or not episode):
raise TypeError('Missing argument: title or season and episode are required')
2017-01-04 20:38:04 +00:00
if title:
key = '/library/metadata/%s/allLeaves' % self.ratingKey
return self.fetchItem(key, title__iexact=title)
2017-01-04 20:38:04 +00:00
elif season and episode:
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]
raise NotFound('Couldnt find %s S%s E%s' % (self.title, season, episode))
2014-12-29 03:21:58 +00:00
def watched(self):
2017-01-02 21:06:40 +00:00
"""Return a list of watched episodes"""
return self.episodes(viewCount__gt=0)
def unwatched(self):
2017-01-02 21:06:40 +00:00
"""Return a list of unwatched episodes"""
return self.episodes(viewCount=0)
2014-12-29 03:21:58 +00:00
def get(self, title):
2017-01-02 21:06:40 +00:00
"""Get a Episode with a title.
Args:
title (str): fx Secret santa
"""
2014-12-29 03:21:58 +00:00
return self.episode(title)
2017-01-09 14:21:54 +00:00
def analyze(self):
""" """
raise 'Cant analyse a show' # fix me
def refresh(self):
2017-01-02 21:06:40 +00:00
"""Refresh the metadata."""
self._server.query('/library/metadata/%s/refresh' % self.ratingKey, method=self._server._session.put)
2017-01-09 14:21:54 +00:00
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
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
2014-12-29 03:21:58 +00:00
@utils.register_libtype
2014-12-29 03:21:58 +00:00
class Season(Video):
TYPE = 'season'
def _loadData(self, data):
2016-12-16 23:38:08 +00:00
"""Used to set the attributes
2017-01-02 21:06:40 +00:00
2016-12-16 23:38:08 +00:00
Args:
data (Element): Usually built from server.query
2016-12-16 23:38:08 +00:00
"""
Video._loadData(self, data)
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')
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount'))
2016-12-16 23:38:08 +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])
@property
def isWatched(self):
return bool(self.viewedLeafCount == self.leafCount)
2014-12-29 03:21:58 +00:00
@property
def seasonNumber(self):
2017-01-09 14:21:54 +00:00
"""Returns season number."""
return self.index
2017-02-09 04:08:25 +00:00
def episodes(self, **kwargs):
""" Returs a list of Episode. """
key = '/library/metadata/%s/children' % self.ratingKey
2017-02-09 04:08:25 +00:00
return self.fetchItems(key, type=Episode.TYPE, **kwargs)
2014-12-29 03:21:58 +00:00
def episode(self, title=None, num=None):
""" Returns the episode with the given title or number.
2017-01-02 21:06:40 +00:00
Parameters:
title (str): Title of the episode to return.
num (int): Number of the episode to return (if title not specified).
Raises:
TypeError: If title and episode is missing.
NotFound: If that episode cant be found.
Examples:
>>> plex.search('The blacklist').season(1).episode(episode=1)
<Episode:116263:The.Freelancer>
>>> plex.search('The blacklist').season(1).episode('The Freelancer')
<Episode:116263:The.Freelancer>
2016-12-16 23:38:08 +00:00
"""
if not title and not num:
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:
return self.fetchItem(key, title=title)
return self.fetchItem(key, seasonNumber=self.index, index=num)
2017-01-04 20:38:04 +00:00
2014-12-29 03:21:58 +00:00
def get(self, title):
""" Alias for self.episode. """
2014-12-29 03:21:58 +00:00
return self.episode(title)
def show(self):
2017-01-02 21:06:40 +00:00
"""Return this seasons show."""
return self.fetchItem(self.parentKey)
2014-12-29 03:21:58 +00:00
def watched(self):
2017-01-02 21:06:40 +00:00
"""Returns a list of watched Episode"""
return self.episodes(watched=True)
def unwatched(self):
2017-01-02 21:06:40 +00:00
"""Returns a list of unwatched Episode"""
return self.episodes(watched=False)
2017-01-09 14:21:54 +00:00
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
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
2014-12-29 03:21:58 +00:00
@utils.register_libtype
class Episode(Video, Playable):
2014-12-29 03:21:58 +00:00
TYPE = 'episode'
def _loadData(self, data):
2017-01-02 21:06:40 +00:00
"""Used to set the attributes
Args:
data (Element): Usually built from server.query
2017-01-02 21:06:40 +00:00
"""
Video._loadData(self, data)
Playable._loadData(self, data)
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'))
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-08 05:36:22 +00:00
self.directors = self._buildItems(data, media.Director, bytag=True)
self.media = self._buildItems(data, media.Media, bytag=True)
self.writers = self._buildItems(data, media.Writer, bytag=True)
# data for active sessions and history
2017-02-04 17:43:50 +00:00
self.sessionKey = utils.cast(int, data.attrib.get('sessionKey'))
self.username = utils.findUsername(data)
self.player = utils.findPlayer(self._server, data)
self.transcodeSession = utils.findTranscodeSession(self._server, data)
def __repr__(self):
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])
@property
def isWatched(self):
2017-01-02 21:06:40 +00:00
"""Returns True if watched, False if not."""
return bool(self.viewCount > 0)
2014-12-29 03:21:58 +00:00
@property
def seasonNumber(self):
2017-01-02 21:06:40 +00:00
"""Return this episode seasonnumber."""
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)
2015-02-24 03:42:29 +00:00
@property
def thumbUrl(self):
2017-01-02 21:06:40 +00:00
"""Return url to thumb image."""
2017-01-02 21:19:07 +00:00
if self.grandparentThumb:
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-01-02 21:06:40 +00:00
"""Return this episode Season"""
return self.fetchItem(self.parentKey)
2014-12-29 03:21:58 +00:00
def show(self):
2017-01-02 21:06:40 +00:00
"""Return this episodes Show"""
return self.fetchItem(self.grandparentKey)
2017-01-09 14:21:54 +00:00
@property
def location(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
"""
# Note this should probably belong to some parent.
files = [i.file for i in self.iterParts() if i]
if len(files) == 1:
files = files[0]
return files
def _prettyfilename(self):
2017-02-02 03:53:05 +00:00
return '%s.S%sE%s' % (self.grandparentTitle.replace(' ', '.'),
str(self.seasonNumber).zfill(2), str(self.index).zfill(2))