mirror of
https://github.com/pkkid/python-plexapi
synced 2024-11-21 19:23:05 +00:00
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:
parent
d94b6efc41
commit
013d1a2d00
4 changed files with 152 additions and 87 deletions
|
@ -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.
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
||||
|
|
|
@ -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>
|
||||
"""
|
||||
|
||||
|
|
|
@ -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}")
|
||||
|
|
Loading…
Reference in a new issue