mirror of
https://github.com/pkkid/python-plexapi
synced 2024-11-26 05:30:20 +00:00
Merge branch 'master' of github.com:pkkid/python-plexapi
This commit is contained in:
commit
9d4966d842
15 changed files with 164 additions and 35 deletions
|
@ -168,12 +168,12 @@ class Artist(Audio):
|
|||
""" Alias of :func:`~plexapi.audio.Artist.track`. """
|
||||
return self.track(title)
|
||||
|
||||
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
|
||||
def download(self, savepath=None, keep_original_name=False, **kwargs):
|
||||
""" Downloads all tracks for this artist to the specified location.
|
||||
|
||||
Parameters:
|
||||
savepath (str): Title of the track to return.
|
||||
keep_orginal_name (bool): Set True to keep the original filename as stored in
|
||||
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
|
||||
|
@ -184,7 +184,7 @@ class Artist(Audio):
|
|||
filepaths = []
|
||||
for album in self.albums():
|
||||
for track in album.tracks():
|
||||
filepaths += track.download(savepath, keep_orginal_name, **kwargs)
|
||||
filepaths += track.download(savepath, keep_original_name, **kwargs)
|
||||
return filepaths
|
||||
|
||||
|
||||
|
@ -251,12 +251,12 @@ class Album(Audio):
|
|||
""" Return :func:`~plexapi.audio.Artist` of this album. """
|
||||
return self.fetchItem(self.parentKey)
|
||||
|
||||
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
|
||||
def download(self, savepath=None, keep_original_name=False, **kwargs):
|
||||
""" Downloads all tracks for this artist to the specified location.
|
||||
|
||||
Parameters:
|
||||
savepath (str): Title of the track to return.
|
||||
keep_orginal_name (bool): Set True to keep the original filename as stored in
|
||||
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
|
||||
|
@ -266,7 +266,7 @@ class Album(Audio):
|
|||
"""
|
||||
filepaths = []
|
||||
for track in self.tracks():
|
||||
filepaths += track.download(savepath, keep_orginal_name, **kwargs)
|
||||
filepaths += track.download(savepath, keep_original_name, **kwargs)
|
||||
return filepaths
|
||||
|
||||
def _defaultSyncTitle(self):
|
||||
|
|
|
@ -519,13 +519,13 @@ class Playable(object):
|
|||
"""
|
||||
client.playMedia(self)
|
||||
|
||||
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
|
||||
def download(self, savepath=None, keep_original_name=False, **kwargs):
|
||||
""" Downloads this items media 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_orginal_name (bool): Set True to keep the original filename as stored in
|
||||
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
|
||||
|
@ -537,7 +537,7 @@ class Playable(object):
|
|||
locations = [i for i in self.iterParts() if i]
|
||||
for location in locations:
|
||||
filename = location.file
|
||||
if keep_orginal_name is False:
|
||||
if keep_original_name is False:
|
||||
filename = '%s.%s' % (self._prettyfilename(), location.container)
|
||||
# So this seems to be a alot slower but allows transcode.
|
||||
if kwargs:
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import time
|
||||
import requests
|
||||
|
||||
from requests.status_codes import _codes as codes
|
||||
|
@ -70,6 +70,7 @@ class PlexClient(PlexObject):
|
|||
self._session = session or server_session or requests.Session()
|
||||
self._proxyThroughServer = False
|
||||
self._commandId = 0
|
||||
self._last_call = 0
|
||||
if not any([data, initpath, baseurl, token]):
|
||||
self._baseurl = CONFIG.get('auth.client_baseurl', 'http://localhost:32433')
|
||||
self._token = logfilter.add_secret(CONFIG.get('auth.client_token'))
|
||||
|
@ -181,14 +182,26 @@ class PlexClient(PlexObject):
|
|||
"""
|
||||
command = command.strip('/')
|
||||
controller = command.split('/')[0]
|
||||
headers = {'X-Plex-Target-Client-Identifier': self.machineIdentifier}
|
||||
if controller not in self.protocolCapabilities:
|
||||
log.debug('Client %s doesnt support %s controller.'
|
||||
'What your trying might not work' % (self.title, controller))
|
||||
|
||||
# Workaround for ptp. See https://github.com/pkkid/python-plexapi/issues/244
|
||||
t = time.time()
|
||||
if t - self._last_call >= 80 and self.product in ('ptp', 'Plex Media Player'):
|
||||
url = '/player/timeline/poll?wait=0&commandID=%s' % self._nextCommandId()
|
||||
if proxy:
|
||||
self._server.query(url, headers=headers)
|
||||
else:
|
||||
self.query(url, headers=headers)
|
||||
self._last_call = t
|
||||
|
||||
params['commandID'] = self._nextCommandId()
|
||||
key = '/player/%s%s' % (command, utils.joinArgs(params))
|
||||
headers = {'X-Plex-Target-Client-Identifier': self.machineIdentifier}
|
||||
|
||||
proxy = self._proxyThroughServer if proxy is None else proxy
|
||||
|
||||
if proxy:
|
||||
return self._server.query(key, headers=headers)
|
||||
return self.query(key, headers=headers)
|
||||
|
|
|
@ -377,9 +377,17 @@ class LibrarySection(PlexObject):
|
|||
key = '/library/sections/%s/all' % self.key
|
||||
return self.fetchItem(key, title__iexact=title)
|
||||
|
||||
def all(self, **kwargs):
|
||||
""" Returns a list of media from this library section. """
|
||||
key = '/library/sections/%s/all' % self.key
|
||||
def all(self, sort=None, **kwargs):
|
||||
""" Returns a list of media from this library section.
|
||||
|
||||
Parameters:
|
||||
sort (string): The sort string
|
||||
"""
|
||||
sortStr = ''
|
||||
if sort != None:
|
||||
sortStr = '?sort=' + sort
|
||||
|
||||
key = '/library/sections/%s/all%s' % (self.key, sortStr)
|
||||
return self.fetchItems(key, **kwargs)
|
||||
|
||||
def onDeck(self):
|
||||
|
@ -776,8 +784,8 @@ class MusicSection(LibrarySection):
|
|||
TAG (str): 'Directory'
|
||||
TYPE (str): 'artist'
|
||||
"""
|
||||
ALLOWED_FILTERS = ('genre', 'country', 'collection', 'mood')
|
||||
ALLOWED_SORT = ('addedAt', 'lastViewedAt', 'viewCount', 'titleSort')
|
||||
ALLOWED_FILTERS = ('genre', 'country', 'collection', 'mood', 'track.userRating')
|
||||
ALLOWED_SORT = ('addedAt', 'lastViewedAt', 'viewCount', 'titleSort', 'userRating')
|
||||
TAG = 'Directory'
|
||||
TYPE = 'artist'
|
||||
|
||||
|
|
|
@ -125,6 +125,34 @@ class MediaPart(PlexObject):
|
|||
""" Returns a list of :class:`~plexapi.media.SubtitleStream` objects in this MediaPart. """
|
||||
return [stream for stream in self.streams if stream.streamType == SubtitleStream.STREAMTYPE]
|
||||
|
||||
def setDefaultAudioStream(self, stream):
|
||||
""" Set the default :class:`~plexapi.media.AudioStream` for this MediaPart.
|
||||
|
||||
Parameters:
|
||||
stream (:class:`~plexapi.media.AudioStream`): AudioStream to set as default
|
||||
"""
|
||||
if isinstance(stream, AudioStream):
|
||||
key = "/library/parts/%d?audioStreamID=%d&allParts=1" % (self.id, stream.id)
|
||||
else:
|
||||
key = "/library/parts/%d?audioStreamID=%d&allParts=1" % (self.id, stream)
|
||||
self._server.query(key, method=self._server._session.put)
|
||||
|
||||
def setDefaultSubtitleStream(self, stream):
|
||||
""" Set the default :class:`~plexapi.media.SubtitleStream` for this MediaPart.
|
||||
|
||||
Parameters:
|
||||
stream (:class:`~plexapi.media.SubtitleStream`): SubtitleStream to set as default.
|
||||
"""
|
||||
if isinstance(stream, SubtitleStream):
|
||||
key = "/library/parts/%d?subtitleStreamID=%d&allParts=1" % (self.id, stream.id)
|
||||
else:
|
||||
key = "/library/parts/%d?subtitleStreamID=%d&allParts=1" % (self.id, stream)
|
||||
self._server.query(key, method=self._server._session.put)
|
||||
|
||||
def resetDefaultSubtitleStream(self):
|
||||
""" Set default subtitle of this MediaPart to 'none'. """
|
||||
key = "/library/parts/%d?subtitleStreamID=0&allParts=1" % (self.id)
|
||||
self._server.query(key, method=self._server._session.put)
|
||||
|
||||
class MediaPartStream(PlexObject):
|
||||
""" Base class for media streams. These consist of video, audio and subtitles.
|
||||
|
@ -256,6 +284,7 @@ class SubtitleStream(MediaPartStream):
|
|||
Attributes:
|
||||
TAG (str): 'Stream'
|
||||
STREAMTYPE (int): 3
|
||||
forced (bool): True if this is a forced subtitle
|
||||
format (str): Subtitle format (ex: srt).
|
||||
key (str): Key of this subtitle stream (ex: /library/streams/212284).
|
||||
title (str): Title of this subtitle stream.
|
||||
|
@ -266,6 +295,7 @@ class SubtitleStream(MediaPartStream):
|
|||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
super(SubtitleStream, self)._loadData(data)
|
||||
self.forced = cast(bool, data.attrib.get('forced', '0'))
|
||||
self.format = data.attrib.get('format')
|
||||
self.key = data.attrib.get('key')
|
||||
self.title = data.attrib.get('title')
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from plexapi import utils
|
||||
from plexapi.base import PlexPartialObject, Playable
|
||||
from plexapi.exceptions import BadRequest, Unsupported
|
||||
from plexapi.library import LibrarySection
|
||||
from plexapi.playqueue import PlayQueue
|
||||
from plexapi.utils import cast, toDatetime
|
||||
from plexapi.compat import quote_plus
|
||||
|
@ -127,9 +128,9 @@ class Playlist(PlexPartialObject, Playable):
|
|||
return PlayQueue.create(self._server, self, *args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def create(cls, server, title, items):
|
||||
def _create(cls, server, title, items):
|
||||
""" Create a playlist. """
|
||||
if not isinstance(items, (list, tuple)):
|
||||
if items and not isinstance(items, (list, tuple)):
|
||||
items = [items]
|
||||
ratingKeys = []
|
||||
for item in items:
|
||||
|
@ -147,6 +148,61 @@ class Playlist(PlexPartialObject, Playable):
|
|||
data = server.query(key, method=server._session.post)[0]
|
||||
return cls(server, data, initpath=key)
|
||||
|
||||
@classmethod
|
||||
def create(cls, server, title, items=None, section=None, limit=None, smart=False, **kwargs):
|
||||
"""Create a playlist.
|
||||
|
||||
Parameters:
|
||||
server (:class:`~plexapi.server.PlexServer`): Server your connected to.
|
||||
title (str): Title of the playlist.
|
||||
items (Iterable): Iterable of objects that should be in the playlist.
|
||||
section (:class:`~plexapi.library.LibrarySection`, str):
|
||||
limit (int): default None.
|
||||
smart (bool): default False.
|
||||
|
||||
**kwargs (dict): is passed to the filters. For a example see the search method.
|
||||
|
||||
Returns:
|
||||
:class:`plexapi.playlist.Playlist`: an instance of created Playlist.
|
||||
"""
|
||||
if smart:
|
||||
return cls._createSmart(server, title, section, limit, **kwargs)
|
||||
|
||||
else:
|
||||
return cls._create(server, title, items)
|
||||
|
||||
@classmethod
|
||||
def _createSmart(cls, server, title, section, limit=None, **kwargs):
|
||||
""" Create a Smart playlist. """
|
||||
|
||||
if not isinstance(section, LibrarySection):
|
||||
section = server.library.section(section)
|
||||
|
||||
sectionType = utils.searchType(section.type)
|
||||
sectionId = section.key
|
||||
uuid = section.uuid
|
||||
uri = 'library://%s/directory//library/sections/%s/all?type=%s' % (uuid,
|
||||
sectionId,
|
||||
sectionType)
|
||||
if limit:
|
||||
uri = uri + '&limit=%s' % str(limit)
|
||||
|
||||
for category, value in kwargs.items():
|
||||
sectionChoices = section.listChoices(category)
|
||||
for choice in sectionChoices:
|
||||
if str(choice.title).lower() == str(value).lower():
|
||||
uri = uri + '&%s=%s' % (category.lower(), str(choice.key))
|
||||
|
||||
uri = uri + '&sourceType=%s' % sectionType
|
||||
key = '/playlists%s' % utils.joinArgs({
|
||||
'uri': uri,
|
||||
'type': section.CONTENT_TYPE,
|
||||
'title': title,
|
||||
'smart': 1,
|
||||
})
|
||||
data = server.query(key, method=server._session.post)[0]
|
||||
return cls(server, data, initpath=key)
|
||||
|
||||
def copyToUser(self, user):
|
||||
""" Copy playlist to another user account. """
|
||||
from plexapi.server import PlexServer
|
||||
|
|
|
@ -93,6 +93,7 @@ class PlexServer(PlexObject):
|
|||
|
||||
def __init__(self, baseurl=None, token=None, session=None, timeout=None):
|
||||
self._baseurl = baseurl or CONFIG.get('auth.server_baseurl', 'http://localhost:32400')
|
||||
self._baseurl = self._baseurl.rstrip('/')
|
||||
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()
|
||||
|
@ -240,14 +241,14 @@ class PlexServer(PlexObject):
|
|||
|
||||
raise NotFound('Unknown client name: %s' % name)
|
||||
|
||||
def createPlaylist(self, title, items):
|
||||
def createPlaylist(self, title, items=None, section=None, limit=None, smart=None, **kwargs):
|
||||
""" Creates and returns a new :class:`~plexapi.playlist.Playlist`.
|
||||
|
||||
Parameters:
|
||||
title (str): Title of the playlist to be created.
|
||||
items (list<Media>): List of media items to include in the playlist.
|
||||
"""
|
||||
return Playlist.create(self, title, items)
|
||||
return Playlist.create(self, title, items=items, limit=limit, section=section, smart=smart, **kwargs)
|
||||
|
||||
def createPlayQueue(self, item, **kwargs):
|
||||
""" Creates and returns a new :class:`~plexapi.playqueue.PlayQueue`.
|
||||
|
|
|
@ -101,11 +101,12 @@ class Setting(PlexObject):
|
|||
"""
|
||||
_bool_cast = lambda x: True if x == 'true' or x == '1' else False
|
||||
_bool_str = lambda x: str(x).lower()
|
||||
_str = lambda x: str(x).encode('utf-8')
|
||||
TYPES = {
|
||||
'bool': {'type': bool, 'cast': _bool_cast, 'tostr': _bool_str},
|
||||
'double': {'type': float, 'cast': float, 'tostr': string_type},
|
||||
'int': {'type': int, 'cast': int, 'tostr': string_type},
|
||||
'text': {'type': string_type, 'cast': string_type, 'tostr': string_type},
|
||||
'double': {'type': float, 'cast': float, 'tostr': _str},
|
||||
'int': {'type': int, 'cast': int, 'tostr': _str},
|
||||
'text': {'type': string_type, 'cast': _str, 'tostr': _str},
|
||||
}
|
||||
|
||||
def _loadData(self, data):
|
||||
|
|
|
@ -178,6 +178,11 @@ def toDatetime(value, format=None):
|
|||
if format:
|
||||
value = datetime.strptime(value, format)
|
||||
else:
|
||||
# https://bugs.python.org/issue30684
|
||||
# And platform support for before epoch seems to be flaky.
|
||||
# TODO check for others errors too.
|
||||
if int(value) == 0:
|
||||
value = 86400
|
||||
value = datetime.fromtimestamp(int(value))
|
||||
return value
|
||||
|
||||
|
|
|
@ -224,12 +224,12 @@ class Movie(Playable, Video):
|
|||
# This is just for compat.
|
||||
return self.title
|
||||
|
||||
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
|
||||
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_orginal_name (bool): True to keep the original file name otherwise
|
||||
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()`.
|
||||
"""
|
||||
|
@ -237,7 +237,7 @@ class Movie(Playable, Video):
|
|||
locations = [i for i in self.iterParts() if i]
|
||||
for location in locations:
|
||||
name = location.file
|
||||
if not keep_orginal_name:
|
||||
if not keep_original_name:
|
||||
title = self.title.replace(' ', '.')
|
||||
name = '%s.%s' % (title, location.container)
|
||||
if kwargs is not None:
|
||||
|
@ -376,18 +376,18 @@ class Show(Video):
|
|||
""" Alias to :func:`~plexapi.video.Show.episode()`. """
|
||||
return self.episode(title, season, episode)
|
||||
|
||||
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
|
||||
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_orginal_name (bool): True to keep the original file name otherwise
|
||||
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 = []
|
||||
for episode in self.episodes():
|
||||
filepaths += episode.download(savepath, keep_orginal_name, **kwargs)
|
||||
filepaths += episode.download(savepath, keep_original_name, **kwargs)
|
||||
return filepaths
|
||||
|
||||
|
||||
|
@ -477,18 +477,18 @@ class Season(Video):
|
|||
""" Returns list of unwatched :class:`~plexapi.video.Episode` objects. """
|
||||
return self.episodes(watched=False)
|
||||
|
||||
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
|
||||
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_orginal_name (bool): True to keep the original file name otherwise
|
||||
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 = []
|
||||
for episode in self.episodes():
|
||||
filepaths += episode.download(savepath, keep_orginal_name, **kwargs)
|
||||
filepaths += episode.download(savepath, keep_original_name, **kwargs)
|
||||
return filepaths
|
||||
|
||||
def _defaultSyncTitle(self):
|
||||
|
|
|
@ -36,7 +36,7 @@ AUDIOCHANNELS = {2, 6}
|
|||
AUDIOLAYOUTS = {'5.1', '5.1(side)', 'stereo'}
|
||||
CODECS = {'aac', 'ac3', 'dca', 'h264', 'mp3', 'mpeg4'}
|
||||
CONTAINERS = {'avi', 'mp4', 'mkv'}
|
||||
CONTENTRATINGS = {'TV-14', 'TV-MA', 'G', 'NR'}
|
||||
CONTENTRATINGS = {'TV-14', 'TV-MA', 'G', 'NR', 'Not Rated'}
|
||||
FRAMERATES = {'24p', 'PAL', 'NTSC'}
|
||||
PROFILES = {'advanced simple', 'main', 'constrained baseline'}
|
||||
RESOLUTIONS = {'sd', '480', '576', '720', '1080'}
|
||||
|
|
|
@ -109,3 +109,9 @@ def test_copyToUser(plex, show, fresh_plex, shared_username):
|
|||
assert playlist.title in [p.title for p in user_plex.playlists()]
|
||||
finally:
|
||||
playlist.delete()
|
||||
|
||||
|
||||
def test_smart_playlist(plex, movies):
|
||||
pl = plex.createPlaylist(title='smart_playlist', smart=True, limit=1, section=movies, year=2008)
|
||||
assert len(pl.items()) == 1
|
||||
assert pl.smart
|
||||
|
|
|
@ -17,3 +17,12 @@ def test_settings_set(plex):
|
|||
plex.settings.save()
|
||||
plex._settings = None
|
||||
assert plex.settings.get('autoEmptyTrash').value == new_value
|
||||
|
||||
|
||||
def test_settings_set_str(plex):
|
||||
cd = plex.settings.get('OnDeckWindow')
|
||||
new_value = 99
|
||||
cd.set(new_value)
|
||||
plex.settings.save()
|
||||
plex._settings = None
|
||||
assert plex.settings.get('OnDeckWindow').value == 99
|
||||
|
|
|
@ -91,7 +91,7 @@ def test_video_Movie_attrs(movies):
|
|||
assert utils.is_metadata(movie.art)
|
||||
assert movie.artUrl
|
||||
assert movie.audienceRating == 8.5
|
||||
# Disabled this since it failed on the last run, wasnt in the orginal xml result.
|
||||
# Disabled this since it failed on the last run, wasnt in the original xml result.
|
||||
#assert movie.audienceRatingImage == 'rottentomatoes://image.rating.upright'
|
||||
movie.reload() # RELOAD
|
||||
assert movie.chapterSource is None
|
||||
|
|
Loading…
Reference in a new issue