Skip to content

Commit 777ea75

Browse files
authored
Add methods for accepting and cancelling friend invites (#849)
* Add accepting and cancelling friend invites * Add mocked tests for acceptInvite and cancelInvite * Rename methods to pendingInvite
1 parent 4e8a613 commit 777ea75

3 files changed

Lines changed: 141 additions & 24 deletions

File tree

plexapi/myplex.py

Lines changed: 113 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,6 @@ class MyPlexAccount(PlexObject):
7070
PLEXSERVERS = 'https://plex.tv/api/servers/{machineId}' # get
7171
FRIENDUPDATE = 'https://plex.tv/api/friends/{userId}' # put with args, delete
7272
REMOVEHOMEUSER = 'https://plex.tv/api/home/users/{userId}' # delete
73-
REMOVEINVITE = 'https://plex.tv/api/invites/requested/{userId}?friend=1&server=1&home=1' # delete
74-
REQUESTED = 'https://plex.tv/api/invites/requested' # get
75-
REQUESTS = 'https://plex.tv/api/invites/requests' # get
7673
SIGNIN = 'https://plex.tv/users/sign_in.xml' # get with auth
7774
WEBHOOKS = 'https://plex.tv/api/v2/user/webhooks' # get, post with data
7875
OPTOUTS = 'https://plex.tv/api/v2/user/%(userUUID)s/settings/opt_outs' # get
@@ -365,26 +362,55 @@ def createExistingUser(self, user, server, sections=None, allowSync=False, allow
365362
return self.query(url, self._session.post, headers=headers)
366363

367364
def removeFriend(self, user):
368-
""" Remove the specified user from all sharing.
365+
""" Remove the specified user from your friends.
369366
370367
Parameters:
371-
user (str): MyPlexUser, username, email of the user to be added.
368+
user (str): :class:`~plexapi.myplex.MyPlexUser`, username, or email of the user to be removed.
372369
"""
373-
user = self.user(user)
374-
url = self.FRIENDUPDATE if user.friend else self.REMOVEINVITE
375-
url = url.format(userId=user.id)
370+
user = user if isinstance(user, MyPlexUser) else self.user(user)
371+
url = self.FRIENDUPDATE.format(userId=user.id)
376372
return self.query(url, self._session.delete)
377373

378374
def removeHomeUser(self, user):
379-
""" Remove the specified managed user from home.
375+
""" Remove the specified user from your home users.
380376
381377
Parameters:
382-
user (str): MyPlexUser, username, email of the user to be removed from home.
378+
user (str): :class:`~plexapi.myplex.MyPlexUser`, username, or email of the user to be removed.
383379
"""
384-
user = self.user(user)
380+
user = user if isinstance(user, MyPlexUser) else self.user(user)
385381
url = self.REMOVEHOMEUSER.format(userId=user.id)
386382
return self.query(url, self._session.delete)
387383

384+
def acceptInvite(self, user):
385+
""" Accept a pending firend invite from the specified user.
386+
387+
Parameters:
388+
user (str): :class:`~plexapi.myplex.MyPlexInvite`, username, or email of the friend invite to accept.
389+
"""
390+
invite = user if isinstance(user, MyPlexInvite) else self.pendingInvite(user, includeSent=False)
391+
params = {
392+
'friend': int(invite.friend),
393+
'home': int(invite.home),
394+
'server': int(invite.server)
395+
}
396+
url = MyPlexInvite.REQUESTS + '/%s' % invite.id + utils.joinArgs(params)
397+
return self.query(url, self._session.put)
398+
399+
def cancelInvite(self, user):
400+
""" Cancel a pending firend invite for the specified user.
401+
402+
Parameters:
403+
user (str): :class:`~plexapi.myplex.MyPlexInvite`, username, or email of the friend invite to cancel.
404+
"""
405+
invite = user if isinstance(user, MyPlexInvite) else self.pendingInvite(user, includeReceived=False)
406+
params = {
407+
'friend': int(invite.friend),
408+
'home': int(invite.home),
409+
'server': int(invite.server)
410+
}
411+
url = MyPlexInvite.REQUESTED + '/%s' % invite.id + utils.joinArgs(params)
412+
return self.query(url, self._session.delete)
413+
388414
def updateFriend(self, user, server, sections=None, removeSections=False, allowSync=None, allowCameraUpload=None,
389415
allowChannels=None, filterMovies=None, filterTelevision=None, filterMusic=None):
390416
""" Update the specified user's share settings.
@@ -455,7 +481,7 @@ def updateFriend(self, user, server, sections=None, removeSections=False, allowS
455481
return response_servers, response_filters
456482

457483
def user(self, username):
458-
""" Returns the :class:`~plexapi.myplex.MyPlexUser` that matches the email or username specified.
484+
""" Returns the :class:`~plexapi.myplex.MyPlexUser` that matches the specified username or email.
459485
460486
Parameters:
461487
username (str): Username, email or id of the user to return.
@@ -467,19 +493,50 @@ def user(self, username):
467493
return user
468494

469495
elif (user.username and user.email and user.id and username.lower() in
470-
(user.username.lower(), user.email.lower(), str(user.id))):
496+
(user.username.lower(), user.email.lower(), str(user.id))):
471497
return user
472498

473499
raise NotFound('Unable to find user %s' % username)
474500

475501
def users(self):
476502
""" Returns a list of all :class:`~plexapi.myplex.MyPlexUser` objects connected to your account.
477-
This includes both friends and pending invites. You can reference the user.friend to
478-
distinguish between the two.
479503
"""
480-
friends = [MyPlexUser(self, elem) for elem in self.query(MyPlexUser.key)]
481-
requested = [MyPlexUser(self, elem, self.REQUESTED) for elem in self.query(self.REQUESTED)]
482-
return friends + requested
504+
elem = self.query(MyPlexUser.key)
505+
return self.findItems(elem, cls=MyPlexUser)
506+
507+
def pendingInvite(self, username, includeSent=True, includeReceived=True):
508+
""" Returns the :class:`~plexapi.myplex.MyPlexInvite` that matches the specified username or email.
509+
Note: This can be a pending invite sent from your account or received to your account.
510+
511+
Parameters:
512+
username (str): Username, email or id of the user to return.
513+
includeSent (bool): True to include sent invites.
514+
includeReceived (bool): True to include received invites.
515+
"""
516+
username = str(username)
517+
for invite in self.pendingInvites(includeSent, includeReceived):
518+
if (invite.username and invite.email and invite.id and username.lower() in
519+
(invite.username.lower(), invite.email.lower(), str(invite.id))):
520+
return invite
521+
522+
raise NotFound('Unable to find invite %s' % username)
523+
524+
def pendingInvites(self, includeSent=True, includeReceived=True):
525+
""" Returns a list of all :class:`~plexapi.myplex.MyPlexInvite` objects connected to your account.
526+
Note: This includes all pending invites sent from your account and received to your account.
527+
528+
Parameters:
529+
includeSent (bool): True to include sent invites.
530+
includeReceived (bool): True to include received invites.
531+
"""
532+
invites = []
533+
if includeSent:
534+
elem = self.query(MyPlexInvite.REQUESTED)
535+
invites += self.findItems(elem, cls=MyPlexInvite)
536+
if includeReceived:
537+
elem = self.query(MyPlexInvite.REQUESTS)
538+
invites += self.findItems(elem, cls=MyPlexInvite)
539+
return invites
483540

484541
def _getSectionIds(self, server, sections):
485542
""" Converts a list of section objects or names to sectionIds needed for library sharing. """
@@ -731,10 +788,10 @@ class MyPlexUser(PlexObject):
731788
protected (False): Unknown (possibly SSL enabled?).
732789
recommendationsPlaylistId (str): Unknown.
733790
restricted (str): Unknown.
791+
servers (List<:class:`~plexapi.myplex.<MyPlexServerShare`>)): Servers shared with the user.
734792
thumb (str): Link to the users avatar.
735793
title (str): Seems to be an aliad for username.
736794
username (str): User's username.
737-
servers: Servers shared between user and friend
738795
"""
739796
TAG = 'User'
740797
key = 'https://plex.tv/api/users/'
@@ -796,6 +853,43 @@ def history(self, maxresults=9999999, mindate=None):
796853
return hist
797854

798855

856+
class MyPlexInvite(PlexObject):
857+
""" This object represents pending friend invites.
858+
859+
Attributes:
860+
TAG (str): 'Invite'
861+
createdAt (datetime): Datetime the user was invited.
862+
email (str): User's email address (user@gmail.com).
863+
friend (bool): True or False if the user is invited as a friend.
864+
friendlyName (str): The user's friendly name.
865+
home (bool): True or False if the user is invited to a Plex Home.
866+
id (int): User's Plex account ID.
867+
server (bool): True or False if the user is invited to any servers.
868+
servers (List<:class:`~plexapi.myplex.<MyPlexServerShare`>)): Servers shared with the user.
869+
thumb (str): Link to the users avatar.
870+
username (str): User's username.
871+
"""
872+
TAG = 'Invite'
873+
REQUESTS = 'https://plex.tv/api/invites/requests'
874+
REQUESTED = 'https://plex.tv/api/invites/requested'
875+
876+
def _loadData(self, data):
877+
""" Load attribute values from Plex XML response. """
878+
self._data = data
879+
self.createdAt = utils.toDatetime(data.attrib.get('createdAt'))
880+
self.email = data.attrib.get('email')
881+
self.friend = utils.cast(bool, data.attrib.get('friend'))
882+
self.friendlyName = data.attrib.get('friendlyName')
883+
self.home = utils.cast(bool, data.attrib.get('home'))
884+
self.id = utils.cast(int, data.attrib.get('id'))
885+
self.server = utils.cast(bool, data.attrib.get('server'))
886+
self.servers = self.findItems(data, MyPlexServerShare)
887+
self.thumb = data.attrib.get('thumb')
888+
self.username = data.attrib.get('username', '')
889+
for server in self.servers:
890+
server.accountID = self.id
891+
892+
799893
class Section(PlexObject):
800894
""" This refers to a shared section. The raw xml for the data presented here
801895
can be found at: https://plex.tv/api/servers/{machineId}/shared_servers

tests/payloads.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,10 @@
3434
<TranscodeSession key="qucs2leop3yzm0sng4urq1o0" throttled="0" complete="0" progress="1.2999999523162842" size="73138224" speed="6.4000000953674316" duration="6654989" remaining="988" context="streaming" sourceVideoCodec="h264" sourceAudioCodec="dca" videoDecision="transcode" audioDecision="transcode" protocol="dash" container="mp4" videoCodec="h264" audioCodec="aac" audioChannels="2" transcodeHwRequested="1" transcodeHwDecoding="dxva2" transcodeHwDecodingTitle="Windows (DXVA2)" transcodeHwEncoding="qsv" transcodeHwEncodingTitle="Intel (QuickSync)" transcodeHwFullPipeline="0" timeStamp="1611533677.0316164" maxOffsetAvailable="84.000667334000667" minOffsetAvailable="0" height="720" width="1280" />
3535
</MediaContainer>
3636
"""
37+
38+
MYPLEX_INVITE = """<MediaContainer friendlyName="myPlex" identifier="com.plexapp.plugins.myplex" machineIdentifier="xxxxxxxxxx" size="1">
39+
<Invite id="12345" createdAt="1635126033" friend="1" home="1" server="1" username="testuser" email="testuser@email.com" thumb="https://plex.tv/users/1234567890abcdef/avatar?c=12345" friendlyName="testuser">
40+
<Server name="testserver" numLibraries="2"/>
41+
</Invite>
42+
</MediaContainer>
43+
"""

tests/test_myplex.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# -*- coding: utf-8 -*-
22
import pytest
33
from plexapi.exceptions import BadRequest, NotFound
4+
from plexapi.myplex import MyPlexInvite
45

56
from . import conftest as utils
7+
from .payloads import MYPLEX_INVITE
68

79

810
def test_myplex_accounts(account, plex):
@@ -150,7 +152,7 @@ def test_myplex_onlineMediaSources_optOut(account):
150152
onlineMediaSources[0]._updateOptOut('unknown')
151153

152154

153-
def test_myplex_inviteFriend_remove(account, plex, mocker):
155+
def test_myplex_inviteFriend(account, plex, mocker):
154156
inv_user = "hellowlol"
155157
vid_filter = {"contentRating": ["G"], "label": ["foo"]}
156158
secs = plex.library.sections()
@@ -172,9 +174,21 @@ def test_myplex_inviteFriend_remove(account, plex, mocker):
172174

173175
assert inv_user not in [u.title for u in account.users()]
174176

175-
with pytest.raises(NotFound):
176-
with utils.callable_http_patch():
177-
account.removeFriend(inv_user)
177+
178+
def test_myplex_acceptInvite(account, requests_mock):
179+
url = MyPlexInvite.REQUESTS
180+
requests_mock.get(url, text=MYPLEX_INVITE)
181+
invite = account.pendingInvite('testuser', includeSent=False)
182+
with utils.callable_http_patch():
183+
account.acceptInvite(invite)
184+
185+
186+
def test_myplex_cancelInvite(account, requests_mock):
187+
url = MyPlexInvite.REQUESTED
188+
requests_mock.get(url, text=MYPLEX_INVITE)
189+
invite = account.pendingInvite('testuser', includeReceived=False)
190+
with utils.callable_http_patch():
191+
account.cancelInvite(invite)
178192

179193

180194
def test_myplex_updateFriend(account, plex, mocker, shared_username):
@@ -186,7 +200,6 @@ def test_myplex_updateFriend(account, plex, mocker, shared_username):
186200
mocker.patch.object(account, "_getSectionIds", return_value=ids)
187201
mocker.patch.object(account, "user", return_value=user)
188202
with utils.callable_http_patch():
189-
190203
account.updateFriend(
191204
shared_username,
192205
plex,
@@ -200,6 +213,9 @@ def test_myplex_updateFriend(account, plex, mocker, shared_username):
200213
filterMusic={"label": ["foo"]},
201214
)
202215

216+
with utils.callable_http_patch():
217+
account.removeFriend(shared_username)
218+
203219

204220
def test_myplex_createExistingUser(account, plex, shared_username):
205221
user = account.user(shared_username)

0 commit comments

Comments
 (0)