mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-10 15:32:38 -07:00
Bump plexapi from 4.12.1 to 4.13.1 (#1888)
Bumps [plexapi](https://github.com/pkkid/python-plexapi) from 4.12.1 to 4.13.1. - [Release notes](https://github.com/pkkid/python-plexapi/releases) - [Commits](https://github.com/pkkid/python-plexapi/compare/4.12.1...4.13.1) --- updated-dependencies: - dependency-name: plexapi dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> [skip ci]
This commit is contained in:
parent
3af08f0d07
commit
e79da07973
20 changed files with 1791 additions and 724 deletions
|
@ -1,21 +1,21 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
from urllib.parse import quote_plus, urlencode
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from plexapi import media, utils
|
||||
from plexapi.base import Playable, PlexPartialObject
|
||||
from plexapi.base import Playable, PlexPartialObject, PlexSession
|
||||
from plexapi.exceptions import BadRequest
|
||||
from plexapi.mixins import (
|
||||
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
|
||||
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin,
|
||||
ArtUrlMixin, ArtMixin, BannerMixin, PosterUrlMixin, PosterMixin, ThemeUrlMixin, ThemeMixin,
|
||||
ContentRatingMixin, OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, StudioMixin,
|
||||
ContentRatingMixin, EditionTitleMixin, OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, StudioMixin,
|
||||
SummaryMixin, TaglineMixin, TitleMixin,
|
||||
CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin,
|
||||
WatchlistMixin
|
||||
)
|
||||
|
||||
|
||||
class Video(PlexPartialObject):
|
||||
class Video(PlexPartialObject, PlayedUnplayedMixin):
|
||||
""" Base class for all video objects including :class:`~plexapi.video.Movie`,
|
||||
:class:`~plexapi.video.Show`, :class:`~plexapi.video.Season`,
|
||||
:class:`~plexapi.video.Episode`, and :class:`~plexapi.video.Clip`.
|
||||
|
@ -71,25 +71,10 @@ class Video(PlexPartialObject):
|
|||
self.userRating = utils.cast(float, data.attrib.get('userRating'))
|
||||
self.viewCount = utils.cast(int, data.attrib.get('viewCount', 0))
|
||||
|
||||
@property
|
||||
def isWatched(self):
|
||||
""" Returns True if this video is watched. """
|
||||
return bool(self.viewCount > 0) if self.viewCount else False
|
||||
|
||||
def url(self, part):
|
||||
""" Returns the full url for something. Typically used for getting a specific image. """
|
||||
return self._server.url(part, includeToken=True) if part else None
|
||||
|
||||
def markWatched(self):
|
||||
""" Mark the video as played. """
|
||||
key = '/:/scrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey
|
||||
self._server.query(key)
|
||||
|
||||
def markUnwatched(self):
|
||||
""" Mark the video as unplayed. """
|
||||
key = '/:/unscrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey
|
||||
self._server.query(key)
|
||||
|
||||
def augmentation(self):
|
||||
""" Returns a list of :class:`~plexapi.library.Hub` objects.
|
||||
Augmentation returns hub items relating to online media sources
|
||||
|
@ -132,7 +117,7 @@ class Video(PlexPartialObject):
|
|||
|
||||
def uploadSubtitles(self, filepath):
|
||||
""" Upload Subtitle file for video. """
|
||||
url = '%s/subtitles' % self.key
|
||||
url = f'{self.key}/subtitles'
|
||||
filename = os.path.basename(filepath)
|
||||
subFormat = os.path.splitext(filepath)[1][1:]
|
||||
with open(filepath, 'rb') as subfile:
|
||||
|
@ -141,6 +126,7 @@ class Video(PlexPartialObject):
|
|||
}
|
||||
headers = {'Accept': 'text/plain, */*'}
|
||||
self._server.query(url, self._server._session.post, data=subfile, params=params, headers=headers)
|
||||
return self
|
||||
|
||||
def removeSubtitles(self, streamID=None, streamTitle=None):
|
||||
""" Remove Subtitle from movie's subtitles listing.
|
||||
|
@ -151,74 +137,103 @@ class Video(PlexPartialObject):
|
|||
for stream in self.subtitleStreams():
|
||||
if streamID == stream.id or streamTitle == stream.title:
|
||||
self._server.query(stream.key, self._server._session.delete)
|
||||
return self
|
||||
|
||||
def optimize(self, title=None, target="", targetTagID=None, locationID=-1, policyScope='all',
|
||||
policyValue="", policyUnwatched=0, videoQuality=None, deviceProfile=None):
|
||||
""" Optimize item
|
||||
def optimize(self, title='', target='', deviceProfile='', videoQuality=None,
|
||||
locationID=-1, limit=None, unwatched=False):
|
||||
""" Create an optimized version of the video.
|
||||
|
||||
locationID (int): -1 in folder with original items
|
||||
2 library path id
|
||||
library path id is found in library.locations[i].id
|
||||
Parameters:
|
||||
title (str, optional): Title of the optimized video.
|
||||
target (str, optional): Target quality profile:
|
||||
"Optimized for Mobile" ("mobile"), "Optimized for TV" ("tv"), "Original Quality" ("original"),
|
||||
or custom quality profile name (default "Custom: {deviceProfile}").
|
||||
deviceProfile (str, optional): Custom quality device profile:
|
||||
"Android", "iOS", "Universal Mobile", "Universal TV", "Windows Phone", "Windows", "Xbox One".
|
||||
Required if ``target`` is custom.
|
||||
videoQuality (int, optional): Index of the quality profile, one of ``VIDEO_QUALITY_*``
|
||||
values defined in the :mod:`~plexapi.sync` module. Only used if ``target`` is custom.
|
||||
locationID (int or :class:`~plexapi.library.Location`, optional): Default -1 for
|
||||
"In folder with original items", otherwise a :class:`~plexapi.library.Location` object or ID.
|
||||
See examples below.
|
||||
limit (int, optional): Maximum count of items to optimize, unlimited if ``None``.
|
||||
unwatched (bool, optional): ``True`` to only optimized unwatched videos.
|
||||
|
||||
target (str): custom quality name.
|
||||
if none provided use "Custom: {deviceProfile}"
|
||||
|
||||
targetTagID (int): Default quality settings
|
||||
1 Mobile
|
||||
2 TV
|
||||
3 Original Quality
|
||||
|
||||
deviceProfile (str): Android, IOS, Universal TV, Universal Mobile, Windows Phone,
|
||||
Windows, Xbox One
|
||||
Raises:
|
||||
:exc:`~plexapi.exceptions.BadRequest`: Unknown quality profile target
|
||||
or missing deviceProfile and videoQuality.
|
||||
:exc:`~plexapi.exceptions.BadRequest`: Unknown location ID.
|
||||
|
||||
Example:
|
||||
Optimize for Mobile
|
||||
item.optimize(targetTagID="Mobile") or item.optimize(targetTagID=1")
|
||||
Optimize for Android 10 MBPS 1080p
|
||||
item.optimize(deviceProfile="Android", videoQuality=10)
|
||||
Optimize for IOS Original Quality
|
||||
item.optimize(deviceProfile="IOS", videoQuality=-1)
|
||||
|
||||
* see sync.py VIDEO_QUALITIES for additional information for using videoQuality
|
||||
.. code-block:: python
|
||||
|
||||
# Optimize for mobile using defaults
|
||||
video.optimize(target="mobile")
|
||||
|
||||
# Optimize for Android at 10 Mbps 1080p
|
||||
from plexapi.sync import VIDEO_QUALITY_10_MBPS_1080p
|
||||
video.optimize(deviceProfile="Android", videoQuality=sync.VIDEO_QUALITY_10_MBPS_1080p)
|
||||
|
||||
# Optimize for iOS at original quality in library location
|
||||
from plexapi.sync import VIDEO_QUALITY_ORIGINAL
|
||||
locations = plex.library.section("Movies")._locations()
|
||||
video.optimize(deviceProfile="iOS", videoQuality=VIDEO_QUALITY_ORIGINAL, locationID=locations[0])
|
||||
|
||||
# Optimize for tv the next 5 unwatched episodes
|
||||
show.optimize(target="tv", limit=5, unwatched=True)
|
||||
|
||||
"""
|
||||
tagValues = [1, 2, 3]
|
||||
tagKeys = ["Mobile", "TV", "Original Quality"]
|
||||
tagIDs = tagKeys + tagValues
|
||||
|
||||
if targetTagID not in tagIDs and (deviceProfile is None or videoQuality is None):
|
||||
raise BadRequest('Unexpected or missing quality profile.')
|
||||
|
||||
libraryLocationIDs = [location.id for location in self.section()._locations()]
|
||||
libraryLocationIDs.append(-1)
|
||||
|
||||
if locationID not in libraryLocationIDs:
|
||||
raise BadRequest('Unexpected library path ID. %s not in %s' %
|
||||
(locationID, libraryLocationIDs))
|
||||
|
||||
if isinstance(targetTagID, str):
|
||||
tagIndex = tagKeys.index(targetTagID)
|
||||
targetTagID = tagValues[tagIndex]
|
||||
|
||||
if title is None:
|
||||
title = self.title
|
||||
from plexapi.library import Location
|
||||
from plexapi.sync import Policy, MediaSettings
|
||||
|
||||
backgroundProcessing = self.fetchItem('/playlists?type=42')
|
||||
key = '%s/items?' % backgroundProcessing.key
|
||||
key = f'{backgroundProcessing.key}/items'
|
||||
|
||||
tags = {t.tag.lower(): t.id for t in self._server.library.tags('mediaProcessingTarget')}
|
||||
# Additional keys for shorthand values
|
||||
tags['mobile'] = tags['optimized for mobile']
|
||||
tags['tv'] = tags['optimized for tv']
|
||||
tags['original'] = tags['original quality']
|
||||
|
||||
targetTagID = tags.get(target.lower(), '')
|
||||
if not targetTagID and (not deviceProfile or videoQuality is None):
|
||||
raise BadRequest('Unknown quality profile target or missing deviceProfile and videoQuality.')
|
||||
if targetTagID:
|
||||
target = ''
|
||||
elif deviceProfile and not target:
|
||||
target = f'Custom: {deviceProfile}'
|
||||
|
||||
section = self.section()
|
||||
libraryLocationIDs = [-1] + [location.id for location in section._locations()]
|
||||
if isinstance(locationID, Location):
|
||||
locationID = locationID.id
|
||||
if locationID not in libraryLocationIDs:
|
||||
raise BadRequest(f'Unknown location ID "{locationID}" not in {libraryLocationIDs}')
|
||||
|
||||
if isinstance(self, (Show, Season)):
|
||||
uri = f'library:///directory/{quote_plus(f"{self.key}/children")}'
|
||||
else:
|
||||
uri = f'library://{section.uuid}/item/{quote_plus(self.key)}'
|
||||
|
||||
policy = Policy.create(limit, unwatched)
|
||||
|
||||
params = {
|
||||
'Item[type]': 42,
|
||||
'Item[title]': title or self._defaultSyncTitle(),
|
||||
'Item[target]': target,
|
||||
'Item[targetTagID]': targetTagID if targetTagID else '',
|
||||
'Item[targetTagID]': targetTagID,
|
||||
'Item[locationID]': locationID,
|
||||
'Item[Policy][scope]': policyScope,
|
||||
'Item[Policy][value]': policyValue,
|
||||
'Item[Policy][unwatched]': policyUnwatched
|
||||
'Item[Location][uri]': uri,
|
||||
'Item[Policy][scope]': policy.scope,
|
||||
'Item[Policy][value]': str(policy.value),
|
||||
'Item[Policy][unwatched]': str(int(policy.unwatched)),
|
||||
}
|
||||
|
||||
if deviceProfile:
|
||||
params['Item[Device][profile]'] = deviceProfile
|
||||
|
||||
if videoQuality:
|
||||
from plexapi.sync import MediaSettings
|
||||
mediaSettings = MediaSettings.createVideo(videoQuality)
|
||||
params['Item[MediaSettings][videoQuality]'] = mediaSettings.videoQuality
|
||||
params['Item[MediaSettings][videoResolution]'] = mediaSettings.videoResolution
|
||||
|
@ -227,14 +242,11 @@ class Video(PlexPartialObject):
|
|||
params['Item[MediaSettings][subtitleSize]'] = ''
|
||||
params['Item[MediaSettings][musicBitrate]'] = ''
|
||||
params['Item[MediaSettings][photoQuality]'] = ''
|
||||
params['Item[MediaSettings][photoResolution]'] = ''
|
||||
|
||||
titleParam = {'Item[title]': title}
|
||||
section = self._server.library.sectionByID(self.librarySectionID)
|
||||
params['Item[Location][uri]'] = 'library://' + section.uuid + '/item/' + \
|
||||
quote_plus(self.key + '?includeExternalMedia=1')
|
||||
|
||||
data = key + urlencode(params) + '&' + urlencode(titleParam)
|
||||
return self._server.query(data, method=self._server._session.put)
|
||||
url = key + utils.joinArgs(params)
|
||||
self._server.query(url, method=self._server._session.put)
|
||||
return self
|
||||
|
||||
def sync(self, videoQuality, client=None, clientId=None, limit=None, unwatched=False, title=None):
|
||||
""" Add current video (movie, tv-show, season or episode) as sync item for specified device.
|
||||
|
@ -267,7 +279,7 @@ class Video(PlexPartialObject):
|
|||
|
||||
section = self._server.library.sectionByID(self.librarySectionID)
|
||||
|
||||
sync_item.location = 'library://%s/item/%s' % (section.uuid, quote_plus(self.key))
|
||||
sync_item.location = f'library://{section.uuid}/item/{quote_plus(self.key)}'
|
||||
sync_item.policy = Policy.create(limit, unwatched)
|
||||
sync_item.mediaSettings = MediaSettings.createVideo(videoQuality)
|
||||
|
||||
|
@ -279,7 +291,7 @@ class Movie(
|
|||
Video, Playable,
|
||||
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
|
||||
ArtMixin, PosterMixin, ThemeMixin,
|
||||
ContentRatingMixin, OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, StudioMixin,
|
||||
ContentRatingMixin, EditionTitleMixin, OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, StudioMixin,
|
||||
SummaryMixin, TaglineMixin, TitleMixin,
|
||||
CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin,
|
||||
WatchlistMixin
|
||||
|
@ -298,6 +310,7 @@ class Movie(
|
|||
countries (List<:class:`~plexapi.media.Country`>): List of countries objects.
|
||||
directors (List<:class:`~plexapi.media.Director`>): List of director objects.
|
||||
duration (int): Duration of the movie in milliseconds.
|
||||
editionTitle (str): The edition title of the movie (e.g. Director's Cut, Extended Edition, etc.).
|
||||
genres (List<:class:`~plexapi.media.Genre`>): List of genre objects.
|
||||
guids (List<:class:`~plexapi.media.Guid`>): List of guid objects.
|
||||
labels (List<:class:`~plexapi.media.Label`>): List of label objects.
|
||||
|
@ -338,6 +351,7 @@ class Movie(
|
|||
self.countries = self.findItems(data, media.Country)
|
||||
self.directors = self.findItems(data, media.Director)
|
||||
self.duration = utils.cast(int, data.attrib.get('duration'))
|
||||
self.editionTitle = data.attrib.get('editionTitle')
|
||||
self.genres = self.findItems(data, media.Genre)
|
||||
self.guids = self.findItems(data, media.Guid)
|
||||
self.labels = self.findItems(data, media.Label)
|
||||
|
@ -381,13 +395,23 @@ class Movie(
|
|||
|
||||
def _prettyfilename(self):
|
||||
""" Returns a filename for use in download. """
|
||||
return '%s (%s)' % (self.title, self.year)
|
||||
return f'{self.title} ({self.year})'
|
||||
|
||||
def reviews(self):
|
||||
""" Returns a list of :class:`~plexapi.media.Review` objects. """
|
||||
data = self._server.query(self._details_key)
|
||||
return self.findItems(data, media.Review, rtag='Video')
|
||||
|
||||
def editions(self):
|
||||
""" Returns a list of :class:`~plexapi.video.Movie` objects
|
||||
for other editions of the same movie.
|
||||
"""
|
||||
filters = {
|
||||
'guid': self.guid,
|
||||
'id!': self.ratingKey
|
||||
}
|
||||
return self.section().search(filters=filters)
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Show(
|
||||
|
@ -499,8 +523,8 @@ class Show(
|
|||
return self.roles
|
||||
|
||||
@property
|
||||
def isWatched(self):
|
||||
""" Returns True if the show is fully watched. """
|
||||
def isPlayed(self):
|
||||
""" Returns True if the show is fully played. """
|
||||
return bool(self.viewedLeafCount == self.leafCount)
|
||||
|
||||
def onDeck(self):
|
||||
|
@ -520,7 +544,7 @@ class Show(
|
|||
Raises:
|
||||
:exc:`~plexapi.exceptions.BadRequest`: If title or season parameter is missing.
|
||||
"""
|
||||
key = '/library/metadata/%s/children?excludeAllLeaves=1' % self.ratingKey
|
||||
key = f'{self.key}/children?excludeAllLeaves=1'
|
||||
if title is not None and not isinstance(title, int):
|
||||
return self.fetchItem(key, Season, title__iexact=title)
|
||||
elif season is not None or isinstance(title, int):
|
||||
|
@ -533,8 +557,8 @@ class Show(
|
|||
|
||||
def seasons(self, **kwargs):
|
||||
""" Returns a list of :class:`~plexapi.video.Season` objects in the show. """
|
||||
key = '/library/metadata/%s/children?excludeAllLeaves=1' % self.ratingKey
|
||||
return self.fetchItems(key, Season, **kwargs)
|
||||
key = f'{self.key}/children?excludeAllLeaves=1'
|
||||
return self.fetchItems(key, Season, container_size=self.childCount, **kwargs)
|
||||
|
||||
def episode(self, title=None, season=None, episode=None):
|
||||
""" Find a episode using a title or season and episode.
|
||||
|
@ -547,7 +571,7 @@ class Show(
|
|||
Raises:
|
||||
:exc:`~plexapi.exceptions.BadRequest`: If title or season and episode parameters are missing.
|
||||
"""
|
||||
key = '/library/metadata/%s/allLeaves' % self.ratingKey
|
||||
key = f'{self.key}/allLeaves'
|
||||
if title is not None:
|
||||
return self.fetchItem(key, Episode, title__iexact=title)
|
||||
elif season is not None and episode is not None:
|
||||
|
@ -556,7 +580,7 @@ class Show(
|
|||
|
||||
def episodes(self, **kwargs):
|
||||
""" Returns a list of :class:`~plexapi.video.Episode` objects in the show. """
|
||||
key = '/library/metadata/%s/allLeaves' % self.ratingKey
|
||||
key = f'{self.key}/allLeaves'
|
||||
return self.fetchItems(key, Episode, **kwargs)
|
||||
|
||||
def get(self, title=None, season=None, episode=None):
|
||||
|
@ -583,7 +607,7 @@ class Show(
|
|||
"""
|
||||
filepaths = []
|
||||
for episode in self.episodes():
|
||||
_savepath = os.path.join(savepath, 'Season %s' % str(episode.seasonNumber).zfill(2)) if subfolders else savepath
|
||||
_savepath = os.path.join(savepath, f'Season {str(episode.seasonNumber).zfill(2)}') if subfolders else savepath
|
||||
filepaths += episode.download(_savepath, keep_original_name, **kwargs)
|
||||
return filepaths
|
||||
|
||||
|
@ -647,15 +671,17 @@ class Season(
|
|||
yield episode
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s>' % ':'.join([p for p in [
|
||||
self.__class__.__name__,
|
||||
self.key.replace('/library/metadata/', '').replace('/children', ''),
|
||||
'%s-s%s' % (self.parentTitle.replace(' ', '-')[:20], self.seasonNumber),
|
||||
] if p])
|
||||
return '<{}>'.format(
|
||||
':'.join([p for p in [
|
||||
self.__class__.__name__,
|
||||
self.key.replace('/library/metadata/', '').replace('/children', ''),
|
||||
f"{self.parentTitle.replace(' ', '-')[:20]}-{self.seasonNumber}",
|
||||
] if p])
|
||||
)
|
||||
|
||||
@property
|
||||
def isWatched(self):
|
||||
""" Returns True if the season is fully watched. """
|
||||
def isPlayed(self):
|
||||
""" Returns True if the season is fully played. """
|
||||
return bool(self.viewedLeafCount == self.leafCount)
|
||||
|
||||
@property
|
||||
|
@ -665,7 +691,7 @@ class Season(
|
|||
|
||||
def episodes(self, **kwargs):
|
||||
""" Returns a list of :class:`~plexapi.video.Episode` objects in the season. """
|
||||
key = '/library/metadata/%s/children' % self.ratingKey
|
||||
key = f'{self.key}/children'
|
||||
return self.fetchItems(key, Episode, **kwargs)
|
||||
|
||||
def episode(self, title=None, episode=None):
|
||||
|
@ -678,7 +704,7 @@ class Season(
|
|||
Raises:
|
||||
:exc:`~plexapi.exceptions.BadRequest`: If title or episode parameter is missing.
|
||||
"""
|
||||
key = '/library/metadata/%s/children' % self.ratingKey
|
||||
key = f'{self.key}/children'
|
||||
if title is not None and not isinstance(title, int):
|
||||
return self.fetchItem(key, Episode, title__iexact=title)
|
||||
elif episode is not None or isinstance(title, int):
|
||||
|
@ -728,7 +754,7 @@ class Season(
|
|||
|
||||
def _defaultSyncTitle(self):
|
||||
""" Returns str, default title for a new syncItem. """
|
||||
return '%s - %s' % (self.parentTitle, self.title)
|
||||
return f'{self.parentTitle} - {self.title}'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
|
@ -835,18 +861,20 @@ class Episode(
|
|||
if not self.parentRatingKey and self.grandparentRatingKey:
|
||||
self.parentRatingKey = self.show().season(season=self.parentIndex).ratingKey
|
||||
if self.parentRatingKey:
|
||||
self.parentKey = '/library/metadata/%s' % self.parentRatingKey
|
||||
self.parentKey = f'/library/metadata/{self.parentRatingKey}'
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s>' % ':'.join([p for p in [
|
||||
self.__class__.__name__,
|
||||
self.key.replace('/library/metadata/', '').replace('/children', ''),
|
||||
'%s-%s' % (self.grandparentTitle.replace(' ', '-')[:20], self.seasonEpisode),
|
||||
] if p])
|
||||
return '<{}>'.format(
|
||||
':'.join([p for p in [
|
||||
self.__class__.__name__,
|
||||
self.key.replace('/library/metadata/', '').replace('/children', ''),
|
||||
f"{self.grandparentTitle.replace(' ', '-')[:20]}-{self.seasonEpisode}",
|
||||
] if p])
|
||||
)
|
||||
|
||||
def _prettyfilename(self):
|
||||
""" Returns a filename for use in download. """
|
||||
return '%s - %s - %s' % (self.grandparentTitle, self.seasonEpisode, self.title)
|
||||
return f'{self.grandparentTitle} - {self.seasonEpisode} - {self.title}'
|
||||
|
||||
@property
|
||||
def actors(self):
|
||||
|
@ -878,7 +906,7 @@ class Episode(
|
|||
@property
|
||||
def seasonEpisode(self):
|
||||
""" Returns the s00e00 string containing the season and episode numbers. """
|
||||
return 's%se%s' % (str(self.seasonNumber).zfill(2), str(self.episodeNumber).zfill(2))
|
||||
return f's{str(self.seasonNumber).zfill(2)}e{str(self.episodeNumber).zfill(2)}'
|
||||
|
||||
@property
|
||||
def hasCommercialMarker(self):
|
||||
|
@ -905,7 +933,7 @@ class Episode(
|
|||
|
||||
def _defaultSyncTitle(self):
|
||||
""" Returns str, default title for a new syncItem. """
|
||||
return '%s - %s - (%s) %s' % (self.grandparentTitle, self.parentTitle, self.seasonEpisode, self.title)
|
||||
return f'{self.grandparentTitle} - {self.parentTitle} - ({self.seasonEpisode}) {self.title}'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
|
@ -979,4 +1007,43 @@ class Extra(Clip):
|
|||
|
||||
def _prettyfilename(self):
|
||||
""" Returns a filename for use in download. """
|
||||
return '%s (%s)' % (self.title, self.subtype)
|
||||
return f'{self.title} ({self.subtype})'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class MovieSession(PlexSession, Movie):
|
||||
""" Represents a single Movie session
|
||||
loaded from :func:`~plexapi.server.PlexServer.sessions`.
|
||||
"""
|
||||
_SESSIONTYPE = True
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
Movie._loadData(self, data)
|
||||
PlexSession._loadData(self, data)
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class EpisodeSession(PlexSession, Episode):
|
||||
""" Represents a single Episode session
|
||||
loaded from :func:`~plexapi.server.PlexServer.sessions`.
|
||||
"""
|
||||
_SESSIONTYPE = True
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
Episode._loadData(self, data)
|
||||
PlexSession._loadData(self, data)
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class ClipSession(PlexSession, Clip):
|
||||
""" Represents a single Clip session
|
||||
loaded from :func:`~plexapi.server.PlexServer.sessions`.
|
||||
"""
|
||||
_SESSIONTYPE = True
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
Clip._loadData(self, data)
|
||||
PlexSession._loadData(self, data)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue