Skip to content

Commit 9d4966d

Browse files
committed
Merge branch 'master' of github.com:pkkid/python-plexapi
2 parents bf57116 + 5980abe commit 9d4966d

File tree

15 files changed

+164
-35
lines changed

15 files changed

+164
-35
lines changed

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Plex Web Client. A few of the many features we currently support are:
1717
* Perform library actions such as scan, analyze, empty trash.
1818
* Remote control and play media on connected clients.
1919
* Listen in on all Plex Server notifications.
20-
20+
2121

2222
Installation & Documentation
2323
----------------------------

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')
780-
ALLOWED_SORT = ('addedAt', 'lastViewedAt', 'viewCount', 'titleSort')
787+
ALLOWED_FILTERS = ('genre', 'country', 'collection', 'mood', 'track.userRating')
788+
ALLOWED_SORT = ('addedAt', 'lastViewedAt', 'viewCount', 'titleSort', 'userRating')
781789
TAG = 'Directory'
782790
TYPE = 'artist'
783791

plexapi/media.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,35 @@ def audioStreams(self):
124124
def subtitleStreams(self):
125125
""" Returns a list of :class:`~plexapi.media.SubtitleStream` objects in this MediaPart. """
126126
return [stream for stream in self.streams if stream.streamType == SubtitleStream.STREAMTYPE]
127+
128+
def setDefaultAudioStream(self, stream):
129+
""" Set the default :class:`~plexapi.media.AudioStream` for this MediaPart.
127130
131+
Parameters:
132+
stream (:class:`~plexapi.media.AudioStream`): AudioStream to set as default
133+
"""
134+
if isinstance(stream, AudioStream):
135+
key = "/library/parts/%d?audioStreamID=%d&allParts=1" % (self.id, stream.id)
136+
else:
137+
key = "/library/parts/%d?audioStreamID=%d&allParts=1" % (self.id, stream)
138+
self._server.query(key, method=self._server._session.put)
139+
140+
def setDefaultSubtitleStream(self, stream):
141+
""" Set the default :class:`~plexapi.media.SubtitleStream` for this MediaPart.
142+
143+
Parameters:
144+
stream (:class:`~plexapi.media.SubtitleStream`): SubtitleStream to set as default.
145+
"""
146+
if isinstance(stream, SubtitleStream):
147+
key = "/library/parts/%d?subtitleStreamID=%d&allParts=1" % (self.id, stream.id)
148+
else:
149+
key = "/library/parts/%d?subtitleStreamID=%d&allParts=1" % (self.id, stream)
150+
self._server.query(key, method=self._server._session.put)
151+
152+
def resetDefaultSubtitleStream(self):
153+
""" Set default subtitle of this MediaPart to 'none'. """
154+
key = "/library/parts/%d?subtitleStreamID=0&allParts=1" % (self.id)
155+
self._server.query(key, method=self._server._session.put)
128156

129157
class MediaPartStream(PlexObject):
130158
""" Base class for media streams. These consist of video, audio and subtitles.
@@ -256,6 +284,7 @@ class SubtitleStream(MediaPartStream):
256284
Attributes:
257285
TAG (str): 'Stream'
258286
STREAMTYPE (int): 3
287+
forced (bool): True if this is a forced subtitle
259288
format (str): Subtitle format (ex: srt).
260289
key (str): Key of this subtitle stream (ex: /library/streams/212284).
261290
title (str): Title of this subtitle stream.
@@ -266,6 +295,7 @@ class SubtitleStream(MediaPartStream):
266295
def _loadData(self, data):
267296
""" Load attribute values from Plex XML response. """
268297
super(SubtitleStream, self)._loadData(data)
298+
self.forced = cast(bool, data.attrib.get('forced', '0'))
269299
self.format = data.attrib.get('format')
270300
self.key = data.attrib.get('key')
271301
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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

0 commit comments

Comments
 (0)