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
|
@ -17,7 +17,7 @@ Plex Web Client. A few of the many features we currently support are:
|
||||||
* Perform library actions such as scan, analyze, empty trash.
|
* Perform library actions such as scan, analyze, empty trash.
|
||||||
* Remote control and play media on connected clients.
|
* Remote control and play media on connected clients.
|
||||||
* Listen in on all Plex Server notifications.
|
* Listen in on all Plex Server notifications.
|
||||||
|
|
||||||
|
|
||||||
Installation & Documentation
|
Installation & Documentation
|
||||||
----------------------------
|
----------------------------
|
||||||
|
|
|
@ -168,12 +168,12 @@ class Artist(Audio):
|
||||||
""" Alias of :func:`~plexapi.audio.Artist.track`. """
|
""" Alias of :func:`~plexapi.audio.Artist.track`. """
|
||||||
return self.track(title)
|
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.
|
""" Downloads all tracks for this artist to the specified location.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
savepath (str): Title of the track to return.
|
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
|
the Plex server. False will create a new filename with the format
|
||||||
"<Atrist> - <Album> <Track>".
|
"<Atrist> - <Album> <Track>".
|
||||||
kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL()` will
|
kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL()` will
|
||||||
|
@ -184,7 +184,7 @@ class Artist(Audio):
|
||||||
filepaths = []
|
filepaths = []
|
||||||
for album in self.albums():
|
for album in self.albums():
|
||||||
for track in album.tracks():
|
for track in album.tracks():
|
||||||
filepaths += track.download(savepath, keep_orginal_name, **kwargs)
|
filepaths += track.download(savepath, keep_original_name, **kwargs)
|
||||||
return filepaths
|
return filepaths
|
||||||
|
|
||||||
|
|
||||||
|
@ -251,12 +251,12 @@ class Album(Audio):
|
||||||
""" Return :func:`~plexapi.audio.Artist` of this album. """
|
""" Return :func:`~plexapi.audio.Artist` of this album. """
|
||||||
return self.fetchItem(self.parentKey)
|
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.
|
""" Downloads all tracks for this artist to the specified location.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
savepath (str): Title of the track to return.
|
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
|
the Plex server. False will create a new filename with the format
|
||||||
"<Atrist> - <Album> <Track>".
|
"<Atrist> - <Album> <Track>".
|
||||||
kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL()` will
|
kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL()` will
|
||||||
|
@ -266,7 +266,7 @@ class Album(Audio):
|
||||||
"""
|
"""
|
||||||
filepaths = []
|
filepaths = []
|
||||||
for track in self.tracks():
|
for track in self.tracks():
|
||||||
filepaths += track.download(savepath, keep_orginal_name, **kwargs)
|
filepaths += track.download(savepath, keep_original_name, **kwargs)
|
||||||
return filepaths
|
return filepaths
|
||||||
|
|
||||||
def _defaultSyncTitle(self):
|
def _defaultSyncTitle(self):
|
||||||
|
|
|
@ -519,13 +519,13 @@ class Playable(object):
|
||||||
"""
|
"""
|
||||||
client.playMedia(self)
|
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
|
""" Downloads this items media to the specified location. Returns a list of
|
||||||
filepaths that have been saved to disk.
|
filepaths that have been saved to disk.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
savepath (str): Title of the track to return.
|
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
|
the Plex server. False will create a new filename with the format
|
||||||
"<Artist> - <Album> <Track>".
|
"<Artist> - <Album> <Track>".
|
||||||
kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL()` will
|
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]
|
locations = [i for i in self.iterParts() if i]
|
||||||
for location in locations:
|
for location in locations:
|
||||||
filename = location.file
|
filename = location.file
|
||||||
if keep_orginal_name is False:
|
if keep_original_name is False:
|
||||||
filename = '%s.%s' % (self._prettyfilename(), location.container)
|
filename = '%s.%s' % (self._prettyfilename(), location.container)
|
||||||
# So this seems to be a alot slower but allows transcode.
|
# So this seems to be a alot slower but allows transcode.
|
||||||
if kwargs:
|
if kwargs:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
import time
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from requests.status_codes import _codes as codes
|
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._session = session or server_session or requests.Session()
|
||||||
self._proxyThroughServer = False
|
self._proxyThroughServer = False
|
||||||
self._commandId = 0
|
self._commandId = 0
|
||||||
|
self._last_call = 0
|
||||||
if not any([data, initpath, baseurl, token]):
|
if not any([data, initpath, baseurl, token]):
|
||||||
self._baseurl = CONFIG.get('auth.client_baseurl', 'http://localhost:32433')
|
self._baseurl = CONFIG.get('auth.client_baseurl', 'http://localhost:32433')
|
||||||
self._token = logfilter.add_secret(CONFIG.get('auth.client_token'))
|
self._token = logfilter.add_secret(CONFIG.get('auth.client_token'))
|
||||||
|
@ -181,14 +182,26 @@ class PlexClient(PlexObject):
|
||||||
"""
|
"""
|
||||||
command = command.strip('/')
|
command = command.strip('/')
|
||||||
controller = command.split('/')[0]
|
controller = command.split('/')[0]
|
||||||
|
headers = {'X-Plex-Target-Client-Identifier': self.machineIdentifier}
|
||||||
if controller not in self.protocolCapabilities:
|
if controller not in self.protocolCapabilities:
|
||||||
log.debug('Client %s doesnt support %s controller.'
|
log.debug('Client %s doesnt support %s controller.'
|
||||||
'What your trying might not work' % (self.title, 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()
|
params['commandID'] = self._nextCommandId()
|
||||||
key = '/player/%s%s' % (command, utils.joinArgs(params))
|
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
|
proxy = self._proxyThroughServer if proxy is None else proxy
|
||||||
|
|
||||||
if proxy:
|
if proxy:
|
||||||
return self._server.query(key, headers=headers)
|
return self._server.query(key, headers=headers)
|
||||||
return self.query(key, headers=headers)
|
return self.query(key, headers=headers)
|
||||||
|
|
|
@ -377,9 +377,17 @@ class LibrarySection(PlexObject):
|
||||||
key = '/library/sections/%s/all' % self.key
|
key = '/library/sections/%s/all' % self.key
|
||||||
return self.fetchItem(key, title__iexact=title)
|
return self.fetchItem(key, title__iexact=title)
|
||||||
|
|
||||||
def all(self, **kwargs):
|
def all(self, sort=None, **kwargs):
|
||||||
""" Returns a list of media from this library section. """
|
""" Returns a list of media from this library section.
|
||||||
key = '/library/sections/%s/all' % self.key
|
|
||||||
|
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)
|
return self.fetchItems(key, **kwargs)
|
||||||
|
|
||||||
def onDeck(self):
|
def onDeck(self):
|
||||||
|
@ -776,8 +784,8 @@ class MusicSection(LibrarySection):
|
||||||
TAG (str): 'Directory'
|
TAG (str): 'Directory'
|
||||||
TYPE (str): 'artist'
|
TYPE (str): 'artist'
|
||||||
"""
|
"""
|
||||||
ALLOWED_FILTERS = ('genre', 'country', 'collection', 'mood')
|
ALLOWED_FILTERS = ('genre', 'country', 'collection', 'mood', 'track.userRating')
|
||||||
ALLOWED_SORT = ('addedAt', 'lastViewedAt', 'viewCount', 'titleSort')
|
ALLOWED_SORT = ('addedAt', 'lastViewedAt', 'viewCount', 'titleSort', 'userRating')
|
||||||
TAG = 'Directory'
|
TAG = 'Directory'
|
||||||
TYPE = 'artist'
|
TYPE = 'artist'
|
||||||
|
|
||||||
|
|
|
@ -124,7 +124,35 @@ class MediaPart(PlexObject):
|
||||||
def subtitleStreams(self):
|
def subtitleStreams(self):
|
||||||
""" Returns a list of :class:`~plexapi.media.SubtitleStream` objects in this MediaPart. """
|
""" Returns a list of :class:`~plexapi.media.SubtitleStream` objects in this MediaPart. """
|
||||||
return [stream for stream in self.streams if stream.streamType == SubtitleStream.STREAMTYPE]
|
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):
|
class MediaPartStream(PlexObject):
|
||||||
""" Base class for media streams. These consist of video, audio and subtitles.
|
""" Base class for media streams. These consist of video, audio and subtitles.
|
||||||
|
@ -256,6 +284,7 @@ class SubtitleStream(MediaPartStream):
|
||||||
Attributes:
|
Attributes:
|
||||||
TAG (str): 'Stream'
|
TAG (str): 'Stream'
|
||||||
STREAMTYPE (int): 3
|
STREAMTYPE (int): 3
|
||||||
|
forced (bool): True if this is a forced subtitle
|
||||||
format (str): Subtitle format (ex: srt).
|
format (str): Subtitle format (ex: srt).
|
||||||
key (str): Key of this subtitle stream (ex: /library/streams/212284).
|
key (str): Key of this subtitle stream (ex: /library/streams/212284).
|
||||||
title (str): Title of this subtitle stream.
|
title (str): Title of this subtitle stream.
|
||||||
|
@ -266,6 +295,7 @@ class SubtitleStream(MediaPartStream):
|
||||||
def _loadData(self, data):
|
def _loadData(self, data):
|
||||||
""" Load attribute values from Plex XML response. """
|
""" Load attribute values from Plex XML response. """
|
||||||
super(SubtitleStream, self)._loadData(data)
|
super(SubtitleStream, self)._loadData(data)
|
||||||
|
self.forced = cast(bool, data.attrib.get('forced', '0'))
|
||||||
self.format = data.attrib.get('format')
|
self.format = data.attrib.get('format')
|
||||||
self.key = data.attrib.get('key')
|
self.key = data.attrib.get('key')
|
||||||
self.title = data.attrib.get('title')
|
self.title = data.attrib.get('title')
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
from plexapi import utils
|
from plexapi import utils
|
||||||
from plexapi.base import PlexPartialObject, Playable
|
from plexapi.base import PlexPartialObject, Playable
|
||||||
from plexapi.exceptions import BadRequest, Unsupported
|
from plexapi.exceptions import BadRequest, Unsupported
|
||||||
|
from plexapi.library import LibrarySection
|
||||||
from plexapi.playqueue import PlayQueue
|
from plexapi.playqueue import PlayQueue
|
||||||
from plexapi.utils import cast, toDatetime
|
from plexapi.utils import cast, toDatetime
|
||||||
from plexapi.compat import quote_plus
|
from plexapi.compat import quote_plus
|
||||||
|
@ -127,9 +128,9 @@ class Playlist(PlexPartialObject, Playable):
|
||||||
return PlayQueue.create(self._server, self, *args, **kwargs)
|
return PlayQueue.create(self._server, self, *args, **kwargs)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, server, title, items):
|
def _create(cls, server, title, items):
|
||||||
""" Create a playlist. """
|
""" Create a playlist. """
|
||||||
if not isinstance(items, (list, tuple)):
|
if items and not isinstance(items, (list, tuple)):
|
||||||
items = [items]
|
items = [items]
|
||||||
ratingKeys = []
|
ratingKeys = []
|
||||||
for item in items:
|
for item in items:
|
||||||
|
@ -147,6 +148,61 @@ class Playlist(PlexPartialObject, Playable):
|
||||||
data = server.query(key, method=server._session.post)[0]
|
data = server.query(key, method=server._session.post)[0]
|
||||||
return cls(server, data, initpath=key)
|
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):
|
def copyToUser(self, user):
|
||||||
""" Copy playlist to another user account. """
|
""" Copy playlist to another user account. """
|
||||||
from plexapi.server import PlexServer
|
from plexapi.server import PlexServer
|
||||||
|
|
|
@ -93,6 +93,7 @@ class PlexServer(PlexObject):
|
||||||
|
|
||||||
def __init__(self, baseurl=None, token=None, session=None, timeout=None):
|
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 = 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._token = logfilter.add_secret(token or CONFIG.get('auth.server_token'))
|
||||||
self._showSecrets = CONFIG.get('log.show_secrets', '').lower() == 'true'
|
self._showSecrets = CONFIG.get('log.show_secrets', '').lower() == 'true'
|
||||||
self._session = session or requests.Session()
|
self._session = session or requests.Session()
|
||||||
|
@ -240,14 +241,14 @@ class PlexServer(PlexObject):
|
||||||
|
|
||||||
raise NotFound('Unknown client name: %s' % name)
|
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`.
|
""" Creates and returns a new :class:`~plexapi.playlist.Playlist`.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
title (str): Title of the playlist to be created.
|
title (str): Title of the playlist to be created.
|
||||||
items (list<Media>): List of media items to include in the playlist.
|
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):
|
def createPlayQueue(self, item, **kwargs):
|
||||||
""" Creates and returns a new :class:`~plexapi.playqueue.PlayQueue`.
|
""" 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_cast = lambda x: True if x == 'true' or x == '1' else False
|
||||||
_bool_str = lambda x: str(x).lower()
|
_bool_str = lambda x: str(x).lower()
|
||||||
|
_str = lambda x: str(x).encode('utf-8')
|
||||||
TYPES = {
|
TYPES = {
|
||||||
'bool': {'type': bool, 'cast': _bool_cast, 'tostr': _bool_str},
|
'bool': {'type': bool, 'cast': _bool_cast, 'tostr': _bool_str},
|
||||||
'double': {'type': float, 'cast': float, 'tostr': string_type},
|
'double': {'type': float, 'cast': float, 'tostr': _str},
|
||||||
'int': {'type': int, 'cast': int, 'tostr': string_type},
|
'int': {'type': int, 'cast': int, 'tostr': _str},
|
||||||
'text': {'type': string_type, 'cast': string_type, 'tostr': string_type},
|
'text': {'type': string_type, 'cast': _str, 'tostr': _str},
|
||||||
}
|
}
|
||||||
|
|
||||||
def _loadData(self, data):
|
def _loadData(self, data):
|
||||||
|
|
|
@ -178,6 +178,11 @@ def toDatetime(value, format=None):
|
||||||
if format:
|
if format:
|
||||||
value = datetime.strptime(value, format)
|
value = datetime.strptime(value, format)
|
||||||
else:
|
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))
|
value = datetime.fromtimestamp(int(value))
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
|
@ -224,12 +224,12 @@ class Movie(Playable, Video):
|
||||||
# This is just for compat.
|
# This is just for compat.
|
||||||
return self.title
|
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.
|
""" Download video files to specified directory.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
savepath (str): Defaults to current working dir.
|
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.
|
a friendlier is generated.
|
||||||
**kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL()`.
|
**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]
|
locations = [i for i in self.iterParts() if i]
|
||||||
for location in locations:
|
for location in locations:
|
||||||
name = location.file
|
name = location.file
|
||||||
if not keep_orginal_name:
|
if not keep_original_name:
|
||||||
title = self.title.replace(' ', '.')
|
title = self.title.replace(' ', '.')
|
||||||
name = '%s.%s' % (title, location.container)
|
name = '%s.%s' % (title, location.container)
|
||||||
if kwargs is not None:
|
if kwargs is not None:
|
||||||
|
@ -376,18 +376,18 @@ class Show(Video):
|
||||||
""" Alias to :func:`~plexapi.video.Show.episode()`. """
|
""" Alias to :func:`~plexapi.video.Show.episode()`. """
|
||||||
return self.episode(title, season, 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.
|
""" Download video files to specified directory.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
savepath (str): Defaults to current working dir.
|
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.
|
a friendlier is generated.
|
||||||
**kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL()`.
|
**kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL()`.
|
||||||
"""
|
"""
|
||||||
filepaths = []
|
filepaths = []
|
||||||
for episode in self.episodes():
|
for episode in self.episodes():
|
||||||
filepaths += episode.download(savepath, keep_orginal_name, **kwargs)
|
filepaths += episode.download(savepath, keep_original_name, **kwargs)
|
||||||
return filepaths
|
return filepaths
|
||||||
|
|
||||||
|
|
||||||
|
@ -477,18 +477,18 @@ class Season(Video):
|
||||||
""" Returns list of unwatched :class:`~plexapi.video.Episode` objects. """
|
""" Returns list of unwatched :class:`~plexapi.video.Episode` objects. """
|
||||||
return self.episodes(watched=False)
|
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.
|
""" Download video files to specified directory.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
savepath (str): Defaults to current working dir.
|
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.
|
a friendlier is generated.
|
||||||
**kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL()`.
|
**kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL()`.
|
||||||
"""
|
"""
|
||||||
filepaths = []
|
filepaths = []
|
||||||
for episode in self.episodes():
|
for episode in self.episodes():
|
||||||
filepaths += episode.download(savepath, keep_orginal_name, **kwargs)
|
filepaths += episode.download(savepath, keep_original_name, **kwargs)
|
||||||
return filepaths
|
return filepaths
|
||||||
|
|
||||||
def _defaultSyncTitle(self):
|
def _defaultSyncTitle(self):
|
||||||
|
|
|
@ -36,7 +36,7 @@ AUDIOCHANNELS = {2, 6}
|
||||||
AUDIOLAYOUTS = {'5.1', '5.1(side)', 'stereo'}
|
AUDIOLAYOUTS = {'5.1', '5.1(side)', 'stereo'}
|
||||||
CODECS = {'aac', 'ac3', 'dca', 'h264', 'mp3', 'mpeg4'}
|
CODECS = {'aac', 'ac3', 'dca', 'h264', 'mp3', 'mpeg4'}
|
||||||
CONTAINERS = {'avi', 'mp4', 'mkv'}
|
CONTAINERS = {'avi', 'mp4', 'mkv'}
|
||||||
CONTENTRATINGS = {'TV-14', 'TV-MA', 'G', 'NR'}
|
CONTENTRATINGS = {'TV-14', 'TV-MA', 'G', 'NR', 'Not Rated'}
|
||||||
FRAMERATES = {'24p', 'PAL', 'NTSC'}
|
FRAMERATES = {'24p', 'PAL', 'NTSC'}
|
||||||
PROFILES = {'advanced simple', 'main', 'constrained baseline'}
|
PROFILES = {'advanced simple', 'main', 'constrained baseline'}
|
||||||
RESOLUTIONS = {'sd', '480', '576', '720', '1080'}
|
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()]
|
assert playlist.title in [p.title for p in user_plex.playlists()]
|
||||||
finally:
|
finally:
|
||||||
playlist.delete()
|
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.save()
|
||||||
plex._settings = None
|
plex._settings = None
|
||||||
assert plex.settings.get('autoEmptyTrash').value == new_value
|
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 utils.is_metadata(movie.art)
|
||||||
assert movie.artUrl
|
assert movie.artUrl
|
||||||
assert movie.audienceRating == 8.5
|
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'
|
#assert movie.audienceRatingImage == 'rottentomatoes://image.rating.upright'
|
||||||
movie.reload() # RELOAD
|
movie.reload() # RELOAD
|
||||||
assert movie.chapterSource is None
|
assert movie.chapterSource is None
|
||||||
|
|
Loading…
Reference in a new issue