Update MyPlexAccount to use Plex API v2 (#1129)

* Update MyPlexAccount to use Plex API v2

* Update MyPlexAccount test

* Add rememberMe option to MyPlexAccount sign in

* Update MyPlexAccount do string
This commit is contained in:
JonnyWong16 2023-07-27 14:00:46 -07:00 committed by GitHub
parent d94b6efc41
commit 013d1a2d00
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 152 additions and 87 deletions

View file

@ -22,51 +22,76 @@ from requests.status_codes import _codes as codes
class MyPlexAccount(PlexObject): class MyPlexAccount(PlexObject):
""" MyPlex account and profile information. This object represents the data found Account on """ MyPlex account and profile information. This object represents the data found Account on
the myplex.tv servers at the url https://plex.tv/users/account. You may create this object the myplex.tv servers at the url https://plex.tv/api/v2/user. You may create this object
directly by passing in your username & password (or token). There is also a convenience directly by passing in your username & password (or token). There is also a convenience
method provided at :class:`~plexapi.server.PlexServer.myPlexAccount()` which will create method provided at :class:`~plexapi.server.PlexServer.myPlexAccount()` which will create
and return this object. and return this object.
Parameters: Parameters:
username (str): Your MyPlex username. username (str): Plex login username if not using a token.
password (str): Your MyPlex password. password (str): Plex login password if not using a token.
token (str): Plex authentication token instead of username and password.
session (requests.Session, optional): Use your own session object if you want to session (requests.Session, optional): Use your own session object if you want to
cache the http responses from PMS cache the http responses from PMS.
timeout (int): timeout in seconds on initial connect to myplex (default config.TIMEOUT). timeout (int): timeout in seconds on initial connect to myplex (default config.TIMEOUT).
code (str): Two-factor authentication code to use when logging in. code (str): Two-factor authentication code to use when logging in with username and password.
remember (bool): Remember the account token for 14 days (Default True).
Attributes: Attributes:
SIGNIN (str): 'https://plex.tv/users/sign_in.xml' key (str): 'https://plex.tv/api/v2/user'
key (str): 'https://plex.tv/users/account' adsConsent (str): Unknown.
authenticationToken (str): Unknown. adsConsentReminderAt (str): Unknown.
certificateVersion (str): Unknown. adsConsentSetAt (str): Unknown.
cloudSyncDevice (str): Unknown. anonymous (str): Unknown.
email (str): Your current Plex email address. authToken (str): The account token.
backupCodesCreated (bool): If the two-factor authentication backup codes have been created.
confirmed (bool): If the account has been confirmed.
country (str): The account country.
email (str): The account email address.
emailOnlyAuth (bool): If login with email only is enabled.
experimentalFeatures (bool): If experimental features are enabled.
friendlyName (str): Your account full name.
entitlements (List<str>): List of devices your allowed to use with this account. entitlements (List<str>): List of devices your allowed to use with this account.
guest (bool): Unknown. guest (bool): If the account is a Plex Home guest user.
home (bool): Unknown. hasPassword (bool): If the account has a password.
homeSize (int): Unknown. home (bool): If the account is a Plex Home user.
id (int): Your Plex account ID. homeAdmin (bool): If the account is the Plex Home admin.
locale (str): Your Plex locale homeSize (int): The number of accounts in the Plex Home.
mailing_list_status (str): Your current mailing list status. id (int): The Plex account ID.
maxHomeSize (int): Unknown. joinedAt (datetime): Date the account joined Plex.
locale (str): the account locale
mailingListActive (bool): If you are subscribed to the Plex newsletter.
mailingListStatus (str): Your current mailing list status.
maxHomeSize (int): The maximum number of accounts allowed in the Plex Home.
pin (str): The hashed Plex Home PIN. pin (str): The hashed Plex Home PIN.
queueEmail (str): Email address to add items to your `Watch Later` queue. profileAutoSelectAudio (bool): If the account has automatically select audio and subtitle tracks enabled.
queueUid (str): Unknown. profileDefaultAudioLanguage (str): The preferred audio language for the account.
restricted (bool): Unknown. profileDefaultSubtitleLanguage (str): The preferred subtitle language for the account.
profileAutoSelectSubtitle (int): The auto-select subtitle mode
(0 = Manually selected, 1 = Shown with foreign audio, 2 = Always enabled).
profileDefaultSubtitleAccessibility (int): The subtitles for the deaf or hard-of-hearing (SDH) searches mode
(0 = Prefer non-SDH subtitles, 1 = Prefer SDH subtitles, 2 = Only show SDH subtitles,
3 = Only shown non-SDH subtitles).
profileDefaultSubtitleForced (int): The forced subtitles searches mode
(0 = Prefer non-forced subtitles, 1 = Prefer forced subtitles, 2 = Only show forced subtitles,
3 = Only show non-forced subtitles).
protected (bool): If the account has a Plex Home PIN enabled.
rememberExpiresAt (datetime): Date the token expires.
restricted (bool): If the account is a Plex Home managed user.
roles: (List<str>) Lit of account roles. Plexpass membership listed here. roles: (List<str>) Lit of account roles. Plexpass membership listed here.
scrobbleTypes (str): Description scrobbleTypes (List<int>): Unknown.
secure (bool): Description subscriptionActive (bool): If the account's Plex Pass subscription is active.
subscriptionActive (bool): True if your subscription is active. subscriptionDescription (str): Description of the Plex Pass subscription.
subscriptionFeatures: (List<str>) List of features allowed on your subscription. subscriptionFeatures: (List<str>) List of features allowed on your Plex Pass subscription.
subscriptionPlan (str): Name of subscription plan. subscriptionPaymentService (str): Payment service used for your Plex Pass subscription.
subscriptionStatus (str): String representation of `subscriptionActive`. subscriptionPlan (str): Name of Plex Pass subscription plan.
thumb (str): URL of your account thumbnail. subscriptionStatus (str): String representation of ``subscriptionActive``.
title (str): Unknown. - Looks like an alias for `username`. subscriptionSubscribedAt (datetime): Date the account subscribed to Plex Pass.
username (str): Your account username. thumb (str): URL of the account thumbnail.
uuid (str): Unknown. title (str): The title of the account (username or friendly name).
_token (str): Token used to access this client. twoFactorEnabled (bool): If two-factor authentication is enabled.
_session (obj): Requests session object used to access this client. username (str): The account username.
uuid (str): The account UUID.
""" """
FRIENDINVITE = 'https://plex.tv/api/servers/{machineId}/shared_servers' # post with data FRIENDINVITE = 'https://plex.tv/api/servers/{machineId}/shared_servers' # post with data
HOMEUSERS = 'https://plex.tv/api/home/users' HOMEUSERS = 'https://plex.tv/api/home/users'
@ -77,7 +102,8 @@ class MyPlexAccount(PlexObject):
FRIENDUPDATE = 'https://plex.tv/api/friends/{userId}' # put with args, delete FRIENDUPDATE = 'https://plex.tv/api/friends/{userId}' # put with args, delete
HOMEUSER = 'https://plex.tv/api/home/users/{userId}' # delete, put HOMEUSER = 'https://plex.tv/api/home/users/{userId}' # delete, put
MANAGEDHOMEUSER = 'https://plex.tv/api/v2/home/users/restricted/{userId}' # put MANAGEDHOMEUSER = 'https://plex.tv/api/v2/home/users/restricted/{userId}' # put
SIGNIN = 'https://plex.tv/users/sign_in.xml' # get with auth SIGNIN = 'https://plex.tv/api/v2/users/signin' # post with auth
SIGNOUT = 'https://plex.tv/api/v2/users/signout' # delete
WEBHOOKS = 'https://plex.tv/api/v2/user/webhooks' # get, post with data WEBHOOKS = 'https://plex.tv/api/v2/user/webhooks' # get, post with data
OPTOUTS = 'https://plex.tv/api/v2/user/{userUUID}/settings/opt_outs' # get OPTOUTS = 'https://plex.tv/api/v2/user/{userUUID}/settings/opt_outs' # get
LINK = 'https://plex.tv/api/v2/pins/link' # put LINK = 'https://plex.tv/api/v2/pins/link' # put
@ -86,86 +112,106 @@ class MyPlexAccount(PlexObject):
VOD = 'https://vod.provider.plex.tv' # get VOD = 'https://vod.provider.plex.tv' # get
MUSIC = 'https://music.provider.plex.tv' # get MUSIC = 'https://music.provider.plex.tv' # get
METADATA = 'https://metadata.provider.plex.tv' METADATA = 'https://metadata.provider.plex.tv'
# Key may someday switch to the following url. For now the current value works. key = 'https://plex.tv/api/v2/user'
# https://plex.tv/api/v2/user?X-Plex-Token={token}&X-Plex-Client-Identifier={clientId}
key = 'https://plex.tv/users/account'
def __init__(self, username=None, password=None, token=None, session=None, timeout=None, code=None): def __init__(self, username=None, password=None, token=None, session=None, timeout=None, code=None, remember=True):
self._token = token or CONFIG.get('auth.server_token') self._token = logfilter.add_secret(token or CONFIG.get('auth.server_token'))
self._session = session or requests.Session() self._session = session or requests.Session()
self._sonos_cache = [] self._sonos_cache = []
self._sonos_cache_timestamp = 0 self._sonos_cache_timestamp = 0
data, initpath = self._signin(username, password, code, timeout) data, initpath = self._signin(username, password, code, remember, timeout)
super(MyPlexAccount, self).__init__(self, data, initpath) super(MyPlexAccount, self).__init__(self, data, initpath)
def _signin(self, username, password, code, timeout): def _signin(self, username, password, code, remember, timeout):
if self._token: if self._token:
return self.query(self.key), self.key return self.query(self.key), self.key
username = username or CONFIG.get('auth.myplex_username') payload = {
password = password or CONFIG.get('auth.myplex_password') 'login': username or CONFIG.get('auth.myplex_username'),
'password': password or CONFIG.get('auth.myplex_password'),
'rememberMe': remember
}
if code: if code:
password += code payload['verificationCode'] = code
data = self.query(self.SIGNIN, method=self._session.post, auth=(username, password), timeout=timeout) data = self.query(self.SIGNIN, method=self._session.post, data=payload, timeout=timeout)
return data, self.SIGNIN return data, self.SIGNIN
def signout(self):
""" Sign out of the Plex account. Invalidates the authentication token. """
return self.query(self.SIGNOUT, method=self._session.delete)
def _loadData(self, data): def _loadData(self, data):
""" Load attribute values from Plex XML response. """ """ Load attribute values from Plex XML response. """
self._data = data self._data = data
self._token = logfilter.add_secret(data.attrib.get('authenticationToken')) self._token = logfilter.add_secret(data.attrib.get('authToken'))
self._webhooks = [] self._webhooks = []
self.authenticationToken = self._token
self.certificateVersion = data.attrib.get('certificateVersion') self.adsConsent = data.attrib.get('adsConsent')
self.cloudSyncDevice = data.attrib.get('cloudSyncDevice') self.adsConsentReminderAt = data.attrib.get('adsConsentReminderAt')
self.adsConsentSetAt = data.attrib.get('adsConsentSetAt')
self.anonymous = data.attrib.get('anonymous')
self.authToken = self._token
self.backupCodesCreated = utils.cast(bool, data.attrib.get('backupCodesCreated'))
self.confirmed = utils.cast(bool, data.attrib.get('confirmed'))
self.country = data.attrib.get('country')
self.email = data.attrib.get('email') self.email = data.attrib.get('email')
self.emailOnlyAuth = utils.cast(bool, data.attrib.get('emailOnlyAuth'))
self.experimentalFeatures = utils.cast(bool, data.attrib.get('experimentalFeatures'))
self.friendlyName = data.attrib.get('friendlyName')
self.guest = utils.cast(bool, data.attrib.get('guest')) self.guest = utils.cast(bool, data.attrib.get('guest'))
self.hasPassword = utils.cast(bool, data.attrib.get('hasPassword'))
self.home = utils.cast(bool, data.attrib.get('home')) self.home = utils.cast(bool, data.attrib.get('home'))
self.homeAdmin = utils.cast(bool, data.attrib.get('homeAdmin'))
self.homeSize = utils.cast(int, data.attrib.get('homeSize')) self.homeSize = utils.cast(int, data.attrib.get('homeSize'))
self.id = utils.cast(int, data.attrib.get('id')) self.id = utils.cast(int, data.attrib.get('id'))
self.joinedAt = utils.toDatetime(data.attrib.get('joinedAt'))
self.locale = data.attrib.get('locale') self.locale = data.attrib.get('locale')
self.mailing_list_status = data.attrib.get('mailing_list_status') self.mailingListActive = utils.cast(bool, data.attrib.get('mailingListActive'))
self.mailingListStatus = data.attrib.get('mailingListStatus')
self.maxHomeSize = utils.cast(int, data.attrib.get('maxHomeSize')) self.maxHomeSize = utils.cast(int, data.attrib.get('maxHomeSize'))
self.pin = data.attrib.get('pin') self.pin = data.attrib.get('pin')
self.queueEmail = data.attrib.get('queueEmail') self.protected = utils.cast(bool, data.attrib.get('protected'))
self.queueUid = data.attrib.get('queueUid') self.rememberExpiresAt = utils.toDatetime(data.attrib.get('rememberExpiresAt'))
self.restricted = utils.cast(bool, data.attrib.get('restricted')) self.restricted = utils.cast(bool, data.attrib.get('restricted'))
self.scrobbleTypes = data.attrib.get('scrobbleTypes') self.scrobbleTypes = [utils.cast(int, x) for x in data.attrib.get('scrobbleTypes').split(',')]
self.secure = utils.cast(bool, data.attrib.get('secure'))
self.thumb = data.attrib.get('thumb') self.thumb = data.attrib.get('thumb')
self.title = data.attrib.get('title') self.title = data.attrib.get('title')
self.twoFactorEnabled = utils.cast(bool, data.attrib.get('twoFactorEnabled'))
self.username = data.attrib.get('username') self.username = data.attrib.get('username')
self.uuid = data.attrib.get('uuid') self.uuid = data.attrib.get('uuid')
subscription = data.find('subscription') subscription = data.find('subscription')
self.subscriptionActive = utils.cast(bool, subscription.attrib.get('active')) self.subscriptionActive = utils.cast(bool, subscription.attrib.get('active'))
self.subscriptionStatus = subscription.attrib.get('status') self.subscriptionDescription = data.attrib.get('subscriptionDescription')
self.subscriptionFeatures = self.listAttrs(subscription, 'id', rtag='features', etag='feature')
self.subscriptionPaymentService = subscription.attrib.get('paymentService')
self.subscriptionPlan = subscription.attrib.get('plan') self.subscriptionPlan = subscription.attrib.get('plan')
self.subscriptionFeatures = self.listAttrs(subscription, 'id', etag='feature') self.subscriptionStatus = subscription.attrib.get('status')
self.subscriptionSubscribedAt = utils.toDatetime(subscription.attrib.get('subscribedAt'), '%Y-%m-%d %H:%M:%S %Z')
self.roles = self.listAttrs(data, 'id', rtag='roles', etag='role') profile = data.find('profile')
self.profileAutoSelectAudio = utils.cast(bool, profile.attrib.get('autoSelectAudio'))
self.profileDefaultAudioLanguage = profile.attrib.get('defaultAudioLanguage')
self.profileDefaultSubtitleLanguage = profile.attrib.get('defaultSubtitleLanguage')
self.profileAutoSelectSubtitle = utils.cast(int, profile.attrib.get('autoSelectSubtitle'))
self.profileDefaultSubtitleAccessibility = utils.cast(int, profile.attrib.get('defaultSubtitleAccessibility'))
self.profileDefaultSubtitleForces = utils.cast(int, profile.attrib.get('defaultSubtitleForces'))
self.entitlements = self.listAttrs(data, 'id', rtag='entitlements', etag='entitlement') self.entitlements = self.listAttrs(data, 'id', rtag='entitlements', etag='entitlement')
self.roles = self.listAttrs(data, 'id', rtag='roles', etag='role')
# TODO: Fetch missing MyPlexAccount attributes # TODO: Fetch missing MyPlexAccount services
self.profile_settings = None
self.services = None self.services = None
self.joined_at = None
def device(self, name=None, clientId=None): @property
""" Returns the :class:`~plexapi.myplex.MyPlexDevice` that matches the name specified. def authenticationToken(self):
""" Returns the authentication token for the account. Alias for ``authToken``. """
return self.authToken
Parameters: def _reload(self, key=None, **kwargs):
name (str): Name to match against. """ Perform the actual reload. """
clientId (str): clientIdentifier to match against. data = self.query(self.key)
""" self._loadData(data)
for device in self.devices(): return self
if (name and device.name.lower() == name.lower() or device.clientIdentifier == clientId):
return device
raise NotFound(f'Unable to find device {name}')
def devices(self):
""" Returns a list of all :class:`~plexapi.myplex.MyPlexDevice` objects connected to the server. """
data = self.query(MyPlexDevice.key)
return [MyPlexDevice(self, elem) for elem in data]
def _headers(self, **kwargs): def _headers(self, **kwargs):
""" Returns dict containing base headers for all requests to the server. """ """ Returns dict containing base headers for all requests to the server. """
@ -198,6 +244,23 @@ class MyPlexAccount(PlexObject):
data = response.text.encode('utf8') data = response.text.encode('utf8')
return ElementTree.fromstring(data) if data.strip() else None return ElementTree.fromstring(data) if data.strip() else None
def device(self, name=None, clientId=None):
""" Returns the :class:`~plexapi.myplex.MyPlexDevice` that matches the name specified.
Parameters:
name (str): Name to match against.
clientId (str): clientIdentifier to match against.
"""
for device in self.devices():
if (name and device.name.lower() == name.lower() or device.clientIdentifier == clientId):
return device
raise NotFound(f'Unable to find device {name}')
def devices(self):
""" Returns a list of all :class:`~plexapi.myplex.MyPlexDevice` objects connected to the server. """
data = self.query(MyPlexDevice.key)
return [MyPlexDevice(self, elem) for elem in data]
def resource(self, name): def resource(self, name):
""" Returns the :class:`~plexapi.myplex.MyPlexResource` that matches the name specified. """ Returns the :class:`~plexapi.myplex.MyPlexResource` that matches the name specified.

View file

@ -157,7 +157,7 @@ def account_synctarget(account_plexpass):
@pytest.fixture() @pytest.fixture()
def mocked_account(requests_mock): def mocked_account(requests_mock):
requests_mock.get("https://plex.tv/users/account", text=ACCOUNT_XML) requests_mock.get("https://plex.tv/api/v2/user", text=ACCOUNT_XML)
return MyPlexAccount(token="faketoken") return MyPlexAccount(token="faketoken")

View file

@ -1,18 +1,21 @@
ACCOUNT_XML = """<?xml version="1.0" encoding="UTF-8"?> ACCOUNT_XML = """<?xml version="1.0" encoding="UTF-8"?>
<user email="testuser@email.com" id="12345" uuid="1234567890" mailing_list_status="active" thumb="https://plex.tv/users/1234567890abcdef/avatar?c=12345" username="testuser" title="testuser" cloudSyncDevice="" locale="" authenticationToken="faketoken" authToken="faketoken" scrobbleTypes="" restricted="0" home="1" guest="0" queueEmail="queue+1234567890@save.plex.tv" queueUid="" hasPassword="true" homeSize="2" maxHomeSize="15" secure="1" certificateVersion="2"> <user id="12345" uuid="1234567890" username="testuser" title="Test User" email="testuser@email.com" friendlyName="Test User" locale="" confirmed="1" joinedAt="946730096" emailOnlyAuth="0" hasPassword="1" protected="0" thumb="https://plex.tv/users/1234567890abcdef/avatar?c=12345" authToken="faketoken" mailingListStatus="unsubscribed" mailingListActive="0" scrobbleTypes="" country="CA" subscriptionDescription="" restricted="0" anonymous="" home="1" guest="0" homeSize="2" homeAdmin="1" maxHomeSize="15" rememberExpiresAt="1680893707" adsConsent="" adsConsentSetAt="" adsConsentReminderAt="" experimentalFeatures="0" twoFactorEnabled="1" backupCodesCreated="1">
<subscription active="1" status="Active" plan="lifetime"> <subscription active="1" subscribedAt="2023-03-24 00:00:00 UTC" status="Active" paymentService="" plan="lifetime">
<features>
<feature id="companions_sonos"/> <feature id="companions_sonos"/>
</features>
</subscription> </subscription>
<profile autoSelectAudio="0" defaultAudioLanguage="en" defaultSubtitleLanguage="en" autoSelectSubtitle="0" defaultSubtitleAccessibility="0" defaultSubtitleForced="0"/>
<entitlements>
<entitlement id="all"/>
</entitlements>
<roles> <roles>
<role id="plexpass"/> <role id="plexpass"/>
</roles> </roles>
<entitlements all="1"/> <subscriptions>
<profile_settings default_audio_language="en" default_subtitle_language="en" auto_select_subtitle="1" auto_select_audio="1" default_subtitle_accessibility="0" default_subtitle_forced="0"/> </subscriptions>
<services/> <services>
<username>testuser</username> </services>
<email>testuser@email.com</email>
<joined-at type="datetime">2000-01-01 12:348:56 UTC</joined-at>
<authentication-token>faketoken</authentication-token>
</user> </user>
""" """

View file

@ -18,7 +18,6 @@ def test_myplex_accounts(account, plex):
assert account.authenticationToken, "Account has no authenticationToken" assert account.authenticationToken, "Account has no authenticationToken"
assert account.email, "Account has no email" assert account.email, "Account has no email"
assert account.home is not None, "Account has no home" assert account.home is not None, "Account has no home"
assert account.queueEmail, "Account has no queueEmail"
account = plex.account() account = plex.account()
print("Local PlexServer.account():") print("Local PlexServer.account():")
print(f"username: {account.username}") print(f"username: {account.username}")