Added playlist support

This commit is contained in:
Michael Shepanski 2016-04-10 23:49:23 -04:00
parent 09a7ae80db
commit 3138ad1087
8 changed files with 147 additions and 1 deletions

View file

@ -8,6 +8,7 @@ NA = utils.NA
class Audio(PlexPartialObject): class Audio(PlexPartialObject):
TYPE = None
def __init__(self, server, data, initpath): def __init__(self, server, data, initpath):
super(Audio, self).__init__(data, initpath, server) super(Audio, self).__init__(data, initpath, server)
@ -38,6 +39,7 @@ class Audio(PlexPartialObject):
@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)
@ -73,6 +75,7 @@ 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)
@ -112,6 +115,7 @@ 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

@ -10,6 +10,7 @@ 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)
@ -41,6 +42,7 @@ class Photoalbum(PlexPartialObject):
@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)

View file

@ -2,7 +2,9 @@
""" """
PlexPlaylist PlexPlaylist
""" """
import requests
from plexapi import utils from plexapi import utils
from plexapi.exceptions import BadRequest
from plexapi.utils import cast, toDatetime from plexapi.utils import cast, toDatetime
from plexapi.utils import PlexPartialObject, Playable from plexapi.utils import PlexPartialObject, Playable
NA = utils.NA NA = utils.NA
@ -23,7 +25,7 @@ class Playlist(PlexPartialObject, Playable):
self.durationInSeconds = cast(int, data.attrib.get('durationInSeconds', NA)) self.durationInSeconds = cast(int, data.attrib.get('durationInSeconds', NA))
self.guid = data.attrib.get('guid', NA) self.guid = data.attrib.get('guid', NA)
self.key = data.attrib.get('key', NA) self.key = data.attrib.get('key', NA)
if self.key: self.key = self.key.replace('/items', '') # FIX_BUG_50 self.key = self.key.replace('/items', '') if self.key else self.key # FIX_BUG_50
self.leafCount = cast(int, data.attrib.get('leafCount', NA)) self.leafCount = cast(int, data.attrib.get('leafCount', NA))
self.playlistType = data.attrib.get('playlistType', NA) self.playlistType = data.attrib.get('playlistType', NA)
self.ratingKey = data.attrib.get('ratingKey', NA) self.ratingKey = data.attrib.get('ratingKey', NA)
@ -36,3 +38,62 @@ class Playlist(PlexPartialObject, Playable):
def items(self): def items(self):
path = '%s/items' % self.key path = '%s/items' % self.key
return utils.listItems(self.server, path) return utils.listItems(self.server, path)
def addItems(self, items):
# 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))
ratingKeys.append(item.ratingKey)
path = '%s/items%s' % (self.key, utils.joinArgs({
'uri': 'library://__GID__/directory//library/metadata/%s' % ','.join(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
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:
raise BadRequest('Can not mix media types when building a playlist')
ratingKeys.append(item.ratingKey)
# build and send the request
path = '/playlists%s' % utils.joinArgs({
'uri': 'library://__GID__/directory//library/metadata/%s' % ','.join(ratingKeys),
'type': listtype,
'title': title,
'smart': 0
})
data = server.query(path, method=server.session.post)[0]
return cls(server, data, initpath=path)

View file

@ -23,6 +23,7 @@ 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..
# NOTE: I have not yet figured out what __GID__ is below or where the proper value # 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. # can be obtained. However, the good news is passing anything in seems to work.
path = '/playQueues%s' % utils.joinArgs({ path = '/playQueues%s' % utils.joinArgs({

View file

@ -11,6 +11,7 @@ from plexapi.compat import quote
from plexapi.client import PlexClient from plexapi.client import PlexClient
from plexapi.exceptions import BadRequest, NotFound from plexapi.exceptions import BadRequest, NotFound
from plexapi.library import Library from plexapi.library import Library
from plexapi.playlist import Playlist
from plexapi.playqueue import PlayQueue from plexapi.playqueue import PlayQueue
from xml.etree import ElementTree from xml.etree import ElementTree
@ -69,6 +70,9 @@ class PlexServer(object):
return PlexClient(baseurl, server=self, data=elem) return PlexClient(baseurl, server=self, data=elem)
raise NotFound('Unknown client name: %s' % name) raise NotFound('Unknown client name: %s' % name)
def createPlaylist(self, title, items):
return Playlist.create(self, title, items)
def createPlayQueue(self, item): def createPlayQueue(self, item):
return PlayQueue.create(self, item) return PlayQueue.create(self, item)
@ -82,6 +86,8 @@ class PlexServer(object):
return utils.listItems(self, '/status/sessions/history/all') return utils.listItems(self, '/status/sessions/history/all')
def playlists(self): def playlists(self):
# TODO: Add sort and type options?
# /playlists/all?type=15&sort=titleSort%3Aasc&playlistType=video&smart=0
return utils.listItems(self, '/playlists') return utils.listItems(self, '/playlists')
def playlist(self, title=None): # noqa def playlist(self, title=None): # noqa

View file

@ -92,6 +92,8 @@ class Playable(object):
self.transcodeSession = findTranscodeSession(self.server, data) self.transcodeSession = findTranscodeSession(self.server, data)
# data for history details (/status/sessions/history/all) # data for history details (/status/sessions/history/all)
self.viewedAt = toDatetime(data.attrib.get('viewedAt', NA)) self.viewedAt = toDatetime(data.attrib.get('viewedAt', NA))
# data for playlist items
self.playlistItemID = cast(int, data.attrib.get('playlistItemID', NA))
def getStreamURL(self, **params): def getStreamURL(self, **params):
if self.TYPE not in ('movie', 'episode', 'track'): if self.TYPE not in ('movie', 'episode', 'track'):

View file

@ -55,6 +55,7 @@ class Video(PlexPartialObject):
@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)
@ -101,6 +102,7 @@ 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)
@ -162,6 +164,7 @@ 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)
@ -198,6 +201,7 @@ 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

@ -271,6 +271,72 @@ def test_refresh_video(plex, account=None):
result[0].refresh() result[0].refresh()
#-----------------------
# Playlists
#-----------------------
@register('playlist')
def test_list_playlists(plex, account=None):
playlists = plex.playlists()
for playlist in playlists:
log(2, playlist.title)
@register('playlist')
def test_create_playlist(plex, account=None):
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)
log(4, 'Duration: %s min' % int(playlist.duration / 60000.0))
assert playlist.title == title, 'Playlist not created successfully.'
assert len(items) == 3, 'Playlist does not contain 3 items.'
assert items[0].ratingKey == episodes[0].ratingKey, 'Items not in proper order [0a].'
assert items[1].ratingKey == episodes[1].ratingKey, 'Items not in proper order [1a].'
assert items[2].ratingKey == episodes[2].ratingKey, 'Items not in proper order [2a].'
# move items around (b)
log(2, 'Testing move items..')
playlist.moveItem(items[1])
items = playlist.items()
assert items[0].ratingKey == episodes[1].ratingKey, 'Items not in proper order [0b].'
assert items[1].ratingKey == episodes[0].ratingKey, 'Items not in proper order [1b].'
assert items[2].ratingKey == episodes[2].ratingKey, 'Items not in proper order [2b].'
# move items around (c)
playlist.moveItem(items[0], items[1])
items = playlist.items()
assert items[0].ratingKey == episodes[0].ratingKey, 'Items not in proper order [0c].'
assert items[1].ratingKey == episodes[1].ratingKey, 'Items not in proper order [1c].'
assert items[2].ratingKey == episodes[2].ratingKey, 'Items not in proper order [2c].'
# add an item
log(2, 'Testing add item: %s' % episodes[3])
playlist.addItems(episodes[3])
items = playlist.items()
log(4, '4th Item: %s' % items[3])
assert items[3].ratingKey == episodes[3].ratingKey, 'Missing added item: %s' % episodes[3]
# add two items
log(2, 'Testing add item: %s' % episodes[4:6])
playlist.addItems(episodes[4:6])
items = playlist.items()
log(4, '5th+ Items: %s' % items[4:])
assert items[4].ratingKey == episodes[4].ratingKey, 'Missing added item: %s' % episodes[4]
assert items[5].ratingKey == episodes[5].ratingKey, 'Missing added item: %s' % episodes[5]
assert len(items) == 6, 'Playlist should have 6 items, %s found' % len(items)
# remove item
toremove = items[3]
log(2, 'Testing remove item: %s' % toremove)
playlist.removeItem(toremove)
items = playlist.items()
assert toremove not in items, 'Removed item still in playlist: %s' % items[3]
assert len(items) == 5, 'Playlist should have 5 items, %s found' % len(items)
finally:
playlist.delete()
#----------------------- #-----------------------
# Metadata # Metadata
#----------------------- #-----------------------