Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reviews, Extras, and Online Media Source Options #530

Merged
merged 66 commits into from
Jun 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
4b908a8
create media.Review class
blacktwin Jul 15, 2020
c7f8b86
add reviews method to video.Movie class
blacktwin Jul 15, 2020
7b364a5
create Extra class in video
blacktwin Jul 15, 2020
3fcfe23
add extras method to video.Movie class
blacktwin Jul 15, 2020
6f37a7b
move hubs into video.Video
blacktwin Jul 15, 2020
451b689
create MediaContainer class in base.py
blacktwin Jul 15, 2020
c0454f6
import MediaContainer from base
blacktwin Jul 15, 2020
f36f549
video.Video hubs method correction
blacktwin Jul 16, 2020
c46aa3b
create augmentation method in video.Video
blacktwin Jul 16, 2020
fd89bac
add SETTINGS endpoint for user settings
blacktwin Jul 17, 2020
5051573
create onlineMediaSources method
blacktwin Jul 17, 2020
120dbc5
create AccountOptOut class
blacktwin Jul 17, 2020
b917a33
create settings method and myplex.AccountSettings class
blacktwin Jul 17, 2020
de58965
add check in augmentation method for Plex Pass or tidal opt-in
blacktwin Jul 17, 2020
998ed04
update docstrings for AccountSettings and AccountOptOut
blacktwin Jul 17, 2020
34e1ea3
master conflict resolution
blacktwin Aug 30, 2020
1316d4a
spacing
blacktwin Aug 30, 2020
de83d24
add updateOptOut method for MyAccount.AccountOptOut class
blacktwin Aug 30, 2020
2d08f2d
add test for opting out of a onlineMediaSource
blacktwin Aug 30, 2020
132e379
Merge branch 'master' into reviews_extras
blacktwin Aug 30, 2020
aaa0a97
add tests for presence of movie reviews and extras
blacktwin Aug 30, 2020
6dbb454
Merge remote-tracking branch 'origin/reviews_extras' into reviews_extras
blacktwin Aug 30, 2020
bb9a1b2
flake fix
blacktwin Sep 1, 2020
675cfd3
append original choice back in
blacktwin Sep 1, 2020
1f683c5
Merge branch 'master' into reviews_extras
blacktwin Oct 7, 2020
d948669
failing in the unclaimed server CI run
blacktwin Oct 7, 2020
06bbd28
correction, failing in the unclaimed server CI run
blacktwin Oct 7, 2020
3865ab9
Merge remote-tracking branch 'remotes/origin/master' into reviews_extras
blacktwin Oct 23, 2020
ddbd07e
Merge branch 'master' into reviews_extras
blacktwin Jan 18, 2021
0f0cd03
correction of import
blacktwin Jan 18, 2021
82e6264
Merge branch 'master' into reviews_extras
blacktwin Jan 25, 2021
63a1d10
remove hubs method from Video class
blacktwin Jan 25, 2021
9d2ec95
Merge branch 'master' into reviews_extras
blacktwin Mar 9, 2021
227bb51
remove Release
blacktwin Apr 12, 2021
f92878d
Merge branch 'master' into reviews_extras
blacktwin Apr 12, 2021
790d125
flake fix
blacktwin Apr 12, 2021
5d67036
fixing import
blacktwin Apr 12, 2021
0bacdbe
readding MediaContainer import
blacktwin Apr 12, 2021
8aaa0c0
spelling & indent correction
blacktwin Apr 14, 2021
c0b6efc
Merge branch 'master' into reviews_extras
blacktwin May 24, 2021
0c8e5f9
Extra atrributes update cast int
blacktwin May 27, 2021
aa0596d
SETTINGS url string substitution update %s instead of .format()
blacktwin May 27, 2021
4f6634d
Review attribute ordering
blacktwin May 27, 2021
76719d9
onlineMediaSources clean up
blacktwin May 27, 2021
277f2ea
onlineMediaSources further clean up
blacktwin May 27, 2021
226b983
MyPlex.settings slight clean up and property
blacktwin May 27, 2021
49732cf
Extra.addedAt docstring update.
blacktwin May 27, 2021
9f5f2b7
MediaContainer attribute ordering
blacktwin May 28, 2021
9982b8e
MediaContainer docstring
blacktwin May 28, 2021
70ad8ba
updateOptOut docstring
blacktwin May 28, 2021
44b4066
Clip.section docstring update
blacktwin May 28, 2021
ca97c60
replace eval with json.loads
blacktwin May 31, 2021
5013028
SETTINGS url change and OPTOUT url creation
blacktwin May 31, 2021
4a4a945
Move section method from Clip to Extra
blacktwin May 31, 2021
056b5cf
flag onlineMediaSources required to be authenticated
blacktwin May 31, 2021
fab55e1
Merge branch 'master' into reviews_extras
JonnyWong16 Jun 6, 2021
8f2ad95
Merge branch 'master' into reviews_extras
JonnyWong16 Jun 6, 2021
6f44933
Clean up Extra object
JonnyWong16 Jun 6, 2021
1d03116
Cleanup movie reviews and extras
JonnyWong16 Jun 6, 2021
f8c7fef
Cleanup Account onlineMediaSources OptOut methods
JonnyWong16 Jun 6, 2021
4d60101
Remove Account settings
JonnyWong16 Jun 6, 2021
aaa9020
Update account onlineMediaSources optOut test
JonnyWong16 Jun 7, 2021
0626334
Add tests for movie augmentation, reviews, and extras
JonnyWong16 Jun 7, 2021
7ddf47c
Add doc string for Review object
JonnyWong16 Jun 7, 2021
6405d22
Mark movies extras test only available for Plex Pass
JonnyWong16 Jun 7, 2021
67b3fc6
Allow clips and extras to be streamed and downloaded
JonnyWong16 Jun 7, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion plexapi/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ def getStreamURL(self, **params):
Raises:
:exc:`~plexapi.exceptions.Unsupported`: When the item doesn't support fetching a stream URL.
"""
if self.TYPE not in ('movie', 'episode', 'track'):
if self.TYPE not in ('movie', 'episode', 'track', 'clip'):
raise Unsupported('Fetching stream URL for %s is unsupported.' % self.TYPE)
mvb = params.get('maxVideoBitrate')
vr = params.get('videoResolution', '')
Expand Down Expand Up @@ -715,3 +715,34 @@ def updateTimeline(self, time, state='stopped', duration=None):
key %= (self.ratingKey, self.key, time, state, durationStr)
self._server.query(key)
self.reload()


class MediaContainer(PlexObject):
""" Represents a single MediaContainer.

Attributes:
TAG (str): 'MediaContainer'
blacktwin marked this conversation as resolved.
Show resolved Hide resolved
allowSync (int): Sync/Download is allowed/disallowed for feature.
augmentationKey (str): API URL (/library/metadata/augmentations/<augmentationKey>).
identifier (str): "com.plexapp.plugins.library"
librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID.
librarySectionTitle (str): :class:`~plexapi.library.LibrarySection` title.
librarySectionUUID (str): :class:`~plexapi.library.LibrarySection` UUID.
mediaTagPrefix (str): "/system/bundle/media/flags/"
mediaTagVersion (int): Unknown
size (int): The number of items in the hub.

"""
TAG = 'MediaContainer'

def _loadData(self, data):
self._data = data
self.allowSync = utils.cast(int, data.attrib.get('allowSync'))
self.augmentationKey = data.attrib.get('augmentationKey')
self.identifier = data.attrib.get('identifier')
self.librarySectionID = utils.cast(int, data.attrib.get('librarySectionID'))
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
self.librarySectionUUID = data.attrib.get('librarySectionUUID')
self.mediaTagPrefix = data.attrib.get('mediaTagPrefix')
self.mediaTagVersion = data.attrib.get('mediaTagVersion')
self.size = utils.cast(int, data.attrib.get('size'))
1 change: 0 additions & 1 deletion plexapi/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
updatedAt (datatime): Datetime the collection was updated.
userRating (float): Rating of the collection (0.0 - 10.0) equaling (0 stars - 5 stars).
"""

TAG = 'Directory'
TYPE = 'collection'

Expand Down
27 changes: 27 additions & 0 deletions plexapi/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,33 @@ class Guid(GuidTag):
TAG = 'Guid'


@utils.registerPlexObject
class Review(PlexObject):
""" Represents a single Review for a Movie.

Attributes:
TAG (str): 'Review'
filter (str): filter for reviews?
id (int): The ID of the review.
image (str): The image uri for the review.
link (str): The url to the online review.
source (str): The source of the review.
tag (str): The name of the reviewer.
text (str): The text of the review.
"""
TAG = 'Review'

def _loadData(self, data):
self._data = data
self.filter = data.attrib.get('filter')
self.id = utils.cast(int, data.attrib.get('id', 0))
self.image = data.attrib.get('image')
self.link = data.attrib.get('link')
self.source = data.attrib.get('source')
self.tag = data.attrib.get('tag')
self.text = data.attrib.get('text')


class BaseImage(PlexObject):
""" Base class for all Art, Banner, and Poster objects.

Expand Down
59 changes: 59 additions & 0 deletions plexapi/myplex.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class MyPlexAccount(PlexObject):
REQUESTS = 'https://plex.tv/api/invites/requests' # get
SIGNIN = 'https://plex.tv/users/sign_in.xml' # get with auth
WEBHOOKS = 'https://plex.tv/api/v2/user/webhooks' # get, post with data
OPTOUTS = 'https://plex.tv/api/v2/user/%(userUUID)s/settings/opt_outs' # get
LINK = 'https://plex.tv/api/v2/pins/link' # put
# Hub sections
VOD = 'https://vod.provider.plex.tv/' # get
Expand Down Expand Up @@ -690,6 +691,13 @@ def tidal(self):
elem = ElementTree.fromstring(req.text)
return self.findItems(elem)

def onlineMediaSources(self):
""" Returns a list of user account Online Media Sources settings :class:`~plexapi.myplex.AccountOptOut`
"""
url = self.OPTOUTS % {'userUUID': self.uuid}
elem = self.query(url)
return self.findItems(elem, cls=AccountOptOut, etag='optOut')

def link(self, pin):
""" Link a device to the account using a pin code.

Expand Down Expand Up @@ -1327,3 +1335,54 @@ def _chooseConnection(ctype, name, results):
log.debug('Connecting to %s: %s?X-Plex-Token=%s', ctype, results[0]._baseurl, results[0]._token)
return results[0]
raise NotFound('Unable to connect to %s: %s' % (ctype.lower(), name))


class AccountOptOut(PlexObject):
""" Represents a single AccountOptOut
'https://plex.tv/api/v2/user/{userUUID}/settings/opt_outs'

Attributes:
TAG (str): optOut
key (str): Online Media Source key
value (str): Online Media Source opt_in, opt_out, or opt_out_managed
"""
TAG = 'optOut'
CHOICES = {'opt_in', 'opt_out', 'opt_out_managed'}

def _loadData(self, data):
self.key = data.attrib.get('key')
self.value = data.attrib.get('value')

def _updateOptOut(self, option):
""" Sets the Online Media Sources option.

Parameters:
option (str): see CHOICES

Raises:
:exc:`~plexapi.exceptions.NotFound`: ``option`` str not found in CHOICES.
"""
if option not in self.CHOICES:
raise NotFound('%s not found in available choices: %s' % (option, self.CHOICES))
url = self._server.OPTOUTS % {'userUUID': self._server.uuid}
params = {'key': self.key, 'value': option}
self._server.query(url, method=self._server._session.post, params=params)
self.value = option # assume query successful and set the value to option

def optIn(self):
""" Sets the Online Media Source to "Enabled". """
self._updateOptOut('opt_in')

def optOut(self):
""" Sets the Online Media Source to "Disabled". """
self._updateOptOut('opt_out')

def optOutManaged(self):
""" Sets the Online Media Source to "Disabled for Managed Users".

Raises:
:exc:`~plexapi.exceptions.BadRequest`: When trying to opt out music.
"""
if self.key == 'tv.plex.provider.music':
raise BadRequest('%s does not have the option to opt out managed users.' % self.key)
self._updateOptOut('opt_out_managed')
51 changes: 49 additions & 2 deletions plexapi/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,24 @@ def markUnwatched(self):
key = '/:/unscrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey
self._server.query(key)

def augmentation(self):
""" Returns a list of :class:`~plexapi.library.Hub` objects.
Augmentation returns hub items relating to online media sources
such as Tidal Music "Track from {item}" or "Soundtrack of {item}".
Plex Pass and linked Tidal account are required.
"""
account = self._server.myPlexAccount()
tidalOptOut = next(
(service.value for service in account.onlineMediaSources()
if service.key == 'tv.plex.provider.music'),
None
)
if account.subscriptionStatus != 'Active' or tidalOptOut == 'opt_out':
raise BadRequest('Requires Plex Pass and Tidal Music enabled.')
data = self._server.query(self.key + '?asyncAugmentMetadata=1')
augmentationKey = data.attrib.get('augmentationKey')
return self.fetchItems(augmentationKey)

def _defaultSyncTitle(self):
""" Returns str, default title for a new syncItem. """
return self.title
Expand Down Expand Up @@ -342,6 +360,16 @@ def _prettyfilename(self):
# This is just for compat.
return self.title

def reviews(self):
""" Returns a list of :class:`~plexapi.media.Review` objects. """
data = self._server.query(self._details_key)
return self.findItems(data, media.Review, rtag='Video')

def extras(self):
""" Returns a list of :class:`~plexapi.video.Extra` objects. """
data = self._server.query(self._details_key)
return self.findItems(data, Extra, rtag='Extras')

def hubs(self):
""" Returns a list of :class:`~plexapi.library.Hub` objects. """
data = self._server.query(self._details_key)
Expand Down Expand Up @@ -878,7 +906,6 @@ class Clip(Video, Playable, ArtUrlMixin, PosterUrlMixin):
viewOffset (int): View offset in milliseconds.
year (int): Year clip was released.
"""

TAG = 'Video'
TYPE = 'clip'
METADATA_TYPE = 'clip'
Expand All @@ -888,11 +915,13 @@ def _loadData(self, data):
Video._loadData(self, data)
Playable._loadData(self, data)
self._data = data
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
self.duration = utils.cast(int, data.attrib.get('duration'))
self.extraType = utils.cast(int, data.attrib.get('extraType'))
self.index = utils.cast(int, data.attrib.get('index'))
self.media = self.findItems(data, media.Media)
self.originallyAvailableAt = data.attrib.get('originallyAvailableAt')
self.originallyAvailableAt = utils.toDatetime(
data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
self.skipDetails = utils.cast(int, data.attrib.get('skipDetails'))
self.subtype = data.attrib.get('subtype')
self.thumbAspectRatio = data.attrib.get('thumbAspectRatio')
Expand All @@ -908,3 +937,21 @@ def locations(self):
List<str> of file paths where the clip is found on disk.
"""
return [part.file for part in self.iterParts() if part]

def _prettyfilename(self):
return self.title


class Extra(Clip):
""" Represents a single Extra (trailer, behindTheScenes, etc). """

def _loadData(self, data):
""" Load attribute values from Plex XML response. """
super(Extra, self)._loadData(data)
parent = self._parent()
self.librarySectionID = parent.librarySectionID
self.librarySectionKey = parent.librarySectionKey
self.librarySectionTitle = parent.librarySectionTitle

def _prettyfilename(self):
return '%s (%s)' % (self.title, self.subtype)
26 changes: 26 additions & 0 deletions tests/test_myplex.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,32 @@ def enabled():
utils.wait_until(lambda: enabled() == (False, False))


@pytest.mark.authenticated
def test_myplex_onlineMediaSources_optOut(account):
onlineMediaSources = account.onlineMediaSources()
for optOut in onlineMediaSources:
if optOut.key == 'tv.plex.provider.news':
# News is no longer available
continue

optOutValue = optOut.value
optOut.optIn()
assert optOut.value == 'opt_in'
optOut.optOut()
assert optOut.value == 'opt_out'
if optOut.key == 'tv.plex.provider.music':
with pytest.raises(BadRequest):
optOut.optOutManaged()
else:
optOut.optOutManaged()
assert optOut.value == 'opt_out_managed'
# Reset original value
optOut._updateOptOut(optOutValue)

with pytest.raises(NotFound):
assert onlineMediaSources[0]._updateOptOut('unknown')


def test_myplex_inviteFriend_remove(account, plex, mocker):
inv_user = "hellowlol"
vid_filter = {"contentRating": ["G"], "label": ["foo"]}
Expand Down
45 changes: 45 additions & 0 deletions tests/test_video.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,51 @@ def test_video_Movie_hubs(movies):
assert hub.size == 1


@pytest.mark.authenticated
def test_video_Movie_augmentation(movie, account):
onlineMediaSources = account.onlineMediaSources()
tidalOptOut = next(
optOut for optOut in onlineMediaSources
if optOut.key == 'tv.plex.provider.music'
)
optOutValue = tidalOptOut.value

tidalOptOut.optOut()
with pytest.raises(BadRequest):
movie.augmentation()

tidalOptOut.optIn()
augmentations = movie.augmentation()
assert augmentations or augmentations == []

# Reset original Tidal opt out value
tidalOptOut._updateOptOut(optOutValue)


def test_video_Movie_reviews(movies):
movie = movies.get("Sita Sings The Blues")
reviews = movie.reviews()
assert reviews
review = next(r for r in reviews if r.link)
assert review.filter
assert utils.is_int(review.id)
assert review.image.startswith("rottentomatoes://")
assert review.link.startswith("http")
assert review.source
assert review.tag
assert review.text


@pytest.mark.authenticated
def test_video_Movie_extras(movies):
movie = movies.get("Sita Sings The Blues")
extras = movie.extras()
assert extras
extra = extras[0]
assert extra.type == 'clip'
assert extra.section() == movies


def test_video_Show_attrs(show):
assert utils.is_datetime(show.addedAt)
if show.art:
Expand Down