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):
""" 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
method provided at :class:`~plexapi.server.PlexServer.myPlexAccount()` which will create
and return this object.
Parameters:
username (str): Your MyPlex username.
password (str): Your MyPlex password.
username (str): Plex login username if not using a token.
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
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).
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:
SIGNIN (str): 'https://plex.tv/users/sign_in.xml'
key (str): 'https://plex.tv/users/account'
authenticationToken (str): Unknown.
certificateVersion (str): Unknown.
cloudSyncDevice (str): Unknown.
email (str): Your current Plex email address.
key (str): 'https://plex.tv/api/v2/user'
adsConsent (str): Unknown.
adsConsentReminderAt (str): Unknown.
adsConsentSetAt (str): Unknown.
anonymous (str): Unknown.
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.
guest (bool): Unknown.
home (bool): Unknown.
homeSize (int): Unknown.
id (int): Your Plex account ID.
locale (str): Your Plex locale
mailing_list_status (str): Your current mailing list status.
maxHomeSize (int): Unknown.
guest (bool): If the account is a Plex Home guest user.
hasPassword (bool): If the account has a password.
home (bool): If the account is a Plex Home user.
homeAdmin (bool): If the account is the Plex Home admin.
homeSize (int): The number of accounts in the Plex Home.
id (int): The Plex account ID.
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.
queueEmail (str): Email address to add items to your `Watch Later` queue.
queueUid (str): Unknown.
restricted (bool): Unknown.
profileAutoSelectAudio (bool): If the account has automatically select audio and subtitle tracks enabled.
profileDefaultAudioLanguage (str): The preferred audio language for the account.
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.
scrobbleTypes (str): Description
secure (bool): Description
subscriptionActive (bool): True if your subscription is active.
subscriptionFeatures: (List<str>) List of features allowed on your subscription.
subscriptionPlan (str): Name of subscription plan.
subscriptionStatus (str): String representation of `subscriptionActive`.
thumb (str): URL of your account thumbnail.
title (str): Unknown. - Looks like an alias for `username`.
username (str): Your account username.
uuid (str): Unknown.
_token (str): Token used to access this client.
_session (obj): Requests session object used to access this client.
scrobbleTypes (List<int>): Unknown.
subscriptionActive (bool): If the account's Plex Pass subscription is active.
subscriptionDescription (str): Description of the Plex Pass subscription.
subscriptionFeatures: (List<str>) List of features allowed on your Plex Pass subscription.
subscriptionPaymentService (str): Payment service used for your Plex Pass subscription.
subscriptionPlan (str): Name of Plex Pass subscription plan.
subscriptionStatus (str): String representation of ``subscriptionActive``.
subscriptionSubscribedAt (datetime): Date the account subscribed to Plex Pass.
thumb (str): URL of the account thumbnail.
title (str): The title of the account (username or friendly name).
twoFactorEnabled (bool): If two-factor authentication is enabled.
username (str): The account username.
uuid (str): The account UUID.
"""
FRIENDINVITE = 'https://plex.tv/api/servers/{machineId}/shared_servers' # post with data
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
HOMEUSER = 'https://plex.tv/api/home/users/{userId}' # delete, 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
OPTOUTS = 'https://plex.tv/api/v2/user/{userUUID}/settings/opt_outs' # get
LINK = 'https://plex.tv/api/v2/pins/link' # put
@ -86,86 +112,106 @@ class MyPlexAccount(PlexObject):
VOD = 'https://vod.provider.plex.tv' # get
MUSIC = 'https://music.provider.plex.tv' # get
METADATA = 'https://metadata.provider.plex.tv'
# Key may someday switch to the following url. For now the current value works.
# https://plex.tv/api/v2/user?X-Plex-Token={token}&X-Plex-Client-Identifier={clientId}
key = 'https://plex.tv/users/account'
key = 'https://plex.tv/api/v2/user'
def __init__(self, username=None, password=None, token=None, session=None, timeout=None, code=None):
self._token = token or CONFIG.get('auth.server_token')
def __init__(self, username=None, password=None, token=None, session=None, timeout=None, code=None, remember=True):
self._token = logfilter.add_secret(token or CONFIG.get('auth.server_token'))
self._session = session or requests.Session()
self._sonos_cache = []
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)
def _signin(self, username, password, code, timeout):
def _signin(self, username, password, code, remember, timeout):
if self._token:
return self.query(self.key), self.key
username = username or CONFIG.get('auth.myplex_username')
password = password or CONFIG.get('auth.myplex_password')
payload = {
'login': username or CONFIG.get('auth.myplex_username'),
'password': password or CONFIG.get('auth.myplex_password'),
'rememberMe': remember
}
if code:
password += code
data = self.query(self.SIGNIN, method=self._session.post, auth=(username, password), timeout=timeout)
payload['verificationCode'] = code
data = self.query(self.SIGNIN, method=self._session.post, data=payload, timeout=timeout)
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):
""" Load attribute values from Plex XML response. """
self._data = data
self._token = logfilter.add_secret(data.attrib.get('authenticationToken'))
self._token = logfilter.add_secret(data.attrib.get('authToken'))
self._webhooks = []
self.authenticationToken = self._token
self.certificateVersion = data.attrib.get('certificateVersion')
self.cloudSyncDevice = data.attrib.get('cloudSyncDevice')
self.adsConsent = data.attrib.get('adsConsent')
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.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.hasPassword = utils.cast(bool, data.attrib.get('hasPassword'))
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.id = utils.cast(int, data.attrib.get('id'))
self.joinedAt = utils.toDatetime(data.attrib.get('joinedAt'))
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.pin = data.attrib.get('pin')
self.queueEmail = data.attrib.get('queueEmail')
self.queueUid = data.attrib.get('queueUid')
self.protected = utils.cast(bool, data.attrib.get('protected'))
self.rememberExpiresAt = utils.toDatetime(data.attrib.get('rememberExpiresAt'))
self.restricted = utils.cast(bool, data.attrib.get('restricted'))
self.scrobbleTypes = data.attrib.get('scrobbleTypes')
self.secure = utils.cast(bool, data.attrib.get('secure'))
self.scrobbleTypes = [utils.cast(int, x) for x in data.attrib.get('scrobbleTypes').split(',')]
self.thumb = data.attrib.get('thumb')
self.title = data.attrib.get('title')
self.twoFactorEnabled = utils.cast(bool, data.attrib.get('twoFactorEnabled'))
self.username = data.attrib.get('username')
self.uuid = data.attrib.get('uuid')
subscription = data.find('subscription')
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.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.roles = self.listAttrs(data, 'id', rtag='roles', etag='role')
# TODO: Fetch missing MyPlexAccount attributes
self.profile_settings = None
# TODO: Fetch missing MyPlexAccount services
self.services = None
self.joined_at = None
def device(self, name=None, clientId=None):
""" Returns the :class:`~plexapi.myplex.MyPlexDevice` that matches the name specified.
@property
def authenticationToken(self):
""" Returns the authentication token for the account. Alias for ``authToken``. """
return self.authToken
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 _reload(self, key=None, **kwargs):
""" Perform the actual reload. """
data = self.query(self.key)
self._loadData(data)
return self
def _headers(self, **kwargs):
""" Returns dict containing base headers for all requests to the server. """
@ -198,6 +244,23 @@ class MyPlexAccount(PlexObject):
data = response.text.encode('utf8')
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):
""" Returns the :class:`~plexapi.myplex.MyPlexResource` that matches the name specified.

View file

@ -157,7 +157,7 @@ def account_synctarget(account_plexpass):
@pytest.fixture()
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")

View file

@ -1,18 +1,21 @@
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">
<subscription active="1" status="Active" plan="lifetime">
<feature id="companions_sonos"/>
<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" subscribedAt="2023-03-24 00:00:00 UTC" status="Active" paymentService="" plan="lifetime">
<features>
<feature id="companions_sonos"/>
</features>
</subscription>
<profile autoSelectAudio="0" defaultAudioLanguage="en" defaultSubtitleLanguage="en" autoSelectSubtitle="0" defaultSubtitleAccessibility="0" defaultSubtitleForced="0"/>
<entitlements>
<entitlement id="all"/>
</entitlements>
<roles>
<role id="plexpass"/>
</roles>
<entitlements all="1"/>
<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"/>
<services/>
<username>testuser</username>
<email>testuser@email.com</email>
<joined-at type="datetime">2000-01-01 12:348:56 UTC</joined-at>
<authentication-token>faketoken</authentication-token>
<subscriptions>
</subscriptions>
<services>
</services>
</user>
"""

View file

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