|
1 | 1 | # -*- coding: utf-8 -*- |
2 | 2 | import copy |
| 3 | +import threading |
3 | 4 | import time |
4 | 5 |
|
5 | 6 | import requests |
@@ -1052,6 +1053,186 @@ def syncItems(self): |
1052 | 1053 | return self._server.syncItems(client=self) |
1053 | 1054 |
|
1054 | 1055 |
|
| 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 | + |
1055 | 1236 | def _connect(cls, url, token, timeout, results, i, job_is_done_event=None): |
1056 | 1237 | """ Connects to the specified cls with url and token. Stores the connection |
1057 | 1238 | information to results[i] in a threadsafe way. |
|
0 commit comments