mirror of
https://github.com/pkkid/python-plexapi
synced 2024-11-10 06:04:15 +00:00
CHECKPOINT: Lots going on; Added new base class PlexObject that everything inherits from, this ensures all constructors are similar; Lots of work on new tool plexattrs that parses a full Plex library to find differences in attributes plexapi implements and what the Plex XML API offers up; Tests will most definetly be broken at this point, but I wanted to save my work.
This commit is contained in:
parent
fc28f7c1e6
commit
6a35f50a43
16 changed files with 741 additions and 812 deletions
115
plexapi/audio.py
115
plexapi/audio.py
|
@ -1,6 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from plexapi import media, utils
|
||||
from plexapi.base import Playable, PlexPartialObject
|
||||
from plexapi.exceptions import NotFound
|
||||
|
||||
|
||||
class Audio(PlexPartialObject):
|
||||
|
@ -30,11 +31,9 @@ class Audio(PlexPartialObject):
|
|||
"""
|
||||
TYPE = None
|
||||
|
||||
def __init__(self, server, data, initpath):
|
||||
super(Audio, self).__init__(data, initpath, server)
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.listType = 'audio'
|
||||
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
|
||||
self.index = data.attrib.get('index')
|
||||
|
@ -54,15 +53,15 @@ class Audio(PlexPartialObject):
|
|||
def thumbUrl(self):
|
||||
""" Returns the URL to this items thumbnail image. """
|
||||
if self.thumb:
|
||||
return self.server.url(self.thumb)
|
||||
return self._root.url(self.thumb)
|
||||
|
||||
def refresh(self):
|
||||
""" Tells Plex to refresh the metadata for this and all subitems. """
|
||||
self.server.query('%s/refresh' % self.key, method=self.server.session.put)
|
||||
self._root.query('%s/refresh' % self.key, method=self._root.session.put)
|
||||
|
||||
def section(self):
|
||||
""" Returns the :class:`~plexapi.library.LibrarySection` this item belongs to. """
|
||||
return self.server.library.sectionByID(self.librarySectionID)
|
||||
return self._root.library.sectionByID(self.librarySectionID)
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
|
@ -92,15 +91,9 @@ class Artist(Audio):
|
|||
self.guid = data.attrib.get('guid')
|
||||
self.key = self.key.replace('/children', '') # FIX_BUG_50
|
||||
self.location = utils.findLocations(data, single=True)
|
||||
if self.isFullObject(): # check if this is needed
|
||||
self.countries = [media.Country(self.server, e) for e in data if e.tag == media.Country.TYPE]
|
||||
self.genres = [media.Genre(self.server, e) for e in data if e.tag == media.Genre.TYPE]
|
||||
self.similar = [media.Similar(self.server, e) for e in data if e.tag == media.Similar.TYPE]
|
||||
|
||||
def albums(self):
|
||||
""" Returns a list of :class:`~plexapi.audio.Album` objects by this artist. """
|
||||
path = '%s/children' % self.key
|
||||
return utils.listItems(self.server, path, Album.TYPE)
|
||||
self.countries = self._buildSubitems(data, media.Country)
|
||||
self.genres = self._buildSubitems(data, media.Genre)
|
||||
self.similar = self._buildSubitems(data, media.Similar)
|
||||
|
||||
def album(self, title):
|
||||
""" Returns the :class:`~plexapi.audio.Album` that matches the specified title.
|
||||
|
@ -108,13 +101,15 @@ class Artist(Audio):
|
|||
Parameters:
|
||||
title (str): Title of the album to return.
|
||||
"""
|
||||
path = '%s/children' % self.key
|
||||
return utils.findItem(self.server, path, title)
|
||||
for album in self.albums():
|
||||
if album.title.lower() == title.lower():
|
||||
return album
|
||||
raise NotFound('Unable to find album %s' % title)
|
||||
|
||||
def tracks(self):
|
||||
""" Returns a list of :class:`~plexapi.audio.Track` objects by this artist. """
|
||||
path = '%s/allLeaves' % self.key
|
||||
return utils.listItems(self.server, path)
|
||||
def albums(self):
|
||||
""" Returns a list of :class:`~plexapi.audio.Album` objects by this artist. """
|
||||
key = '%s/children' % self.key
|
||||
return self._fetchItems(key, Album.TYPE)
|
||||
|
||||
def track(self, title):
|
||||
""" Returns the :class:`~plexapi.audio.Track` that matches the specified title.
|
||||
|
@ -122,8 +117,15 @@ class Artist(Audio):
|
|||
Parameters:
|
||||
title (str): Title of the track to return.
|
||||
"""
|
||||
path = '%s/allLeaves' % self.key
|
||||
return utils.findItem(self.server, path, title)
|
||||
for track in self.tracks():
|
||||
if track.title.lower() == title.lower():
|
||||
return track
|
||||
raise NotFound('Unable to find track %s' % title)
|
||||
|
||||
def tracks(self):
|
||||
""" Returns a list of :class:`~plexapi.audio.Track` objects by this artist. """
|
||||
key = '%s/allLeaves' % self.key
|
||||
return self._fetchItems(key)
|
||||
|
||||
def get(self, title):
|
||||
""" Alias of :func:`~plexapi.audio.Artist.track`. """
|
||||
|
@ -142,13 +144,11 @@ class Artist(Audio):
|
|||
function. If kwargs is not specified, the media items will be downloaded
|
||||
and saved to disk.
|
||||
"""
|
||||
downloaded = []
|
||||
filepaths = []
|
||||
for album in self.albums():
|
||||
for track in album.tracks():
|
||||
dl = track.download(savepath=savepath, keep_orginal_name=keep_orginal_name, **kwargs)
|
||||
if dl:
|
||||
downloaded.extend(dl)
|
||||
return downloaded
|
||||
filepaths += track.download(savepath, keep_orginal_name, **kwargs)
|
||||
return filepaths
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
|
@ -186,13 +186,7 @@ class Album(Audio):
|
|||
self.parentTitle = data.attrib.get('parentTitle')
|
||||
self.studio = data.attrib.get('studio')
|
||||
self.year = utils.cast(int, data.attrib.get('year'))
|
||||
if self.isFullObject():
|
||||
self.genres = [media.Genre(self.server, e) for e in data if e.tag == media.Genre.TYPE]
|
||||
|
||||
def tracks(self):
|
||||
""" Returns a list of :class:`~plexapi.audio.Track` objects in this album. """
|
||||
path = '%s/children' % self.key
|
||||
return utils.listItems(self.server, path)
|
||||
self.genres = self._buildSubitems(data, media.Genre)
|
||||
|
||||
def track(self, title):
|
||||
""" Returns the :class:`~plexapi.audio.Track` that matches the specified title.
|
||||
|
@ -200,8 +194,15 @@ class Album(Audio):
|
|||
Parameters:
|
||||
title (str): Title of the track to return.
|
||||
"""
|
||||
path = '%s/children' % self.key
|
||||
return utils.findItem(self.server, path, title)
|
||||
for track in self.tracks():
|
||||
if track.title.lower() == title.lower():
|
||||
return track
|
||||
raise NotFound('Unable to find track %s' % title)
|
||||
|
||||
def tracks(self):
|
||||
""" Returns a list of :class:`~plexapi.audio.Track` objects in this album. """
|
||||
key = '%s/children' % self.key
|
||||
return self._fetchItems(key)
|
||||
|
||||
def get(self, title):
|
||||
""" Alias of :func:`~plexapi.audio.Album.track`. """
|
||||
|
@ -209,7 +210,7 @@ class Album(Audio):
|
|||
|
||||
def artist(self):
|
||||
""" Return :func:`~plexapi.audio.Artist` of this album. """
|
||||
return utils.listItems(self.server, self.parentKey)[0]
|
||||
return self._fetchItems(self.parentKey)[0]
|
||||
|
||||
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
|
||||
""" Downloads all tracks for this artist to the specified location.
|
||||
|
@ -224,13 +225,10 @@ class Album(Audio):
|
|||
function. If kwargs is not specified, the media items will be downloaded
|
||||
and saved to disk.
|
||||
"""
|
||||
downloaded = []
|
||||
for ep in self.tracks():
|
||||
dl = ep.download(savepath=savepath, keep_orginal_name=keep_orginal_name, **kwargs)
|
||||
if dl:
|
||||
downloaded.extend(dl)
|
||||
|
||||
return downloaded
|
||||
filepaths = []
|
||||
for track in self.tracks():
|
||||
filepaths += track.download(savepath, keep_orginal_name, **kwargs)
|
||||
return filepaths
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
|
@ -295,33 +293,28 @@ class Track(Audio, Playable):
|
|||
self.ratingCount = utils.cast(int, data.attrib.get('ratingCount'))
|
||||
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
||||
self.year = utils.cast(int, data.attrib.get('year'))
|
||||
# media is included in /children
|
||||
self.media = [media.Media(self.server, e, self.initpath, self)
|
||||
for e in data if e.tag == media.Media.TYPE]
|
||||
if self.isFullObject(): # check me
|
||||
self.moods = [media.Mood(self.server, e) for e in data if e.tag == media.Mood.TYPE]
|
||||
#self.media = [media.Media(self.server, e, self.initpath, self)
|
||||
# for e in data if e.tag == media.Media.TYPE]
|
||||
self.media = self._buildSubitems(data, media.Media)
|
||||
self.moods = self._buildSubitems(data, media.Mood)
|
||||
# data for active sessions and history
|
||||
self.sessionKey = utils.cast(int, data.attrib.get('sessionKey'))
|
||||
self.username = utils.findUsername(data)
|
||||
self.player = utils.findPlayer(self.server, data)
|
||||
self.transcodeSession = utils.findTranscodeSession(self.server, data)
|
||||
self.player = utils.findPlayer(self._root, data)
|
||||
self.transcodeSession = utils.findTranscodeSession(self._root, data)
|
||||
|
||||
def _prettyfilename(self):
|
||||
""" Returns a filename for use in download. """
|
||||
return '%s - %s %s' % (self.grandparentTitle, self.parentTitle, self.title)
|
||||
|
||||
@property
|
||||
def thumbUrl(self):
|
||||
""" Returns the URL thumbnail image for this track's album. """
|
||||
if self.parentThumb:
|
||||
return self.server.url(self.parentThumb)
|
||||
return self._root.url(self.parentThumb)
|
||||
|
||||
def album(self):
|
||||
""" Return this track's :class:`~plexapi.audio.Album`. """
|
||||
return utils.listItems(self.server, self.parentKey)[0]
|
||||
return self._fetchItems(self.parentKey)[0]
|
||||
|
||||
def artist(self):
|
||||
""" Return this track's :class:`~plexapi.audio.Artist`. """
|
||||
return utils.listItems(self.server, self.grandparentKey)[0]
|
||||
|
||||
def _prettyfilename(self):
|
||||
""" Returns a filename for use in download. """
|
||||
return '%s - %s %s' % (self.grandparentTitle, self.parentTitle, self.title)
|
||||
return self._fetchItems(self.grandparentKey)[0]
|
||||
|
|
131
plexapi/base.py
131
plexapi/base.py
|
@ -1,8 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
from plexapi import utils
|
||||
from plexapi import log, utils
|
||||
from plexapi.compat import urlencode
|
||||
from plexapi.exceptions import Unsupported
|
||||
from plexapi.exceptions import NotFound, UnknownType, Unsupported
|
||||
|
||||
|
||||
class Playable(object):
|
||||
|
@ -23,8 +23,8 @@ class Playable(object):
|
|||
# Load data for active sessions (/status/sessions)
|
||||
self.sessionKey = utils.cast(int, data.attrib.get('sessionKey'))
|
||||
self.username = utils.findUsername(data)
|
||||
self.player = utils.findPlayer(self.server, data)
|
||||
self.transcodeSession = utils.findTranscodeSession(self.server, data)
|
||||
self.player = utils.findPlayer(self._root, data)
|
||||
self.transcodeSession = utils.findTranscodeSession(self._root, data)
|
||||
# Load data for history details (/status/sessions/history/all)
|
||||
self.viewedAt = utils.toDatetime(data.attrib.get('viewedAt'))
|
||||
# Load data for playlist items
|
||||
|
@ -60,7 +60,7 @@ class Playable(object):
|
|||
streamtype = 'audio' if self.TYPE in ('track', 'album') else 'video'
|
||||
# sort the keys since the randomness fucks with my tests..
|
||||
sorted_params = sorted(params.items(), key=lambda val: val[0])
|
||||
return self.server.url('/%s/:/transcode/universal/start.m3u8?%s' %
|
||||
return self._root.url('/%s/:/transcode/universal/start.m3u8?%s' %
|
||||
(streamtype, urlencode(sorted_params)))
|
||||
|
||||
def iterParts(self):
|
||||
|
@ -101,15 +101,86 @@ class Playable(object):
|
|||
if kwargs:
|
||||
download_url = self.getStreamURL(**kwargs)
|
||||
else:
|
||||
download_url = self.server.url('%s?download=1' % location.key)
|
||||
download_url = self._root.url('%s?download=1' % location.key)
|
||||
filepath = utils.download(download_url, filename=filename,
|
||||
savepath=savepath, session=self.server.session)
|
||||
savepath=savepath, session=self._root.session)
|
||||
if filepath:
|
||||
filepaths.append(filepath)
|
||||
return filepaths
|
||||
|
||||
|
||||
class PlexPartialObject(object):
|
||||
class PlexObject(object):
|
||||
""" Base class for all Plex objects.
|
||||
TODO: Finish documenting this.
|
||||
"""
|
||||
key = None
|
||||
|
||||
def __init__(self, root, data, initpath=None):
|
||||
self._root = root # Root MyPlexAccount or PlexServer
|
||||
self._data = data # XML data needed to build object
|
||||
self._initpath = initpath or self.key # Request path used to fetch data
|
||||
self._loadData(data)
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
if value is not None or attr.startswith('_'):
|
||||
self.__dict__[attr] = value
|
||||
|
||||
def _buildItem(self, elem, initpath, bytag=False):
|
||||
""" Factory function to build objects based on registered LIBRARY_TYPES. """
|
||||
libtype = elem.tag if bytag else elem.attrib.get('type')
|
||||
if libtype == 'photo' and elem.tag == 'Directory':
|
||||
libtype = 'photoalbum'
|
||||
if libtype in utils.LIBRARY_TYPES:
|
||||
cls = utils.LIBRARY_TYPES[libtype]
|
||||
return cls(self._root, elem, initpath)
|
||||
raise UnknownType('Unknown library type: %s' % libtype)
|
||||
|
||||
def _buildSubitems(self, data, cls, tag=None, filters=None, *args):
|
||||
""" Build and return a list of items (optionally filtered by tag). """
|
||||
items = []
|
||||
tag = tag or cls.TYPE
|
||||
filters = filters or {}
|
||||
for elem in data:
|
||||
if elem.tag == tag:
|
||||
for attr, value in filters.items():
|
||||
if elem.attrib.get(attr) != str(value):
|
||||
continue
|
||||
items.append(cls(self._root, elem, self._initpath, *args))
|
||||
return items
|
||||
|
||||
def _fetchItem(self, key, title=None, name=None):
|
||||
for elem in self._root._query(key):
|
||||
if title and elem.attrib.get('title').lower() == title.lower():
|
||||
return self._buildItem(elem, key)
|
||||
if name and elem.attrib.get('name').lower() == name.lower():
|
||||
return self._buildItem(elem, key)
|
||||
raise NotFound('Unable to find item: %s' % (title or name))
|
||||
|
||||
def _fetchItems(self, key, libtype=None, bytag=False):
|
||||
""" Fetch and build items from the specified key. """
|
||||
items = []
|
||||
for elem in self._root._query(key):
|
||||
try:
|
||||
if not libtype or elem.attrib.get('type') == libtype:
|
||||
items.append(self._buildItem(elem, key, bytag))
|
||||
except UnknownType:
|
||||
pass
|
||||
return items
|
||||
|
||||
def _loadData(self, data):
|
||||
raise NotImplemented('Abstract method not implemented.')
|
||||
|
||||
def reload(self, safe=False):
|
||||
""" Reload the data for this object from self.key. """
|
||||
if not self.key:
|
||||
if safe: return None
|
||||
raise Unsupported('Cannot reload an object not built from a URL.')
|
||||
self._initpath = self.key
|
||||
data = self._root._query(self.key)
|
||||
self._loadData(data[0])
|
||||
|
||||
|
||||
class PlexPartialObject(PlexObject):
|
||||
""" Not all objects in the Plex listings return the complete list of elements
|
||||
for the object. This object will allow you to assume each object is complete,
|
||||
and if the specified value you request is None it will fetch the full object
|
||||
|
@ -120,12 +191,6 @@ class PlexPartialObject(object):
|
|||
initpath (str): Relative path requested when retrieving specified `data` (optional).
|
||||
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
|
||||
"""
|
||||
def __init__(self, data, initpath, server=None):
|
||||
self.server = server
|
||||
self.initpath = initpath
|
||||
self._loadData(data)
|
||||
self._reloaded = False
|
||||
|
||||
def __eq__(self, other):
|
||||
return other is not None and self.key == other.key
|
||||
|
||||
|
@ -135,26 +200,20 @@ class PlexPartialObject(object):
|
|||
title = self.title.replace(' ', '.')[0:20].encode('utf8')
|
||||
return '<%s:%s:%s>' % (clsname, key, title)
|
||||
|
||||
def __getattr__(self, attr):
|
||||
from plexapi import log
|
||||
# Auto reload from the full key (path) when needed.
|
||||
if attr == 'key' or self.__dict__.get(attr) or self.isFullObject() or attr.startswith('_'):
|
||||
return self.__dict__.get(attr)
|
||||
# Log warning about reloading the objects
|
||||
def __getattribute__(self, attr):
|
||||
# Check a few cases where we dont want to reload
|
||||
value = super(PlexPartialObject, self).__getattribute__(attr)
|
||||
if attr == 'key' or attr.startswith('_'): return value
|
||||
if value not in (None, []): return value
|
||||
if self.isFullObject(): return value
|
||||
# Log warning that were reloading the object
|
||||
clsname = self.__class__.__name__
|
||||
title = self.__dict__.get('title') or self.__dict__.get('name')
|
||||
title = self.__dict__.get('title', self.__dict__.get('name'))
|
||||
objname = "%s '%s'" % (clsname, title) if title else clsname
|
||||
log.warn("Reloading %s for attr '%s'" % (objname, attr))
|
||||
# Reload the object
|
||||
# Reload and return the value
|
||||
self.reload()
|
||||
return self.__dict__.get(attr)
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
if value is not None or self.isFullObject():
|
||||
self.__dict__[attr] = value
|
||||
|
||||
def _loadData(self, data):
|
||||
raise NotImplemented('Abstract method not implemented.')
|
||||
return super(PlexPartialObject, self).__getattribute__(attr)
|
||||
|
||||
def isFullObject(self):
|
||||
""" Retruns True if this is already a full object. A full object means all attributes
|
||||
|
@ -162,16 +221,8 @@ class PlexPartialObject(object):
|
|||
search result for a movie often only contain a portion of the attributes a full
|
||||
object (main url) for that movie contain.
|
||||
"""
|
||||
return not self.key or self.key == self.initpath
|
||||
return not self.key or self.key == self._initpath
|
||||
|
||||
def isPartialObject(self):
|
||||
""" Returns True if this is NOT a full object. """
|
||||
""" Returns True if this is not a full object. """
|
||||
return not self.isFullObject()
|
||||
|
||||
def reload(self):
|
||||
""" Reload the data for this object from PlexServer XML. """
|
||||
data = self.server.query(self.key)
|
||||
self.initpath = self.key
|
||||
self._loadData(data[0])
|
||||
self._reloaded = True
|
||||
return self
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import requests
|
||||
from requests.status_codes import _codes as codes
|
||||
from plexapi import BASE_HEADERS, TIMEOUT, log, utils
|
||||
from plexapi.exceptions import BadRequest, NotFound, Unsupported
|
||||
from plexapi import BASE_HEADERS, CONFIG, TIMEOUT
|
||||
from plexapi import log, logfilter, utils
|
||||
from plexapi.base import PlexObject
|
||||
from plexapi.exceptions import BadRequest, Unsupported
|
||||
from xml.etree import ElementTree
|
||||
|
||||
|
||||
class PlexClient(object):
|
||||
class PlexClient(PlexObject):
|
||||
""" Main class for interacting with a Plex client. This class can connect
|
||||
directly to the client and control it or proxy commands through your
|
||||
Plex Server. To better understand the Plex client API's read this page:
|
||||
|
@ -42,16 +44,31 @@ class PlexClient(object):
|
|||
_proxyThroughServer (bool): Set to True after calling
|
||||
:func:`~plexapi.client.PlexClient.proxyThroughServer()` (default False).
|
||||
"""
|
||||
key = '/resources'
|
||||
|
||||
def __init__(self, baseurl, token=None, session=None, server=None, data=None):
|
||||
self.baseurl = baseurl.strip('/')
|
||||
self.token = token
|
||||
self.server = server
|
||||
self._baseurl = baseurl or CONFIG.get('authentication.client_baseurl')
|
||||
self._token = token or CONFIG.get('authentication.client_token')
|
||||
if self._token:
|
||||
logfilter.add_secret(self._token)
|
||||
self._server = server
|
||||
# session > server.session > requests.Session
|
||||
_server_session = server.session if server else None
|
||||
self.session = session or _server_session or requests.Session()
|
||||
self._loadData(data) if data is not None else self.connect()
|
||||
_server_session = server._session if server else None
|
||||
self._session = session or _server_session or requests.Session()
|
||||
self._proxyThroughServer = False
|
||||
self._commandId = 0
|
||||
data = data if data is not None else self._query('/resources')[0]
|
||||
super(PlexClient, self).__init__(self, data, self.key)
|
||||
|
||||
def connect(self, safe=False):
|
||||
""" Alias of reload as any subsequent requests to this client will be
|
||||
made directly to the device even if the object attributes were initially
|
||||
populated from a PlexServer.
|
||||
"""
|
||||
try:
|
||||
self.reload()
|
||||
except Exception:
|
||||
if not safe: raise
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
|
@ -72,26 +89,41 @@ class PlexClient(object):
|
|||
self.vendor = data.attrib.get('vendor')
|
||||
self.version = data.attrib.get('version')
|
||||
|
||||
def connect(self):
|
||||
""" Connects to the client and reloads all class attributes.
|
||||
|
||||
Raises:
|
||||
:class:`~plexapi.exceptions.NotFound`: No client found at the specified url.
|
||||
"""
|
||||
try:
|
||||
data = self.query('/resources')[0]
|
||||
self._loadData(data)
|
||||
except Exception as err:
|
||||
log.error('%s: %s', self.baseurl, err)
|
||||
raise NotFound('No client found at: %s' % self.baseurl)
|
||||
def __repr__(self):
|
||||
return '<%s:%s>' % (self.__class__.__name__, self._baseurl)
|
||||
|
||||
def headers(self):
|
||||
def _query(self, path, method=None, headers=None, **kwargs):
|
||||
""" Main method used to handle HTTPS requests to the Plex client. This method helps
|
||||
by encoding the response to utf-8 and parsing the returned XML into and
|
||||
ElementTree object. Returns None if no data exists in the response.
|
||||
"""
|
||||
url = self._url(path)
|
||||
method = method or self._session.get
|
||||
log.info('%s %s', method.__name__.upper(), url)
|
||||
headers = self._headers(**headers or {})
|
||||
response = method(url, headers=headers, timeout=TIMEOUT, **kwargs)
|
||||
if response.status_code not in (200, 201):
|
||||
codename = codes.get(response.status_code)[0]
|
||||
log.warn('BadRequest (%s) %s %s' % (response.status_code, codename, response.url))
|
||||
raise BadRequest('(%s) %s %s' % (response.status_code, codename, response.url))
|
||||
data = response.text.encode('utf8')
|
||||
return ElementTree.fromstring(data) if data else None
|
||||
|
||||
def _headers(self, **kwargs):
|
||||
""" Returns a dict of all default headers for Client requests. """
|
||||
headers = BASE_HEADERS
|
||||
if self.token:
|
||||
headers['X-Plex-Token'] = self.token
|
||||
if self._token:
|
||||
headers['X-Plex-Token'] = self._token
|
||||
headers.update(kwargs)
|
||||
return headers
|
||||
|
||||
def _url(self, key):
|
||||
""" Build a URL string with proper token argument. """
|
||||
if self._token:
|
||||
delim = '&' if '?' in key else '?'
|
||||
return '%s%s%sX-Plex-Token=%s' % (self._baseurl, key, delim, self._token)
|
||||
return '%s%s' % (self._baseurl, key)
|
||||
|
||||
def proxyThroughServer(self, value=True):
|
||||
""" Tells this PlexClient instance to proxy all future commands through the PlexServer.
|
||||
Useful if you do not wish to connect directly to the Client device itself.
|
||||
|
@ -106,30 +138,6 @@ class PlexClient(object):
|
|||
raise Unsupported('Cannot use client proxy with unknown server.')
|
||||
self._proxyThroughServer = value
|
||||
|
||||
def query(self, path, method=None, headers=None, **kwargs):
|
||||
""" Returns an ElementTree object containing the response
|
||||
from the specified request path.
|
||||
|
||||
Parameters:
|
||||
path (str): Relative path to query.
|
||||
method (func): `self.session.get` or `self.session.post`
|
||||
headers (dict): Additional headers to include or override in the request.
|
||||
**kwargs (TYPE): Additional arguments to inclde in the request.<method> call.
|
||||
|
||||
Raises:
|
||||
:class:`~plexapi.exceptions.BadRequest`: When the response is not in [200, 201]
|
||||
"""
|
||||
url = self.url(path)
|
||||
method = method or self.session.get
|
||||
log.info('%s %s', method.__name__.upper(), url)
|
||||
headers = dict(self.headers(), **(headers or {})) # remove hack
|
||||
response = method(url, headers=headers, timeout=TIMEOUT, **kwargs)
|
||||
if response.status_code not in [200, 201]:
|
||||
codename = codes.get(response.status_code)[0]
|
||||
raise BadRequest('(%s) %s' % (response.status_code, codename))
|
||||
data = response.text.encode('utf8')
|
||||
return ElementTree.fromstring(data) if data else None
|
||||
|
||||
def sendCommand(self, command, proxy=None, **params):
|
||||
""" Convenience wrapper around :func:`~plexapi.client.PlexClient.query()` to more easily
|
||||
send simple commands to the client. Returns an ElementTree object containing
|
||||
|
@ -156,18 +164,7 @@ class PlexClient(object):
|
|||
if proxy:
|
||||
return self.server.query(path, headers=headers)
|
||||
path = '/player/%s%s' % (command, utils.joinArgs(params))
|
||||
return self.query(path, headers=headers)
|
||||
|
||||
def url(self, path):
|
||||
""" Given a path, this retuns the full PlexClient the PlexServer URL to request.
|
||||
|
||||
Parameters:
|
||||
path (str): Relative path to be converted.
|
||||
"""
|
||||
if self.token:
|
||||
delim = '&' if '?' in path else '?'
|
||||
return '%s%s%sX-Plex-Token=%s' % (self.baseurl, path, delim, self.token)
|
||||
return '%s%s' % (self.baseurl, path)
|
||||
return self._query(path, headers=headers)
|
||||
|
||||
#---------------------
|
||||
# Navigation Commands
|
||||
|
@ -240,7 +237,7 @@ class PlexClient(object):
|
|||
"""
|
||||
if not self.server:
|
||||
raise Unsupported('A server must be specified before using this command.')
|
||||
server_url = media.server.baseurl.split(':')
|
||||
server_url = media.server._baseurl.split(':')
|
||||
self.sendCommand('mirror/details', **dict({
|
||||
'machineIdentifier': self.server.machineIdentifier,
|
||||
'address': server_url[1].strip('/'),
|
||||
|
@ -407,7 +404,7 @@ class PlexClient(object):
|
|||
"""
|
||||
if not self.server:
|
||||
raise Unsupported('A server must be specified before using this command.')
|
||||
server_url = media.server.baseurl.split(':')
|
||||
server_url = media.server._baseurl.split(':')
|
||||
playqueue = self.server.createPlayQueue(media)
|
||||
self.sendCommand('playback/playMedia', **dict({
|
||||
'machineIdentifier': self.server.machineIdentifier,
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
from plexapi import X_PLEX_CONTAINER_SIZE, log, utils
|
||||
from plexapi.base import PlexObject
|
||||
from plexapi.compat import unquote
|
||||
from plexapi.media import MediaTag, Genre, Role, Director
|
||||
from plexapi.exceptions import BadRequest, NotFound
|
||||
|
||||
|
||||
class Library(object):
|
||||
class Library(PlexObject):
|
||||
""" Represents a PlexServer library. This contains all sections of media defined
|
||||
in your Plex server including video, shows and audio.
|
||||
|
||||
|
@ -17,14 +18,15 @@ class Library(object):
|
|||
title1 (str): 'Plex Library' (not sure how useful this is).
|
||||
title2 (str): Second title (this is blank on my setup).
|
||||
"""
|
||||
def __init__(self, server, data):
|
||||
key = '/library'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self._sectionsByID = {} # cached Section UUIDs
|
||||
self.identifier = data.attrib.get('identifier')
|
||||
self.mediaTagVersion = data.attrib.get('mediaTagVersion')
|
||||
self.server = server
|
||||
self.title1 = data.attrib.get('title1')
|
||||
self.title2 = data.attrib.get('title2')
|
||||
self._sectionsByID = {} # cached Section UUIDs
|
||||
|
||||
def __repr__(self):
|
||||
return '<Library:%s>' % self.title1.encode('utf8')
|
||||
|
@ -34,19 +36,15 @@ class Library(object):
|
|||
:class:`~plexapi.library.MovieSection`, :class:`~plexapi.library.ShowSection`,
|
||||
:class:`~plexapi.library.MusicSection`, :class:`~plexapi.library.PhotoSection`.
|
||||
"""
|
||||
SECTION_TYPES = {MovieSection.TYPE:MovieSection, ShowSection.TYPE:ShowSection,
|
||||
MusicSection.TYPE: MusicSection, PhotoSection.TYPE: PhotoSection}
|
||||
items = []
|
||||
SECTION_TYPES = {
|
||||
MovieSection.TYPE: MovieSection,
|
||||
ShowSection.TYPE: ShowSection,
|
||||
MusicSection.TYPE: MusicSection,
|
||||
PhotoSection.TYPE: PhotoSection,
|
||||
}
|
||||
path = '/library/sections'
|
||||
for elem in self.server.query(path):
|
||||
key = '/library/sections'
|
||||
for elem in self._root._query(key):
|
||||
stype = elem.attrib['type']
|
||||
if stype in SECTION_TYPES:
|
||||
cls = SECTION_TYPES[stype]
|
||||
section = cls(self.server, elem, path)
|
||||
section = cls(self._root, elem, key)
|
||||
self._sectionsByID[section.key] = section
|
||||
items.append(section)
|
||||
return items
|
||||
|
@ -157,7 +155,7 @@ class Library(object):
|
|||
return len(self.sections())
|
||||
|
||||
|
||||
class LibrarySection(object):
|
||||
class LibrarySection(PlexObject):
|
||||
""" Base class for a single library section.
|
||||
|
||||
Parameters:
|
||||
|
@ -189,17 +187,15 @@ class LibrarySection(object):
|
|||
ALLOWED_SORT = ()
|
||||
BOOLEAN_FILTERS = ('unwatched', 'duplicate')
|
||||
|
||||
def __init__(self, server, data, initpath):
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self.server = server
|
||||
self.initpath = initpath
|
||||
self.agent = data.attrib.get('agent')
|
||||
self.allowSync = utils.cast(bool, data.attrib.get('allowSync'))
|
||||
self.art = data.attrib.get('art')
|
||||
self.composite = data.attrib.get('composite')
|
||||
self.createdAt = utils.toDatetime(data.attrib.get('createdAt'))
|
||||
self.filters = data.attrib.get('filters')
|
||||
self.key = data.attrib.get('key')
|
||||
self.key = data.attrib.get('key') # invalid key from plex
|
||||
self.language = data.attrib.get('language')
|
||||
self.locations = utils.findLocations(data)
|
||||
self.refreshing = utils.cast(bool, data.attrib.get('refreshing'))
|
||||
|
@ -220,12 +216,13 @@ class LibrarySection(object):
|
|||
Parameters:
|
||||
title (str): Title of the item to return.
|
||||
"""
|
||||
path = '/library/sections/%s/all' % self.key
|
||||
return utils.findItem(self.server, path, title)
|
||||
key = '/library/sections/%s/all' % self.key
|
||||
return utils.findItem(self.server, key, title)
|
||||
|
||||
def all(self):
|
||||
""" Returns a list of media from this library section. """
|
||||
return utils.listItems(self.server, '/library/sections/%s/all' % self.key)
|
||||
key = '/library/sections/%s/all' % self.key
|
||||
return self._fetchItems(key)
|
||||
|
||||
def onDeck(self):
|
||||
""" Returns a list of media items on deck from this library section. """
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from plexapi.base import PlexObject
|
||||
from plexapi.exceptions import BadRequest
|
||||
from plexapi.utils import cast, listItems
|
||||
|
||||
|
||||
class Media(object):
|
||||
class Media(PlexObject):
|
||||
""" Container object for all MediaPart objects. Provides useful data about the
|
||||
video this media belong to such as video framerate, resolution, etc.
|
||||
|
||||
|
@ -35,10 +36,8 @@ class Media(object):
|
|||
"""
|
||||
TYPE = 'Media'
|
||||
|
||||
def __init__(self, server, data, initpath, video):
|
||||
self.server = server
|
||||
self.initpath = initpath
|
||||
self.video = video
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self.aspectRatio = cast(float, data.attrib.get('aspectRatio'))
|
||||
self.audioChannels = cast(int, data.attrib.get('audioChannels'))
|
||||
self.audioCodec = data.attrib.get('audioCodec')
|
||||
|
@ -53,14 +52,14 @@ class Media(object):
|
|||
self.videoFrameRate = data.attrib.get('videoFrameRate')
|
||||
self.videoResolution = data.attrib.get('videoResolution')
|
||||
self.width = cast(int, data.attrib.get('width'))
|
||||
self.parts = [MediaPart(server, e, initpath, self) for e in data]
|
||||
self.parts = self._buildSubitems(data, MediaPart)
|
||||
|
||||
def __repr__(self):
|
||||
title = self.video.title.replace(' ','.')[0:20]
|
||||
return '<%s:%s>' % (self.__class__.__name__, title.encode('utf8'))
|
||||
|
||||
|
||||
class MediaPart(object):
|
||||
class MediaPart(PlexObject):
|
||||
""" Represents a single media part (often a single file) for the media this belongs to.
|
||||
|
||||
Attributes:
|
||||
|
@ -77,37 +76,23 @@ class MediaPart(object):
|
|||
"""
|
||||
TYPE = 'Part'
|
||||
|
||||
def __init__(self, server, data, initpath, media):
|
||||
self.server = server
|
||||
self.initpath = initpath
|
||||
self.media = media
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self.container = data.attrib.get('container')
|
||||
self.duration = cast(int, data.attrib.get('duration'))
|
||||
self.file = data.attrib.get('file')
|
||||
self.id = cast(int, data.attrib.get('id'))
|
||||
self.key = data.attrib.get('key')
|
||||
self.size = cast(int, data.attrib.get('size'))
|
||||
self.streams = [MediaPartStream.parse(self.server, e, self.initpath, self) for e in data if e.tag == 'Stream']
|
||||
self.videoStreams = self._buildSubitems(data, VideoStream, 'Stream', {'streamType':VideoStream.STREAMTYPE})
|
||||
self.audioStreams = self._buildSubitems(data, AudioStream, 'Stream', {'streamType':AudioStream.STREAMTYPE})
|
||||
self.subtitleStreams = self._buildSubitems(data, SubtitleStream, 'Stream', {'streamType':SubtitleStream.STREAMTYPE})
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s:%s>' % (self.__class__.__name__, self.id)
|
||||
|
||||
def selectedStream(self, stream_type):
|
||||
""" Return the selected stream for the specified stream_type.
|
||||
|
||||
Paramters:
|
||||
stream_type (int): Specify which stream type you want the result for. This value
|
||||
should be one of (1=:class:`~plexapi.media.VideoStream`,
|
||||
2=:class:`~plexapi.media.AudioStream`, 3=:class:`~plexapi.media.SubtitleStream`).
|
||||
"""
|
||||
streams = filter(lambda x: stream_type == x.type, self.streams)
|
||||
selected = list(filter(lambda x: x.selected is True, streams))
|
||||
if len(selected) == 0:
|
||||
return None
|
||||
return selected[0]
|
||||
|
||||
|
||||
class MediaPartStream(object):
|
||||
class MediaPartStream(PlexObject):
|
||||
""" Base class for media streams. These consist of video, audio and subtitles.
|
||||
|
||||
Attributes:
|
||||
|
@ -128,10 +113,8 @@ class MediaPartStream(object):
|
|||
TYPE = None
|
||||
STREAMTYPE = None
|
||||
|
||||
def __init__(self, server, data, initpath, part):
|
||||
self.server = server
|
||||
self.initpath = initpath
|
||||
self.part = part
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self.codec = data.attrib.get('codec')
|
||||
self.codecID = data.attrib.get('codecID')
|
||||
self.id = cast(int, data.attrib.get('id'))
|
||||
|
@ -143,12 +126,12 @@ class MediaPartStream(object):
|
|||
self.type = cast(int, data.attrib.get('streamType'))
|
||||
|
||||
@staticmethod
|
||||
def parse(server, data, initpath, part):
|
||||
def parse(server, data, initpath):
|
||||
""" Factory method returns a new MediaPartStream from xml data. """
|
||||
STREAMCLS = {1:VideoStream, 2:AudioStream, 3:SubtitleStream}
|
||||
stype = cast(int, data.attrib.get('streamType'))
|
||||
cls = STREAMCLS.get(stype, MediaPartStream)
|
||||
return cls(server, data, initpath, part)
|
||||
return cls(server, data, initpath)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s:%s>' % (self.__class__.__name__, self.id)
|
||||
|
@ -178,8 +161,8 @@ class VideoStream(MediaPartStream):
|
|||
TYPE = 'videostream'
|
||||
STREAMTYPE = 1
|
||||
|
||||
def __init__(self, server, data, initpath, part):
|
||||
super(VideoStream, self).__init__(server, data, initpath, part)
|
||||
def _loadData(self, data):
|
||||
super(VideoStream, self)._loadData(data)
|
||||
self.bitDepth = cast(int, data.attrib.get('bitDepth'))
|
||||
self.bitrate = cast(int, data.attrib.get('bitrate'))
|
||||
self.cabac = cast(int, data.attrib.get('cabac'))
|
||||
|
@ -215,8 +198,8 @@ class AudioStream(MediaPartStream):
|
|||
TYPE = 'audiostream'
|
||||
STREAMTYPE = 2
|
||||
|
||||
def __init__(self, server, data, initpath, part):
|
||||
super(AudioStream, self).__init__(server, data, initpath, part)
|
||||
def _loadData(self, data):
|
||||
super(AudioStream, self)._loadData(data)
|
||||
self.audioChannelLayout = data.attrib.get('audioChannelLayout')
|
||||
self.bitDepth = cast(int, data.attrib.get('bitDepth'))
|
||||
self.bitrate = cast(int, data.attrib.get('bitrate'))
|
||||
|
@ -239,21 +222,21 @@ class SubtitleStream(MediaPartStream):
|
|||
TYPE = 'subtitlestream'
|
||||
STREAMTYPE = 3
|
||||
|
||||
def __init__(self, server, data, initpath, part):
|
||||
super(SubtitleStream, self).__init__(server, data, initpath, part)
|
||||
def _loadData(self, data):
|
||||
super(SubtitleStream, self)._loadData(data)
|
||||
self.format = data.attrib.get('format')
|
||||
self.key = data.attrib.get('key')
|
||||
self.title = data.attrib.get('title')
|
||||
|
||||
|
||||
class TranscodeSession(object):
|
||||
class TranscodeSession(PlexObject):
|
||||
""" Represents a current transcode session.
|
||||
TODO: Document this.
|
||||
"""
|
||||
TYPE = 'TranscodeSession'
|
||||
|
||||
def __init__(self, server, data):
|
||||
self.server = server
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self.audioChannels = cast(int, data.attrib.get('audioChannels'))
|
||||
self.audioCodec = data.attrib.get('audioCodec')
|
||||
self.audioDecision = data.attrib.get('audioDecision')
|
||||
|
@ -272,7 +255,7 @@ class TranscodeSession(object):
|
|||
self.width = cast(int, data.attrib.get('width'))
|
||||
|
||||
|
||||
class MediaTag(object):
|
||||
class MediaTag(PlexObject):
|
||||
""" Base class for media tags used for filtering and searching your library
|
||||
items or navigating the metadata of media items in your library. Tags are
|
||||
the construct used for things such as Country, Director, Genre, etc.
|
||||
|
@ -300,9 +283,8 @@ class MediaTag(object):
|
|||
"""
|
||||
TYPE = None
|
||||
|
||||
def __init__(self, server, data):
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self.server = server
|
||||
self.id = cast(int, data.attrib.get('id'))
|
||||
self.role = data.attrib.get('role')
|
||||
self.tag = data.attrib.get('tag')
|
||||
|
@ -333,51 +315,44 @@ class Collection(MediaTag):
|
|||
TYPE = 'Collection'
|
||||
FILTER = 'collection'
|
||||
|
||||
|
||||
class Country(MediaTag):
|
||||
TYPE = 'Country'
|
||||
FILTER = 'country'
|
||||
|
||||
|
||||
class Director(MediaTag):
|
||||
TYPE = 'Director'
|
||||
FILTER = 'director'
|
||||
|
||||
|
||||
class Genre(MediaTag):
|
||||
TYPE = 'Genre'
|
||||
FILTER = 'genre'
|
||||
|
||||
|
||||
class Mood(MediaTag):
|
||||
TYPE = 'Mood'
|
||||
FILTER = 'mood'
|
||||
|
||||
|
||||
class Producer(MediaTag):
|
||||
TYPE = 'Producer'
|
||||
FILTER = 'producer'
|
||||
|
||||
|
||||
class Role(MediaTag):
|
||||
TYPE = 'Role'
|
||||
FILTER = 'role'
|
||||
|
||||
|
||||
class Similar(MediaTag):
|
||||
TYPE = 'Similar'
|
||||
FILTER = 'similar'
|
||||
|
||||
|
||||
class Writer(MediaTag):
|
||||
TYPE = 'Writer'
|
||||
FILTER = 'writer'
|
||||
|
||||
|
||||
class Field(object):
|
||||
class Field(PlexObject):
|
||||
TYPE = 'Field'
|
||||
|
||||
def __init__(self, data):
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self.name = data.attrib.get('name')
|
||||
self.locked = cast(bool, data.attrib.get('locked'))
|
||||
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import plexapi, requests
|
||||
from plexapi import TIMEOUT, log, logfilter, utils
|
||||
from plexapi.exceptions import BadRequest, NotFound, Unauthorized
|
||||
import requests
|
||||
from requests.status_codes import _codes as codes
|
||||
from plexapi import BASE_HEADERS, CONFIG, TIMEOUT
|
||||
from plexapi import log, logfilter, utils
|
||||
from plexapi.base import PlexObject
|
||||
from plexapi.exceptions import BadRequest, NotFound
|
||||
from plexapi.client import PlexClient
|
||||
from plexapi.compat import ElementTree
|
||||
from plexapi.server import PlexServer
|
||||
from requests.status_codes import _codes as codes
|
||||
CONFIG = plexapi.CONFIG
|
||||
|
||||
|
||||
class MyPlexAccount(object):
|
||||
class MyPlexAccount(PlexObject):
|
||||
""" MyPlex account and profile information. The easiest way to build
|
||||
this object is by calling the staticmethod :func:`~plexapi.myplex.MyPlexAccount.signin`
|
||||
with your username and password. This object represents the data found Account on
|
||||
|
@ -43,15 +44,26 @@ class MyPlexAccount(object):
|
|||
username (str): Your account username.
|
||||
uuid (str): <Unknown>
|
||||
"""
|
||||
BASEURL = 'https://plex.tv/users/account'
|
||||
SIGNIN = 'https://my.plexapp.com/users/sign_in.xml'
|
||||
key = 'https://plex.tv/users/account'
|
||||
|
||||
def __init__(self, data=None, initpath=None, session=None):
|
||||
self._data = data
|
||||
def __init__(self, username=None, password=None, session=None):
|
||||
self._session = session or requests.Session()
|
||||
self.authenticationToken = data.attrib.get('authenticationToken')
|
||||
if self.authenticationToken:
|
||||
logfilter.add_secret(self.authenticationToken)
|
||||
self._token = None
|
||||
username = username or CONFIG.get('authentication.myplex_username')
|
||||
password = password or CONFIG.get('authentication.myplex_password')
|
||||
data = self._query(self.SIGNIN, method=self._session.post, auth=(username, password))
|
||||
super(MyPlexAccount, self).__init__(self, data, self.SIGNIN)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s:%s:%s>' % (self.__class__.__name__, self.id, self.username.encode('utf8'))
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self._token = data.attrib.get('authenticationToken')
|
||||
if self._token:
|
||||
logfilter.add_secret(self._token)
|
||||
self.authenticationToken = self._token
|
||||
self.certificateVersion = data.attrib.get('certificateVersion')
|
||||
self.cloudSyncDevice = data.attrib.get('cloudSyncDevice')
|
||||
self.email = data.attrib.get('email')
|
||||
|
@ -79,8 +91,20 @@ class MyPlexAccount(object):
|
|||
self.roles = None
|
||||
self.entitlements = None
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s:%s:%s>' % (self.__class__.__name__, self.id, self.username.encode('utf8'))
|
||||
def _query(self, url, method=None, headers=None, **kwargs):
|
||||
method = method or self._session.get
|
||||
delim = '&' if '?' in url else '?'
|
||||
url = '%s%sX-Plex-Token=%s' % (url, delim, self._token)
|
||||
log.info('%s %s', method.__name__.upper(), url)
|
||||
allheaders = BASE_HEADERS.copy()
|
||||
allheaders.update(headers or {})
|
||||
response = method(url, headers=allheaders, timeout=TIMEOUT, **kwargs)
|
||||
if response.status_code not in (200, 201):
|
||||
codename = codes.get(response.status_code)[0]
|
||||
log.warn('BadRequest (%s) %s %s' % (response.status_code, codename, response.url))
|
||||
raise BadRequest('(%s) %s %s' % (response.status_code, codename, response.url))
|
||||
text = response.text.encode('utf8')
|
||||
return ElementTree.fromstring(text) if text else None
|
||||
|
||||
def device(self, name):
|
||||
""" Returns the :class:`~plexapi.myplex.MyPlexDevice` that matches the name specified.
|
||||
|
@ -88,15 +112,15 @@ class MyPlexAccount(object):
|
|||
Parameters:
|
||||
name (str): Name to match against.
|
||||
"""
|
||||
return _findItem(self.devices(), name)
|
||||
for device in self.devices():
|
||||
if device.name.lower() == name.lower():
|
||||
return device
|
||||
raise NotFound('Unable to find device %s' % name)
|
||||
|
||||
def devices(self):
|
||||
""" Returns a list of all :class:`~plexapi.myplex.MyPlexDevice` objects connected to the server. """
|
||||
return _listItems(MyPlexDevice.BASEURL, self.authenticationToken, MyPlexDevice)
|
||||
|
||||
def resources(self):
|
||||
""" Returns a list of all :class:`~plexapi.myplex.MyPlexResource` objects connected to the server. """
|
||||
return _listItems(MyPlexResource.BASEURL, self.authenticationToken, MyPlexResource)
|
||||
data = self._query(MyPlexDevice.key)
|
||||
return [MyPlexDevice(self, elem) for elem in data]
|
||||
|
||||
def resource(self, name):
|
||||
""" Returns the :class:`~plexapi.myplex.MyPlexResource` that matches the name specified.
|
||||
|
@ -104,11 +128,15 @@ class MyPlexAccount(object):
|
|||
Parameters:
|
||||
name (str): Name to match against.
|
||||
"""
|
||||
return _findItem(self.resources(), name)
|
||||
for resource in self.resources():
|
||||
if resource.name.lower() == name.lower():
|
||||
return resource
|
||||
raise NotFound('Unable to find resource %s' % name)
|
||||
|
||||
def users(self):
|
||||
""" Returns a list of all :class:`~plexapi.myplex.MyPlexUser` objects connected to your account. """
|
||||
return _listItems(MyPlexUser.BASEURL, self.authenticationToken, MyPlexUser)
|
||||
def resources(self):
|
||||
""" Returns a list of all :class:`~plexapi.myplex.MyPlexResource` objects connected to the server. """
|
||||
data = self._query(MyPlexResource.key)
|
||||
return [MyPlexResource(self, elem) for elem in data]
|
||||
|
||||
def user(self, email):
|
||||
""" Returns the :class:`~myplex.MyPlexUser` that matches the email or username specified.
|
||||
|
@ -116,41 +144,18 @@ class MyPlexAccount(object):
|
|||
Parameters:
|
||||
email (str): Username or email to match against.
|
||||
"""
|
||||
return _findItem(self.users(), email, ['username', 'email'])
|
||||
for user in self.users():
|
||||
if email.lower() in (user.username.lower(), user.email.lower()):
|
||||
return user
|
||||
raise NotFound('Unable to find user %s' % email)
|
||||
|
||||
@classmethod
|
||||
def signin(cls, username=None, password=None, session=None):
|
||||
""" Returns a new :class:`~myplex.MyPlexAccount` object by connecting to MyPlex with the
|
||||
specified username and password. This is essentially logging into MyPlex and often
|
||||
the very first entry point to using this API.
|
||||
|
||||
Parameters:
|
||||
username (str): Your MyPlex.tv username. If not specified, it will check the config.ini file.
|
||||
password (str): Your MyPlex.tv password. If not specified, it will check the config.ini file.
|
||||
|
||||
Raises:
|
||||
:class:`~plexapi.exceptions.Unauthorized`: (401) If the username or password are invalid.
|
||||
:class:`~plexapi.exceptions.BadRequest`: If any other errors occured not allowing us to log into MyPlex.tv.
|
||||
"""
|
||||
if 'X-Plex-Token' in plexapi.BASE_HEADERS:
|
||||
del plexapi.BASE_HEADERS['X-Plex-Token']
|
||||
username = username or CONFIG.get('authentication.username')
|
||||
password = password or CONFIG.get('authentication.password')
|
||||
auth = (username, password)
|
||||
log.info('POST %s', cls.SIGNIN)
|
||||
sess = session or requests.Session()
|
||||
response = sess.post(
|
||||
cls.SIGNIN, headers=plexapi.BASE_HEADERS, auth=auth, timeout=TIMEOUT)
|
||||
if response.status_code != requests.codes.created:
|
||||
codename = codes.get(response.status_code)[0]
|
||||
if response.status_code == 401:
|
||||
raise Unauthorized('(%s) %s' % (response.status_code, codename))
|
||||
raise BadRequest('(%s) %s' % (response.status_code, codename))
|
||||
data = ElementTree.fromstring(response.text.encode('utf8'))
|
||||
return MyPlexAccount(data, cls.SIGNIN, session=sess)
|
||||
def users(self):
|
||||
""" Returns a list of all :class:`~plexapi.myplex.MyPlexUser` objects connected to your account. """
|
||||
data = self._query(MyPlexUser.key)
|
||||
return [MyPlexUser(self, elem) for elem in data]
|
||||
|
||||
|
||||
class MyPlexUser(object):
|
||||
class MyPlexUser(PlexObject):
|
||||
""" This object represents non-signed in users such as friends and linked
|
||||
accounts. NOTE: This should not be confused with the :class:`~myplex.MyPlexAccount`
|
||||
which is your specific account. The raw xml for the data presented here
|
||||
|
@ -175,9 +180,9 @@ class MyPlexUser(object):
|
|||
title (str): Seems to be an aliad for username
|
||||
username (str): User's username
|
||||
"""
|
||||
BASEURL = 'https://plex.tv/api/users/'
|
||||
key = 'https://plex.tv/api/users/'
|
||||
|
||||
def __init__(self, data, initpath=None):
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self.allowCameraUpload = utils.cast(bool, data.attrib.get('allowCameraUpload'))
|
||||
self.allowChannels = utils.cast(bool, data.attrib.get('allowChannels'))
|
||||
|
@ -201,7 +206,7 @@ class MyPlexUser(object):
|
|||
return '<%s:%s:%s>' % (self.__class__.__name__, self.id, self.username)
|
||||
|
||||
|
||||
class MyPlexResource(object):
|
||||
class MyPlexResource(PlexObject):
|
||||
""" This object represents resources connected to your Plex server that can provide
|
||||
content such as Plex Media Servers, iPhone or Android clients, etc. The raw xml
|
||||
for the data presented here can be found at: https://plex.tv/api/resources?includeHttps=1
|
||||
|
@ -226,9 +231,9 @@ class MyPlexResource(object):
|
|||
player, pubsub-player, etc.)
|
||||
synced (bool): Unknown (possibly True if the resource has synced content?)
|
||||
"""
|
||||
BASEURL = 'https://plex.tv/api/resources?includeHttps=1'
|
||||
key = 'https://plex.tv/api/resources?includeHttps=1'
|
||||
|
||||
def __init__(self, data):
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self.name = data.attrib.get('name')
|
||||
self.accessToken = data.attrib.get('accessToken')
|
||||
|
@ -247,12 +252,12 @@ class MyPlexResource(object):
|
|||
self.home = utils.cast(bool, data.attrib.get('home'))
|
||||
self.synced = utils.cast(bool, data.attrib.get('synced'))
|
||||
self.presence = utils.cast(bool, data.attrib.get('presence'))
|
||||
self.connections = [ResourceConnection(elem) for elem in data if elem.tag == 'Connection']
|
||||
self.connections = self._buildSubitems(data, ResourceConnection)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s:%s>' % (self.__class__.__name__, self.name.encode('utf8'))
|
||||
|
||||
def connect(self, ssl=None):
|
||||
def connect(self, ssl=None, safe=False):
|
||||
""" Returns a new :class:`~server.PlexServer` object. Often times there is more than
|
||||
one address specified for a server or client. This function will prioritize local
|
||||
connections before remote and HTTPS before HTTP. After trying to connect to all
|
||||
|
@ -263,6 +268,8 @@ class MyPlexResource(object):
|
|||
ssl (optional): Set True to only connect to HTTPS connections. Set False to
|
||||
only connect to HTTP connections. Set None (default) to connect to any
|
||||
HTTP or HTTPS connection.
|
||||
safe (bool): If True this function will return None if unable to connect
|
||||
instead of raising an exception (default False).
|
||||
|
||||
Raises:
|
||||
:class:`~plexapi.exceptions.NotFound`: When unable to connect to any addresses for this resource.
|
||||
|
@ -289,18 +296,20 @@ class MyPlexResource(object):
|
|||
log.info('Testing resource connection: %s?X-Plex-Token=%s %s', url, token, okerr)
|
||||
results = [r[2] for r in results if r and r[2] is not None]
|
||||
if not results:
|
||||
if safe: return log.warn('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]
|
||||
|
||||
def _connect(self, url, results, i):
|
||||
try:
|
||||
results[i] = (url, self.accessToken, PlexServer(url, self.accessToken))
|
||||
except NotFound:
|
||||
except Exception as err:
|
||||
log.error('%s: %s', url, err)
|
||||
results[i] = (url, self.accessToken, None)
|
||||
|
||||
|
||||
class ResourceConnection(object):
|
||||
class ResourceConnection(PlexObject):
|
||||
""" Represents a Resource Connection object found within the
|
||||
:class:`~myplex.MyPlexResource` objects.
|
||||
|
||||
|
@ -312,7 +321,9 @@ class ResourceConnection(object):
|
|||
protocol (str): HTTP or HTTPS
|
||||
uri (str): External address
|
||||
"""
|
||||
def __init__(self, data):
|
||||
TYPE = 'Connection'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self.protocol = data.attrib.get('protocol')
|
||||
self.address = data.attrib.get('address')
|
||||
|
@ -325,7 +336,7 @@ class ResourceConnection(object):
|
|||
return '<%s:%s>' % (self.__class__.__name__, self.uri.encode('utf8'))
|
||||
|
||||
|
||||
class MyPlexDevice(object):
|
||||
class MyPlexDevice(PlexObject):
|
||||
""" This object represents resources connected to your Plex server that provide
|
||||
playback ability from your Plex Server, iPhone or Android clients, Plex Web,
|
||||
this API, etc. The raw xml for the data presented here can be found at:
|
||||
|
@ -351,9 +362,9 @@ class MyPlexDevice(object):
|
|||
vendor (str): Device vendor (ubuntu, etc).
|
||||
version (str): Unknown (1, 2, 1.3.3.3148-b38628e, 1.3.15, etc.)
|
||||
"""
|
||||
BASEURL = 'https://plex.tv/devices.xml'
|
||||
key = 'https://plex.tv/devices.xml'
|
||||
|
||||
def __init__(self, data):
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self.name = data.attrib.get('name')
|
||||
self.publicAddress = data.attrib.get('publicAddress')
|
||||
|
@ -378,16 +389,20 @@ class MyPlexDevice(object):
|
|||
def __repr__(self):
|
||||
return '<%s:%s:%s>' % (self.__class__.__name__, self.name.encode('utf8'), self.product.encode('utf8'))
|
||||
|
||||
def connect(self):
|
||||
def connect(self, safe=False):
|
||||
""" Returns a new :class:`~plexapi.client.PlexClient` object. Sometimes there is more than
|
||||
one address specified for a server or client. After trying to connect to all
|
||||
available addresses for this resource and assuming at least one connection was
|
||||
available addresses for this client and assuming at least one connection was
|
||||
successful, the PlexClient object is built and returned.
|
||||
|
||||
Parameters:
|
||||
safe (bool): If True this function will return None if unable to connect
|
||||
instead of raising an exception (default False).
|
||||
|
||||
Raises:
|
||||
:class:`~plexapi.exceptions.NotFound`: When unable to connect to any addresses for this device.
|
||||
"""
|
||||
# Try connecting to all known resource connections in parellel, but
|
||||
# Try connecting to all known clients in parellel, but
|
||||
# only return the first server (in order) that provides a response.
|
||||
listargs = [[c] for c in self.connections]
|
||||
results = utils.threaded(self._connect, listargs)
|
||||
|
@ -399,34 +414,14 @@ class MyPlexDevice(object):
|
|||
log.info('Testing device connection: %s?X-Plex-Token=%s %s', url, token, okerr)
|
||||
results = [r[2] for r in results if r and r[2] is not None]
|
||||
if not results:
|
||||
raise NotFound('Unable to connect to resource: %s' % self.name)
|
||||
log.info('Connecting to server: %s?X-Plex-Token=%s', results[0].baseurl, results[0].token)
|
||||
if safe: return log.warn('Unable to connect to client: %s' % self.name)
|
||||
raise NotFound('Unable to connect to client: %s' % self.name)
|
||||
log.info('Connecting to client: %s?X-Plex-Token=%s', results[0]._baseurl, results[0]._token)
|
||||
return results[0]
|
||||
|
||||
def _connect(self, url, results, i):
|
||||
try:
|
||||
results[i] = (url, self.token, PlexClient(url, self.token))
|
||||
except NotFound:
|
||||
except Exception as err:
|
||||
log.error('%s: %s', url, err)
|
||||
results[i] = (url, self.token, None)
|
||||
|
||||
|
||||
def _findItem(items, value, attrs=None):
|
||||
""" This will return the first item in the list of items where value is
|
||||
found in any of the specified attributes.
|
||||
"""
|
||||
attrs = attrs or ['name']
|
||||
for item in items:
|
||||
for attr in attrs:
|
||||
if value.lower() == getattr(item, attr).lower():
|
||||
return item
|
||||
raise NotFound('Unable to find item %s' % value)
|
||||
|
||||
|
||||
def _listItems(url, token, cls):
|
||||
""" Builds list of classes from a XML response. """
|
||||
headers = plexapi.BASE_HEADERS
|
||||
headers['X-Plex-Token'] = token
|
||||
log.info('GET %s?X-Plex-Token=%s', url, token)
|
||||
response = requests.get(url, headers=headers, timeout=TIMEOUT)
|
||||
data = ElementTree.fromstring(response.text.encode('utf8'))
|
||||
return [cls(elem) for elem in data]
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from plexapi import media, utils
|
||||
from plexapi.base import PlexPartialObject
|
||||
from plexapi.exceptions import NotFound
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
|
@ -30,9 +31,6 @@ class Photoalbum(PlexPartialObject):
|
|||
"""
|
||||
TYPE = 'photoalbum'
|
||||
|
||||
def __init__(self, server, data, initpath):
|
||||
super(Photoalbum, self).__init__(data, initpath, server)
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.listType = 'photo'
|
||||
|
@ -52,17 +50,19 @@ class Photoalbum(PlexPartialObject):
|
|||
|
||||
def photos(self):
|
||||
""" Returns a list of :class:`~plexapi.photo.Photo` objects in this album. """
|
||||
path = '/library/metadata/%s/children' % self.ratingKey
|
||||
return utils.listItems(self.server, path, Photo.TYPE)
|
||||
key = '/library/metadata/%s/children' % self.ratingKey
|
||||
return self._fetchItems(key)
|
||||
|
||||
def photo(self, title):
|
||||
""" Returns the :class:`~plexapi.photo.Photo` that matches the specified title. """
|
||||
path = '/library/metadata/%s/children' % self.ratingKey
|
||||
return utils.findItem(self.server, path, title)
|
||||
for photo in self.photos():
|
||||
if photo.attrib.get('title').lower() == title.lower():
|
||||
return photo
|
||||
raise NotFound('Unable to find photo: %s' % title)
|
||||
|
||||
def section(self):
|
||||
""" Returns the :class:`~plexapi.library.LibrarySection` this item belongs to. """
|
||||
return self.server.library.sectionByID(self.librarySectionID)
|
||||
return self._root.library.sectionByID(self.librarySectionID)
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
|
@ -93,9 +93,6 @@ class Photo(PlexPartialObject):
|
|||
"""
|
||||
TYPE = 'photo'
|
||||
|
||||
def __init__(self, server, data, initpath):
|
||||
super(Photo, self).__init__(data, initpath, server)
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.listType = 'photo'
|
||||
|
@ -113,14 +110,12 @@ class Photo(PlexPartialObject):
|
|||
self.type = data.attrib.get('type')
|
||||
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
|
||||
self.year = utils.cast(int, data.attrib.get('year'))
|
||||
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 = self._buildSubitems(data, media.Media)
|
||||
|
||||
def photoalbum(self):
|
||||
""" Return this photo's :class:`~plexapi.photo.Photoalbum`. """
|
||||
return utils.listItems(self.server, self.parentKey)[0]
|
||||
return utils.listItems(self._root, self.parentKey)[0]
|
||||
|
||||
def section(self):
|
||||
""" Returns the :class:`~plexapi.library.LibrarySection` this item belongs to. """
|
||||
return self.server.library.sectionByID(self.photoalbum().librarySectionID)
|
||||
return self._root.library.sectionByID(self.photoalbum().librarySectionID)
|
||||
|
|
|
@ -9,23 +9,7 @@ from plexapi.utils import cast, toDatetime
|
|||
class Playlist(PlexPartialObject, Playable):
|
||||
TYPE = 'playlist'
|
||||
|
||||
def __init__(self, server, data, initpath):
|
||||
"""Playlist stuff.
|
||||
|
||||
Args:
|
||||
server (Plexserver): The PMS server your connected to
|
||||
data (Element): Element built from server.query
|
||||
initpath (str): Relativ path fx /library/sections/1/all
|
||||
|
||||
"""
|
||||
super(Playlist, self).__init__(data, initpath, server)
|
||||
|
||||
def _loadData(self, data):
|
||||
"""Used to set the attributes
|
||||
|
||||
Args:
|
||||
data (Element): Usually built from server.query
|
||||
"""
|
||||
Playable._loadData(self, data)
|
||||
self.addedAt = toDatetime(data.attrib.get('addedAt'))
|
||||
self.composite = data.attrib.get('composite') # url to thumbnail
|
||||
|
@ -44,9 +28,9 @@ class Playlist(PlexPartialObject, Playable):
|
|||
self.updatedAt = toDatetime(data.attrib.get('updatedAt'))
|
||||
|
||||
def items(self):
|
||||
"""Return all items in the playlist."""
|
||||
path = '%s/items' % self.key
|
||||
return utils.listItems(self.server, path)
|
||||
""" Returns a list of all items in the playlist. """
|
||||
key = '%s/items' % self.key
|
||||
return self._fetchItems(key)
|
||||
|
||||
def addItems(self, items):
|
||||
"""Add items to a playlist."""
|
||||
|
@ -62,28 +46,28 @@ class Playlist(PlexPartialObject, Playable):
|
|||
path = '%s/items%s' % (self.key, utils.joinArgs({
|
||||
'uri': 'library://%s/directory//library/metadata/%s' % (uuid, ratingKeys),
|
||||
}))
|
||||
return self.server.query(path, method=self.server.session.put)
|
||||
return self._root._query(path, method=self._root._session.put)
|
||||
|
||||
def removeItem(self, item):
|
||||
"""Remove a file from a playlist."""
|
||||
path = '%s/items/%s' % (self.key, item.playlistItemID)
|
||||
return self.server.query(path, method=self.server.session.delete)
|
||||
return self._root._query(path, method=self._root._session.delete)
|
||||
|
||||
def moveItem(self, item, after=None):
|
||||
"""Move a to a new position in playlist."""
|
||||
path = '%s/items/%s/move' % (self.key, item.playlistItemID)
|
||||
if after:
|
||||
path += '?after=%s' % after.playlistItemID
|
||||
return self.server.query(path, method=self.server.session.put)
|
||||
return self._root._query(path, method=self._root._session.put)
|
||||
|
||||
def edit(self, title=None, summary=None):
|
||||
"""Edit playlist."""
|
||||
path = '/library/metadata/%s%s' % (self.ratingKey, utils.joinArgs({'title':title, 'summary':summary}))
|
||||
return self.server.query(path, method=self.server.session.put)
|
||||
return self._root._query(path, method=self._root._session.put)
|
||||
|
||||
def delete(self):
|
||||
"""Delete playlist."""
|
||||
return self.server.query(self.key, method=self.server.session.delete)
|
||||
return self._root._query(self.key, method=self._root._session.delete)
|
||||
|
||||
@classmethod
|
||||
def create(cls, server, title, items):
|
||||
|
@ -91,12 +75,10 @@ class Playlist(PlexPartialObject, Playable):
|
|||
if not isinstance(items, (list, tuple)):
|
||||
items = [items]
|
||||
ratingKeys = []
|
||||
|
||||
for item in items:
|
||||
if item.listType != items[0].listType:
|
||||
raise BadRequest('Can not mix media types when building a playlist')
|
||||
ratingKeys.append(str(item.ratingKey))
|
||||
|
||||
ratingKeys = ','.join(ratingKeys)
|
||||
uuid = items[0].section().uuid
|
||||
path = '/playlists%s' % utils.joinArgs({
|
||||
|
@ -105,6 +87,5 @@ class Playlist(PlexPartialObject, Playable):
|
|||
'title': title,
|
||||
'smart': 0
|
||||
})
|
||||
|
||||
data = server.query(path, method=server.session.post)[0]
|
||||
data = server._query(path, method=server._session.post)[0]
|
||||
return cls(server, data, initpath=path)
|
||||
|
|
|
@ -1,35 +1,27 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import plexapi
|
||||
import requests
|
||||
from plexapi import utils
|
||||
from plexapi.base import PlexObject
|
||||
|
||||
|
||||
class PlayQueue(object):
|
||||
"""Summary
|
||||
class PlayQueue(PlexObject):
|
||||
""" Summary
|
||||
|
||||
Attributes:
|
||||
identifier (TYPE): Description
|
||||
initpath (TYPE): Description
|
||||
items (TYPE): Description
|
||||
mediaTagPrefix (TYPE): Description
|
||||
mediaTagVersion (TYPE): Description
|
||||
playQueueID (TYPE): Description
|
||||
playQueueSelectedItemID (TYPE): Description
|
||||
playQueueSelectedItemOffset (TYPE): Description
|
||||
playQueueTotalCount (TYPE): Description
|
||||
playQueueVersion (TYPE): Description
|
||||
server (TYPE): Description
|
||||
"""
|
||||
def __init__(self, server, data, initpath):
|
||||
"""Summary
|
||||
|
||||
Args:
|
||||
server (TYPE): Description
|
||||
data (TYPE): Description
|
||||
Attributes:
|
||||
identifier (TYPE): Description
|
||||
initpath (TYPE): Description
|
||||
"""
|
||||
self.server = server
|
||||
self.initpath = initpath
|
||||
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 _loadData(self, data):
|
||||
self._data = data
|
||||
self.identifier = data.attrib.get('identifier')
|
||||
self.mediaTagPrefix = data.attrib.get('mediaTagPrefix')
|
||||
self.mediaTagVersion = data.attrib.get('mediaTagVersion')
|
||||
|
@ -38,22 +30,22 @@ class PlayQueue(object):
|
|||
self.playQueueSelectedItemOffset = data.attrib.get('playQueueSelectedItemOffset')
|
||||
self.playQueueTotalCount = data.attrib.get('playQueueTotalCount')
|
||||
self.playQueueVersion = data.attrib.get('playQueueVersion')
|
||||
self.items = [utils.buildItem(server, elem, initpath) for elem in data]
|
||||
self.items = [self._buildItem(elem, self._initpath) for elem in data]
|
||||
|
||||
@classmethod
|
||||
def create(cls, server, item, shuffle=0, repeat=0, includeChapters=1, includeRelated=1):
|
||||
"""Summary
|
||||
""" Create a new playqueue
|
||||
|
||||
Args:
|
||||
server (TYPE): Description
|
||||
item (TYPE): Description
|
||||
shuffle (int, optional): Description
|
||||
repeat (int, optional): Description
|
||||
includeChapters (int, optional): Description
|
||||
includeRelated (int, optional): Description
|
||||
Paramaters:
|
||||
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
|
||||
Returns:
|
||||
TYPE: Description
|
||||
"""
|
||||
args = {}
|
||||
args['includeChapters'] = includeChapters
|
||||
|
@ -69,5 +61,5 @@ class PlayQueue(object):
|
|||
args['type'] = item.listType
|
||||
args['uri'] = 'library://%s/item/%s' % (uuid, item.key)
|
||||
path = '/playQueues%s' % utils.joinArgs(args)
|
||||
data = server.query(path, method=requests.post)
|
||||
data = server._query(path, method=requests.post)
|
||||
return cls(server, data, initpath=path)
|
||||
|
|
|
@ -3,6 +3,7 @@ import requests
|
|||
from requests.status_codes import _codes as codes
|
||||
from plexapi import BASE_HEADERS, CONFIG, TIMEOUT
|
||||
from plexapi import log, logfilter, utils
|
||||
from plexapi.base import PlexObject
|
||||
from plexapi.client import PlexClient
|
||||
from plexapi.compat import ElementTree, urlencode
|
||||
from plexapi.exceptions import BadRequest, NotFound
|
||||
|
@ -10,11 +11,11 @@ from plexapi.library import Library
|
|||
from plexapi.playlist import Playlist
|
||||
from plexapi.playqueue import PlayQueue
|
||||
from plexapi.utils import cast
|
||||
# import media to populate utils.LIBRARY_TYPES.
|
||||
# We import media to populate utils.LIBRARY_TYPES
|
||||
from plexapi import audio, video, photo, playlist as _pl
|
||||
|
||||
|
||||
class PlexServer(object):
|
||||
class PlexServer(PlexObject):
|
||||
""" This is the main entry point to interacting with a Plex server. It allows you to
|
||||
list connected clients, browse your library sections and perform actions such as
|
||||
emptying trash. If you do not know the auth token required to access your Plex
|
||||
|
@ -83,14 +84,16 @@ class PlexServer(object):
|
|||
version (str): Current Plex version (ex: 1.3.2.3112-1751929)
|
||||
voiceSearch (bool): True if voice search is enabled. (is this Google Voice search?)
|
||||
"""
|
||||
key = '/'
|
||||
|
||||
def __init__(self, baseurl='http://localhost:32400', token=None, session=None):
|
||||
self.baseurl = baseurl or CONFIG.get('authentication.baseurl')
|
||||
self.token = token or CONFIG.get('authentication.token')
|
||||
if self.token:
|
||||
logfilter.add_secret(self.token)
|
||||
self.session = session or requests.Session()
|
||||
self._baseurl = baseurl or CONFIG.get('authentication.server_baseurl')
|
||||
self._token = token or CONFIG.get('authentication.server_token')
|
||||
if self._token:
|
||||
logfilter.add_secret(self._token)
|
||||
self._session = session or requests.Session()
|
||||
self._library = None # cached library
|
||||
self.reload()
|
||||
super(PlexServer, self).__init__(self, self._query(self.key), self.key)
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
|
@ -138,18 +141,51 @@ class PlexServer(object):
|
|||
self.voiceSearch = cast(bool, data.attrib.get('voiceSearch'))
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s:%s>' % (self.__class__.__name__, self.baseurl)
|
||||
return '<%s:%s>' % (self.__class__.__name__, self._baseurl)
|
||||
|
||||
def _query(self, key, method=None, headers=None, **kwargs):
|
||||
""" Main method used to handle HTTPS requests to the Plex server. This method helps
|
||||
by encoding the response to utf-8 and parsing the returned XML into and
|
||||
ElementTree object. Returns None if no data exists in the response.
|
||||
"""
|
||||
url = self._url(key)
|
||||
method = method or self._session.get
|
||||
log.info('%s %s', method.__name__.upper(), url)
|
||||
headers = self._headers(**headers or {})
|
||||
response = method(url, headers=headers, timeout=TIMEOUT, **kwargs)
|
||||
if response.status_code not in (200, 201):
|
||||
codename = codes.get(response.status_code)[0]
|
||||
log.warn('BadRequest (%s) %s %s' % (response.status_code, codename, response.url))
|
||||
raise BadRequest('(%s) %s %s' % (response.status_code, codename, response.url))
|
||||
data = response.text.encode('utf8')
|
||||
return ElementTree.fromstring(data) if data else None
|
||||
|
||||
def _headers(self, **kwargs):
|
||||
""" Returns dict containing base headers for all requests to the server. """
|
||||
headers = BASE_HEADERS.copy()
|
||||
if self._token:
|
||||
headers['X-Plex-Token'] = self._token
|
||||
headers.update(kwargs)
|
||||
return headers
|
||||
|
||||
def _url(self, key):
|
||||
""" Build a URL string with proper token argument. """
|
||||
if self._token:
|
||||
delim = '&' if '?' in key else '?'
|
||||
return '%s%s%sX-Plex-Token=%s' % (self._baseurl, key, delim, self._token)
|
||||
return '%s%s' % (self._baseurl, key)
|
||||
|
||||
@property
|
||||
def library(self):
|
||||
""" Library to browse or search your media. """
|
||||
if not self._library:
|
||||
self._library = Library(self, self.query('/library/'))
|
||||
data = self._query(Library.key)
|
||||
self._library = Library(self, data)
|
||||
return self._library
|
||||
|
||||
def account(self):
|
||||
""" Returns the :class:`~plexapi.server.Account` object this server belongs to. """
|
||||
data = self.query('/myplex/account')
|
||||
data = self._query(Account.key)
|
||||
return Account(self, data)
|
||||
|
||||
def clients(self):
|
||||
|
@ -157,7 +193,8 @@ class PlexServer(object):
|
|||
connected to this server.
|
||||
"""
|
||||
items = []
|
||||
for elem in self.query('/clients'):
|
||||
for elem in self._query('/clients'):
|
||||
print(elem.attrib)
|
||||
baseurl = 'http://%s:%s' % (elem.attrib['host'], elem.attrib['port'])
|
||||
items.append(PlexClient(baseurl, server=self, data=elem))
|
||||
return items
|
||||
|
@ -171,7 +208,7 @@ class PlexServer(object):
|
|||
Raises:
|
||||
:class:`~plexapi.exceptions.NotFound`: Unknown client name
|
||||
"""
|
||||
for elem in self.query('/clients'):
|
||||
for elem in self._query('/clients'):
|
||||
if elem.attrib.get('name').lower() == name.lower():
|
||||
baseurl = 'http://%s:%s' % (elem.attrib['host'], elem.attrib['port'])
|
||||
return PlexClient(baseurl, server=self, data=elem)
|
||||
|
@ -194,22 +231,16 @@ class PlexServer(object):
|
|||
"""
|
||||
return PlayQueue.create(self, item)
|
||||
|
||||
def headers(self):
|
||||
""" Returns a dict containing base headers to include in all requests to the server. """
|
||||
headers = BASE_HEADERS
|
||||
if self.token:
|
||||
headers['X-Plex-Token'] = self.token
|
||||
return headers
|
||||
|
||||
def history(self):
|
||||
""" Returns a list of media items from watched history. """
|
||||
return utils.listItems(self, '/status/sessions/history/all')
|
||||
#return utils.listItems(self, '/status/sessions/history/all')
|
||||
return self._fetchItems('/status/sessions/history/all')
|
||||
|
||||
def playlists(self):
|
||||
""" Returns a list of all :class:`~plexapi.playlist.Playlist` objects saved on the server. """
|
||||
# TODO: Add sort and type options?
|
||||
# /playlists/all?type=15&sort=titleSort%3Aasc&playlistType=video&smart=0
|
||||
return utils.listItems(self, '/playlists')
|
||||
return self._fetchItems('/playlists')
|
||||
|
||||
def playlist(self, title):
|
||||
""" Returns the :class:`~plexapi.client.Playlist` that matches the specified title.
|
||||
|
@ -225,45 +256,6 @@ class PlexServer(object):
|
|||
return item
|
||||
raise NotFound('Invalid playlist title: %s' % title)
|
||||
|
||||
def query(self, path, method=None, headers=None, **kwargs):
|
||||
""" Main method used to handle HTTPS requests to the Plex server. This method helps
|
||||
by encoding the response to utf-8 and parsing the returned XML into and
|
||||
ElementTree object. Returns None if no data exists in the response.
|
||||
|
||||
Parameters:
|
||||
path (str): Relative path to query on the server api (ex: '/search?query=HELLO')
|
||||
method (func): requests.method to use for this query (request.get or
|
||||
requests.put; defaults to get)
|
||||
headers (dict): Optionally include additional headers for this request.
|
||||
**kwargs (dict): Optionally include additional kwargs for in the specified
|
||||
reuqest method. These kwargs are simply passed through to method().
|
||||
|
||||
Raises:
|
||||
:class:`~plexapi.exceptions.BadRequest`: Raised when response is not in (200, 201).
|
||||
"""
|
||||
url = self.url(path)
|
||||
method = method or self.session.get
|
||||
log.info('%s %s', method.__name__.upper(), url)
|
||||
h = self.headers().copy()
|
||||
if headers:
|
||||
h.update(headers)
|
||||
response = method(url, headers=h, timeout=TIMEOUT, **kwargs)
|
||||
#print(response.url)
|
||||
if response.status_code not in [200, 201]: # pragma: no cover
|
||||
codename = codes.get(response.status_code)[0]
|
||||
raise BadRequest('(%s) %s %s' % (response.status_code, codename, response.url))
|
||||
data = response.text.encode('utf8')
|
||||
return ElementTree.fromstring(data) if data else None
|
||||
|
||||
def reload(self):
|
||||
""" Reload attribute values from Plex XML response. """
|
||||
try:
|
||||
data = self.query('/')
|
||||
self._loadData(data)
|
||||
except Exception as err:
|
||||
log.error('%s: %s', self.baseurl, err)
|
||||
raise NotFound('No server found at: %s' % self.baseurl)
|
||||
|
||||
def search(self, query, mediatype=None, limit=None):
|
||||
""" Returns a list of media items or filter categories from the resulting
|
||||
`Hub Search <https://www.plex.tv/blog/seek-plex-shall-find-leveling-web-app/>`_
|
||||
|
@ -294,16 +286,8 @@ class PlexServer(object):
|
|||
|
||||
def sessions(self):
|
||||
""" Returns a list of all active session (currently playing) media objects. """
|
||||
return utils.listItems(self, '/status/sessions')
|
||||
|
||||
def url(self, path):
|
||||
""" Utility function to help build proper URL strings as well as always include
|
||||
the requred authentication token for all api requests to the server.
|
||||
"""
|
||||
if self.token:
|
||||
delim = '&' if '?' in path else '?'
|
||||
return '%s%s%sX-Plex-Token=%s' % (self.baseurl, path, delim, self.token)
|
||||
return '%s%s' % (self.baseurl, path)
|
||||
return self._fetchItems('/status/sessions')
|
||||
#return utils.listItems(self, '/status/sessions')
|
||||
|
||||
def transcodeImage(self, media, height, width, opacity=100, saturation=100):
|
||||
""" Returns the URL for a transcoded image from the specified media object.
|
||||
|
@ -322,7 +306,7 @@ class PlexServer(object):
|
|||
return self.url(transcode_url)
|
||||
|
||||
|
||||
class Account(object):
|
||||
class Account(PlexObject):
|
||||
""" Contains the locally cached MyPlex account information. The properties provided don't
|
||||
match the :class:`~plexapi.myplex.MyPlexAccount` object very well. I believe this exists
|
||||
because access to myplex is not required to get basic plex information. I can't imagine
|
||||
|
@ -351,7 +335,9 @@ class Account(object):
|
|||
subscriptionState (str): 'Active' if this subscription is active.
|
||||
username (str): Plex account username (user@example.com).
|
||||
"""
|
||||
def __init__(self, server, data):
|
||||
key = '/myplex/account'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self.authToken = data.attrib.get('authToken')
|
||||
self.username = data.attrib.get('username')
|
||||
|
|
|
@ -5,32 +5,29 @@ from plexapi.exceptions import NotFound
|
|||
|
||||
|
||||
class SyncItem(object):
|
||||
"""Summary
|
||||
""" Sync Item (NOT WORKING?)
|
||||
|
||||
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
|
||||
Attributes:
|
||||
device (TYPE): Description
|
||||
id (TYPE): Description
|
||||
location (TYPE): Description
|
||||
machineIdentifier (TYPE): Description
|
||||
MediaSettings (TYPE): Description
|
||||
metadataType (TYPE): Description
|
||||
policy (TYPE): Description
|
||||
rootTitle (TYPE): Description
|
||||
servers (TYPE): Description
|
||||
status (TYPE): Description
|
||||
title (TYPE): Description
|
||||
version (TYPE): Description
|
||||
"""
|
||||
def __init__(self, device, data, servers=None):
|
||||
"""Summary
|
||||
self._device = device
|
||||
self._servers = servers
|
||||
self._loadData(data)
|
||||
|
||||
Args:
|
||||
device (TYPE): Description
|
||||
data (TYPE): Description
|
||||
servers (None, optional): Description
|
||||
"""
|
||||
self.device = device
|
||||
self.servers = servers
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self.id = utils.cast(int, data.attrib.get('id'))
|
||||
self.version = utils.cast(int, data.attrib.get('version'))
|
||||
self.rootTitle = data.attrib.get('rootTitle')
|
||||
|
@ -43,49 +40,21 @@ class SyncItem(object):
|
|||
self.location = data.find('Location').attrib.copy()
|
||||
|
||||
def __repr__(self):
|
||||
"""Summary
|
||||
|
||||
Returns:
|
||||
TYPE: Description
|
||||
"""
|
||||
return '<%s:%s>' % (self.__class__.__name__, self.id)
|
||||
|
||||
def server(self):
|
||||
"""Summary
|
||||
|
||||
Returns:
|
||||
TYPE: Description
|
||||
|
||||
Raises:
|
||||
NotFound: Description
|
||||
"""
|
||||
server = list(filter(lambda x: x.machineIdentifier ==
|
||||
self.machineIdentifier, self.servers))
|
||||
server = list(filter(lambda x: x.machineIdentifier == self.machineIdentifier, self._servers))
|
||||
if 0 == len(server):
|
||||
raise NotFound('Unable to find server with uuid %s' %
|
||||
self.machineIdentifier)
|
||||
raise NotFound('Unable to find server with uuid %s' % self.machineIdentifier)
|
||||
return server[0]
|
||||
|
||||
def getMedia(self):
|
||||
"""Summary
|
||||
|
||||
Returns:
|
||||
TYPE: Description
|
||||
"""
|
||||
server = self.server().connect()
|
||||
items = utils.listItems(server, '/sync/items/%s' % self.id)
|
||||
return items
|
||||
|
||||
def markAsDone(self, sync_id):
|
||||
"""Summary
|
||||
|
||||
Args:
|
||||
sync_id (TYPE): Description
|
||||
|
||||
Returns:
|
||||
TYPE: Description
|
||||
"""
|
||||
server = self.server().connect()
|
||||
url = '/sync/%s/%s/files/%s/downloaded' % (
|
||||
self.device.clientIdentifier, server.machineIdentifier, sync_id)
|
||||
self._device.clientIdentifier, server.machineIdentifier, sync_id)
|
||||
server.query(url, method=requests.put)
|
||||
|
|
|
@ -39,26 +39,26 @@ def register_libtype(cls):
|
|||
return cls
|
||||
|
||||
|
||||
def buildItem(server, elem, initpath, bytag=False):
|
||||
""" Factory function to build the objects used within the PlexAPI.
|
||||
# def buildItem(server, elem, initpath, bytag=False):
|
||||
# """ Factory function to build the objects used within the PlexAPI.
|
||||
|
||||
Parameters:
|
||||
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
|
||||
elem (ElementTree): XML data needed to build the object.
|
||||
initpath (str): Relative path requested when retrieving specified `data` (optional).
|
||||
bytag (bool): Creates the object from the name specified by the tag instead of the
|
||||
default which builds the object specified by the type attribute. <tag type='foo' />
|
||||
# Parameters:
|
||||
# server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
|
||||
# elem (ElementTree): XML data needed to build the object.
|
||||
# initpath (str): Relative path requested when retrieving specified `data` (optional).
|
||||
# bytag (bool): Creates the object from the name specified by the tag instead of the
|
||||
# default which builds the object specified by the type attribute. <tag type='foo' />
|
||||
|
||||
Raises:
|
||||
UnknownType: Unknown library type.
|
||||
"""
|
||||
libtype = elem.tag if bytag else elem.attrib.get('type')
|
||||
if libtype == 'photo' and elem.tag == 'Directory':
|
||||
libtype = 'photoalbum'
|
||||
if libtype in LIBRARY_TYPES:
|
||||
cls = LIBRARY_TYPES[libtype]
|
||||
return cls(server, elem, initpath)
|
||||
raise UnknownType('Unknown library type: %s' % libtype)
|
||||
# Raises:
|
||||
# UnknownType: Unknown library type.
|
||||
# """
|
||||
# libtype = elem.tag if bytag else elem.attrib.get('type')
|
||||
# if libtype == 'photo' and elem.tag == 'Directory':
|
||||
# libtype = 'photoalbum'
|
||||
# if libtype in LIBRARY_TYPES:
|
||||
# cls = LIBRARY_TYPES[libtype]
|
||||
# return cls(server, elem, initpath)
|
||||
# raise UnknownType('Unknown library type: %s' % libtype)
|
||||
|
||||
|
||||
def cast(func, value):
|
||||
|
@ -319,7 +319,6 @@ def threaded(callback, listargs):
|
|||
threads[-1].start()
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
|
||||
return results
|
||||
|
||||
|
||||
|
@ -366,6 +365,7 @@ def download(url, filename=None, savepath=None, session=None, chunksize=4024, mo
|
|||
>>> download(a_episode.getStreamURL(), a_episode.location)
|
||||
/path/to/file
|
||||
"""
|
||||
# TODO: Review this; It should be properly logging and raising exceptions..
|
||||
session = session or requests.Session()
|
||||
print('Mocked download %s' % mocked)
|
||||
if savepath is None:
|
||||
|
|
153
plexapi/video.py
153
plexapi/video.py
|
@ -7,23 +7,9 @@ from plexapi.base import Playable, PlexPartialObject
|
|||
class Video(PlexPartialObject):
|
||||
TYPE = None
|
||||
|
||||
def __init__(self, server, data, initpath):
|
||||
"""Default class for all video types.
|
||||
|
||||
Args:
|
||||
server (Plexserver): The PMS server your connected to
|
||||
data (Element): Element built from server.query
|
||||
initpath (str): Relativ path fx /library/sections/1/all
|
||||
|
||||
"""
|
||||
super(Video, self).__init__(data, initpath, server)
|
||||
|
||||
def _loadData(self, data):
|
||||
"""Used to set the attributes
|
||||
|
||||
Args:
|
||||
data (Element): Usually built from server.query
|
||||
"""
|
||||
""" Used to set the attributes. """
|
||||
self._data = data
|
||||
self.listType = 'video'
|
||||
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
|
||||
self.key = data.attrib.get('key')
|
||||
|
@ -42,7 +28,7 @@ class Video(PlexPartialObject):
|
|||
def thumbUrl(self):
|
||||
"""Return url to thumb image."""
|
||||
if self.thumb:
|
||||
return self.server.url(self.thumb)
|
||||
return self._root.url(self.thumb)
|
||||
|
||||
def analyze(self):
|
||||
"""The primary purpose of media analysis is to gather information about
|
||||
|
@ -50,28 +36,28 @@ class Video(PlexPartialObject):
|
|||
that are useful to know–whether it's a video file,
|
||||
a music track, or one of your photos.
|
||||
"""
|
||||
self.server.query('/%s/analyze' % self.key.lstrip('/'), method=self.server.session.put)
|
||||
self._root.query('/%s/analyze' % self.key.lstrip('/'), method=self._root._session.put)
|
||||
|
||||
def markWatched(self):
|
||||
"""Mark a items as watched."""
|
||||
path = '/:/scrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey
|
||||
self.server.query(path)
|
||||
self._root.query(path)
|
||||
self.reload()
|
||||
|
||||
def markUnwatched(self):
|
||||
"""Mark a item as unwatched."""
|
||||
path = '/:/unscrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey
|
||||
self.server.query(path)
|
||||
self._root.query(path)
|
||||
self.reload()
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh a item."""
|
||||
self.server.query('%s/refresh' %
|
||||
self.key, method=self.server.session.put)
|
||||
self._root.query('%s/refresh' %
|
||||
self.key, method=self._root._session.put)
|
||||
|
||||
def section(self):
|
||||
"""Library section."""
|
||||
return self.server.library.sectionByID(self.librarySectionID)
|
||||
return self._root.library.sectionByID(self.librarySectionID)
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
|
@ -106,20 +92,18 @@ class Movie(Video, Playable):
|
|||
self.userRating = utils.cast(float, data.attrib.get('userRating'))
|
||||
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
||||
self.year = utils.cast(int, data.attrib.get('year'))
|
||||
if self.isFullObject(): # check this
|
||||
self.collections = [media.Collection(self.server, e) for e in data if e.tag == media.Collection.TYPE]
|
||||
self.countries = [media.Country(self.server, e) for e in data if e.tag == media.Country.TYPE]
|
||||
self.directors = [media.Director(self.server, e) for e in data if e.tag == media.Director.TYPE]
|
||||
self.genres = [media.Genre(self.server, e) for e in data if e.tag == media.Genre.TYPE]
|
||||
self.media = [media.Media(self.server, e, self.initpath, self) for e in data if e.tag == media.Media.TYPE]
|
||||
self.producers = [media.Producer(self.server, e) for e in data if e.tag == media.Producer.TYPE]
|
||||
self.roles = [media.Role(self.server, e) for e in data if e.tag == media.Role.TYPE]
|
||||
self.writers = [media.Writer(self.server, e) for e in data if e.tag == media.Writer.TYPE]
|
||||
self.fields = [media.Field(e) for e in data if e.tag == media.Field.TYPE]
|
||||
self.videoStreams = utils.findStreams(self.media, 'videostream')
|
||||
self.audioStreams = utils.findStreams(self.media, 'audiostream')
|
||||
self.subtitleStreams = utils.findStreams(
|
||||
self.media, 'subtitlestream')
|
||||
self.collections = self._buildSubitems(data, media.Collection)
|
||||
self.countries = self._buildSubitems(data, media.Country)
|
||||
self.directors = self._buildSubitems(data, media.Director)
|
||||
self.fields = self._buildSubitems(data, media.Field)
|
||||
self.genres = self._buildSubitems(data, media.Genre)
|
||||
self.media = self._buildSubitems(data, media.Media)
|
||||
self.producers = self._buildSubitems(data, media.Producer)
|
||||
self.roles = self._buildSubitems(data, media.Role)
|
||||
self.writers = self._buildSubitems(data, media.Writer)
|
||||
# self.videoStreams = utils.findStreams(self.media, 'videostream') # these dont go here
|
||||
# self.audioStreams = utils.findStreams(self.media, 'audiostream') # these dont go here
|
||||
# self.subtitleStreams = utils.findStreams(self.media, 'subtitlestream') # these dont go here
|
||||
|
||||
@property
|
||||
def actors(self):
|
||||
|
@ -152,8 +136,8 @@ class Movie(Video, Playable):
|
|||
if kwargs:
|
||||
download_url = self.getStreamURL(**kwargs)
|
||||
else:
|
||||
download_url = self.server.url('%s?download=1' % loc.key)
|
||||
dl = utils.download(download_url, filename=name, savepath=savepath, session=self.server.session)
|
||||
download_url = self._root.url('%s?download=1' % loc.key)
|
||||
dl = utils.download(download_url, filename=name, savepath=savepath, session=self._root._session)
|
||||
if dl:
|
||||
downloaded.append(dl)
|
||||
return downloaded
|
||||
|
@ -164,13 +148,8 @@ class Show(Video):
|
|||
TYPE = 'show'
|
||||
|
||||
def _loadData(self, data):
|
||||
"""Used to set the attributes
|
||||
|
||||
Args:
|
||||
data (Element): Usually built from server.query
|
||||
"""
|
||||
Video._loadData(self, data)
|
||||
# Incase this was loaded from search etc
|
||||
# fix the key if this was loaded from search..
|
||||
self.key = self.key.replace('/children', '')
|
||||
self.art = data.attrib.get('art')
|
||||
self.banner = data.attrib.get('banner')
|
||||
|
@ -186,12 +165,10 @@ class Show(Video):
|
|||
self.rating = utils.cast(float, data.attrib.get('rating'))
|
||||
self.studio = data.attrib.get('studio')
|
||||
self.theme = data.attrib.get('theme')
|
||||
self.viewedLeafCount = utils.cast(
|
||||
int, data.attrib.get('viewedLeafCount'))
|
||||
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount'))
|
||||
self.year = utils.cast(int, data.attrib.get('year'))
|
||||
if self.isFullObject(): # will be fixed with docs.
|
||||
self.genres = [media.Genre(self.server, e) for e in data if e.tag == media.Genre.TYPE]
|
||||
self.roles = [media.Role(self.server, e) for e in data if e.tag == media.Role.TYPE]
|
||||
self.genres = self._buildSubitems(data, media.Genre)
|
||||
self.roles = self._buildSubitems(data, media.Role)
|
||||
|
||||
@property
|
||||
def actors(self):
|
||||
|
@ -203,20 +180,19 @@ class Show(Video):
|
|||
|
||||
def seasons(self):
|
||||
"""Returns a list of Season."""
|
||||
path = '/library/metadata/%s/children' % self.ratingKey
|
||||
return utils.listItems(self.server, path, Season.TYPE)
|
||||
key = '/library/metadata/%s/children' % self.ratingKey
|
||||
return self._fetchItems(key, Season.TYPE)
|
||||
|
||||
def season(self, title=None):
|
||||
"""Returns a Season
|
||||
""" Returns the season with the specified title or number.
|
||||
|
||||
Args:
|
||||
title (str, int): fx Season 1
|
||||
Parameters:
|
||||
title (str or int): Title or Number of the season to return.
|
||||
"""
|
||||
if isinstance(title, int):
|
||||
title = 'Season %s' % title
|
||||
|
||||
path = '/library/metadata/%s/children' % self.ratingKey
|
||||
return utils.findItem(self.server, path, title)
|
||||
key = '/library/metadata/%s/children' % self.ratingKey
|
||||
return self._fetchItem(key, title)
|
||||
|
||||
def episodes(self, watched=None):
|
||||
"""Returs a list of Episode
|
||||
|
@ -225,7 +201,7 @@ class Show(Video):
|
|||
watched (bool): Defaults to None. Exclude watched episodes
|
||||
"""
|
||||
leavesKey = '/library/metadata/%s/allLeaves' % self.ratingKey
|
||||
return utils.listItems(self.server, leavesKey, watched=watched)
|
||||
return utils.listItems(self._root, leavesKey, watched=watched)
|
||||
|
||||
def episode(self, title=None, season=None, episode=None):
|
||||
"""Find a episode using a title or season and episode.
|
||||
|
@ -254,18 +230,14 @@ class Show(Video):
|
|||
"""
|
||||
if not title and (not season or not episode):
|
||||
raise TypeError('Missing argument: title or season and episode are required')
|
||||
|
||||
if title:
|
||||
path = '/library/metadata/%s/allLeaves' % self.ratingKey
|
||||
return utils.findItem(self.server, path, title)
|
||||
|
||||
return utils.findItem(self._root, path, title)
|
||||
elif season and episode:
|
||||
results = [i for i in self.episodes()
|
||||
if i.seasonNumber == season and i.index == episode]
|
||||
results = [i for i in self.episodes() if i.seasonNumber == season and i.index == episode]
|
||||
if results:
|
||||
return results[0]
|
||||
else:
|
||||
raise NotFound('Couldnt find %s S%s E%s' % (self.title, season, episode))
|
||||
raise NotFound('Couldnt find %s S%s E%s' % (self.title, season, episode))
|
||||
|
||||
def watched(self):
|
||||
"""Return a list of watched episodes"""
|
||||
|
@ -289,7 +261,7 @@ class Show(Video):
|
|||
|
||||
def refresh(self):
|
||||
"""Refresh the metadata."""
|
||||
self.server.query('/library/metadata/%s/refresh' % self.ratingKey, method=self.server.session.put)
|
||||
self._root.query('/library/metadata/%s/refresh' % self.ratingKey, method=self._root._session.put)
|
||||
|
||||
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
|
||||
downloaded = []
|
||||
|
@ -317,8 +289,7 @@ class Season(Video):
|
|||
self.parentKey = data.attrib.get('parentKey')
|
||||
self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey'))
|
||||
self.parentTitle = data.attrib.get('parentTitle')
|
||||
self.viewedLeafCount = utils.cast(
|
||||
int, data.attrib.get('viewedLeafCount'))
|
||||
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount'))
|
||||
|
||||
@property
|
||||
def isWatched(self):
|
||||
|
@ -330,18 +301,15 @@ class Season(Video):
|
|||
return self.index
|
||||
|
||||
def episodes(self, watched=None):
|
||||
"""Returs a list of Episode
|
||||
""" Returs a list of Episode
|
||||
|
||||
Args:
|
||||
Parameters:
|
||||
watched (bool): Defaults to None. Exclude watched episodes
|
||||
|
||||
Returns:
|
||||
list: of Episode
|
||||
|
||||
|
||||
"""
|
||||
childrenKey = '/library/metadata/%s/children' % self.ratingKey
|
||||
return utils.listItems(self.server, childrenKey, watched=watched)
|
||||
key = '/library/metadata/%s/children' % self.ratingKey
|
||||
return self._fetchItems(key, Episode.TYPE)
|
||||
# childrenKey = '/library/metadata/%s/children' % self.ratingKey
|
||||
# return utils.listItems(self._root, childrenKey, watched=watched)
|
||||
|
||||
def episode(self, title=None, episode=None):
|
||||
"""Find a episode using a title or season and episode.
|
||||
|
@ -371,7 +339,7 @@ class Season(Video):
|
|||
raise TypeError('Missing argument, you need to use title or episode.')
|
||||
if title:
|
||||
path = '/library/metadata/%s/children' % self.ratingKey
|
||||
return utils.findItem(self.server, path, title)
|
||||
return utils.findItem(self._root, path, title)
|
||||
elif episode:
|
||||
results = [i for i in self.episodes() if i.seasonNumber == self.index and i.index == episode]
|
||||
if results:
|
||||
|
@ -391,7 +359,7 @@ class Season(Video):
|
|||
|
||||
def show(self):
|
||||
"""Return this seasons show."""
|
||||
return utils.listItems(self.server, self.parentKey)[0]
|
||||
return utils.listItems(self._root, self.parentKey)[0]
|
||||
|
||||
def watched(self):
|
||||
"""Returns a list of watched Episode"""
|
||||
|
@ -428,6 +396,7 @@ class Episode(Video, Playable):
|
|||
"""
|
||||
Video._loadData(self, data)
|
||||
Playable._loadData(self, data)
|
||||
self._seasonNumber = None # cached season number
|
||||
self.art = data.attrib.get('art')
|
||||
self.chapterSource = data.attrib.get('chapterSource')
|
||||
self.contentRating = data.attrib.get('contentRating')
|
||||
|
@ -448,19 +417,17 @@ class Episode(Video, Playable):
|
|||
self.rating = utils.cast(float, data.attrib.get('rating'))
|
||||
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
||||
self.year = utils.cast(int, data.attrib.get('year'))
|
||||
self.directors = [media.Director(self.server, e) for e in data if e.tag == media.Director.TYPE]
|
||||
self.media = [media.Media(self.server, e, self.initpath, self) for e in data if e.tag == media.Media.TYPE]
|
||||
self.writers = [media.Writer(self.server, e) for e in data if e.tag == media.Writer.TYPE]
|
||||
self.videoStreams = utils.findStreams(self.media, 'videostream')
|
||||
self.audioStreams = utils.findStreams(self.media, 'audiostream')
|
||||
self.subtitleStreams = utils.findStreams(self.media, 'subtitlestream')
|
||||
self.directors = self._buildSubitems(data, media.Director)
|
||||
self.media = self._buildSubitems(data, media.Media)
|
||||
self.writers = self._buildSubitems(data, media.Writer)
|
||||
# self.videoStreams = utils.findStreams(self.media, 'videostream')
|
||||
# self.audioStreams = utils.findStreams(self.media, 'audiostream')
|
||||
# self.subtitleStreams = utils.findStreams(self.media, 'subtitlestream')
|
||||
# data for active sessions and history
|
||||
self.sessionKey = utils.cast(int, data.attrib.get('sessionKey'))
|
||||
self.username = utils.findUsername(data)
|
||||
self.player = utils.findPlayer(self.server, data)
|
||||
self.transcodeSession = utils.findTranscodeSession(self.server, data)
|
||||
# Cached season number
|
||||
self._seasonNumber = None
|
||||
self.player = utils.findPlayer(self._root, data)
|
||||
self.transcodeSession = utils.findTranscodeSession(self._root, data)
|
||||
|
||||
def __repr__(self):
|
||||
clsname = self.__class__.__name__
|
||||
|
@ -484,15 +451,15 @@ class Episode(Video, Playable):
|
|||
def thumbUrl(self):
|
||||
"""Return url to thumb image."""
|
||||
if self.grandparentThumb:
|
||||
return self.server.url(self.grandparentThumb)
|
||||
return self._root.url(self.grandparentThumb)
|
||||
|
||||
def season(self):
|
||||
"""Return this episode Season"""
|
||||
return utils.listItems(self.server, self.parentKey)[0]
|
||||
return utils.listItems(self._root, self.parentKey)[0]
|
||||
|
||||
def show(self):
|
||||
"""Return this episodes Show"""
|
||||
return utils.listItems(self.server, self.grandparentKey)[0]
|
||||
return utils.listItems(self._root, self.grandparentKey)[0]
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
|
|
|
@ -15,9 +15,9 @@ from utils import log, itertests
|
|||
|
||||
def runtests(args):
|
||||
# Get username and password from environment
|
||||
username = args.username or CONFIG.get('authentication.username')
|
||||
password = args.password or CONFIG.get('authentication.password')
|
||||
resource = args.resource or CONFIG.get('authentication.resource')
|
||||
username = args.username or CONFIG.get('authentication.myplex_username')
|
||||
password = args.password or CONFIG.get('authentication.myplex_password')
|
||||
resource = args.resource or CONFIG.get('authentication.server_resource')
|
||||
# Register known tests
|
||||
for loader, name, ispkg in pkgutil.iter_modules([dirname(abspath(__file__))]):
|
||||
if name.startswith('test_'):
|
||||
|
|
|
@ -58,7 +58,7 @@ def plex_account():
|
|||
username = test_username
|
||||
password = test_password
|
||||
assert username and password
|
||||
account = MyPlexAccount.signin(username, password)
|
||||
account = MyPlexAccount(username, password)
|
||||
assert account
|
||||
return account
|
||||
|
||||
|
|
|
@ -5,120 +5,250 @@ This script loops through all media items to build a collection of attributes on
|
|||
each media type. The resulting list can be compared with the current object
|
||||
implementation in python-plex api to track new attributes and depricate old ones.
|
||||
"""
|
||||
import argparse, copy, pickle, plexapi, os, sys
|
||||
import argparse, copy, pickle, plexapi, os, sys, time
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from plexapi import library
|
||||
from plexapi.base import PlexObject
|
||||
from plexapi.myplex import MyPlexAccount
|
||||
from plexapi.server import PlexServer
|
||||
from plexapi.utils import NA
|
||||
from plexapi.playqueue import PlayQueue
|
||||
|
||||
CACHEPATH = '/tmp/findattrs.pickle'
|
||||
NAMESPACE = {
|
||||
'xml': defaultdict(int),
|
||||
'obj': defaultdict(int),
|
||||
'examples': defaultdict(set),
|
||||
'total': 0
|
||||
'categories': defaultdict(set),
|
||||
'total': 0,
|
||||
'old': 0,
|
||||
'new': 0,
|
||||
}
|
||||
IGNORES = {
|
||||
'server.PlexServer': ['baseurl', 'token', 'session'],
|
||||
'myplex.ResourceConnection': ['httpuri'],
|
||||
'client.PlexClient': ['baseurl'],
|
||||
}
|
||||
DONT_RELOAD = (
|
||||
'library.MovieSection',
|
||||
'library.MusicSection',
|
||||
'library.PhotoSection',
|
||||
'library.ShowSection',
|
||||
'media.MediaPart', # tries to download the file
|
||||
'media.VideoStream',
|
||||
'media.AudioStream',
|
||||
'media.SubtitleStream',
|
||||
'myplex.MyPlexAccount',
|
||||
'myplex.MyPlexUser',
|
||||
'myplex.MyPlexResource',
|
||||
'myplex.ResourceConnection',
|
||||
'myplex.MyPlexDevice',
|
||||
'photo.Photoalbum',
|
||||
'server.Account',
|
||||
'client.PlexClient', # we dont have the token to reload.
|
||||
#'server.PlexServer', # setting version to None? :(
|
||||
)
|
||||
STOP_RECURSING_AT = (
|
||||
#'media.MediaPart',
|
||||
)
|
||||
|
||||
class PlexAttributes():
|
||||
|
||||
def __init__(self, opts):
|
||||
self.opts = opts # command line options
|
||||
self.clsnames = [c for c in opts.clsnames.split(',') if c] # list of clsnames to report (blank=all)
|
||||
self.account = MyPlexAccount.signin() # MyPlexAccount instance
|
||||
self.account = MyPlexAccount() # MyPlexAccount instance
|
||||
self.plex = PlexServer() # PlexServer instance
|
||||
self.total = 0 # Total objects parsed
|
||||
self.attrs = defaultdict(dict) # Attrs result set
|
||||
|
||||
def run(self):
|
||||
# MyPlex
|
||||
self._load_attrs(self.account)
|
||||
self._load_attrs(self.account.devices())
|
||||
for resource in self.account.resources():
|
||||
self._load_attrs(resource)
|
||||
self._load_attrs(resource.connections)
|
||||
self._load_attrs(self.account.users())
|
||||
# Server
|
||||
self._load_attrs(self.plex)
|
||||
self._load_attrs(self.plex.account())
|
||||
self._load_attrs(self.plex.history()[:20])
|
||||
self._load_attrs(self.plex.playlists())
|
||||
for search in ('cre', 'ani', 'mik', 'she'):
|
||||
self._load_attrs(self.plex.search('cre'))
|
||||
self._load_attrs(self.plex.sessions())
|
||||
# Library
|
||||
self._load_attrs(self.plex.library)
|
||||
self._load_attrs(self.plex.library.sections())
|
||||
self._load_attrs(self.plex.library.all()[:20])
|
||||
self._load_attrs(self.plex.library.onDeck()[:20])
|
||||
self._load_attrs(self.plex.library.recentlyAdded()[:20])
|
||||
for search in ('cat', 'dog', 'rat'):
|
||||
self._load_attrs(self.plex.library.search(search)[:20])
|
||||
# Client
|
||||
self._load_attrs(self.plex.clients())
|
||||
starttime = time.time()
|
||||
# self._parse_myplex()
|
||||
# self._parse_server()
|
||||
# self._parse_library()
|
||||
# self._parse_audio()
|
||||
# self._parse_photo()
|
||||
# self._parse_movie()
|
||||
# self._parse_show()
|
||||
# self._parse_client()
|
||||
# self._parse_playlist()
|
||||
# self._parse_sync()
|
||||
self.runtime = round((time.time() - starttime) / 60.0, 1)
|
||||
return self
|
||||
|
||||
def _load_attrs(self, obj):
|
||||
if isinstance(obj, (list, tuple)):
|
||||
return [self._parse_objects(x) for x in obj]
|
||||
return self._parse_objects(obj)
|
||||
def _parse_myplex(self):
|
||||
self._load_attrs(self.account, 'myplex')
|
||||
self._load_attrs(self.account.devices(), 'myplex')
|
||||
for resource in self.account.resources():
|
||||
self._load_attrs(resource, 'myplex')
|
||||
self._load_attrs(resource.connections, 'myplex')
|
||||
self._load_attrs(self.account.users(), 'myplex')
|
||||
|
||||
def _parse_objects(self, obj):
|
||||
def _parse_server(self):
|
||||
self._load_attrs(self.plex, 'serv')
|
||||
self._load_attrs(self.plex.account(), 'serv')
|
||||
self._load_attrs(self.plex.history()[:20], 'hist')
|
||||
# self._load_attrs(self.plex.playlists())
|
||||
# for search in ('cre', 'ani', 'mik', 'she'):
|
||||
# self._load_attrs(self.plex.search('cre'))
|
||||
# self._load_attrs(self.plex.sessions(), 'sess')
|
||||
|
||||
def _parse_library(self):
|
||||
cat = 'lib'
|
||||
self._load_attrs(self.plex.library, cat)
|
||||
# self._load_attrs(self.plex.library.sections())
|
||||
# self._load_attrs(self.plex.library.all()[:20])
|
||||
# self._load_attrs(self.plex.library.onDeck()[:20])
|
||||
# self._load_attrs(self.plex.library.recentlyAdded()[:20])
|
||||
# for search in ('cat', 'dog', 'rat'):
|
||||
# self._load_attrs(self.plex.library.search(search)[:20])
|
||||
|
||||
def _parse_audio(self):
|
||||
cat = 'lib'
|
||||
for musicsection in self.plex.library.sections():
|
||||
if musicsection.TYPE == library.MusicSection.TYPE:
|
||||
self._load_attrs(musicsection, cat)
|
||||
for artist in musicsection.all():
|
||||
self._load_attrs(artist, cat)
|
||||
for album in artist.albums():
|
||||
self._load_attrs(album, cat)
|
||||
for track in album.tracks():
|
||||
self._load_attrs(track, cat)
|
||||
|
||||
def _parse_photo(self):
|
||||
cat = 'lib'
|
||||
for photosection in self.plex.library.sections():
|
||||
if photosection.TYPE == library.PhotoSection.TYPE:
|
||||
self._load_attrs(photosection, cat)
|
||||
for photoalbum in photosection.all():
|
||||
self._load_attrs(photoalbum, cat)
|
||||
for photo in photoalbum.photos():
|
||||
self._load_attrs(photo, cat)
|
||||
|
||||
def _parse_movie(self):
|
||||
cat = 'lib'
|
||||
for moviesection in self.plex.library.sections():
|
||||
if moviesection.TYPE == library.MovieSection.TYPE:
|
||||
self._load_attrs(moviesection, cat)
|
||||
for movie in moviesection.all():
|
||||
self._load_attrs(movie, cat)
|
||||
|
||||
def _parse_show(self):
|
||||
cat = 'lib'
|
||||
for showsection in self.plex.library.sections():
|
||||
if showsection.TYPE == library.ShowSection.TYPE:
|
||||
self._load_attrs(showsection, cat)
|
||||
for show in showsection.all():
|
||||
self._load_attrs(show, cat)
|
||||
for season in show.seasons():
|
||||
self._load_attrs(show, cat)
|
||||
for episode in season.episodes():
|
||||
self._load_attrs(episode, cat)
|
||||
|
||||
def _parse_client(self):
|
||||
for device in self.account.devices():
|
||||
client = device.connect(safe=True)
|
||||
if client is not None:
|
||||
self._load_attrs(client, 'myplex')
|
||||
for client in self.plex.clients():
|
||||
client.connect(safe=True)
|
||||
self._load_attrs(client, 'client')
|
||||
|
||||
def _parse_playlist(self):
|
||||
for playlist in self.plex.playlists():
|
||||
self._load_attrs(playlist, 'pl')
|
||||
for item in playlist.items():
|
||||
self._load_attrs(item, 'pl')
|
||||
playqueue = PlayQueue.create(self.plex, playlist)
|
||||
self._load_attrs(playqueue, 'pq')
|
||||
|
||||
def _parse_sync(self):
|
||||
# TODO: Get this working..
|
||||
pass
|
||||
|
||||
def _load_attrs(self, obj, cat=None):
|
||||
if isinstance(obj, (list, tuple)):
|
||||
return [self._parse_objects(item, cat) for item in obj]
|
||||
self._parse_objects(obj, cat)
|
||||
|
||||
def _parse_objects(self, obj, cat=None):
|
||||
clsname = '%s.%s' % (obj.__module__, obj.__class__.__name__)
|
||||
clsname = clsname.replace('plexapi.', '')
|
||||
if self.clsnames and clsname not in self.clsnames:
|
||||
return None
|
||||
sys.stdout.write('.'); sys.stdout.flush()
|
||||
self._print_the_little_dot()
|
||||
if clsname not in self.attrs:
|
||||
self.attrs[clsname] = copy.deepcopy(NAMESPACE)
|
||||
self.attrs[clsname]['total'] += 1
|
||||
self._load_xml_attrs(clsname, obj._data, self.attrs[clsname]['xml'], self.attrs[clsname]['examples'])
|
||||
self._load_xml_attrs(clsname, obj._data, self.attrs[clsname]['xml'],
|
||||
self.attrs[clsname]['examples'], self.attrs[clsname]['categories'], cat)
|
||||
self._load_obj_attrs(clsname, obj, self.attrs[clsname]['obj'])
|
||||
|
||||
def _load_xml_attrs(self, clsname, elem, attrs, examples):
|
||||
if elem in (None, NA): return None
|
||||
def _print_the_little_dot(self):
|
||||
self.total += 1
|
||||
if not self.total % 100:
|
||||
sys.stdout.write('.')
|
||||
if not self.total % 8000:
|
||||
sys.stdout.write('\n')
|
||||
sys.stdout.flush()
|
||||
|
||||
def _load_xml_attrs(self, clsname, elem, attrs, examples, categories, cat):
|
||||
if elem is None: return None
|
||||
for attr in sorted(elem.attrib.keys()):
|
||||
attrs[attr] += 1
|
||||
if cat: categories[attr].add(cat)
|
||||
if elem.attrib[attr] and len(examples[attr]) <= self.opts.examples:
|
||||
examples[attr].add(elem.attrib[attr])
|
||||
|
||||
def _load_obj_attrs(self, clsname, obj, attrs):
|
||||
if clsname in STOP_RECURSING_AT: return None
|
||||
if isinstance(obj, PlexObject) and clsname not in DONT_RELOAD: obj.reload(safe=True)
|
||||
for attr, value in obj.__dict__.items():
|
||||
if value in (None, NA) or isinstance(value, (str, bool, float, int, datetime)):
|
||||
if value is None or isinstance(value, (str, bool, float, int, datetime)):
|
||||
if not attr.startswith('_') and attr not in IGNORES.get(clsname, []):
|
||||
attrs[attr] += 1
|
||||
if isinstance(value, list):
|
||||
if not attr.startswith('_') and attr not in IGNORES.get(clsname, []):
|
||||
if value and isinstance(value[0], PlexObject):
|
||||
attrs['%s[]' % attr] += 1
|
||||
[self._parse_objects(obj) for obj in value]
|
||||
|
||||
def print_report(self):
|
||||
total_attrs = 0
|
||||
for clsname in sorted(self.attrs.keys()):
|
||||
meta = self.attrs[clsname]
|
||||
count = meta['total']
|
||||
print(_('\n%s (%s)\n%s' % (clsname, count, '-'*(len(clsname)+8)), 'yellow'))
|
||||
print(_('\n%s (%s)\n%s' % (clsname, count, '-'*30), 'yellow'))
|
||||
attrs = sorted(set(list(meta['xml'].keys()) + list(meta['obj'].keys())))
|
||||
for attr in attrs:
|
||||
state = self._attr_state(attr, meta)
|
||||
state = self._attr_state(clsname, attr, meta)
|
||||
count = meta['xml'].get(attr, 0)
|
||||
example = list(meta['examples'].get(attr, ['--']))[0][:80]
|
||||
print('%-4s %4s %-30s %s' % (state, count, attr, example))
|
||||
categories = ','.join(meta['categories'].get(attr, ['--']))
|
||||
examples = '; '.join(list(meta['examples'].get(attr, ['--']))[:3])[:80]
|
||||
print('%7s %3s %-30s %-20s %s' % (count, state, attr, categories, examples))
|
||||
total_attrs += count
|
||||
print(_('\nSUMMARY\n------------', 'yellow'))
|
||||
print('Plex Version %s' % self.plex.version)
|
||||
print('PlexAPI Version %s' % plexapi.VERSION)
|
||||
print('Total Objects %s\n' % sum([x['total'] for x in self.attrs.values()]))
|
||||
print(_('\nSUMMARY\n%s' % ('-'*30), 'yellow'))
|
||||
print('%7s %3s %3s %-20s %s' % ('total', 'new', 'old', 'categories', 'clsname'))
|
||||
for clsname in sorted(self.attrs.keys()):
|
||||
print('%-34s %s' % (clsname, self.attrs[clsname]['total']))
|
||||
print()
|
||||
print('%7s %12s %12s %s' % (self.attrs[clsname]['total'],
|
||||
_(self.attrs[clsname]['new'] or '', 'cyan'),
|
||||
_(self.attrs[clsname]['old'] or '', 'red'),
|
||||
clsname))
|
||||
print('\nPlex Version %s' % self.plex.version)
|
||||
print('PlexAPI Version %s' % plexapi.VERSION)
|
||||
print('Total Objects %s' % sum([x['total'] for x in self.attrs.values()]))
|
||||
print('Runtime %s min\n' % self.runtime)
|
||||
|
||||
def _attr_state(self, attr, meta):
|
||||
if attr in meta['xml'].keys() and attr not in meta['obj'].keys(): return _('new', 'blue')
|
||||
if attr not in meta['xml'].keys() and attr in meta['obj'].keys(): return _('old', 'red')
|
||||
def _attr_state(self, clsname, attr, meta):
|
||||
if attr in meta['xml'].keys() and attr not in meta['obj'].keys():
|
||||
self.attrs[clsname]['new'] += 1
|
||||
return _('new', 'blue')
|
||||
if attr not in meta['xml'].keys() and attr in meta['obj'].keys():
|
||||
self.attrs[clsname]['old'] += 1
|
||||
return _('old', 'red')
|
||||
return _(' ', 'green')
|
||||
|
||||
|
||||
|
||||
def _(text, color):
|
||||
FMTSTR = '\033[%dm%s\033[0m'
|
||||
|
@ -144,102 +274,3 @@ if __name__ == '__main__':
|
|||
pickle.dump(plexattrs, handle)
|
||||
# Print Report
|
||||
plexattrs.print_report()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# MAX_SEEN = 9999
|
||||
# MAX_EXAMPLES = 10
|
||||
# PICKLE_PATH = '/tmp/findattrs.pickle'
|
||||
# ETYPES = ['artist', 'album', 'track', 'movie', 'show', 'season', 'episode']
|
||||
# COLORS = {'blue':34, 'cyan':36, 'green':32, 'grey':30, 'magenta':35, 'red':31, 'white':37, 'yellow':33}
|
||||
# RESET = '\033[0m'
|
||||
|
||||
# def color(text, color=None):
|
||||
# """ Colorize text {red, green, yellow, blue, magenta, cyan, white}. """
|
||||
# fmt_str = '\033[%dm%s'
|
||||
# if color is not None:
|
||||
# text = fmt_str % (COLORS[color], text)
|
||||
# text += RESET
|
||||
# return text
|
||||
|
||||
# def find_attrs(plex, key, result, examples, seen, indent=0):
|
||||
# for elem in plex.query(key):
|
||||
# attrs = sorted(elem.attrib.keys())
|
||||
# etype = elem.attrib.get('type')
|
||||
# if not etype or seen[etype] >= MAX_SEEN:
|
||||
# continue
|
||||
# seen[etype] += 1
|
||||
# sys.stdout.write('.'); sys.stdout.flush()
|
||||
# # find all attriutes in this element
|
||||
# for attr in attrs:
|
||||
# result[attr].add(etype)
|
||||
# if elem.attrib[attr] and len(examples[attr]) <= MAX_EXAMPLES:
|
||||
# examples[attr].add(elem.attrib[attr])
|
||||
# # iterate all subelements
|
||||
# for subelem in elem:
|
||||
# if subelem.tag not in ETYPES:
|
||||
# subattr = subelem.tag + '[]'
|
||||
# result[subattr].add(etype)
|
||||
# if len(examples[subattr]) <= MAX_EXAMPLES:
|
||||
# xmlstr = ElementTree.tostring(subelem, encoding='utf8').split('\n')[1]
|
||||
# examples[subattr].add(xmlstr)
|
||||
# # recurse into the main element (loading its key)
|
||||
# if etype in ETYPES:
|
||||
# subkey = elem.attrib['key']
|
||||
# if key != subkey and seen[etype] < MAX_SEEN:
|
||||
# find_attrs(plex, subkey, result, examples, seen, indent+2)
|
||||
# return result
|
||||
|
||||
# def find_all_attrs():
|
||||
# try:
|
||||
# result = defaultdict(set)
|
||||
# examples = defaultdict(set)
|
||||
# seen = defaultdict(int)
|
||||
# plex = PlexServer()
|
||||
# find_attrs(plex, '/status/sessions', result, examples, seen)
|
||||
# for section in plex.library.sections():
|
||||
# for elem in section.all():
|
||||
# # check weve seen too many of this type of elem
|
||||
# if seen[elem.type] >= MAX_SEEN:
|
||||
# continue
|
||||
# seen[elem.type] += 1
|
||||
# # fetch the xml for this elem
|
||||
# key = elem.key.replace('/children', '')
|
||||
# find_attrs(plex, key, result, examples, seen)
|
||||
# except KeyboardInterrupt:
|
||||
# pass
|
||||
# return result, examples, seen
|
||||
|
||||
# def print_results(result, examples, seen):
|
||||
# print_general_summary(result, examples)
|
||||
# print_summary_by_etype(result, examples)
|
||||
# print_seen_etypes(seen)
|
||||
|
||||
# def print_general_summary(result, examples):
|
||||
# print(color('\n--- general summary ---', 'cyan'))
|
||||
# for attr, etypes in sorted(result.items(), key=lambda x: x[0].lower()):
|
||||
# args = [attr]
|
||||
# args += [etype if etype in etypes else '--' for etype in ETYPES]
|
||||
# args.append(list(examples[attr])[0][:30] if examples[attr] else '_NA_')
|
||||
# print('%-23s %-8s %-8s %-8s %-8s %-8s %-8s %-8s %s' % tuple(args))
|
||||
|
||||
# def print_summary_by_etype(result, examples):
|
||||
# # find super attributes (in all etypes)
|
||||
# result['super'] = set(['librarySectionID', 'index', 'titleSort'])
|
||||
# for attr, etypes in result.items():
|
||||
# if len(etypes) == 7:
|
||||
# result['super'].add(attr)
|
||||
# # print the summary
|
||||
# for etype in ['super'] + ETYPES:
|
||||
# print(color('\n--- %s ---' % etype, 'cyan'))
|
||||
# for attr in sorted(result.keys(), key=lambda x:x[0].lower()):
|
||||
# if (etype in result[attr] and attr not in result['super']) or (etype == 'super' and attr in result['super']):
|
||||
# example = list(examples[attr])[0][:80] if examples[attr] else '_NA_'
|
||||
# print('%-23s %s' % (attr, example))
|
||||
|
||||
# def print_seen_etypes(seen):
|
||||
# print('\n')
|
||||
# for etype, count in seen.items():
|
||||
# print('%-8s %s' % (etype, count))
|
||||
|
|
Loading…
Reference in a new issue