Update plexapi==4.8.0

This commit is contained in:
JonnyWong16 2021-11-28 14:17:35 -08:00
commit 3a50981976
No known key found for this signature in database
GPG key ID: B1F1F9807184697A
20 changed files with 522 additions and 314 deletions

View file

@ -29,14 +29,18 @@ class AlertListener(threading.Thread):
callback (func): Callback function to call on received messages. The callback function
will be sent a single argument 'data' which will contain a dictionary of data
received from the server. :samp:`def my_callback(data): ...`
callbackError (func): Callback function to call on errors. The callback function
will be sent a single argument 'error' which will contain the Error object.
:samp:`def my_callback(error): ...`
"""
key = '/:/websockets/notifications'
def __init__(self, server, callback=None):
def __init__(self, server, callback=None, callbackError=None):
super(AlertListener, self).__init__()
self.daemon = True
self._server = server
self._callback = callback
self._callbackError = callbackError
self._ws = None
def run(self):
@ -84,4 +88,9 @@ class AlertListener(threading.Thread):
This is to support compatibility with current and previous releases of websocket-client.
"""
err = args[-1]
log.error('AlertListener Error: %s', err)
try:
log.error('AlertListener Error: %s', err)
if self._callbackError:
self._callbackError(err)
except Exception as err: # pragma: no cover
log.error('AlertListener Error: Error: %s', err)

View file

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
import os
from urllib.parse import quote_plus
from plexapi import library, media, utils
@ -205,23 +206,20 @@ class Artist(Audio, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, S
""" Alias of :func:`~plexapi.audio.Artist.track`. """
return self.track(title, album, track)
def download(self, savepath=None, keep_original_name=False, **kwargs):
""" Downloads all tracks for the artist to the specified location.
def download(self, savepath=None, keep_original_name=False, subfolders=False, **kwargs):
""" Download all tracks from the artist. See :func:`~plexapi.base.Playable.download` for details.
Parameters:
savepath (str): Title of the track to return.
keep_original_name (bool): Set True to keep the original filename as stored in
the Plex server. False will create a new filename with the format
"<Atrist> - <Album> <Track>".
kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL` will
be returned and the additional arguments passed in will be sent to that
function. If kwargs is not specified, the media items will be downloaded
and saved to disk.
savepath (str): Defaults to current working dir.
keep_original_name (bool): True to keep the original filename otherwise
a friendlier filename is generated.
subfolders (bool): True to separate tracks in to album folders.
**kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL`.
"""
filepaths = []
for album in self.albums():
for track in album.tracks():
filepaths += track.download(savepath, keep_original_name, **kwargs)
for track in self.tracks():
_savepath = os.path.join(savepath, track.parentTitle) if subfolders else savepath
filepaths += track.download(_savepath, keep_original_name, **kwargs)
return filepaths
@ -314,17 +312,13 @@ class Album(Audio, ArtMixin, PosterMixin, RatingMixin, UnmatchMatchMixin,
return self.fetchItem(self.parentKey)
def download(self, savepath=None, keep_original_name=False, **kwargs):
""" Downloads all tracks for the artist to the specified location.
""" Download all tracks from the album. See :func:`~plexapi.base.Playable.download` for details.
Parameters:
savepath (str): Title of the track to return.
keep_original_name (bool): Set True to keep the original filename as stored in
the Plex server. False will create a new filename with the format
"<Atrist> - <Album> <Track>".
kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL` will
be returned and the additional arguments passed in will be sent to that
function. If kwargs is not specified, the media items will be downloaded
and saved to disk.
savepath (str): Defaults to current working dir.
keep_original_name (bool): True to keep the original filename otherwise
a friendlier filename is generated.
**kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL`.
"""
filepaths = []
for track in self.tracks():
@ -398,7 +392,8 @@ class Track(Audio, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixin,
def _prettyfilename(self):
""" Returns a filename for use in download. """
return '%s - %s %s' % (self.grandparentTitle, self.parentTitle, self.title)
return '%s - %s - %s - %s' % (
self.grandparentTitle, self.parentTitle, str(self.trackNumber).zfill(2), self.title)
def album(self):
""" Return the track's :class:`~plexapi.audio.Album`. """

View file

@ -681,34 +681,50 @@ class Playable(object):
client.playMedia(self)
def download(self, savepath=None, keep_original_name=False, **kwargs):
""" Downloads this items media to the specified location. Returns a list of
""" Downloads the media item to the specified location. Returns a list of
filepaths that have been saved to disk.
Parameters:
savepath (str): Title of the track to return.
keep_original_name (bool): Set True to keep the original filename as stored in
the Plex server. False will create a new filename with the format
"<Artist> - <Album> <Track>".
kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL` will
be returned and the additional arguments passed in will be sent to that
function. If kwargs is not specified, the media items will be downloaded
and saved to disk.
savepath (str): Defaults to current working dir.
keep_original_name (bool): True to keep the original filename otherwise
a friendlier filename is generated. See filenames below.
**kwargs (dict): Additional options passed into :func:`~plexapi.audio.Track.getStreamURL`
to download a transcoded stream, otherwise the media item will be downloaded
as-is and saved to disk.
**Filenames**
* Movie: ``<title> (<year>)``
* Episode: ``<show title> - s00e00 - <episode title>``
* Track: ``<artist title> - <album title> - 00 - <track title>``
* Photo: ``<photoalbum title> - <photo/clip title>`` or ``<photo/clip title>``
"""
filepaths = []
locations = [i for i in self.iterParts() if i]
for location in locations:
filename = location.file
if keep_original_name is False:
filename = '%s.%s' % (self._prettyfilename(), location.container)
# So this seems to be a alot slower but allows transcode.
parts = [i for i in self.iterParts() if i]
for part in parts:
if not keep_original_name:
filename = utils.cleanFilename('%s.%s' % (self._prettyfilename(), part.container))
else:
filename = part.file
if kwargs:
# So this seems to be a alot slower but allows transcode.
download_url = self.getStreamURL(**kwargs)
else:
download_url = self._server.url('%s?download=1' % location.key)
filepath = utils.download(download_url, self._server._token, filename=filename,
savepath=savepath, session=self._server._session)
download_url = self._server.url('%s?download=1' % part.key)
filepath = utils.download(
download_url,
self._server._token,
filename=filename,
savepath=savepath,
session=self._server._session
)
if filepath:
filepaths.append(filepath)
return filepaths
def stop(self, reason=''):

View file

@ -3,7 +3,7 @@
# Library version
MAJOR_VERSION = 4
MINOR_VERSION = 7
PATCH_VERSION = 2
MINOR_VERSION = 8
PATCH_VERSION = 0
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}"

View file

@ -26,47 +26,61 @@ class Library(PlexObject):
def _loadData(self, data):
self._data = data
self._sectionsByID = {} # cached Section UUIDs
self.identifier = data.attrib.get('identifier')
self.mediaTagVersion = data.attrib.get('mediaTagVersion')
self.title1 = data.attrib.get('title1')
self.title2 = data.attrib.get('title2')
self._sectionsByID = {} # cached sections by key
self._sectionsByTitle = {} # cached sections by title
def _loadSections(self):
""" Loads and caches all the library sections. """
key = '/library/sections'
self._sectionsByID = {}
self._sectionsByTitle = {}
for elem in self._server.query(key):
for cls in (MovieSection, ShowSection, MusicSection, PhotoSection):
if elem.attrib.get('type') == cls.TYPE:
section = cls(self._server, elem, key)
self._sectionsByID[section.key] = section
self._sectionsByTitle[section.title.lower()] = section
def sections(self):
""" Returns a list of all media sections in this library. Library sections may be any of
:class:`~plexapi.library.MovieSection`, :class:`~plexapi.library.ShowSection`,
:class:`~plexapi.library.MusicSection`, :class:`~plexapi.library.PhotoSection`.
"""
key = '/library/sections'
sections = []
for elem in self._server.query(key):
for cls in (MovieSection, ShowSection, MusicSection, PhotoSection):
if elem.attrib.get('type') == cls.TYPE:
section = cls(self._server, elem, key)
self._sectionsByID[section.key] = section
sections.append(section)
return sections
self._loadSections()
return list(self._sectionsByID.values())
def section(self, title=None):
def section(self, title):
""" Returns the :class:`~plexapi.library.LibrarySection` that matches the specified title.
Parameters:
title (str): Title of the section to return.
"""
for section in self.sections():
if section.title.lower() == title.lower():
return section
raise NotFound('Invalid library section: %s' % title)
if not self._sectionsByTitle or title not in self._sectionsByTitle:
self._loadSections()
try:
return self._sectionsByTitle[title.lower()]
except KeyError:
raise NotFound('Invalid library section: %s' % title) from None
def sectionByID(self, sectionID):
""" Returns the :class:`~plexapi.library.LibrarySection` that matches the specified sectionID.
Parameters:
sectionID (int): ID of the section to return.
Raises:
:exc:`~plexapi.exceptions.NotFound`: The library section ID is not found on the server.
"""
if not self._sectionsByID or sectionID not in self._sectionsByID:
self.sections()
return self._sectionsByID[sectionID]
self._loadSections()
try:
return self._sectionsByID[sectionID]
except KeyError:
raise NotFound('Invalid library sectionID: %s' % sectionID) from None
def all(self, **kwargs):
""" Returns a list of all media from all library sections.
@ -356,6 +370,9 @@ class LibrarySection(PlexObject):
self._filterTypes = None
self._fieldTypes = None
self._totalViewSize = None
self._totalSize = None
self._totalDuration = None
self._totalStorage = None
def fetchItems(self, ekey, cls=None, container_start=None, container_size=None, **kwargs):
""" Load the specified key to find and build all items with the specified tag
@ -394,7 +411,36 @@ class LibrarySection(PlexObject):
@property
def totalSize(self):
""" Returns the total number of items in the library for the default library type. """
return self.totalViewSize(includeCollections=False)
if self._totalSize is None:
self._totalSize = self.totalViewSize(includeCollections=False)
return self._totalSize
@property
def totalDuration(self):
""" Returns the total duration (in milliseconds) of items in the library. """
if self._totalDuration is None:
self._getTotalDurationStorage()
return self._totalDuration
@property
def totalStorage(self):
""" Returns the total storage (in bytes) of items in the library. """
if self._totalStorage is None:
self._getTotalDurationStorage()
return self._totalStorage
def _getTotalDurationStorage(self):
""" Queries the Plex server for the total library duration and storage and caches the values. """
data = self._server.query('/media/providers?includeStorage=1')
xpath = (
'./MediaProvider[@identifier="com.plexapp.plugins.library"]'
'/Feature[@type="content"]'
'/Directory[@id="%s"]'
) % self.key
directory = next(iter(data.findall(xpath)), None)
if directory:
self._totalDuration = utils.cast(int, directory.attrib.get('durationTotal'))
self._totalStorage = utils.cast(int, directory.attrib.get('storageTotal'))
def totalViewSize(self, libtype=None, includeCollections=True):
""" Returns the total number of items in the library for a specified libtype.
@ -432,8 +478,12 @@ class LibrarySection(PlexObject):
log.error(msg)
raise
def reload(self, key=None):
return self._server.library.section(self.title)
def reload(self):
""" Reload the data for the library section. """
self._server.library._loadSections()
newLibrary = self._server.library.sectionByID(self.key)
self.__dict__.update(newLibrary.__dict__)
return self
def edit(self, agent=None, **kwargs):
""" Edit a library (Note: agent is required). See :class:`~plexapi.library.Library` for example usage.
@ -446,11 +496,6 @@ class LibrarySection(PlexObject):
part = '/library/sections/%s?agent=%s&%s' % (self.key, agent, urlencode(kwargs))
self._server.query(part, method=self._server._session.put)
# Reload this way since the self.key dont have a full path, but is simply a id.
for s in self._server.library.sections():
if s.key == self.key:
return s
def get(self, title):
""" Returns the media item with the specified title.

View file

@ -79,13 +79,16 @@ class Media(PlexObject):
self.make = data.attrib.get('make')
self.model = data.attrib.get('model')
parent = self._parent()
self._parentKey = parent.key
@property
def isOptimizedVersion(self):
""" Returns True if the media is a Plex optimized version. """
return self.proxyType == utils.SEARCHTYPES['optimizedVersion']
def delete(self):
part = self._initpath + '/media/%s' % self.id
part = '%s/media/%s' % (self._parentKey, self.id)
try:
return self._server.query(part, method=self._server._session.delete)
except BadRequest:

View file

@ -70,9 +70,6 @@ class MyPlexAccount(PlexObject):
PLEXSERVERS = 'https://plex.tv/api/servers/{machineId}' # get
FRIENDUPDATE = 'https://plex.tv/api/friends/{userId}' # put with args, delete
REMOVEHOMEUSER = 'https://plex.tv/api/home/users/{userId}' # delete
REMOVEINVITE = 'https://plex.tv/api/invites/requested/{userId}?friend=1&server=1&home=1' # delete
REQUESTED = 'https://plex.tv/api/invites/requested' # get
REQUESTS = 'https://plex.tv/api/invites/requests' # get
SIGNIN = 'https://plex.tv/users/sign_in.xml' # get with auth
WEBHOOKS = 'https://plex.tv/api/v2/user/webhooks' # get, post with data
OPTOUTS = 'https://plex.tv/api/v2/user/%(userUUID)s/settings/opt_outs' # get
@ -365,26 +362,55 @@ class MyPlexAccount(PlexObject):
return self.query(url, self._session.post, headers=headers)
def removeFriend(self, user):
""" Remove the specified user from all sharing.
""" Remove the specified user from your friends.
Parameters:
user (str): MyPlexUser, username, email of the user to be added.
user (str): :class:`~plexapi.myplex.MyPlexUser`, username, or email of the user to be removed.
"""
user = self.user(user)
url = self.FRIENDUPDATE if user.friend else self.REMOVEINVITE
url = url.format(userId=user.id)
user = user if isinstance(user, MyPlexUser) else self.user(user)
url = self.FRIENDUPDATE.format(userId=user.id)
return self.query(url, self._session.delete)
def removeHomeUser(self, user):
""" Remove the specified managed user from home.
""" Remove the specified user from your home users.
Parameters:
user (str): MyPlexUser, username, email of the user to be removed from home.
user (str): :class:`~plexapi.myplex.MyPlexUser`, username, or email of the user to be removed.
"""
user = self.user(user)
user = user if isinstance(user, MyPlexUser) else self.user(user)
url = self.REMOVEHOMEUSER.format(userId=user.id)
return self.query(url, self._session.delete)
def acceptInvite(self, user):
""" Accept a pending firend invite from the specified user.
Parameters:
user (str): :class:`~plexapi.myplex.MyPlexInvite`, username, or email of the friend invite to accept.
"""
invite = user if isinstance(user, MyPlexInvite) else self.pendingInvite(user, includeSent=False)
params = {
'friend': int(invite.friend),
'home': int(invite.home),
'server': int(invite.server)
}
url = MyPlexInvite.REQUESTS + '/%s' % invite.id + utils.joinArgs(params)
return self.query(url, self._session.put)
def cancelInvite(self, user):
""" Cancel a pending firend invite for the specified user.
Parameters:
user (str): :class:`~plexapi.myplex.MyPlexInvite`, username, or email of the friend invite to cancel.
"""
invite = user if isinstance(user, MyPlexInvite) else self.pendingInvite(user, includeReceived=False)
params = {
'friend': int(invite.friend),
'home': int(invite.home),
'server': int(invite.server)
}
url = MyPlexInvite.REQUESTED + '/%s' % invite.id + utils.joinArgs(params)
return self.query(url, self._session.delete)
def updateFriend(self, user, server, sections=None, removeSections=False, allowSync=None, allowCameraUpload=None,
allowChannels=None, filterMovies=None, filterTelevision=None, filterMusic=None):
""" Update the specified user's share settings.
@ -455,7 +481,7 @@ class MyPlexAccount(PlexObject):
return response_servers, response_filters
def user(self, username):
""" Returns the :class:`~plexapi.myplex.MyPlexUser` that matches the email or username specified.
""" Returns the :class:`~plexapi.myplex.MyPlexUser` that matches the specified username or email.
Parameters:
username (str): Username, email or id of the user to return.
@ -467,19 +493,50 @@ class MyPlexAccount(PlexObject):
return user
elif (user.username and user.email and user.id and username.lower() in
(user.username.lower(), user.email.lower(), str(user.id))):
(user.username.lower(), user.email.lower(), str(user.id))):
return user
raise NotFound('Unable to find user %s' % username)
def users(self):
""" Returns a list of all :class:`~plexapi.myplex.MyPlexUser` objects connected to your account.
This includes both friends and pending invites. You can reference the user.friend to
distinguish between the two.
"""
friends = [MyPlexUser(self, elem) for elem in self.query(MyPlexUser.key)]
requested = [MyPlexUser(self, elem, self.REQUESTED) for elem in self.query(self.REQUESTED)]
return friends + requested
elem = self.query(MyPlexUser.key)
return self.findItems(elem, cls=MyPlexUser)
def pendingInvite(self, username, includeSent=True, includeReceived=True):
""" Returns the :class:`~plexapi.myplex.MyPlexInvite` that matches the specified username or email.
Note: This can be a pending invite sent from your account or received to your account.
Parameters:
username (str): Username, email or id of the user to return.
includeSent (bool): True to include sent invites.
includeReceived (bool): True to include received invites.
"""
username = str(username)
for invite in self.pendingInvites(includeSent, includeReceived):
if (invite.username and invite.email and invite.id and username.lower() in
(invite.username.lower(), invite.email.lower(), str(invite.id))):
return invite
raise NotFound('Unable to find invite %s' % username)
def pendingInvites(self, includeSent=True, includeReceived=True):
""" Returns a list of all :class:`~plexapi.myplex.MyPlexInvite` objects connected to your account.
Note: This includes all pending invites sent from your account and received to your account.
Parameters:
includeSent (bool): True to include sent invites.
includeReceived (bool): True to include received invites.
"""
invites = []
if includeSent:
elem = self.query(MyPlexInvite.REQUESTED)
invites += self.findItems(elem, cls=MyPlexInvite)
if includeReceived:
elem = self.query(MyPlexInvite.REQUESTS)
invites += self.findItems(elem, cls=MyPlexInvite)
return invites
def _getSectionIds(self, server, sections):
""" Converts a list of section objects or names to sectionIds needed for library sharing. """
@ -731,10 +788,10 @@ class MyPlexUser(PlexObject):
protected (False): Unknown (possibly SSL enabled?).
recommendationsPlaylistId (str): Unknown.
restricted (str): Unknown.
servers (List<:class:`~plexapi.myplex.<MyPlexServerShare`>)): Servers shared with the user.
thumb (str): Link to the users avatar.
title (str): Seems to be an aliad for username.
username (str): User's username.
servers: Servers shared between user and friend
"""
TAG = 'User'
key = 'https://plex.tv/api/users/'
@ -796,6 +853,43 @@ class MyPlexUser(PlexObject):
return hist
class MyPlexInvite(PlexObject):
""" This object represents pending friend invites.
Attributes:
TAG (str): 'Invite'
createdAt (datetime): Datetime the user was invited.
email (str): User's email address (user@gmail.com).
friend (bool): True or False if the user is invited as a friend.
friendlyName (str): The user's friendly name.
home (bool): True or False if the user is invited to a Plex Home.
id (int): User's Plex account ID.
server (bool): True or False if the user is invited to any servers.
servers (List<:class:`~plexapi.myplex.<MyPlexServerShare`>)): Servers shared with the user.
thumb (str): Link to the users avatar.
username (str): User's username.
"""
TAG = 'Invite'
REQUESTS = 'https://plex.tv/api/invites/requests'
REQUESTED = 'https://plex.tv/api/invites/requested'
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
self._data = data
self.createdAt = utils.toDatetime(data.attrib.get('createdAt'))
self.email = data.attrib.get('email')
self.friend = utils.cast(bool, data.attrib.get('friend'))
self.friendlyName = data.attrib.get('friendlyName')
self.home = utils.cast(bool, data.attrib.get('home'))
self.id = utils.cast(int, data.attrib.get('id'))
self.server = utils.cast(bool, data.attrib.get('server'))
self.servers = self.findItems(data, MyPlexServerShare)
self.thumb = data.attrib.get('thumb')
self.username = data.attrib.get('username', '')
for server in self.servers:
server.accountID = self.id
class Section(PlexObject):
""" This refers to a shared section. The raw xml for the data presented here
can be found at: https://plex.tv/api/servers/{machineId}/shared_servers

View file

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
import os
from urllib.parse import quote_plus
from plexapi import media, utils, video
@ -107,34 +108,21 @@ class Photoalbum(PlexPartialObject, ArtMixin, PosterMixin, RatingMixin):
""" Alias to :func:`~plexapi.photo.Photoalbum.photo`. """
return self.episode(title)
def iterParts(self):
""" Iterates over the parts of the media item. """
for album in self.albums():
for photo in album.photos():
for part in photo.iterParts():
yield part
def download(self, savepath=None, keep_original_name=False, showstatus=False):
""" Download photo files to specified directory.
def download(self, savepath=None, keep_original_name=False, subfolders=False):
""" Download all photos and clips from the photo ablum. See :func:`~plexapi.base.Playable.download` for details.
Parameters:
savepath (str): Defaults to current working dir.
keep_original_name (bool): True to keep the original file name otherwise
a friendlier is generated.
showstatus(bool): Display a progressbar.
keep_original_name (bool): True to keep the original filename otherwise
a friendlier filename is generated.
subfolders (bool): True to separate photos/clips in to photo album folders.
"""
filepaths = []
locations = [i for i in self.iterParts() if i]
for location in locations:
name = location.file
if not keep_original_name:
title = self.title.replace(' ', '.')
name = '%s.%s' % (title, location.container)
url = self._server.url('%s?download=1' % location.key)
filepath = utils.download(url, self._server._token, filename=name, showstatus=showstatus,
savepath=savepath, session=self._server._session)
if filepath:
filepaths.append(filepath)
for album in self.albums():
_savepath = os.path.join(savepath, album.title) if subfolders else savepath
filepaths += album.download(_savepath, keep_original_name)
for photo in self.photos() + self.clips():
filepaths += photo.download(savepath, keep_original_name)
return filepaths
def _getWebURL(self, base=None):
@ -218,6 +206,12 @@ class Photo(PlexPartialObject, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixi
self.userRating = utils.cast(float, data.attrib.get('userRating'))
self.year = utils.cast(int, data.attrib.get('year'))
def _prettyfilename(self):
""" Returns a filename for use in download. """
if self.parentTitle:
return '%s - %s' % (self.parentTitle, self.title)
return self.title
def photoalbum(self):
""" Return the photo's :class:`~plexapi.photo.Photoalbum`. """
return self.fetchItem(self.parentKey)
@ -241,12 +235,6 @@ class Photo(PlexPartialObject, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixi
"""
return [part.file for item in self.media for part in item.parts if part]
def iterParts(self):
""" Iterates over the parts of the media item. """
for item in self.media:
for part in item.parts:
yield part
def sync(self, resolution, client=None, clientId=None, limit=None, title=None):
""" Add current photo as sync item for specified device.
See :func:`~plexapi.myplex.MyPlexAccount.sync` for possible exceptions.
@ -283,29 +271,6 @@ class Photo(PlexPartialObject, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixi
return myplex.sync(sync_item, client=client, clientId=clientId)
def download(self, savepath=None, keep_original_name=False, showstatus=False):
""" Download photo files to specified directory.
Parameters:
savepath (str): Defaults to current working dir.
keep_original_name (bool): True to keep the original file name otherwise
a friendlier is generated.
showstatus(bool): Display a progressbar.
"""
filepaths = []
locations = [i for i in self.iterParts() if i]
for location in locations:
name = location.file
if not keep_original_name:
title = self.title.replace(' ', '.')
name = '%s.%s' % (title, location.container)
url = self._server.url('%s?download=1' % location.key)
filepath = utils.download(url, self._server._token, filename=name, showstatus=showstatus,
savepath=savepath, session=self._server._session)
if filepath:
filepaths.append(filepath)
return filepaths
def _getWebURL(self, base=None):
""" Get the Plex Web URL with the correct parameters. """
return self._server._buildWebURL(base=base, endpoint='details', key=self.parentKey, legacy=1)

View file

@ -734,21 +734,46 @@ class PlexServer(PlexObject):
notifier.start()
return notifier
def transcodeImage(self, media, height, width, opacity=100, saturation=100):
""" Returns the URL for a transcoded image from the specified media object.
Returns None if no media specified (needed if user tries to pass thumb
or art directly).
def transcodeImage(self, imageUrl, height, width,
opacity=None, saturation=None, blur=None, background=None,
minSize=True, upscale=True, imageFormat=None):
""" Returns the URL for a transcoded image.
Parameters:
imageUrl (str): The URL to the image
(eg. returned by :func:`~plexapi.mixins.PosterUrlMixin.thumbUrl`
or :func:`~plexapi.mixins.ArtUrlMixin.artUrl`).
The URL can be an online image.
height (int): Height to transcode the image to.
width (int): Width to transcode the image to.
opacity (int): Opacity of the resulting image (possibly deprecated).
saturation (int): Saturating of the resulting image.
opacity (int, optional): Change the opacity of the image (0 to 100)
saturation (int, optional): Change the saturation of the image (0 to 100).
blur (int, optional): The blur to apply to the image in pixels (e.g. 3).
background (str, optional): The background hex colour to apply behind the opacity (e.g. '000000').
minSize (bool, optional): Maintain smallest dimension. Default True.
upscale (bool, optional): Upscale the image if required. Default True.
imageFormat (str, optional): 'jpeg' (default) or 'png'.
"""
if media:
transcode_url = '/photo/:/transcode?height=%s&width=%s&opacity=%s&saturation=%s&url=%s' % (
height, width, opacity, saturation, media)
return self.url(transcode_url, includeToken=True)
params = {
'url': imageUrl,
'height': height,
'width': width,
'minSize': int(bool(minSize)),
'upscale': int(bool(upscale))
}
if opacity is not None:
params['opacity'] = opacity
if saturation is not None:
params['saturation'] = saturation
if blur is not None:
params['blur'] = blur
if background is not None:
params['background'] = str(background).strip('#')
if imageFormat is not None:
params['format'] = imageFormat.lower()
key = '/photo/:/transcode%s' % utils.joinArgs(params)
return self.url(key, includeToken=True)
def url(self, key, includeToken=None):
""" Build a URL string with proper token argument. Token will be appended to the URL

View file

@ -4,7 +4,9 @@ import functools
import logging
import os
import re
import string
import time
import unicodedata
import warnings
import zipfile
from datetime import datetime
@ -251,6 +253,13 @@ def toList(value, itemcast=None, delim=','):
return [itemcast(item) for item in value.split(delim) if item != '']
def cleanFilename(filename, replace='_'):
whitelist = "-_.()[] {}{}".format(string.ascii_letters, string.digits)
cleaned_filename = unicodedata.normalize('NFKD', filename).encode('ASCII', 'ignore').decode()
cleaned_filename = ''.join(c if c in whitelist else replace for c in cleaned_filename)
return cleaned_filename
def downloadSessionImages(server, filename=None, height=150, width=150,
opacity=100, saturation=100): # pragma: no cover
""" Helper to download a bif image or thumb.url from plex.server.sessions.

View file

@ -357,8 +357,8 @@ class Movie(Video, Playable, AdvancedSettingsMixin, ArtMixin, PosterMixin, Ratin
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
""" Returns a filename for use in download. """
return '%s (%s)' % (self.title, self.year)
def reviews(self):
""" Returns a list of :class:`~plexapi.media.Review` objects. """
@ -375,32 +375,6 @@ class Movie(Video, Playable, AdvancedSettingsMixin, ArtMixin, PosterMixin, Ratin
data = self._server.query(self._details_key)
return self.findItems(data, library.Hub, rtag='Related')
def download(self, savepath=None, keep_original_name=False, **kwargs):
""" Download video files to specified directory.
Parameters:
savepath (str): Defaults to current working dir.
keep_original_name (bool): True to keep the original file name otherwise
a friendlier is generated.
**kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL`.
"""
filepaths = []
locations = [i for i in self.iterParts() if i]
for location in locations:
name = location.file
if not keep_original_name:
title = self.title.replace(' ', '.')
name = '%s.%s' % (title, location.container)
if kwargs is not None:
url = self.getStreamURL(**kwargs)
else:
self._server.url('%s?download=1' % location.key)
filepath = utils.download(url, self._server._token, filename=name,
savepath=savepath, session=self._server._session)
if filepath:
filepaths.append(filepath)
return filepaths
@utils.registerPlexObject
class Show(Video, AdvancedSettingsMixin, ArtMixin, BannerMixin, PosterMixin, RatingMixin, SplitMergeMixin, UnmatchMatchMixin,
@ -582,18 +556,20 @@ class Show(Video, AdvancedSettingsMixin, ArtMixin, BannerMixin, PosterMixin, Rat
""" Returns list of unwatched :class:`~plexapi.video.Episode` objects. """
return self.episodes(viewCount=0)
def download(self, savepath=None, keep_original_name=False, **kwargs):
""" Download video files to specified directory.
def download(self, savepath=None, keep_original_name=False, subfolders=False, **kwargs):
""" Download all episodes from the show. See :func:`~plexapi.base.Playable.download` for details.
Parameters:
savepath (str): Defaults to current working dir.
keep_original_name (bool): True to keep the original file name otherwise
a friendlier is generated.
keep_original_name (bool): True to keep the original filename otherwise
a friendlier filename is generated.
subfolders (bool): True to separate episodes in to season folders.
**kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL`.
"""
filepaths = []
for episode in self.episodes():
filepaths += episode.download(savepath, keep_original_name, **kwargs)
_savepath = os.path.join(savepath, 'Season %s' % str(episode.seasonNumber).zfill(2)) if subfolders else savepath
filepaths += episode.download(_savepath, keep_original_name, **kwargs)
return filepaths
@ -714,12 +690,12 @@ class Season(Video, ArtMixin, PosterMixin, RatingMixin, CollectionMixin):
return self.episodes(viewCount=0)
def download(self, savepath=None, keep_original_name=False, **kwargs):
""" Download video files to specified directory.
""" Download all episodes from the season. See :func:`~plexapi.base.Playable.download` for details.
Parameters:
savepath (str): Defaults to current working dir.
keep_original_name (bool): True to keep the original file name otherwise
a friendlier is generated.
keep_original_name (bool): True to keep the original filename otherwise
a friendlier filename is generated.
**kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL`.
"""
filepaths = []
@ -839,8 +815,8 @@ class Episode(Video, Playable, ArtMixin, PosterMixin, RatingMixin,
] if p])
def _prettyfilename(self):
""" Returns a human friendly filename. """
return '%s.%s' % (self.grandparentTitle.replace(' ', '.'), self.seasonEpisode)
""" Returns a filename for use in download. """
return '%s - %s - %s' % (self.grandparentTitle, self.seasonEpisode, self.title)
@property
def actors(self):
@ -953,6 +929,7 @@ class Clip(Video, Playable, ArtUrlMixin, PosterUrlMixin):
return [part.file for part in self.iterParts() if part]
def _prettyfilename(self):
""" Returns a filename for use in download. """
return self.title
@ -968,4 +945,5 @@ class Extra(Clip):
self.librarySectionTitle = parent.librarySectionTitle
def _prettyfilename(self):
""" Returns a filename for use in download. """
return '%s (%s)' % (self.title, self.subtype)