Skip to content

Commit 3f6eede

Browse files
authored
Merge branch 'master' into patch-1
2 parents b9d163a + accb092 commit 3f6eede

File tree

14 files changed

+136
-35
lines changed

14 files changed

+136
-35
lines changed

plexapi/audio.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,12 @@ def get(self, title):
168168
""" Alias of :func:`~plexapi.audio.Artist.track`. """
169169
return self.track(title)
170170

171-
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
171+
def download(self, savepath=None, keep_original_name=False, **kwargs):
172172
""" Downloads all tracks for this artist to the specified location.
173173
174174
Parameters:
175175
savepath (str): Title of the track to return.
176-
keep_orginal_name (bool): Set True to keep the original filename as stored in
176+
keep_original_name (bool): Set True to keep the original filename as stored in
177177
the Plex server. False will create a new filename with the format
178178
"<Atrist> - <Album> <Track>".
179179
kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL()` will
@@ -184,7 +184,7 @@ def download(self, savepath=None, keep_orginal_name=False, **kwargs):
184184
filepaths = []
185185
for album in self.albums():
186186
for track in album.tracks():
187-
filepaths += track.download(savepath, keep_orginal_name, **kwargs)
187+
filepaths += track.download(savepath, keep_original_name, **kwargs)
188188
return filepaths
189189

190190

@@ -251,12 +251,12 @@ def artist(self):
251251
""" Return :func:`~plexapi.audio.Artist` of this album. """
252252
return self.fetchItem(self.parentKey)
253253

254-
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
254+
def download(self, savepath=None, keep_original_name=False, **kwargs):
255255
""" Downloads all tracks for this artist to the specified location.
256256
257257
Parameters:
258258
savepath (str): Title of the track to return.
259-
keep_orginal_name (bool): Set True to keep the original filename as stored in
259+
keep_original_name (bool): Set True to keep the original filename as stored in
260260
the Plex server. False will create a new filename with the format
261261
"<Atrist> - <Album> <Track>".
262262
kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL()` will
@@ -266,7 +266,7 @@ def download(self, savepath=None, keep_orginal_name=False, **kwargs):
266266
"""
267267
filepaths = []
268268
for track in self.tracks():
269-
filepaths += track.download(savepath, keep_orginal_name, **kwargs)
269+
filepaths += track.download(savepath, keep_original_name, **kwargs)
270270
return filepaths
271271

272272
def _defaultSyncTitle(self):

plexapi/base.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -519,13 +519,13 @@ def play(self, client):
519519
"""
520520
client.playMedia(self)
521521

522-
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
522+
def download(self, savepath=None, keep_original_name=False, **kwargs):
523523
""" Downloads this items media to the specified location. Returns a list of
524524
filepaths that have been saved to disk.
525525
526526
Parameters:
527527
savepath (str): Title of the track to return.
528-
keep_orginal_name (bool): Set True to keep the original filename as stored in
528+
keep_original_name (bool): Set True to keep the original filename as stored in
529529
the Plex server. False will create a new filename with the format
530530
"<Artist> - <Album> <Track>".
531531
kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL()` will
@@ -537,7 +537,7 @@ def download(self, savepath=None, keep_orginal_name=False, **kwargs):
537537
locations = [i for i in self.iterParts() if i]
538538
for location in locations:
539539
filename = location.file
540-
if keep_orginal_name is False:
540+
if keep_original_name is False:
541541
filename = '%s.%s' % (self._prettyfilename(), location.container)
542542
# So this seems to be a alot slower but allows transcode.
543543
if kwargs:

plexapi/client.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# -*- coding: utf-8 -*-
2-
2+
import time
33
import requests
44

55
from requests.status_codes import _codes as codes
@@ -70,6 +70,7 @@ def __init__(self, server=None, data=None, initpath=None, baseurl=None,
7070
self._session = session or server_session or requests.Session()
7171
self._proxyThroughServer = False
7272
self._commandId = 0
73+
self._last_call = 0
7374
if not any([data, initpath, baseurl, token]):
7475
self._baseurl = CONFIG.get('auth.client_baseurl', 'http://localhost:32433')
7576
self._token = logfilter.add_secret(CONFIG.get('auth.client_token'))
@@ -181,14 +182,26 @@ def sendCommand(self, command, proxy=None, **params):
181182
"""
182183
command = command.strip('/')
183184
controller = command.split('/')[0]
185+
headers = {'X-Plex-Target-Client-Identifier': self.machineIdentifier}
184186
if controller not in self.protocolCapabilities:
185187
log.debug('Client %s doesnt support %s controller.'
186188
'What your trying might not work' % (self.title, controller))
187189

190+
# Workaround for ptp. See https://github.com/pkkid/python-plexapi/issues/244
191+
t = time.time()
192+
if t - self._last_call >= 80 and self.product in ('ptp', 'Plex Media Player'):
193+
url = '/player/timeline/poll?wait=0&commandID=%s' % self._nextCommandId()
194+
if proxy:
195+
self._server.query(url, headers=headers)
196+
else:
197+
self.query(url, headers=headers)
198+
self._last_call = t
199+
188200
params['commandID'] = self._nextCommandId()
189201
key = '/player/%s%s' % (command, utils.joinArgs(params))
190-
headers = {'X-Plex-Target-Client-Identifier': self.machineIdentifier}
202+
191203
proxy = self._proxyThroughServer if proxy is None else proxy
204+
192205
if proxy:
193206
return self._server.query(key, headers=headers)
194207
return self.query(key, headers=headers)

plexapi/library.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -377,9 +377,17 @@ def get(self, title):
377377
key = '/library/sections/%s/all' % self.key
378378
return self.fetchItem(key, title__iexact=title)
379379

380-
def all(self, **kwargs):
381-
""" Returns a list of media from this library section. """
382-
key = '/library/sections/%s/all' % self.key
380+
def all(self, sort=None, **kwargs):
381+
""" Returns a list of media from this library section.
382+
383+
Parameters:
384+
sort (string): The sort string
385+
"""
386+
sortStr = ''
387+
if sort != None:
388+
sortStr = '?sort=' + sort
389+
390+
key = '/library/sections/%s/all%s' % (self.key, sortStr)
383391
return self.fetchItems(key, **kwargs)
384392

385393
def onDeck(self):
@@ -776,8 +784,8 @@ class MusicSection(LibrarySection):
776784
TAG (str): 'Directory'
777785
TYPE (str): 'artist'
778786
"""
779-
ALLOWED_FILTERS = ('genre', 'country', 'collection', 'mood', 'year')
780-
ALLOWED_SORT = ('addedAt', 'lastViewedAt', 'viewCount', 'titleSort')
787+
ALLOWED_FILTERS = ('genre', 'country', 'collection', 'mood', 'year', 'track.userRating')
788+
ALLOWED_SORT = ('addedAt', 'lastViewedAt', 'viewCount', 'titleSort', 'userRating')
781789
TAG = 'Directory'
782790
TYPE = 'artist'
783791

plexapi/media.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ class SubtitleStream(MediaPartStream):
256256
Attributes:
257257
TAG (str): 'Stream'
258258
STREAMTYPE (int): 3
259+
forced (bool): True if this is a forced subtitle
259260
format (str): Subtitle format (ex: srt).
260261
key (str): Key of this subtitle stream (ex: /library/streams/212284).
261262
title (str): Title of this subtitle stream.
@@ -266,6 +267,7 @@ class SubtitleStream(MediaPartStream):
266267
def _loadData(self, data):
267268
""" Load attribute values from Plex XML response. """
268269
super(SubtitleStream, self)._loadData(data)
270+
self.forced = cast(bool, data.attrib.get('forced', '0'))
269271
self.format = data.attrib.get('format')
270272
self.key = data.attrib.get('key')
271273
self.title = data.attrib.get('title')

plexapi/playlist.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from plexapi import utils
33
from plexapi.base import PlexPartialObject, Playable
44
from plexapi.exceptions import BadRequest, Unsupported
5+
from plexapi.library import LibrarySection
56
from plexapi.playqueue import PlayQueue
67
from plexapi.utils import cast, toDatetime
78
from plexapi.compat import quote_plus
@@ -127,9 +128,9 @@ def playQueue(self, *args, **kwargs):
127128
return PlayQueue.create(self._server, self, *args, **kwargs)
128129

129130
@classmethod
130-
def create(cls, server, title, items):
131+
def _create(cls, server, title, items):
131132
""" Create a playlist. """
132-
if not isinstance(items, (list, tuple)):
133+
if items and not isinstance(items, (list, tuple)):
133134
items = [items]
134135
ratingKeys = []
135136
for item in items:
@@ -147,6 +148,61 @@ def create(cls, server, title, items):
147148
data = server.query(key, method=server._session.post)[0]
148149
return cls(server, data, initpath=key)
149150

151+
@classmethod
152+
def create(cls, server, title, items=None, section=None, limit=None, smart=False, **kwargs):
153+
"""Create a playlist.
154+
155+
Parameters:
156+
server (:class:`~plexapi.server.PlexServer`): Server your connected to.
157+
title (str): Title of the playlist.
158+
items (Iterable): Iterable of objects that should be in the playlist.
159+
section (:class:`~plexapi.library.LibrarySection`, str):
160+
limit (int): default None.
161+
smart (bool): default False.
162+
163+
**kwargs (dict): is passed to the filters. For a example see the search method.
164+
165+
Returns:
166+
:class:`plexapi.playlist.Playlist`: an instance of created Playlist.
167+
"""
168+
if smart:
169+
return cls._createSmart(server, title, section, limit, **kwargs)
170+
171+
else:
172+
return cls._create(server, title, items)
173+
174+
@classmethod
175+
def _createSmart(cls, server, title, section, limit=None, **kwargs):
176+
""" Create a Smart playlist. """
177+
178+
if not isinstance(section, LibrarySection):
179+
section = server.library.section(section)
180+
181+
sectionType = utils.searchType(section.type)
182+
sectionId = section.key
183+
uuid = section.uuid
184+
uri = 'library://%s/directory//library/sections/%s/all?type=%s' % (uuid,
185+
sectionId,
186+
sectionType)
187+
if limit:
188+
uri = uri + '&limit=%s' % str(limit)
189+
190+
for category, value in kwargs.items():
191+
sectionChoices = section.listChoices(category)
192+
for choice in sectionChoices:
193+
if str(choice.title).lower() == str(value).lower():
194+
uri = uri + '&%s=%s' % (category.lower(), str(choice.key))
195+
196+
uri = uri + '&sourceType=%s' % sectionType
197+
key = '/playlists%s' % utils.joinArgs({
198+
'uri': uri,
199+
'type': section.CONTENT_TYPE,
200+
'title': title,
201+
'smart': 1,
202+
})
203+
data = server.query(key, method=server._session.post)[0]
204+
return cls(server, data, initpath=key)
205+
150206
def copyToUser(self, user):
151207
""" Copy playlist to another user account. """
152208
from plexapi.server import PlexServer

plexapi/server.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ class PlexServer(PlexObject):
9393

9494
def __init__(self, baseurl=None, token=None, session=None, timeout=None):
9595
self._baseurl = baseurl or CONFIG.get('auth.server_baseurl', 'http://localhost:32400')
96+
self._baseurl = self._baseurl.rstrip('/')
9697
self._token = logfilter.add_secret(token or CONFIG.get('auth.server_token'))
9798
self._showSecrets = CONFIG.get('log.show_secrets', '').lower() == 'true'
9899
self._session = session or requests.Session()
@@ -240,14 +241,14 @@ def client(self, name):
240241

241242
raise NotFound('Unknown client name: %s' % name)
242243

243-
def createPlaylist(self, title, items):
244+
def createPlaylist(self, title, items=None, section=None, limit=None, smart=None, **kwargs):
244245
""" Creates and returns a new :class:`~plexapi.playlist.Playlist`.
245246
246247
Parameters:
247248
title (str): Title of the playlist to be created.
248249
items (list<Media>): List of media items to include in the playlist.
249250
"""
250-
return Playlist.create(self, title, items)
251+
return Playlist.create(self, title, items=items, limit=limit, section=section, smart=smart, **kwargs)
251252

252253
def createPlayQueue(self, item, **kwargs):
253254
""" Creates and returns a new :class:`~plexapi.playqueue.PlayQueue`.

plexapi/settings.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,12 @@ class Setting(PlexObject):
101101
"""
102102
_bool_cast = lambda x: True if x == 'true' or x == '1' else False
103103
_bool_str = lambda x: str(x).lower()
104+
_str = lambda x: str(x).encode('utf-8')
104105
TYPES = {
105106
'bool': {'type': bool, 'cast': _bool_cast, 'tostr': _bool_str},
106-
'double': {'type': float, 'cast': float, 'tostr': string_type},
107-
'int': {'type': int, 'cast': int, 'tostr': string_type},
108-
'text': {'type': string_type, 'cast': string_type, 'tostr': string_type},
107+
'double': {'type': float, 'cast': float, 'tostr': _str},
108+
'int': {'type': int, 'cast': int, 'tostr': _str},
109+
'text': {'type': string_type, 'cast': _str, 'tostr': _str},
109110
}
110111

111112
def _loadData(self, data):

plexapi/utils.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import requests
66
import time
77
import zipfile
8-
from datetime import datetime
8+
from datetime import datetime, timedelta
99
from getpass import getpass
1010
from threading import Thread, Event
1111
from tqdm import tqdm
@@ -178,6 +178,11 @@ def toDatetime(value, format=None):
178178
if format:
179179
value = datetime.strptime(value, format)
180180
else:
181+
# https://bugs.python.org/issue30684
182+
# And platform support for before epoch seems to be flaky.
183+
# TODO check for others errors too.
184+
if int(value) == 0:
185+
value = 86400
181186
value = datetime.fromtimestamp(int(value))
182187
return value
183188

plexapi/video.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -224,20 +224,20 @@ def _prettyfilename(self):
224224
# This is just for compat.
225225
return self.title
226226

227-
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
227+
def download(self, savepath=None, keep_original_name=False, **kwargs):
228228
""" Download video files to specified directory.
229229
230230
Parameters:
231231
savepath (str): Defaults to current working dir.
232-
keep_orginal_name (bool): True to keep the original file name otherwise
232+
keep_original_name (bool): True to keep the original file name otherwise
233233
a friendlier is generated.
234234
**kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL()`.
235235
"""
236236
filepaths = []
237237
locations = [i for i in self.iterParts() if i]
238238
for location in locations:
239239
name = location.file
240-
if not keep_orginal_name:
240+
if not keep_original_name:
241241
title = self.title.replace(' ', '.')
242242
name = '%s.%s' % (title, location.container)
243243
if kwargs is not None:
@@ -376,18 +376,18 @@ def get(self, title=None, season=None, episode=None):
376376
""" Alias to :func:`~plexapi.video.Show.episode()`. """
377377
return self.episode(title, season, episode)
378378

379-
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
379+
def download(self, savepath=None, keep_original_name=False, **kwargs):
380380
""" Download video files to specified directory.
381381
382382
Parameters:
383383
savepath (str): Defaults to current working dir.
384-
keep_orginal_name (bool): True to keep the original file name otherwise
384+
keep_original_name (bool): True to keep the original file name otherwise
385385
a friendlier is generated.
386386
**kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL()`.
387387
"""
388388
filepaths = []
389389
for episode in self.episodes():
390-
filepaths += episode.download(savepath, keep_orginal_name, **kwargs)
390+
filepaths += episode.download(savepath, keep_original_name, **kwargs)
391391
return filepaths
392392

393393

@@ -477,18 +477,18 @@ def unwatched(self):
477477
""" Returns list of unwatched :class:`~plexapi.video.Episode` objects. """
478478
return self.episodes(watched=False)
479479

480-
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
480+
def download(self, savepath=None, keep_original_name=False, **kwargs):
481481
""" Download video files to specified directory.
482482
483483
Parameters:
484484
savepath (str): Defaults to current working dir.
485-
keep_orginal_name (bool): True to keep the original file name otherwise
485+
keep_original_name (bool): True to keep the original file name otherwise
486486
a friendlier is generated.
487487
**kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL()`.
488488
"""
489489
filepaths = []
490490
for episode in self.episodes():
491-
filepaths += episode.download(savepath, keep_orginal_name, **kwargs)
491+
filepaths += episode.download(savepath, keep_original_name, **kwargs)
492492
return filepaths
493493

494494
def _defaultSyncTitle(self):

0 commit comments

Comments
 (0)