Merge branch 'master' into library_hubs

This commit is contained in:
blacktwin 2020-09-28 08:13:59 -04:00 committed by GitHub
commit d5f9004e7b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 564 additions and 105 deletions

View file

@ -15,7 +15,7 @@ CONFIG = PlexConfig(CONFIG_PATH)
# PlexAPI Settings
PROJECT = 'PlexAPI'
VERSION = '4.0.0'
VERSION = '4.1.1'
TIMEOUT = CONFIG.get('plexapi.timeout', 30, int)
X_PLEX_CONTAINER_SIZE = CONFIG.get('plexapi.container_size', 100, int)
X_PLEX_ENABLE_FAST_CONNECT = CONFIG.get('plexapi.enable_fast_connect', False, bool)

View file

@ -213,7 +213,7 @@ class Album(Audio):
TYPE = 'album'
def __iter__(self):
for track in self.tracks:
for track in self.tracks():
yield track
def _loadData(self, data):

View file

@ -598,6 +598,7 @@ class Playable(object):
if item is being transcoded (None otherwise).
viewedAt (datetime): Datetime item was last viewed (history).
playlistItemID (int): Playlist item ID (only populated for :class:`~plexapi.playlist.Playlist` items).
playQueueItemID (int): PlayQueue item ID (only populated for :class:`~plexapi.playlist.PlayQueue` items).
"""
def _loadData(self, data):
@ -609,6 +610,7 @@ class Playable(object):
self.viewedAt = utils.toDatetime(data.attrib.get('viewedAt')) # history
self.accountID = utils.cast(int, data.attrib.get('accountID')) # history
self.playlistItemID = utils.cast(int, data.attrib.get('playlistItemID')) # playlist
self.playQueueItemID = utils.cast(int, data.attrib.get('playQueueItemID')) # playqueue
def isFullObject(self):
""" Retruns True if this is already a full object. A full object means all attributes

View file

@ -506,6 +506,12 @@ class LibrarySection(PlexObject):
data = self._server.query(key)
return self.findItems(data, cls=Setting)
def timeline(self):
""" Returns a timeline query for this library section. """
key = '/library/sections/%s/timeline' % self.key
data = self._server.query(key)
return LibraryTimeline(self, data)
def onDeck(self):
""" Returns a list of media items on deck from this library section. """
key = '/library/sections/%s/onDeck' % self.key
@ -1072,6 +1078,46 @@ class FilterChoice(PlexObject):
self.type = data.attrib.get('type')
@utils.registerPlexObject
class LibraryTimeline(PlexObject):
"""Represents a LibrarySection timeline.
Attributes:
TAG (str): 'LibraryTimeline'
size (int): Unknown
allowSync (bool): Unknown
art (str): Relative path to art image.
content (str): "secondary"
identifier (str): "com.plexapp.plugins.library"
latestEntryTime (int): Epoch timestamp
mediaTagPrefix (str): "/system/bundle/media/flags/"
mediaTagVersion (int): Unknown
thumb (str): Relative path to library thumb image.
title1 (str): Name of library section.
updateQueueSize (int): Number of items queued to update.
viewGroup (str): "secondary"
viewMode (int): Unknown
"""
TAG = 'LibraryTimeline'
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
self._data = data
self.size = utils.cast(int, data.attrib.get('size'))
self.allowSync = utils.cast(bool, data.attrib.get('allowSync'))
self.art = data.attrib.get('art')
self.content = data.attrib.get('content')
self.identifier = data.attrib.get('identifier')
self.latestEntryTime = utils.cast(int, data.attrib.get('latestEntryTime'))
self.mediaTagPrefix = data.attrib.get('mediaTagPrefix')
self.mediaTagVersion = utils.cast(int, data.attrib.get('mediaTagVersion'))
self.thumb = data.attrib.get('thumb')
self.title1 = data.attrib.get('title1')
self.updateQueueSize = utils.cast(int, data.attrib.get('updateQueueSize'))
self.viewGroup = data.attrib.get('viewGroup')
self.viewMode = utils.cast(int, data.attrib.get('viewMode'))
@utils.registerPlexObject
class Location(PlexObject):
""" Represents a single library Location.

View file

@ -41,6 +41,10 @@ class Playlist(PlexPartialObject, Playable):
def __len__(self): # pragma: no cover
return len(self.items())
def __iter__(self): # pragma: no cover
for item in self.items():
yield item
@property
def metadataType(self):
if self.isVideo:

View file

@ -1,75 +1,244 @@
# -*- coding: utf-8 -*-
from urllib.parse import quote_plus
from plexapi import utils
from plexapi.base import PlexObject
from plexapi.exceptions import BadRequest, Unsupported
class PlayQueue(PlexObject):
""" Control a PlayQueue.
"""Control a PlayQueue.
Attributes:
key (str): This is only added to support playMedia
identifier (str): com.plexapp.plugins.library
initpath (str): Relative url where data was grabbed from.
items (list): List of :class:`~plexapi.media.Media` or class:`~plexapi.playlist.Playlist`
mediaTagPrefix (str): Fx /system/bundle/media/flags/
mediaTagVersion (str): Fx 1485957738
playQueueID (str): a id for the playqueue
playQueueSelectedItemID (str): playQueueSelectedItemID
playQueueSelectedItemOffset (str): playQueueSelectedItemOffset
playQueueSelectedMetadataItemID (<type 'str'>): 7
playQueueShuffled (bool): True if shuffled
playQueueSourceURI (str): Fx library://150425c9-0d99-4242-821e-e5ab81cd2221/item//library/metadata/7
playQueueTotalCount (str): How many items in the play queue.
playQueueVersion (str): What version the playqueue is.
server (:class:`~plexapi.server.PlexServer`): Server you are connected to.
size (str): Seems to be a alias for playQueueTotalCount.
Attributes:
TAG (str): 'PlayQueue'
TYPE (str): 'playqueue'
identifier (str): com.plexapp.plugins.library
items (list): List of :class:`~plexapi.media.Media` or :class:`~plexapi.playlist.Playlist`
mediaTagPrefix (str): Fx /system/bundle/media/flags/
mediaTagVersion (int): Fx 1485957738
playQueueID (int): ID of the PlayQueue.
playQueueLastAddedItemID (int):
Defines where the "Up Next" region starts. Empty unless PlayQueue is modified after creation.
playQueueSelectedItemID (int): The queue item ID of the currently selected item.
playQueueSelectedItemOffset (int):
The offset of the selected item in the PlayQueue, from the beginning of the queue.
playQueueSelectedMetadataItemID (int): ID of the currently selected item, matches ratingKey.
playQueueShuffled (bool): True if shuffled.
playQueueSourceURI (str): Original URI used to create the PlayQueue.
playQueueTotalCount (int): How many items in the PlayQueue.
playQueueVersion (int): Version of the PlayQueue. Increments every time a change is made to the PlayQueue.
selectedItem (:class:`~plexapi.media.Media`): Media object for the currently selected item.
_server (:class:`~plexapi.server.PlexServer`): PlexServer associated with the PlayQueue.
size (int): Alias for playQueueTotalCount.
"""
TAG = "PlayQueue"
TYPE = "playqueue"
def _loadData(self, data):
self._data = data
self.identifier = data.attrib.get('identifier')
self.mediaTagPrefix = data.attrib.get('mediaTagPrefix')
self.mediaTagVersion = data.attrib.get('mediaTagVersion')
self.playQueueID = data.attrib.get('playQueueID')
self.playQueueSelectedItemID = data.attrib.get('playQueueSelectedItemID')
self.playQueueSelectedItemOffset = data.attrib.get('playQueueSelectedItemOffset')
self.playQueueSelectedMetadataItemID = data.attrib.get('playQueueSelectedMetadataItemID')
self.playQueueShuffled = utils.cast(bool, data.attrib.get('playQueueShuffled', 0))
self.playQueueSourceURI = data.attrib.get('playQueueSourceURI')
self.playQueueTotalCount = data.attrib.get('playQueueTotalCount')
self.playQueueVersion = data.attrib.get('playQueueVersion')
self.size = utils.cast(int, data.attrib.get('size', 0))
self.identifier = data.attrib.get("identifier")
self.mediaTagPrefix = data.attrib.get("mediaTagPrefix")
self.mediaTagVersion = utils.cast(int, data.attrib.get("mediaTagVersion"))
self.playQueueID = utils.cast(int, data.attrib.get("playQueueID"))
self.playQueueLastAddedItemID = utils.cast(
int, data.attrib.get("playQueueLastAddedItemID")
)
self.playQueueSelectedItemID = utils.cast(
int, data.attrib.get("playQueueSelectedItemID")
)
self.playQueueSelectedItemOffset = utils.cast(
int, data.attrib.get("playQueueSelectedItemOffset")
)
self.playQueueSelectedMetadataItemID = utils.cast(
int, data.attrib.get("playQueueSelectedMetadataItemID")
)
self.playQueueShuffled = utils.cast(
bool, data.attrib.get("playQueueShuffled", 0)
)
self.playQueueSourceURI = data.attrib.get("playQueueSourceURI")
self.playQueueTotalCount = utils.cast(
int, data.attrib.get("playQueueTotalCount")
)
self.playQueueVersion = utils.cast(int, data.attrib.get("playQueueVersion"))
self.size = utils.cast(int, data.attrib.get("size", 0))
self.items = self.findItems(data)
self.selectedItem = self[self.playQueueSelectedItemOffset]
def __getitem__(self, key):
return self.items[key]
def __len__(self):
return self.playQueueTotalCount
def __iter__(self):
yield from self.items
def __contains__(self, media):
"""Returns True if the PlayQueue contains the provided media item."""
return any(x.playQueueItemID == media.playQueueItemID for x in self.items)
def getQueueItem(self, item):
"""
Accepts a media item and returns a similar object from this PlayQueue.
Useful for looking up playQueueItemIDs using items obtained from the Library.
"""
matches = [x for x in self.items if x == item]
if len(matches) == 1:
return matches[0]
elif len(matches) > 1:
raise BadRequest(
f"{item} occurs multiple times in this PlayQueue, provide exact item"
)
else:
raise BadRequest(f"{item} not valid for this PlayQueue")
@classmethod
def create(cls, server, item, shuffle=0, repeat=0, includeChapters=1, includeRelated=1):
""" Create and returns a new :class:`~plexapi.playqueue.PlayQueue`.
def create(
cls,
server,
items,
startItem=None,
shuffle=0,
repeat=0,
includeChapters=1,
includeRelated=1,
continuous=0,
):
"""Create and return a new :class:`~plexapi.playqueue.PlayQueue`.
Paramaters:
server (:class:`~plexapi.server.PlexServer`): Server you are connected to.
item (:class:`~plexapi.media.Media` or class:`~plexapi.playlist.Playlist`): A media or Playlist.
shuffle (int, optional): Start the playqueue shuffled.
repeat (int, optional): Start the playqueue shuffled.
includeChapters (int, optional): include Chapters.
includeRelated (int, optional): include Related.
Parameters:
server (:class:`~plexapi.server.PlexServer`): Server you are connected to.
items (:class:`~plexapi.media.Media` or :class:`~plexapi.playlist.Playlist`):
A media item, list of media items, or Playlist.
startItem (:class:`~plexapi.media.Media`, optional):
Media item in the PlayQueue where playback should begin.
shuffle (int, optional): Start the playqueue shuffled.
repeat (int, optional): Start the playqueue shuffled.
includeChapters (int, optional): include Chapters.
includeRelated (int, optional): include Related.
continuous (int, optional): include additional items after the initial item.
For a show this would be the next episodes, for a movie it does nothing.
"""
args = {}
args['includeChapters'] = includeChapters
args['includeRelated'] = includeRelated
args['repeat'] = repeat
args['shuffle'] = shuffle
if item.type == 'playlist':
args['playlistID'] = item.ratingKey
args['type'] = item.playlistType
args = {
"includeChapters": includeChapters,
"includeRelated": includeRelated,
"repeat": repeat,
"shuffle": shuffle,
"continuous": continuous,
}
if isinstance(items, list):
item_keys = ",".join([str(x.ratingKey) for x in items])
uri_args = quote_plus(f"/library/metadata/{item_keys}")
args["uri"] = f"library:///directory/{uri_args}"
args["type"] = items[0].listType
elif items.type == "playlist":
args["playlistID"] = items.ratingKey
args["type"] = items.playlistType
else:
uuid = item.section().uuid
args['key'] = item.key
args['type'] = item.listType
args['uri'] = 'library://%s/item/%s' % (uuid, item.key)
path = '/playQueues%s' % utils.joinArgs(args)
uuid = items.section().uuid
args["type"] = items.listType
args["uri"] = f"library://{uuid}/item/{items.key}"
if startItem:
args["key"] = startItem.key
path = f"/playQueues{utils.joinArgs(args)}"
data = server.query(path, method=server._session.post)
c = cls(server, data, initpath=path)
# we manually add a key so we can pass this to playMedia
# since the data, does not contain a key.
c.key = item.key
c.playQueueType = args["type"]
c._server = server
return c
def addItem(self, item, playNext=False, refresh=True):
"""
Append the provided item to the "Up Next" section of the PlayQueue.
Items can only be added to the section immediately following the current playing item.
Parameters:
item (:class:`~plexapi.media.Media` or :class:`~plexapi.playlist.Playlist`): Single media item or Playlist.
playNext (bool, optional): If True, add this item to the front of the "Up Next" section.
If False, the item will be appended to the end of the "Up Next" section.
Only has an effect if an item has already been added to the "Up Next" section.
See https://support.plex.tv/articles/202188298-play-queues/ for more details.
refresh (bool, optional): Refresh the PlayQueue from the server before updating.
"""
if refresh:
self.refresh()
args = {}
if item.type == "playlist":
args["playlistID"] = item.ratingKey
itemType = item.playlistType
else:
uuid = item.section().uuid
itemType = item.listType
args["uri"] = f"library://{uuid}/item{item.key}"
if itemType != self.playQueueType:
raise Unsupported("Item type does not match PlayQueue type")
if playNext:
args["next"] = 1
path = f"/playQueues/{self.playQueueID}{utils.joinArgs(args)}"
data = self._server.query(path, method=self._server._session.put)
self._loadData(data)
def moveItem(self, item, after=None, refresh=True):
"""
Moves an item to the beginning of the PlayQueue. If `after` is provided,
the item will be placed immediately after the specified item.
Parameters:
item (:class:`~plexapi.base.Playable`): An existing item in the PlayQueue to move.
afterItemID (:class:`~plexapi.base.Playable`, optional): A different item in the PlayQueue.
If provided, `item` will be placed in the PlayQueue after this item.
refresh (bool, optional): Refresh the PlayQueue from the server before updating.
"""
args = {}
if refresh:
self.refresh()
if item not in self:
item = self.getQueueItem(item)
if after:
if after not in self:
after = self.getQueueItem(after)
args["after"] = after.playQueueItemID
path = f"/playQueues/{self.playQueueID}/items/{item.playQueueItemID}/move{utils.joinArgs(args)}"
data = self._server.query(path, method=self._server._session.put)
self._loadData(data)
def removeItem(self, item, refresh=True):
"""Remove an item from the PlayQueue.
Parameters:
item (:class:`~plexapi.base.Playable`): An existing item in the PlayQueue to move.
refresh (bool, optional): Refresh the PlayQueue from the server before updating.
"""
if refresh:
self.refresh()
if item not in self:
item = self.getQueueItem(item)
path = f"/playQueues/{self.playQueueID}/items/{item.playQueueItemID}"
data = self._server.query(path, method=self._server._session.delete)
self._loadData(data)
def clear(self):
"""Remove all items from the PlayQueue."""
path = f"/playQueues/{self.playQueueID}/items"
data = self._server.query(path, method=self._server._session.delete)
self._loadData(data)
def refresh(self):
"""Refresh the PlayQueue from the Plex server."""
path = f"/playQueues/{self.playQueueID}"
data = self._server.query(path, method=self._server._session.get)
self._loadData(data)

View file

@ -190,6 +190,14 @@ class PlexServer(PlexObject):
data = self.query(Account.key)
return Account(self, data)
@property
def activities(self):
"""Returns all current PMS activities."""
activities = []
for elem in self.query(Activity.key):
activities.append(Activity(self, elem))
return activities
def agents(self, mediaType=None):
""" Returns the `:class:`~plexapi.media.Agent` objects this server has available. """
key = '/system/agents'
@ -284,7 +292,7 @@ class PlexServer(PlexObject):
Parameters:
item (Media or Playlist): Media or playlist to add to PlayQueue.
kwargs (dict): See `~plexapi.playerque.PlayQueue.create`.
kwargs (dict): See `~plexapi.playqueue.PlayQueue.create`.
"""
return PlayQueue.create(self, item, **kwargs)
@ -601,6 +609,20 @@ class Account(PlexObject):
self.subscriptionState = data.attrib.get('subscriptionState')
class Activity(PlexObject):
"""A currently running activity on the PlexServer."""
key = '/activities'
def _loadData(self, data):
self._data = data
self.cancellable = cast(bool, data.attrib.get('cancellable'))
self.progress = cast(int, data.attrib.get('progress'))
self.title = data.attrib.get('title')
self.subtitle = data.attrib.get('subtitle')
self.type = data.attrib.get('type')
self.uuid = data.attrib.get('uuid')
class SystemAccount(PlexObject):
""" Minimal api to list system accounts. """
key = '/accounts'

View file

@ -4,7 +4,7 @@ import os
import re
import time
import zipfile
from datetime import datetime, timedelta
from datetime import datetime
from getpass import getpass
from threading import Event, Thread
from urllib.parse import quote
@ -212,7 +212,7 @@ def millisecondToHumanstr(milliseconds):
milliseconds (str,int): time duration in milliseconds.
"""
milliseconds = int(milliseconds)
r = datetime.datetime.utcfromtimestamp(milliseconds / 1000)
r = datetime.utcfromtimestamp(milliseconds / 1000)
f = r.strftime("%H:%M:%S.%f")
return f[:-2]

View file

@ -10,7 +10,7 @@ from plexapi.exceptions import BadRequest, NotFound
class Video(PlexPartialObject):
""" Base class for all video objects including :class:`~plexapi.video.Movie`,
:class:`~plexapi.video.Show`, :class:`~plexapi.video.Season`,
:class:`~plexapi.video.Episode`.
:class:`~plexapi.video.Episode`, :class:`~plexapi.video.Clip`.
Attributes:
addedAt (datetime): Datetime this item was added to the library.
@ -774,24 +774,38 @@ class Episode(Playable, Video):
@utils.registerPlexObject
class Clip(Playable, Video):
""" Represents a single Clip."""
"""Represents a single Clip.
Attributes:
TAG (str): 'Video'
TYPE (str): 'clip'
duration (int): Duration of movie in milliseconds.
extraType (int): Unknown
guid: Plex GUID (com.plexapp.agents.imdb://tt4302938?lang=en).
index (int): Plex index (?)
originallyAvailableAt (datetime): Datetime movie was released.
subtype (str): Type of clip
viewOffset (int): View offset in milliseconds.
"""
TAG = 'Video'
TYPE = 'clip'
METADATA_TYPE = 'clip'
def _loadData(self, data):
self._data = data
self.addedAt = data.attrib.get('addedAt')
self.duration = data.attrib.get('duration')
"""Load attribute values from Plex XML response."""
Video._loadData(self, data)
Playable._loadData(self, data)
self.duration = utils.cast(int, data.attrib.get('duration'))
self.extraType = utils.cast(int, data.attrib.get('extraType'))
self.guid = data.attrib.get('guid')
self.key = data.attrib.get('key')
self.index = utils.cast(int, data.attrib.get('index'))
self.originallyAvailableAt = data.attrib.get('originallyAvailableAt')
self.ratingKey = data.attrib.get('ratingKey')
self.skipDetails = utils.cast(int, data.attrib.get('skipDetails'))
self.subtype = data.attrib.get('subtype')
self.thumb = data.attrib.get('thumb')
self.thumbAspectRatio = data.attrib.get('thumbAspectRatio')
self.title = data.attrib.get('title')
self.type = data.attrib.get('type')
self.year = data.attrib.get('year')
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
def section(self):
"""Return the :class:`~plexapi.library.LibrarySection` this item belongs to."""
# Clip payloads currently do not contain 'librarySectionID'.
# Return None to avoid unnecessary attribute lookup attempts.
return None

52
tests/test__prepare.py Normal file
View file

@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
import time
import pytest
MAX_ATTEMPTS = 60
def wait_for_idle_server(server):
"""Wait for PMS activities to complete with a timeout."""
attempts = 0
while server.activities and attempts < MAX_ATTEMPTS:
print(f"Waiting for activities to finish: {server.activities}")
time.sleep(1)
attempts += 1
assert attempts < MAX_ATTEMPTS, f"Server still busy after {MAX_ATTEMPTS}s"
def wait_for_metadata_processing(server):
"""Wait for async metadata processing to complete."""
attempts = 0
while True:
busy = False
for section in server.library.sections():
tl = section.timeline()
if tl.updateQueueSize > 0:
busy = True
print(f"{section.title}: {tl.updateQueueSize} items left")
if not busy or attempts > MAX_ATTEMPTS:
break
time.sleep(1)
attempts += 1
assert attempts < MAX_ATTEMPTS, f"Metadata still processing after {MAX_ATTEMPTS}s"
def test_ensure_activities_completed(plex):
wait_for_idle_server(plex)
@pytest.mark.authenticated
def test_ensure_activities_completed_authenticated(plex):
wait_for_idle_server(plex)
def test_ensure_metadata_scans_completed(plex):
wait_for_metadata_processing(plex)
@pytest.mark.authenticated
def test_ensure_metadata_scans_completed_authenticated(plex):
wait_for_metadata_processing(plex)

View file

@ -7,8 +7,9 @@ from . import conftest as utils
def test_audio_Artist_attr(artist):
artist.reload()
assert utils.is_datetime(artist.addedAt)
assert artist.countries == []
assert "Electronic" in [i.tag for i in artist.genres]
if artist.countries:
assert "United States" in [i.tag for i in artist.countries]
#assert "Electronic" in [i.tag for i in artist.genres]
assert utils.is_string(artist.guid, gte=5)
assert artist.index == "1"
assert utils.is_metadata(artist._initpath)
@ -20,7 +21,8 @@ def test_audio_Artist_attr(artist):
assert artist.ratingKey >= 1
assert artist._server._baseurl == utils.SERVER_BASEURL
assert isinstance(artist.similar, list)
assert "Alias" in artist.summary
if artist.summary:
assert "Alias" in artist.summary
assert artist.title == "Broke For Free"
assert artist.titleSort == "Broke For Free"
assert artist.type == "artist"
@ -75,7 +77,7 @@ def test_audio_Album_attrs(album):
assert album.parentTitle == "Broke For Free"
assert album.ratingKey >= 1
assert album._server._baseurl == utils.SERVER_BASEURL
assert album.studio is None
assert album.studio == "[no label]"
assert album.summary == ""
if album.thumb:
assert utils.is_metadata(album.thumb, contains="/thumb/")
@ -113,13 +115,15 @@ def test_audio_Album_tracks(album):
# assert utils.is_int(track.parentIndex)
assert utils.is_metadata(track.parentKey)
assert utils.is_int(track.parentRatingKey)
assert utils.is_metadata(track.parentThumb, contains="/thumb/")
if track.parentThumb:
assert utils.is_metadata(track.parentThumb, contains="/thumb/")
assert track.parentTitle == "Layers"
# assert track.ratingCount == 9 # Flaky
assert utils.is_int(track.ratingKey)
assert track._server._baseurl == utils.SERVER_BASEURL
assert track.summary == ""
assert utils.is_metadata(track.thumb, contains="/thumb/")
if track.thumb:
assert utils.is_metadata(track.thumb, contains="/thumb/")
assert track.title == "As Colourful as Ever"
assert track.titleSort == "As Colourful as Ever"
assert not track.transcodeSessions
@ -148,13 +152,15 @@ def test_audio_Album_track(album, track=None):
assert utils.is_int(track.parentIndex)
assert utils.is_metadata(track.parentKey)
assert utils.is_int(track.parentRatingKey)
assert utils.is_metadata(track.parentThumb, contains="/thumb/")
if track.parentThumb:
assert utils.is_metadata(track.parentThumb, contains="/thumb/")
assert track.parentTitle == "Layers"
# assert track.ratingCount == 9
assert utils.is_int(track.ratingKey)
assert track._server._baseurl == utils.SERVER_BASEURL
assert track.summary == ""
assert utils.is_metadata(track.thumb, contains="/thumb/")
if track.thumb:
assert utils.is_metadata(track.thumb, contains="/thumb/")
assert track.title == "As Colourful as Ever"
assert track.titleSort == "As Colourful as Ever"
assert not track.transcodeSessions
@ -213,7 +219,7 @@ def test_audio_Track_attrs(album):
if track.grandparentThumb:
assert utils.is_metadata(track.grandparentThumb, contains="/thumb/")
assert track.grandparentTitle == "Broke For Free"
assert track.guid.startswith("local://")
assert track.guid.startswith("mbid://") or track.guid.startswith("plex://track/")
assert int(track.index) == 1
assert utils.is_metadata(track._initpath)
assert utils.is_metadata(track.key)
@ -228,7 +234,8 @@ def test_audio_Track_attrs(album):
assert int(track.parentIndex) == 1
assert utils.is_metadata(track.parentKey)
assert utils.is_int(track.parentRatingKey)
assert utils.is_metadata(track.parentThumb, contains="/thumb/")
if track.parentThumb:
assert utils.is_metadata(track.parentThumb, contains="/thumb/")
assert track.parentTitle == "Layers"
assert track.playlistItemID is None
assert track.primaryExtraKey is None
@ -237,7 +244,8 @@ def test_audio_Track_attrs(album):
assert track._server._baseurl == utils.SERVER_BASEURL
assert track.sessionKey is None
assert track.summary == ""
assert utils.is_metadata(track.thumb, contains="/thumb/")
if track.thumb:
assert utils.is_metadata(track.thumb, contains="/thumb/")
assert track.title == "As Colourful as Ever"
assert track.titleSort == "As Colourful as Ever"
assert not track.transcodeSessions

View file

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
from datetime import datetime
import pytest
from plexapi.exceptions import NotFound
@ -277,3 +278,22 @@ def test_crazy_search(plex, movie):
assert len(movies.search(container_size=1)) == 4
assert len(movies.search(container_start=9999, container_size=1)) == 0
assert len(movies.search(container_start=2, container_size=1)) == 2
def test_library_section_timeline(plex):
movies = plex.library.section("Movies")
tl = movies.timeline()
assert tl.TAG == "LibraryTimeline"
assert tl.size > 0
assert tl.allowSync is False
assert tl.art == "/:/resources/movie-fanart.jpg"
assert tl.content == "secondary"
assert tl.identifier == "com.plexapp.plugins.library"
assert datetime.fromtimestamp(tl.latestEntryTime).date() == datetime.today().date()
assert tl.mediaTagPrefix == "/system/bundle/media/flags/"
assert tl.mediaTagVersion > 1
assert tl.thumb == "/:/resources/movie.png"
assert tl.title1 == "Movies"
assert tl.updateQueueSize == 0
assert tl.viewGroup == "secondary"
assert tl.viewMode == 65592

View file

@ -74,16 +74,6 @@ def test_playlist_photos(plex, photoalbum):
assert playlist_name not in [i.title for i in plex.playlists()]
def test_playlist_playQueue(plex, album):
try:
playlist = plex.createPlaylist('test_playlist', album)
playqueue = playlist.playQueue(**dict(shuffle=1))
assert 'shuffle=1' in playqueue._initpath
assert playqueue.playQueueShuffled is True
finally:
playlist.delete()
@pytest.mark.client
def test_play_photos(plex, client, photoalbum):
photos = photoalbum.photos()
@ -92,14 +82,6 @@ def test_play_photos(plex, client, photoalbum):
time.sleep(2)
def test_playqueues(plex):
episode = plex.library.section('TV Shows').get('the 100').get('Pilot')
playqueue = plex.createPlayQueue(episode)
assert len(playqueue.items) == 1, 'No items in play queue.'
assert playqueue.items[0].title == episode.title, 'Wrong show queued.'
assert playqueue.playQueueID, 'Play queue ID not set.'
def test_copyToUser(plex, show, fresh_plex, shared_username):
episodes = show.episodes()
playlist = plex.createPlaylist('shared_from_test_plexapi', episodes)

140
tests/test_playqueue.py Normal file
View file

@ -0,0 +1,140 @@
# -*- coding: utf-8 -*-
from plexapi.exceptions import BadRequest
import pytest
def test_create_playqueue(plex, show):
# create the playlist
episodes = show.episodes()
pq = plex.createPlayQueue(episodes[:3])
assert len(pq) == 3, "PlayQueue does not contain 3 items."
assert pq.playQueueLastAddedItemID is None
assert pq.playQueueSelectedMetadataItemID == episodes[0].ratingKey
assert (
pq.items[0].ratingKey == episodes[0].ratingKey
), "Items not in proper order [0a]."
assert (
pq.items[1].ratingKey == episodes[1].ratingKey
), "Items not in proper order [1a]."
assert (
pq.items[2].ratingKey == episodes[2].ratingKey
), "Items not in proper order [2a]."
# Test move items around (b)
pq.moveItem(pq.items[1])
assert pq.playQueueLastAddedItemID is None
assert pq.playQueueSelectedMetadataItemID == episodes[0].ratingKey
assert (
pq.items[0].ratingKey == episodes[1].ratingKey
), "Items not in proper order [0b]."
assert (
pq.items[1].ratingKey == episodes[0].ratingKey
), "Items not in proper order [1b]."
assert (
pq.items[2].ratingKey == episodes[2].ratingKey
), "Items not in proper order [2b]."
# Test move items around (c)
pq.moveItem(pq.items[0], after=pq.items[1])
assert pq.playQueueLastAddedItemID == pq.items[1].playQueueItemID
assert pq.playQueueSelectedMetadataItemID == episodes[0].ratingKey
assert (
pq.items[0].ratingKey == episodes[0].ratingKey
), "Items not in proper order [0c]."
assert (
pq.items[1].ratingKey == episodes[1].ratingKey
), "Items not in proper order [1c]."
assert (
pq.items[2].ratingKey == episodes[2].ratingKey
), "Items not in proper order [2c]."
# Test adding an item to Up Next section
pq.addItem(episodes[3])
assert pq.playQueueLastAddedItemID == pq.items[2].playQueueItemID
assert pq.playQueueSelectedMetadataItemID == episodes[0].ratingKey
assert pq.items[2].ratingKey == episodes[3].ratingKey, (
"Missing added item: %s" % episodes[3]
)
# Test adding an item to play next
pq.addItem(episodes[4], playNext=True)
assert pq.playQueueLastAddedItemID == pq.items[3].playQueueItemID
assert pq.playQueueSelectedMetadataItemID == episodes[0].ratingKey
assert pq.items[1].ratingKey == episodes[4].ratingKey, (
"Missing added item: %s" % episodes[4]
)
# Test add another item into Up Next section
pq.addItem(episodes[5])
assert pq.playQueueLastAddedItemID == pq.items[4].playQueueItemID
assert pq.playQueueSelectedMetadataItemID == episodes[0].ratingKey
assert pq.items[4].ratingKey == episodes[5].ratingKey, (
"Missing added item: %s" % episodes[5]
)
# Test removing an item
toremove = pq.items[3]
pq.removeItem(toremove)
assert pq.playQueueLastAddedItemID == pq.items[3].playQueueItemID
assert pq.playQueueSelectedMetadataItemID == episodes[0].ratingKey
assert toremove not in pq, "Removed item still in PlayQueue: %s" % toremove
assert len(pq) == 5, "PlayQueue should have 5 items, %s found" % len(pq)
# Test clearing the PlayQueue
pq.clear()
assert pq.playQueueSelectedMetadataItemID == episodes[0].ratingKey
assert len(pq) == 1, "PlayQueue should have 1 item, %s found" % len(pq)
# Test adding an item again
pq.addItem(episodes[7])
assert pq.playQueueLastAddedItemID == pq.items[1].playQueueItemID
assert pq.playQueueSelectedMetadataItemID == episodes[0].ratingKey
assert pq.items[1].ratingKey == episodes[7].ratingKey, (
"Missing added item: %s" % episodes[7]
)
def test_create_playqueue_with_single_item(plex, movie):
pq = plex.createPlayQueue(movie)
assert len(pq) == 1
assert pq.items[0].ratingKey == movie.ratingKey
def test_create_playqueue_with_start_choice(plex, show):
episodes = show.episodes()
pq = plex.createPlayQueue(episodes[:3], startItem=episodes[1])
assert pq.playQueueSelectedMetadataItemID == pq.items[1].ratingKey
def test_modify_playqueue_with_library_media(plex, show):
episodes = show.episodes()
pq = plex.createPlayQueue(episodes[:3])
assert len(pq) == 3, "PlayQueue does not contain 3 items."
# Test move PlayQueue using library items
pq.moveItem(episodes[1], after=episodes[2])
assert pq.items[0].ratingKey == episodes[0].ratingKey, "Items not in proper order."
assert pq.items[2].ratingKey == episodes[1].ratingKey, "Items not in proper order."
assert pq.items[1].ratingKey == episodes[2].ratingKey, "Items not in proper order."
# Test too many mathcing library items
pq.addItem(episodes[0])
pq.addItem(episodes[0])
with pytest.raises(BadRequest):
pq.moveItem(episodes[2], after=episodes[0])
# Test items not in PlayQueue
with pytest.raises(BadRequest):
pq.moveItem(episodes[9], after=episodes[0])
with pytest.raises(BadRequest):
pq.removeItem(episodes[9])
def test_create_playqueue_from_playlist(plex, album):
try:
playlist = plex.createPlaylist("test_playlist", album)
pq = playlist.playQueue(shuffle=1)
assert pq.playQueueShuffled is True
assert len(playlist) == len(album.tracks())
assert len(pq) == len(playlist)
pq.addItem(playlist)
assert len(pq) == 2 * len(playlist)
finally:
playlist.delete()

View file

@ -81,4 +81,4 @@ def test_utils_download(plex, episode):
def test_millisecondToHumanstr():
res = utils.millisecondToHumanstr(1000)
assert res == "00:00:01:0000"
assert res == "00:00:01.0000"

View file

@ -541,8 +541,8 @@ if __name__ == "__main__":
name="Music",
type="artist",
location="/data/Music" if opts.no_docker is False else music_path,
agent="com.plexapp.agents.lastfm",
scanner="Plex Music Scanner",
agent="tv.plex.agents.music",
scanner="Plex Music",
expected_media_count=song_c,
)
)