mirror of
https://github.com/pkkid/python-plexapi
synced 2024-11-22 11:43:13 +00:00
Added playlist support
This commit is contained in:
parent
09a7ae80db
commit
3138ad1087
8 changed files with 147 additions and 1 deletions
|
@ -8,6 +8,7 @@ NA = utils.NA
|
|||
|
||||
|
||||
class Audio(PlexPartialObject):
|
||||
TYPE = None
|
||||
|
||||
def __init__(self, server, data, initpath):
|
||||
super(Audio, self).__init__(data, initpath, server)
|
||||
|
@ -38,6 +39,7 @@ class Audio(PlexPartialObject):
|
|||
@utils.register_libtype
|
||||
class Artist(Audio):
|
||||
TYPE = 'artist'
|
||||
LISTTYPE = 'audio'
|
||||
|
||||
def _loadData(self, data):
|
||||
Audio._loadData(self, data)
|
||||
|
@ -73,6 +75,7 @@ class Artist(Audio):
|
|||
@utils.register_libtype
|
||||
class Album(Audio):
|
||||
TYPE = 'album'
|
||||
LISTTYPE = 'audio'
|
||||
|
||||
def _loadData(self, data):
|
||||
Audio._loadData(self, data)
|
||||
|
@ -112,6 +115,7 @@ class Album(Audio):
|
|||
@utils.register_libtype
|
||||
class Track(Audio, Playable):
|
||||
TYPE = 'track'
|
||||
LISTTYPE = 'audio'
|
||||
|
||||
def _loadData(self, data):
|
||||
Audio._loadData(self, data)
|
||||
|
|
|
@ -10,6 +10,7 @@ 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)
|
||||
|
@ -41,6 +42,7 @@ class Photoalbum(PlexPartialObject):
|
|||
@utils.register_libtype
|
||||
class Photo(PlexPartialObject):
|
||||
TYPE = 'photo'
|
||||
LISTTYPE = 'photo'
|
||||
|
||||
def __init__(self, server, data, initpath):
|
||||
super(Photo, self).__init__(data, initpath, server)
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
"""
|
||||
PlexPlaylist
|
||||
"""
|
||||
import requests
|
||||
from plexapi import utils
|
||||
from plexapi.exceptions import BadRequest
|
||||
from plexapi.utils import cast, toDatetime
|
||||
from plexapi.utils import PlexPartialObject, Playable
|
||||
NA = utils.NA
|
||||
|
@ -23,7 +25,7 @@ class Playlist(PlexPartialObject, Playable):
|
|||
self.durationInSeconds = cast(int, data.attrib.get('durationInSeconds', NA))
|
||||
self.guid = data.attrib.get('guid', 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.playlistType = data.attrib.get('playlistType', NA)
|
||||
self.ratingKey = data.attrib.get('ratingKey', NA)
|
||||
|
@ -36,3 +38,62 @@ class Playlist(PlexPartialObject, Playable):
|
|||
def items(self):
|
||||
path = '%s/items' % self.key
|
||||
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)
|
||||
|
|
|
@ -23,6 +23,7 @@ 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.
|
||||
path = '/playQueues%s' % utils.joinArgs({
|
||||
|
|
|
@ -11,6 +11,7 @@ from plexapi.compat import quote
|
|||
from plexapi.client import PlexClient
|
||||
from plexapi.exceptions import BadRequest, NotFound
|
||||
from plexapi.library import Library
|
||||
from plexapi.playlist import Playlist
|
||||
from plexapi.playqueue import PlayQueue
|
||||
from xml.etree import ElementTree
|
||||
|
||||
|
@ -69,6 +70,9 @@ class PlexServer(object):
|
|||
return PlexClient(baseurl, server=self, data=elem)
|
||||
raise NotFound('Unknown client name: %s' % name)
|
||||
|
||||
def createPlaylist(self, title, items):
|
||||
return Playlist.create(self, title, items)
|
||||
|
||||
def createPlayQueue(self, item):
|
||||
return PlayQueue.create(self, item)
|
||||
|
||||
|
@ -82,6 +86,8 @@ class PlexServer(object):
|
|||
return utils.listItems(self, '/status/sessions/history/all')
|
||||
|
||||
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')
|
||||
|
||||
def playlist(self, title=None): # noqa
|
||||
|
|
|
@ -92,6 +92,8 @@ class Playable(object):
|
|||
self.transcodeSession = findTranscodeSession(self.server, data)
|
||||
# data for history details (/status/sessions/history/all)
|
||||
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):
|
||||
if self.TYPE not in ('movie', 'episode', 'track'):
|
||||
|
|
|
@ -55,6 +55,7 @@ class Video(PlexPartialObject):
|
|||
@utils.register_libtype
|
||||
class Movie(Video, Playable):
|
||||
TYPE = 'movie'
|
||||
LISTTYPE = 'video'
|
||||
|
||||
def _loadData(self, data):
|
||||
Video._loadData(self, data)
|
||||
|
@ -101,6 +102,7 @@ class Movie(Video, Playable):
|
|||
@utils.register_libtype
|
||||
class Show(Video):
|
||||
TYPE = 'show'
|
||||
LISTTYPE = 'video'
|
||||
|
||||
def _loadData(self, data):
|
||||
Video._loadData(self, data)
|
||||
|
@ -162,6 +164,7 @@ class Show(Video):
|
|||
@utils.register_libtype
|
||||
class Season(Video):
|
||||
TYPE = 'season'
|
||||
LISTTYPE = 'video'
|
||||
|
||||
def _loadData(self, data):
|
||||
Video._loadData(self, data)
|
||||
|
@ -198,6 +201,7 @@ class Season(Video):
|
|||
@utils.register_libtype
|
||||
class Episode(Video, Playable):
|
||||
TYPE = 'episode'
|
||||
LISTTYPE = 'video'
|
||||
|
||||
def _loadData(self, data):
|
||||
Video._loadData(self, data)
|
||||
|
|
|
@ -271,6 +271,72 @@ def test_refresh_video(plex, account=None):
|
|||
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
|
||||
#-----------------------
|
||||
|
|
Loading…
Reference in a new issue