Add Plex dialog to switch users
This commit is contained in:
parent
233f6065ee
commit
98e38ae9a8
14 changed files with 2007 additions and 256 deletions
|
@ -1119,6 +1119,11 @@ msgctxt "#39228"
|
|||
msgid "Plex user:"
|
||||
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"
|
||||
msgid "Running the image cache process can take some time. It will happen in the background. Are you sure you want continue?"
|
||||
msgstr ""
|
||||
|
|
302
resources/lib/backgroundthread.py
Normal file
302
resources/lib/backgroundthread.py
Normal 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()
|
|
@ -74,7 +74,6 @@ def toggle_plex_tv_sign_in():
|
|||
utils.settings('plexLogin', value="")
|
||||
utils.settings('plexToken', value="")
|
||||
utils.settings('plexid', value="")
|
||||
utils.settings('plexHomeSize', value="1")
|
||||
utils.settings('plexAvatar', value="")
|
||||
utils.settings('plex_status', value=utils.lang(39226))
|
||||
|
||||
|
|
|
@ -164,11 +164,14 @@ class InitialSetup(object):
|
|||
|
||||
Returns True if successful, or False if not
|
||||
"""
|
||||
result = plex_tv.sign_in_with_pin()
|
||||
if result:
|
||||
self.plex_login = result['username']
|
||||
self.plex_token = result['token']
|
||||
self.plexid = result['plexid']
|
||||
try:
|
||||
user = plex_tv.sign_in_with_pin()
|
||||
except:
|
||||
utils.ERROR()
|
||||
if user:
|
||||
self.plex_login = user.username
|
||||
self.plex_token = user.authToken
|
||||
self.plexid = user.id
|
||||
return True
|
||||
return False
|
||||
|
||||
|
@ -209,10 +212,7 @@ class InitialSetup(object):
|
|||
else:
|
||||
utils.settings('plexLogin', value=self.plex_login)
|
||||
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('plexHomeSize',
|
||||
value=xml.attrib.get('homeSize', '1'))
|
||||
LOG.info('Updated Plex info from plex.tv')
|
||||
return answer
|
||||
|
||||
|
|
|
@ -90,27 +90,22 @@ def GetPlexLoginFromSettings():
|
|||
Returns a dict:
|
||||
'plexLogin': utils.settings('plexLogin'),
|
||||
'plexToken': utils.settings('plexToken'),
|
||||
'plexhome': utils.settings('plexhome'),
|
||||
'plexid': utils.settings('plexid'),
|
||||
'myplexlogin': utils.settings('myplexlogin'),
|
||||
'plexAvatar': utils.settings('plexAvatar'),
|
||||
'plexHomeSize': utils.settings('plexHomeSize')
|
||||
|
||||
Returns strings or unicode
|
||||
|
||||
Returns empty strings '' for a setting if not found.
|
||||
|
||||
myplexlogin is 'true' if user opted to log into plex.tv (the default)
|
||||
plexhome is 'true' if plex home is used (the default)
|
||||
"""
|
||||
return {
|
||||
'plexLogin': utils.settings('plexLogin'),
|
||||
'plexToken': utils.settings('plexToken'),
|
||||
'plexhome': utils.settings('plexhome'),
|
||||
'plexid': utils.settings('plexid'),
|
||||
'myplexlogin': utils.settings('myplexlogin'),
|
||||
'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
|
||||
wrong.
|
||||
"""
|
||||
users = plex_tv.list_home_users(utils.settings('plexToken'))
|
||||
users = plex_tv.plex_home_users(utils.settings('plexToken'))
|
||||
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:
|
||||
if username in user['title']:
|
||||
url = user['thumb']
|
||||
if user.title == username:
|
||||
url = user.thumb
|
||||
LOG.debug("Avatar url for user %s is: %s", username, url)
|
||||
return url
|
||||
|
||||
|
|
|
@ -2,118 +2,41 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
from xbmc import sleep, executebuiltin
|
||||
import time
|
||||
import threading
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
|
||||
from .downloadutils import DownloadUtils as DU
|
||||
from . import utils
|
||||
from . import variables as v
|
||||
from . import state
|
||||
from . import utils, variables as v, 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
|
||||
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)
|
||||
Turns an etree xml answer into an object with attributes
|
||||
"""
|
||||
# Get list of Plex home users
|
||||
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
|
||||
}
|
||||
pass
|
||||
|
||||
|
||||
def switch_home_user(userid, pin, token, machineIdentifier):
|
||||
def homeuser_to_settings(user):
|
||||
"""
|
||||
Retrieves Plex home token for a Plex home user.
|
||||
Returns False if unsuccessful
|
||||
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):
|
||||
"""
|
||||
Retrieves Plex home token for a Plex home user. Returns None if this fails
|
||||
|
||||
Input:
|
||||
userid id of the Plex home user
|
||||
|
@ -121,40 +44,37 @@ def switch_home_user(userid, pin, token, machineIdentifier):
|
|||
token token for plex.tv
|
||||
|
||||
Output:
|
||||
{
|
||||
'username'
|
||||
'usertoken' Might be empty strings if no token found
|
||||
for the machineIdentifier that was chosen
|
||||
}
|
||||
usertoken Might be empty strings if no token found
|
||||
for the machine_identifier that was chosen
|
||||
|
||||
utils.settings('userid') and utils.settings('username') with new plex token
|
||||
"""
|
||||
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:
|
||||
url += '?pin=' + pin
|
||||
answer = DU().downloadUrl(url,
|
||||
authenticate=False,
|
||||
action_type="POST",
|
||||
headerOptions={'X-Plex-Token': token})
|
||||
url += '?pin=%s' % pin
|
||||
xml = DU().downloadUrl(url,
|
||||
authenticate=False,
|
||||
action_type="POST",
|
||||
headerOptions={'X-Plex-Token': token})
|
||||
try:
|
||||
answer.attrib
|
||||
xml.attrib
|
||||
except AttributeError:
|
||||
LOG.error('Error: plex.tv switch HomeUser change failed')
|
||||
return False
|
||||
LOG.error('Switch HomeUser change failed')
|
||||
return
|
||||
|
||||
username = answer.attrib.get('title', '')
|
||||
token = answer.attrib.get('authenticationToken', '')
|
||||
username = xml.get('title', '')
|
||||
token = xml.get('authenticationToken', '')
|
||||
|
||||
# Write to settings file
|
||||
utils.settings('username', username)
|
||||
utils.settings('accessToken', token)
|
||||
utils.settings('userid', answer.attrib.get('id', ''))
|
||||
utils.settings('userid', xml.get('id', ''))
|
||||
utils.settings('plex_restricteduser',
|
||||
'true' if answer.attrib.get('restricted', '0') == '1'
|
||||
'true' if xml.get('restricted', '0') == '1'
|
||||
else 'false')
|
||||
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
|
||||
url = 'https://plex.tv/api/resources?includeHttps=1'
|
||||
|
@ -169,133 +89,186 @@ def switch_home_user(userid, pin, token, machineIdentifier):
|
|||
xml = []
|
||||
|
||||
found = 0
|
||||
LOG.debug('Our machineIdentifier is %s', machineIdentifier)
|
||||
LOG.debug('Our machine_identifier is %s', machine_identifier)
|
||||
for device in xml:
|
||||
identifier = device.attrib.get('clientIdentifier')
|
||||
LOG.debug('Found a Plex machineIdentifier: %s', identifier)
|
||||
if identifier == machineIdentifier:
|
||||
LOG.debug('Found the Plex clientIdentifier: %s', identifier)
|
||||
if identifier == machine_identifier:
|
||||
found += 1
|
||||
token = device.attrib.get('accessToken')
|
||||
|
||||
result = {
|
||||
'username': username,
|
||||
}
|
||||
if found == 0:
|
||||
LOG.info('No tokens found for your server! Using empty string')
|
||||
result['usertoken'] = ''
|
||||
else:
|
||||
result['usertoken'] = token
|
||||
token = ''
|
||||
LOG.info('Plex.tv switch HomeUser change successfull for user %s',
|
||||
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.
|
||||
|
||||
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
|
||||
Returns a list of HomeUser elements from plex.tv
|
||||
"""
|
||||
xml = DU().downloadUrl('https://plex.tv/api/home/users/',
|
||||
authenticate=False,
|
||||
headerOptions={'X-Plex-Token': token})
|
||||
users = []
|
||||
try:
|
||||
xml.attrib
|
||||
except AttributeError:
|
||||
LOG.error('Download of Plex home users failed.')
|
||||
return False
|
||||
users = []
|
||||
for user in xml:
|
||||
users.append(user.attrib)
|
||||
else:
|
||||
for user in xml:
|
||||
users.append(HomeUser(user.attrib))
|
||||
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():
|
||||
"""
|
||||
Prompts user to sign in by visiting https://plex.tv/pin
|
||||
|
||||
Writes to Kodi settings file. Also returns:
|
||||
{
|
||||
'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.
|
||||
Writes to Kodi settings file and returns the HomeUser or None
|
||||
"""
|
||||
code, identifier = get_pin()
|
||||
if not code:
|
||||
# Problems trying to contact plex.tv. Try again later
|
||||
utils.dialog('ok', heading='{plex}', line1=utils.lang(39303))
|
||||
return False
|
||||
# Go to https://plex.tv/pin and enter the code:
|
||||
# Or press No to cancel the sign in.
|
||||
answer = utils.dialog('yesno',
|
||||
heading='{plex}',
|
||||
line1='%s%s' % (utils.lang(39304), "\n\n"),
|
||||
line2='%s%s' % (code, "\n\n"),
|
||||
line3=utils.lang(39311))
|
||||
if not answer:
|
||||
return False
|
||||
count = 0
|
||||
# Wait for approx 30 seconds (since the PIN is not visible anymore :-))
|
||||
while count < 30:
|
||||
xml = check_pin(identifier)
|
||||
if xml is not False:
|
||||
break
|
||||
# Wait for 1 seconds
|
||||
sleep(1000)
|
||||
count += 1
|
||||
if xml is False:
|
||||
# Could not sign in to plex.tv Try again later
|
||||
utils.dialog('ok', heading='{plex}', line1=utils.lang(39305))
|
||||
return False
|
||||
# Parse xml
|
||||
userid = xml.attrib.get('id')
|
||||
home = xml.get('home', '0')
|
||||
if home == '1':
|
||||
home = 'true'
|
||||
else:
|
||||
home = 'false'
|
||||
username = xml.get('username', '')
|
||||
avatar = xml.get('thumb', '')
|
||||
token = xml.findtext('authentication-token')
|
||||
home_size = xml.get('homeSize', '1')
|
||||
result = {
|
||||
'plexhome': home,
|
||||
'username': username,
|
||||
'avatar': avatar,
|
||||
'token': token,
|
||||
'plexid': userid,
|
||||
'homesize': home_size
|
||||
}
|
||||
utils.settings('plexLogin', username)
|
||||
utils.settings('plexToken', token)
|
||||
utils.settings('plexhome', home)
|
||||
utils.settings('plexid', userid)
|
||||
utils.settings('plexAvatar', avatar)
|
||||
utils.settings('plexHomeSize', home_size)
|
||||
# Let Kodi log into plex.tv on startup from now on
|
||||
utils.settings('myplexlogin', 'true')
|
||||
utils.settings('plex_status', value=utils.lang(39227))
|
||||
return result
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
except AttributeError:
|
||||
LOG.error("Could not find token in plex.tv answer")
|
||||
return False
|
||||
return
|
||||
if not temp_token:
|
||||
return False
|
||||
return
|
||||
# Use temp token to get the final plex credentials
|
||||
xml = DU().downloadUrl('https://plex.tv/users/account',
|
||||
authenticate=False,
|
||||
|
|
|
@ -6,10 +6,10 @@ from threading import Thread
|
|||
|
||||
from xbmc import sleep, executebuiltin
|
||||
|
||||
from .windows import userselect
|
||||
from .downloadutils import DownloadUtils as DU
|
||||
from . import utils
|
||||
from . import path_ops
|
||||
from . import plex_tv
|
||||
from . import plex_functions as PF
|
||||
from . import variables as v
|
||||
from . import state
|
||||
|
@ -237,22 +237,22 @@ class UserClient(Thread):
|
|||
plextoken = utils.settings('plexToken')
|
||||
if plextoken:
|
||||
LOG.info("Trying to connect to plex.tv to get a user list")
|
||||
userInfo = plex_tv.choose_home_user(plextoken)
|
||||
if userInfo is False:
|
||||
user = userselect.start()
|
||||
if not user:
|
||||
# FAILURE: Something went wrong, try again
|
||||
self.auth = True
|
||||
self.retry += 1
|
||||
return False
|
||||
username = userInfo['username']
|
||||
userId = userInfo['userid']
|
||||
usertoken = userInfo['token']
|
||||
username = user.title
|
||||
user_id = user.id
|
||||
usertoken = user.authToken
|
||||
else:
|
||||
LOG.info("Trying to authenticate without a token")
|
||||
username = ''
|
||||
userId = ''
|
||||
user_id = ''
|
||||
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
|
||||
return True
|
||||
# Something went wrong, try again
|
||||
|
|
|
@ -18,13 +18,12 @@ from functools import wraps, partial
|
|||
from urllib import quote_plus
|
||||
import hashlib
|
||||
import re
|
||||
import gc
|
||||
import xbmc
|
||||
import xbmcaddon
|
||||
import xbmcgui
|
||||
|
||||
from . import path_ops
|
||||
from . import variables as v
|
||||
from . import state
|
||||
from . import path_ops, variables as v, state
|
||||
|
||||
###############################################################################
|
||||
|
||||
|
@ -52,6 +51,14 @@ REGEX_PLEX_ID_FROM_URL = re.compile(r'''metadata%2F(\d+)''')
|
|||
# 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):
|
||||
"""
|
||||
Displays an OK prompt with 'Kodi will now restart to apply the changes'
|
||||
|
@ -124,6 +131,14 @@ def lang(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):
|
||||
"""
|
||||
Displays xbmcgui Dialog. Pass a string as typus:
|
||||
|
@ -194,6 +209,46 @@ def dialog(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):
|
||||
"""
|
||||
Converts time in milliseconds to the time dict used by the Kodi JSON RPC:
|
||||
|
|
1
resources/lib/windows/__init__.py
Normal file
1
resources/lib/windows/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
# Dummy file to make this directory a package.
|
174
resources/lib/windows/dropdown.py
Normal file
174
resources/lib/windows/dropdown.py
Normal 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
|
1004
resources/lib/windows/kodigui.py
Normal file
1004
resources/lib/windows/kodigui.py
Normal file
File diff suppressed because it is too large
Load diff
58
resources/lib/windows/optionsdialog.py
Normal file
58
resources/lib/windows/optionsdialog.py
Normal 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
|
191
resources/lib/windows/userselect.py
Normal file
191
resources/lib/windows/userselect.py
Normal 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
|
|
@ -26,9 +26,7 @@
|
|||
<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 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="plexHomeSize" type="number" default="1" visible="false" />
|
||||
<setting type="sep" text=""/>
|
||||
<setting type="lsep" label="39008" />
|
||||
<setting id="plexCompanion" label="39004" type="bool" default="true" />
|
||||
|
|
Loading…
Reference in a new issue