mirror of
https://github.com/pkkid/python-plexapi
synced 2024-11-10 14:14:19 +00:00
Merge remote-tracking branch 'upstream/master' into feature/plex_tv
This commit is contained in:
commit
a9597a9008
14 changed files with 347 additions and 146 deletions
|
@ -408,6 +408,11 @@ class Track(Audio, Playable, ArtUrlMixin, PosterUrlMixin, CollectionMixin, MoodM
|
|||
"""
|
||||
return [part.file for part in self.iterParts() if part]
|
||||
|
||||
@property
|
||||
def trackNumber(self):
|
||||
""" Returns the track number. """
|
||||
return self.index
|
||||
|
||||
def _defaultSyncTitle(self):
|
||||
""" Returns str, default title for a new syncItem. """
|
||||
return '%s - %s - %s' % (self.grandparentTitle, self.parentTitle, self.title)
|
||||
|
|
|
@ -1698,6 +1698,12 @@ class HubMediaTag(PlexObject):
|
|||
self.tagValue = utils.cast(int, data.attrib.get('tagValue'))
|
||||
self.thumb = data.attrib.get('thumb')
|
||||
|
||||
def items(self, *args, **kwargs):
|
||||
""" Return the list of items within this tag. """
|
||||
if not self.key:
|
||||
raise BadRequest('Key is not defined for this tag: %s' % self.tag)
|
||||
return self.fetchItems(self.key)
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Tag(HubMediaTag):
|
||||
|
|
246
plexapi/media.py
246
plexapi/media.py
|
@ -112,7 +112,7 @@ class MediaPart(PlexObject):
|
|||
has64bitOffsets (bool): True if the file has 64 bit offsets.
|
||||
hasThumbnail (bool): True if the file (track) has an embedded thumbnail.
|
||||
id (int): The unique ID for this media part on the server.
|
||||
indexes (str, None): sd if the file has generated BIF thumbnails.
|
||||
indexes (str, None): sd if the file has generated preview (BIF) thumbnails.
|
||||
key (str): API URL (ex: /library/parts/46618/1389985872/file.mkv).
|
||||
optimizedForStreaming (bool): True if the file is optimized for streaming.
|
||||
packetLength (int): The packet length of the file.
|
||||
|
@ -157,6 +157,11 @@ class MediaPart(PlexObject):
|
|||
streams.extend(items)
|
||||
return streams
|
||||
|
||||
@property
|
||||
def hasPreviewThumbnails(self):
|
||||
""" Returns True if the media part has generated preview (BIF) thumbnails. """
|
||||
return self.indexes == 'sd'
|
||||
|
||||
def videoStreams(self):
|
||||
""" Returns a list of :class:`~plexapi.media.VideoStream` objects in this MediaPart. """
|
||||
return [stream for stream in self.streams if isinstance(stream, VideoStream)]
|
||||
|
@ -558,6 +563,13 @@ class Optimized(PlexObject):
|
|||
self.target = data.attrib.get('target')
|
||||
self.targetTagID = data.attrib.get('targetTagID')
|
||||
|
||||
def items(self):
|
||||
""" Returns a list of all :class:`~plexapi.media.Video` objects
|
||||
in this optimized item.
|
||||
"""
|
||||
key = '%s/%s/items' % (self._initpath, self.id)
|
||||
return self.fetchItems(key)
|
||||
|
||||
def remove(self):
|
||||
""" Remove an Optimized item"""
|
||||
key = '%s/%s' % (self._initpath, self.id)
|
||||
|
@ -641,59 +653,43 @@ class MediaTag(PlexObject):
|
|||
the construct used for things such as Country, Director, Genre, etc.
|
||||
|
||||
Attributes:
|
||||
server (:class:`~plexapi.server.PlexServer`): Server this client is connected to.
|
||||
filter (str): The library filter for the tag.
|
||||
id (id): Tag ID (This seems meaningless except to use it as a unique id).
|
||||
role (str): Unknown
|
||||
key (str): API URL (/library/section/<librarySectionID>/all?<filter>).
|
||||
role (str): The name of the character role for :class:`~plexapi.media.Role` only.
|
||||
tag (str): Name of the tag. This will be Animation, SciFi etc for Genres. The name of
|
||||
person for Directors and Roles (ex: Animation, Stephen Graham, etc).
|
||||
<Hub_Search_Attributes>: Attributes only applicable in search results from
|
||||
PlexServer :func:`~plexapi.server.PlexServer.search`. They provide details of which
|
||||
library section the tag was found as well as the url to dig deeper into the results.
|
||||
|
||||
* key (str): API URL to dig deeper into this tag (ex: /library/sections/1/all?actor=9081).
|
||||
* librarySectionID (int): Section ID this tag was generated from.
|
||||
* librarySectionTitle (str): Library section title this tag was found.
|
||||
* librarySectionType (str): Media type of the library section this tag was found.
|
||||
* tagType (int): Tag type ID.
|
||||
* thumb (str): URL to thumbnail image.
|
||||
thumb (str): URL to thumbnail image for :class:`~plexapi.media.Role` only.
|
||||
"""
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.filter = data.attrib.get('filter')
|
||||
self.id = cast(int, data.attrib.get('id'))
|
||||
self.key = data.attrib.get('key')
|
||||
self.role = data.attrib.get('role')
|
||||
self.tag = data.attrib.get('tag')
|
||||
# additional attributes only from hub search
|
||||
self.key = data.attrib.get('key')
|
||||
self.librarySectionID = cast(int, data.attrib.get('librarySectionID'))
|
||||
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||
self.librarySectionType = data.attrib.get('librarySectionType')
|
||||
self.tagType = cast(int, data.attrib.get('tagType'))
|
||||
self.thumb = data.attrib.get('thumb')
|
||||
|
||||
def items(self, *args, **kwargs):
|
||||
""" Return the list of items within this tag. This function is only applicable
|
||||
in search results from PlexServer :func:`~plexapi.server.PlexServer.search`.
|
||||
"""
|
||||
parent = self._parent()
|
||||
self._librarySectionID = utils.cast(int, parent._data.attrib.get('librarySectionID'))
|
||||
self._librarySectionKey = parent._data.attrib.get('librarySectionKey')
|
||||
self._librarySectionTitle = parent._data.attrib.get('librarySectionTitle')
|
||||
self._parentType = parent.TYPE
|
||||
|
||||
if self._librarySectionKey and self.filter:
|
||||
self.key = '%s/all?%s&type=%s' % (
|
||||
self._librarySectionKey, self.filter, utils.searchType(self._parentType))
|
||||
|
||||
def items(self):
|
||||
""" Return the list of items within this tag. """
|
||||
if not self.key:
|
||||
raise BadRequest('Key is not defined for this tag: %s' % self.tag)
|
||||
raise BadRequest('Key is not defined for this tag: %s. '
|
||||
'Reload the parent object.' % self.tag)
|
||||
return self.fetchItems(self.key)
|
||||
|
||||
|
||||
class GuidTag(PlexObject):
|
||||
""" Base class for guid tags used only for Guids, as they contain only a string identifier
|
||||
|
||||
Attributes:
|
||||
id (id): The guid for external metadata sources (e.g. IMDB, TMDB, TVDB).
|
||||
"""
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.id = data.attrib.get('id')
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Collection(MediaTag):
|
||||
""" Represents a single Collection media tag.
|
||||
|
@ -705,36 +701,11 @@ class Collection(MediaTag):
|
|||
TAG = 'Collection'
|
||||
FILTER = 'collection'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Label(MediaTag):
|
||||
""" Represents a single Label media tag.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Label'
|
||||
FILTER (str): 'label'
|
||||
"""
|
||||
TAG = 'Label'
|
||||
FILTER = 'label'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Tag(MediaTag):
|
||||
""" Represents a single Tag media tag.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Tag'
|
||||
FILTER (str): 'tag'
|
||||
"""
|
||||
TAG = 'Tag'
|
||||
FILTER = 'tag'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self.id = cast(int, data.attrib.get('id', 0))
|
||||
self.filter = data.attrib.get('filter')
|
||||
self.tag = data.attrib.get('tag')
|
||||
self.title = self.tag
|
||||
def collection(self):
|
||||
""" Return the :class:`~plexapi.collection.Collection` object for this collection tag.
|
||||
"""
|
||||
key = '%s/collections' % self._librarySectionKey
|
||||
return self.fetchItem(key, etag='Directory', index=self.id)
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
|
@ -774,13 +745,15 @@ class Genre(MediaTag):
|
|||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Guid(GuidTag):
|
||||
""" Represents a single Guid media tag.
|
||||
class Label(MediaTag):
|
||||
""" Represents a single Label media tag.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Guid'
|
||||
TAG (str): 'Label'
|
||||
FILTER (str): 'label'
|
||||
"""
|
||||
TAG = "Guid"
|
||||
TAG = 'Label'
|
||||
FILTER = 'label'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
|
@ -795,6 +768,42 @@ class Mood(MediaTag):
|
|||
FILTER = 'mood'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Producer(MediaTag):
|
||||
""" Represents a single Producer media tag.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Producer'
|
||||
FILTER (str): 'producer'
|
||||
"""
|
||||
TAG = 'Producer'
|
||||
FILTER = 'producer'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Role(MediaTag):
|
||||
""" Represents a single Role (actor/actress) media tag.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Role'
|
||||
FILTER (str): 'role'
|
||||
"""
|
||||
TAG = 'Role'
|
||||
FILTER = 'role'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Similar(MediaTag):
|
||||
""" Represents a single Similar media tag.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Similar'
|
||||
FILTER (str): 'similar'
|
||||
"""
|
||||
TAG = 'Similar'
|
||||
FILTER = 'similar'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Style(MediaTag):
|
||||
""" Represents a single Style media tag.
|
||||
|
@ -807,6 +816,53 @@ class Style(MediaTag):
|
|||
FILTER = 'style'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Tag(MediaTag):
|
||||
""" Represents a single Tag media tag.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Tag'
|
||||
FILTER (str): 'tag'
|
||||
"""
|
||||
TAG = 'Tag'
|
||||
FILTER = 'tag'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Writer(MediaTag):
|
||||
""" Represents a single Writer media tag.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Writer'
|
||||
FILTER (str): 'writer'
|
||||
"""
|
||||
TAG = 'Writer'
|
||||
FILTER = 'writer'
|
||||
|
||||
|
||||
class GuidTag(PlexObject):
|
||||
""" Base class for guid tags used only for Guids, as they contain only a string identifier
|
||||
|
||||
Attributes:
|
||||
id (id): The guid for external metadata sources (e.g. IMDB, TMDB, TVDB).
|
||||
"""
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.id = data.attrib.get('id')
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Guid(GuidTag):
|
||||
""" Represents a single Guid media tag.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Guid'
|
||||
"""
|
||||
TAG = 'Guid'
|
||||
|
||||
|
||||
class BaseImage(PlexObject):
|
||||
""" Base class for all Art, Banner, and Poster objects.
|
||||
|
||||
|
@ -849,54 +905,6 @@ class Poster(BaseImage):
|
|||
""" Represents a single Poster object. """
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Producer(MediaTag):
|
||||
""" Represents a single Producer media tag.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Producer'
|
||||
FILTER (str): 'producer'
|
||||
"""
|
||||
TAG = 'Producer'
|
||||
FILTER = 'producer'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Role(MediaTag):
|
||||
""" Represents a single Role (actor/actress) media tag.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Role'
|
||||
FILTER (str): 'role'
|
||||
"""
|
||||
TAG = 'Role'
|
||||
FILTER = 'role'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Similar(MediaTag):
|
||||
""" Represents a single Similar media tag.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Similar'
|
||||
FILTER (str): 'similar'
|
||||
"""
|
||||
TAG = 'Similar'
|
||||
FILTER = 'similar'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Writer(MediaTag):
|
||||
""" Represents a single Writer media tag.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Writer'
|
||||
FILTER (str): 'writer'
|
||||
"""
|
||||
TAG = 'Writer'
|
||||
FILTER = 'writer'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Chapter(PlexObject):
|
||||
""" Represents a single Writer media tag.
|
||||
|
|
|
@ -470,6 +470,7 @@ class MyPlexAccount(PlexObject):
|
|||
Parameters:
|
||||
username (str): Username, email or id of the user to return.
|
||||
"""
|
||||
username = str(username)
|
||||
for user in self.users():
|
||||
# Home users don't have email, username etc.
|
||||
if username.lower() == user.title.lower():
|
||||
|
|
|
@ -253,15 +253,13 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin):
|
|||
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())
|
||||
""" Copy playlist to another user account.
|
||||
|
||||
Parameters:
|
||||
user (str): Username, email or user id of the user to copy the playlist to.
|
||||
"""
|
||||
userServer = self._server.switchUser(user)
|
||||
return self.create(userServer, self.title, self.items())
|
||||
|
||||
def sync(self, videoQuality=None, photoResolution=None, audioBitrate=None, client=None, clientId=None, limit=None,
|
||||
unwatched=False, title=None):
|
||||
|
|
|
@ -38,8 +38,9 @@ class PlexServer(PlexObject):
|
|||
baseurl (str): Base url for to access the Plex Media Server (default: 'http://localhost:32400').
|
||||
token (str): Required Plex authentication token to access the server.
|
||||
session (requests.Session, optional): Use your own session object if you want to
|
||||
cache the http responses from PMS
|
||||
timeout (int): timeout in seconds on initial connect to server (default config.TIMEOUT).
|
||||
cache the http responses from the server.
|
||||
timeout (int, optional): Timeout in seconds on initial connection to the server
|
||||
(default config.TIMEOUT).
|
||||
|
||||
Attributes:
|
||||
allowCameraUpload (bool): True if server allows camera upload.
|
||||
|
@ -105,12 +106,13 @@ class PlexServer(PlexObject):
|
|||
self._token = logfilter.add_secret(token or CONFIG.get('auth.server_token'))
|
||||
self._showSecrets = CONFIG.get('log.show_secrets', '').lower() == 'true'
|
||||
self._session = session or requests.Session()
|
||||
self._timeout = timeout
|
||||
self._library = None # cached library
|
||||
self._settings = None # cached settings
|
||||
self._myPlexAccount = None # cached myPlexAccount
|
||||
self._systemAccounts = None # cached list of SystemAccount
|
||||
self._systemDevices = None # cached list of SystemDevice
|
||||
data = self.query(self.key, timeout=timeout)
|
||||
data = self.query(self.key, timeout=self._timeout)
|
||||
super(PlexServer, self).__init__(self, data, self.key)
|
||||
|
||||
def _loadData(self, data):
|
||||
|
@ -209,13 +211,45 @@ class PlexServer(PlexObject):
|
|||
return self.fetchItems(key)
|
||||
|
||||
def createToken(self, type='delegation', scope='all'):
|
||||
"""Create a temp access token for the server."""
|
||||
""" Create a temp access token for the server. """
|
||||
if not self._token:
|
||||
# Handle unclaimed servers
|
||||
return None
|
||||
q = self.query('/security/token?type=%s&scope=%s' % (type, scope))
|
||||
return q.attrib.get('token')
|
||||
|
||||
def switchUser(self, username, session=None, timeout=None):
|
||||
""" Returns a new :class:`~plexapi.server.PlexServer` object logged in as the given username.
|
||||
Note: Only the admin account can switch to other users.
|
||||
|
||||
Parameters:
|
||||
username (str): Username, email or user id of the user to log in to the server.
|
||||
session (requests.Session, optional): Use your own session object if you want to
|
||||
cache the http responses from the server. This will default to the same
|
||||
session as the admin account if no new session is provided.
|
||||
timeout (int, optional): Timeout in seconds on initial connection to the server.
|
||||
This will default to the same timeout as the admin account if no new timeout
|
||||
is provided.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from plexapi.server import PlexServer
|
||||
# Login to the Plex server using the admin token
|
||||
plex = PlexServer('http://plexserver:32400', token='2ffLuB84dqLswk9skLos')
|
||||
# Login to the same Plex server using a different account
|
||||
userPlex = plex.switchUser("Username")
|
||||
|
||||
"""
|
||||
user = self.myPlexAccount().user(username)
|
||||
userToken = user.get_token(self.machineIdentifier)
|
||||
if session is None:
|
||||
session = self._session
|
||||
if timeout is None:
|
||||
timeout = self._timeout
|
||||
return PlexServer(self._baseurl, token=userToken, session=session, timeout=timeout)
|
||||
|
||||
def systemAccounts(self):
|
||||
""" Returns a list of :class:`~plexapi.server.SystemAccount` objects this server contains. """
|
||||
if self._systemAccounts is None:
|
||||
|
@ -489,6 +523,7 @@ class PlexServer(PlexObject):
|
|||
backgroundProcessing = self.fetchItem('/playlists?type=42')
|
||||
return self.fetchItems('%s/items' % backgroundProcessing.key, cls=Optimized)
|
||||
|
||||
@deprecated('use "plexapi.media.Optimized.items()" instead')
|
||||
def optimizedItem(self, optimizedID):
|
||||
""" Returns single queued optimized item :class:`~plexapi.media.Video` object.
|
||||
Allows for using optimized item ID to connect back to source item.
|
||||
|
|
|
@ -340,6 +340,11 @@ class Movie(Video, Playable, AdvancedSettingsMixin, ArtMixin, PosterMixin, Split
|
|||
"""
|
||||
return [part.file for part in self.iterParts() if part]
|
||||
|
||||
@property
|
||||
def hasPreviewThumbnails(self):
|
||||
""" Returns True if any of the media parts has generated preview (BIF) thumbnails. """
|
||||
return any(part.hasPreviewThumbnails for media in self.media for part in media.parts)
|
||||
|
||||
def _prettyfilename(self):
|
||||
# This is just for compat.
|
||||
return self.title
|
||||
|
@ -648,7 +653,7 @@ class Season(Video, ArtMixin, PosterMixin, CollectionMixin):
|
|||
|
||||
@property
|
||||
def seasonNumber(self):
|
||||
""" Returns season number. """
|
||||
""" Returns the season number. """
|
||||
return self.index
|
||||
|
||||
def episodes(self, **kwargs):
|
||||
|
@ -839,17 +844,22 @@ class Episode(Video, Playable, ArtMixin, PosterMixin, CollectionMixin, DirectorM
|
|||
"""
|
||||
return [part.file for part in self.iterParts() if part]
|
||||
|
||||
@property
|
||||
def episodeNumber(self):
|
||||
""" Returns the episode number. """
|
||||
return self.index
|
||||
|
||||
@property
|
||||
def seasonNumber(self):
|
||||
""" Returns the episodes season number. """
|
||||
""" Returns the episode's season number. """
|
||||
if self._seasonNumber is None:
|
||||
self._seasonNumber = self.parentIndex if self.parentIndex else self.season().seasonNumber
|
||||
return utils.cast(int, self._seasonNumber)
|
||||
|
||||
@property
|
||||
def seasonEpisode(self):
|
||||
""" Returns the s00e00 string containing the season and episode. """
|
||||
return 's%se%s' % (str(self.seasonNumber).zfill(2), str(self.index).zfill(2))
|
||||
""" Returns the s00e00 string containing the season and episode numbers. """
|
||||
return 's%se%s' % (str(self.seasonNumber).zfill(2), str(self.episodeNumber).zfill(2))
|
||||
|
||||
@property
|
||||
def hasIntroMarker(self):
|
||||
|
@ -858,6 +868,11 @@ class Episode(Video, Playable, ArtMixin, PosterMixin, CollectionMixin, DirectorM
|
|||
self.reload()
|
||||
return any(marker.type == 'intro' for marker in self.markers)
|
||||
|
||||
@property
|
||||
def hasPreviewThumbnails(self):
|
||||
""" Returns True if any of the media parts has generated preview (BIF) thumbnails. """
|
||||
return any(part.hasPreviewThumbnails for media in self.media for part in media.parts)
|
||||
|
||||
def season(self):
|
||||
"""" Return the episode's :class:`~plexapi.video.Season`. """
|
||||
return self.fetchItem(self.parentKey)
|
||||
|
|
|
@ -234,11 +234,11 @@ def movie(movies):
|
|||
@pytest.fixture()
|
||||
def collection(movies):
|
||||
try:
|
||||
return movies.collections(title="marvel")[0]
|
||||
return movies.collections(title="Marvel")[0]
|
||||
except IndexError:
|
||||
movie = movies.get("Elephants Dream")
|
||||
movie.addCollection("marvel")
|
||||
return movies.collections(title="marvel")[0]
|
||||
movie.addCollection("Marvel")
|
||||
return movies.collections(title="Marvel")[0]
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import conftest as utils
|
||||
from . import test_mixins
|
||||
from . import test_media, test_mixins
|
||||
|
||||
|
||||
def test_audio_Artist_attr(artist):
|
||||
|
@ -87,6 +87,16 @@ def test_audio_Artist_mixins_tags(artist):
|
|||
test_mixins.edit_style(artist)
|
||||
|
||||
|
||||
def test_audio_Artist_media_tags(artist):
|
||||
artist.reload()
|
||||
test_media.tag_collection(artist)
|
||||
test_media.tag_country(artist)
|
||||
test_media.tag_genre(artist)
|
||||
test_media.tag_mood(artist)
|
||||
test_media.tag_similar(artist)
|
||||
test_media.tag_style(artist)
|
||||
|
||||
|
||||
def test_audio_Album_attrs(album):
|
||||
assert utils.is_datetime(album.addedAt)
|
||||
if album.art:
|
||||
|
@ -165,6 +175,15 @@ def test_audio_Album_mixins_tags(album):
|
|||
test_mixins.edit_style(album)
|
||||
|
||||
|
||||
def test_audio_Album_media_tags(album):
|
||||
album.reload()
|
||||
test_media.tag_collection(album)
|
||||
test_media.tag_genre(album)
|
||||
test_media.tag_label(album)
|
||||
test_media.tag_mood(album)
|
||||
test_media.tag_style(album)
|
||||
|
||||
|
||||
def test_audio_Track_attrs(album):
|
||||
track = album.get("As Colourful As Ever").reload()
|
||||
assert utils.is_datetime(track.addedAt)
|
||||
|
@ -180,7 +199,8 @@ def test_audio_Track_attrs(album):
|
|||
assert utils.is_thumb(track.grandparentThumb)
|
||||
assert track.grandparentTitle == "Broke For Free"
|
||||
assert track.guid.startswith("mbid://") or track.guid.startswith("plex://track/")
|
||||
assert int(track.index) == 1
|
||||
assert track.index == 1
|
||||
assert track.trackNumber == track.index
|
||||
assert utils.is_metadata(track._initpath)
|
||||
assert utils.is_metadata(track.key)
|
||||
assert utils.is_datetime(track.lastViewedAt)
|
||||
|
@ -294,6 +314,12 @@ def test_audio_Track_mixins_tags(track):
|
|||
test_mixins.edit_mood(track)
|
||||
|
||||
|
||||
def test_audio_Track_media_tags(track):
|
||||
track.reload()
|
||||
test_media.tag_collection(track)
|
||||
test_media.tag_mood(track)
|
||||
|
||||
|
||||
def test_audio_Audio_section(artist, album, track):
|
||||
assert artist.section()
|
||||
assert album.section()
|
||||
|
|
|
@ -33,7 +33,7 @@ def test_Collection_attrs(collection):
|
|||
assert collection.summary == ""
|
||||
assert collection.thumb.startswith("/library/collections/%s/composite" % collection.ratingKey)
|
||||
assert collection.thumbBlurHash is None
|
||||
assert collection.title == "marvel"
|
||||
assert collection.title == "Marvel"
|
||||
assert collection.titleSort == collection.title
|
||||
assert collection.type == "collection"
|
||||
assert utils.is_datetime(collection.updatedAt)
|
||||
|
|
54
tests/test_media.py
Normal file
54
tests/test_media.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
def _test_media_tag(obj, attr):
|
||||
tags = getattr(obj, attr)
|
||||
if tags:
|
||||
assert obj in tags[0].items()
|
||||
|
||||
|
||||
def tag_collection(obj):
|
||||
_test_media_tag(obj, "collections")
|
||||
|
||||
|
||||
def tag_country(obj):
|
||||
_test_media_tag(obj, "countries")
|
||||
|
||||
|
||||
def tag_director(obj):
|
||||
_test_media_tag(obj, "directors")
|
||||
|
||||
|
||||
def tag_genre(obj):
|
||||
_test_media_tag(obj, "genres")
|
||||
|
||||
|
||||
def tag_label(obj):
|
||||
_test_media_tag(obj, "labels")
|
||||
|
||||
|
||||
def tag_mood(obj):
|
||||
_test_media_tag(obj, "moods")
|
||||
|
||||
|
||||
def tag_producer(obj):
|
||||
_test_media_tag(obj, "producers")
|
||||
|
||||
|
||||
def tag_role(obj):
|
||||
_test_media_tag(obj, "roles")
|
||||
|
||||
|
||||
def tag_similar(obj):
|
||||
_test_media_tag(obj, "similar")
|
||||
|
||||
|
||||
def tag_style(obj):
|
||||
_test_media_tag(obj, "styles")
|
||||
|
||||
|
||||
def tag_tag(obj):
|
||||
_test_media_tag(obj, "tags")
|
||||
|
||||
|
||||
def tag_writer(obj):
|
||||
_test_media_tag(obj, "writers")
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import test_mixins
|
||||
from . import test_media, test_mixins
|
||||
|
||||
|
||||
def test_photo_Photoalbum(photoalbum):
|
||||
|
@ -20,3 +20,8 @@ def test_photo_Photoalbum_mixins_images(photoalbum):
|
|||
|
||||
def test_photo_Photo_mixins_tags(photo):
|
||||
test_mixins.edit_tag(photo)
|
||||
|
||||
|
||||
def test_photo_Photo_media_tags(photo):
|
||||
photo.reload()
|
||||
test_media.tag_tag(photo)
|
||||
|
|
|
@ -135,6 +135,7 @@ def test_server_search(plex, movie):
|
|||
assert hub_tag.tagType == 1
|
||||
assert hub_tag.tagValue is None
|
||||
assert hub_tag.thumb is None
|
||||
assert movie in hub_tag.items()
|
||||
# Test director search
|
||||
director = movie.directors[0]
|
||||
assert plex.search(director.tag, mediatype="director")
|
||||
|
|
|
@ -8,7 +8,7 @@ import pytest
|
|||
from plexapi.exceptions import BadRequest, NotFound
|
||||
|
||||
from . import conftest as utils
|
||||
from . import test_mixins
|
||||
from . import test_media, test_mixins
|
||||
|
||||
|
||||
def test_video_Movie(movies, movie):
|
||||
|
@ -59,6 +59,30 @@ def test_video_Movie_mixins_tags(movie):
|
|||
test_mixins.edit_writer(movie)
|
||||
|
||||
|
||||
def test_video_Movie_media_tags(movie):
|
||||
movie.reload()
|
||||
test_media.tag_collection(movie)
|
||||
test_media.tag_country(movie)
|
||||
test_media.tag_director(movie)
|
||||
test_media.tag_genre(movie)
|
||||
test_media.tag_label(movie)
|
||||
test_media.tag_producer(movie)
|
||||
test_media.tag_role(movie)
|
||||
test_media.tag_similar(movie)
|
||||
test_media.tag_writer(movie)
|
||||
|
||||
|
||||
def test_video_Movie_media_tags_Exception(movie):
|
||||
with pytest.raises(BadRequest):
|
||||
movie.genres[0].items()
|
||||
|
||||
|
||||
def test_video_Movie_media_tags_collection(movie, collection):
|
||||
movie.reload()
|
||||
collection_tag = next(c for c in movie.collections if c.tag == "Marvel")
|
||||
assert collection == collection_tag.collection()
|
||||
|
||||
|
||||
def test_video_Movie_getStreamURL(movie, account):
|
||||
key = movie.ratingKey
|
||||
assert movie.getStreamURL() == (
|
||||
|
@ -188,6 +212,7 @@ def test_video_Movie_attrs(movies):
|
|||
assert "Animation" in [i.tag for i in movie.genres]
|
||||
assert "imdb://tt1172203" in [i.id for i in movie.guids]
|
||||
assert movie.guid == "plex://movie/5d776846880197001ec967c6"
|
||||
assert movie.hasPreviewThumbnails is False
|
||||
assert utils.is_metadata(movie._initpath)
|
||||
assert utils.is_metadata(movie.key)
|
||||
assert movie.languageOverride is None
|
||||
|
@ -338,6 +363,7 @@ def test_video_Movie_attrs(movies):
|
|||
assert part.exists
|
||||
assert len(part.file) >= 10
|
||||
assert part.has64bitOffsets is False
|
||||
assert part.hasPreviewThumbnails is False
|
||||
assert part.hasThumbnail is None
|
||||
assert utils.is_int(part.id)
|
||||
assert part.indexes is None
|
||||
|
@ -707,6 +733,15 @@ def test_video_Show_mixins_tags(show):
|
|||
test_mixins.edit_label(show)
|
||||
|
||||
|
||||
def test_video_Show_media_tags(show):
|
||||
show.reload()
|
||||
test_media.tag_collection(show)
|
||||
test_media.tag_genre(show)
|
||||
test_media.tag_label(show)
|
||||
test_media.tag_role(show)
|
||||
test_media.tag_similar(show)
|
||||
|
||||
|
||||
def test_video_Season(show):
|
||||
seasons = show.seasons()
|
||||
assert len(seasons) == 2
|
||||
|
@ -896,13 +931,16 @@ def test_video_Episode_attrs(episode):
|
|||
assert episode.grandparentTitle == "Game of Thrones"
|
||||
assert episode.guid == "plex://episode/5d9c1275e98e47001eb84029"
|
||||
assert "tvdb://3254641" in [i.id for i in episode.guids]
|
||||
assert episode.hasPreviewThumbnails is False
|
||||
assert episode.index == 1
|
||||
assert episode.episodeNumber == episode.index
|
||||
assert utils.is_metadata(episode._initpath)
|
||||
assert utils.is_metadata(episode.key)
|
||||
assert episode.listType == "video"
|
||||
assert utils.is_datetime(episode.originallyAvailableAt)
|
||||
assert episode.parentGuid == "plex://season/602e67d31d3358002c411c39"
|
||||
assert utils.is_int(episode.parentIndex)
|
||||
assert episode.seasonNumber == episode.parentIndex
|
||||
assert utils.is_metadata(episode.parentKey)
|
||||
assert utils.is_int(episode.parentRatingKey)
|
||||
if episode.parentThumb:
|
||||
|
@ -930,6 +968,7 @@ def test_video_Episode_attrs(episode):
|
|||
assert episode.isWatched in [True, False]
|
||||
assert len(episode.locations) == 1
|
||||
assert len(episode.locations[0]) >= 10
|
||||
assert episode.seasonEpisode == "s01e01"
|
||||
# Media
|
||||
media = episode.media[0]
|
||||
assert media.aspectRatio == 1.78
|
||||
|
@ -953,6 +992,7 @@ def test_video_Episode_attrs(episode):
|
|||
assert part.container in utils.CONTAINERS
|
||||
assert utils.is_int(part.duration, gte=150000)
|
||||
assert len(part.file) >= 10
|
||||
assert part.hasPreviewThumbnails is False
|
||||
assert utils.is_int(part.id)
|
||||
assert utils.is_metadata(part._initpath)
|
||||
assert len(part.key) >= 10
|
||||
|
@ -994,6 +1034,13 @@ def test_video_Episode_mixins_tags(episode):
|
|||
test_mixins.edit_writer(episode)
|
||||
|
||||
|
||||
def test_video_Episode_media_tags(episode):
|
||||
episode.reload()
|
||||
test_media.tag_collection(episode)
|
||||
test_media.tag_director(episode)
|
||||
test_media.tag_writer(episode)
|
||||
|
||||
|
||||
def test_that_reload_return_the_same_object(plex):
|
||||
# we want to check this that all the urls are correct
|
||||
movie_library_search = plex.library.section("Movies").search("Elephants Dream")[0]
|
||||
|
@ -1104,7 +1151,7 @@ def test_video_optimize(movie, plex):
|
|||
assert len(plex.conversions()) == 0
|
||||
assert len(plex.optimizedItems()) == 1
|
||||
optimized = plex.optimizedItems()[0]
|
||||
video = plex.optimizedItem(optimizedID=optimized.id)
|
||||
assert movie.key == video.key
|
||||
videos = optimized.items()
|
||||
assert movie in videos
|
||||
plex.optimizedItems(removeAll=True)
|
||||
assert len(plex.optimizedItems()) == 0
|
||||
|
|
Loading…
Reference in a new issue