Add Plex dialog to switch users

This commit is contained in:
croneter 2018-09-10 20:53:46 +02:00
parent 233f6065ee
commit 98e38ae9a8
14 changed files with 2007 additions and 256 deletions

View file

@ -1119,6 +1119,11 @@ msgctxt "#39228"
msgid "Plex user:" msgid "Plex user:"
msgstr "" msgstr ""
# Error message if user could not log in; the actual user name will be appended at the end of the string
msgctxt "#39229"
msgid "Login failed with plex.tv for user"
msgstr ""
msgctxt "#39250" msgctxt "#39250"
msgid "Running the image cache process can take some time. It will happen in the background. Are you sure you want continue?" msgid "Running the image cache process can take some time. It will happen in the background. Are you sure you want continue?"
msgstr "" msgstr ""

View file

@ -0,0 +1,302 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
import threading
import Queue
import heapq
import xbmc
from . import utils
LOG = getLogger('PLEX.' + __name__)
class KillableThread(threading.Thread):
pass
'''A thread class that supports raising exception in the thread from
another thread.
'''
# def _get_my_tid(self):
# """determines this (self's) thread id
# CAREFUL : this function is executed in the context of the caller
# thread, to get the identity of the thread represented by this
# instance.
# """
# if not self.isAlive():
# raise threading.ThreadError("the thread is not active")
# return self.ident
# def _raiseExc(self, exctype):
# """Raises the given exception type in the context of this thread.
# If the thread is busy in a system call (time.sleep(),
# socket.accept(), ...), the exception is simply ignored.
# If you are sure that your exception should terminate the thread,
# one way to ensure that it works is:
# t = ThreadWithExc( ... )
# ...
# t.raiseExc( SomeException )
# while t.isAlive():
# time.sleep( 0.1 )
# t.raiseExc( SomeException )
# If the exception is to be caught by the thread, you need a way to
# check that your thread has caught it.
# CAREFUL : this function is executed in the context of the
# caller thread, to raise an excpetion in the context of the
# thread represented by this instance.
# """
# _async_raise(self._get_my_tid(), exctype)
def kill(self, force_and_wait=False):
pass
# try:
# self._raiseExc(KillThreadException)
# if force_and_wait:
# time.sleep(0.1)
# while self.isAlive():
# self._raiseExc(KillThreadException)
# time.sleep(0.1)
# except threading.ThreadError:
# pass
# def onKilled(self):
# pass
# def run(self):
# try:
# self._Thread__target(*self._Thread__args, **self._Thread__kwargs)
# except KillThreadException:
# self.onKilled()
class Tasks(list):
def add(self, task):
for t in self:
if not t.isValid():
self.remove(t)
if isinstance(task, list):
self += task
else:
self.append(task)
def cancel(self):
while self:
self.pop().cancel()
class Task:
def __init__(self, priority=None):
self._priority = priority
self._canceled = False
self.finished = False
def __cmp__(self, other):
return self._priority - other._priority
def start(self):
BGThreader.addTask(self)
def _run(self):
self.run()
self.finished = True
def run(self):
pass
def cancel(self):
self._canceled = True
def isCanceled(self):
return self._canceled or xbmc.abortRequested
def isValid(self):
return not self.finished and not self._canceled
class MutablePriorityQueue(Queue.PriorityQueue):
def _get(self, heappop=heapq.heappop):
self.queue.sort()
return heappop(self.queue)
def lowest(self):
"""Return the lowest priority item in the queue (not reliable!)."""
self.mutex.acquire()
try:
lowest = self.queue and min(self.queue) or None
except:
lowest = None
utils.ERROR()
finally:
self.mutex.release()
return lowest
class BackgroundWorker:
def __init__(self, queue, name=None):
self._queue = queue
self.name = name
self._thread = None
self._abort = False
self._task = None
def _runTask(self, task):
if task._canceled:
return
try:
task._run()
except:
utils.ERROR()
def abort(self):
self._abort = True
return self
def aborted(self):
return self._abort or xbmc.abortRequested
def start(self):
if self._thread and self._thread.isAlive():
return
self._thread = KillableThread(target=self._queueLoop, name='BACKGROUND-WORKER({0})'.format(self.name))
self._thread.start()
def _queueLoop(self):
if self._queue.empty():
return
LOG.debug('(%s): Active', self.name)
try:
while not self.aborted():
self._task = self._queue.get_nowait()
self._runTask(self._task)
self._queue.task_done()
self._task = None
except Queue.Empty:
LOG.debug('(%s): Idle', self.name)
def shutdown(self):
self.abort()
if self._task:
self._task.cancel()
if self._thread and self._thread.isAlive():
LOG.debug('thread (%s): Waiting...', self.name)
self._thread.join()
LOG.debug('thread (%s): Done', self.name)
def working(self):
return self._thread and self._thread.isAlive()
class BackgroundThreader:
def __init__(self, name=None, worker_count=8):
self.name = name
self._queue = MutablePriorityQueue()
self._abort = False
self._priority = -1
self.workers = [BackgroundWorker(self._queue, 'queue.{0}:worker.{1}'.format(self.name, x)) for x in range(worker_count)]
def _nextPriority(self):
self._priority += 1
return self._priority
def abort(self):
self._abort = True
for w in self.workers:
w.abort()
return self
def aborted(self):
return self._abort or xbmc.abortRequested
def shutdown(self):
self.abort()
for w in self.workers:
w.shutdown()
def addTask(self, task):
task._priority = self._nextPriority()
self._queue.put(task)
self.startWorkers()
def addTasks(self, tasks):
for t in tasks:
t._priority = self._nextPriority()
self._queue.put(t)
self.startWorkers()
def addTasksToFront(self, tasks):
lowest = self.getLowestPrority()
if lowest is None:
return self.addTasks(tasks)
p = lowest - len(tasks)
for t in tasks:
t._priority = p
self._queue.put(t)
p += 1
self.startWorkers()
def startWorkers(self):
for w in self.workers:
w.start()
def working(self):
return not self._queue.empty() or self.hasTask()
def hasTask(self):
return any([w.working() for w in self.workers])
def getLowestPrority(self):
lowest = self._queue.lowest()
if not lowest:
return None
return lowest._priority
def moveToFront(self, qitem):
lowest = self.getLowestPrority()
if lowest is None:
return
qitem._priority = lowest - 1
class ThreaderManager:
def __init__(self):
self.index = 0
self.abandoned = []
self.threader = BackgroundThreader(str(self.index))
def __getattr__(self, name):
return getattr(self.threader, name)
def reset(self):
if self.threader._queue.empty() and not self.threader.hasTask():
return
self.index += 1
self.abandoned.append(self.threader.abort())
self.threader = BackgroundThreader(str(self.index))
def shutdown(self):
self.threader.shutdown()
for a in self.abandoned:
a.shutdown()
BGThreader = ThreaderManager()

View file

@ -74,7 +74,6 @@ def toggle_plex_tv_sign_in():
utils.settings('plexLogin', value="") utils.settings('plexLogin', value="")
utils.settings('plexToken', value="") utils.settings('plexToken', value="")
utils.settings('plexid', value="") utils.settings('plexid', value="")
utils.settings('plexHomeSize', value="1")
utils.settings('plexAvatar', value="") utils.settings('plexAvatar', value="")
utils.settings('plex_status', value=utils.lang(39226)) utils.settings('plex_status', value=utils.lang(39226))

View file

@ -164,11 +164,14 @@ class InitialSetup(object):
Returns True if successful, or False if not Returns True if successful, or False if not
""" """
result = plex_tv.sign_in_with_pin() try:
if result: user = plex_tv.sign_in_with_pin()
self.plex_login = result['username'] except:
self.plex_token = result['token'] utils.ERROR()
self.plexid = result['plexid'] if user:
self.plex_login = user.username
self.plex_token = user.authToken
self.plexid = user.id
return True return True
return False return False
@ -209,10 +212,7 @@ class InitialSetup(object):
else: else:
utils.settings('plexLogin', value=self.plex_login) utils.settings('plexLogin', value=self.plex_login)
home = 'true' if xml.attrib.get('home') == '1' else 'false' home = 'true' if xml.attrib.get('home') == '1' else 'false'
utils.settings('plexhome', value=home)
utils.settings('plexAvatar', value=xml.attrib.get('thumb')) utils.settings('plexAvatar', value=xml.attrib.get('thumb'))
utils.settings('plexHomeSize',
value=xml.attrib.get('homeSize', '1'))
LOG.info('Updated Plex info from plex.tv') LOG.info('Updated Plex info from plex.tv')
return answer return answer

View file

@ -90,27 +90,22 @@ def GetPlexLoginFromSettings():
Returns a dict: Returns a dict:
'plexLogin': utils.settings('plexLogin'), 'plexLogin': utils.settings('plexLogin'),
'plexToken': utils.settings('plexToken'), 'plexToken': utils.settings('plexToken'),
'plexhome': utils.settings('plexhome'),
'plexid': utils.settings('plexid'), 'plexid': utils.settings('plexid'),
'myplexlogin': utils.settings('myplexlogin'), 'myplexlogin': utils.settings('myplexlogin'),
'plexAvatar': utils.settings('plexAvatar'), 'plexAvatar': utils.settings('plexAvatar'),
'plexHomeSize': utils.settings('plexHomeSize')
Returns strings or unicode Returns strings or unicode
Returns empty strings '' for a setting if not found. Returns empty strings '' for a setting if not found.
myplexlogin is 'true' if user opted to log into plex.tv (the default) myplexlogin is 'true' if user opted to log into plex.tv (the default)
plexhome is 'true' if plex home is used (the default)
""" """
return { return {
'plexLogin': utils.settings('plexLogin'), 'plexLogin': utils.settings('plexLogin'),
'plexToken': utils.settings('plexToken'), 'plexToken': utils.settings('plexToken'),
'plexhome': utils.settings('plexhome'),
'plexid': utils.settings('plexid'), 'plexid': utils.settings('plexid'),
'myplexlogin': utils.settings('myplexlogin'), 'myplexlogin': utils.settings('myplexlogin'),
'plexAvatar': utils.settings('plexAvatar'), 'plexAvatar': utils.settings('plexAvatar'),
'plexHomeSize': utils.settings('plexHomeSize')
} }
@ -812,15 +807,11 @@ def GetUserArtworkURL(username):
Returns the URL for the user's Avatar. Or False if something went Returns the URL for the user's Avatar. Or False if something went
wrong. wrong.
""" """
users = plex_tv.list_home_users(utils.settings('plexToken')) users = plex_tv.plex_home_users(utils.settings('plexToken'))
url = '' url = ''
# If an error is encountered, set to False
if not users:
LOG.info("Couldnt get user from plex.tv. No URL for user avatar")
return False
for user in users: for user in users:
if username in user['title']: if user.title == username:
url = user['thumb'] url = user.thumb
LOG.debug("Avatar url for user %s is: %s", username, url) LOG.debug("Avatar url for user %s is: %s", username, url)
return url return url

View file

@ -2,118 +2,41 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals from __future__ import absolute_import, division, unicode_literals
from logging import getLogger from logging import getLogger
from xbmc import sleep, executebuiltin import time
import threading
import xbmc
import xbmcgui
from .downloadutils import DownloadUtils as DU from .downloadutils import DownloadUtils as DU
from . import utils from . import utils, variables as v, state
from . import variables as v
from . import state
############################################################################### ###############################################################################
LOG = getLogger('PLEX.plex_tx') LOG = getLogger('PLEX.plex_tv')
############################################################################### ###############################################################################
def choose_home_user(token): class HomeUser(utils.AttributeDict):
""" """
Let's user choose from a list of Plex home users. Will switch to that Turns an etree xml answer into an object with attributes
user accordingly.
Returns a dict:
{
'username': Unicode
'userid': '' Plex ID of the user
'token': '' User's token
'protected': True if PIN is needed, else False
}
Will return False if something went wrong (wrong PIN, no connection)
""" """
# Get list of Plex home users pass
users = list_home_users(token)
if not users:
LOG.error("User download failed.")
return False
userlist = []
userlist_coded = []
for user in users:
username = user['title']
userlist.append(username)
# To take care of non-ASCII usernames
userlist_coded.append(utils.try_encode(username))
usernumber = len(userlist)
username = ''
usertoken = ''
trials = 0
while trials < 3:
if usernumber > 1:
# Select user
user_select = utils.dialog(
'select',
'%s%s' % (utils.lang(29999), utils.lang(39306)),
userlist_coded)
if user_select == -1:
LOG.info("No user selected.")
utils.settings('username', value='')
executebuiltin('Addon.Openutils.settings(%s)' % v.ADDON_ID)
return False
# Only 1 user received, choose that one
else:
user_select = 0
selected_user = userlist[user_select]
LOG.info("Selected user: %s", selected_user)
user = users[user_select]
# Ask for PIN, if protected:
pin = None
if user['protected'] == '1':
LOG.debug('Asking for users PIN')
pin = utils.dialog('input',
'%s%s' % (utils.lang(39307), selected_user),
'',
type='{numeric}',
option='{hide}')
# User chose to cancel
# Plex bug: don't call url for protected user with empty PIN
if not pin:
trials += 1
continue
# Switch to this Plex Home user, if applicable
result = switch_home_user(user['id'],
pin,
token,
utils.settings('plex_machineIdentifier'))
if result:
# Successfully retrieved username: break out of while loop
username = result['username']
usertoken = result['usertoken']
break
# Couldn't get user auth
else:
trials += 1
# Could not login user, please try again
if not utils.dialog('yesno',
heading='{plex}',
line1='%s%s' % (utils.lang(39308),
selected_user),
line2=utils.lang(39309)):
# User chose to cancel
break
if not username:
LOG.error('Failed signing in a user to plex.tv')
executebuiltin('Addon.Openutils.settings(%s)' % v.ADDON_ID)
return False
return {
'username': username,
'userid': user['id'],
'protected': True if user['protected'] == '1' else False,
'token': usertoken
}
def switch_home_user(userid, pin, token, machineIdentifier): def homeuser_to_settings(user):
""" """
Retrieves Plex home token for a Plex home user. Writes one HomeUser to the Kodi settings file
Returns False if unsuccessful """
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):
"""
Retrieves Plex home token for a Plex home user. Returns None if this fails
Input: Input:
userid id of the Plex home user userid id of the Plex home user
@ -121,40 +44,37 @@ def switch_home_user(userid, pin, token, machineIdentifier):
token token for plex.tv token token for plex.tv
Output: Output:
{ usertoken Might be empty strings if no token found
'username' for the machine_identifier that was chosen
'usertoken' Might be empty strings if no token found
for the machineIdentifier that was chosen
}
utils.settings('userid') and utils.settings('username') with new plex token utils.settings('userid') and utils.settings('username') with new plex token
""" """
LOG.info('Switching to user %s', userid) LOG.info('Switching to user %s', userid)
url = 'https://plex.tv/api/home/users/' + userid + '/switch' url = 'https://plex.tv/api/home/users/%s/switch' % userid
if pin: if pin:
url += '?pin=' + pin url += '?pin=%s' % pin
answer = DU().downloadUrl(url, xml = DU().downloadUrl(url,
authenticate=False, authenticate=False,
action_type="POST", action_type="POST",
headerOptions={'X-Plex-Token': token}) headerOptions={'X-Plex-Token': token})
try: try:
answer.attrib xml.attrib
except AttributeError: except AttributeError:
LOG.error('Error: plex.tv switch HomeUser change failed') LOG.error('Switch HomeUser change failed')
return False return
username = answer.attrib.get('title', '') username = xml.get('title', '')
token = answer.attrib.get('authenticationToken', '') token = xml.get('authenticationToken', '')
# Write to settings file # Write to settings file
utils.settings('username', username) utils.settings('username', username)
utils.settings('accessToken', token) utils.settings('accessToken', token)
utils.settings('userid', answer.attrib.get('id', '')) utils.settings('userid', xml.get('id', ''))
utils.settings('plex_restricteduser', utils.settings('plex_restricteduser',
'true' if answer.attrib.get('restricted', '0') == '1' 'true' if xml.get('restricted', '0') == '1'
else 'false') else 'false')
state.RESTRICTED_USER = True if \ state.RESTRICTED_USER = True if \
answer.attrib.get('restricted', '0') == '1' else False xml.get('restricted', '0') == '1' else False
# Get final token to the PMS we've chosen # Get final token to the PMS we've chosen
url = 'https://plex.tv/api/resources?includeHttps=1' url = 'https://plex.tv/api/resources?includeHttps=1'
@ -169,133 +89,186 @@ def switch_home_user(userid, pin, token, machineIdentifier):
xml = [] xml = []
found = 0 found = 0
LOG.debug('Our machineIdentifier is %s', machineIdentifier) LOG.debug('Our machine_identifier is %s', machine_identifier)
for device in xml: for device in xml:
identifier = device.attrib.get('clientIdentifier') identifier = device.attrib.get('clientIdentifier')
LOG.debug('Found a Plex machineIdentifier: %s', identifier) LOG.debug('Found the Plex clientIdentifier: %s', identifier)
if identifier == machineIdentifier: if identifier == machine_identifier:
found += 1 found += 1
token = device.attrib.get('accessToken') token = device.attrib.get('accessToken')
result = {
'username': username,
}
if found == 0: if found == 0:
LOG.info('No tokens found for your server! Using empty string') LOG.info('No tokens found for your server! Using empty string')
result['usertoken'] = '' token = ''
else:
result['usertoken'] = token
LOG.info('Plex.tv switch HomeUser change successfull for user %s', LOG.info('Plex.tv switch HomeUser change successfull for user %s',
username) username)
return result return token
def list_home_users(token): def plex_home_users(token):
""" """
Returns a list for myPlex home users for the current plex.tv account. Returns a list of HomeUser elements from plex.tv
Input:
token for plex.tv
Output:
List of users, where one entry is of the form:
"id": userId,
"admin": '1'/'0',
"guest": '1'/'0',
"restricted": '1'/'0',
"protected": '1'/'0',
"email": email,
"title": title,
"username": username,
"thumb": thumb_url
}
If any value is missing, None is returned instead (or "" from plex.tv)
If an error is encountered, False is returned
""" """
xml = DU().downloadUrl('https://plex.tv/api/home/users/', xml = DU().downloadUrl('https://plex.tv/api/home/users/',
authenticate=False, authenticate=False,
headerOptions={'X-Plex-Token': token}) headerOptions={'X-Plex-Token': token})
users = []
try: try:
xml.attrib xml.attrib
except AttributeError: except AttributeError:
LOG.error('Download of Plex home users failed.') LOG.error('Download of Plex home users failed.')
return False else:
users = []
for user in xml: for user in xml:
users.append(user.attrib) users.append(HomeUser(user.attrib))
return users return users
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
def sign_in_with_pin(): def sign_in_with_pin():
""" """
Prompts user to sign in by visiting https://plex.tv/pin Prompts user to sign in by visiting https://plex.tv/pin
Writes to Kodi settings file. Also returns: Writes to Kodi settings file and returns the HomeUser or None
{
'plexhome': 'true' if Plex Home, 'false' otherwise
'username':
'avatar': URL to user avator
'token':
'plexid': Plex user ID
'homesize': Number of Plex home users (defaults to '1')
}
Returns False if authentication did not work.
""" """
code, identifier = get_pin() xml = _sign_in_with_pin()
if not code: if not xml:
# Problems trying to contact plex.tv. Try again later return
utils.dialog('ok', heading='{plex}', line1=utils.lang(39303)) user = HomeUser(xml.attrib)
return False homeuser_to_settings(user)
# Go to https://plex.tv/pin and enter the code: return user
# Or press No to cancel the sign in.
answer = utils.dialog('yesno',
heading='{plex}', class TestWindow(xbmcgui.Window):
line1='%s%s' % (utils.lang(39304), "\n\n"), def onAction(self, action):
line2='%s%s' % (code, "\n\n"), LOG.debug('onAction: %s', action)
line3=utils.lang(39311))
if not answer: def _sign_in_with_pin():
return False """
count = 0 Returns the user xml answer from plex.tv or None if unsuccessful
# Wait for approx 30 seconds (since the PIN is not visible anymore :-)) """
while count < 30: from .dialogs import signin
xml = check_pin(identifier) return
if xml is not False:
break back = signin.Background.create()
# Wait for 1 seconds try:
sleep(1000) pre = signin.PreSignInWindow.open()
count += 1 try:
if xml is False: 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 # Could not sign in to plex.tv Try again later
utils.dialog('ok', heading='{plex}', line1=utils.lang(39305)) utils.dialog('ok',
return False heading='{plex}',
# Parse xml line1=utils.lang(39305))
userid = xml.attrib.get('id') return
home = xml.get('home', '0') pin_login_window.setPin(pinlogin.pin)
if home == '1': pinlogin.start_token_poll()
home = 'true' while not pinlogin.finished:
else: if pin_login_window.abort:
home = 'false' LOG.debug('Pin login aborted')
username = xml.get('username', '') pinlogin.abort()
avatar = xml.get('thumb', '') return
token = xml.findtext('authentication-token') time.sleep(0.1)
home_size = xml.get('homeSize', '1') if not pinlogin.expired:
result = { if pinlogin.xml:
'plexhome': home, pin_login_window.setLinking()
'username': username, return pinlogin.xml
'avatar': avatar, return
'token': token, finally:
'plexid': userid, pin_login_window.doClose()
'homesize': home_size del pin_login_window
} if pinlogin.expired:
utils.settings('plexLogin', username) LOG.debug('Pin expired')
utils.settings('plexToken', token) expired_window = signin.ExpiredWindow.open()
utils.settings('plexhome', home) try:
utils.settings('plexid', userid) if not expired_window.refresh:
utils.settings('plexAvatar', avatar) LOG.debug('Pin refresh aborted')
utils.settings('plexHomeSize', home_size) return
# Let Kodi log into plex.tv on startup from now on finally:
utils.settings('myplexlogin', 'true') del expired_window
utils.settings('plex_status', value=utils.lang(39227)) finally:
return result back.doClose()
del back
def get_pin(): def get_pin():
@ -323,7 +296,7 @@ def check_pin(identifier):
""" """
Checks with plex.tv whether user entered the correct PIN on plex.tv/pin Checks with plex.tv whether user entered the correct PIN on plex.tv/pin
Returns False if not yet done so, or the XML response file as etree Returns None if not yet done so, or the XML response file as etree
""" """
# Try to get a temporary token # Try to get a temporary token
xml = DU().downloadUrl('https://plex.tv/pins/%s.xml' % identifier, xml = DU().downloadUrl('https://plex.tv/pins/%s.xml' % identifier,
@ -332,9 +305,9 @@ def check_pin(identifier):
temp_token = xml.find('auth_token').text temp_token = xml.find('auth_token').text
except AttributeError: except AttributeError:
LOG.error("Could not find token in plex.tv answer") LOG.error("Could not find token in plex.tv answer")
return False return
if not temp_token: if not temp_token:
return False return
# Use temp token to get the final plex credentials # Use temp token to get the final plex credentials
xml = DU().downloadUrl('https://plex.tv/users/account', xml = DU().downloadUrl('https://plex.tv/users/account',
authenticate=False, authenticate=False,

View file

@ -6,10 +6,10 @@ from threading import Thread
from xbmc import sleep, executebuiltin from xbmc import sleep, executebuiltin
from .windows import userselect
from .downloadutils import DownloadUtils as DU from .downloadutils import DownloadUtils as DU
from . import utils from . import utils
from . import path_ops from . import path_ops
from . import plex_tv
from . import plex_functions as PF from . import plex_functions as PF
from . import variables as v from . import variables as v
from . import state from . import state
@ -237,22 +237,22 @@ class UserClient(Thread):
plextoken = utils.settings('plexToken') plextoken = utils.settings('plexToken')
if plextoken: if plextoken:
LOG.info("Trying to connect to plex.tv to get a user list") LOG.info("Trying to connect to plex.tv to get a user list")
userInfo = plex_tv.choose_home_user(plextoken) user = userselect.start()
if userInfo is False: if not user:
# FAILURE: Something went wrong, try again # FAILURE: Something went wrong, try again
self.auth = True self.auth = True
self.retry += 1 self.retry += 1
return False return False
username = userInfo['username'] username = user.title
userId = userInfo['userid'] user_id = user.id
usertoken = userInfo['token'] usertoken = user.authToken
else: else:
LOG.info("Trying to authenticate without a token") LOG.info("Trying to authenticate without a token")
username = '' username = ''
userId = '' user_id = ''
usertoken = '' usertoken = ''
if self.load_user(username, userId, usertoken, authenticated=False): if self.load_user(username, user_id, usertoken, authenticated=False):
# SUCCESS: loaded a user from the settings # SUCCESS: loaded a user from the settings
return True return True
# Something went wrong, try again # Something went wrong, try again

View file

@ -18,13 +18,12 @@ from functools import wraps, partial
from urllib import quote_plus from urllib import quote_plus
import hashlib import hashlib
import re import re
import gc
import xbmc import xbmc
import xbmcaddon import xbmcaddon
import xbmcgui import xbmcgui
from . import path_ops from . import path_ops, variables as v, state
from . import variables as v
from . import state
############################################################################### ###############################################################################
@ -52,6 +51,14 @@ REGEX_PLEX_ID_FROM_URL = re.compile(r'''metadata%2F(\d+)''')
# Main methods # Main methods
def garbageCollect():
gc.collect(2)
def setGlobalProperty(key, val):
xbmcgui.Window(10000).setProperty('script.plex.{0}'.format(key), val)
def reboot_kodi(message=None): def reboot_kodi(message=None):
""" """
Displays an OK prompt with 'Kodi will now restart to apply the changes' Displays an OK prompt with 'Kodi will now restart to apply the changes'
@ -124,6 +131,14 @@ def lang(stringid):
xbmc.getLocalizedString(stringid)) xbmc.getLocalizedString(stringid))
def messageDialog(heading, msg):
"""
Shows a dialog using the Plex layout
"""
from .windows import optionsdialog
optionsdialog.show(heading, msg, 'OK')
def dialog(typus, *args, **kwargs): def dialog(typus, *args, **kwargs):
""" """
Displays xbmcgui Dialog. Pass a string as typus: Displays xbmcgui Dialog. Pass a string as typus:
@ -194,6 +209,46 @@ def dialog(typus, *args, **kwargs):
return types[typus](*args, **kwargs) return types[typus](*args, **kwargs)
def ERROR(txt='', hide_tb=False, notify=False):
import sys
short = str(sys.exc_info()[1])
LOG.error('Error encountered: %s - %s', txt, short)
if hide_tb:
return short
import traceback
trace = traceback.format_exc()
LOG.error("_____________________________________________________________")
for line in trace.splitlines():
LOG.error(' ' + line)
LOG.error("_____________________________________________________________")
if notify:
dialog('notification',
heading='{plex}',
message=short,
icon='{error}')
return short
class AttributeDict(dict):
"""
Turns an etree xml response's xml.attrib into an object with attributes
"""
def __getattr__(self, attr):
return self.get(attr)
def __setattr__(self, attr, value):
self[attr] = value
def __unicode__(self):
return '<{0}:{1}:{2}>'.format(self.__class__.__name__,
self.id,
self.get('title', 'None'))
def __repr__(self):
return self.__unicode__().encode('utf8')
def millis_to_kodi_time(milliseconds): def millis_to_kodi_time(milliseconds):
""" """
Converts time in milliseconds to the time dict used by the Kodi JSON RPC: Converts time in milliseconds to the time dict used by the Kodi JSON RPC:

View file

@ -0,0 +1 @@
# Dummy file to make this directory a package.

View file

@ -0,0 +1,174 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from . import kodigui
from .. import utils, variables as v
SEPARATOR = None
class DropdownDialog(kodigui.BaseDialog):
xmlFile = 'script-plex-dropdown.xml'
path = v.ADDON_PATH
theme = 'Main'
res = '1080i'
width = 1920
height = 1080
GROUP_ID = 100
OPTIONS_LIST_ID = 250
def __init__(self, *args, **kwargs):
kodigui.BaseDialog.__init__(self, *args, **kwargs)
self.options = kwargs.get('options')
self.pos = kwargs.get('pos')
self.posIsBottom = kwargs.get('pos_is_bottom')
self.closeDirection = kwargs.get('close_direction')
self.setDropdownProp = kwargs.get('set_dropdown_prop', False)
self.withIndicator = kwargs.get('with_indicator', False)
self.suboptionCallback = kwargs.get('suboption_callback')
self.closeOnPlaybackEnded = kwargs.get('close_on_playback_ended', False)
self.header = kwargs.get('header')
self.choice = None
@property
def x(self):
return self.pos[0]
@property
def y(self):
y = self.pos[1]
if self.posIsBottom:
y -= (len(self.options) * 66) + 80
return y
def onFirstInit(self):
self.setProperty('dropdown', self.setDropdownProp and '1' or '')
self.setProperty('header', self.header)
self.optionsList = kodigui.ManagedControlList(self, self.OPTIONS_LIST_ID, 8)
self.showOptions()
height = min(66 * 14, (len(self.options) * 66)) + 80
self.getControl(100).setPosition(self.x, self.y)
shadowControl = self.getControl(110)
if self.header:
shadowControl.setHeight(height + 86)
self.getControl(111).setHeight(height + 6)
else:
shadowControl.setHeight(height)
self.setProperty('show', '1')
self.setProperty('close.direction', self.closeDirection)
if self.closeOnPlaybackEnded:
from lib import player
player.PLAYER.on('session.ended', self.playbackSessionEnded)
def onAction(self, action):
try:
pass
except:
utils.ERROR()
kodigui.BaseDialog.onAction(self, action)
def onClick(self, controlID):
if controlID == self.OPTIONS_LIST_ID:
self.setChoice()
else:
self.doClose()
def playbackSessionEnded(self, **kwargs):
self.doClose()
def setChoice(self):
mli = self.optionsList.getSelectedItem()
if not mli:
return
choice = self.options[self.optionsList.getSelectedPosition()]
if choice.get('ignore'):
return
if self.suboptionCallback:
options = self.suboptionCallback(choice)
if options:
sub = showDropdown(options, (self.x + 290, self.y + 10), close_direction='left', with_indicator=True)
if not sub:
return
choice['sub'] = sub
self.choice = choice
self.doClose()
def showOptions(self):
items = []
options = []
for o in self.options:
if o:
item = kodigui.ManagedListItem(o['display'], thumbnailImage=o.get('indicator', ''), data_source=o)
item.setProperty('with.indicator', self.withIndicator and '1' or '')
items.append(item)
options.append(o)
else:
if items:
items[-1].setProperty('separator', '1')
self.options = options
if len(items) > 1:
items[0].setProperty('first', '1')
items[-1].setProperty('last', '1')
elif items:
items[0].setProperty('only', '1')
self.optionsList.reset()
self.optionsList.addItems(items)
self.setFocusId(self.OPTIONS_LIST_ID)
class DropdownHeaderDialog(DropdownDialog):
xmlFile = 'script-plex-dropdown_header.xml'
def showDropdown(
options, pos=None,
pos_is_bottom=False,
close_direction='top',
set_dropdown_prop=True,
with_indicator=False,
suboption_callback=None,
close_on_playback_ended=False,
header=None
):
if header:
pos = pos or (660, 400)
w = DropdownHeaderDialog.open(
options=options, pos=pos,
pos_is_bottom=pos_is_bottom,
close_direction=close_direction,
set_dropdown_prop=set_dropdown_prop,
with_indicator=with_indicator,
suboption_callback=suboption_callback,
close_on_playback_ended=close_on_playback_ended,
header=header
)
else:
pos = pos or (810, 400)
w = DropdownDialog.open(
options=options, pos=pos,
pos_is_bottom=pos_is_bottom,
close_direction=close_direction,
set_dropdown_prop=set_dropdown_prop,
with_indicator=with_indicator,
suboption_callback=suboption_callback,
close_on_playback_ended=close_on_playback_ended,
header=header
)
choice = w.choice
del w
utils.garbageCollect()
return choice

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,58 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
import xbmc
from . import kodigui
from .. import utils, variables as v
class OptionsDialog(kodigui.BaseDialog):
xmlFile = 'script-plex-options_dialog.xml'
path = v.ADDON_PATH
theme = 'Main'
res = '1080i'
width = 1920
height = 1080
GROUP_ID = 100
BUTTON_IDS = (1001, 1002, 1003)
def __init__(self, *args, **kwargs):
kodigui.BaseDialog.__init__(self, *args, **kwargs)
self.header = kwargs.get('header')
self.info = kwargs.get('info')
self.button0 = kwargs.get('button0')
self.button1 = kwargs.get('button1')
self.button2 = kwargs.get('button2')
self.buttonChoice = None
def onFirstInit(self):
self.setProperty('header', self.header)
self.setProperty('info', self.info)
if self.button2:
self.setProperty('button.2', self.button2)
if self.button1:
self.setProperty('button.1', self.button1)
if self.button0:
self.setProperty('button.0', self.button0)
self.setBoolProperty('initialized', True)
xbmc.Monitor().waitForAbort(0.1)
self.setFocusId(self.BUTTON_IDS[0])
def onClick(self, controlID):
if controlID in self.BUTTON_IDS:
self.buttonChoice = self.BUTTON_IDS.index(controlID)
self.doClose()
def show(header, info, button0=None, button1=None, button2=None):
w = OptionsDialog.open(header=header, info=info, button0=button0, button1=button1, button2=button2)
choice = w.buttonChoice
del w
utils.garbageCollect()
return choice

View file

@ -0,0 +1,191 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
:module: plexkodiconnect.userselect
:synopsis: This module shows a dialog to let one choose between different Plex
(home) users
"""
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
import xbmc
import xbmcgui
from . import kodigui
from .. import backgroundthread, utils, plex_tv, variables as v
LOG = getLogger('PLEX.' + __name__)
class UserThumbTask(backgroundthread.Task):
def setup(self, users, callback):
self.users = users
self.callback = callback
return self
def run(self):
for user in self.users:
if self.isCanceled():
return
thumb, back = user.thumb, ''
self.callback(user, thumb, back)
class UserSelectWindow(kodigui.BaseWindow):
xmlFile = 'script-plex-user_select.xml'
path = v.ADDON_PATH
theme = 'Main'
res = '1080i'
width = 1920
height = 1080
USER_LIST_ID = 101
PIN_ENTRY_GROUP_ID = 400
SHUTDOWN_BUTTON_ID = 500 # Todo: DELETE
def __init__(self, *args, **kwargs):
self.task = None
self.user = None
kodigui.BaseWindow.__init__(self, *args, **kwargs)
def onFirstInit(self):
self.userList = kodigui.ManagedControlList(self, self.USER_LIST_ID, 6)
self.start()
def onAction(self, action):
try:
ID = action.getId()
if 57 < ID < 68:
if not xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format(self.PIN_ENTRY_GROUP_ID)):
item = self.userList.getSelectedItem()
if not item.dataSource.protected:
return
self.setFocusId(self.PIN_ENTRY_GROUP_ID)
self.pinEntryClicked(ID + 142)
return
elif 142 <= ID <= 149: # JumpSMS action
if not xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format(self.PIN_ENTRY_GROUP_ID)):
item = self.userList.getSelectedItem()
if not item.dataSource.protected:
return
self.setFocusId(self.PIN_ENTRY_GROUP_ID)
self.pinEntryClicked(ID + 60)
return
elif ID in (xbmcgui.ACTION_NAV_BACK, xbmcgui.ACTION_BACKSPACE):
if xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format(self.PIN_ENTRY_GROUP_ID)):
self.pinEntryClicked(211)
return
except:
utils.ERROR()
kodigui.BaseWindow.onAction(self, action)
def onClick(self, controlID):
if controlID == self.USER_LIST_ID:
item = self.userList.getSelectedItem()
if item.dataSource.protected:
self.setFocusId(self.PIN_ENTRY_GROUP_ID)
else:
self.userSelected(item)
elif 200 < controlID < 212:
self.pinEntryClicked(controlID)
def onFocus(self, controlID):
if controlID == self.USER_LIST_ID:
item = self.userList.getSelectedItem()
item.setProperty('editing.pin', '')
def userThumbCallback(self, user, thumb, back):
item = self.userList.getListItemByDataSource(user)
if item:
item.setThumbnailImage(thumb)
item.setProperty('back.image', back)
def start(self):
self.setProperty('busy', '1')
try:
users = plex_tv.plex_home_users(utils.settings('plexToken'))
items = []
for user in users:
# thumb, back = image.getImage(user.thumb, user.id)
# mli = kodigui.ManagedListItem(user.title, thumbnailImage=thumb, data_source=user)
mli = kodigui.ManagedListItem(user.title, user.title[0].upper(), data_source=user)
mli.setProperty('pin', user.title)
# mli.setProperty('back.image', back)
mli.setProperty('protected', user.protected == '1' and '1' or '')
mli.setProperty('admin', user.admin == '1' and '1' or '')
items.append(mli)
self.userList.addItems(items)
self.task = UserThumbTask().setup(users, self.userThumbCallback)
backgroundthread.BGThreader.addTask(self.task)
self.setFocusId(self.USER_LIST_ID)
self.setProperty('initialized', '1')
finally:
self.setProperty('busy', '')
def pinEntryClicked(self, controlID):
item = self.userList.getSelectedItem()
if item.getProperty('editing.pin'):
pin = item.getProperty('editing.pin')
else:
pin = ''
if len(pin) > 3:
return
if controlID < 210:
pin += str(controlID - 200)
elif controlID == 210:
pin += '0'
elif controlID == 211:
pin = pin[:-1]
if pin:
item.setProperty('pin', ' '.join(list(u"\u2022" * len(pin))))
item.setProperty('editing.pin', pin)
if len(pin) > 3:
self.userSelected(item, pin)
else:
item.setProperty('pin', item.dataSource.title)
item.setProperty('editing.pin', '')
def userSelected(self, item, pin=None):
self.user = item.dataSource
LOG.info('Home user selected: %s', self.user)
self.user.authToken = plex_tv.switch_home_user(
self.user.id,
pin,
utils.settings('plexToken'),
utils.settings('plex_machineIdentifier'))
if self.user.authToken is None:
self.user = None
item.setProperty('pin', item.dataSource.title)
item.setProperty('editing.pin', '')
# 'Error': 'Login failed with plex.tv for user'
utils.messageDialog(utils.lang(30135),
'%s %s' % (utils.lang(39229),
self.user.username))
return
self.doClose()
def finished(self):
if self.task:
self.task.cancel()
def start():
"""
Hit this function to open a dialog to choose the Plex user
Returns
=======
user : HomeUser
Or None if user switch failed or aborted by the user)
"""
w = UserSelectWindow.open()
user = w.user
del w
return user

View file

@ -26,9 +26,7 @@
<setting id="plexLogin" label="39228" type="text" default="" enable="false" /> <setting id="plexLogin" label="39228" type="text" default="" enable="false" />
<setting id="myplexlogin" label="39025" type="bool" default="true" /> <!-- Log into plex.tv on startup --> <setting id="myplexlogin" label="39025" type="bool" default="true" /> <!-- Log into plex.tv on startup -->
<setting label="39209" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect?mode=togglePlexTV)" option="close" /> <setting label="39209" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect?mode=togglePlexTV)" option="close" />
<setting id="plexhome" label="Plex home in use" type="bool" default="" visible="false" />
<setting id="plexToken" label="plexToken" type="text" default="" visible="false" /> <setting id="plexToken" label="plexToken" type="text" default="" visible="false" />
<setting id="plexHomeSize" type="number" default="1" visible="false" />
<setting type="sep" text=""/> <setting type="sep" text=""/>
<setting type="lsep" label="39008" /> <setting type="lsep" label="39008" />
<setting id="plexCompanion" label="39004" type="bool" default="true" /> <setting id="plexCompanion" label="39004" type="bool" default="true" />