Skip to content

Commit 772c41d

Browse files
authored
Merge pull request pushingkarmaorg#411 from Montellese/feature/myplex_pinlogin
Add support for MyPlex PIN login
2 parents 30ab858 + 62cb758 commit 772c41d

1 file changed

Lines changed: 181 additions & 0 deletions

File tree

‎plexapi/myplex.py‎

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# -*- coding: utf-8 -*-
22
import copy
3+
import threading
34
import time
45

56
import requests
@@ -1052,6 +1053,186 @@ def syncItems(self):
10521053
return self._server.syncItems(client=self)
10531054

10541055

1056+
class MyPlexPinLogin(object):
1057+
"""
1058+
MyPlex PIN login class which supports getting the four character PIN which the user must
1059+
enter on https://plex.tv/link to authenticate the client and provide an access token to
1060+
create a :class:`~plexapi.myplex.MyPlexAccount` instance.
1061+
This helper class supports a polling, threaded and callback approach.
1062+
1063+
- The polling approach expects the developer to periodically check if the PIN login was
1064+
successful using :func:`plexapi.myplex.MyPlexPinLogin.checkLogin`.
1065+
- The threaded approach expects the developer to call
1066+
:func:`plexapi.myplex.MyPlexPinLogin.run` and then at a later time call
1067+
:func:`plexapi.myplex.MyPlexPinLogin.waitForLogin` to wait for and check the result.
1068+
- The callback approach is an extension of the threaded approach and expects the developer
1069+
to pass the `callback` parameter to the call to :func:`plexapi.myplex.MyPlexPinLogin.run`.
1070+
The callback will be called when the thread waiting for the PIN login to succeed either
1071+
finishes or expires. The parameter passed to the callback is the received authentication
1072+
token or `None` if the login expired.
1073+
1074+
Parameters:
1075+
session (requests.Session, optional): Use your own session object if you want to
1076+
cache the http responses from PMS
1077+
requestTimeout (int): timeout in seconds on initial connect to plex.tv (default config.TIMEOUT).
1078+
1079+
Attributes:
1080+
PINS (str): 'https://plex.tv/pins.xml'
1081+
CHECKPINS (str): 'https://plex.tv/pins/{pinid}.xml'
1082+
POLLINTERVAL (int): 1
1083+
finished (bool): Whether the pin login has finished or not.
1084+
expired (bool): Whether the pin login has expired or not.
1085+
token (str): Token retrieved through the pin login.
1086+
pin (str): Pin to use for the login on https://plex.tv/link.
1087+
"""
1088+
PINS = 'https://plex.tv/pins.xml' # get
1089+
CHECKPINS = 'https://plex.tv/pins/{pinid}.xml' # get
1090+
POLLINTERVAL = 1
1091+
1092+
def __init__(self, session=None, requestTimeout=None):
1093+
super(MyPlexPinLogin, self).__init__()
1094+
self._session = session or requests.Session()
1095+
self._requestTimeout = requestTimeout or TIMEOUT
1096+
1097+
self._loginTimeout = None
1098+
self._callback = None
1099+
self._thread = None
1100+
self._abort = False
1101+
self._id = None
1102+
1103+
self.finished = False
1104+
self.expired = False
1105+
self.token = None
1106+
self.pin = self._getPin()
1107+
1108+
def run(self, callback=None, timeout=None):
1109+
""" Starts the thread which monitors the PIN login state.
1110+
Parameters:
1111+
callback (Callable[str]): Callback called with the received authentication token (optional).
1112+
timeout (int): Timeout in seconds waiting for the PIN login to succeed (optional).
1113+
1114+
Raises:
1115+
:class:`RuntimeError`: if the thread is already running.
1116+
:class:`RuntimeError`: if the PIN login for the current PIN has expired.
1117+
"""
1118+
if self._thread and not self._abort:
1119+
raise RuntimeError('MyPlexPinLogin thread is already running')
1120+
if self.expired:
1121+
raise RuntimeError('MyPlexPinLogin has expired')
1122+
1123+
self._loginTimeout = timeout
1124+
self._callback = callback
1125+
self._abort = False
1126+
self.finished = False
1127+
self._thread = threading.Thread(target=self._pollLogin, name='plexapi.myplex.MyPlexPinLogin')
1128+
self._thread.start()
1129+
1130+
def waitForLogin(self):
1131+
""" Waits for the PIN login to succeed or expire.
1132+
Parameters:
1133+
callback (Callable[str]): Callback called with the received authentication token (optional).
1134+
timeout (int): Timeout in seconds waiting for the PIN login to succeed (optional).
1135+
1136+
Returns:
1137+
`True` if the PIN login succeeded or `False` otherwise.
1138+
"""
1139+
if not self._thread or self._abort:
1140+
return False
1141+
1142+
self._thread.join()
1143+
if self.expired or not self.token:
1144+
return False
1145+
1146+
return True
1147+
1148+
def stop(self):
1149+
""" Stops the thread monitoring the PIN login state. """
1150+
if not self._thread or self._abort:
1151+
return
1152+
1153+
self._abort = True
1154+
self._thread.join()
1155+
1156+
def checkLogin(self):
1157+
""" Returns `True` if the PIN login has succeeded. """
1158+
if self._thread:
1159+
return False
1160+
1161+
try:
1162+
return self._checkLogin()
1163+
except Exception:
1164+
self.expired = True
1165+
self.finished = True
1166+
1167+
return False
1168+
1169+
def _getPin(self):
1170+
if self.pin:
1171+
return self.pin
1172+
1173+
url = self.PINS
1174+
response = self._query(url, self._session.post)
1175+
if not response:
1176+
return None
1177+
1178+
self._id = response.find('id').text
1179+
self.pin = response.find('code').text
1180+
1181+
return self.pin
1182+
1183+
def _checkLogin(self):
1184+
if not self._id:
1185+
return False
1186+
1187+
if self.token:
1188+
return True
1189+
1190+
url = self.CHECKPINS.format(pinid=self._id)
1191+
response = self._query(url)
1192+
if not response:
1193+
return False
1194+
1195+
token = response.find('auth_token').text
1196+
if not token:
1197+
return False
1198+
1199+
self.token = token
1200+
self.finished = True
1201+
return True
1202+
1203+
def _pollLogin(self):
1204+
try:
1205+
start = time.time()
1206+
while not self._abort and (not self._loginTimeout or (time.time() - start) < self._loginTimeout):
1207+
try:
1208+
result = self._checkLogin()
1209+
except Exception:
1210+
self.expired = True
1211+
break
1212+
1213+
if result:
1214+
break
1215+
1216+
time.sleep(self.POLLINTERVAL)
1217+
1218+
if self.token and self._callback:
1219+
self._callback(self.token)
1220+
finally:
1221+
self.finished = True
1222+
1223+
def _query(self, url, method=None):
1224+
method = method or self._session.get
1225+
log.debug('%s %s', method.__name__.upper(), url)
1226+
headers = BASE_HEADERS.copy()
1227+
response = method(url, headers=headers, timeout=self._requestTimeout)
1228+
if not response.ok: # pragma: no cover
1229+
codename = codes.get(response.status_code)[0]
1230+
errtext = response.text.replace('\n', ' ')
1231+
raise BadRequest('(%s) %s %s; %s' % (response.status_code, codename, response.url, errtext))
1232+
data = response.text.encode('utf8')
1233+
return ElementTree.fromstring(data) if data.strip() else None
1234+
1235+
10551236
def _connect(cls, url, token, timeout, results, i, job_is_done_event=None):
10561237
""" Connects to the specified cls with url and token. Stores the connection
10571238
information to results[i] in a threadsafe way.

0 commit comments

Comments
 (0)