2018-07-13 02:46:02 +10:00
|
|
|
#!/usr/bin/env python
|
2018-02-11 03:59:20 +11:00
|
|
|
# -*- coding: utf-8 -*-
|
2018-07-13 02:46:02 +10:00
|
|
|
from __future__ import absolute_import, division, unicode_literals
|
2018-02-11 03:59:20 +11:00
|
|
|
from logging import getLogger
|
2018-09-11 04:53:46 +10:00
|
|
|
import time
|
|
|
|
import threading
|
|
|
|
import xbmc
|
|
|
|
import xbmcgui
|
2018-02-11 03:59:20 +11:00
|
|
|
|
2018-06-22 03:24:37 +10:00
|
|
|
from .downloadutils import DownloadUtils as DU
|
2018-09-11 04:53:46 +10:00
|
|
|
from . import utils, variables as v, state
|
2018-02-11 03:59:20 +11:00
|
|
|
|
|
|
|
###############################################################################
|
2018-09-11 04:53:46 +10:00
|
|
|
LOG = getLogger('PLEX.plex_tv')
|
2018-02-11 03:59:20 +11:00
|
|
|
###############################################################################
|
|
|
|
|
|
|
|
|
2018-09-11 04:53:46 +10:00
|
|
|
class HomeUser(utils.AttributeDict):
|
2018-02-11 03:59:20 +11:00
|
|
|
"""
|
2018-09-11 04:53:46 +10:00
|
|
|
Turns an etree xml answer into an object with attributes
|
2018-09-15 23:46:06 +10:00
|
|
|
Adds the additional properties:
|
|
|
|
isProtected
|
|
|
|
isAdmin
|
|
|
|
isManaged
|
2018-02-11 03:59:20 +11:00
|
|
|
"""
|
2018-09-15 23:46:06 +10:00
|
|
|
@property
|
|
|
|
def isProtected(self):
|
|
|
|
return self.protected == '1'
|
|
|
|
|
|
|
|
@property
|
|
|
|
def isAdmin(self):
|
|
|
|
return self.admin == '1'
|
|
|
|
|
|
|
|
@property
|
|
|
|
def isManaged(self):
|
|
|
|
return self.restricted == '1'
|
2018-09-11 04:53:46 +10:00
|
|
|
|
|
|
|
|
|
|
|
def homeuser_to_settings(user):
|
|
|
|
"""
|
|
|
|
Writes one HomeUser to the Kodi settings file
|
|
|
|
"""
|
|
|
|
utils.settings('myplexlogin', 'true')
|
|
|
|
utils.settings('plexLogin', user.title)
|
|
|
|
utils.settings('plexToken', user.authToken)
|
|
|
|
utils.settings('plexid', user.id)
|
|
|
|
utils.settings('plexAvatar', user.thumb)
|
|
|
|
utils.settings('plex_status', value=utils.lang(39227))
|
|
|
|
|
|
|
|
|
|
|
|
def switch_home_user(userid, pin, token, machine_identifier):
|
2018-02-11 03:59:20 +11:00
|
|
|
"""
|
2018-09-11 04:53:46 +10:00
|
|
|
Retrieves Plex home token for a Plex home user. Returns None if this fails
|
2018-02-11 03:59:20 +11:00
|
|
|
|
|
|
|
Input:
|
|
|
|
userid id of the Plex home user
|
|
|
|
pin PIN of the Plex home user, if protected
|
|
|
|
token token for plex.tv
|
|
|
|
|
|
|
|
Output:
|
2018-09-11 04:53:46 +10:00
|
|
|
usertoken Might be empty strings if no token found
|
|
|
|
for the machine_identifier that was chosen
|
2018-02-11 03:59:20 +11:00
|
|
|
|
2018-06-22 03:24:37 +10:00
|
|
|
utils.settings('userid') and utils.settings('username') with new plex token
|
2018-02-11 03:59:20 +11:00
|
|
|
"""
|
|
|
|
LOG.info('Switching to user %s', userid)
|
2018-09-11 04:53:46 +10:00
|
|
|
url = 'https://plex.tv/api/home/users/%s/switch' % userid
|
2018-02-11 03:59:20 +11:00
|
|
|
if pin:
|
2018-09-11 04:53:46 +10:00
|
|
|
url += '?pin=%s' % pin
|
|
|
|
xml = DU().downloadUrl(url,
|
|
|
|
authenticate=False,
|
|
|
|
action_type="POST",
|
|
|
|
headerOptions={'X-Plex-Token': token})
|
2018-02-11 03:59:20 +11:00
|
|
|
try:
|
2018-09-11 04:53:46 +10:00
|
|
|
xml.attrib
|
2018-02-11 03:59:20 +11:00
|
|
|
except AttributeError:
|
2018-09-11 04:53:46 +10:00
|
|
|
LOG.error('Switch HomeUser change failed')
|
|
|
|
return
|
2018-02-11 03:59:20 +11:00
|
|
|
|
2018-09-11 04:53:46 +10:00
|
|
|
username = xml.get('title', '')
|
|
|
|
token = xml.get('authenticationToken', '')
|
2018-02-11 03:59:20 +11:00
|
|
|
|
|
|
|
# Write to settings file
|
2018-06-22 03:24:37 +10:00
|
|
|
utils.settings('username', username)
|
|
|
|
utils.settings('accessToken', token)
|
2018-09-11 04:53:46 +10:00
|
|
|
utils.settings('userid', xml.get('id', ''))
|
2018-06-22 03:24:37 +10:00
|
|
|
utils.settings('plex_restricteduser',
|
2018-09-11 04:53:46 +10:00
|
|
|
'true' if xml.get('restricted', '0') == '1'
|
2018-06-22 03:24:37 +10:00
|
|
|
else 'false')
|
2018-02-11 03:59:20 +11:00
|
|
|
state.RESTRICTED_USER = True if \
|
2018-09-11 04:53:46 +10:00
|
|
|
xml.get('restricted', '0') == '1' else False
|
2018-02-11 03:59:20 +11:00
|
|
|
|
|
|
|
# Get final token to the PMS we've chosen
|
|
|
|
url = 'https://plex.tv/api/resources?includeHttps=1'
|
|
|
|
xml = DU().downloadUrl(url,
|
|
|
|
authenticate=False,
|
|
|
|
headerOptions={'X-Plex-Token': token})
|
|
|
|
try:
|
|
|
|
xml.attrib
|
|
|
|
except AttributeError:
|
|
|
|
LOG.error('Answer from plex.tv not as excepted')
|
|
|
|
# Set to empty iterable list for loop
|
|
|
|
xml = []
|
|
|
|
|
|
|
|
found = 0
|
2018-09-11 04:53:46 +10:00
|
|
|
LOG.debug('Our machine_identifier is %s', machine_identifier)
|
2018-02-11 03:59:20 +11:00
|
|
|
for device in xml:
|
|
|
|
identifier = device.attrib.get('clientIdentifier')
|
2018-09-11 04:53:46 +10:00
|
|
|
LOG.debug('Found the Plex clientIdentifier: %s', identifier)
|
|
|
|
if identifier == machine_identifier:
|
2018-02-11 03:59:20 +11:00
|
|
|
found += 1
|
|
|
|
token = device.attrib.get('accessToken')
|
|
|
|
if found == 0:
|
|
|
|
LOG.info('No tokens found for your server! Using empty string')
|
2018-09-11 04:53:46 +10:00
|
|
|
token = ''
|
2018-02-11 03:59:20 +11:00
|
|
|
LOG.info('Plex.tv switch HomeUser change successfull for user %s',
|
|
|
|
username)
|
2018-09-11 04:53:46 +10:00
|
|
|
return token
|
2018-02-11 03:59:20 +11:00
|
|
|
|
|
|
|
|
2018-09-11 04:53:46 +10:00
|
|
|
def plex_home_users(token):
|
2018-02-11 03:59:20 +11:00
|
|
|
"""
|
2018-09-11 04:53:46 +10:00
|
|
|
Returns a list of HomeUser elements from plex.tv
|
2018-02-11 03:59:20 +11:00
|
|
|
"""
|
|
|
|
xml = DU().downloadUrl('https://plex.tv/api/home/users/',
|
|
|
|
authenticate=False,
|
|
|
|
headerOptions={'X-Plex-Token': token})
|
2018-09-11 04:53:46 +10:00
|
|
|
users = []
|
2018-02-11 03:59:20 +11:00
|
|
|
try:
|
|
|
|
xml.attrib
|
|
|
|
except AttributeError:
|
|
|
|
LOG.error('Download of Plex home users failed.')
|
2018-09-11 04:53:46 +10:00
|
|
|
else:
|
|
|
|
for user in xml:
|
|
|
|
users.append(HomeUser(user.attrib))
|
2018-02-11 03:59:20 +11:00
|
|
|
return users
|
|
|
|
|
|
|
|
|
2018-09-11 04:53:46 +10:00
|
|
|
class PinLogin(object):
|
|
|
|
"""
|
|
|
|
Signs user in to plex.tv
|
|
|
|
"""
|
|
|
|
INIT = 'https://plex.tv/pins.xml'
|
|
|
|
POLL = 'https://plex.tv/pins/{0}.xml'
|
|
|
|
ACCOUNT = 'https://plex.tv/users/account'
|
|
|
|
POLL_INTERVAL = 1
|
|
|
|
|
|
|
|
def __init__(self, callback=None):
|
|
|
|
self._callback = callback
|
|
|
|
self.id = None
|
|
|
|
self.pin = None
|
|
|
|
self.token = None
|
|
|
|
self.finished = False
|
|
|
|
self._abort = False
|
|
|
|
self.expired = False
|
|
|
|
self.xml = None
|
|
|
|
self._init()
|
|
|
|
|
|
|
|
def _init(self):
|
|
|
|
xml = DU().downloadUrl(self.INIT,
|
|
|
|
authenticate=False,
|
|
|
|
action_type="POST")
|
|
|
|
try:
|
|
|
|
xml.attrib
|
|
|
|
except AttributeError:
|
|
|
|
LOG.error("Error, no PIN from plex.tv provided")
|
|
|
|
raise RuntimeError
|
|
|
|
self.pin = xml.find('code').text
|
|
|
|
self.id = xml.find('id').text
|
|
|
|
LOG.debug('Successfully retrieved code and id from plex.tv')
|
|
|
|
|
|
|
|
def _poll(self):
|
|
|
|
LOG.debug('Start polling plex.tv for token')
|
|
|
|
start = time.time()
|
|
|
|
while (not self._abort and
|
|
|
|
time.time() - start < 300 and
|
|
|
|
not state.STOP_PKC):
|
|
|
|
xml = DU().downloadUrl(self.POLL.format(self.id),
|
|
|
|
authenticate=False)
|
|
|
|
try:
|
|
|
|
token = xml.find('auth_token').text
|
|
|
|
except AttributeError:
|
|
|
|
time.sleep(self.POLL_INTERVAL)
|
|
|
|
continue
|
|
|
|
if token:
|
|
|
|
self.token = token
|
|
|
|
break
|
|
|
|
time.sleep(self.POLL_INTERVAL)
|
|
|
|
if self._callback:
|
|
|
|
self._callback(self.token, self.xml)
|
|
|
|
if self.token:
|
|
|
|
# Use temp token to get the final plex credentials
|
|
|
|
self.xml = DU().downloadUrl(self.ACCOUNT,
|
|
|
|
authenticate=False,
|
|
|
|
parameters={'X-Plex-Token': self.token})
|
|
|
|
self.finished = True
|
|
|
|
LOG.debug('Polling done')
|
|
|
|
|
|
|
|
def start_token_poll(self):
|
|
|
|
t = threading.Thread(target=self._poll, name='PIN-LOGIN:Token-Poll')
|
|
|
|
t.start()
|
|
|
|
return t
|
|
|
|
|
|
|
|
def wait_for_token(self):
|
|
|
|
t = self.start_token_poll()
|
|
|
|
t.join()
|
|
|
|
return self.token
|
|
|
|
|
|
|
|
def abort(self):
|
|
|
|
self._abort = True
|
|
|
|
|
|
|
|
|
2018-02-11 03:59:20 +11:00
|
|
|
def sign_in_with_pin():
|
|
|
|
"""
|
|
|
|
Prompts user to sign in by visiting https://plex.tv/pin
|
|
|
|
|
2018-09-11 04:53:46 +10:00
|
|
|
Writes to Kodi settings file and returns the HomeUser or None
|
2018-02-11 03:59:20 +11:00
|
|
|
"""
|
2018-09-11 04:53:46 +10:00
|
|
|
xml = _sign_in_with_pin()
|
|
|
|
if not xml:
|
|
|
|
return
|
|
|
|
user = HomeUser(xml.attrib)
|
|
|
|
homeuser_to_settings(user)
|
|
|
|
return user
|
|
|
|
|
|
|
|
|
|
|
|
class TestWindow(xbmcgui.Window):
|
|
|
|
def onAction(self, action):
|
|
|
|
LOG.debug('onAction: %s', action)
|
|
|
|
|
|
|
|
def _sign_in_with_pin():
|
|
|
|
"""
|
|
|
|
Returns the user xml answer from plex.tv or None if unsuccessful
|
|
|
|
"""
|
|
|
|
from .dialogs import signin
|
|
|
|
return
|
|
|
|
|
|
|
|
back = signin.Background.create()
|
|
|
|
try:
|
|
|
|
pre = signin.PreSignInWindow.open()
|
|
|
|
try:
|
|
|
|
if not pre.doSignin:
|
|
|
|
return
|
|
|
|
finally:
|
|
|
|
del pre
|
|
|
|
|
|
|
|
while True:
|
|
|
|
pin_login_window = signin.PinLoginWindow.create()
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
pinlogin = PinLogin()
|
|
|
|
except RuntimeError:
|
|
|
|
# Could not sign in to plex.tv Try again later
|
|
|
|
utils.dialog('ok',
|
|
|
|
heading='{plex}',
|
|
|
|
line1=utils.lang(39305))
|
|
|
|
return
|
|
|
|
pin_login_window.setPin(pinlogin.pin)
|
|
|
|
pinlogin.start_token_poll()
|
|
|
|
while not pinlogin.finished:
|
|
|
|
if pin_login_window.abort:
|
|
|
|
LOG.debug('Pin login aborted')
|
|
|
|
pinlogin.abort()
|
|
|
|
return
|
|
|
|
time.sleep(0.1)
|
|
|
|
if not pinlogin.expired:
|
|
|
|
if pinlogin.xml:
|
|
|
|
pin_login_window.setLinking()
|
|
|
|
return pinlogin.xml
|
|
|
|
return
|
|
|
|
finally:
|
|
|
|
pin_login_window.doClose()
|
|
|
|
del pin_login_window
|
|
|
|
if pinlogin.expired:
|
|
|
|
LOG.debug('Pin expired')
|
|
|
|
expired_window = signin.ExpiredWindow.open()
|
|
|
|
try:
|
|
|
|
if not expired_window.refresh:
|
|
|
|
LOG.debug('Pin refresh aborted')
|
|
|
|
return
|
|
|
|
finally:
|
|
|
|
del expired_window
|
|
|
|
finally:
|
|
|
|
back.doClose()
|
|
|
|
del back
|
2018-02-11 03:59:20 +11:00
|
|
|
|
|
|
|
|
|
|
|
def get_pin():
|
|
|
|
"""
|
|
|
|
For plex.tv sign-in: returns 4-digit code and identifier as 2 str
|
|
|
|
"""
|
|
|
|
code = None
|
|
|
|
identifier = None
|
|
|
|
# Download
|
|
|
|
xml = DU().downloadUrl('https://plex.tv/pins.xml',
|
|
|
|
authenticate=False,
|
|
|
|
action_type="POST")
|
|
|
|
try:
|
|
|
|
xml.attrib
|
|
|
|
except AttributeError:
|
|
|
|
LOG.error("Error, no PIN from plex.tv provided")
|
|
|
|
return None, None
|
|
|
|
code = xml.find('code').text
|
|
|
|
identifier = xml.find('id').text
|
|
|
|
LOG.info('Successfully retrieved code and id from plex.tv')
|
|
|
|
return code, identifier
|
|
|
|
|
|
|
|
|
|
|
|
def check_pin(identifier):
|
|
|
|
"""
|
|
|
|
Checks with plex.tv whether user entered the correct PIN on plex.tv/pin
|
|
|
|
|
2018-09-11 04:53:46 +10:00
|
|
|
Returns None if not yet done so, or the XML response file as etree
|
2018-02-11 03:59:20 +11:00
|
|
|
"""
|
|
|
|
# Try to get a temporary token
|
|
|
|
xml = DU().downloadUrl('https://plex.tv/pins/%s.xml' % identifier,
|
|
|
|
authenticate=False)
|
|
|
|
try:
|
|
|
|
temp_token = xml.find('auth_token').text
|
|
|
|
except AttributeError:
|
|
|
|
LOG.error("Could not find token in plex.tv answer")
|
2018-09-11 04:53:46 +10:00
|
|
|
return
|
2018-02-11 03:59:20 +11:00
|
|
|
if not temp_token:
|
2018-09-11 04:53:46 +10:00
|
|
|
return
|
2018-02-11 03:59:20 +11:00
|
|
|
# Use temp token to get the final plex credentials
|
|
|
|
xml = DU().downloadUrl('https://plex.tv/users/account',
|
|
|
|
authenticate=False,
|
|
|
|
parameters={'X-Plex-Token': temp_token})
|
|
|
|
return xml
|