Add Plex dialog to switch users

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

View file

@ -1119,6 +1119,11 @@ msgctxt "#39228"
msgid "Plex user:"
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 ""

View file

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

View file

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

View file

@ -164,11 +164,14 @@ class InitialSetup(object):
Returns True if successful, or False if not
"""
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

View file

@ -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

View file

@ -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,
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 = []
else:
for user in xml:
users.append(user.attrib)
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:
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 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
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,

View file

@ -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

View file

@ -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:

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -26,9 +26,7 @@
<setting id="plexLogin" label="39228" type="text" default="" enable="false" />
<setting id="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" />