mirror of
https://github.com/pkkid/python-plexapi
synced 2024-11-10 06:04:15 +00:00
Create registry of library items we may see in xml; Cleanup how we handle build_item and list_items
This commit is contained in:
parent
cc58b87c9b
commit
276ba26b77
9 changed files with 160 additions and 221 deletions
|
@ -7,14 +7,14 @@ from platform import platform, uname
|
|||
from plexapi.config import PlexConfig, reset_base_headers
|
||||
from uuid import getnode
|
||||
|
||||
PROJECT = 'PlexAPI'
|
||||
VERSION = '1.1.0'
|
||||
|
||||
# Load User Defined Config
|
||||
CONFIG_PATH = os.path.expanduser('~/.config/plexapi/config.ini')
|
||||
CONFIG = PlexConfig(CONFIG_PATH)
|
||||
|
||||
# Core Settings
|
||||
PROJECT = 'PlexAPI'
|
||||
VERSION = '1.1.0'
|
||||
TIMEOUT = CONFIG.get('plexapi.timeout', 5, int)
|
||||
|
||||
# Plex Header Configuation
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
"""
|
||||
PlexAudio
|
||||
"""
|
||||
from plexapi import utils
|
||||
from plexapi.media import Media, Genre, Producer
|
||||
from plexapi.exceptions import NotFound, UnknownType, Unsupported
|
||||
from plexapi.exceptions import Unsupported
|
||||
from plexapi.utils import NA
|
||||
from plexapi.utils import cast, toDatetime
|
||||
from plexapi.utils import cast, toDatetime, register_libtype
|
||||
from plexapi.video import Video # TODO: remove this when Audio class can stand on its own legs
|
||||
|
||||
try:
|
||||
from urllib import urlencode # Python2
|
||||
except ImportError:
|
||||
|
@ -67,6 +67,7 @@ class Audio(Video): # TODO: inherit from PlexPartialObject, like the Video clas
|
|||
self._loadData(data[0])
|
||||
|
||||
|
||||
@register_libtype
|
||||
class Artist(Audio):
|
||||
TYPE = 'artist'
|
||||
|
||||
|
@ -88,19 +89,19 @@ class Artist(Audio):
|
|||
|
||||
def albums(self):
|
||||
path = '/library/metadata/%s/children' % self.ratingKey
|
||||
return list_items(self.server, path, Album.TYPE)
|
||||
return utils.list_items(self.server, path, Album.TYPE)
|
||||
|
||||
def album(self, title):
|
||||
path = '/library/metadata/%s/children' % self.ratingKey
|
||||
return find_item(self.server, path, title)
|
||||
return utils.find_item(self.server, path, title)
|
||||
|
||||
def tracks(self, watched=None):
|
||||
leavesKey = '/library/metadata/%s/allLeaves' % self.ratingKey
|
||||
return list_items(self.server, leavesKey, watched=watched)
|
||||
return utils.list_items(self.server, leavesKey, watched=watched)
|
||||
|
||||
def track(self, title):
|
||||
path = '/library/metadata/%s/allLeaves' % self.ratingKey
|
||||
return find_item(self.server, path, title)
|
||||
return utils.find_item(self.server, path, title)
|
||||
|
||||
def watched(self):
|
||||
return self.episodes(watched=True)
|
||||
|
@ -115,6 +116,7 @@ class Artist(Audio):
|
|||
self.server.query('/library/metadata/%s/refresh' % self.ratingKey)
|
||||
|
||||
|
||||
@register_libtype
|
||||
class Album(Audio):
|
||||
TYPE = 'album'
|
||||
|
||||
|
@ -137,17 +139,17 @@ class Album(Audio):
|
|||
|
||||
def tracks(self, watched=None):
|
||||
childrenKey = '/library/metadata/%s/children' % self.ratingKey
|
||||
return list_items(self.server, childrenKey, watched=watched)
|
||||
return utils.list_items(self.server, childrenKey, watched=watched)
|
||||
|
||||
def track(self, title):
|
||||
path = '/library/metadata/%s/children' % self.ratingKey
|
||||
return find_item(self.server, path, title)
|
||||
return utils.find_item(self.server, path, title)
|
||||
|
||||
def get(self, title):
|
||||
return self.track(title)
|
||||
|
||||
def artist(self):
|
||||
return list_items(self.server, self.parentKey)[0]
|
||||
return utils.list_items(self.server, self.parentKey)[0]
|
||||
|
||||
def watched(self):
|
||||
return self.tracks(watched=True)
|
||||
|
@ -156,6 +158,7 @@ class Album(Audio):
|
|||
return self.tracks(watched=False)
|
||||
|
||||
|
||||
@register_libtype
|
||||
class Track(Audio):
|
||||
TYPE = 'track'
|
||||
|
||||
|
@ -182,54 +185,8 @@ class Track(Audio):
|
|||
return self.server.url(self.grandparentThumb)
|
||||
|
||||
def album(self):
|
||||
return list_items(self.server, self.parentKey)[0]
|
||||
return utils.list_items(self.server, self.parentKey)[0]
|
||||
|
||||
def artist(self):
|
||||
raise NotImplemented
|
||||
#return list_items(self.server, self.grandparentKey)[0]
|
||||
|
||||
|
||||
def build_item(server, elem, initpath):
|
||||
AUDIOCLS = {Artist.TYPE:Artist, Album.TYPE:Album, Track.TYPE:Track}
|
||||
atype = elem.attrib.get('type')
|
||||
if atype in AUDIOCLS:
|
||||
cls = AUDIOCLS[atype]
|
||||
return cls(server, elem, initpath)
|
||||
raise UnknownType('Unknown audio type: %s' % atype)
|
||||
|
||||
|
||||
def find_key(server, key):
|
||||
path = '/library/metadata/{0}'.format(key)
|
||||
try:
|
||||
# Video seems to be the first sub element
|
||||
elem = server.query(path)[0]
|
||||
return build_item(server, elem, path)
|
||||
except:
|
||||
raise NotFound('Unable to find key: %s' % key)
|
||||
|
||||
|
||||
def find_item(server, path, title):
|
||||
for elem in server.query(path):
|
||||
if elem.attrib.get('title').lower() == title.lower():
|
||||
return build_item(server, elem, path)
|
||||
raise NotFound('Unable to find title: %s' % title)
|
||||
|
||||
|
||||
def list_items(server, path, audiotype=None, watched=None):
|
||||
items = []
|
||||
for elem in server.query(path):
|
||||
if audiotype and elem.attrib.get('type') != audiotype: continue
|
||||
if watched is True and elem.attrib.get('viewCount', 0) == 0: continue
|
||||
if watched is False and elem.attrib.get('viewCount', 0) >= 1: continue
|
||||
try:
|
||||
items.append(build_item(server, elem, path))
|
||||
except UnknownType:
|
||||
pass
|
||||
return items
|
||||
|
||||
|
||||
def search_type(audiotype):
|
||||
if audiotype == Artist.TYPE: return 8
|
||||
elif audiotype == Album.TYPE: return 9
|
||||
elif audiotype == Track.TYPE: return 10
|
||||
raise NotFound('Unknown audiotype: %s' % audiotype)
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
"""
|
||||
PlexLibrary
|
||||
"""
|
||||
from plexapi import video, audio, utils
|
||||
from plexapi import utils
|
||||
from plexapi.exceptions import NotFound
|
||||
from threading import Thread
|
||||
from Queue import Queue
|
||||
|
||||
|
||||
class Library(object):
|
||||
|
@ -41,44 +39,28 @@ class Library(object):
|
|||
raise NotFound('Invalid library section: %s' % title)
|
||||
|
||||
def all(self):
|
||||
return video.list_items(self.server, '/library/all')
|
||||
return utils.list_items(self.server, '/library/all')
|
||||
|
||||
def onDeck(self):
|
||||
return video.list_items(self.server, '/library/onDeck')
|
||||
return utils.list_items(self.server, '/library/onDeck')
|
||||
|
||||
def recentlyAdded(self):
|
||||
return video.list_items(self.server, '/library/recentlyAdded')
|
||||
return utils.list_items(self.server, '/library/recentlyAdded')
|
||||
|
||||
def get(self, title):
|
||||
return video.find_item(self.server, '/library/all', title)
|
||||
return utils.find_item(self.server, '/library/all', title)
|
||||
|
||||
def getByKey(self, key):
|
||||
return video.find_key(self.server, key)
|
||||
return utils.find_key(self.server, key)
|
||||
|
||||
def search(self, title, prefilter='all', mtype=None, **tags):
|
||||
# TODO: Handle tags much better.
|
||||
_audio = lambda: self._search(audio, title, prefilter, mtype, **tags)
|
||||
_video = lambda: self._search(video, title, prefilter, mtype, **tags)
|
||||
results = []
|
||||
qresults = utils.threaded([_audio, _video])
|
||||
for item in iter(qresults.get_nowait, None):
|
||||
results += item
|
||||
return results
|
||||
|
||||
def _search(self, module, title, prefilter='all', mtype=None, **tags):
|
||||
""" Search audio content.
|
||||
title: Title to search (pass None to search all titles).
|
||||
prefilter: One of {'all', 'onDeck', 'recentlyAdded'}.
|
||||
mtype: One of {'artist', 'album', 'track', 'movie', 'show', 'season', 'episode'}.
|
||||
tags: One of {country, director, genre, producer, actor, writer}.
|
||||
"""
|
||||
def search(self, title, prefilter='all', libtype=None, **tags):
|
||||
args = {}
|
||||
if title: args['title'] = title
|
||||
if mtype: args['type'] = module.search_type(mtype)
|
||||
if libtype: args['type'] = utils.search_type(libtype)
|
||||
for tag, obj in tags.items():
|
||||
args[tag] = obj.id
|
||||
query = '/library/%s%s' % (prefilter, utils.joinArgs(args))
|
||||
return module.list_items(self.server, query)
|
||||
return utils.list_items(self.server, query)
|
||||
|
||||
def cleanBundles(self):
|
||||
self.server.query('/library/clean/bundles')
|
||||
|
@ -110,13 +92,13 @@ class LibrarySection(object):
|
|||
return '<%s:%s>' % (self.__class__.__name__, title.encode('utf8'))
|
||||
|
||||
def _primary_list(self, key):
|
||||
return video.list_items(self.server, '/library/sections/%s/%s' % (self.key, key))
|
||||
return utils.list_items(self.server, '/library/sections/%s/%s' % (self.key, key))
|
||||
|
||||
def _secondary_list(self, key, input=None):
|
||||
choices = list_choices(self.server, '/library/sections/%s/%s' % (self.key, key))
|
||||
if not input:
|
||||
return list(choices.keys())
|
||||
return video.list_items(self.server, '/library/sections/%s/%s/%s' % (self.key, key, choices[input]))
|
||||
return utils.list_items(self.server, '/library/sections/%s/%s/%s' % (self.key, key, choices[input]))
|
||||
|
||||
def all(self):
|
||||
return self._primary_list('all')
|
||||
|
@ -150,7 +132,7 @@ class LibrarySection(object):
|
|||
|
||||
def get(self, title):
|
||||
path = '/library/sections/%s/all' % self.key
|
||||
return video.find_item(self.server, path, title)
|
||||
return utils.find_item(self.server, path, title)
|
||||
|
||||
def search(self, title, filter='all', vtype=None, **tags):
|
||||
""" Search section content.
|
||||
|
@ -161,11 +143,11 @@ class LibrarySection(object):
|
|||
"""
|
||||
args = {}
|
||||
if title: args['title'] = title
|
||||
if vtype: args['type'] = video.search_type(vtype)
|
||||
if vtype: args['type'] = utils.search_type(vtype)
|
||||
for tag, obj in tags.items():
|
||||
args[tag] = obj.id
|
||||
query = '/library/sections/%s/%s%s' % (self.key, filter, utils.joinArgs(args))
|
||||
return video.list_items(self.server, query)
|
||||
return utils.list_items(self.server, query)
|
||||
|
||||
def analyze(self):
|
||||
self.server.query('/library/sections/%s/analyze' % self.key)
|
||||
|
@ -199,7 +181,7 @@ class MovieSection(LibrarySection):
|
|||
return self._secondary_list('resolution', input)
|
||||
|
||||
def search(self, title, filter='all', **tags):
|
||||
return super(MovieSection, self).search(title, filter=filter, vtype=video.Movie.TYPE, **tags)
|
||||
return super(MovieSection, self).search(title, filter=filter, vtype='movie', **tags)
|
||||
|
||||
|
||||
class ShowSection(LibrarySection):
|
||||
|
@ -209,10 +191,10 @@ class ShowSection(LibrarySection):
|
|||
return self._primary_list('recentlyViewedShows')
|
||||
|
||||
def search(self, title, filter='all', **tags):
|
||||
return super(ShowSection, self).search(title, filter=filter, vtype=video.Show.TYPE, **tags)
|
||||
return super(ShowSection, self).search(title, filter=filter, vtype='show', **tags)
|
||||
|
||||
def searchEpisodes(self, title, filter='all', **tags):
|
||||
return super(ShowSection, self).search(title, filter=filter, vtype=video.Episode.TYPE, **tags)
|
||||
return super(ShowSection, self).search(title, filter=filter, vtype='episode', **tags)
|
||||
|
||||
|
||||
class MusicSection(LibrarySection):
|
||||
|
@ -227,23 +209,23 @@ class MusicSection(LibrarySection):
|
|||
"""
|
||||
args = {}
|
||||
if title: args['title'] = title
|
||||
if atype: args['type'] = audio.search_type(atype)
|
||||
if atype: args['type'] = utils.search_type(atype)
|
||||
for tag, obj in tags.items():
|
||||
args[tag] = obj.id
|
||||
query = '/library/sections/%s/%s%s' % (self.key, filter, utils.joinArgs(args))
|
||||
return audio.list_items(self.server, query)
|
||||
return utils.list_items(self.server, query)
|
||||
|
||||
def recentlyViewedShows(self):
|
||||
return self._primary_list('recentlyViewedShows')
|
||||
|
||||
def searchArtists(self, title, filter='all', **tags):
|
||||
return self.search(title, filter=filter, atype=audio.Artist.TYPE, **tags)
|
||||
return self.search(title, filter=filter, atype='artist', **tags)
|
||||
|
||||
def searchAlbums(self, title, filter='all', **tags):
|
||||
return self.search(title, filter=filter, atype=audio.Album.TYPE, **tags)
|
||||
return self.search(title, filter=filter, atype='album', **tags)
|
||||
|
||||
def searchTracks(self, title, filter='all', **tags):
|
||||
return self.search(title, filter=filter, atype=audio.Track.TYPE, **tags)
|
||||
return self.search(title, filter=filter, atype='track', **tags)
|
||||
|
||||
|
||||
def list_choices(server, path):
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
PlexAPI Play PlayQueues
|
||||
"""
|
||||
import plexapi, requests
|
||||
from plexapi import video
|
||||
from plexapi import utils
|
||||
|
||||
|
||||
|
@ -19,7 +18,7 @@ class PlayQueue(object):
|
|||
self.playQueueSelectedItemOffset = data.attrib.get('playQueueSelectedItemOffset')
|
||||
self.playQueueTotalCount = data.attrib.get('playQueueTotalCount')
|
||||
self.playQueueVersion = data.attrib.get('playQueueVersion')
|
||||
self.items = [video.build_item(server, elem, initpath) for elem in data]
|
||||
self.items = [utils.build_item(server, elem, initpath) for elem in data]
|
||||
|
||||
@classmethod
|
||||
def create(cls, server, video, shuffle=0, continuous=0):
|
||||
|
|
|
@ -4,7 +4,7 @@ PlexServer
|
|||
import requests
|
||||
from requests.status_codes import _codes as codes
|
||||
from plexapi import BASE_HEADERS, TIMEOUT
|
||||
from plexapi import log, audio, video, utils
|
||||
from plexapi import log, utils
|
||||
from plexapi.client import Client
|
||||
from plexapi.exceptions import BadRequest, NotFound
|
||||
from plexapi.library import Library
|
||||
|
@ -12,7 +12,6 @@ from plexapi.myplex import MyPlexAccount
|
|||
from plexapi.playqueue import PlayQueue
|
||||
from xml.etree import ElementTree
|
||||
|
||||
|
||||
try:
|
||||
from urllib import quote # Python2
|
||||
except ImportError:
|
||||
|
@ -95,24 +94,13 @@ class PlexServer(object):
|
|||
return ElementTree.fromstring(data) if data else None
|
||||
|
||||
def search(self, query, mediatype=None):
|
||||
# Searching at this level is meant to be quick and dirty, if you're looking
|
||||
# for more in-depth search filters look at plex.library.search().
|
||||
_audio = lambda: self._search(audio, query, mediatype)
|
||||
_video = lambda: self._search(video, query, mediatype)
|
||||
results = []
|
||||
qresults = utils.threaded([_audio, _video])
|
||||
for item in iter(qresults.get_nowait, None):
|
||||
results += item
|
||||
return results
|
||||
|
||||
def _search(self, module, query, mediatype=None):
|
||||
items = module.list_items(self, '/search?query=%s' % quote(query))
|
||||
items = utils.list_items(self, '/search?query=%s' % quote(query))
|
||||
if mediatype:
|
||||
return [item for item in items if item.type == mediatype]
|
||||
return items
|
||||
|
||||
def sessions(self):
|
||||
return video.list_items(self, '/status/sessions')
|
||||
return utils.list_items(self, '/status/sessions')
|
||||
|
||||
def url(self, path):
|
||||
if self.token:
|
||||
|
|
|
@ -4,11 +4,22 @@ PlexAPI Utils
|
|||
from datetime import datetime
|
||||
from threading import Thread
|
||||
from Queue import Queue
|
||||
from plexapi.exceptions import UnknownType
|
||||
try:
|
||||
from urllib import quote # Python2
|
||||
except ImportError:
|
||||
from urllib.parse import quote # Python3
|
||||
|
||||
|
||||
# Registry of library types we may come across when parsing XML. This allows us to
|
||||
# define a few helper functions to dynamically convery the XML into objects.
|
||||
# see build_item() below for an example.
|
||||
LIBRARY_TYPES = {}
|
||||
def register_libtype(cls):
|
||||
LIBRARY_TYPES[cls.TYPE] = cls
|
||||
return cls
|
||||
|
||||
|
||||
# This used to be a simple variable equal to '__NA__'. However, there has been need to
|
||||
# compare NA against None in some use cases. This object allows the internals of PlexAPI
|
||||
# to distinguish between unfetched values and fetched, but non-existent values.
|
||||
|
@ -19,14 +30,12 @@ class __NA__(object):
|
|||
def __nonzero__(self): return False # Python2; flake8: noqa
|
||||
def __repr__(self): return '__NA__' # flake8: noqa
|
||||
NA = __NA__()
|
||||
|
||||
|
||||
|
||||
# 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 automatically and update itself.
|
||||
class PlexPartialObject(object):
|
||||
""" 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 automatically and update itself.
|
||||
"""
|
||||
|
||||
def __init__(self, server, data, initpath):
|
||||
self.server = server
|
||||
|
@ -64,6 +73,55 @@ class PlexPartialObject(object):
|
|||
self._loadData(data[0])
|
||||
|
||||
|
||||
def build_item(server, elem, initpath):
|
||||
libtype = elem.attrib.get('type')
|
||||
if libtype in LIBRARY_TYPES:
|
||||
cls = LIBRARY_TYPES[libtype]
|
||||
return cls(server, elem, initpath)
|
||||
raise UnknownType('Unknown library type: %s' % libtype)
|
||||
|
||||
|
||||
def find_key(server, key):
|
||||
path = '/library/metadata/{0}'.format(key)
|
||||
try:
|
||||
# Item seems to be the first sub element
|
||||
elem = server.query(path)[0]
|
||||
return build_item(server, elem, path)
|
||||
except:
|
||||
raise NotFound('Unable to find key: %s' % key)
|
||||
|
||||
|
||||
def find_item(server, path, title):
|
||||
for elem in server.query(path):
|
||||
if elem.attrib.get('title').lower() == title.lower():
|
||||
return build_item(server, elem, path)
|
||||
raise NotFound('Unable to find item: %s' % title)
|
||||
|
||||
|
||||
def list_items(server, path, libtype=None, watched=None):
|
||||
items = []
|
||||
for elem in server.query(path):
|
||||
if libtype and elem.attrib.get('type') != libtype: continue
|
||||
if watched is True and elem.attrib.get('viewCount', 0) == 0: continue
|
||||
if watched is False and elem.attrib.get('viewCount', 0) >= 1: continue
|
||||
try:
|
||||
items.append(build_item(server, elem, path))
|
||||
except UnknownType:
|
||||
pass
|
||||
return items
|
||||
|
||||
|
||||
def search_type(libtype):
|
||||
if libtype == LIBRARY_TYPES['movie'].TYPE: return 1
|
||||
elif libtype == LIBRARY_TYPES['show'].TYPE: return 2
|
||||
elif libtype == LIBRARY_TYPES['season'].TYPE: return 3
|
||||
elif libtype == LIBRARY_TYPES['episode'].TYPE: return 4
|
||||
elif libtype == LIBRARY_TYPES['artist'].TYPE: return 8
|
||||
elif libtype == LIBRARY_TYPES['album'].TYPE: return 9
|
||||
elif libtype == LIBRARY_TYPES['track'].TYPE: return 10
|
||||
raise NotFound('Unknown libtype: %s' % libtype)
|
||||
|
||||
|
||||
def cast(func, value):
|
||||
if value not in [None, NA]:
|
||||
if func == bool:
|
||||
|
|
144
plexapi/video.py
144
plexapi/video.py
|
@ -3,20 +3,19 @@ PlexVideo
|
|||
"""
|
||||
import re
|
||||
from requests import put
|
||||
from plexapi import media, utils
|
||||
from plexapi.client import Client
|
||||
from plexapi.media import Media, TranscodeSession, Country, Director, Genre, Producer, Actor, Writer
|
||||
from plexapi.myplex import MyPlexUser
|
||||
from plexapi.exceptions import NotFound, UnknownType, Unsupported
|
||||
from plexapi.utils import PlexPartialObject, NA
|
||||
from plexapi.utils import cast, toDatetime
|
||||
|
||||
from plexapi.exceptions import Unsupported
|
||||
try:
|
||||
from urllib import urlencode # Python2
|
||||
except ImportError:
|
||||
from urllib.parse import urlencode # Python3
|
||||
|
||||
NA = utils.NA
|
||||
|
||||
class Video(PlexPartialObject):
|
||||
|
||||
class Video(utils.PlexPartialObject):
|
||||
TYPE = None
|
||||
|
||||
def _loadData(self, data):
|
||||
|
@ -29,22 +28,22 @@ class Video(PlexPartialObject):
|
|||
self.summary = data.attrib.get('summary', NA)
|
||||
self.art = data.attrib.get('art', NA)
|
||||
self.thumb = data.attrib.get('thumb', NA)
|
||||
self.addedAt = toDatetime(data.attrib.get('addedAt', NA))
|
||||
self.updatedAt = toDatetime(data.attrib.get('updatedAt', NA))
|
||||
self.lastViewedAt = toDatetime(data.attrib.get('lastViewedAt', NA))
|
||||
self.sessionKey = cast(int, data.attrib.get('sessionKey', NA))
|
||||
self.addedAt = utils.toDatetime(data.attrib.get('addedAt', NA))
|
||||
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt', NA))
|
||||
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt', NA))
|
||||
self.sessionKey = utils.cast(int, data.attrib.get('sessionKey', NA))
|
||||
self.user = self._find_user(data) # for active sessions
|
||||
self.player = self._find_player(data) # for active sessions
|
||||
self.transcodeSession = self._find_transcodeSession(data)
|
||||
if self.isFullObject():
|
||||
# These are auto-populated when requested
|
||||
self.media = [Media(self.server, elem, self.initpath, self) for elem in data if elem.tag == Media.TYPE]
|
||||
self.countries = [Country(self.server, elem) for elem in data if elem.tag == Country.TYPE]
|
||||
self.directors = [Director(self.server, elem) for elem in data if elem.tag == Director.TYPE]
|
||||
self.genres = [Genre(self.server, elem) for elem in data if elem.tag == Genre.TYPE]
|
||||
self.producers = [Producer(self.server, elem) for elem in data if elem.tag == Producer.TYPE]
|
||||
self.actors = [Actor(self.server, elem) for elem in data if elem.tag == Actor.TYPE]
|
||||
self.writers = [Writer(self.server, elem) for elem in data if elem.tag == Writer.TYPE]
|
||||
self.media = [media.Media(self.server, elem, self.initpath, self) for elem in data if elem.tag == media.Media.TYPE]
|
||||
self.countries = [media.Country(self.server, elem) for elem in data if elem.tag == media.Country.TYPE]
|
||||
self.directors = [media.Director(self.server, elem) for elem in data if elem.tag == media.Director.TYPE]
|
||||
self.genres = [media.Genre(self.server, elem) for elem in data if elem.tag == media.Genre.TYPE]
|
||||
self.producers = [media.Producer(self.server, elem) for elem in data if elem.tag == media.Producer.TYPE]
|
||||
self.actors = [media.Actor(self.server, elem) for elem in data if elem.tag == media.Actor.TYPE]
|
||||
self.writers = [media.Writer(self.server, elem) for elem in data if elem.tag == media.Writer.TYPE]
|
||||
|
||||
@property
|
||||
def thumbUrl(self):
|
||||
|
@ -65,12 +64,12 @@ class Video(PlexPartialObject):
|
|||
def _find_transcodeSession(self, data):
|
||||
elem = data.find('TranscodeSession')
|
||||
if elem is not None:
|
||||
return TranscodeSession(self.server, elem)
|
||||
return media.TranscodeSession(self.server, elem)
|
||||
return None
|
||||
|
||||
def iter_parts(self):
|
||||
for media in self.media:
|
||||
for part in media.parts:
|
||||
for item in self.media:
|
||||
for part in item.parts:
|
||||
yield part
|
||||
|
||||
def analyze(self):
|
||||
|
@ -116,6 +115,7 @@ class Video(PlexPartialObject):
|
|||
self.server.query('%s/refresh' % self.key, method=put)
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
class Movie(Video):
|
||||
TYPE = 'movie'
|
||||
|
||||
|
@ -124,17 +124,18 @@ class Movie(Video):
|
|||
self.studio = data.attrib.get('studio', NA)
|
||||
self.contentRating = data.attrib.get('contentRating', NA)
|
||||
self.rating = data.attrib.get('rating', NA)
|
||||
self.viewCount = cast(int, data.attrib.get('viewCount', 0))
|
||||
self.viewOffset = cast(int, data.attrib.get('viewOffset', 0))
|
||||
self.year = cast(int, data.attrib.get('year', NA))
|
||||
self.viewCount = utils.cast(int, data.attrib.get('viewCount', 0))
|
||||
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
||||
self.year = utils.cast(int, data.attrib.get('year', NA))
|
||||
self.summary = data.attrib.get('summary', NA)
|
||||
self.tagline = data.attrib.get('tagline', NA)
|
||||
self.duration = cast(int, data.attrib.get('duration', NA))
|
||||
self.originallyAvailableAt = toDatetime(data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d')
|
||||
self.duration = utils.cast(int, data.attrib.get('duration', NA))
|
||||
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d')
|
||||
self.primaryExtraKey = data.attrib.get('primaryExtraKey', NA)
|
||||
self.is_watched = bool(self.viewCount > 0) # custom attr
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
class Show(Video):
|
||||
TYPE = 'show'
|
||||
|
||||
|
@ -143,30 +144,30 @@ class Show(Video):
|
|||
self.studio = data.attrib.get('studio', NA)
|
||||
self.contentRating = data.attrib.get('contentRating', NA)
|
||||
self.rating = data.attrib.get('rating', NA)
|
||||
self.year = cast(int, data.attrib.get('year', NA))
|
||||
self.year = utils.cast(int, data.attrib.get('year', NA))
|
||||
self.banner = data.attrib.get('banner', NA)
|
||||
self.theme = data.attrib.get('theme', NA)
|
||||
self.duration = cast(int, data.attrib.get('duration', NA))
|
||||
self.originallyAvailableAt = toDatetime(data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d')
|
||||
self.leafCount = cast(int, data.attrib.get('leafCount', NA))
|
||||
self.viewedLeafCount = cast(int, data.attrib.get('viewedLeafCount', NA))
|
||||
self.childCount = cast(int, data.attrib.get('childCount', NA))
|
||||
self.duration = utils.cast(int, data.attrib.get('duration', NA))
|
||||
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d')
|
||||
self.leafCount = utils.cast(int, data.attrib.get('leafCount', NA))
|
||||
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount', NA))
|
||||
self.childCount = utils.cast(int, data.attrib.get('childCount', NA))
|
||||
|
||||
def seasons(self):
|
||||
path = '/library/metadata/%s/children' % self.ratingKey
|
||||
return list_items(self.server, path, Season.TYPE)
|
||||
return utils.list_items(self.server, path, Season.TYPE)
|
||||
|
||||
def season(self, title):
|
||||
path = '/library/metadata/%s/children' % self.ratingKey
|
||||
return find_item(self.server, path, title)
|
||||
return utils.find_item(self.server, path, title)
|
||||
|
||||
def episodes(self, watched=None):
|
||||
leavesKey = '/library/metadata/%s/allLeaves' % self.ratingKey
|
||||
return list_items(self.server, leavesKey, watched=watched)
|
||||
return utils.list_items(self.server, leavesKey, watched=watched)
|
||||
|
||||
def episode(self, title):
|
||||
path = '/library/metadata/%s/allLeaves' % self.ratingKey
|
||||
return find_item(self.server, path, title)
|
||||
return utils.find_item(self.server, path, title)
|
||||
|
||||
def watched(self):
|
||||
return self.episodes(watched=True)
|
||||
|
@ -181,6 +182,7 @@ class Show(Video):
|
|||
self.server.query('/library/metadata/%s/refresh' % self.ratingKey)
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
class Season(Video):
|
||||
TYPE = 'season'
|
||||
|
||||
|
@ -196,22 +198,22 @@ class Season(Video):
|
|||
self.parentIndex = data.attrib.get('parentIndex', NA)
|
||||
self.parentThumb = data.attrib.get('parentThumb', NA)
|
||||
self.parentTheme = data.attrib.get('parentTheme', NA)
|
||||
self.leafCount = cast(int, data.attrib.get('leafCount', NA))
|
||||
self.viewedLeafCount = cast(int, data.attrib.get('viewedLeafCount', NA))
|
||||
self.leafCount = utils.cast(int, data.attrib.get('leafCount', NA))
|
||||
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount', NA))
|
||||
|
||||
def episodes(self, watched=None):
|
||||
childrenKey = '/library/metadata/%s/children' % self.ratingKey
|
||||
return list_items(self.server, childrenKey, watched=watched)
|
||||
return utils.list_items(self.server, childrenKey, watched=watched)
|
||||
|
||||
def episode(self, title):
|
||||
path = '/library/metadata/%s/children' % self.ratingKey
|
||||
return find_item(self.server, path, title)
|
||||
return utils.find_item(self.server, path, title)
|
||||
|
||||
def get(self, title):
|
||||
return self.episode(title)
|
||||
|
||||
def show(self):
|
||||
return list_items(self.server, self.parentKey)[0]
|
||||
return utils.list_items(self.server, self.parentKey)[0]
|
||||
|
||||
def watched(self):
|
||||
return self.episodes(watched=True)
|
||||
|
@ -220,6 +222,7 @@ class Season(Video):
|
|||
return self.episodes(watched=False)
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
class Episode(Video):
|
||||
TYPE = 'episode'
|
||||
|
||||
|
@ -236,11 +239,11 @@ class Episode(Video):
|
|||
self.contentRating = data.attrib.get('contentRating', NA)
|
||||
self.index = data.attrib.get('index', NA)
|
||||
self.rating = data.attrib.get('rating', NA)
|
||||
self.viewCount = cast(int, data.attrib.get('viewCount', 0))
|
||||
self.viewOffset = cast(int, data.attrib.get('viewOffset', 0))
|
||||
self.year = cast(int, data.attrib.get('year', NA))
|
||||
self.duration = cast(int, data.attrib.get('duration', NA))
|
||||
self.originallyAvailableAt = toDatetime(data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d')
|
||||
self.viewCount = utils.cast(int, data.attrib.get('viewCount', 0))
|
||||
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
||||
self.year = utils.cast(int, data.attrib.get('year', NA))
|
||||
self.duration = utils.cast(int, data.attrib.get('duration', NA))
|
||||
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d')
|
||||
self.is_watched = bool(self.viewCount > 0) # custom attr
|
||||
|
||||
@property
|
||||
|
@ -248,54 +251,7 @@ class Episode(Video):
|
|||
return self.server.url(self.grandparentThumb)
|
||||
|
||||
def season(self):
|
||||
return list_items(self.server, self.parentKey)[0]
|
||||
return utils.list_items(self.server, self.parentKey)[0]
|
||||
|
||||
def show(self):
|
||||
return list_items(self.server, self.grandparentKey)[0]
|
||||
|
||||
|
||||
def build_item(server, elem, initpath):
|
||||
VIDEOCLS = {Movie.TYPE:Movie, Show.TYPE:Show, Season.TYPE:Season, Episode.TYPE:Episode}
|
||||
vtype = elem.attrib.get('type')
|
||||
if vtype in VIDEOCLS:
|
||||
cls = VIDEOCLS[vtype]
|
||||
return cls(server, elem, initpath)
|
||||
raise UnknownType('Unknown video type: %s' % vtype)
|
||||
|
||||
|
||||
def find_key(server, key):
|
||||
path = '/library/metadata/{0}'.format(key)
|
||||
try:
|
||||
# Video seems to be the first sub element
|
||||
elem = server.query(path)[0]
|
||||
return build_item(server, elem, path)
|
||||
except:
|
||||
raise NotFound('Unable to find key: %s' % key)
|
||||
|
||||
|
||||
def find_item(server, path, title):
|
||||
for elem in server.query(path):
|
||||
if elem.attrib.get('title').lower() == title.lower():
|
||||
return build_item(server, elem, path)
|
||||
raise NotFound('Unable to find title: %s' % title)
|
||||
|
||||
|
||||
def list_items(server, path, videotype=None, watched=None):
|
||||
items = []
|
||||
for elem in server.query(path):
|
||||
if videotype and elem.attrib.get('type') != videotype: continue
|
||||
if watched is True and elem.attrib.get('viewCount', 0) == 0: continue
|
||||
if watched is False and elem.attrib.get('viewCount', 0) >= 1: continue
|
||||
try:
|
||||
items.append(build_item(server, elem, path))
|
||||
except UnknownType:
|
||||
pass
|
||||
return items
|
||||
|
||||
|
||||
def search_type(videotype):
|
||||
if videotype == Movie.TYPE: return 1
|
||||
elif videotype == Show.TYPE: return 2
|
||||
elif videotype == Season.TYPE: return 3
|
||||
elif videotype == Episode.TYPE: return 4
|
||||
raise NotFound('Unknown videotype: %s' % videotype)
|
||||
return utils.list_items(self.server, self.grandparentKey)[0]
|
||||
|
|
|
@ -164,10 +164,9 @@ def test_navigate_around_show(plex, user=None):
|
|||
|
||||
@register('navigate,audio')
|
||||
def test_navigate_around_artist(plex, user=None):
|
||||
section = plex.library.section(AUDIO_SECTION)
|
||||
print(section)
|
||||
artist = plex.library.section(AUDIO_SECTION).get(AUDIO_ARTIST)
|
||||
print(artist)
|
||||
#section = plex.library.section(AUDIO_SECTION)
|
||||
#artist = plex.library.section(AUDIO_SECTION).get(AUDIO_ARTIST)
|
||||
assert False, 'Test not implemented!'
|
||||
|
||||
# seasons = show.seasons()
|
||||
# season = show.season(SHOW_SEASON)
|
||||
|
|
|
@ -62,7 +62,7 @@ def run_tests(module, args):
|
|||
tests['passed'] += 1
|
||||
except Exception as err:
|
||||
errstr = str(err)
|
||||
errstr += '\n%s' % traceback.format_exc() if args.verbose else errstr
|
||||
errstr += '\n%s' % traceback.format_exc() if args.verbose else ''
|
||||
log(2, 'FAIL!: %s' % errstr, 'red')
|
||||
tests['failed'] += 1
|
||||
log(0, '')
|
||||
|
|
Loading…
Reference in a new issue