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

View file

@ -11,11 +11,12 @@ from plexapi.exceptions import BadRequest, NotFound
class Library(object): class Library(object):
def __init__(self, server, data): def __init__(self, server, data):
self.server = server
self.identifier = data.attrib.get('identifier') self.identifier = data.attrib.get('identifier')
self.mediaTagVersion = data.attrib.get('mediaTagVersion') self.mediaTagVersion = data.attrib.get('mediaTagVersion')
self.server = server
self.title1 = data.attrib.get('title1') self.title1 = data.attrib.get('title1')
self.title2 = data.attrib.get('title2') self.title2 = data.attrib.get('title2')
self._sectionsByID = {} # cached section UUIDs
def __repr__(self): def __repr__(self):
return '<Library:%s>' % self.title1.encode('utf8') return '<Library:%s>' % self.title1.encode('utf8')
@ -33,7 +34,9 @@ class Library(object):
stype = elem.attrib['type'] stype = elem.attrib['type']
if stype in SECTION_TYPES: if stype in SECTION_TYPES:
cls = SECTION_TYPES[stype] 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 return items
def section(self, title=None): def section(self, title=None):
@ -42,6 +45,11 @@ class Library(object):
return item return item
raise NotFound('Invalid library section: %s' % title) raise NotFound('Invalid library section: %s' % title)
def sectionByID(self, sectionID):
if not self._sectionsByID:
self.sections()
return self._sectionsByID[sectionID]
def all(self): def all(self):
return utils.listItems(self.server, '/library/all') return utils.listItems(self.server, '/library/all')
@ -94,12 +102,23 @@ class LibrarySection(object):
def __init__(self, server, data, initpath): def __init__(self, server, data, initpath):
self.server = server self.server = server
self.initpath = initpath 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.key = data.attrib.get('key')
self.title = data.attrib.get('title')
self.scanner = data.attrib.get('scanner')
self.language = data.attrib.get('language') 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): def __repr__(self):
title = self.title.replace(' ','.')[0:20] title = self.title.replace(' ','.')[0:20]

View file

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

View file

@ -40,58 +40,50 @@ class Playlist(PlexPartialObject, Playable):
return utils.listItems(self.server, path) return utils.listItems(self.server, path)
def addItems(self, items): 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)): if not isinstance(items, (list, tuple)):
items = [items] items = [items]
ratingKeys = [] ratingKeys = []
for item in items: for item in items:
if item.__class__.LISTTYPE != self.playlistType: if item.listType != self.playlistType:
raise BadRequest('Can not mix media types when building a playlist: %s and %s' % (self.playlistType, item.__class__.LISTTYPE)) raise BadRequest('Can not mix media types when building a playlist: %s and %s' % (self.playlistType, item.listType))
ratingKeys.append(item.ratingKey) ratingKeys.append(item.ratingKey)
uuid = items[0].section().uuid
ratingKeys = ','.join(ratingKeys)
path = '%s/items%s' % (self.key, utils.joinArgs({ 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) return self.server.query(path, method=self.server.session.put)
def removeItem(self, item): def removeItem(self, item):
# DELETE /playlists/29988/items/4866
path = '%s/items/%s' % (self.key, item.playlistItemID) path = '%s/items/%s' % (self.key, item.playlistItemID)
return self.server.query(path, method=self.server.session.delete) return self.server.query(path, method=self.server.session.delete)
def moveItem(self, item, after=None): def moveItem(self, item, after=None):
# 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) path = '%s/items/%s/move' % (self.key, item.playlistItemID)
if after: if after: path += '?after=%s' % after.playlistItemID
path += '?after=%s' % after.playlistItemID
return self.server.query(path, method=self.server.session.put) return self.server.query(path, method=self.server.session.put)
def edit(self, title=None, summary=None): def edit(self, title=None, summary=None):
# PUT /library/metadata/29988?title=You%20Look%20Like%20Gollum2&summary=foobar
path = '/library/metadata/%s%s' % (self.ratingKey, utils.joinArgs({'title':title, 'summary':summary})) path = '/library/metadata/%s%s' % (self.ratingKey, utils.joinArgs({'title':title, 'summary':summary}))
return self.server.query(path, method=self.server.session.put) return self.server.query(path, method=self.server.session.put)
def delete(self): def delete(self):
# DELETE /library/metadata/29988
return self.server.query(self.key, method=self.server.session.delete) return self.server.query(self.key, method=self.server.session.delete)
@classmethod @classmethod
def create(cls, server, title, items): def create(cls, server, title, items):
# 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)): if not isinstance(items, (list, tuple)):
items = [items] items = [items]
# collect a list of itemkeys and make sure all items share the same listtype
listtype = items[0].__class__.LISTTYPE
ratingKeys = [] ratingKeys = []
for item in items: 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') raise BadRequest('Can not mix media types when building a playlist')
ratingKeys.append(item.ratingKey) ratingKeys.append(item.ratingKey)
# build and send the request uuid = items[0].section().uuid
ratingKeys = ','.join(ratingKeys)
path = '/playlists%s' % utils.joinArgs({ path = '/playlists%s' % utils.joinArgs({
'uri': 'library://__GID__/directory//library/metadata/%s' % ','.join(ratingKeys), 'uri': 'library://%s/directory//library/metadata/%s' % (uuid, ratingKeys),
'type': listtype, 'type': items[0].listType,
'title': title, 'title': title,
'smart': 0 'smart': 0
}) })

View file

@ -23,11 +23,9 @@ class PlayQueue(object):
@classmethod @classmethod
def create(cls, server, video, shuffle=0, continuous=0): def create(cls, server, video, shuffle=0, continuous=0):
# TODO: Fix this up, create tests.. uuid = video.section().uuid
# 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.
path = '/playQueues%s' % utils.joinArgs({ path = '/playQueues%s' % utils.joinArgs({
'uri': 'library://__GID__/item/%s' % video.key, 'uri': 'library://%s/item/%s' % (uuid, video.key),
'key': video.key, 'key': video.key,
'type': 'video', 'type': 'video',
'shuffle': shuffle, 'shuffle': shuffle,

View file

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

View file

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

View file

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

View file

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