python-plexapi/plexapi/playlist.py
Andrey Yantsen 68fc970d7a Improvements in tests process (#297)
* lets begin

* skip plexpass tests if there is not plexpass on account

* test new myplex attrubutes

* bootstrap: proper photos organisation

* fix rest of photos tests

* fix myplex new attributes test

* fix music bootstrap by setting agent to lastfm

* fix sync tests

* increase bootstrap timeout

* remove timeout from .travis.yml

* do not create playlist-style photoalbums in plex-bootstraptest.py

* allow negative filtering in LibrarySection.search()

* fix sync tests once again

* use sendCrashReports in test_settings

* fix test_settings

* fix test_video

* do not accept eula in bootstrap

* fix PlexServer.isLatest()

* add test against old version of PlexServer

* fix MyPlexAccount.OutOut

* add flag for one-time testing in Travis

* fix test_library onDeck tests

* fix more tests

* use tqdm in plex-bootstraptest for media scanning progress

* create sections one-by-one

* update docs on AlertListener for timeline entries

* fix plex-bootstraptest for server version 1.3.2

* display skip/xpass/xfail reasons

* fix tests on 1.3

* wait for music to be fully processed in plex-bootstraptest

* fix misplaced TEST_ACCOUNT_ONCE

* fix test_myplex_users, not sure if in proper-way

* add pytest-rerunfailures; mark test_myplex_optout as flaky

* fix comment

* Revert "add pytest-rerunfailures; mark test_myplex_optout as flaky"

This reverts commit 580e4c95a7.

* restart plex container on failure

* add conftest.wait_until() and used where some retries are required

* add more wait_until() usage in test_sync

* fix managed user search

* fix updating managed users in myplex

* allow to add new servers to existent users

* add new server to a shared user while bootstrapping

* add some docs on testing process

* perform few attemps when unable to get the claim token

* unlock websocket-client in requirements_dev

* fix docblock in tools/plex-teardowntest

* do not hardcode mediapart size in test_video

* remove cache:pip from travis

* Revert "unlock websocket-client in requirements_dev"

This reverts commit 0d536bd06d.

* remove debug from server.py

* improve webhook tests

* fix type() check to isinstance()

* remove excessive `else` branch due to Hellowlol advice

* add `unknown` as allowed `myPlexMappingState` in test_server
2018-09-14 20:03:23 +02:00

214 lines
9 KiB
Python

# -*- coding: utf-8 -*-
from plexapi import utils
from plexapi.base import PlexPartialObject, Playable
from plexapi.exceptions import BadRequest, Unsupported
from plexapi.playqueue import PlayQueue
from plexapi.utils import cast, toDatetime
from plexapi.compat import quote_plus
@utils.registerPlexObject
class Playlist(PlexPartialObject, Playable):
""" Represents a single Playlist object.
# TODO: Document attributes
"""
TAG = 'Playlist'
TYPE = 'playlist'
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
Playable._loadData(self, data)
self.addedAt = toDatetime(data.attrib.get('addedAt'))
self.composite = data.attrib.get('composite') # url to thumbnail
self.duration = cast(int, data.attrib.get('duration'))
self.durationInSeconds = cast(int, data.attrib.get('durationInSeconds'))
self.guid = data.attrib.get('guid')
self.key = data.attrib.get('key')
self.key = self.key.replace('/items', '') if self.key else self.key # FIX_BUG_50
self.leafCount = cast(int, data.attrib.get('leafCount'))
self.playlistType = data.attrib.get('playlistType')
self.ratingKey = cast(int, data.attrib.get('ratingKey'))
self.smart = cast(bool, data.attrib.get('smart'))
self.summary = data.attrib.get('summary')
self.title = data.attrib.get('title')
self.type = data.attrib.get('type')
self.updatedAt = toDatetime(data.attrib.get('updatedAt'))
self.allowSync = cast(bool, data.attrib.get('allowSync'))
self._items = None # cache for self.items
def __len__(self): # pragma: no cover
return len(self.items())
@property
def metadataType(self):
if self.isVideo:
return 'movie'
elif self.isAudio:
return 'track'
elif self.isPhoto:
return 'photo'
else:
raise Unsupported('Unexpected playlist type')
@property
def isVideo(self):
return self.playlistType == 'video'
@property
def isAudio(self):
return self.playlistType == 'audio'
@property
def isPhoto(self):
return self.playlistType == 'photo'
def __contains__(self, other): # pragma: no cover
return any(i.key == other.key for i in self.items())
def __getitem__(self, key): # pragma: no cover
return self.items()[key]
def items(self):
""" Returns a list of all items in the playlist. """
if self._items is None:
key = '%s/items' % self.key
items = self.fetchItems(key)
self._items = items
return self._items
def addItems(self, items):
""" Add items to a playlist. """
if not isinstance(items, (list, tuple)):
items = [items]
ratingKeys = []
for item in items:
if item.listType != self.playlistType: # pragma: no cover
raise BadRequest('Can not mix media types when building a playlist: %s and %s' %
(self.playlistType, item.listType))
ratingKeys.append(str(item.ratingKey))
uuid = items[0].section().uuid
ratingKeys = ','.join(ratingKeys)
key = '%s/items%s' % (self.key, utils.joinArgs({
'uri': 'library://%s/directory//library/metadata/%s' % (uuid, ratingKeys)
}))
result = self._server.query(key, method=self._server._session.put)
self.reload()
return result
def removeItem(self, item):
""" Remove a file from a playlist. """
key = '%s/items/%s' % (self.key, item.playlistItemID)
result = self._server.query(key, method=self._server._session.delete)
self.reload()
return result
def moveItem(self, item, after=None):
""" Move a to a new position in playlist. """
key = '%s/items/%s/move' % (self.key, item.playlistItemID)
if after:
key += '?after=%s' % after.playlistItemID
result = self._server.query(key, method=self._server._session.put)
self.reload()
return result
def edit(self, title=None, summary=None):
""" Edit playlist. """
key = '/library/metadata/%s%s' % (self.ratingKey, utils.joinArgs({'title': title, 'summary': summary}))
result = self._server.query(key, method=self._server._session.put)
self.reload()
return result
def delete(self):
""" Delete playlist. """
return self._server.query(self.key, method=self._server._session.delete)
def playQueue(self, *args, **kwargs):
""" Create a playqueue from this playlist. """
return PlayQueue.create(self._server, self, *args, **kwargs)
@classmethod
def create(cls, server, title, items):
""" Create a playlist. """
if not isinstance(items, (list, tuple)):
items = [items]
ratingKeys = []
for item in items:
if item.listType != items[0].listType: # pragma: no cover
raise BadRequest('Can not mix media types when building a playlist')
ratingKeys.append(str(item.ratingKey))
ratingKeys = ','.join(ratingKeys)
uuid = items[0].section().uuid
key = '/playlists%s' % utils.joinArgs({
'uri': 'library://%s/directory//library/metadata/%s' % (uuid, ratingKeys),
'type': items[0].listType,
'title': title,
'smart': 0
})
data = server.query(key, method=server._session.post)[0]
return cls(server, data, initpath=key)
def copyToUser(self, user):
""" Copy playlist to another user account. """
from plexapi.server import PlexServer
myplex = self._server.myPlexAccount()
user = myplex.user(user)
# Get the token for your machine.
token = user.get_token(self._server.machineIdentifier)
# Login to your server using your friends credentials.
user_server = PlexServer(self._server._baseurl, token)
return self.create(user_server, self.title, self.items())
def sync(self, videoQuality=None, photoResolution=None, audioBitrate=None, client=None, clientId=None, limit=None,
unwatched=False, title=None):
""" Add current playlist as sync item for specified device.
See :func:`plexapi.myplex.MyPlexAccount.sync()` for possible exceptions.
Parameters:
videoQuality (int): idx of quality of the video, one of VIDEO_QUALITY_* values defined in
:mod:`plexapi.sync` module. Used only when playlist contains video.
photoResolution (str): maximum allowed resolution for synchronized photos, see PHOTO_QUALITY_* values in
the module :mod:`plexapi.sync`. Used only when playlist contains photos.
audioBitrate (int): maximum bitrate for synchronized music, better use one of MUSIC_BITRATE_* values
from the module :mod:`plexapi.sync`. Used only when playlist contains audio.
client (:class:`plexapi.myplex.MyPlexDevice`): sync destination, see
:func:`plexapi.myplex.MyPlexAccount.sync`.
clientId (str): sync destination, see :func:`plexapi.myplex.MyPlexAccount.sync`.
limit (int): maximum count of items to sync, unlimited if `None`.
unwatched (bool): if `True` watched videos wouldn't be synced.
title (str): descriptive title for the new :class:`plexapi.sync.SyncItem`, if empty the value would be
generated from metadata of current photo.
Raises:
:class:`plexapi.exceptions.BadRequest`: when playlist is not allowed to sync.
:class:`plexapi.exceptions.Unsupported`: when playlist content is unsupported.
Returns:
:class:`plexapi.sync.SyncItem`: an instance of created syncItem.
"""
if not self.allowSync:
raise BadRequest('The playlist is not allowed to sync')
from plexapi.sync import SyncItem, Policy, MediaSettings
myplex = self._server.myPlexAccount()
sync_item = SyncItem(self._server, None)
sync_item.title = title if title else self.title
sync_item.rootTitle = self.title
sync_item.contentType = self.playlistType
sync_item.metadataType = self.metadataType
sync_item.machineIdentifier = self._server.machineIdentifier
sync_item.location = 'playlist:///%s' % quote_plus(self.guid)
sync_item.policy = Policy.create(limit, unwatched)
if self.isVideo:
sync_item.mediaSettings = MediaSettings.createVideo(videoQuality)
elif self.isAudio:
sync_item.mediaSettings = MediaSettings.createMusic(audioBitrate)
elif self.isPhoto:
sync_item.mediaSettings = MediaSettings.createPhoto(photoResolution)
else:
raise Unsupported('Unsupported playlist content')
return myplex.sync(sync_item, client=client, clientId=clientId)