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:"
|
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 ""
|
||||||
|
|
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('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))
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
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="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" />
|
||||||
|
|
Loading…
Reference in a new issue