This commit is contained in:
Hellowlol 2016-12-17 00:38:08 +01:00
parent 8d05808236
commit 740e7a5b9b
3 changed files with 343 additions and 83 deletions

View file

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
"""
PlexServer
"""
@ -183,19 +184,20 @@ class PlexServer(object):
path (sting): relative path to PMS, fx /search?query=HELLO
method (None, optional): requests.method, fx requests.put
headers (None, optional): Headers that will be passed to PMS
**kwargs (dict): Loads of different stuff
**kwargs (dict): Used for filter and sorting.
Raises:
BadRequest: Description
Returns:
ElementTree or None
xml.etree.ElementTree.Element or None
"""
url = self.url(path)
method = method or self.session.get
log.info('%s %s', method.__name__.upper(), url)
h = headers.copy()
h.update(headers or {})
h = self.headers().copy()
if headers:
h.update(headers)
response = method(url, headers=h, timeout=TIMEOUT, **kwargs)
if response.status_code not in [200, 201]:
codename = codes.get(response.status_code)[0]
@ -236,7 +238,7 @@ class Account(object):
is not required to get basic plex information.
Attributes:
authToken (TYPE): Description
authToken (sting): X-Plex-Token, using for authenication with PMS
mappingError (TYPE): Description
mappingErrorMessage (TYPE): Description
mappingState (TYPE): Description
@ -252,6 +254,10 @@ class Account(object):
"""
def __init__(self, server, data):
"""Args:
server (Plexclient):
data (xml.etree.ElementTree.Element): used to set the class attributes.
"""
self.authToken = data.attrib.get('authToken')
self.username = data.attrib.get('username')
self.mappingState = data.attrib.get('mappingState')

View file

@ -39,15 +39,38 @@ class _NA(object):
"""
def __bool__(self):
"""Summary
Returns:
TYPE: Description
"""
return False
def __eq__(self, other):
"""Summary
Args:
other (TYPE): Description
Returns:
TYPE: Description
"""
return isinstance(other, _NA) or other in [None, '__NA__']
def __nonzero__(self):
"""Summary
Returns:
TYPE: Description
"""
return False
def __repr__(self):
"""Summary
Returns:
TYPE: Description
"""
return '__NA__'
NA = _NA()
@ -65,11 +88,25 @@ class PlexPartialObject(object):
"""
def __init__(self, data, initpath, server=None):
"""
Args:
data (xml.etree.ElementTree.Element): passed from server.query
initpath (string): Relative path
server (None or Plexserver, optional): PMS class your connected to
"""
self.server = server
self.initpath = initpath
self._loadData(data)
def __eq__(self, other):
"""Summary
Args:
other (TYPE): Description
Returns:
TYPE: Description
"""
return other is not None and self.key == other.key
def __repr__(self):
@ -80,18 +117,32 @@ class PlexPartialObject(object):
return '<%s:%s:%s>' % (clsname, key, title)
def __getattr__(self, attr):
"""Auto reload self, if the attribute is NA"""
"""Auto reload self, if the attribute is NA
Args:
attr (string): fx key
"""
if attr == 'key' or self.__dict__.get(attr) or self.isFullObject():
return self.__dict__.get(attr, NA)
self.reload()
return self.__dict__.get(attr, NA)
def __setattr__(self, attr, value):
"""Set attribute
Args:
attr (string): fx key
value (TYPE): Description
"""
if value != NA or self.isFullObject():
self.__dict__[attr] = value # twice as fast
#super(PlexPartialObject, self).__setattr__(attr, value)
self.__dict__[attr] = value
def _loadData(self, data):
"""Uses a element to set a attrs.
Args:
data (Element): Used by attrs
"""
raise Exception('Abstract method not implemented.')
def isFullObject(self):
@ -101,29 +152,32 @@ class PlexPartialObject(object):
return not self.isFullObject()
def reload(self):
"""Reload the data for this object from PlexServer XML.
"""
"""Reload the data for this object from PlexServer XML."""
data = self.server.query(self.key)
self.initpath = self.key
self._loadData(data[0])
class Playable(object):
"""This is a general place to store functions specific to media that is Playable. Things
were getting mixed up a bit when dealing with Shows, Season, Artists, Albums which
are all not playable.
"""This is a general place to store functions specific to media that is Playable.
Things were getting mixed up a bit when dealing with Shows, Season,
Artists, Albums which are all not playable.
Attributes:
Attributes: # todo
player (TYPE): Description
playlistItemID (TYPE): Description
sessionKey (TYPE): Description
transcodeSession (TYPE): Description
username (TYPE): Description
viewedAt (TYPE): Description
viewedAt (datetime): Description
"""
def _loadData(self, data):
"""Set the class attributes
Args:
data (xml.etree.ElementTree.Element): usually from server.query
"""
# data for active sessions (/status/sessions)
self.sessionKey = cast(int, data.attrib.get('sessionKey', NA))
self.username = findUsername(data)
@ -135,6 +189,17 @@ class Playable(object):
self.playlistItemID = cast(int, data.attrib.get('playlistItemID', NA))
def getStreamURL(self, **params):
"""Make a stream url that can be used by vlc.
Args:
**params (dict): Description
Returns:
string: ''
Raises:
Unsupported: Raises a error is the type is wrong.
"""
if self.TYPE not in ('movie', 'episode', 'track'):
raise Unsupported(
'Fetching stream URL for %s is unsupported.' % self.TYPE)
@ -156,32 +221,31 @@ class Playable(object):
return self.server.url('/%s/:/transcode/universal/start.m3u8?%s' % (streamtype, urlencode(params)))
def iterParts(self):
"""Yield parts
"""
"""Yield parts."""
for item in self.media:
for part in item.parts:
yield part
def play(self, client):
"""Start playback on a client."""
"""Start playback on a client.
Args:
client (PlexClient): The client to start playing on.
"""
client.playMedia(self)
def buildItem(server, elem, initpath, bytag=False):
"""Build classes used by the plexapi.
Args:
server (Plexserver): Server
elem (ElementThree):
initpath (string): init path of the url, used to determin
if this is a full object.
bytag (bool): Dunno # TOO
Args:
server (Plexserver): Your connected to.
elem (xml.etree.ElementTree.Element): xml from PMS
initpath (string): Relative path
bytag (bool, optional): Description # figure out what this do
Returns:
library type
Raises:
UnknownType
Raises:
UnknownType: Unknown library type libtype
"""
libtype = elem.tag if bytag else elem.attrib.get('type')
@ -194,7 +258,12 @@ def buildItem(server, elem, initpath, bytag=False):
def cast(func, value):
"""Helper to change to the correct type"""
"""Helper to change to the correct type
Args:
func (function): function to used [int, bool float]
value (string, int, float): value to cast
"""
if value not in [None, NA]:
if func == bool:
return bool(int(value))
@ -208,7 +277,15 @@ def cast(func, value):
def findKey(server, key):
"""Finds and builds a object based on ratingKey."""
"""Finds and builds a object based on ratingKey.
Args:
server (Plexserver): PMS your connected to
key (int): key to look for
Raises:
NotFound: Unable to find key. Key
"""
path = '/library/metadata/{0}'.format(key)
try:
# Item seems to be the first sub element
@ -219,7 +296,16 @@ def findKey(server, key):
def findItem(server, path, title):
"""Finds and builds a object based on title."""
"""Finds and builds a object based on title.
Args:
server (Plexserver): Description
path (string): Relative path
title (string): Fx 16 blocks
Raises:
NotFound: Unable to find item: title
"""
for elem in server.query(path):
if elem.attrib.get('title').lower() == title.lower():
return buildItem(server, elem, path)
@ -229,12 +315,12 @@ def findItem(server, path, title):
def findLocations(data, single=False):
"""Extract the path from a location tag
Args:
data (elementthree):
single (bool): One or more locations
Args:
data (xml.etree.ElementTree.Element): xml from PMS as Element
single (bool, optional): Only return one
Returns:
list: of filepaths
Returns:
filepath string if single is True else list of filepaths
"""
locations = []
for elem in data:
@ -248,11 +334,12 @@ def findLocations(data, single=False):
def findPlayer(server, data):
"""Find a player in a elementthee
Args:
data (elementthree):
Args:
server (Plexserver): PMS your connected to
data (xml.etree.ElementTree.Element): xml from pms as a element
Returns:
PlexClient or None
Returns:
PlexClient or None
"""
elem = data.find('Player')
if elem is not None:
@ -264,6 +351,15 @@ def findPlayer(server, data):
def findStreams(media, streamtype):
"""Find streams.
Args:
media (Show, Movie, Episode): A item where find streams
streamtype (string): Possible options [movie, show, episode] # is this correct?
Returns:
list: of streams
"""
streams = []
for mediaitem in media:
for part in mediaitem.parts:
@ -274,6 +370,16 @@ def findStreams(media, streamtype):
def findTranscodeSession(server, data):
"""Find transcode session.
Args:
server (Plexserver): PMS your connected to
data (xml.etree.ElementTree.Element): XML response from PMS as Element
Returns:
media.TranscodeSession or None
"""
elem = data.find('TranscodeSession')
if elem is not None:
from plexapi import media
@ -282,6 +388,14 @@ def findTranscodeSession(server, data):
def findUsername(data):
"""Find a username in a Element
Args:
data (xml.etree.ElementTree.Element): XML from PMS as a Element
Returns:
username or None
"""
elem = data.find('User')
if elem is not None:
return elem.attrib.get('title')
@ -289,6 +403,7 @@ def findUsername(data):
def isInt(string):
"""Check of a string is a int"""
try:
int(string)
return True
@ -297,6 +412,15 @@ def isInt(string):
def joinArgs(args):
"""Builds a query string where only
the value is quoted.
Args:
args (dict): ex {'genre': 'action', 'type': 1337}
Returns:
string: ?genre=action&type=1337
"""
if not args:
return ''
arglist = []
@ -307,10 +431,31 @@ def joinArgs(args):
def listChoices(server, path):
"""ListChoices is by _cleanSort etc.
Args:
server (Plexserver): Server your connected to
path (string): Relative path to PMS
Returns:
dict: title:key
"""
return {c.attrib['title']: c.attrib['key'] for c in server.query(path)}
def listItems(server, path, libtype=None, watched=None, bytag=False):
"""Return a list buildItem. See buildItem doc.
Args:
server (Plexserver): PMS your connected to.
path (string): Relative path to PMS
libtype (None or string, optional): [movie, show, episode, music] # check me
watched (None, True, False, optional): Skip or include watched items
bytag (bool, optional): Dunno wtf this is used for # todo
Returns:
list: of buildItem
"""
items = []
for elem in server.query(path):
if libtype and elem.attrib.get('type') != libtype:
@ -347,6 +492,19 @@ def rget(obj, attrstr, default=None, delim='.'):
def searchType(libtype):
"""Map search type name to int using SEACHTYPES
Used when querying PMS.
Args:
libtype (string): Possible options see SEARCHTYPES
Returns:
int: fx 1
Raises:
NotFound: Unknown libtype: libtype
"""
libtype = str(libtype)
if libtype in [str(v) for v in SEARCHTYPES.values()]:
return libtype
@ -356,6 +514,13 @@ def searchType(libtype):
def threaded(callback, listargs):
"""Run some function in threads.
Args:
callback (function): funcion to run in thread
listargs (list): args parssed to the callback
"""
threads, results = [], []
for args in listargs:
args += [results, len(results)]
@ -368,7 +533,15 @@ def threaded(callback, listargs):
def toDatetime(value, format=None):
"""Helper for datetime"""
"""Helper for datetime
Args:
value (string): value to use to make datetime
format (None, optional): string as strptime.
Returns:
datetime
"""
if value and value != NA:
if format:
value = datetime.strptime(value, format)

View file

@ -1,6 +1,10 @@
# -*- coding: utf-8 -*-
"""
PlexVideo
Attributes:
NA (TYPE): Description
"""
from plexapi import media, utils
from plexapi.utils import Playable, PlexPartialObject
@ -11,15 +15,28 @@ class Video(PlexPartialObject):
TYPE = None
def __init__(self, server, data, initpath):
"""
Args:
server (Plexserver): The PMS server your connected to
data (Element): Element built from server.query
initpath (string): Relativ path fx /library/sections/1/all
"""
super(Video, self).__init__(data, initpath, server)
def _loadData(self, data):
"""Used to set the attributes
Args:
data (Element): Usually built from server.query
"""
self.listType = 'video'
self.addedAt = utils.toDatetime(data.attrib.get('addedAt', NA))
self.key = data.attrib.get('key', NA)
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt', NA))
self.lastViewedAt = utils.toDatetime(
data.attrib.get('lastViewedAt', NA))
self.librarySectionID = data.attrib.get('librarySectionID', NA)
self.ratingKey = data.attrib.get('ratingKey', NA)
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey', NA))
self.summary = data.attrib.get('summary', NA)
self.thumb = data.attrib.get('thumb', NA)
self.title = data.attrib.get('title', NA)
@ -33,26 +50,36 @@ class Video(PlexPartialObject):
return self.server.url(self.thumb)
def analyze(self):
""" The primary purpose of media analysis is to gather information about that media
item. 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.
"""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.
"""
self.server.query('/%s/analyze' % self.key)
def markWatched(self):
"""Mark a items as watched.
"""
path = '/:/scrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey
self.server.query(path)
self.reload()
def markUnwatched(self):
"""Mark a item as unwatched.
"""
path = '/:/unscrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey
self.server.query(path)
self.reload()
def refresh(self):
self.server.query('%s/refresh' % self.key, method=self.server.session.put)
"""Refresh a item.
"""
self.server.query('%s/refresh' %
self.key, method=self.server.session.put)
def section(self):
"""Library section.
"""
return self.server.library.sectionByID(self.librarySectionID)
@ -61,17 +88,24 @@ class Movie(Video, Playable):
TYPE = 'movie'
def _loadData(self, data):
"""Used to set the attributes
Args:
data (Element): Usually built from server.query
"""
Video._loadData(self, data)
Playable._loadData(self, data)
self.art = data.attrib.get('art', NA)
self.audienceRating = utils.cast(float, data.attrib.get('audienceRating', NA))
self.audienceRating = utils.cast(
float, data.attrib.get('audienceRating', NA))
self.audienceRatingImage = data.attrib.get('audienceRatingImage', NA)
self.chapterSource = data.attrib.get('chapterSource', NA)
self.contentRating = data.attrib.get('contentRating', NA)
self.duration = utils.cast(int, data.attrib.get('duration', NA))
self.guid = data.attrib.get('guid', NA)
self.originalTitle = data.attrib.get('originalTitle', NA)
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d')
self.originallyAvailableAt = utils.toDatetime(
data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d')
self.primaryExtraKey = data.attrib.get('primaryExtraKey', NA)
self.rating = data.attrib.get('rating', NA)
self.ratingImage = data.attrib.get('ratingImage', NA)
@ -80,24 +114,34 @@ class Movie(Video, Playable):
self.userRating = utils.cast(float, data.attrib.get('userRating', NA))
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
self.year = utils.cast(int, data.attrib.get('year', NA))
if self.isFullObject():
self.collections = [media.Collection(self.server, e) for e in data if e.tag == media.Collection.TYPE]
self.countries = [media.Country(self.server, e) for e in data if e.tag == media.Country.TYPE]
self.directors = [media.Director(self.server, e) for e in data if e.tag == media.Director.TYPE]
self.genres = [media.Genre(self.server, e) for e in data if e.tag == media.Genre.TYPE]
self.media = [media.Media(self.server, e, self.initpath, self) for e in data if e.tag == media.Media.TYPE]
self.producers = [media.Producer(self.server, e) for e in data if e.tag == media.Producer.TYPE]
self.roles = [media.Role(self.server, e) for e in data if e.tag == media.Role.TYPE]
self.writers = [media.Writer(self.server, e) for e in data if e.tag == media.Writer.TYPE]
self.fields = [media.Field(e) for e in data if e.tag == media.Field.TYPE]
if self.isFullObject(): # check this
self.collections = [media.Collection(
self.server, e) for e in data if e.tag == media.Collection.TYPE]
self.countries = [media.Country(self.server, e)
for e in data if e.tag == media.Country.TYPE]
self.directors = [media.Director(
self.server, e) for e in data if e.tag == media.Director.TYPE]
self.genres = [media.Genre(self.server, e)
for e in data if e.tag == media.Genre.TYPE]
self.media = [media.Media(self.server, e, self.initpath, self)
for e in data if e.tag == media.Media.TYPE]
self.producers = [media.Producer(
self.server, e) for e in data if e.tag == media.Producer.TYPE]
self.roles = [media.Role(self.server, e)
for e in data if e.tag == media.Role.TYPE]
self.writers = [media.Writer(self.server, e)
for e in data if e.tag == media.Writer.TYPE]
self.fields = [media.Field(e)
for e in data if e.tag == media.Field.TYPE]
self.videoStreams = utils.findStreams(self.media, 'videostream')
self.audioStreams = utils.findStreams(self.media, 'audiostream')
self.subtitleStreams = utils.findStreams(self.media, 'subtitlestream')
self.subtitleStreams = utils.findStreams(
self.media, 'subtitlestream')
@property
def actors(self):
return self.roles
@property
def isWatched(self):
return bool(self.viewCount > 0)
@ -108,6 +152,11 @@ class Show(Video):
TYPE = 'show'
def _loadData(self, data):
"""Used to set the attributes
Args:
data (Element): Usually built from server.query
"""
Video._loadData(self, data)
self.art = data.attrib.get('art', NA)
self.banner = data.attrib.get('banner', NA)
@ -118,25 +167,31 @@ class Show(Video):
self.index = data.attrib.get('index', NA)
self.leafCount = utils.cast(int, data.attrib.get('leafCount', NA))
self.location = utils.findLocations(data, single=True)
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d')
self.originallyAvailableAt = utils.toDatetime(
data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d')
self.rating = utils.cast(float, data.attrib.get('rating', NA))
self.studio = data.attrib.get('studio', NA)
self.theme = data.attrib.get('theme', NA)
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount', NA))
self.viewedLeafCount = utils.cast(
int, data.attrib.get('viewedLeafCount', NA))
self.year = utils.cast(int, data.attrib.get('year', NA))
if self.isFullObject():
self.genres = [media.Genre(self.server, e) for e in data if e.tag == media.Genre.TYPE]
self.roles = [media.Role(self.server, e) for e in data if e.tag == media.Role.TYPE]
self.genres = [media.Genre(self.server, e)
for e in data if e.tag == media.Genre.TYPE]
self.roles = [media.Role(self.server, e)
for e in data if e.tag == media.Role.TYPE]
@property
def actors(self):
return self.roles
@property
def isWatched(self):
return bool(self.viewedLeafCount == self.leafCount)
def seasons(self):
"""Returns a list of Season
"""
path = '/library/metadata/%s/children' % self.ratingKey
return utils.listItems(self.server, path, Season.TYPE)
@ -153,15 +208,21 @@ class Show(Video):
return utils.findItem(self.server, path, title)
def watched(self):
"""Return a list of watched episodes
"""
return self.episodes(watched=True)
def unwatched(self):
"""Return a list of unwatched episodes
"""
return self.episodes(watched=False)
def get(self, title):
return self.episode(title)
def refresh(self):
"""Refresh the metadata
"""
self.server.query('/library/metadata/%s/refresh' % self.ratingKey)
@ -170,13 +231,19 @@ class Season(Video):
TYPE = 'season'
def _loadData(self, data):
"""Used to set the attributes
Args:
data (Element): Usually built from server.query
"""
Video._loadData(self, data)
self.leafCount = utils.cast(int, data.attrib.get('leafCount', NA))
self.index = data.attrib.get('index', NA)
self.parentKey = data.attrib.get('parentKey', NA)
self.parentRatingKey = data.attrib.get('parentRatingKey', NA)
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount', NA))
self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey', NA))
self.viewedLeafCount = utils.cast(
int, data.attrib.get('viewedLeafCount', NA))
@property
def isWatched(self):
return bool(self.viewedLeafCount == self.leafCount)
@ -186,10 +253,20 @@ class Season(Video):
return self.index
def episodes(self, watched=None):
"""Return list of Episode
Args:
watched (None, optional): Description
"""
childrenKey = '/library/metadata/%s/children' % self.ratingKey
return utils.listItems(self.server, childrenKey, watched=watched)
def episode(self, title):
"""Return Episode
Args:
title (TYPE): Description
"""
path = '/library/metadata/%s/children' % self.ratingKey
return utils.findItem(self.server, path, title)
@ -219,27 +296,31 @@ class Episode(Video, Playable):
self.duration = utils.cast(int, data.attrib.get('duration', NA))
self.grandparentArt = data.attrib.get('grandparentArt', NA)
self.grandparentKey = data.attrib.get('grandparentKey', NA)
self.grandparentRatingKey = data.attrib.get('grandparentRatingKey', NA)
self.grandparentRatingKey = utils.cast(int, data.attrib.get('grandparentRatingKey', NA))
self.grandparentTheme = data.attrib.get('grandparentTheme', NA)
self.grandparentThumb = data.attrib.get('grandparentThumb', NA)
self.grandparentTitle = data.attrib.get('grandparentTitle', NA)
self.guid = data.attrib.get('guid', NA)
self.index = data.attrib.get('index', NA)
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d')
self.originallyAvailableAt = utils.toDatetime(
data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d')
self.parentIndex = data.attrib.get('parentIndex', NA)
self.parentKey = data.attrib.get('parentKey', NA)
self.parentRatingKey = data.attrib.get('parentRatingKey', NA)
self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey', NA))
self.parentThumb = data.attrib.get('parentThumb', NA)
self.rating = utils.cast(float, data.attrib.get('rating', NA))
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
self.year = utils.cast(int, data.attrib.get('year', NA))
if self.isFullObject():
self.directors = [media.Director(self.server, e) for e in data if e.tag == media.Director.TYPE]
self.media = [media.Media(self.server, e, self.initpath, self) for e in data if e.tag == media.Media.TYPE]
self.writers = [media.Writer(self.server, e) for e in data if e.tag == media.Writer.TYPE]
self.videoStreams = utils.findStreams(self.media, 'videostream')
self.audioStreams = utils.findStreams(self.media, 'audiostream')
self.subtitleStreams = utils.findStreams(self.media, 'subtitlestream')
#if self.isFullObject():
self.directors = [media.Director(self.server, e)
for e in data if e.tag == media.Director.TYPE]
self.media = [media.Media(self.server, e, self.initpath, self)
for e in data if e.tag == media.Media.TYPE]
self.writers = [media.Writer(self.server, e)
for e in data if e.tag == media.Writer.TYPE]
self.videoStreams = utils.findStreams(self.media, 'videostream')
self.audioStreams = utils.findStreams(self.media, 'audiostream')
self.subtitleStreams = utils.findStreams(self.media, 'subtitlestream')
# data for active sessions and history
self.sessionKey = utils.cast(int, data.attrib.get('sessionKey', NA))
self.username = utils.findUsername(data)