mirror of
https://github.com/pkkid/python-plexapi
synced 2024-11-24 20:53:09 +00:00
Merge branch 'master' into library_hubs
This commit is contained in:
commit
d5f9004e7b
16 changed files with 564 additions and 105 deletions
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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
52
tests/test__prepare.py
Normal 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)
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
140
tests/test_playqueue.py
Normal 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()
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue