Cleanup playlist support; Fix UUID on URLs; Better method to store listTypes; Cache section IDs in library

This commit is contained in:
Michael Shepanski 2016-04-11 22:43:21 -04:00
parent 3138ad1087
commit 748fc68406
9 changed files with 70 additions and 51 deletions

View file

@ -14,6 +14,7 @@ class Audio(PlexPartialObject):
super(Audio, self).__init__(data, initpath, server)
def _loadData(self, data):
self.listType = 'audio'
self.addedAt = utils.toDatetime(data.attrib.get('addedAt', NA))
self.index = data.attrib.get('index', NA)
self.key = data.attrib.get('key', NA)
@ -34,19 +35,21 @@ class Audio(PlexPartialObject):
def refresh(self):
self.server.query('%s/refresh' % self.key, method=self.server.session.put)
def section(self):
return self.server.library.sectionByID(self.librarySectionID)
@utils.register_libtype
class Artist(Audio):
TYPE = 'artist'
LISTTYPE = 'audio'
def _loadData(self, data):
Audio._loadData(self, data)
self.art = data.attrib.get('art', NA)
self.guid = data.attrib.get('guid', NA)
self.key = self.key.replace('/children', '') # FIX_BUG_50
self.location = utils.findLocation(data)
self.location = utils.findLocations(data, single=True)
if self.isFullObject():
self.countries = [media.Country(self.server, e) for e in data if e.tag == media.Country.TYPE]
self.genres = [media.Genre(self.server, e) for e in data if e.tag == media.Genre.TYPE]
@ -75,7 +78,6 @@ class Artist(Audio):
@utils.register_libtype
class Album(Audio):
TYPE = 'album'
LISTTYPE = 'audio'
def _loadData(self, data):
Audio._loadData(self, data)
@ -115,7 +117,6 @@ class Album(Audio):
@utils.register_libtype
class Track(Audio, Playable):
TYPE = 'track'
LISTTYPE = 'audio'
def _loadData(self, data):
Audio._loadData(self, data)

View file

@ -11,11 +11,12 @@ from plexapi.exceptions import BadRequest, NotFound
class Library(object):
def __init__(self, server, data):
self.server = server
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')
@ -33,7 +34,9 @@ class Library(object):
stype = elem.attrib['type']
if stype in SECTION_TYPES:
cls = SECTION_TYPES[stype]
items.append(cls(self.server, elem, path))
section = cls(self.server, elem, path)
self._sectionsByID[section.key] = section
items.append(section)
return items
def section(self, title=None):
@ -41,6 +44,11 @@ class Library(object):
if item.title == title:
return item
raise NotFound('Invalid library section: %s' % title)
def sectionByID(self, sectionID):
if not self._sectionsByID:
self.sections()
return self._sectionsByID[sectionID]
def all(self):
return utils.listItems(self.server, '/library/all')
@ -94,12 +102,23 @@ class LibrarySection(object):
def __init__(self, server, data, initpath):
self.server = server
self.initpath = initpath
self.type = data.attrib.get('type')
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.title = data.attrib.get('title')
self.scanner = data.attrib.get('scanner')
self.language = data.attrib.get('language')
# TODO: Add Location
self.language = data.attrib.get('language')
self.locations = utils.findLocations(data)
self.refreshing = utils.cast(bool, data.attrib.get('refreshing'))
self.scanner = data.attrib.get('scanner')
self.thumb = data.attrib.get('thumb')
self.title = data.attrib.get('title')
self.type = data.attrib.get('type')
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
self.uuid = data.attrib.get('uuid')
def __repr__(self):
title = self.title.replace(' ','.')[0:20]

View file

@ -10,12 +10,12 @@ NA = utils.NA
@utils.register_libtype
class Photoalbum(PlexPartialObject):
TYPE = 'photoalbum'
LISTTYPE = 'photo'
def __init__(self, server, data, initpath):
super(Photoalbum, self).__init__(data, initpath, server)
def _loadData(self, data):
self.listType = 'photo'
self.addedAt = utils.toDatetime(data.attrib.get('addedAt', NA))
self.art = data.attrib.get('art', NA)
self.composite = data.attrib.get('composite', NA)
@ -37,17 +37,20 @@ class Photoalbum(PlexPartialObject):
def photo(self, title):
path = '/library/metadata/%s/children' % self.ratingKey
return utils.findItem(self.server, path, title)
def section(self):
return self.server.library.sectionByID(self.librarySectionID)
@utils.register_libtype
class Photo(PlexPartialObject):
TYPE = 'photo'
LISTTYPE = 'photo'
def __init__(self, server, data, initpath):
super(Photo, self).__init__(data, initpath, server)
def _loadData(self, data):
self.listType = 'photo'
self.addedAt = utils.toDatetime(data.attrib.get('addedAt', NA))
self.index = utils.cast(int, data.attrib.get('index', NA))
self.key = data.attrib.get('key', NA)

View file

@ -40,58 +40,50 @@ class Playlist(PlexPartialObject, Playable):
return utils.listItems(self.server, path)
def addItems(self, items):
# PUT /playlists/29988/items?uri=library%3A%2F%2F32268d7c-3e8c-4ab5-98ad-bad8a3b78c63%2Fitem%2F%252Flibrary%252Fmetadata%252F801
if not isinstance(items, (list, tuple)):
items = [items]
ratingKeys = []
for item in items:
if item.__class__.LISTTYPE != self.playlistType:
raise BadRequest('Can not mix media types when building a playlist: %s and %s' % (self.playlistType, item.__class__.LISTTYPE))
if item.listType != self.playlistType:
raise BadRequest('Can not mix media types when building a playlist: %s and %s' % (self.playlistType, item.listType))
ratingKeys.append(item.ratingKey)
uuid = items[0].section().uuid
ratingKeys = ','.join(ratingKeys)
path = '%s/items%s' % (self.key, utils.joinArgs({
'uri': 'library://__GID__/directory//library/metadata/%s' % ','.join(ratingKeys),
'uri': 'library://%s/directory//library/metadata/%s' % (uuid, ratingKeys),
}))
return self.server.query(path, method=self.server.session.put)
def removeItem(self, item):
# DELETE /playlists/29988/items/4866
path = '%s/items/%s' % (self.key, item.playlistItemID)
return self.server.query(path, method=self.server.session.delete)
def moveItem(self, item, after=None):
# PUT /playlists/29988/items/4556/move?after=4445
# PUT /playlists/29988/items/4556/move (to first item)
path = '%s/items/%s/move' % (self.key, item.playlistItemID)
if after:
path += '?after=%s' % after.playlistItemID
if after: path += '?after=%s' % after.playlistItemID
return self.server.query(path, method=self.server.session.put)
def edit(self, title=None, summary=None):
# PUT /library/metadata/29988?title=You%20Look%20Like%20Gollum2&summary=foobar
path = '/library/metadata/%s%s' % (self.ratingKey, utils.joinArgs({'title':title, 'summary':summary}))
return self.server.query(path, method=self.server.session.put)
def delete(self):
# DELETE /library/metadata/29988
return self.server.query(self.key, method=self.server.session.delete)
@classmethod
def create(cls, server, title, items):
# NOTE: I have not yet figured out what __GID__ is below or where the proper value
# can be obtained. However, the good news is passing anything in seems to work.
if not isinstance(items, (list, tuple)):
items = [items]
# collect a list of itemkeys and make sure all items share the same listtype
listtype = items[0].__class__.LISTTYPE
ratingKeys = []
for item in items:
if item.__class__.LISTTYPE != listtype:
if item.listType != items[0].listType:
raise BadRequest('Can not mix media types when building a playlist')
ratingKeys.append(item.ratingKey)
# build and send the request
uuid = items[0].section().uuid
ratingKeys = ','.join(ratingKeys)
path = '/playlists%s' % utils.joinArgs({
'uri': 'library://__GID__/directory//library/metadata/%s' % ','.join(ratingKeys),
'type': listtype,
'uri': 'library://%s/directory//library/metadata/%s' % (uuid, ratingKeys),
'type': items[0].listType,
'title': title,
'smart': 0
})

View file

@ -23,11 +23,9 @@ class PlayQueue(object):
@classmethod
def create(cls, server, video, shuffle=0, continuous=0):
# TODO: Fix this up, create tests..
# NOTE: I have not yet figured out what __GID__ is below or where the proper value
# can be obtained. However, the good news is passing anything in seems to work.
uuid = video.section().uuid
path = '/playQueues%s' % utils.joinArgs({
'uri': 'library://__GID__/item/%s' % video.key,
'uri': 'library://%s/item/%s' % (uuid, video.key),
'key': video.key,
'type': 'video',
'shuffle': shuffle,

View file

@ -37,6 +37,7 @@ class PlexServer(object):
self.transcoderActiveVideoSessions = int(data.attrib.get('transcoderActiveVideoSessions', 0))
self.updatedAt = int(data.attrib.get('updatedAt', 0))
self.version = data.attrib.get('version')
self._library = None # cached library
def __repr__(self):
return '<%s:%s>' % (self.__class__.__name__, self.baseurl)
@ -50,7 +51,9 @@ class PlexServer(object):
@property
def library(self):
return Library(self, self.query('/library/'))
if not self._library:
self._library = Library(self, self.query('/library/'))
return self._library
def account(self):
data = self.query('/myplex/account')

View file

@ -163,11 +163,14 @@ def findItem(server, path, title):
raise NotFound('Unable to find item: %s' % title)
def findLocation(data):
elem = data.find('Location')
if elem is not None:
return elem.attrib.get('path')
return None
def findLocations(data, single=False):
locations = []
for elem in data:
if elem.tag == 'Location':
locations.append(elem.attrib.get('path'))
if single:
return locations[0] if locations else None
return locations
def findPlayer(server, data):

View file

@ -14,6 +14,7 @@ class Video(PlexPartialObject):
super(Video, self).__init__(data, initpath, server)
def _loadData(self, data):
self.listType = 'video'
self.addedAt = utils.toDatetime(data.attrib.get('addedAt', NA))
self.key = data.attrib.get('key', NA)
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt', NA))
@ -50,12 +51,14 @@ class Video(PlexPartialObject):
def refresh(self):
self.server.query('%s/refresh' % self.key, method=self.server.session.put)
def section(self):
return self.server.library.sectionByID(self.librarySectionID)
@utils.register_libtype
class Movie(Video, Playable):
TYPE = 'movie'
LISTTYPE = 'video'
def _loadData(self, data):
Video._loadData(self, data)
@ -102,7 +105,6 @@ class Movie(Video, Playable):
@utils.register_libtype
class Show(Video):
TYPE = 'show'
LISTTYPE = 'video'
def _loadData(self, data):
Video._loadData(self, data)
@ -113,7 +115,7 @@ class Show(Video):
self.duration = utils.cast(int, data.attrib.get('duration', NA))
self.guid = data.attrib.get('guid', NA)
self.leafCount = utils.cast(int, data.attrib.get('leafCount', NA))
self.location = utils.findLocation(data)
self.location = utils.findLocations(data, single=True)
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d')
self.rating = utils.cast(float, data.attrib.get('rating', NA))
self.studio = data.attrib.get('studio', NA)
@ -164,7 +166,6 @@ class Show(Video):
@utils.register_libtype
class Season(Video):
TYPE = 'season'
LISTTYPE = 'video'
def _loadData(self, data):
Video._loadData(self, data)
@ -201,7 +202,6 @@ class Season(Video):
@utils.register_libtype
class Episode(Video, Playable):
TYPE = 'episode'
LISTTYPE = 'video'
def _loadData(self, data):
Video._loadData(self, data)

View file

@ -284,12 +284,12 @@ def test_list_playlists(plex, account=None):
@register('playlist')
def test_create_playlist(plex, account=None):
# create the playlist
title = 'test_create_playlist'
log(2, 'Creating playlist %s..' % title)
episodes = plex.library.section(SHOW_SECTION).get(SHOW_TITLE).episodes()
playlist = plex.createPlaylist(title, episodes[:3])
try:
# create the playlist
title = 'test_create_playlist'
log(2, 'Creating playlist %s..' % title)
episodes = plex.library.section(SHOW_SECTION).get(SHOW_TITLE).episodes()
playlist = plex.createPlaylist(title, episodes[:3])
items = playlist.items()
log(4, 'Title: %s' % playlist.title)
log(4, 'Items: %s' % items)