mirror of
https://github.com/pkkid/python-plexapi
synced 2024-11-22 19:53:17 +00:00
parent
8686e6e5bb
commit
1075f65bb4
10 changed files with 890 additions and 220 deletions
181
plexapi/audio.py
181
plexapi/audio.py
|
@ -1,26 +1,58 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
PlexAPI Audio
|
||||
"""
|
||||
|
||||
from plexapi import media, utils
|
||||
from plexapi.utils import Playable, PlexPartialObject
|
||||
|
||||
NA = utils.NA
|
||||
|
||||
|
||||
class Audio(PlexPartialObject):
|
||||
"""Base class for audio.
|
||||
|
||||
Attributes:
|
||||
addedAt (int): int from epoch, datetime.datetime
|
||||
index (sting): 1
|
||||
key (str): Fx /library/metadata/102631
|
||||
lastViewedAt (datetime.datetime): parse int into datetime.datetime.
|
||||
librarySectionID (int):
|
||||
listType (str): audio
|
||||
ratingKey (int): Unique key to identify this item
|
||||
summary (str): Summery of the artist, track, album
|
||||
thumb (str): Url to thumb image
|
||||
title (str): Fx Aerosmith
|
||||
titleSort (str): Defaults title if None
|
||||
TYPE (str): overwritten by subclass
|
||||
type (string, NA): Description
|
||||
updatedAt (datatime.datetime): parse int to datetime.datetime
|
||||
viewCount (int): How many time has this item been played
|
||||
"""
|
||||
TYPE = None
|
||||
|
||||
def __init__(self, server, data, initpath):
|
||||
"""Used to set the attributes.
|
||||
|
||||
Args:
|
||||
server (Plexserver): PMS your connected to
|
||||
data (Element): XML reponse from PMS as Element
|
||||
normally built from server.query
|
||||
initpath (str): Fx /library/sections/7/all
|
||||
"""
|
||||
super(Audio, self).__init__(data, initpath, server)
|
||||
|
||||
def _loadData(self, data):
|
||||
"""Used to set the attributes.
|
||||
|
||||
Args:
|
||||
data (Element): XML reponse from PMS as Element
|
||||
normally built from server.query
|
||||
"""
|
||||
self.listType = 'audio'
|
||||
self.addedAt = utils.toDatetime(data.attrib.get('addedAt', NA))
|
||||
self.index = data.attrib.get('index', NA)
|
||||
self.key = data.attrib.get('key', 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)
|
||||
|
@ -31,55 +63,121 @@ class Audio(PlexPartialObject):
|
|||
|
||||
@property
|
||||
def thumbUrl(self):
|
||||
"""Return url to thumb image."""
|
||||
return self.server.url(self.thumb)
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh the metadata."""
|
||||
self.server.query('%s/refresh' % self.key, method=self.server.session.put)
|
||||
|
||||
def section(self):
|
||||
"""Library section."""
|
||||
return self.server.library.sectionByID(self.librarySectionID)
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
class Artist(Audio):
|
||||
"""Artist.
|
||||
|
||||
Attributes:
|
||||
art (str): /library/metadata/102631/art/1469310342
|
||||
countries (list): List of media.County fx [<Country:24200:United.States>]
|
||||
genres (list): List of media.Genre fx [<Genre:25555:Classic.Rock>]
|
||||
guid (str): Fx guid com.plexapp.agents.plexmusic://gracenote/artist/05517B8701668D28?lang=en
|
||||
key (str): Fx /library/metadata/102631
|
||||
location (str): Filepath
|
||||
similar (list): List of media.Similar fx [<Similar:25220:Guns.N'.Roses>]
|
||||
TYPE (str): artist
|
||||
"""
|
||||
|
||||
TYPE = 'artist'
|
||||
|
||||
def _loadData(self, data):
|
||||
"""Used to set the attributes.
|
||||
|
||||
Args:
|
||||
data (Element): XML reponse from PMS as Element
|
||||
normally built from server.query
|
||||
"""
|
||||
Audio._loadData(self, data)
|
||||
self.art = data.attrib.get('art', NA)
|
||||
self.guid = data.attrib.get('guid', NA)
|
||||
self.key = self.key.replace('/children', '') # FIX_BUG_50
|
||||
self.location = utils.findLocations(data, single=True)
|
||||
if self.isFullObject():
|
||||
self.countries = [media.Country(self.server, e) for e in data if e.tag == media.Country.TYPE]
|
||||
self.genres = [media.Genre(self.server, e) for e in data if e.tag == media.Genre.TYPE]
|
||||
self.similar = [media.Similar(self.server, e) for e in data if e.tag == media.Similar.TYPE]
|
||||
if self.isFullObject(): # check if this is needed
|
||||
self.countries = [media.Country(self.server, e)
|
||||
for e in data if e.tag == media.Country.TYPE]
|
||||
self.genres = [media.Genre(self.server, e)
|
||||
for e in data if e.tag == media.Genre.TYPE]
|
||||
self.similar = [media.Similar(self.server, e)
|
||||
for e in data if e.tag == media.Similar.TYPE]
|
||||
|
||||
def albums(self):
|
||||
"""Return a list of Albums by thus artist."""
|
||||
path = '%s/children' % self.key
|
||||
return utils.listItems(self.server, path, Album.TYPE)
|
||||
|
||||
def album(self, title):
|
||||
"""Return a album from this artist that match title."""
|
||||
path = '%s/children' % self.key
|
||||
return utils.findItem(self.server, path, title)
|
||||
|
||||
def tracks(self, watched=None):
|
||||
"""Return all tracks to this artist.
|
||||
|
||||
Args:
|
||||
watched(None, False, True): Default to None.
|
||||
|
||||
Returns:
|
||||
List: of Track
|
||||
"""
|
||||
path = '%s/allLeaves' % self.key
|
||||
return utils.listItems(self.server, path, watched=watched)
|
||||
|
||||
def track(self, title):
|
||||
"""Return a Track that matches title.
|
||||
|
||||
Args:
|
||||
title (str): Fx song name
|
||||
|
||||
Returns:
|
||||
Track:
|
||||
"""
|
||||
path = '%s/allLeaves' % self.key
|
||||
return utils.findItem(self.server, path, title)
|
||||
|
||||
def get(self, title):
|
||||
"""Alias. See track."""
|
||||
return self.track(title)
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
class Album(Audio):
|
||||
"""Album.
|
||||
|
||||
Attributes:
|
||||
art (str): Fx /library/metadata/102631/art/1469310342
|
||||
genres (list): List of media.Genre
|
||||
key (str): Fx /library/metadata/102632
|
||||
originallyAvailableAt (TYPE): Description
|
||||
parentKey (str): /library/metadata/102631
|
||||
parentRatingKey (int): Fx 1337
|
||||
parentThumb (TYPE): Relative url to parent thumb image
|
||||
parentTitle (str): Aerosmith
|
||||
studio (str):
|
||||
TYPE (str): album
|
||||
year (int): 1999
|
||||
"""
|
||||
|
||||
TYPE = 'album'
|
||||
|
||||
def _loadData(self, data):
|
||||
"""Used to set the attributes.
|
||||
|
||||
Args:
|
||||
data (Element): XML reponse from PMS as Element
|
||||
normally built from server.query
|
||||
"""
|
||||
Audio._loadData(self, data)
|
||||
self.art = data.attrib.get('art', NA)
|
||||
self.key = self.key.replace('/children', '') # FIX_BUG_50
|
||||
|
@ -94,31 +192,87 @@ class Album(Audio):
|
|||
self.genres = [media.Genre(self.server, e) for e in data if e.tag == media.Genre.TYPE]
|
||||
|
||||
def tracks(self, watched=None):
|
||||
"""Return all tracks to this album.
|
||||
|
||||
Args:
|
||||
watched(None, False, True): Default to None.
|
||||
|
||||
Returns:
|
||||
List: of Track
|
||||
"""
|
||||
path = '%s/children' % self.key
|
||||
return utils.listItems(self.server, path, watched=watched)
|
||||
|
||||
def track(self, title):
|
||||
"""Return a Track that matches title.
|
||||
|
||||
Args:
|
||||
title (str): Fx song name
|
||||
|
||||
Returns:
|
||||
Track:
|
||||
"""
|
||||
path = '%s/children' % self.key
|
||||
return utils.findItem(self.server, path, title)
|
||||
|
||||
def get(self, title):
|
||||
"""Alias. See track."""
|
||||
return self.track(title)
|
||||
|
||||
def artist(self):
|
||||
"""Return Artist of this album."""
|
||||
return utils.listItems(self.server, self.parentKey)[0]
|
||||
|
||||
def watched(self):
|
||||
"""Return Track that is lisson on."""
|
||||
return self.tracks(watched=True)
|
||||
|
||||
def unwatched(self):
|
||||
"""Return Track that is not lisson on."""
|
||||
return self.tracks(watched=False)
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
class Track(Audio, Playable):
|
||||
"""Track.
|
||||
|
||||
Attributes:
|
||||
art (str): Relative path fx /library/metadata/102631/art/1469310342
|
||||
chapterSource (TYPE): Description
|
||||
duration (TYPE): Description
|
||||
grandparentArt (str): Relative path
|
||||
grandparentKey (str): Relative path Fx /library/metadata/102631
|
||||
grandparentRatingKey (TYPE): Description
|
||||
grandparentThumb (str): Relative path to Artist thumb img
|
||||
grandparentTitle (str): Aerosmith
|
||||
guid (TYPE): Description
|
||||
media (list): List of media.Media
|
||||
moods (list): List of media.Moods
|
||||
originalTitle (str): Some track title
|
||||
parentIndex (int): 1
|
||||
parentKey (str): Relative path Fx /library/metadata/102632
|
||||
parentRatingKey (int): 1337
|
||||
parentThumb (str): Relative path to Album thumb
|
||||
parentTitle (str): Album title
|
||||
player (None): #TODO
|
||||
primaryExtraKey (TYPE): #TODO
|
||||
ratingCount (int): 10
|
||||
sessionKey (int): Description
|
||||
transcodeSession (None):
|
||||
TYPE (str): track
|
||||
username (str): username@mail.com
|
||||
viewOffset (int): 100
|
||||
year (int): 1999
|
||||
"""
|
||||
|
||||
TYPE = 'track'
|
||||
|
||||
def _loadData(self, data):
|
||||
"""Used to set the attributes
|
||||
|
||||
Args:
|
||||
data (Element): Usually built from server.query
|
||||
"""
|
||||
Audio._loadData(self, data)
|
||||
Playable._loadData(self, data)
|
||||
self.art = data.attrib.get('art', NA)
|
||||
|
@ -140,9 +294,11 @@ class Track(Audio, Playable):
|
|||
self.ratingCount = utils.cast(int, data.attrib.get('ratingCount', 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.moods = [media.Mood(self.server, e) for e in data if e.tag == media.Mood.TYPE]
|
||||
self.media = [media.Media(self.server, e, self.initpath, self) for e in data if e.tag == media.Media.TYPE]
|
||||
if self.isFullObject(): # check me
|
||||
self.moods = [media.Mood(self.server, e)
|
||||
for e in data if e.tag == media.Mood.TYPE]
|
||||
self.media = [media.Media(self.server, e, self.initpath, self)
|
||||
for e in data if e.tag == media.Media.TYPE]
|
||||
# data for active sessions and history
|
||||
self.sessionKey = utils.cast(int, data.attrib.get('sessionKey', NA))
|
||||
self.username = utils.findUsername(data)
|
||||
|
@ -151,10 +307,13 @@ class Track(Audio, Playable):
|
|||
|
||||
@property
|
||||
def thumbUrl(self):
|
||||
"""Return url to thumb image."""
|
||||
return self.server.url(self.parentThumb)
|
||||
|
||||
def album(self):
|
||||
"""Return this track's Album."""
|
||||
return utils.listItems(self.server, self.parentKey)[0]
|
||||
|
||||
def artist(self):
|
||||
"""Return this track's Artist."""
|
||||
return utils.listItems(self.server, self.grandparentKey)[0]
|
||||
|
|
|
@ -13,27 +13,27 @@ from xml.etree import ElementTree
|
|||
|
||||
|
||||
class PlexClient(object):
|
||||
"""Main class for interacting with a client
|
||||
"""Main class for interacting with a client.
|
||||
|
||||
Attributes:
|
||||
baseurl (string): http adress for the client
|
||||
baseurl (str): http adress for the client
|
||||
device (None): Description
|
||||
deviceClass (sting): pc, phone
|
||||
machineIdentifier (string): uuid fx 5471D9EA-1467-4051-9BE7-FCBDF490ACE3
|
||||
machineIdentifier (str): uuid fx 5471D9EA-1467-4051-9BE7-FCBDF490ACE3
|
||||
model (TYPE): Description
|
||||
platform (TYPE): Description
|
||||
platformVersion (TYPE): Description
|
||||
product (string): plex for ios
|
||||
protocol (string): plex
|
||||
product (str): plex for ios
|
||||
protocol (str): plex
|
||||
protocolCapabilities (list): List of what client can do
|
||||
protocolVersion (string): 1
|
||||
protocolVersion (str): 1
|
||||
server (plexapi.server.Plexserver): PMS your connected to
|
||||
session (None or requests.Session): Add your own session object to cache stuff
|
||||
state (None): Description
|
||||
title (string): fx Johns Iphone
|
||||
token (string): X-Plex-Token, using for authenication with PMS
|
||||
vendor (string): Description
|
||||
version (string): fx. 4.6
|
||||
title (str): fx Johns Iphone
|
||||
token (str): X-Plex-Token, using for authenication with PMS
|
||||
vendor (str): Description
|
||||
version (str): fx. 4.6
|
||||
"""
|
||||
|
||||
def __init__(self, baseurl, token=None, session=None, server=None, data=None):
|
||||
|
@ -115,7 +115,7 @@ class PlexClient(object):
|
|||
"""Used to fetch relative paths to pms.
|
||||
|
||||
Args:
|
||||
path (string): Relative path
|
||||
path (str): Relative path
|
||||
method (None, optional): requests.post etc
|
||||
headers (None, optional): Set headers manually
|
||||
**kwargs (TYPE): Passord to the http request used for filter, sorting.
|
||||
|
@ -141,7 +141,7 @@ class PlexClient(object):
|
|||
"""Send a command to the client
|
||||
|
||||
Args:
|
||||
command (string): See the commands listed below
|
||||
command (str): See the commands listed below
|
||||
proxy (None, optional): Description
|
||||
**params (dict): Description
|
||||
|
||||
|
@ -170,7 +170,7 @@ class PlexClient(object):
|
|||
"""Return a full url
|
||||
|
||||
Args:
|
||||
path (string): Relative path
|
||||
path (str): Relative path
|
||||
|
||||
Returns:
|
||||
string: full path to PMS
|
||||
|
@ -234,7 +234,7 @@ class PlexClient(object):
|
|||
"""Go to a media on the client.
|
||||
|
||||
Args:
|
||||
media (string): movie, music, photo
|
||||
media (str): movie, music, photo
|
||||
**params (TYPE): Description # todo
|
||||
|
||||
Raises:
|
||||
|
@ -261,7 +261,7 @@ class PlexClient(object):
|
|||
"""Pause playback
|
||||
|
||||
Args:
|
||||
mtype (string): music, photo, video
|
||||
mtype (str): music, photo, video
|
||||
"""
|
||||
self.sendCommand('playback/pause', type=mtype)
|
||||
|
||||
|
@ -269,7 +269,7 @@ class PlexClient(object):
|
|||
"""Start playback
|
||||
|
||||
Args:
|
||||
mtype (string): music, photo, video
|
||||
mtype (str): music, photo, video
|
||||
"""
|
||||
self.sendCommand('playback/play', type=mtype)
|
||||
|
||||
|
@ -346,7 +346,7 @@ class PlexClient(object):
|
|||
"""Stop playback
|
||||
|
||||
Args:
|
||||
mtype (string): video, music, photo
|
||||
mtype (str): video, music, photo
|
||||
|
||||
"""
|
||||
self.sendCommand('playback/stop', type=mtype)
|
||||
|
@ -383,7 +383,7 @@ class PlexClient(object):
|
|||
|
||||
Args:
|
||||
audioStreamID (TYPE): Description
|
||||
mtype (string): video, music, photo
|
||||
mtype (str): video, music, photo
|
||||
"""
|
||||
self.setStreams(audioStreamID=audioStreamID, mtype=mtype)
|
||||
|
||||
|
@ -392,7 +392,7 @@ class PlexClient(object):
|
|||
|
||||
Args:
|
||||
subtitleStreamID (TYPE): Description
|
||||
mtype (string): video, music, photo
|
||||
mtype (str): video, music, photo
|
||||
"""
|
||||
self.setStreams(subtitleStreamID=subtitleStreamID, mtype=mtype)
|
||||
|
||||
|
@ -401,7 +401,7 @@ class PlexClient(object):
|
|||
|
||||
Args:
|
||||
videoStreamID (TYPE): Description
|
||||
mtype (string): video, music, photo
|
||||
mtype (str): video, music, photo
|
||||
|
||||
"""
|
||||
self.setStreams(videoStreamID=videoStreamID, mtype=mtype)
|
||||
|
@ -410,7 +410,7 @@ class PlexClient(object):
|
|||
"""Start playback on a media item.
|
||||
|
||||
Args:
|
||||
media (string): movie, music, photo
|
||||
media (str): movie, music, photo
|
||||
**params (TYPE): Description
|
||||
|
||||
Raises:
|
||||
|
|
|
@ -1,22 +1,68 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
PlexAPI MyPlex
|
||||
"""
|
||||
import plexapi, requests
|
||||
import sys
|
||||
|
||||
if sys.version_info <= (3, 3):
|
||||
try:
|
||||
from xml.etree import cElementTree as ElementTree
|
||||
except ImportError:
|
||||
from xml.etree import ElementTree
|
||||
else:
|
||||
# py 3.3 and above selects the fastest automatically
|
||||
from xml.etree import ElementTree
|
||||
|
||||
from requests.status_codes import _codes as codes
|
||||
|
||||
import plexapi
|
||||
import requests
|
||||
from plexapi import TIMEOUT, log, utils
|
||||
from plexapi.exceptions import BadRequest, NotFound, Unauthorized
|
||||
from plexapi.client import PlexClient
|
||||
from plexapi.server import PlexServer
|
||||
from requests.status_codes import _codes as codes
|
||||
from xml.etree import ElementTree
|
||||
|
||||
|
||||
# Your personal MyPlex account and profile information
|
||||
class MyPlexAccount(object):
|
||||
"""Your personal MyPlex account and profile information
|
||||
|
||||
Attributes:
|
||||
authenticationToken (TYPE): Description
|
||||
BASEURL (str): Description
|
||||
certificateVersion (TYPE): Description
|
||||
cloudSyncDevice (TYPE): Description
|
||||
email (TYPE): Description
|
||||
entitlements (TYPE): Description
|
||||
guest (TYPE): Description
|
||||
home (TYPE): Description
|
||||
homeSize (TYPE): Description
|
||||
id (TYPE): Description
|
||||
locale (TYPE): Description
|
||||
mailing_list_status (TYPE): Description
|
||||
maxHomeSize (TYPE): Description
|
||||
queueEmail (TYPE): Description
|
||||
queueUid (TYPE): Description
|
||||
restricted (TYPE): Description
|
||||
roles (TYPE): Description
|
||||
scrobbleTypes (TYPE): Description
|
||||
secure (TYPE): Description
|
||||
SIGNIN (str): Description
|
||||
subscriptionActive (TYPE): Description
|
||||
subscriptionFeatures (TYPE): Description
|
||||
subscriptionPlan (TYPE): Description
|
||||
subscriptionStatus (TYPE): Description
|
||||
thumb (TYPE): Description
|
||||
title (TYPE): Description
|
||||
username (TYPE): Description
|
||||
uuid (TYPE): Description
|
||||
"""
|
||||
BASEURL = 'https://plex.tv/users/account'
|
||||
SIGNIN = 'https://my.plexapp.com/users/sign_in.xml'
|
||||
|
||||
def __init__(self, data, initpath=None):
|
||||
"""Sets the attrs.
|
||||
|
||||
Args:
|
||||
data (Element): XML response from PMS as a Element
|
||||
initpath (string, optional): relative path.
|
||||
"""
|
||||
self.authenticationToken = data.attrib.get('authenticationToken')
|
||||
self.certificateVersion = data.attrib.get('certificateVersion')
|
||||
self.cloudSyncDevice = data.attrib.get('cloudSyncDevice')
|
||||
|
@ -47,37 +93,92 @@ class MyPlexAccount(object):
|
|||
self.entitlements = None
|
||||
|
||||
def __repr__(self):
|
||||
"""Pretty print."""
|
||||
return '<%s:%s:%s>' % (self.__class__.__name__, self.id, self.username.encode('utf8'))
|
||||
|
||||
def devices(self):
|
||||
"""Return a all devices connected to the plex account.
|
||||
|
||||
Returns:
|
||||
list: of MyPlexDevice
|
||||
"""
|
||||
return _listItems(MyPlexDevice.BASEURL, self.authenticationToken, MyPlexDevice)
|
||||
|
||||
def device(self, name):
|
||||
"""Return a device wth a matching name.
|
||||
|
||||
Args:
|
||||
name (str): Name to match against.
|
||||
|
||||
Returns:
|
||||
class: MyPlexDevice
|
||||
"""
|
||||
return _findItem(self.devices(), name)
|
||||
|
||||
def resources(self):
|
||||
"""Resources.
|
||||
|
||||
Returns:
|
||||
List: of MyPlexResource
|
||||
"""
|
||||
return _listItems(MyPlexResource.BASEURL, self.authenticationToken, MyPlexResource)
|
||||
|
||||
def resource(self, name):
|
||||
"""Find resource ny name.
|
||||
|
||||
Args:
|
||||
name (str): to find
|
||||
|
||||
Returns:
|
||||
class: MyPlexResource
|
||||
"""
|
||||
return _findItem(self.resources(), name)
|
||||
|
||||
def users(self):
|
||||
"""List of users.
|
||||
|
||||
Returns:
|
||||
List: of MyPlexuser
|
||||
"""
|
||||
return _listItems(MyPlexUser.BASEURL, self.authenticationToken, MyPlexUser)
|
||||
|
||||
def user(self, email):
|
||||
"""Find a user by email.
|
||||
|
||||
Args:
|
||||
email (str): Username to match against.
|
||||
|
||||
Returns:
|
||||
class: User
|
||||
"""
|
||||
return _findItem(self.users(), email, ['username', 'email'])
|
||||
|
||||
@classmethod
|
||||
def signin(cls, username, password):
|
||||
"""Summary
|
||||
|
||||
Args:
|
||||
username (str): username
|
||||
password (str): password
|
||||
|
||||
Returns:
|
||||
class: MyPlexAccount
|
||||
|
||||
Raises:
|
||||
BadRequest: (HTTPCODE) http codename
|
||||
Unauthorized: (HTTPCODE) http codename
|
||||
"""
|
||||
if 'X-Plex-Token' in plexapi.BASE_HEADERS:
|
||||
del plexapi.BASE_HEADERS['X-Plex-Token']
|
||||
auth = (username, password)
|
||||
log.info('POST %s', cls.SIGNIN)
|
||||
response = requests.post(cls.SIGNIN, headers=plexapi.BASE_HEADERS, auth=auth, timeout=TIMEOUT)
|
||||
response = requests.post(
|
||||
cls.SIGNIN, headers=plexapi.BASE_HEADERS, auth=auth, timeout=TIMEOUT)
|
||||
if response.status_code != requests.codes.created:
|
||||
codename = codes.get(response.status_code)[0]
|
||||
if response.status_code == 401:
|
||||
raise Unauthorized('(%s) %s' % (response.status_code, codename))
|
||||
raise Unauthorized('(%s) %s' %
|
||||
(response.status_code, codename))
|
||||
raise BadRequest('(%s) %s' % (response.status_code, codename))
|
||||
data = ElementTree.fromstring(response.text.encode('utf8'))
|
||||
return cls(data, cls.SIGNIN)
|
||||
|
@ -86,10 +187,39 @@ class MyPlexAccount(object):
|
|||
# Not to be confused with the MyPlexAccount, this represents
|
||||
# non-signed in users such as friends and linked accounts.
|
||||
class MyPlexUser(object):
|
||||
"""Class to other users.
|
||||
|
||||
Attributes:
|
||||
allowCameraUpload (bool): True if this user can upload images
|
||||
allowChannels (bool): True if this user has access to channels
|
||||
allowSync (bool): True if this user can sync
|
||||
BASEURL (str): Description
|
||||
email (str): user@gmail.com
|
||||
filterAll (str): Description
|
||||
filterMovies (str): Description
|
||||
filterMusic (str): Description
|
||||
filterPhotos (str): Description
|
||||
filterTelevision (str): Description
|
||||
home (bool):
|
||||
id (int): 1337
|
||||
protected (False): Is this if ssl? check it
|
||||
recommendationsPlaylistId (str): Description
|
||||
restricted (str): fx 0
|
||||
thumb (str): Link to the users avatar
|
||||
title (str): Hellowlol
|
||||
username (str): Hellowlol
|
||||
"""
|
||||
BASEURL = 'https://plex.tv/api/users/'
|
||||
|
||||
def __init__(self, data, initpath=None):
|
||||
self.allowCameraUpload = utils.cast(bool, data.attrib.get('allowCameraUpload'))
|
||||
"""Summary
|
||||
|
||||
Args:
|
||||
data (Element): XML repsonse as Element
|
||||
initpath (None, optional): Relative url str
|
||||
"""
|
||||
self.allowCameraUpload = utils.cast(
|
||||
bool, data.attrib.get('allowCameraUpload'))
|
||||
self.allowChannels = utils.cast(bool, data.attrib.get('allowChannels'))
|
||||
self.allowSync = utils.cast(bool, data.attrib.get('allowSync'))
|
||||
self.email = data.attrib.get('email')
|
||||
|
@ -101,20 +231,48 @@ class MyPlexUser(object):
|
|||
self.home = utils.cast(bool, data.attrib.get('home'))
|
||||
self.id = utils.cast(int, data.attrib.get('id'))
|
||||
self.protected = utils.cast(bool, data.attrib.get('protected'))
|
||||
self.recommendationsPlaylistId = data.attrib.get('recommendationsPlaylistId')
|
||||
self.recommendationsPlaylistId = data.attrib.get(
|
||||
'recommendationsPlaylistId')
|
||||
self.restricted = data.attrib.get('restricted')
|
||||
self.thumb = data.attrib.get('thumb')
|
||||
self.title = data.attrib.get('title')
|
||||
self.username = data.attrib.get('username')
|
||||
|
||||
def __repr__(self):
|
||||
"""Pretty repr."""
|
||||
return '<%s:%s:%s>' % (self.__class__.__name__, self.id, self.username)
|
||||
|
||||
|
||||
class MyPlexResource(object):
|
||||
"""Summary
|
||||
|
||||
Attributes:
|
||||
accessToken (str): This resource accesstoken.
|
||||
BASEURL (TYPE): Description
|
||||
clientIdentifier (str): 1f2fe128794fd...
|
||||
connections (list): of ResourceConnection
|
||||
createdAt (datetime): Description
|
||||
device (str): pc
|
||||
home (None): Dunno wtf this can me
|
||||
lastSeenAt (datetime): Description
|
||||
name (str): Pretty name fx S-PC
|
||||
owned (bool): True if this is your own.
|
||||
platform (str): Windows
|
||||
platformVersion (str): fx. 6.1 (Build 7601)
|
||||
presence (bool): True if online
|
||||
product (str): Plex Media Server
|
||||
productVersion (str): 1.3.3.3148-b38628e
|
||||
provides (str): fx server
|
||||
synced (bool): Description
|
||||
"""
|
||||
BASEURL = 'https://plex.tv/api/resources?includeHttps=1'
|
||||
|
||||
def __init__(self, data):
|
||||
"""Summary
|
||||
|
||||
Args:
|
||||
data (Element): XML response as Element
|
||||
"""
|
||||
self.name = data.attrib.get('name')
|
||||
self.accessToken = data.attrib.get('accessToken')
|
||||
self.product = data.attrib.get('product')
|
||||
|
@ -130,16 +288,31 @@ class MyPlexResource(object):
|
|||
self.home = utils.cast(bool, data.attrib.get('home'))
|
||||
self.synced = utils.cast(bool, data.attrib.get('synced'))
|
||||
self.presence = utils.cast(bool, data.attrib.get('presence'))
|
||||
self.connections = [ResourceConnection(elem) for elem in data if elem.tag == 'Connection']
|
||||
self.connections = [ResourceConnection(
|
||||
elem) for elem in data if elem.tag == 'Connection']
|
||||
|
||||
def __repr__(self):
|
||||
"""Pretty repr."""
|
||||
return '<%s:%s>' % (self.__class__.__name__, self.name.encode('utf8'))
|
||||
|
||||
def connect(self, ssl=None):
|
||||
"""Connect.
|
||||
|
||||
Args:
|
||||
ssl (None, optional): Use ssl.
|
||||
|
||||
Returns:
|
||||
class: Plexserver
|
||||
|
||||
Raises:
|
||||
NotFound: Unable to connect to resource: name
|
||||
"""
|
||||
|
||||
# Sort connections from (https, local) to (http, remote)
|
||||
# Only check non-local connections unless we own the resource
|
||||
forcelocal = lambda c: self.owned or c.local
|
||||
connections = sorted(self.connections, key=lambda c:c.local, reverse=True)
|
||||
connections = sorted(
|
||||
self.connections, key=lambda c: c.local, reverse=True)
|
||||
https = [c.uri for c in self.connections if forcelocal(c)]
|
||||
http = [c.httpuri for c in self.connections if forcelocal(c)]
|
||||
connections = https + http
|
||||
|
@ -148,26 +321,56 @@ class MyPlexResource(object):
|
|||
listargs = [[c] for c in connections]
|
||||
results = utils.threaded(self._connect, listargs)
|
||||
# At this point we have a list of result tuples containing (url, token, PlexServer)
|
||||
# or (url, token, None) in the case a connection could not be established.
|
||||
# or (url, token, None) in the case a connection could not be
|
||||
# established.
|
||||
for url, token, result in results:
|
||||
okerr = 'OK' if result else 'ERR'
|
||||
log.info('Testing resource connection: %s?X-Plex-Token=%s %s', url, token, okerr)
|
||||
results = list(filter(None, [r[2] for r in results if r]))
|
||||
log.info(
|
||||
'Testing resource connection: %s?X-Plex-Token=%s %s', url, token, okerr)
|
||||
|
||||
results = [r[2] for r in results if r and r is not None]
|
||||
if not results:
|
||||
raise NotFound('Unable to connect to resource: %s' % self.name)
|
||||
log.info('Connecting to server: %s?X-Plex-Token=%s', results[0].baseurl, results[0].token)
|
||||
log.info('Connecting to server: %s?X-Plex-Token=%s',
|
||||
results[0].baseurl, results[0].token)
|
||||
|
||||
return results[0]
|
||||
|
||||
def _connect(self, url, results, i):
|
||||
"""Connect.
|
||||
|
||||
Args:
|
||||
url (str): url to the resource
|
||||
results (TYPE): Description
|
||||
i (TYPE): Description
|
||||
|
||||
Returns:
|
||||
TYPE: Description
|
||||
"""
|
||||
try:
|
||||
results[i] = (url, self.accessToken, PlexServer(url, self.accessToken))
|
||||
results[i] = (url, self.accessToken,
|
||||
PlexServer(url, self.accessToken))
|
||||
except NotFound:
|
||||
results[i] = (url, self.accessToken, None)
|
||||
|
||||
|
||||
class ResourceConnection(object):
|
||||
"""ResourceConnection.
|
||||
|
||||
Attributes:
|
||||
address (str): Local ip adress
|
||||
httpuri (str): Full local address
|
||||
local (bool): True if local
|
||||
port (int): 32400
|
||||
protocol (str): http or https
|
||||
uri (str): External adress
|
||||
"""
|
||||
def __init__(self, data):
|
||||
"""Set attrs.
|
||||
|
||||
Args:
|
||||
data (Element): XML response as Element from PMS.
|
||||
"""
|
||||
self.protocol = data.attrib.get('protocol')
|
||||
self.address = data.attrib.get('address')
|
||||
self.port = utils.cast(int, data.attrib.get('port'))
|
||||
|
@ -176,13 +379,42 @@ class ResourceConnection(object):
|
|||
self.httpuri = 'http://%s:%s' % (self.address, self.port)
|
||||
|
||||
def __repr__(self):
|
||||
"""Pretty repr."""
|
||||
return '<%s:%s>' % (self.__class__.__name__, self.uri.encode('utf8'))
|
||||
|
||||
|
||||
class MyPlexDevice(object):
|
||||
"""Device connected.
|
||||
|
||||
Attributes:
|
||||
BASEURL (str): Plex.tv XML device url
|
||||
clientIdentifier (str): 0x685d43d...
|
||||
connections (list):
|
||||
device (str): fx Windows
|
||||
id (str): 123
|
||||
model (str):
|
||||
name (str): fx Computername
|
||||
platform (str): Windows
|
||||
platformVersion (str): Fx 8
|
||||
product (str): Fx PlexAPI
|
||||
productVersion (string): 2.0.2
|
||||
provides (str): fx controller
|
||||
publicAddress (str): Public ip address
|
||||
screenDensity (str): Description
|
||||
screenResolution (str): Description
|
||||
token (str): Auth token
|
||||
vendor (str): Description
|
||||
version (str): fx 2.0.2
|
||||
"""
|
||||
|
||||
BASEURL = 'https://plex.tv/devices.xml'
|
||||
|
||||
def __init__(self, data):
|
||||
"""Set attrs
|
||||
|
||||
Args:
|
||||
data (Element): XML response as Element from PMS
|
||||
"""
|
||||
self.name = data.attrib.get('name')
|
||||
self.publicAddress = data.attrib.get('publicAddress')
|
||||
self.product = data.attrib.get('product')
|
||||
|
@ -199,36 +431,75 @@ class MyPlexDevice(object):
|
|||
self.token = data.attrib.get('token')
|
||||
self.screenResolution = data.attrib.get('screenResolution')
|
||||
self.screenDensity = data.attrib.get('screenDensity')
|
||||
self.connections = [connection.attrib.get('uri') for connection in data.iter('Connection')]
|
||||
self.connections = [connection.attrib.get(
|
||||
'uri') for connection in data.iter('Connection')]
|
||||
|
||||
def __repr__(self):
|
||||
"""Pretty repr."""
|
||||
return '<%s:%s:%s>' % (self.__class__.__name__, self.name.encode('utf8'), self.product.encode('utf8'))
|
||||
|
||||
def connect(self, ssl=None):
|
||||
"""Connect to the first server.
|
||||
|
||||
Args:
|
||||
ssl (None, optional): Use SSL?
|
||||
|
||||
Returns:
|
||||
TYPE: Plexserver
|
||||
|
||||
Raises:
|
||||
NotFound: Unable to connect to resource: name
|
||||
"""
|
||||
# Try connecting to all known resource connections in parellel, but
|
||||
# only return the first server (in order) that provides a response.
|
||||
listargs = [[c] for c in self.connections]
|
||||
results = utils.threaded(self._connect, listargs)
|
||||
# At this point we have a list of result tuples containing (url, token, PlexServer)
|
||||
# or (url, token, None) in the case a connection could not be established.
|
||||
# or (url, token, None) in the case a connection could not be
|
||||
# established.
|
||||
for url, token, result in results:
|
||||
okerr = 'OK' if result else 'ERR'
|
||||
log.info('Testing device connection: %s?X-Plex-Token=%s %s', url, token, okerr)
|
||||
results = list(filter(None, [r[2] for r in results if r]))
|
||||
log.info('Testing device connection: %s?X-Plex-Token=%s %s',
|
||||
url, token, okerr)
|
||||
results = [r[2] for r in results if r and r[2] is not None]
|
||||
if not results:
|
||||
raise NotFound('Unable to connect to resource: %s' % self.name)
|
||||
log.info('Connecting to server: %s?X-Plex-Token=%s', results[0].baseurl, results[0].token)
|
||||
log.info('Connecting to server: %s?X-Plex-Token=%s',
|
||||
results[0].baseurl, results[0].token)
|
||||
|
||||
return results[0]
|
||||
|
||||
def _connect(self, url, results, i):
|
||||
"""Summary
|
||||
|
||||
Args:
|
||||
url (TYPE): Description
|
||||
results (TYPE): Description
|
||||
i (TYPE): Description
|
||||
|
||||
Returns:
|
||||
TYPE: Description
|
||||
"""
|
||||
try:
|
||||
results[i] = (url, self.token, PlexClient(url, self.token))
|
||||
except NotFound as err:
|
||||
print(err)
|
||||
results[i] = (url, self.token, None)
|
||||
|
||||
|
||||
def _findItem(items, value, attrs=None):
|
||||
"""Simple helper to find something using attrs
|
||||
|
||||
Args:
|
||||
items (cls): list of Object to get the attrs from
|
||||
value (str): value to match against
|
||||
attrs (None, optional): attr to match against value.
|
||||
|
||||
Returns:
|
||||
TYPE: Description
|
||||
|
||||
Raises:
|
||||
NotFound: Description
|
||||
"""
|
||||
attrs = attrs or ['name']
|
||||
for item in items:
|
||||
for attr in attrs:
|
||||
|
@ -238,6 +509,16 @@ def _findItem(items, value, attrs=None):
|
|||
|
||||
|
||||
def _listItems(url, token, cls):
|
||||
"""Helper that builds list of classes from a XML response.
|
||||
|
||||
Args:
|
||||
url (str): Description
|
||||
token (str): Description
|
||||
cls (class): Class to initate
|
||||
|
||||
Returns:
|
||||
List: of classes
|
||||
"""
|
||||
headers = plexapi.BASE_HEADERS
|
||||
headers['X-Plex-Token'] = token
|
||||
log.info('GET %s?X-Plex-Token=%s', url, token)
|
||||
|
|
106
plexapi/photo.py
106
plexapi/photo.py
|
@ -1,6 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
PlexPhoto
|
||||
|
||||
Attributes:
|
||||
NA (TYPE): Description
|
||||
"""
|
||||
from plexapi import media, utils
|
||||
from plexapi.utils import PlexPartialObject
|
||||
|
@ -9,12 +12,46 @@ NA = utils.NA
|
|||
|
||||
@utils.register_libtype
|
||||
class Photoalbum(PlexPartialObject):
|
||||
"""Summary
|
||||
|
||||
Attributes:
|
||||
addedAt (TYPE): Description
|
||||
art (TYPE): Description
|
||||
composite (TYPE): Description
|
||||
guid (TYPE): Description
|
||||
index (TYPE): Description
|
||||
key (TYPE): Description
|
||||
librarySectionID (TYPE): Description
|
||||
listType (str): Description
|
||||
ratingKey (TYPE): Description
|
||||
summary (TYPE): Description
|
||||
thumb (TYPE): Description
|
||||
title (TYPE): Description
|
||||
TYPE (str): Description
|
||||
type (TYPE): Description
|
||||
updatedAt (TYPE): Description
|
||||
"""
|
||||
TYPE = 'photoalbum'
|
||||
|
||||
def __init__(self, server, data, initpath):
|
||||
"""Summary
|
||||
|
||||
Args:
|
||||
server (TYPE): Description
|
||||
data (TYPE): Description
|
||||
initpath (TYPE): Description
|
||||
"""
|
||||
super(Photoalbum, self).__init__(data, initpath, server)
|
||||
|
||||
def _loadData(self, data):
|
||||
"""Summary
|
||||
|
||||
Args:
|
||||
data (TYPE): Description
|
||||
|
||||
Returns:
|
||||
TYPE: Description
|
||||
"""
|
||||
self.listType = 'photo'
|
||||
self.addedAt = utils.toDatetime(data.attrib.get('addedAt', NA))
|
||||
self.art = data.attrib.get('art', NA)
|
||||
|
@ -31,30 +68,84 @@ class Photoalbum(PlexPartialObject):
|
|||
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt', NA))
|
||||
|
||||
def photos(self):
|
||||
"""Summary
|
||||
|
||||
Returns:
|
||||
TYPE: Description
|
||||
"""
|
||||
path = '/library/metadata/%s/children' % self.ratingKey
|
||||
return utils.listItems(self.server, path, Photo.TYPE)
|
||||
|
||||
def photo(self, title):
|
||||
"""Summary
|
||||
|
||||
Args:
|
||||
title (TYPE): Description
|
||||
|
||||
Returns:
|
||||
TYPE: Description
|
||||
"""
|
||||
path = '/library/metadata/%s/children' % self.ratingKey
|
||||
return utils.findItem(self.server, path, title)
|
||||
|
||||
def section(self):
|
||||
"""Summary
|
||||
|
||||
Returns:
|
||||
TYPE: Description
|
||||
"""
|
||||
return self.server.library.sectionByID(self.librarySectionID)
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
class Photo(PlexPartialObject):
|
||||
"""Summary
|
||||
|
||||
Attributes:
|
||||
addedAt (TYPE): Description
|
||||
index (TYPE): Description
|
||||
key (TYPE): Description
|
||||
listType (str): Description
|
||||
media (TYPE): Description
|
||||
originallyAvailableAt (TYPE): Description
|
||||
parentKey (TYPE): Description
|
||||
parentRatingKey (TYPE): Description
|
||||
ratingKey (TYPE): Description
|
||||
summary (TYPE): Description
|
||||
thumb (TYPE): Description
|
||||
title (TYPE): Description
|
||||
TYPE (str): Description
|
||||
type (TYPE): Description
|
||||
updatedAt (TYPE): Description
|
||||
year (TYPE): Description
|
||||
"""
|
||||
TYPE = 'photo'
|
||||
|
||||
def __init__(self, server, data, initpath):
|
||||
"""Summary
|
||||
|
||||
Args:
|
||||
server (TYPE): Description
|
||||
data (TYPE): Description
|
||||
initpath (TYPE): Description
|
||||
"""
|
||||
super(Photo, self).__init__(data, initpath, server)
|
||||
|
||||
def _loadData(self, data):
|
||||
"""Summary
|
||||
|
||||
Args:
|
||||
data (TYPE): Description
|
||||
|
||||
Returns:
|
||||
TYPE: Description
|
||||
"""
|
||||
self.listType = 'photo'
|
||||
self.addedAt = utils.toDatetime(data.attrib.get('addedAt', NA))
|
||||
self.index = utils.cast(int, data.attrib.get('index', NA))
|
||||
self.key = data.attrib.get('key', 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.parentKey = data.attrib.get('parentKey', NA)
|
||||
self.parentRatingKey = data.attrib.get('parentRatingKey', NA)
|
||||
self.ratingKey = data.attrib.get('ratingKey', NA)
|
||||
|
@ -65,10 +156,21 @@ class Photo(PlexPartialObject):
|
|||
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt', NA))
|
||||
self.year = utils.cast(int, data.attrib.get('year', NA))
|
||||
if self.isFullObject():
|
||||
self.media = [media.Media(self.server, e, self.initpath, self) for e in data if e.tag == media.Media.TYPE]
|
||||
self.media = [media.Media(self.server, e, self.initpath, self)
|
||||
for e in data if e.tag == media.Media.TYPE]
|
||||
|
||||
def photoalbum(self):
|
||||
"""Summary
|
||||
|
||||
Returns:
|
||||
TYPE: Description
|
||||
"""
|
||||
return utils.listItems(self.server, self.parentKey)[0]
|
||||
|
||||
def section(self):
|
||||
"""Summary
|
||||
|
||||
Returns:
|
||||
TYPE: Description
|
||||
"""
|
||||
return self.server.library.sectionByID(self.photoalbum().librarySectionID)
|
||||
|
|
|
@ -14,9 +14,22 @@ class Playlist(PlexPartialObject, Playable):
|
|||
TYPE = 'playlist'
|
||||
|
||||
def __init__(self, server, data, initpath):
|
||||
"""Playlist stuff.
|
||||
|
||||
Args:
|
||||
server (Plexserver): The PMS server your connected to
|
||||
data (Element): Element built from server.query
|
||||
initpath (str): Relativ path fx /library/sections/1/all
|
||||
|
||||
"""
|
||||
super(Playlist, self).__init__(data, initpath, server)
|
||||
|
||||
def _loadData(self, data):
|
||||
"""Used to set the attributes
|
||||
|
||||
Args:
|
||||
data (Element): Usually built from server.query
|
||||
"""
|
||||
Playable._loadData(self, data)
|
||||
self.addedAt = toDatetime(data.attrib.get('addedAt', NA))
|
||||
self.composite = data.attrib.get('composite', NA) # url to thumbnail
|
||||
|
@ -27,7 +40,7 @@ class Playlist(PlexPartialObject, Playable):
|
|||
self.key = self.key.replace('/items', '') if self.key else self.key # FIX_BUG_50
|
||||
self.leafCount = cast(int, data.attrib.get('leafCount', NA))
|
||||
self.playlistType = data.attrib.get('playlistType', NA)
|
||||
self.ratingKey = data.attrib.get('ratingKey', NA)
|
||||
self.ratingKey = cast(int, data.attrib.get('ratingKey', NA))
|
||||
self.smart = cast(bool, data.attrib.get('smart', NA))
|
||||
self.summary = data.attrib.get('summary', NA)
|
||||
self.title = data.attrib.get('title', NA)
|
||||
|
@ -35,10 +48,12 @@ class Playlist(PlexPartialObject, Playable):
|
|||
self.updatedAt = toDatetime(data.attrib.get('updatedAt', NA))
|
||||
|
||||
def items(self):
|
||||
"""Return all items in the playlist."""
|
||||
path = '%s/items' % self.key
|
||||
return utils.listItems(self.server, path)
|
||||
|
||||
def addItems(self, items):
|
||||
"""Add items to a playlist."""
|
||||
if not isinstance(items, (list, tuple)):
|
||||
items = [items]
|
||||
ratingKeys = []
|
||||
|
@ -54,23 +69,29 @@ class Playlist(PlexPartialObject, Playable):
|
|||
return self.server.query(path, method=self.server.session.put)
|
||||
|
||||
def removeItem(self, item):
|
||||
"""Remove a file from a playlist."""
|
||||
path = '%s/items/%s' % (self.key, item.playlistItemID)
|
||||
return self.server.query(path, method=self.server.session.delete)
|
||||
|
||||
def moveItem(self, item, after=None):
|
||||
"""Move a to a new position in playlist."""
|
||||
path = '%s/items/%s/move' % (self.key, item.playlistItemID)
|
||||
if after: path += '?after=%s' % after.playlistItemID
|
||||
if after:
|
||||
path += '?after=%s' % after.playlistItemID
|
||||
return self.server.query(path, method=self.server.session.put)
|
||||
|
||||
def edit(self, title=None, summary=None):
|
||||
"""Edit playlist."""
|
||||
path = '/library/metadata/%s%s' % (self.ratingKey, utils.joinArgs({'title':title, 'summary':summary}))
|
||||
return self.server.query(path, method=self.server.session.put)
|
||||
|
||||
def delete(self):
|
||||
"""Delete playlist."""
|
||||
return self.server.query(self.key, method=self.server.session.delete)
|
||||
|
||||
@classmethod
|
||||
def create(cls, server, title, items):
|
||||
"""Create a playlist."""
|
||||
if not isinstance(items, (list, tuple)):
|
||||
items = [items]
|
||||
ratingKeys = []
|
||||
|
|
|
@ -1,28 +1,64 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
PlexAPI Play PlayQueues
|
||||
"""
|
||||
import plexapi, requests
|
||||
|
||||
|
||||
import plexapi
|
||||
import requests
|
||||
from plexapi import utils
|
||||
|
||||
|
||||
class PlayQueue(object):
|
||||
"""Summary
|
||||
|
||||
Attributes:
|
||||
identifier (TYPE): Description
|
||||
initpath (TYPE): Description
|
||||
items (TYPE): Description
|
||||
mediaTagPrefix (TYPE): Description
|
||||
mediaTagVersion (TYPE): Description
|
||||
playQueueID (TYPE): Description
|
||||
playQueueSelectedItemID (TYPE): Description
|
||||
playQueueSelectedItemOffset (TYPE): Description
|
||||
playQueueTotalCount (TYPE): Description
|
||||
playQueueVersion (TYPE): Description
|
||||
server (TYPE): Description
|
||||
"""
|
||||
def __init__(self, server, data, initpath):
|
||||
"""Summary
|
||||
|
||||
Args:
|
||||
server (TYPE): Description
|
||||
data (TYPE): Description
|
||||
initpath (TYPE): Description
|
||||
"""
|
||||
self.server = server
|
||||
self.initpath = initpath
|
||||
self.identifier = data.attrib.get('identifier')
|
||||
self.mediaTagPrefix = data.attrib.get('mediaTagPrefix')
|
||||
self.mediaTagVersion = data.attrib.get('mediaTagVersion')
|
||||
self.playQueueID = data.attrib.get('playQueueID')
|
||||
self.playQueueSelectedItemID = data.attrib.get('playQueueSelectedItemID')
|
||||
self.playQueueSelectedItemOffset = data.attrib.get('playQueueSelectedItemOffset')
|
||||
self.playQueueSelectedItemID = data.attrib.get(
|
||||
'playQueueSelectedItemID')
|
||||
self.playQueueSelectedItemOffset = data.attrib.get(
|
||||
'playQueueSelectedItemOffset')
|
||||
self.playQueueTotalCount = data.attrib.get('playQueueTotalCount')
|
||||
self.playQueueVersion = data.attrib.get('playQueueVersion')
|
||||
self.items = [utils.buildItem(server, elem, initpath) for elem in data]
|
||||
|
||||
@classmethod
|
||||
def create(cls, server, item, shuffle=0, repeat=0, includeChapters=1, includeRelated=1):
|
||||
"""Summary
|
||||
|
||||
Args:
|
||||
server (TYPE): Description
|
||||
item (TYPE): Description
|
||||
shuffle (int, optional): Description
|
||||
repeat (int, optional): Description
|
||||
includeChapters (int, optional): Description
|
||||
includeRelated (int, optional): Description
|
||||
|
||||
Returns:
|
||||
TYPE: Description
|
||||
"""
|
||||
args = {}
|
||||
args['includeChapters'] = includeChapters
|
||||
args['includeRelated'] = includeRelated
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
|
||||
"""
|
||||
PlexServer
|
||||
"""
|
||||
|
||||
from xml.etree import ElementTree
|
||||
if sys.version_info <= (3, 3):
|
||||
try:
|
||||
from xml.etree import cElementTree as ElementTree
|
||||
except ImportError:
|
||||
from xml.etree import ElementTree
|
||||
else:
|
||||
# py 3.3 and above selects the fastest automatically
|
||||
from xml.etree import ElementTree
|
||||
|
||||
import requests
|
||||
from requests.status_codes import _codes as codes
|
||||
|
@ -32,29 +36,29 @@ class PlexServer(object):
|
|||
See test/example.py for more examples
|
||||
|
||||
Attributes:
|
||||
baseurl (string): Base url for PMS
|
||||
friendlyName (string): Pretty name for PMS
|
||||
machineIdentifier (string): uuid for PMS
|
||||
myPlex (TYPE): Description
|
||||
myPlexMappingState (TYPE): Description
|
||||
myPlexSigninState (TYPE): Description
|
||||
myPlexSubscription (TYPE): Description
|
||||
myPlexUsername (string): Description
|
||||
platform (string): Description
|
||||
platformVersion (string): Description
|
||||
baseurl (str): Base url for PMS. Fx http://10.0.0.97:32400
|
||||
friendlyName (str): Pretty name for PMS fx s-PC
|
||||
machineIdentifier (str): uuid for PMS
|
||||
myPlex (bool): Description
|
||||
myPlexMappingState (str): fx mapped
|
||||
myPlexSigninState (str): fx ok
|
||||
myPlexSubscription (str): 1
|
||||
myPlexUsername (str): username@email.com
|
||||
platform (str): The platform PMS is running on.
|
||||
platformVersion (str): fx 6.1 (Build 7601)
|
||||
session (requests.Session, optinal): Add your own session object for caching
|
||||
token (string): X-Plex-Token, using for authenication with PMS
|
||||
token (str): X-Plex-Token, using for authenication with PMS
|
||||
transcoderActiveVideoSessions (int): How any active video sessions
|
||||
updatedAt (int): Last updated at
|
||||
version (TYPE): Description
|
||||
updatedAt (int): Last updated at as epoch
|
||||
version (str): fx 1.3.2.3112-1751929
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, baseurl='http://localhost:32400', token=None, session=None):
|
||||
"""
|
||||
Args:
|
||||
baseurl (string): Base url for PMS
|
||||
token (string): X-Plex-Token, using for authenication with PMS
|
||||
baseurl (str): Base url for PMS
|
||||
token (str): X-Plex-Token, using for authenication with PMS
|
||||
session (requests.Session, optional): Use your own session object if you want
|
||||
to cache the http responses from PMS
|
||||
"""
|
||||
|
@ -96,11 +100,12 @@ class PlexServer(object):
|
|||
return self._library
|
||||
|
||||
def account(self):
|
||||
"""Returns Account."""
|
||||
data = self.query('/myplex/account')
|
||||
return Account(self, data)
|
||||
|
||||
def clients(self):
|
||||
"""Query PMS for all clients connected to PMS
|
||||
"""Query PMS for all clients connected to PMS.
|
||||
|
||||
Returns:
|
||||
list: of Plexclient connnected to PMS
|
||||
|
@ -114,13 +119,13 @@ class PlexServer(object):
|
|||
return items
|
||||
|
||||
def client(self, name):
|
||||
"""Querys PMS for all clients connected to PMS
|
||||
"""Querys PMS for all clients connected to PMS.
|
||||
|
||||
Returns:
|
||||
Plexclient
|
||||
|
||||
Args:
|
||||
name (string): client title, John's Iphone
|
||||
name (str): client title, John's Iphone
|
||||
|
||||
Raises:
|
||||
NotFound: Unknown client name Betty
|
||||
|
@ -134,7 +139,7 @@ class PlexServer(object):
|
|||
raise NotFound('Unknown client name: %s' % name)
|
||||
|
||||
def createPlaylist(self, title, items):
|
||||
"""Create a playlist
|
||||
"""Create a playlist.
|
||||
|
||||
Returns:
|
||||
Playlist
|
||||
|
@ -165,10 +170,10 @@ class PlexServer(object):
|
|||
"""Returns a playlist with a given name or raise NotFound.
|
||||
|
||||
Args:
|
||||
title (string): title of the playlist
|
||||
title (str): title of the playlist
|
||||
|
||||
Raises:
|
||||
NotFound: Description
|
||||
NotFound: Invalid playlist title: title
|
||||
"""
|
||||
for item in self.playlists():
|
||||
if item.title == title:
|
||||
|
@ -178,16 +183,16 @@ class PlexServer(object):
|
|||
def query(self, path, method=None, headers=None, **kwargs):
|
||||
"""Main method used to handle http connection to PMS.
|
||||
encodes the response to utf-8 and parses the xml returned
|
||||
from PMS
|
||||
from PMS into a Element
|
||||
|
||||
Args:
|
||||
path (sting): relative path to PMS, fx /search?query=HELLO
|
||||
method (None, optional): requests.method, fx requests.put
|
||||
path (str): relative path to PMS, fx /search?query=HELLO
|
||||
method (None, optional): requests.method, requests.put
|
||||
headers (None, optional): Headers that will be passed to PMS
|
||||
**kwargs (dict): Used for filter and sorting.
|
||||
|
||||
Raises:
|
||||
BadRequest: Description
|
||||
BadRequest: fx (404) Not Found
|
||||
|
||||
Returns:
|
||||
xml.etree.ElementTree.Element or None
|
||||
|
@ -209,8 +214,8 @@ class PlexServer(object):
|
|||
"""Searching within a library section is much more powerful.
|
||||
|
||||
Args:
|
||||
query (string): Search string
|
||||
mediatype (string, optional): Limit your search to a media type.
|
||||
query (str): Search str
|
||||
mediatype (str, optional): Limit your search to a media type.
|
||||
|
||||
Returns:
|
||||
List
|
||||
|
@ -233,28 +238,32 @@ class PlexServer(object):
|
|||
|
||||
|
||||
class Account(object):
|
||||
"""This is the locally cached MyPlex account information. The properties provided don't match
|
||||
the myplex.MyPlexAccount object very well. I believe this is here because access to myplex
|
||||
is not required to get basic plex information.
|
||||
"""This is the locally cached MyPlex account information.
|
||||
The properties provided don't matchthe myplex.MyPlexAccount object very well.
|
||||
I believe this is here because access to myplexis not required
|
||||
to get basic plex information.
|
||||
|
||||
Attributes:
|
||||
authToken (sting): X-Plex-Token, using for authenication with PMS
|
||||
mappingError (TYPE): Description
|
||||
mappingErrorMessage (TYPE): Description
|
||||
mappingError (str):
|
||||
mappingErrorMessage (None, str): Description
|
||||
mappingState (TYPE): Description
|
||||
privateAddress (TYPE): Description
|
||||
privatePort (TYPE): Description
|
||||
publicAddress (TYPE): Description
|
||||
publicPort (TYPE): Description
|
||||
signInState (TYPE): Description
|
||||
subscriptionActive (TYPE): Description
|
||||
subscriptionFeatures (TYPE): Description
|
||||
subscriptionState (TYPE): Description
|
||||
username (TYPE): Description
|
||||
privateAddress (str): Local ip
|
||||
privatePort (str): Local port
|
||||
publicAddress (str): Public ip
|
||||
publicPort (str): Public port
|
||||
signInState (str): ok
|
||||
subscriptionActive (str): is returned as it
|
||||
subscriptionFeatures (str): What feature your account has access to.
|
||||
Fx: camera_upload,cloudsync,content_filter
|
||||
subscriptionState (str): Active
|
||||
username (str): You username
|
||||
"""
|
||||
|
||||
def __init__(self, server, data):
|
||||
"""Args:
|
||||
"""Set attrs.
|
||||
|
||||
Args:
|
||||
server (Plexclient):
|
||||
data (xml.etree.ElementTree.Element): used to set the class attributes.
|
||||
"""
|
||||
|
|
|
@ -1,15 +1,35 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
PlexAPI Sync
|
||||
"""
|
||||
|
||||
import requests
|
||||
from plexapi import utils
|
||||
from plexapi.exceptions import NotFound
|
||||
|
||||
|
||||
class SyncItem(object):
|
||||
"""Summary
|
||||
|
||||
Attributes:
|
||||
device (TYPE): Description
|
||||
id (TYPE): Description
|
||||
location (TYPE): Description
|
||||
machineIdentifier (TYPE): Description
|
||||
MediaSettings (TYPE): Description
|
||||
metadataType (TYPE): Description
|
||||
policy (TYPE): Description
|
||||
rootTitle (TYPE): Description
|
||||
servers (TYPE): Description
|
||||
status (TYPE): Description
|
||||
title (TYPE): Description
|
||||
version (TYPE): Description
|
||||
"""
|
||||
def __init__(self, device, data, servers=None):
|
||||
"""Summary
|
||||
|
||||
Args:
|
||||
device (TYPE): Description
|
||||
data (TYPE): Description
|
||||
servers (None, optional): Description
|
||||
"""
|
||||
self.device = device
|
||||
self.servers = servers
|
||||
self.id = utils.cast(int, data.attrib.get('id'))
|
||||
|
@ -24,20 +44,49 @@ class SyncItem(object):
|
|||
self.location = data.find('Location').attrib.copy()
|
||||
|
||||
def __repr__(self):
|
||||
"""Summary
|
||||
|
||||
Returns:
|
||||
TYPE: Description
|
||||
"""
|
||||
return '<%s:%s>' % (self.__class__.__name__, self.id)
|
||||
|
||||
def server(self):
|
||||
server = list(filter(lambda x: x.machineIdentifier == self.machineIdentifier, self.servers))
|
||||
"""Summary
|
||||
|
||||
Returns:
|
||||
TYPE: Description
|
||||
|
||||
Raises:
|
||||
NotFound: Description
|
||||
"""
|
||||
server = list(filter(lambda x: x.machineIdentifier ==
|
||||
self.machineIdentifier, self.servers))
|
||||
if 0 == len(server):
|
||||
raise NotFound('Unable to find server with uuid %s' % self.machineIdentifier)
|
||||
raise NotFound('Unable to find server with uuid %s' %
|
||||
self.machineIdentifier)
|
||||
return server[0]
|
||||
|
||||
def getMedia(self):
|
||||
"""Summary
|
||||
|
||||
Returns:
|
||||
TYPE: Description
|
||||
"""
|
||||
server = self.server().connect()
|
||||
items = utils.listItems(server, '/sync/items/%s' % self.id)
|
||||
return items
|
||||
|
||||
def markAsDone(self, sync_id):
|
||||
"""Summary
|
||||
|
||||
Args:
|
||||
sync_id (TYPE): Description
|
||||
|
||||
Returns:
|
||||
TYPE: Description
|
||||
"""
|
||||
server = self.server().connect()
|
||||
url = '/sync/%s/%s/files/%s/downloaded' % (self.device.clientIdentifier, server.machineIdentifier, sync_id)
|
||||
url = '/sync/%s/%s/files/%s/downloaded' % (
|
||||
self.device.clientIdentifier, server.machineIdentifier, sync_id)
|
||||
server.query(url, method=requests.put)
|
||||
|
|
|
@ -1,12 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
PlexAPI Utils
|
||||
|
||||
Attributes:
|
||||
LIBRARY_TYPES (dict): Description
|
||||
NA (TYPE): Description
|
||||
SEARCHTYPES (TYPE): Description
|
||||
"""
|
||||
import re
|
||||
from datetime import datetime
|
||||
from plexapi.compat import quote, urlencode
|
||||
|
@ -39,38 +32,29 @@ class _NA(object):
|
|||
"""
|
||||
|
||||
def __bool__(self):
|
||||
"""Summary
|
||||
"""Make sure Na always is False.
|
||||
|
||||
Returns:
|
||||
TYPE: Description
|
||||
bool: False
|
||||
"""
|
||||
return False
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Summary
|
||||
"""Check eq.
|
||||
|
||||
Args:
|
||||
other (TYPE): Description
|
||||
other (str): Description
|
||||
|
||||
Returns:
|
||||
TYPE: Description
|
||||
bool: True is equal
|
||||
"""
|
||||
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
|
||||
"""
|
||||
"""Pretty print."""
|
||||
return '__NA__'
|
||||
|
||||
NA = _NA()
|
||||
|
@ -83,15 +67,15 @@ class PlexPartialObject(object):
|
|||
automatically and update itself.
|
||||
|
||||
Attributes:
|
||||
initpath (TYPE): Description
|
||||
server (TYPE): Description
|
||||
initpath (str): Relative url to PMS
|
||||
server (): Description
|
||||
"""
|
||||
|
||||
def __init__(self, data, initpath, server=None):
|
||||
"""
|
||||
Args:
|
||||
data (xml.etree.ElementTree.Element): passed from server.query
|
||||
initpath (string): Relative path
|
||||
initpath (str): Relative path
|
||||
server (None or Plexserver, optional): PMS class your connected to
|
||||
"""
|
||||
self.server = server
|
||||
|
@ -120,7 +104,7 @@ class PlexPartialObject(object):
|
|||
"""Auto reload self, if the attribute is NA
|
||||
|
||||
Args:
|
||||
attr (string): fx key
|
||||
attr (str): fx key
|
||||
"""
|
||||
if attr == 'key' or self.__dict__.get(attr) or self.isFullObject():
|
||||
return self.__dict__.get(attr, NA)
|
||||
|
@ -131,7 +115,7 @@ class PlexPartialObject(object):
|
|||
"""Set attribute
|
||||
|
||||
Args:
|
||||
attr (string): fx key
|
||||
attr (str): fx key
|
||||
value (TYPE): Description
|
||||
"""
|
||||
if value != NA or self.isFullObject():
|
||||
|
@ -164,12 +148,12 @@ class Playable(object):
|
|||
Artists, Albums which are all not playable.
|
||||
|
||||
Attributes: # todo
|
||||
player (TYPE): Description
|
||||
playlistItemID (TYPE): Description
|
||||
sessionKey (TYPE): Description
|
||||
transcodeSession (TYPE): Description
|
||||
username (TYPE): Description
|
||||
viewedAt (datetime): Description
|
||||
player (Plexclient): Player
|
||||
playlistItemID (int): Playlist item id
|
||||
sessionKey (int): 1223
|
||||
transcodeSession (str): 12312312
|
||||
username (str): Fx Hellowlol
|
||||
viewedAt (datetime): viewed at.
|
||||
"""
|
||||
|
||||
def _loadData(self, data):
|
||||
|
@ -241,7 +225,7 @@ def buildItem(server, elem, initpath, bytag=False):
|
|||
Args:
|
||||
server (Plexserver): Your connected to.
|
||||
elem (xml.etree.ElementTree.Element): xml from PMS
|
||||
initpath (string): Relative path
|
||||
initpath (str): Relative path
|
||||
bytag (bool, optional): Description # figure out what this do
|
||||
|
||||
Raises:
|
||||
|
@ -300,8 +284,8 @@ def findItem(server, path, title):
|
|||
|
||||
Args:
|
||||
server (Plexserver): Description
|
||||
path (string): Relative path
|
||||
title (string): Fx 16 blocks
|
||||
path (str): Relative path
|
||||
title (str): Fx 16 blocks
|
||||
|
||||
Raises:
|
||||
NotFound: Unable to find item: title
|
||||
|
@ -355,7 +339,7 @@ def findStreams(media, streamtype):
|
|||
|
||||
Args:
|
||||
media (Show, Movie, Episode): A item where find streams
|
||||
streamtype (string): Possible options [movie, show, episode] # is this correct?
|
||||
streamtype (str): Possible options [movie, show, episode] # is this correct?
|
||||
|
||||
Returns:
|
||||
list: of streams
|
||||
|
@ -402,10 +386,10 @@ def findUsername(data):
|
|||
return None
|
||||
|
||||
|
||||
def isInt(string):
|
||||
def isInt(str):
|
||||
"""Check of a string is a int"""
|
||||
try:
|
||||
int(string)
|
||||
int(str)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
@ -435,7 +419,7 @@ def listChoices(server, path):
|
|||
|
||||
Args:
|
||||
server (Plexserver): Server your connected to
|
||||
path (string): Relative path to PMS
|
||||
path (str): Relative path to PMS
|
||||
|
||||
Returns:
|
||||
dict: title:key
|
||||
|
@ -448,7 +432,7 @@ def listItems(server, path, libtype=None, watched=None, bytag=False):
|
|||
|
||||
Args:
|
||||
server (Plexserver): PMS your connected to.
|
||||
path (string): Relative path to PMS
|
||||
path (str): 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
|
||||
|
@ -496,7 +480,7 @@ def searchType(libtype):
|
|||
Used when querying PMS.
|
||||
|
||||
Args:
|
||||
libtype (string): Possible options see SEARCHTYPES
|
||||
libtype (str): Possible options see SEARCHTYPES
|
||||
|
||||
Returns:
|
||||
int: fx 1
|
||||
|
@ -533,10 +517,10 @@ def threaded(callback, listargs):
|
|||
|
||||
|
||||
def toDatetime(value, format=None):
|
||||
"""Helper for datetime
|
||||
"""Helper for datetime.
|
||||
|
||||
Args:
|
||||
value (string): value to use to make datetime
|
||||
value (str): value to use to make datetime
|
||||
format (None, optional): string as strptime.
|
||||
|
||||
Returns:
|
||||
|
|
|
@ -1,13 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
PlexVideo
|
||||
|
||||
Attributes:
|
||||
NA (TYPE): Description
|
||||
"""
|
||||
from plexapi import media, utils
|
||||
from plexapi.utils import Playable, PlexPartialObject
|
||||
|
||||
NA = utils.NA
|
||||
|
||||
|
||||
|
@ -15,11 +10,12 @@ class Video(PlexPartialObject):
|
|||
TYPE = None
|
||||
|
||||
def __init__(self, server, data, initpath):
|
||||
"""
|
||||
"""Default class for all video types.
|
||||
|
||||
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
|
||||
initpath (str): Relativ path fx /library/sections/1/all
|
||||
|
||||
"""
|
||||
super(Video, self).__init__(data, initpath, server)
|
||||
|
@ -47,6 +43,7 @@ class Video(PlexPartialObject):
|
|||
|
||||
@property
|
||||
def thumbUrl(self):
|
||||
"""Return url to thumb image."""
|
||||
return self.server.url(self.thumb)
|
||||
|
||||
def analyze(self):
|
||||
|
@ -58,28 +55,24 @@ class Video(PlexPartialObject):
|
|||
self.server.query('/%s/analyze' % self.key)
|
||||
|
||||
def markWatched(self):
|
||||
"""Mark a items as watched.
|
||||
"""
|
||||
"""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.
|
||||
"""
|
||||
"""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):
|
||||
"""Refresh a item.
|
||||
"""
|
||||
"""Refresh a item."""
|
||||
self.server.query('%s/refresh' %
|
||||
self.key, method=self.server.session.put)
|
||||
|
||||
def section(self):
|
||||
"""Library section.
|
||||
"""
|
||||
"""Library section."""
|
||||
return self.server.library.sectionByID(self.librarySectionID)
|
||||
|
||||
|
||||
|
@ -91,7 +84,8 @@ class Movie(Video, Playable):
|
|||
"""Used to set the attributes
|
||||
|
||||
Args:
|
||||
data (Element): Usually built from server.query
|
||||
data (Element): XML reponse from PMS as Element
|
||||
normally built from server.query
|
||||
"""
|
||||
Video._loadData(self, data)
|
||||
Playable._loadData(self, data)
|
||||
|
@ -190,16 +184,25 @@ class Show(Video):
|
|||
return bool(self.viewedLeafCount == self.leafCount)
|
||||
|
||||
def seasons(self):
|
||||
"""Returns a list of Season
|
||||
"""
|
||||
"""Returns a list of Season."""
|
||||
path = '/library/metadata/%s/children' % self.ratingKey
|
||||
return utils.listItems(self.server, path, Season.TYPE)
|
||||
|
||||
def season(self, title):
|
||||
"""Returns a Season
|
||||
|
||||
Args:
|
||||
title (str): fx Season1
|
||||
"""
|
||||
path = '/library/metadata/%s/children' % self.ratingKey
|
||||
return utils.findItem(self.server, path, title)
|
||||
|
||||
def episodes(self, watched=None):
|
||||
"""Returs a list of Episode
|
||||
|
||||
Args:
|
||||
watched (bool): Defaults to None. Exclude watched episodes
|
||||
"""
|
||||
leavesKey = '/library/metadata/%s/allLeaves' % self.ratingKey
|
||||
return utils.listItems(self.server, leavesKey, watched=watched)
|
||||
|
||||
|
@ -208,21 +211,23 @@ class Show(Video):
|
|||
return utils.findItem(self.server, path, title)
|
||||
|
||||
def watched(self):
|
||||
"""Return a list of watched episodes
|
||||
"""
|
||||
"""Return a list of watched episodes"""
|
||||
return self.episodes(watched=True)
|
||||
|
||||
def unwatched(self):
|
||||
"""Return a list of unwatched episodes
|
||||
"""
|
||||
"""Return a list of unwatched episodes"""
|
||||
return self.episodes(watched=False)
|
||||
|
||||
def get(self, title):
|
||||
"""Get a Episode with a title.
|
||||
|
||||
Args:
|
||||
title (str): fx Secret santa
|
||||
"""
|
||||
return self.episode(title)
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh the metadata
|
||||
"""
|
||||
"""Refresh the metadata."""
|
||||
self.server.query('/library/metadata/%s/refresh' % self.ratingKey)
|
||||
|
||||
|
||||
|
@ -250,36 +255,51 @@ class Season(Video):
|
|||
|
||||
@property
|
||||
def seasonNumber(self):
|
||||
"""Reurns season number."""
|
||||
return self.index
|
||||
|
||||
def episodes(self, watched=None):
|
||||
"""Return list of Episode
|
||||
"""Returs a list of Episode
|
||||
|
||||
Args:
|
||||
watched (None, optional): Description
|
||||
watched (bool): Defaults to None. Exclude watched episodes
|
||||
"""
|
||||
childrenKey = '/library/metadata/%s/children' % self.ratingKey
|
||||
return utils.listItems(self.server, childrenKey, watched=watched)
|
||||
|
||||
def episode(self, title):
|
||||
"""Return Episode
|
||||
"""Find a episode with a matching title.
|
||||
|
||||
Args:
|
||||
title (TYPE): Description
|
||||
title (sting): Fx
|
||||
|
||||
Returns:
|
||||
Episode
|
||||
"""
|
||||
path = '/library/metadata/%s/children' % self.ratingKey
|
||||
return utils.findItem(self.server, path, title)
|
||||
|
||||
def get(self, title):
|
||||
"""Get a episode witha mathcing title
|
||||
|
||||
Args:
|
||||
title (str): fx Secret santa
|
||||
|
||||
Returns:
|
||||
Episode
|
||||
"""
|
||||
return self.episode(title)
|
||||
|
||||
def show(self):
|
||||
"""Return this seasons show."""
|
||||
return utils.listItems(self.server, self.parentKey)[0]
|
||||
|
||||
def watched(self):
|
||||
"""Returns a list of watched Episode"""
|
||||
return self.episodes(watched=True)
|
||||
|
||||
def unwatched(self):
|
||||
"""Returns a list of unwatched Episode"""
|
||||
return self.episodes(watched=False)
|
||||
|
||||
|
||||
|
@ -288,6 +308,11 @@ class Episode(Video, Playable):
|
|||
TYPE = 'episode'
|
||||
|
||||
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)
|
||||
|
@ -311,7 +336,6 @@ class Episode(Video, Playable):
|
|||
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)
|
||||
|
@ -331,20 +355,25 @@ class Episode(Video, Playable):
|
|||
|
||||
@property
|
||||
def isWatched(self):
|
||||
"""Returns True if watched, False if not."""
|
||||
return bool(self.viewCount > 0)
|
||||
|
||||
@property
|
||||
def seasonNumber(self):
|
||||
"""Return this episode seasonnumber."""
|
||||
if self._seasonNumber is None:
|
||||
self._seasonNumber = self.parentIndex if self.parentIndex else self.season().seasonNumber
|
||||
return self._seasonNumber
|
||||
|
||||
@property
|
||||
def thumbUrl(self):
|
||||
"""Return url to thumb image."""
|
||||
return self.server.url(self.grandparentThumb)
|
||||
|
||||
def season(self):
|
||||
"""Return this episode Season"""
|
||||
return utils.listItems(self.server, self.parentKey)[0]
|
||||
|
||||
def show(self):
|
||||
"""Return this episodes Show"""
|
||||
return utils.listItems(self.server, self.grandparentKey)[0]
|
||||
|
|
Loading…
Reference in a new issue