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