Skip to content

Commit 9f1d8fd

Browse files
authored
Merge pull request #1 from pkkid/master
Bring my fork to current
2 parents 97903cf + 93de388 commit 9f1d8fd

File tree

14 files changed

+358
-19
lines changed

14 files changed

+358
-19
lines changed

plexapi/audio.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -284,15 +284,15 @@ class Track(Audio, Playable):
284284
art (str): Track artwork (/library/metadata/<ratingkey>/art/<artid>)
285285
chapterSource (TYPE): Unknown
286286
duration (int): Length of this album in seconds.
287-
grandparentArt (str): Artist artowrk.
288-
grandparentKey (str): Artist API URL.
289-
grandparentRatingKey (str): Unique key identifying artist.
290-
grandparentThumb (str): URL to artist thumbnail image.
291-
grandparentTitle (str): Name of the artist for this track.
287+
grandparentArt (str): Album artist artwork.
288+
grandparentKey (str): Album artist API URL.
289+
grandparentRatingKey (str): Unique key identifying album artist.
290+
grandparentThumb (str): URL to album artist thumbnail image.
291+
grandparentTitle (str): Name of the album artist for this track.
292292
guid (str): Unknown (unique ID).
293293
media (list): List of :class:`~plexapi.media.Media` objects for this track.
294294
moods (list): List of :class:`~plexapi.media.Mood` objects for this track.
295-
originalTitle (str): Original track title (if translated).
295+
originalTitle (str): Track artist.
296296
parentIndex (int): Album index.
297297
parentKey (str): Album API URL.
298298
parentRatingKey (int): Unique key identifying album.

plexapi/base.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,15 @@ def delete(self):
421421
'havnt allowed items to be deleted' % self.key)
422422
raise
423423

424+
def history(self, maxresults=9999999, mindate=None):
425+
""" Get Play History for a media item.
426+
Parameters:
427+
maxresults (int): Only return the specified number of results (optional).
428+
mindate (datetime): Min datetime to return results from.
429+
"""
430+
return self._server.history(maxresults=maxresults, mindate=mindate, ratingKey=self.ratingKey)
431+
432+
424433
# The photo tag cant be built atm. TODO
425434
# def arts(self):
426435
# part = '%s/arts' % self.key

plexapi/client.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,8 @@ def goToMedia(self, media, **params):
300300
'address': server_url[1].strip('/'),
301301
'port': server_url[-1],
302302
'key': media.key,
303+
'protocol': server_url[0],
304+
'token': media._server.createToken()
303305
}, **params))
304306

305307
# -------------------
@@ -465,6 +467,13 @@ def playMedia(self, media, offset=0, **params):
465467
server_url = media._server._baseurl.split(':')
466468
server_port = server_url[-1].strip('/')
467469

470+
if hasattr(media, "playlistType"):
471+
mediatype = media.playlistType
472+
elif media.listType == "audio":
473+
mediatype = "music"
474+
else:
475+
mediatype = "video"
476+
468477
if self.product != 'OpenPHT':
469478
try:
470479
self.sendCommand('timeline/subscribe', port=server_port, protocol='http')
@@ -481,7 +490,8 @@ def playMedia(self, media, offset=0, **params):
481490
'port': server_port,
482491
'offset': offset,
483492
'key': media.key,
484-
'token': media._server._token,
493+
'token': media._server.createToken(),
494+
'type': mediatype,
485495
'containerKey': '/playQueues/%s?window=100&own=1' % playqueue.playQueueID,
486496
}, **params))
487497

plexapi/library.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,17 @@ def add(self, name='', type='', agent='', scanner='', location='', language='en'
294294
part += urlencode(kwargs)
295295
return self._server.query(part, method=self._server._session.post)
296296

297+
def history(self, maxresults=9999999, mindate=None):
298+
""" Get Play History for all library Sections for the owner.
299+
Parameters:
300+
maxresults (int): Only return the specified number of results (optional).
301+
mindate (datetime): Min datetime to return results from.
302+
"""
303+
hist = []
304+
for section in self.sections():
305+
hist.extend(section.history(maxresults=maxresults, mindate=mindate))
306+
return hist
307+
297308

298309
class LibrarySection(PlexObject):
299310
""" Base class for a single library section.
@@ -374,7 +385,7 @@ def get(self, title):
374385
Parameters:
375386
title (str): Title of the item to return.
376387
"""
377-
key = '/library/sections/%s/all' % self.key
388+
key = '/library/sections/%s/all?title=%s' % (self.key, title)
378389
return self.fetchItem(key, title__iexact=title)
379390

380391
def all(self, sort=None, **kwargs):
@@ -633,6 +644,14 @@ def sync(self, policy, mediaSettings, client=None, clientId=None, title=None, so
633644

634645
return myplex.sync(client=client, clientId=clientId, sync_item=sync_item)
635646

647+
def history(self, maxresults=9999999, mindate=None):
648+
""" Get Play History for this library Section for the owner.
649+
Parameters:
650+
maxresults (int): Only return the specified number of results (optional).
651+
mindate (datetime): Min datetime to return results from.
652+
"""
653+
return self._server.history(maxresults=maxresults, mindate=mindate, librarySectionID=self.key, accountID=1)
654+
636655

637656
class MovieSection(LibrarySection):
638657
""" Represents a :class:`~plexapi.library.LibrarySection` section containing movies.
@@ -869,7 +888,7 @@ class PhotoSection(LibrarySection):
869888
TYPE (str): 'photo'
870889
"""
871890
ALLOWED_FILTERS = ('all', 'iso', 'make', 'lens', 'aperture', 'exposure', 'device', 'resolution', 'place',
872-
'originallyAvailableAt', 'addedAt', 'title', 'userRating')
891+
'originallyAvailableAt', 'addedAt', 'title', 'userRating', 'tag', 'year')
873892
ALLOWED_SORT = ('addedAt',)
874893
TAG = 'Directory'
875894
TYPE = 'photo'

plexapi/media.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,25 @@ class Label(MediaTag):
419419
FILTER = 'label'
420420

421421

422+
@utils.registerPlexObject
423+
class Tag(MediaTag):
424+
""" Represents a single tag media tag.
425+
426+
Attributes:
427+
TAG (str): 'tag'
428+
FILTER (str): 'tag'
429+
"""
430+
TAG = 'Tag'
431+
FILTER = 'tag'
432+
433+
def _loadData(self, data):
434+
self._data = data
435+
self.id = cast(int, data.attrib.get('id', 0))
436+
self.filter = data.attrib.get('filter')
437+
self.tag = data.attrib.get('tag')
438+
self.title = self.tag
439+
440+
422441
@utils.registerPlexObject
423442
class Country(MediaTag):
424443
""" Represents a single Country media tag.

plexapi/myplex.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,19 @@ def claimToken(self):
600600
raise BadRequest('(%s) %s %s; %s' % (response.status_code, codename, response.url, errtext))
601601
return response.json()['token']
602602

603+
def history(self, maxresults=9999999, mindate=None):
604+
""" Get Play History for all library sections on all servers for the owner.
605+
Parameters:
606+
maxresults (int): Only return the specified number of results (optional).
607+
mindate (datetime): Min datetime to return results from.
608+
"""
609+
servers = [x for x in self.resources() if x.provides == 'server' and x.owned]
610+
hist = []
611+
for server in servers:
612+
conn = server.connect()
613+
hist.extend(conn.history(maxresults=maxresults, mindate=mindate, accountID=1))
614+
return hist
615+
603616

604617
class MyPlexUser(PlexObject):
605618
""" This object represents non-signed in users such as friends and linked
@@ -654,6 +667,8 @@ def _loadData(self, data):
654667
self.title = data.attrib.get('title', '')
655668
self.username = data.attrib.get('username', '')
656669
self.servers = self.findItems(data, MyPlexServerShare)
670+
for server in self.servers:
671+
server.accountID = self.id
657672

658673
def get_token(self, machineIdentifier):
659674
try:
@@ -663,6 +678,29 @@ def get_token(self, machineIdentifier):
663678
except Exception:
664679
log.exception('Failed to get access token for %s' % self.title)
665680

681+
def server(self, name):
682+
""" Returns the :class:`~plexapi.myplex.MyPlexServerShare` that matches the name specified.
683+
684+
Parameters:
685+
name (str): Name of the server to return.
686+
"""
687+
for server in self.servers:
688+
if name.lower() == server.name.lower():
689+
return server
690+
691+
raise NotFound('Unable to find server %s' % name)
692+
693+
def history(self, maxresults=9999999, mindate=None):
694+
""" Get all Play History for a user in all shared servers.
695+
Parameters:
696+
maxresults (int): Only return the specified number of results (optional).
697+
mindate (datetime): Min datetime to return results from.
698+
"""
699+
hist = []
700+
for server in self.servers:
701+
hist.extend(server.history(maxresults=maxresults, mindate=mindate))
702+
return hist
703+
666704

667705
class Section(PlexObject):
668706
""" This refers to a shared section. The raw xml for the data presented here
@@ -689,6 +727,16 @@ def _loadData(self, data):
689727
self.type = data.attrib.get('type')
690728
self.shared = utils.cast(bool, data.attrib.get('shared'))
691729

730+
def history(self, maxresults=9999999, mindate=None):
731+
""" Get all Play History for a user for this section in this shared server.
732+
Parameters:
733+
maxresults (int): Only return the specified number of results (optional).
734+
mindate (datetime): Min datetime to return results from.
735+
"""
736+
server = self._server._server.resource(self._server.name).connect()
737+
return server.history(maxresults=maxresults, mindate=mindate,
738+
accountID=self._server.accountID, librarySectionID=self.sectionKey)
739+
692740

693741
class MyPlexServerShare(PlexObject):
694742
""" Represents a single user's server reference. Used for library sharing.
@@ -711,6 +759,7 @@ def _loadData(self, data):
711759
""" Load attribute values from Plex XML response. """
712760
self._data = data
713761
self.id = utils.cast(int, data.attrib.get('id'))
762+
self.accountID = utils.cast(int, data.attrib.get('accountID'))
714763
self.serverId = utils.cast(int, data.attrib.get('serverId'))
715764
self.machineIdentifier = data.attrib.get('machineIdentifier')
716765
self.name = data.attrib.get('name')
@@ -720,7 +769,21 @@ def _loadData(self, data):
720769
self.owned = utils.cast(bool, data.attrib.get('owned'))
721770
self.pending = utils.cast(bool, data.attrib.get('pending'))
722771

772+
def section(self, name):
773+
""" Returns the :class:`~plexapi.myplex.Section` that matches the name specified.
774+
775+
Parameters:
776+
name (str): Name of the section to return.
777+
"""
778+
for section in self.sections():
779+
if name.lower() == section.title.lower():
780+
return section
781+
782+
raise NotFound('Unable to find section %s' % name)
783+
723784
def sections(self):
785+
""" Returns a list of all :class:`~plexapi.myplex.Section` objects shared with this user.
786+
"""
724787
url = MyPlexAccount.FRIENDSERVERS.format(machineId=self.machineIdentifier, serverId=self.id)
725788
data = self._server.query(url)
726789
sections = []
@@ -731,6 +794,15 @@ def sections(self):
731794

732795
return sections
733796

797+
def history(self, maxresults=9999999, mindate=None):
798+
""" Get all Play History for a user in this shared server.
799+
Parameters:
800+
maxresults (int): Only return the specified number of results (optional).
801+
mindate (datetime): Min datetime to return results from.
802+
"""
803+
server = self._server.resource(self.name).connect()
804+
return server.history(maxresults=maxresults, mindate=mindate, accountID=self.accountID)
805+
734806

735807
class MyPlexResource(PlexObject):
736808
""" This object represents resources connected to your Plex server that can provide

plexapi/photo.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ def _loadData(self, data):
117117
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
118118
self.year = utils.cast(int, data.attrib.get('year'))
119119
self.media = self.findItems(data, media.Media)
120+
self.tag = self.findItems(data, media.Tag)
120121

121122
def photoalbum(self):
122123
""" Return this photo's :class:`~plexapi.photo.Photoalbum`. """

plexapi/server.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ def installUpdate(self):
322322
# figure out what method this is..
323323
return self.query(part, method=self._session.put)
324324

325-
def history(self, maxresults=9999999, mindate=None):
325+
def history(self, maxresults=9999999, mindate=None, ratingKey=None, accountID=None, librarySectionID=None):
326326
""" Returns a list of media items from watched history. If there are many results, they will
327327
be fetched from the server in batches of X_PLEX_CONTAINER_SIZE amounts. If you're only
328328
looking for the first <num> results, it would be wise to set the maxresults option to that
@@ -332,9 +332,18 @@ def history(self, maxresults=9999999, mindate=None):
332332
maxresults (int): Only return the specified number of results (optional).
333333
mindate (datetime): Min datetime to return results from. This really helps speed
334334
up the result listing. For example: datetime.now() - timedelta(days=7)
335+
ratingKey (int/str) Request history for a specific ratingKey item.
336+
accountID (int/str) Request history for a specific account ID.
337+
librarySectionID (int/str) Request history for a specific library section ID.
335338
"""
336339
results, subresults = [], '_init'
337340
args = {'sort': 'viewedAt:desc'}
341+
if ratingKey:
342+
args['metadataItemID'] = ratingKey
343+
if accountID:
344+
args['accountID'] = accountID
345+
if librarySectionID:
346+
args['librarySectionID'] = librarySectionID
338347
if mindate:
339348
args['viewedAt>'] = int(mindate.timestamp())
340349
args['X-Plex-Container-Start'] = 0

plexapi/video.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from plexapi.exceptions import BadRequest, NotFound
44
from plexapi.base import Playable, PlexPartialObject
55
from plexapi.compat import quote_plus
6+
import os
67

78

89
class Video(PlexPartialObject):
@@ -89,6 +90,37 @@ def _defaultSyncTitle(self):
8990
""" Returns str, default title for a new syncItem. """
9091
return self.title
9192

93+
def subtitleStreams(self):
94+
""" Returns a list of :class:`~plexapi.media.SubtitleStream` objects for all MediaParts. """
95+
streams = []
96+
97+
parts = self.iterParts()
98+
for part in parts:
99+
streams += part.subtitleStreams()
100+
return streams
101+
102+
def uploadSubtitles(self, filepath):
103+
""" Upload Subtitle file for video. """
104+
url = '%s/subtitles' % self.key
105+
filename = os.path.basename(filepath)
106+
subFormat = os.path.splitext(filepath)[1][1:]
107+
with open(filepath, 'rb') as subfile:
108+
params = {'title': filename,
109+
'format': subFormat
110+
}
111+
headers = {'Accept': 'text/plain, */*'}
112+
self._server.query(url, self._server._session.post, data=subfile, params=params, headers=headers)
113+
114+
def removeSubtitles(self, streamID=None, streamTitle=None):
115+
""" Remove Subtitle from movie's subtitles listing.
116+
117+
Note: If subtitle file is located inside video directory it will bbe deleted.
118+
Files outside of video directory are not effected.
119+
"""
120+
for stream in self.subtitleStreams():
121+
if streamID == stream.id or streamTitle == stream.title:
122+
self._server.query(stream.key, self._server._session.delete)
123+
92124
def posters(self):
93125
""" Returns list of available poster objects. :class:`~plexapi.media.Poster`:"""
94126

@@ -224,14 +256,6 @@ def locations(self):
224256
"""
225257
return [part.file for part in self.iterParts() if part]
226258

227-
def subtitleStreams(self):
228-
""" Returns a list of :class:`~plexapi.media.SubtitleStream` objects for all MediaParts. """
229-
streams = []
230-
for elem in self.media:
231-
for part in elem.parts:
232-
streams += part.subtitleStreams()
233-
return streams
234-
235259
def _prettyfilename(self):
236260
# This is just for compat.
237261
return self.title

requirements_dev.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ pillow
88
pytest
99
pytest-cache
1010
pytest-cov
11-
pytest-mock
11+
pytest-mock<=1.11.1
1212
recommonmark
1313
requests
1414
sphinx

0 commit comments

Comments
 (0)