Skip to content

Commit bf3f0c1

Browse files
authored
Merge branch 'master' into feature/album_types
2 parents ae4b4d7 + a3178f3 commit bf3f0c1

File tree

16 files changed

+312
-7
lines changed

16 files changed

+312
-7
lines changed

.github/workflows/ci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,6 @@ jobs:
251251
coverage xml
252252
253253
- name: Upload ${{ matrix.plex }} coverage to Codecov
254-
uses: codecov/codecov-action@v2.0.3
254+
uses: codecov/codecov-action@v2.1.0
255255
with:
256256
flags: ${{ matrix.plex }}

plexapi/audio.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,3 +426,7 @@ def trackNumber(self):
426426
def _defaultSyncTitle(self):
427427
""" Returns str, default title for a new syncItem. """
428428
return '%s - %s - %s' % (self.grandparentTitle, self.parentTitle, self.title)
429+
430+
def _getWebURL(self, base=None):
431+
""" Get the Plex Web URL with the correct parameters. """
432+
return self._server._buildWebURL(base=base, endpoint='details', key=self.parentKey)

plexapi/base.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,12 +579,28 @@ def delete(self):
579579

580580
def history(self, maxresults=9999999, mindate=None):
581581
""" Get Play History for a media item.
582+
582583
Parameters:
583584
maxresults (int): Only return the specified number of results (optional).
584585
mindate (datetime): Min datetime to return results from.
585586
"""
586587
return self._server.history(maxresults=maxresults, mindate=mindate, ratingKey=self.ratingKey)
587588

589+
def _getWebURL(self, base=None):
590+
""" Get the Plex Web URL with the correct parameters.
591+
Private method to allow overriding parameters from subclasses.
592+
"""
593+
return self._server._buildWebURL(base=base, endpoint='details', key=self.key)
594+
595+
def getWebURL(self, base=None):
596+
""" Returns the Plex Web URL for a media item.
597+
598+
Parameters:
599+
base (str): The base URL before the fragment (``#!``).
600+
Default is https://app.plex.tv/desktop.
601+
"""
602+
return self._getWebURL(base=base)
603+
588604

589605
class Playable(object):
590606
""" This is a general place to store functions specific to media that is Playable.

plexapi/library.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,10 +456,45 @@ def get(self, title):
456456
457457
Parameters:
458458
title (str): Title of the item to return.
459+
460+
Raises:
461+
:exc:`~plexapi.exceptions.NotFound`: The title is not found in the library.
459462
"""
460-
key = '/library/sections/%s/all?title=%s' % (self.key, quote(title, safe=''))
463+
key = '/library/sections/%s/all?includeGuids=1&title=%s' % (self.key, quote(title, safe=''))
461464
return self.fetchItem(key, title__iexact=title)
462465

466+
def getGuid(self, guid):
467+
""" Returns the media item with the specified external IMDB, TMDB, or TVDB ID.
468+
Note: This search uses a PlexAPI operator so performance may be slow. All items from the
469+
entire Plex library need to be retrieved for each guid search. It is recommended to create
470+
your own lookup dictionary if you are searching for a lot of external guids.
471+
472+
Parameters:
473+
guid (str): The external guid of the item to return.
474+
Examples: IMDB ``imdb://tt0944947``, TMDB ``tmdb://1399``, TVDB ``tvdb://121361``.
475+
476+
Raises:
477+
:exc:`~plexapi.exceptions.NotFound`: The guid is not found in the library.
478+
479+
Example:
480+
481+
.. code-block:: python
482+
483+
# This will retrieve all items in the entire library 3 times
484+
result1 = library.getGuid('imdb://tt0944947')
485+
result2 = library.getGuid('tmdb://1399')
486+
result3 = library.getGuid('tvdb://121361')
487+
488+
# This will only retrieve all items in the library once to create a lookup dictionary
489+
guidLookup = {guid.id: item for item in library.all() for guid in item.guids}
490+
result1 = guidLookup['imdb://tt0944947']
491+
result2 = guidLookup['tmdb://1399']
492+
result3 = guidLookup['tvdb://121361']
493+
494+
"""
495+
key = '/library/sections/%s/all?includeGuids=1' % self.key
496+
return self.fetchItem(key, Guid__id__iexact=guid)
497+
463498
def all(self, libtype=None, **kwargs):
464499
""" Returns a list of all items from this library section.
465500
See description of :func:`~plexapi.library.LibrarySection.search()` for details about filtering / sorting.
@@ -979,6 +1014,8 @@ def _buildSearchKey(self, title=None, sort=None, libtype=None, limit=None, filte
9791014
"""
9801015
args = {}
9811016
filter_args = []
1017+
1018+
args['includeGuids'] = int(bool(kwargs.pop('includeGuids', True)))
9821019
for field, values in list(kwargs.items()):
9831020
if field.split('__')[-1] not in OPERATORS:
9841021
filter_args.append(self._validateFilterField(field, values, libtype))
@@ -1456,6 +1493,23 @@ def filterFields(self, mediaType=None):
14561493
def listChoices(self, category, libtype=None, **kwargs):
14571494
return self.listFilterChoices(field=category, libtype=libtype)
14581495

1496+
def getWebURL(self, base=None, tab=None, key=None):
1497+
""" Returns the Plex Web URL for the library.
1498+
1499+
Parameters:
1500+
base (str): The base URL before the fragment (``#!``).
1501+
Default is https://app.plex.tv/desktop.
1502+
tab (str): The library tab (recommended, library, collections, playlists, timeline).
1503+
key (str): A hub key.
1504+
"""
1505+
params = {'source': self.key}
1506+
if tab is not None:
1507+
params['pivot'] = tab
1508+
if key is not None:
1509+
params['key'] = key
1510+
params['pageType'] = 'list'
1511+
return self._server._buildWebURL(base=base, **params)
1512+
14591513

14601514
class MovieSection(LibrarySection):
14611515
""" Represents a :class:`~plexapi.library.LibrarySection` section containing movies.
@@ -1857,6 +1911,7 @@ def _loadData(self, data):
18571911
self.style = data.attrib.get('style')
18581912
self.title = data.attrib.get('title')
18591913
self.type = data.attrib.get('type')
1914+
self._section = None # cache for self.section
18601915

18611916
def __len__(self):
18621917
return self.size
@@ -1868,6 +1923,13 @@ def reload(self):
18681923
self.more = False
18691924
self.size = len(self.items)
18701925

1926+
def section(self):
1927+
""" Returns the :class:`~plexapi.library.LibrarySection` this hub belongs to.
1928+
"""
1929+
if self._section is None:
1930+
self._section = self._server.library.sectionByID(self.librarySectionID)
1931+
return self._section
1932+
18711933

18721934
class HubMediaTag(PlexObject):
18731935
""" Base class of hub media tag search results.

plexapi/mixins.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,9 @@ def _parseFilters(self, content):
601601
key += '='
602602
value = value[1:]
603603

604-
if key == 'type':
604+
if key == 'includeGuids':
605+
filters['includeGuids'] = int(value)
606+
elif key == 'type':
605607
filters['libtype'] = utils.reverseSearchType(value)
606608
elif key == 'sort':
607609
filters['sort'] = value.split(',')

plexapi/photo.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ def download(self, savepath=None, keep_original_name=False, showstatus=False):
137137
filepaths.append(filepath)
138138
return filepaths
139139

140+
def _getWebURL(self, base=None):
141+
""" Get the Plex Web URL with the correct parameters. """
142+
return self._server._buildWebURL(base=base, endpoint='details', key=self.key, legacy=1)
143+
140144

141145
@utils.registerPlexObject
142146
class Photo(PlexPartialObject, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixin, TagMixin):
@@ -301,3 +305,7 @@ def download(self, savepath=None, keep_original_name=False, showstatus=False):
301305
if filepath:
302306
filepaths.append(filepath)
303307
return filepaths
308+
309+
def _getWebURL(self, base=None):
310+
""" Get the Plex Web URL with the correct parameters. """
311+
return self._server._buildWebURL(base=base, endpoint='details', key=self.parentKey, legacy=1)

plexapi/playlist.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,3 +464,7 @@ def sync(self, videoQuality=None, photoResolution=None, audioBitrate=None, clien
464464
raise Unsupported('Unsupported playlist content')
465465

466466
return myplex.sync(sync_item, client=client, clientId=clientId)
467+
468+
def _getWebURL(self, base=None):
469+
""" Get the Plex Web URL with the correct parameters. """
470+
return self._server._buildWebURL(base=base, endpoint='playlist', key=self.key)

plexapi/server.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -890,6 +890,42 @@ def resources(self):
890890
key = '/statistics/resources?timespan=6'
891891
return self.fetchItems(key, StatisticsResources)
892892

893+
def _buildWebURL(self, base=None, endpoint=None, **kwargs):
894+
""" Build the Plex Web URL for the object.
895+
896+
Parameters:
897+
base (str): The base URL before the fragment (``#!``).
898+
Default is https://app.plex.tv/desktop.
899+
endpoint (str): The Plex Web URL endpoint.
900+
None for server, 'playlist' for playlists, 'details' for all other media types.
901+
**kwargs (dict): Dictionary of URL parameters.
902+
"""
903+
if base is None:
904+
base = 'https://app.plex.tv/desktop/'
905+
906+
if endpoint:
907+
return '%s#!/server/%s/%s%s' % (
908+
base, self.machineIdentifier, endpoint, utils.joinArgs(kwargs)
909+
)
910+
else:
911+
return '%s#!/media/%s/com.plexapp.plugins.library%s' % (
912+
base, self.machineIdentifier, utils.joinArgs(kwargs)
913+
)
914+
915+
def getWebURL(self, base=None, playlistTab=None):
916+
""" Returns the Plex Web URL for the server.
917+
918+
Parameters:
919+
base (str): The base URL before the fragment (``#!``).
920+
Default is https://app.plex.tv/desktop.
921+
playlistTab (str): The playlist tab (audio, video, photo). Only used for the playlist URL.
922+
"""
923+
if playlistTab is not None:
924+
params = {'source': 'playlists', 'pivot': 'playlists.%s' % playlistTab}
925+
else:
926+
params = {'key': '/hubs', 'pageType': 'hub'}
927+
return self._buildWebURL(base=base, **params)
928+
893929

894930
class Account(PlexObject):
895931
""" Contains the locally cached MyPlex account information. The properties provided don't

requirements_dev.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ flake8==3.9.2
77
pillow==8.3.2
88
pytest==6.2.5
99
pytest-cache==1.0
10-
pytest-cov==2.12.1
10+
pytest-cov==3.0.0
1111
pytest-mock<3.6.2
1212
recommonmark==0.7.1
1313
requests==2.26.0
1414
requests-mock==1.9.3
15-
sphinx==4.1.2
16-
sphinx-rtd-theme==0.5.2
17-
tqdm==4.62.2
15+
sphinx==4.2.0
16+
sphinx-rtd-theme==1.0.0
17+
tqdm==4.62.3
1818
websocket-client==1.2.1

tests/test_audio.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# -*- coding: utf-8 -*-
2+
from urllib.parse import quote_plus
3+
24
import pytest
35
from plexapi.exceptions import BadRequest
46

@@ -115,6 +117,14 @@ def test_audio_Artist_media_tags(artist):
115117
test_media.tag_style(artist)
116118

117119

120+
def test_video_Artist_PlexWebURL(plex, artist):
121+
url = artist.getWebURL()
122+
assert url.startswith('https://app.plex.tv/desktop')
123+
assert plex.machineIdentifier in url
124+
assert 'details' in url
125+
assert quote_plus(artist.key) in url
126+
127+
118128
def test_audio_Album_attrs(album):
119129
assert utils.is_datetime(album.addedAt)
120130
if album.art:
@@ -214,6 +224,14 @@ def test_audio_Album_media_tags(album):
214224
test_media.tag_style(album)
215225

216226

227+
def test_video_Album_PlexWebURL(plex, album):
228+
url = album.getWebURL()
229+
assert url.startswith('https://app.plex.tv/desktop')
230+
assert plex.machineIdentifier in url
231+
assert 'details' in url
232+
assert quote_plus(album.key) in url
233+
234+
217235
def test_audio_Track_attrs(album):
218236
track = album.get("As Colourful As Ever").reload()
219237
assert utils.is_datetime(track.addedAt)
@@ -357,6 +375,14 @@ def test_audio_Track_media_tags(track):
357375
test_media.tag_mood(track)
358376

359377

378+
def test_video_Track_PlexWebURL(plex, track):
379+
url = track.getWebURL()
380+
assert url.startswith('https://app.plex.tv/desktop')
381+
assert plex.machineIdentifier in url
382+
assert 'details' in url
383+
assert quote_plus(track.parentKey) in url
384+
385+
360386
def test_audio_Audio_section(artist, album, track):
361387
assert artist.section()
362388
assert album.section()

0 commit comments

Comments
 (0)