Compare commits

...

24 commits

Author SHA1 Message Date
tomkat83
47c1b51014 Merge branch 'master' into develop 2017-08-02 20:13:59 +02:00
tomkat83
583512eb89 Merge branch 'hotfixes' into develop 2017-08-02 20:01:29 +02:00
tomkat83
3310e0ad25 Dedicated server settings 2017-08-02 17:22:53 +02:00
tomkat83
0628b40b8f Code cleanup 2017-07-27 19:58:11 +02:00
tomkat83
04e1fb4ba8 Increase code resiliance 2017-07-27 19:55:41 +02:00
tomkat83
8a4b1c00f3 Increase code resiliance 2017-07-27 19:48:20 +02:00
tomkat83
0f84836533 Improve networking exceptions 2017-07-27 19:16:14 +02:00
tomkat83
b698d2e0e1 Show local PMS as local 2017-07-27 17:48:14 +02:00
tomkat83
af0ac26045 Move media path to variables 2017-07-27 17:43:51 +02:00
tomkat83
c1098f22a4 Dialog: manual PMS entry, part 3 2017-07-27 17:40:18 +02:00
tomkat83
76ca66b38b Merge branch 'master' into develop 2017-07-25 21:53:40 +02:00
tomkat83
87775072c6 Dialog: manual PMS entry, part 2 2017-07-25 18:17:49 +02:00
tomkat83
ac3016c84d Fix bug importing datetime.datetime 2017-07-25 18:16:49 +02:00
tomkat83
d1346b2cd6 Dialog: manual PMS entry 2017-07-16 16:57:57 +02:00
tomkat83
7a9e0611ed Connection manager, part 2 2017-07-16 15:22:08 +02:00
tomkat83
962ce6da1e Switch to state variables 2017-07-02 19:00:47 +02:00
tomkat83
73ce4eeacb Funnel commands to main Python instance 2017-07-02 18:57:17 +02:00
tomkat83
10942558cc Initial dialog for manually entering server 2017-07-02 18:23:58 +02:00
tomkat83
78f6ad7da8 Plex connect server dialog 2017-07-02 18:04:22 +02:00
tomkat83
92a5eac7be Unify XML paths 2017-07-02 15:03:53 +02:00
tomkat83
61b9bbee8f Fix AttributeError 2017-07-02 15:01:04 +02:00
tomkat83
40ba9a495f Some fixes 2017-07-02 14:42:52 +02:00
tomkat83
051006a1ef Merge branch 'master' into develop 2017-07-02 14:18:55 +02:00
tomkat83
32ace844aa Connection manager, part 1 2017-07-01 12:32:23 +02:00
26 changed files with 3027 additions and 2185 deletions

View file

@ -137,7 +137,7 @@ class Main():
window('plex_runLibScan', value='del_textures') window('plex_runLibScan', value='del_textures')
elif mode == 'chooseServer': elif mode == 'chooseServer':
entrypoint.chooseServer() self.__exec('function=choose_server')
elif mode == 'refreshplaylist': elif mode == 'refreshplaylist':
log.info('Requesting playlist/nodes refresh') log.info('Requesting playlist/nodes refresh')
@ -172,8 +172,7 @@ class Main():
# Put the request into the 'queue' # Put the request into the 'queue'
while window('plex_command'): while window('plex_command'):
sleep(50) sleep(50)
window('plex_command', window('plex_command', value='play_%s' % argv[2])
value='play_%s' % argv[2])
# Wait for the result # Wait for the result
while not pickl_window('plex_result'): while not pickl_window('plex_result'):
sleep(50) sleep(50)
@ -190,6 +189,17 @@ class Main():
listitem = convert_PKC_to_listitem(result.listitem) listitem = convert_PKC_to_listitem(result.listitem)
setResolvedUrl(HANDLE, True, listitem) setResolvedUrl(HANDLE, True, listitem)
@staticmethod
def __exec(command):
"""
Used to funnel commands to the main PKC python instance (like play())
"""
# Put the request into the 'queue'
while window('plex_command'):
sleep(50)
window('plex_command', value='exec_%s' % command)
# No need to wait for the result
def deviceid(self): def deviceid(self):
deviceId_old = window('plex_client_Id') deviceId_old = window('plex_client_Id')
from clientinfo import getDeviceId from clientinfo import getDeviceId

View file

@ -83,10 +83,35 @@ msgctxt "#30017"
msgid "Unauthorized for PMS" msgid "Unauthorized for PMS"
msgstr "" msgstr ""
# Title of dialog for manual PMS connection
msgctxt "#30018"
msgid "Manually connect to PMS"
msgstr ""
# Button text for connect dialog
msgctxt "#30019"
msgid "PMS IP address or host name"
msgstr ""
# Button text for connect dialog
msgctxt "#30020"
msgid "Connect"
msgstr ""
# Button text for connect dialog
msgctxt "#30021"
msgid "Please fill in both server and port"
msgstr ""
msgctxt "#30022" msgctxt "#30022"
msgid "Advanced" msgid "Advanced"
msgstr "" msgstr ""
# Used to show that PKC is currently trying to connect to a PMS
msgctxt "#30023"
msgid "Connecting to"
msgstr ""
msgctxt "#30024" msgctxt "#30024"
msgid "Username" msgid "Username"
msgstr "" msgstr ""
@ -96,7 +121,7 @@ msgid "Display message if PMS goes offline"
msgstr "" msgstr ""
msgctxt "#30030" msgctxt "#30030"
msgid "Port Number" msgid "Port number"
msgstr "" msgstr ""
msgctxt "#30031" msgctxt "#30031"
@ -977,7 +1002,7 @@ msgstr ""
# add-on settings # add-on settings
msgctxt "#30500" msgctxt "#30500"
msgid "Verify Host SSL Certificate (more secure)" msgid "Verify SSL Certificate"
msgstr "" msgstr ""
msgctxt "#30501" msgctxt "#30501"
@ -1160,6 +1185,28 @@ msgctxt "#30545"
msgid "Force transcode pictures" msgid "Force transcode pictures"
msgstr "" msgstr ""
# Server selection dialog: button text to sign in or sign out of plex.tv
msgctxt "#30600"
msgid "Sign-in to plex.tv"
msgstr ""
# Server selection dialog: button text to add server manually
msgctxt "#30601"
msgid "Manually add PMS"
msgstr ""
# Button text, e.g. to cancel a dialog
msgctxt "#30602"
msgid "Cancel"
msgstr ""
# Server selection dialog: button text to select the main PMS
msgctxt "#30607"
msgid "Select main PMS"
msgstr ""
# service add-on # service add-on
msgctxt "#33000" msgctxt "#33000"

File diff suppressed because it is too large Load diff

View file

@ -3,11 +3,12 @@ from logging import getLogger
from urllib import urlencode from urllib import urlencode
from ast import literal_eval from ast import literal_eval
from urlparse import urlparse, parse_qsl from urlparse import urlparse, parse_qsl
from urllib import quote_plus
import re import re
from copy import deepcopy from copy import deepcopy
import downloadutils from downloadutils import DownloadUtils
from utils import settings from utils import settings, tryEncode
from variables import PLEX_TO_KODI_TIMEFACTOR from variables import PLEX_TO_KODI_TIMEFACTOR
############################################################################### ###############################################################################
@ -99,10 +100,60 @@ def SelectStreams(url, args):
Does a PUT request to tell the PMS what audio and subtitle streams we have Does a PUT request to tell the PMS what audio and subtitle streams we have
chosen. chosen.
""" """
downloadutils.DownloadUtils().downloadUrl( DownloadUtils().downloadUrl(
url + '?' + urlencode(args), action_type='PUT') url + '?' + urlencode(args), action_type='PUT')
def check_connection(url, token=None, verifySSL=None):
"""
Checks connection to a Plex server, available at url. Can also be used
to check for connection with plex.tv.
Override SSL to skip the check by setting verifySSL=False
if 'None', SSL will be checked (standard requests setting)
if 'True', SSL settings from file settings are used (False/True)
Input:
url URL to Plex server (e.g. https://192.168.1.1:32400)
token appropriate token to access server. If None is passed,
the current token is used
Output:
False if server could not be reached or timeout occured
200 if connection was successfull
int or other HTML status codes as received from the server
"""
headerOptions = {'X-Plex-Token': token} if token is not None else None
if verifySSL is True:
verifySSL = None if settings('sslverify') == 'true' \
else False
if 'plex.tv' in url:
url = 'https://plex.tv/api/home/users'
else:
url = url + '/library/onDeck'
log.debug("Checking connection to server %s with verifySSL=%s"
% (url, verifySSL))
answer = DownloadUtils().downloadUrl(url,
authenticate=False,
headerOptions=headerOptions,
verifySSL=verifySSL)
if answer is None:
log.debug("Could not connect to %s" % url)
return False
try:
# xml received?
answer.attrib
except:
if answer is True:
# Maybe no xml but connection was successful nevertheless
answer = 200
else:
# Success - we downloaded an xml!
answer = 200
# We could connect but maybe were not authenticated. No worries
log.debug("Checking connection successfull. Answer: %s" % answer)
return answer
def GetPlexMetadata(key): def GetPlexMetadata(key):
""" """
Returns raw API metadata for key as an etree XML. Returns raw API metadata for key as an etree XML.
@ -129,7 +180,7 @@ def GetPlexMetadata(key):
# 'includeConcerts': 1 # 'includeConcerts': 1
} }
url = url + '?' + urlencode(arguments) url = url + '?' + urlencode(arguments)
xml = downloadutils.DownloadUtils().downloadUrl(url) xml = DownloadUtils().downloadUrl(url)
if xml == 401: if xml == 401:
# Either unauthorized (taken care of by doUtils) or PMS under strain # Either unauthorized (taken care of by doUtils) or PMS under strain
return 401 return 401
@ -186,8 +237,7 @@ def DownloadChunks(url):
'X-Plex-Container-Size': CONTAINERSIZE, 'X-Plex-Container-Size': CONTAINERSIZE,
'X-Plex-Container-Start': pos 'X-Plex-Container-Start': pos
} }
xmlpart = downloadutils.DownloadUtils().downloadUrl( xmlpart = DownloadUtils().downloadUrl(url + urlencode(args))
url + urlencode(args))
# If something went wrong - skip in the hope that it works next time # If something went wrong - skip in the hope that it works next time
try: try:
xmlpart.attrib xmlpart.attrib
@ -262,8 +312,7 @@ def get_plex_sections():
""" """
Returns all Plex sections (libraries) of the PMS as an etree xml Returns all Plex sections (libraries) of the PMS as an etree xml
""" """
return downloadutils.DownloadUtils().downloadUrl( return DownloadUtils().downloadUrl('{server}/library/sections')
'{server}/library/sections')
def init_plex_playqueue(itemid, librarySectionUUID, mediatype='movie', def init_plex_playqueue(itemid, librarySectionUUID, mediatype='movie',
@ -282,7 +331,7 @@ def init_plex_playqueue(itemid, librarySectionUUID, mediatype='movie',
} }
if trailers is True: if trailers is True:
args['extrasPrefixCount'] = settings('trailerNumber') args['extrasPrefixCount'] = settings('trailerNumber')
xml = downloadutils.DownloadUtils().downloadUrl( xml = DownloadUtils().downloadUrl(
url + '?' + urlencode(args), action_type="POST") url + '?' + urlencode(args), action_type="POST")
try: try:
xml[0].tag xml[0].tag
@ -313,7 +362,7 @@ def PMSHttpsEnabled(url):
Prefers HTTPS over HTTP Prefers HTTPS over HTTP
""" """
doUtils = downloadutils.DownloadUtils().downloadUrl doUtils = DownloadUtils().downloadUrl
res = doUtils('https://%s/identity' % url, res = doUtils('https://%s/identity' % url,
authenticate=False, authenticate=False,
verifySSL=False) verifySSL=False)
@ -343,7 +392,7 @@ def GetMachineIdentifier(url):
Returns None if something went wrong Returns None if something went wrong
""" """
xml = downloadutils.DownloadUtils().downloadUrl('%s/identity' % url, xml = DownloadUtils().downloadUrl('%s/identity' % url,
authenticate=False, authenticate=False,
verifySSL=False, verifySSL=False,
timeout=10) timeout=10)
@ -373,7 +422,7 @@ def GetPMSStatus(token):
or an empty dict. or an empty dict.
""" """
answer = {} answer = {}
xml = downloadutils.DownloadUtils().downloadUrl( xml = DownloadUtils().downloadUrl(
'{server}/status/sessions', '{server}/status/sessions',
headerOptions={'X-Plex-Token': token}) headerOptions={'X-Plex-Token': token})
try: try:
@ -413,7 +462,7 @@ def scrobble(ratingKey, state):
url = "{server}/:/unscrobble?" + urlencode(args) url = "{server}/:/unscrobble?" + urlencode(args)
else: else:
return return
downloadutils.DownloadUtils().downloadUrl(url) DownloadUtils().downloadUrl(url)
log.info("Toggled watched state for Plex item %s" % ratingKey) log.info("Toggled watched state for Plex item %s" % ratingKey)
@ -424,7 +473,7 @@ def delete_item_from_pms(plexid):
Returns True if successful, False otherwise Returns True if successful, False otherwise
""" """
if downloadutils.DownloadUtils().downloadUrl( if DownloadUtils().downloadUrl(
'{server}/library/metadata/%s' % plexid, '{server}/library/metadata/%s' % plexid,
action_type="DELETE") is True: action_type="DELETE") is True:
log.info('Successfully deleted Plex id %s from the PMS' % plexid) log.info('Successfully deleted Plex id %s from the PMS' % plexid)
@ -434,14 +483,53 @@ def delete_item_from_pms(plexid):
return False return False
def get_PMS_settings(url, token): def get_pms_settings(url, token):
""" """
Retrieve the PMS' settings via <url>/:/ Retrieve the PMS' settings via <url>/:/
Call with url: scheme://ip:port Call with url: scheme://ip:port
""" """
return downloadutils.DownloadUtils().downloadUrl( return DownloadUtils().downloadUrl(
'%s/:/prefs' % url, '%s/:/prefs' % url,
authenticate=False, authenticate=False,
verifySSL=False, verifySSL=False,
headerOptions={'X-Plex-Token': token} if token else None) headerOptions={'X-Plex-Token': token} if token else None)
def get_transcode_image_path(self, key, AuthToken, path, width, height):
"""
Transcode Image support
parameters:
key
AuthToken
path - source path of current XML: path[srcXML]
width
height
result:
final path to image file
"""
# external address - can we get a transcoding request for external images?
if key.startswith('http://') or key.startswith('https://'):
path = key
elif key.startswith('/'): # internal full path.
path = 'http://127.0.0.1:32400' + key
else: # internal path, add-on
path = 'http://127.0.0.1:32400' + path + '/' + key
path = tryEncode(path)
# This is bogus (note the extra path component) but ATV is stupid when it
# comes to caching images, it doesn't use querystrings. Fortunately PMS is
# lenient...
transcodePath = '/photo/:/transcode/' + \
str(width) + 'x' + str(height) + '/' + quote_plus(path)
args = dict()
args['width'] = width
args['height'] = height
args['url'] = path
if not AuthToken == '':
args['X-Plex-Token'] = AuthToken
return transcodePath + '?' + urlencode(args)

View file

@ -3,11 +3,13 @@
import logging import logging
from threading import Thread from threading import Thread
from Queue import Queue from Queue import Queue
from urlparse import parse_qsl
from xbmc import sleep from xbmc import sleep
from utils import window, thread_methods from utils import window, thread_methods
import state import state
import entrypoint
############################################################################### ###############################################################################
log = logging.getLogger("PLEX."+__name__) log = logging.getLogger("PLEX."+__name__)
@ -32,6 +34,30 @@ class Monitor_Window(Thread):
self.playback_queue = Queue() self.playback_queue = Queue()
Thread.__init__(self) Thread.__init__(self)
@staticmethod
def __execute(value):
"""
Kick off with new threads. Pass in a string with the information url-
encoded:
function=<function-name in entrypoint.py>
params=<function parameters> (optional)
"""
values = dict(parse_qsl(value))
function = values.get('function')
params = values.get('params')
log.debug('Execution called for function %s with parameters %s'
% (function, params))
function = getattr(entrypoint, function)
try:
if params is not None:
function(params)
else:
function()
except:
log.error('Failed to execute function %s with params %s'
% (function, params))
raise
def run(self): def run(self):
thread_stopped = self.thread_stopped thread_stopped = self.thread_stopped
queue = self.playback_queue queue = self.playback_queue
@ -42,7 +68,9 @@ class Monitor_Window(Thread):
window('plex_command', clear=True) window('plex_command', clear=True)
if value.startswith('play_'): if value.startswith('play_'):
queue.put(value) queue.put(value)
elif value.startswith('exec_'):
t = Thread(target=self.__execute, args=(value[5:], ))
t.start()
elif value == 'SUSPEND_LIBRARY_THREAD-True': elif value == 'SUSPEND_LIBRARY_THREAD-True':
state.SUSPEND_LIBRARY_THREAD = True state.SUSPEND_LIBRARY_THREAD = True
elif value == 'SUSPEND_LIBRARY_THREAD-False': elif value == 'SUSPEND_LIBRARY_THREAD-False':

View file

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

View file

@ -0,0 +1,831 @@
# -*- coding: utf-8 -*-
###############################################################################
from logging import getLogger
from hashlib import md5
import requests
from struct import pack
import socket
import time
from datetime import datetime
import xml.etree.ElementTree as etree
from Queue import Queue
from threading import Thread
from xbmc import sleep
import credentials as cred
from utils import tryDecode
from PlexFunctions import PMSHttpsEnabled
###############################################################################
# Disable annoying requests warnings
import requests.packages.urllib3
requests.packages.urllib3.disable_warnings()
log = getLogger("PLEX."+__name__)
###############################################################################
CONNECTIONSTATE = {
'Unavailable': 0,
'ServerSelection': 1,
'ServerSignIn': 2,
'SignedIn': 3,
'ConnectSignIn': 4,
'ServerUpdateNeeded': 5
}
CONNECTIONMODE = {
'Local': 0,
'Remote': 1,
'Manual': 2
}
# multicast to PMS
IP_PLEXGDM = '239.0.0.250'
PORT_PLEXGDM = 32414
MSG_PLEXGDM = 'M-SEARCH * HTTP/1.0'
###############################################################################
def getServerAddress(server, mode):
modes = {
CONNECTIONMODE['Local']: server.get('LocalAddress'),
CONNECTIONMODE['Remote']: server.get('RemoteAddress'),
CONNECTIONMODE['Manual']: server.get('ManualAddress')
}
return (modes.get(mode) or
server.get('ManualAddress',
server.get('LocalAddress',
server.get('RemoteAddress'))))
class ConnectionManager(object):
default_timeout = 30
apiClients = []
minServerVersion = "1.7.0.0"
connectUser = None
# Token for plex.tv
plexToken = None
def __init__(self, appName, appVersion, deviceName, deviceId,
capabilities=None, devicePixelRatio=None):
log.debug("Instantiating")
self.credentialProvider = cred.Credentials()
self.appName = appName
self.appVersion = appVersion
self.deviceName = deviceName
self.deviceId = deviceId
self.capabilities = capabilities
self.devicePixelRatio = devicePixelRatio
def setFilePath(self, path):
# Set where to save persistant data
self.credentialProvider.setPath(path)
def _getAppVersion(self):
return self.appVersion
def _getCapabilities(self):
return self.capabilities
def _getDeviceId(self):
return self.deviceId
def _connectUserId(self):
return self.credentialProvider.getCredentials().get('ConnectUserId')
def _connectToken(self):
return self.credentialProvider.getCredentials().get('ConnectAccessToken')
def getServerInfo(self, id_):
servers = self.credentialProvider.getCredentials()['Servers']
for s in servers:
if s['Id'] == id_:
return s
def _getLastUsedServer(self):
servers = self.credentialProvider.getCredentials()['Servers']
if not len(servers):
return
try:
servers.sort(key=lambda x: datetime.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ"), reverse=True)
except TypeError:
servers.sort(key=lambda x: datetime(*(time.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ")[0:6])), reverse=True)
return servers[0]
def _mergeServers(self, list1, list2):
for i in range(0, len(list2), 1):
try:
self.credentialProvider.addOrUpdateServer(list1, list2[i])
except KeyError:
continue
return list1
def _connectUser(self):
return self.connectUser
def _resolveFailure(self):
return {
'State': CONNECTIONSTATE['Unavailable'],
'ConnectUser': self._connectUser()
}
def _getMinServerVersion(self, val=None):
if val is not None:
self.minServerVersion = val
return self.minServerVersion
def _updateServerInfo(self, server, systemInfo):
if server is None or systemInfo is None:
return
server['Id'] = systemInfo.attrib['machineIdentifier']
if systemInfo.get('LocalAddress'):
server['LocalAddress'] = systemInfo['LocalAddress']
if systemInfo.get('WanAddress'):
server['RemoteAddress'] = systemInfo['WanAddress']
if systemInfo.get('MacAddress'):
server['WakeOnLanInfos'] = [{'MacAddress': systemInfo['MacAddress']}]
def _getHeaders(self, request):
headers = request.setdefault('headers', {})
headers['Accept'] = '*/*'
headers['Content-type'] = request.get(
'contentType',
"application/x-www-form-urlencoded")
def requestUrl(self, request):
"""
request: dict with the following (optional) keys:
type: GET, POST, ... (mandatory)
url: (mandatory)
timeout
verify: set to False to disable SSL certificate check
...and all the other requests settings
"""
self._getHeaders(request)
request['timeout'] = request.get('timeout') or self.default_timeout
action = request['type']
request.pop('type', None)
log.debug("Requesting %s" % request)
try:
r = self._requests(action, **request)
log.info("ConnectionManager response status: %s" % r.status_code)
r.raise_for_status()
except requests.RequestException as e:
# Elaborate on exceptions?
log.error(e)
raise
else:
try:
return etree.fromstring(r.content)
except etree.ParseError:
# Read response to release connection
log.error('Could not parse PMS response: %s' % r.content)
raise requests.RequestException
def _requests(self, action, **kwargs):
if action == "GET":
r = requests.get(**kwargs)
elif action == "POST":
r = requests.post(**kwargs)
return r
def getEmbyServerUrl(self, baseUrl, handler):
return "%s/emby/%s" % (baseUrl, handler)
def getConnectUrl(self, handler):
return "https://connect.emby.media/service/%s" % handler
@staticmethod
def _findServers(foundServers):
servers = []
for server in foundServers:
if '200 OK' not in server['data']:
continue
ip = server['from'][0]
info = {'LastCONNECTIONMODE': CONNECTIONMODE['Local']}
for line in server['data'].split('\n'):
if line.startswith('Name:'):
info['Name'] = tryDecode(line.split(':')[1].strip())
elif line.startswith('Port:'):
info['Port'] = line.split(':')[1].strip()
elif line.startswith('Resource-Identifier:'):
info['Id'] = line.split(':')[1].strip()
elif line.startswith('Updated-At:'):
pass
elif line.startswith('Version:'):
pass
# Need to check whether we need HTTPS or only HTTP
https = PMSHttpsEnabled('%s:%s' % (ip, info['Port']))
if https is None:
# Error contacting url. Skip for now
continue
elif https is True:
info['LocalAddress'] = 'https://%s:%s' % (ip, info['Port'])
else:
info['LocalAddress'] = 'http://%s:%s' % (ip, info['Port'])
servers.append(info)
return servers
def _serverDiscovery(self):
"""
PlexGDM
"""
servers = []
# setup socket for discovery -> multicast message
try:
GDM = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
GDM.settimeout(2.0)
# Set the time-to-live for messages to 2 for local network
ttl = pack('b', 2)
GDM.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
except (socket.error, socket.herror, socket.gaierror):
log.error('Socket error, abort PlexGDM')
return servers
try:
# Send data to the multicast group
GDM.sendto(MSG_PLEXGDM, (IP_PLEXGDM, PORT_PLEXGDM))
# Look for responses from all recipients
while True:
try:
data, server = GDM.recvfrom(1024)
servers.append({'from': server, 'data': data})
except socket.timeout:
break
except:
# Probably error: (101, 'Network is unreachable')
log.error('Could not find Plex servers using PlexGDM')
import traceback
log.error("Traceback:\n%s" % traceback.format_exc())
finally:
GDM.close()
return servers
def connectToAddress(self, address, options=None):
log.debug('connectToAddress %s with options %s' % (address, options))
def _onFail():
log.error("connectToAddress %s failed with options %s" %
(address, options))
return self._resolveFailure()
try:
publicInfo = self._tryConnect(address, options=options)
except requests.RequestException:
return _onFail()
else:
server = {
'ManualAddress': address,
'LastCONNECTIONMODE': CONNECTIONMODE['Manual'],
'options': options
}
self._updateServerInfo(server, publicInfo)
server = self.connectToServer(server)
if server is False:
return _onFail()
else:
return server
def onAuthenticated(self, result, options={}):
credentials = self.credentialProvider.getCredentials()
for s in credentials['Servers']:
if s['Id'] == result['ServerId']:
server = s
break
else: # Server not found?
return
if options.get('updateDateLastAccessed') is not False:
server['DateLastAccessed'] = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')
server['UserId'] = result['User']['Id']
server['AccessToken'] = result['AccessToken']
self.credentialProvider.addOrUpdateServer(credentials['Servers'], server)
self._saveUserInfoIntoCredentials(server, result['User'])
self.credentialProvider.getCredentials(credentials)
def _tryConnect(self, url, timeout=None, options=None):
request = {
'type': 'GET',
'url': '%s/identity' % url,
'timeout': timeout
}
if options:
request.update(options)
return self.requestUrl(request)
def _addAppInfoToConnectRequest(self):
return "%s/%s" % (self.appName, self.appVersion)
def __get_PMS_servers_from_plex_tv(self):
"""
Retrieves Plex Media Servers from plex.tv/pms/resources
"""
servers = []
try:
xml = self.requestUrl({
'url': 'https://plex.tv/api/resources?includeHttps=1',
'type': 'GET',
'headers': {'X-Plex-Token': self.plexToken},
'timeout': 5.0,
'verify': True})
except requests.RequestException:
log.error('Could not get list of PMS from plex.tv')
return servers
maxAgeSeconds = 2*60*60*24
for device in xml.findall('Device'):
if 'server' not in device.attrib.get('provides'):
# No PMS - skip
continue
cons = device.find('Connection')
if cons is None:
# no valid connection - skip
continue
# check MyPlex data age - skip if >2 days
server = {'Name': device.attrib.get('name')}
infoAge = time.time() - int(device.attrib.get('lastSeenAt'))
if infoAge > maxAgeSeconds:
log.info("Server %s not seen for 2 days - skipping."
% server['Name'])
continue
server['Id'] = device.attrib['clientIdentifier']
server['ConnectServerId'] = device.attrib['clientIdentifier']
# server['AccessToken'] = device.attrib['accessToken']
server['ExchangeToken'] = device.attrib['accessToken']
# One's own Plex home?
server['UserLinkType'] = 'Guest' if device.attrib['owned'] == '0' \
else 'LinkedUser'
# Foreign PMS' user name
server['UserId'] = device.attrib.get('sourceTitle')
for con in cons:
if con.attrib['local'] == '1':
# Local LAN address; there might be several!!
server['LocalAddress'] = con.attrib['uri']
else:
server['RemoteAddress'] = con.attrib['uri']
# Additional stuff, not yet implemented
server['local'] = device.attrib.get('publicAddressMatches')
servers.append(server)
return servers
def _getConnectServers(self, credentials):
log.info("Begin getConnectServers")
servers = []
if not credentials.get('ConnectAccessToken') or not credentials.get('ConnectUserId'):
return servers
url = self.getConnectUrl("servers?userId=%s" % credentials['ConnectUserId'])
request = {
'type': "GET",
'url': url,
'headers': {
'X-Connect-UserToken': credentials['ConnectAccessToken']
}
}
for server in self.requestUrl(request):
servers.append({
'ExchangeToken': server['AccessKey'],
'ConnectServerId': server['Id'],
'Id': server['SystemId'],
'Name': server['Name'],
'RemoteAddress': server['Url'],
'LocalAddress': server['LocalAddress'],
'UserLinkType': "Guest" if server['UserType'].lower() == "guest" else "LinkedUser",
})
return servers
def getAvailableServers(self):
log.info("Begin getAvailableServers")
credentials = self.credentialProvider.getCredentials()
servers = list(credentials['Servers'])
if self.plexToken:
connectServers = self.__get_PMS_servers_from_plex_tv()
self._mergeServers(servers, connectServers)
foundServers = self._findServers(self._serverDiscovery())
self._mergeServers(servers, foundServers)
servers = self._filterServers(servers, connectServers)
try:
servers.sort(key=lambda x: datetime.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ"), reverse=True)
except TypeError:
servers.sort(key=lambda x: datetime(*(time.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ")[0:6])), reverse=True)
credentials['Servers'] = servers
self.credentialProvider.getCredentials(credentials)
return servers
def _filterServers(self, servers, connectServers):
filtered = []
for server in servers:
# It's not a connect server, so assume it's still valid
if server.get('ExchangeToken') is None:
filtered.append(server)
continue
for connectServer in connectServers:
if server['Id'] == connectServer['Id']:
filtered.append(server)
break
else:
return filtered
def _getConnectPasswordHash(self, password):
password = self._cleanConnectPassword(password)
return md5(password).hexdigest()
def _saveUserInfoIntoCredentials(self, server, user):
info = {
'Id': user['Id'],
'IsSignedInOffline': True
}
self.credentialProvider.addOrUpdateUser(server, info)
def _compareVersions(self, a, b):
"""
-1 a is smaller
1 a is larger
0 equal
"""
a = a.split('.')
b = b.split('.')
for i in range(0, max(len(a), len(b)), 1):
try:
aVal = a[i]
except IndexError:
aVal = 0
try:
bVal = b[i]
except IndexError:
bVal = 0
if aVal < bVal:
return -1
if aVal > bVal:
return 1
return 0
def connectToServer(self, server, settings=None):
# First test manual connections, then local, then remote
tests = [
CONNECTIONMODE['Manual'],
CONNECTIONMODE['Local'],
CONNECTIONMODE['Remote']
]
return self._testNextCONNECTIONMODE(tests, 0, server, settings)
def _stringEqualsIgnoreCase(self, str1, str2):
return (str1 or "").lower() == (str2 or "").lower()
def _testNextCONNECTIONMODE(self, tests, index, server, settings):
if index >= len(tests):
log.info("Tested all connection modes. Failing server connection.")
return self._resolveFailure()
mode = tests[index]
address = getServerAddress(server, mode)
skipTest = False
if mode == CONNECTIONMODE['Local']:
if self._stringEqualsIgnoreCase(address,
server.get('ManualAddress')):
# skipping LocalAddress test because it is the same as
# ManualAddress
skipTest = True
if skipTest or not address:
log.debug("skipping test for %s" % mode)
return self._testNextCONNECTIONMODE(tests,
index+1,
server,
settings)
log.debug("testing connection %s with settings %s for server %s"
% (address, settings, server.get('Name')))
try:
result = self._tryConnect(address, options=server.get('options'))
except requests.RequestException:
log.info("Connection test failed for %s with server %s"
% (address, server.get('Name')))
return self._testNextCONNECTIONMODE(tests,
index+1,
server,
settings)
else:
if self._compareVersions(self._getMinServerVersion(),
result.attrib['version']) == 1:
log.warn("Minimal PMS version requirement not met. PMS version"
" is: %s" % result.attrib['version'])
return {
'State': CONNECTIONSTATE['ServerUpdateNeeded'],
'Servers': [server]
}
else:
log.debug("calling onSuccessfulConnection with mode %s, "
"address %s, settings %s with server %s"
% (mode, address, settings, server.get('Name')))
return self._onSuccessfulConnection(server,
result,
mode,
settings)
def _onSuccessfulConnection(self, server, systemInfo, CONNECTIONMODE, options):
credentials = self.credentialProvider.getCredentials()
if credentials.get('ConnectAccessToken') and options.get('enableAutoLogin') is not False:
if self._ensureConnectUser(credentials) is not False:
if server.get('ExchangeToken'):
self._addAuthenticationInfoFromConnect(server, CONNECTIONMODE, credentials, options)
return self._afterConnectValidated(server, credentials, systemInfo, CONNECTIONMODE, True, options)
def _afterConnectValidated(self, server, credentials, systemInfo, CONNECTIONMODE, verifyLocalAuthentication, options):
if options.get('enableAutoLogin') is False:
server['UserId'] = None
server['AccessToken'] = None
elif (verifyLocalAuthentication and server.get('AccessToken') and
options.get('enableAutoLogin') is not False):
if self._validateAuthentication(server, CONNECTIONMODE, options) is not False:
return self._afterConnectValidated(server, credentials, systemInfo, CONNECTIONMODE, False, options)
return
self._updateServerInfo(server, systemInfo)
server['LastCONNECTIONMODE'] = CONNECTIONMODE
if options.get('updateDateLastAccessed') is not False:
server['DateLastAccessed'] = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')
self.credentialProvider.addOrUpdateServer(credentials['Servers'], server)
self.credentialProvider.getCredentials(credentials)
result = {
'Servers': [],
'ConnectUser': self._connectUser()
}
result['State'] = CONNECTIONSTATE['SignedIn'] if (server.get('AccessToken') and options.get('enableAutoLogin') is not False) else CONNECTIONSTATE['ServerSignIn']
result['Servers'].append(server)
# Connected
return result
def _validateAuthentication(self, server, CONNECTIONMODE, options={}):
url = getServerAddress(server, CONNECTIONMODE)
request = {
'type': "GET",
'url': self.getEmbyServerUrl(url, "System/Info"),
'ssl': options.get('ssl'),
'headers': {
'X-MediaBrowser-Token': server['AccessToken']
}
}
try:
systemInfo = self.requestUrl(request)
self._updateServerInfo(server, systemInfo)
if server.get('UserId'):
user = self.requestUrl({
'type': "GET",
'url': self.getEmbyServerUrl(url, "users/%s" % server['UserId']),
'ssl': options.get('ssl'),
'headers': {
'X-MediaBrowser-Token': server['AccessToken']
}
})
except Exception:
server['UserId'] = None
server['AccessToken'] = None
return False
def loginToConnect(self, username, password):
if not username:
raise AttributeError("username cannot be empty")
if not password:
raise AttributeError("password cannot be empty")
md5 = self._getConnectPasswordHash(password)
request = {
'type': "POST",
'url': self.getConnectUrl("user/authenticate"),
'data': {
'nameOrEmail': username,
'password': md5
},
}
try:
result = self.requestUrl(request)
except Exception as e: # Failed to login
log.error(e)
return False
else:
credentials = self.credentialProvider.getCredentials()
credentials['ConnectAccessToken'] = result['AccessToken']
credentials['ConnectUserId'] = result['User']['Id']
credentials['ConnectUser'] = result['User']['DisplayName']
self.credentialProvider.getCredentials(credentials)
# Signed in
self._onConnectUserSignIn(result['User'])
return result
def _onConnectUserSignIn(self, user):
self.connectUser = user
log.info("connectusersignedin %s" % user)
def _getConnectUser(self, userId, accessToken):
if not userId:
raise AttributeError("null userId")
if not accessToken:
raise AttributeError("null accessToken")
url = self.getConnectUrl('user?id=%s' % userId)
return self.requestUrl({
'type': "GET",
'url': url,
'headers': {
'X-Connect-UserToken': accessToken
}
})
def _addAuthenticationInfoFromConnect(self, server, CONNECTIONMODE, credentials, options={}):
if not server.get('ExchangeToken'):
raise KeyError("server['ExchangeToken'] cannot be null")
if not credentials.get('ConnectUserId'):
raise KeyError("credentials['ConnectUserId'] cannot be null")
url = getServerAddress(server, CONNECTIONMODE)
url = self.getEmbyServerUrl(url, "Connect/Exchange?format=json")
auth = ('MediaBrowser Client="%s", Device="%s", DeviceId="%s", Version="%s"'
% (self.appName, self.deviceName, self.deviceId, self.appVersion))
try:
auth = self.requestUrl({
'url': url,
'type': "GET",
'ssl': options.get('ssl'),
'params': {
'ConnectUserId': credentials['ConnectUserId']
},
'headers': {
'X-MediaBrowser-Token': server['ExchangeToken'],
'X-Emby-Authorization': auth
}
})
except Exception:
server['UserId'] = None
server['AccessToken'] = None
return False
else:
server['UserId'] = auth['LocalUserId']
server['AccessToken'] = auth['AccessToken']
return auth
def _ensureConnectUser(self, credentials):
if self.connectUser and self.connectUser['Id'] == credentials['ConnectUserId']:
return
elif credentials.get('ConnectUserId') and credentials.get('ConnectAccessToken'):
self.connectUser = None
try:
result = self._getConnectUser(credentials['ConnectUserId'], credentials['ConnectAccessToken'])
self._onConnectUserSignIn(result)
except Exception:
return False
def connect(self, settings=None):
log.info("Begin connect")
servers = self.getAvailableServers()
return self._connectToServers(servers, settings)
def _connectToServers(self, servers, settings):
log.info("Begin connectToServers, with %s servers" % len(servers))
if len(servers) == 1:
result = self.connectToServer(servers[0], settings)
if result and result.get('State') == CONNECTIONSTATE['Unavailable']:
result['State'] = CONNECTIONSTATE['ConnectSignIn'] if result['ConnectUser'] == None else CONNECTIONSTATE['ServerSelection']
log.info("resolving connectToServers with result['State']: %s" % result)
return result
firstServer = self._getLastUsedServer()
# See if we have any saved credentials and can auto sign in
if firstServer:
result = self.connectToServer(firstServer, settings)
if result and result.get('State') == CONNECTIONSTATE['SignedIn']:
return result
# Return loaded credentials if exists
credentials = self.credentialProvider.getCredentials()
self._ensureConnectUser(credentials)
return {
'Servers': servers,
'State': CONNECTIONSTATE['ConnectSignIn'] if (not len(servers) and not self._connectUser()) else CONNECTIONSTATE['ServerSelection'],
'ConnectUser': self._connectUser()
}
def _cleanConnectPassword(self, password):
password = password or ""
password = password.replace("&", '&amp;')
password = password.replace("/", '&#092;')
password = password.replace("!", '&#33;')
password = password.replace("$", '&#036;')
password = password.replace("\"", '&quot;')
password = password.replace("<", '&lt;')
password = password.replace(">", '&gt;')
password = password.replace("'", '&#39;')
return password
def clearData(self):
log.info("connection manager clearing data")
self.connectUser = None
credentials = self.credentialProvider.getCredentials()
credentials['ConnectAccessToken'] = None
credentials['ConnectUserId'] = None
credentials['Servers'] = []
self.credentialProvider.getCredentials(credentials)

View file

@ -0,0 +1,149 @@
# -*- coding: utf-8 -*-
###############################################################################
import json
from logging import getLogger
import os
import time
from datetime import datetime
###############################################################################
log = getLogger("PLEX."+__name__)
# This is a throwaway variable to deal with a python bug
_ = datetime.strptime('20110101', '%Y%m%d')
###############################################################################
class Credentials(object):
# Borg
_shared_state = {}
credentials = None
path = ""
def __init__(self):
# Borg
self.__dict__ = self._shared_state
def setPath(self, path):
# Path to save persistant data.txt
self.path = path
def _ensure(self):
if self.credentials is None:
try:
with open(os.path.join(self.path, 'data.txt')) as infile:
self.credentials = json.load(infile)
if not isinstance(self.credentials, dict):
raise ValueError("invalid credentials format")
except Exception as e:
# File is either empty or missing
log.warn(e)
self.credentials = {}
log.info("credentials initialized with: %s" % self.credentials)
self.credentials['Servers'] = self.credentials.setdefault('Servers', [])
def _get(self):
self._ensure()
return self.credentials
def _set(self, data):
if data:
self.credentials = data
# Set credentials to file
with open(os.path.join(self.path, 'data.txt'), 'w') as outfile:
for server in data['Servers']:
server['Name'] = server['Name'].encode('utf-8')
json.dump(data, outfile, ensure_ascii=False)
else:
self._clear()
log.info("credentialsupdated")
def _clear(self):
self.credentials = None
# Remove credentials from file
with open(os.path.join(self.path, 'data.txt'), 'w'):
pass
def getCredentials(self, data=None):
if data is not None:
self._set(data)
return self._get()
def addOrUpdateServer(self, list_, server):
if server.get('Id') is None:
raise KeyError("Server['Id'] cannot be null or empty")
# Add default DateLastAccessed if doesn't exist.
server.setdefault('DateLastAccessed', "2001-01-01T00:00:00Z")
for existing in list_:
if existing['Id'] == server['Id']:
# Merge the data
if server.get('DateLastAccessed'):
if self._dateObject(server['DateLastAccessed']) > self._dateObject(existing['DateLastAccessed']):
existing['DateLastAccessed'] = server['DateLastAccessed']
if server.get('UserLinkType'):
existing['UserLinkType'] = server['UserLinkType']
if server.get('AccessToken'):
existing['AccessToken'] = server['AccessToken']
existing['UserId'] = server['UserId']
if server.get('ExchangeToken'):
existing['ExchangeToken'] = server['ExchangeToken']
if server.get('RemoteAddress'):
existing['RemoteAddress'] = server['RemoteAddress']
if server.get('ManualAddress'):
existing['ManualAddress'] = server['ManualAddress']
if server.get('LocalAddress'):
existing['LocalAddress'] = server['LocalAddress']
if server.get('Name'):
existing['Name'] = server['Name']
if server.get('WakeOnLanInfos'):
existing['WakeOnLanInfos'] = server['WakeOnLanInfos']
if server.get('LastConnectionMode') is not None:
existing['LastConnectionMode'] = server['LastConnectionMode']
if server.get('ConnectServerId'):
existing['ConnectServerId'] = server['ConnectServerId']
return existing
else:
list_.append(server)
return server
def addOrUpdateUser(self, server, user):
for existing in server.setdefault('Users', []):
if existing['Id'] == user['Id']:
# Merge the data
existing['IsSignedInOffline'] = True
break
else:
server['Users'].append(user)
def _dateObject(self, date):
# Convert string to date
try:
date_obj = datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ")
except (ImportError, TypeError):
# TypeError: attribute of type 'NoneType' is not callable
# Known Kodi/python error
date_obj = datetime(*(time.strptime(date, "%Y-%m-%dT%H:%M:%SZ")[0:6]))
return date_obj

View file

@ -0,0 +1,449 @@
# -*- coding: utf-8 -*-
from logging import getLogger
from xbmc import sleep, executebuiltin
from utils import window, settings, dialog, language as lang, tryEncode
from clientinfo import getXArgsDeviceInfo
from downloadutils import DownloadUtils
import variables as v
import state
###############################################################################
log = getLogger("PLEX."+__name__)
###############################################################################
def my_plex_sign_in(username, password, options):
"""
MyPlex Sign In
parameters:
username - Plex forum name, MyPlex login, or email address
password
options - dict() of PlexConnect-options as received from aTV -
necessary: PlexConnectUDID
result:
username
authtoken - token for subsequent communication with MyPlex
"""
# create POST request
xml = DownloadUtils().downloadUrl(
'https://plex.tv/users/sign_in.xml',
action_type='POST',
headerOptions=getXArgsDeviceInfo(options),
authenticate=False,
auth=(username, password))
try:
xml.attrib
except AttributeError:
log.error('Could not sign in to plex.tv')
return ('', '')
el_username = xml.find('username')
el_authtoken = xml.find('authentication-token')
if el_username is None or \
el_authtoken is None:
username = ''
authtoken = ''
else:
username = el_username.text
authtoken = el_authtoken.text
return (username, authtoken)
def check_plex_tv_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
"""
# Try to get a temporary token
xml = DownloadUtils().downloadUrl(
'https://plex.tv/pins/%s.xml' % identifier,
authenticate=False)
try:
temp_token = xml.find('auth_token').text
except:
log.error("Could not find token in plex.tv answer")
return False
if not temp_token:
return False
# Use temp token to get the final plex credentials
xml = DownloadUtils().downloadUrl('https://plex.tv/users/account',
authenticate=False,
parameters={'X-Plex-Token': temp_token})
return xml
def get_plex_pin():
"""
For plex.tv sign-in: returns 4-digit code and identifier as 2 str
"""
code = None
identifier = None
# Download
xml = DownloadUtils().downloadUrl('https://plex.tv/pins.xml',
authenticate=False,
action_type="POST")
try:
xml.attrib
except:
log.error("Error, no PIN from plex.tv provided")
return None, None
code = xml.find('code').text
identifier = xml.find('id').text
log.info('Successfully retrieved code and id from plex.tv')
return code, identifier
def get_plex_login_password():
"""
Signs in to plex.tv.
plexLogin, authtoken = get_plex_login_password()
Input: nothing
Output:
plexLogin plex.tv username
authtoken token for plex.tv
Also writes 'plexLogin' and 'token_plex.tv' to Kodi settings file
If not logged in, empty strings are returned for both.
"""
retrievedPlexLogin = ''
plexLogin = 'dummy'
authtoken = ''
while retrievedPlexLogin == '' and plexLogin != '':
# Enter plex.tv username. Or nothing to cancel.
plexLogin = dialog('input',
lang(29999) + lang(39300),
type='{alphanum}')
if plexLogin != "":
# Enter password for plex.tv user
plexPassword = dialog('input',
lang(39301) + plexLogin,
type='{alphanum}',
option='{hide_input}')
retrievedPlexLogin, authtoken = my_plex_sign_in(
plexLogin,
plexPassword,
{'X-Plex-Client-Identifier': window('plex_client_Id')})
log.debug("plex.tv username and token: %s, %s"
% (plexLogin, authtoken))
if plexLogin == '':
# Could not sign in user
dialog('ok', lang(29999), lang(39302) + plexLogin)
# Write to Kodi settings file
settings('plexLogin', value=retrievedPlexLogin)
settings('plexToken', value=authtoken)
return (retrievedPlexLogin, authtoken)
def plex_tv_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.
"""
code, identifier = get_plex_pin()
if not code:
# Problems trying to contact plex.tv. Try again later
dialog('ok', lang(29999), lang(39303))
return False
# Go to https://plex.tv/pin and enter the code:
# Or press No to cancel the sign in.
answer = dialog('yesno',
lang(29999),
lang(39304) + "\n\n",
code + "\n\n",
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_plex_tv_pin(identifier)
if xml is not False:
break
# Wait for 1 seconds
sleep(1000)
count += 1
if xml is False:
# Could not sign in to plex.tv Try again later
dialog('ok', lang(29999), 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')
homeSize = xml.get('homeSize', '1')
result = {
'plexhome': home,
'username': username,
'avatar': avatar,
'token': token,
'plexid': userid,
'homesize': homeSize
}
settings('plexLogin', username)
settings('plexToken', token)
settings('plexhome', home)
settings('plexid', userid)
settings('plexAvatar', avatar)
settings('plexHomeSize', homeSize)
# Let Kodi log into plex.tv on startup from now on
settings('myplexlogin', 'true')
settings('plex_status', value=lang(39227))
return result
def list_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
"""
xml = DownloadUtils().downloadUrl('https://plex.tv/api/home/users/',
authenticate=False,
headerOptions={'X-Plex-Token': token})
try:
xml.attrib
except:
log.error('Download of Plex home users failed.')
return False
users = []
for user in xml:
users.append(user.attrib)
return users
def switch_home_user(userId, pin, token, machineIdentifier):
"""
Retrieves Plex home token for a Plex home user.
Returns False if unsuccessful
Input:
userId id of the Plex home user
pin PIN of the Plex home user, if protected
token token for plex.tv
Output:
{
'username'
'usertoken' Might be empty strings if no token found
for the machineIdentifier that was chosen
}
settings('userid') and settings('username') with new plex token
"""
log.info('Switching to user %s' % userId)
url = 'https://plex.tv/api/home/users/' + userId + '/switch'
if pin:
url += '?pin=' + pin
answer = DownloadUtils.downloadUrl(
url,
authenticate=False,
action_type="POST",
headerOptions={'X-Plex-Token': token})
try:
answer.attrib
except:
log.error('Error: plex.tv switch HomeUser change failed')
return False
username = answer.attrib.get('title', '')
token = answer.attrib.get('authenticationToken', '')
# Write to settings file
settings('username', username)
settings('accessToken', token)
settings('userid', answer.attrib.get('id', ''))
settings('plex_restricteduser',
'true' if answer.attrib.get('restricted', '0') == '1'
else 'false')
state.RESTRICTED_USER = True if \
answer.attrib.get('restricted', '0') == '1' else False
# Get final token to the PMS we've chosen
url = 'https://plex.tv/api/resources?includeHttps=1'
xml = DownloadUtils.downloadUrl(url,
authenticate=False,
headerOptions={'X-Plex-Token': token})
try:
xml.attrib
except:
log.error('Answer from plex.tv not as excepted')
# Set to empty iterable list for loop
xml = []
found = 0
log.debug('Our machineIdentifier is %s' % machineIdentifier)
for device in xml:
identifier = device.attrib.get('clientIdentifier')
log.debug('Found a Plex machineIdentifier: %s' % identifier)
if (identifier in machineIdentifier or
machineIdentifier in 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
log.info('Plex.tv switch HomeUser change successfull for user %s'
% username)
return result
def ChoosePlexHomeUser(plexToken):
"""
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)
"""
# Get list of Plex home users
users = list_plex_home_users(plexToken)
if not users:
log.error("User download failed.")
return False
userlist = []
userlistCoded = []
for user in users:
username = user['title']
userlist.append(username)
# To take care of non-ASCII usernames
userlistCoded.append(tryEncode(username))
usernumber = len(userlist)
username = ''
usertoken = ''
trials = 0
while trials < 3:
if usernumber > 1:
# Select user
user_select = dialog('select',
lang(29999) + lang(39306),
userlistCoded)
if user_select == -1:
log.info("No user selected.")
settings('username', value='')
executebuiltin('Addon.OpenSettings(%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 = dialog('input',
lang(39307) + selected_user,
'',
type='{numeric}',
option='{hide_input}')
# 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,
plexToken,
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 dialog('yesno',
lang(29999),
lang(39308) + selected_user,
lang(39309)):
# User chose to cancel
break
if not username:
log.error('Failed signing in a user to plex.tv')
executebuiltin('Addon.OpenSettings(%s)' % v.ADDON_ID)
return False
return {
'username': username,
'userid': user['id'],
'protected': True if user['protected'] == '1' else False,
'token': usertoken
}
def get_user_artwork_url(username):
"""
Returns the URL for the user's Avatar. Or False if something went
wrong.
"""
plexToken = settings('plexToken')
users = list_plex_home_users(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']
log.debug("Avatar url for user %s is: %s" % (username, url))
return url

View file

@ -0,0 +1,977 @@
# -*- coding: utf-8 -*-
###############################################################################
from logging import getLogger
from copy import deepcopy
from os import makedirs
from xbmc import getIPAddress
# from connect.connectionmanager import ConnectionManager
from downloadutils import DownloadUtils
from dialogs.serverconnect import ServerConnect
from dialogs.servermanual import ServerManual
from connect.plex_tv import plex_tv_sign_in_with_pin
import connect.connectionmanager as connectionmanager
from userclient import UserClient
from utils import window, settings, tryEncode, language as lang, dialog, \
exists_dir
from PlexFunctions import GetMachineIdentifier, get_pms_settings, \
check_connection
import variables as v
import state
###############################################################################
log = getLogger("PLEX."+__name__)
STATE = connectionmanager.CONNECTIONSTATE
XML_PATH = (tryEncode(v.ADDON_PATH), "default", "1080i")
###############################################################################
def get_plex_login_from_settings():
"""
Returns a dict:
'plexLogin': settings('plexLogin'),
'plexToken': settings('plexToken'),
'plexhome': settings('plexhome'),
'plexid': settings('plexid'),
'myplexlogin': settings('myplexlogin'),
'plexAvatar': settings('plexAvatar'),
'plexHomeSize': 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': settings('plexLogin'),
'plexToken': settings('plexToken'),
'plexhome': settings('plexhome'),
'plexid': settings('plexid'),
'myplexlogin': settings('myplexlogin'),
'plexAvatar': settings('plexAvatar'),
'plexHomeSize': settings('plexHomeSize')
}
class ConnectManager(object):
# Borg
__shared_state = {}
state = {}
def __init__(self):
# Borg
self.__dict__ = self.__shared_state
log.debug('Instantiating')
self.doUtils = DownloadUtils().downloadUrl
self.server = UserClient().getServer()
self.serverid = settings('plex_machineIdentifier')
# Get Plex credentials from settings file, if they exist
plexdict = get_plex_login_from_settings()
self.myplexlogin = plexdict['myplexlogin'] == 'true'
self.plexLogin = plexdict['plexLogin']
self.plexid = plexdict['plexid']
# Token for the PMS, not plex.tv
self.__connect = connectionmanager.ConnectionManager(
appName="Kodi",
appVersion=v.ADDON_VERSION,
deviceName=v.DEVICENAME,
deviceId=window('plex_client_Id'))
self.pms_token = settings('accessToken')
self.plexToken = plexdict['plexToken']
self.__connect.plexToken = self.plexToken
if self.plexToken:
log.debug('Found a plex.tv token in the settings')
if not exists_dir(v.ADDON_PATH_DATA):
makedirs(v.ADDON_PATH_DATA)
self.__connect.setFilePath(v.ADDON_PATH_DATA)
if state.CONNECT_STATE:
self.state = state.CONNECT_STATE
else:
self.state = self.__connect.connect()
log.debug("Started with: %s", self.state)
state.CONNECT_STATE = deepcopy(self.state)
def update_state(self):
self.state = self.__connect.connect({'updateDateLastAccessed': False})
return self.get_state()
def get_state(self):
state.CONNECT_STATE = deepcopy(self.state)
return self.state
def get_server(self, server, options={}):
self.state = self.__connect.connectToAddress(server, options)
return self.get_state()
@classmethod
def get_address(cls, server):
return connectionmanager.getServerAddress(server, server['LastConnectionMode'])
def clear_data(self):
self.__connect.clearData()
def select_servers(self):
"""
Will return selected server or raise RuntimeError
"""
status = self.__connect.connect({'enableAutoLogin': False})
dia = ServerConnect("script-plex-connect-server.xml", *XML_PATH)
kwargs = {
'connect_manager': self.__connect,
'username': state.PLEX_USERNAME,
'user_image': state.PLEX_USER_IMAGE,
'servers': status.get('Servers') or [],
'plex_connect': False if status.get('ConnectUser') else True
}
dia.set_args(**kwargs)
dia.doModal()
if dia.is_server_selected():
log.debug("Server selected")
return dia.get_server()
elif dia.is_connect_login():
log.debug("Login to plex.tv")
self.plex_tv_signin()
return self.select_servers()
elif dia.is_manual_server():
log.debug("Add manual server")
try:
# Add manual server address
return self.manual_server()
except RuntimeError:
return self.select_servers()
else:
raise RuntimeError("No server selected")
def manual_server(self):
# Return server or raise error
dia = ServerManual("script-plex-connect-server-manual.xml", *XML_PATH)
dia.set_connect_manager(self.__connect)
dia.doModal()
if dia.is_connected():
return dia.get_server()
else:
raise RuntimeError("Server is not connected")
def login(self, server=None):
# Return user or raise error
server = server or self.state['Servers'][0]
server_address = connectionmanager.getServerAddress(server, server['LastConnectionMode'])
users = "";
try:
users = self.emby.getUsers(server_address)
except Exception as error:
log.info("Error getting users from server: " + str(error))
if not users:
try:
return self.login_manual(server_address)
except RuntimeError:
raise RuntimeError("No user selected")
dia = UsersConnect("script-emby-connect-users.xml", *XML_PATH)
dia.set_server(server_address)
dia.set_users(users)
dia.doModal()
if dia.is_user_selected():
user = dia.get_user()
username = user['Name']
if user['HasPassword']:
log.debug("User has password, present manual login")
try:
return self.login_manual(server_address, username)
except RuntimeError:
return self.login(server)
else:
try:
user = self.emby.loginUser(server_address, username)
except Exception as error:
log.info("Error logging in user: " + str(error))
raise
self.__connect.onAuthenticated(user)
return user
elif dia.is_manual_login():
try:
return self.login_manual(server_address)
except RuntimeError:
return self.login(server)
else:
raise RuntimeError("No user selected")
def login_manual(self, server, user=None):
# Return manual login user authenticated or raise error
dia = LoginManual("script-emby-connect-login-manual.xml", *XML_PATH)
dia.set_server(server)
dia.set_user(user)
dia.doModal()
if dia.is_logged_in():
user = dia.get_user()
self.__connect.onAuthenticated(user)
return user
else:
raise RuntimeError("User is not authenticated")
def update_token(self, server):
credentials = self.__connect.credentialProvider.getCredentials()
self.__connect.credentialProvider.addOrUpdateServer(credentials['Servers'], server)
for server in self.get_state()['Servers']:
for cred_server in credentials['Servers']:
if server['Id'] == cred_server['Id']:
# Update token saved in current state
server.update(cred_server)
# Update the token in data.txt
self.__connect.credentialProvider.getCredentials(credentials)
def get_connect_servers(self):
connect_servers = []
servers = self.__connect.getAvailableServers()
for server in servers:
if 'ExchangeToken' in server:
result = self.connect_server(server)
if result['State'] == STATE['SignedIn']:
connect_servers.append(server)
log.info(connect_servers)
return connect_servers
def connect_server(self, server):
return self.__connect.connectToServer(server, {'updateDateLastAccessed': False})
def pick_pms(self, show_dialog=False):
"""
Searches for PMS in local Lan and optionally (if self.plexToken set)
also on plex.tv
show_dialog=True: let the user pick one
show_dialog=False: automatically pick PMS based on
machineIdentifier
Returns the picked PMS' detail as a dict:
{
'name': friendlyName, the Plex server's name
'address': ip:port
'ip': ip, without http/https
'port': port
'scheme': 'http'/'https', nice for checking for secure connections
'local': '1'/'0', Is the server a local server?
'owned': '1'/'0', Is the server owned by the user?
'machineIdentifier': id, Plex server machine identifier
'accesstoken': token Access token to this server
'baseURL': baseURL scheme://ip:port
'ownername' Plex username of PMS owner
}
or None if unsuccessful
"""
server = None
# If no server is set, let user choose one
if not self.server or not self.serverid:
show_dialog = True
if show_dialog is True:
try:
server = self.select_servers()
except RuntimeError:
pass
log.info("Server: %s", server)
server = self.__user_pick_pms()
else:
server = self.__auto_pick_pms()
if server is not None:
self.write_pms_settings(server['baseURL'], server['accesstoken'])
return server
@staticmethod
def write_pms_settings(url, token):
"""
Sets certain settings for server by asking for the PMS' settings
Call with url: scheme://ip:port
"""
xml = get_pms_settings(url, token)
try:
xml.attrib
except AttributeError:
log.error('Could not get PMS settings for %s' % url)
return
for entry in xml:
if entry.attrib.get('id', '') == 'allowMediaDeletion':
settings('plex_allows_mediaDeletion',
value=entry.attrib.get('value', 'true'))
window('plex_allows_mediaDeletion',
value=entry.attrib.get('value', 'true'))
def __auto_pick_pms(self):
"""
Will try to pick PMS based on machineIdentifier saved in file settings
but only once
Returns server or None if unsuccessful
"""
httpsUpdated = False
checkedPlexTV = False
server = None
while True:
if httpsUpdated is False:
serverlist = self.__get_server_list()
for item in serverlist:
if item.get('machineIdentifier') == self.serverid:
server = item
if server is None:
name = settings('plex_servername')
log.warn('The PMS you have used before with a unique '
'machineIdentifier of %s and name %s is '
'offline' % (self.serverid, name))
return
chk = self._checkServerCon(server)
if chk == 504 and httpsUpdated is False:
# Not able to use HTTP, try HTTPs for now
server['scheme'] = 'https'
httpsUpdated = True
continue
if chk == 401:
log.warn('Not yet authorized for Plex server %s'
% server['name'])
if self.check_plex_tv_signin() is True:
if checkedPlexTV is False:
# Try again
checkedPlexTV = True
httpsUpdated = False
continue
else:
log.warn('Not authorized even though we are signed '
' in to plex.tv correctly')
dialog('ok',
lang(29999), '%s %s'
% (lang(39214),
tryEncode(server['name'])))
return
else:
return
# Problems connecting
elif chk >= 400 or chk is False:
log.warn('Problems connecting to server %s. chk is %s'
% (server['name'], chk))
return
log.info('We found a server to automatically connect to: %s'
% server['name'])
return server
def __user_pick_pms(self):
"""
Lets user pick his/her PMS from a list
Returns server or None if unsuccessful
"""
httpsUpdated = False
while True:
if httpsUpdated is False:
serverlist = self.__get_server_list()
# Exit if no servers found
if len(serverlist) == 0:
log.warn('No plex media servers found!')
dialog('ok', lang(29999), lang(39011))
return
# Get a nicer list
dialoglist = []
for server in serverlist:
if server['local'] == '1':
# server is in the same network as client.
# Add"local"
msg = lang(39022)
else:
# Add 'remote'
msg = lang(39054)
if server.get('ownername'):
# Display username if its not our PMS
dialoglist.append('%s (%s, %s)'
% (server['name'],
server['ownername'],
msg))
else:
dialoglist.append('%s (%s)'
% (server['name'], msg))
# Let user pick server from a list
resp = dialog('select', lang(39012), dialoglist)
if resp == -1:
# User cancelled
return
server = serverlist[resp]
chk = self._checkServerCon(server)
if chk == 504 and httpsUpdated is False:
# Not able to use HTTP, try HTTPs for now
serverlist[resp]['scheme'] = 'https'
httpsUpdated = True
continue
httpsUpdated = False
if chk == 401:
log.warn('Not yet authorized for Plex server %s'
% server['name'])
# Please sign in to plex.tv
dialog('ok',
lang(29999),
lang(39013) + server['name'],
lang(39014))
if self.plex_tv_signin() is False:
# Exit while loop if user cancels
return
# Problems connecting
elif chk >= 400 or chk is False:
# Problems connecting to server. Pick another server?
# Exit while loop if user chooses No
if not dialog('yesno', lang(29999), lang(39015)):
return
# Otherwise: connection worked!
else:
return server
@staticmethod
def write_pms_to_settings(server):
"""
Saves server to file settings. server is a dict of the form:
{
'name': friendlyName, the Plex server's name
'address': ip:port
'ip': ip, without http/https
'port': port
'scheme': 'http'/'https', nice for checking for secure connections
'local': '1'/'0', Is the server a local server?
'owned': '1'/'0', Is the server owned by the user?
'machineIdentifier': id, Plex server machine identifier
'accesstoken': token Access token to this server
'baseURL': baseURL scheme://ip:port
'ownername' Plex username of PMS owner
}
"""
settings('plex_machineIdentifier', server['machineIdentifier'])
settings('plex_servername', server['name'])
settings('plex_serverowned',
'true' if server['owned'] == '1'
else 'false')
# Careful to distinguish local from remote PMS
if server['local'] == '1':
scheme = server['scheme']
settings('ipaddress', server['ip'])
settings('port', server['port'])
log.debug("Setting SSL verify to false, because server is "
"local")
settings('sslverify', 'false')
else:
baseURL = server['baseURL'].split(':')
scheme = baseURL[0]
settings('ipaddress', baseURL[1].replace('//', ''))
settings('port', baseURL[2])
log.debug("Setting SSL verify to true, because server is not "
"local")
settings('sslverify', 'true')
if scheme == 'https':
settings('https', 'true')
else:
settings('https', 'false')
# And finally do some logging
log.debug("Writing to Kodi user settings file")
log.debug("PMS machineIdentifier: %s, ip: %s, port: %s, https: %s "
% (server['machineIdentifier'], server['ip'],
server['port'], server['scheme']))
def plex_tv_signin(self):
"""
Signs (freshly) in to plex.tv (will be saved to file settings)
Returns True if successful, or False if not
"""
result = plex_tv_sign_in_with_pin()
if result:
self.plexLogin = result['username']
self.plexToken = result['token']
self.plexid = result['plexid']
return True
return False
def check_plex_tv_signin(self):
"""
Checks existing connection to plex.tv. If not, triggers sign in
Returns True if signed in, False otherwise
"""
answer = True
chk = check_connection('plex.tv', token=self.plexToken)
if chk in (401, 403):
# HTTP Error: unauthorized. Token is no longer valid
log.info('plex.tv connection returned HTTP %s' % str(chk))
# Delete token in the settings
settings('plexToken', value='')
settings('plexLogin', value='')
# Could not login, please try again
dialog('ok', lang(29999), lang(39009))
answer = self.plex_tv_signin()
elif chk is False or chk >= 400:
# Problems connecting to plex.tv. Network or internet issue?
log.info('Problems connecting to plex.tv; connection returned '
'HTTP %s' % str(chk))
dialog('ok', lang(29999), lang(39010))
answer = False
else:
log.info('plex.tv connection with token successful')
settings('plex_status', value=lang(39227))
# Refresh the info from Plex.tv
xml = self.doUtils('https://plex.tv/users/account',
authenticate=False,
headerOptions={'X-Plex-Token': self.plexToken})
try:
self.plexLogin = xml.attrib['title']
except (AttributeError, KeyError):
log.error('Failed to update Plex info from plex.tv')
else:
settings('plexLogin', value=self.plexLogin)
home = 'true' if xml.attrib.get('home') == '1' else 'false'
settings('plexhome', value=home)
settings('plexAvatar', value=xml.attrib.get('thumb'))
settings('plexHomeSize', value=xml.attrib.get('homeSize', '1'))
log.info('Updated Plex info from plex.tv')
return answer
def check_pms(self):
"""
Check the PMS that was set in file settings.
Will return False if we need to reconnect, because:
PMS could not be reached (no matter the authorization)
machineIdentifier did not match
Will also set the PMS machineIdentifier in the file settings if it was
not set before
"""
answer = True
chk = check_connection(self.server, verifySSL=False)
if chk is False:
log.warn('Could not reach PMS %s' % self.server)
answer = False
if answer is True and not self.serverid:
log.info('No PMS machineIdentifier found for %s. Trying to '
'get the PMS unique ID' % self.server)
self.serverid = GetMachineIdentifier(self.server)
if self.serverid is None:
log.warn('Could not retrieve machineIdentifier')
answer = False
else:
settings('plex_machineIdentifier', value=self.serverid)
elif answer is True:
tempServerid = GetMachineIdentifier(self.server)
if tempServerid != self.serverid:
log.warn('The current PMS %s was expected to have a '
'unique machineIdentifier of %s. But we got '
'%s. Pick a new server to be sure'
% (self.server, self.serverid, tempServerid))
answer = False
return answer
def __get_server_list(self):
"""
Returns a list of servers from GDM and possibly plex.tv
"""
self.discoverPMS(getIPAddress(), plexToken=self.plexToken)
serverlist = self.plx.returnServerList(self.plx.g_PMS)
log.debug('PMS serverlist: %s' % serverlist)
return serverlist
def _checkServerCon(self, server):
"""
Checks for server's connectivity. Returns check_connection result
"""
# Re-direct via plex if remote - will lead to the correct SSL
# certificate
if server['local'] == '1':
url = '%s://%s:%s' \
% (server['scheme'], server['ip'], server['port'])
# Deactive SSL verification if the server is local!
verifySSL = False
else:
url = server['baseURL']
verifySSL = True
chk = check_connection(url,
token=server['accesstoken'],
verifySSL=verifySSL)
return chk
def discoverPMS(self, IP_self, plexToken=None):
"""
parameters:
IP_self Own IP
optional:
plexToken token for plex.tv
result:
self.g_PMS dict set
"""
self.g_PMS = {}
# Look first for local PMS in the LAN
pmsList = self.PlexGDM()
log.debug('PMS found in the local LAN via GDM: %s' % pmsList)
# Get PMS from plex.tv
if plexToken:
log.info('Checking with plex.tv for more PMS to connect to')
self.getPMSListFromMyPlex(plexToken)
else:
log.info('No plex token supplied, only checked LAN for PMS')
for uuid in pmsList:
PMS = pmsList[uuid]
if PMS['uuid'] in self.g_PMS:
log.debug('We already know of PMS %s from plex.tv'
% PMS['serverName'])
# Update with GDM data - potentially more reliable than plex.tv
self.updatePMSProperty(PMS['uuid'], 'ip', PMS['ip'])
self.updatePMSProperty(PMS['uuid'], 'port', PMS['port'])
self.updatePMSProperty(PMS['uuid'], 'local', '1')
self.updatePMSProperty(PMS['uuid'], 'scheme', 'http')
self.updatePMSProperty(PMS['uuid'],
'baseURL',
'http://%s:%s' % (PMS['ip'],
PMS['port']))
else:
self.declarePMS(PMS['uuid'], PMS['serverName'], 'http',
PMS['ip'], PMS['port'])
# Ping to check whether we need HTTPs or HTTP
https = PMSHttpsEnabled('%s:%s' % (PMS['ip'], PMS['port']))
if https is None:
# Error contacting url. Skip for now
continue
elif https is True:
self.updatePMSProperty(PMS['uuid'], 'scheme', 'https')
self.updatePMSProperty(
PMS['uuid'],
'baseURL',
'https://%s:%s' % (PMS['ip'], PMS['port']))
else:
# Already declared with http
pass
# install plex.tv "virtual" PMS - for myPlex, PlexHome
# self.declarePMS('plex.tv', 'plex.tv', 'https', 'plex.tv', '443')
# self.updatePMSProperty('plex.tv', 'local', '-')
# self.updatePMSProperty('plex.tv', 'owned', '-')
# self.updatePMSProperty(
# 'plex.tv', 'accesstoken', plexToken)
# (remote and local) servers from plex.tv
def declarePMS(self, uuid, name, scheme, ip, port):
"""
Plex Media Server handling
parameters:
uuid - PMS ID
name, scheme, ip, port, type, owned, token
"""
address = ip + ':' + port
baseURL = scheme + '://' + ip + ':' + port
self.g_PMS[uuid] = {
'name': name,
'scheme': scheme,
'ip': ip,
'port': port,
'address': address,
'baseURL': baseURL,
'local': '1',
'owned': '1',
'accesstoken': '',
'enableGzip': False
}
def updatePMSProperty(self, uuid, tag, value):
# set property element of PMS by UUID
try:
self.g_PMS[uuid][tag] = value
except:
log.error('%s has not yet been declared ' % uuid)
return False
def getPMSProperty(self, uuid, tag):
# get name of PMS by UUID
try:
answ = self.g_PMS[uuid].get(tag, '')
except:
log.error('%s not found in PMS catalogue' % uuid)
answ = False
return answ
def PlexGDM(self):
"""
PlexGDM
parameters:
none
result:
PMS_list - dict() of PMSs found
"""
import struct
IP_PlexGDM = '239.0.0.250' # multicast to PMS
Port_PlexGDM = 32414
Msg_PlexGDM = 'M-SEARCH * HTTP/1.0'
# setup socket for discovery -> multicast message
GDM = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
GDM.settimeout(2.0)
# Set the time-to-live for messages to 2 for local network
ttl = struct.pack('b', 2)
GDM.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
returnData = []
try:
# Send data to the multicast group
GDM.sendto(Msg_PlexGDM, (IP_PlexGDM, Port_PlexGDM))
# Look for responses from all recipients
while True:
try:
data, server = GDM.recvfrom(1024)
returnData.append({'from': server,
'data': data})
except socket.timeout:
break
except Exception as e:
# Probably error: (101, 'Network is unreachable')
log.error(e)
import traceback
log.error("Traceback:\n%s" % traceback.format_exc())
finally:
GDM.close()
pmsList = {}
for response in returnData:
update = {'ip': response.get('from')[0]}
# Check if we had a positive HTTP response
if "200 OK" not in response.get('data'):
continue
for each in response.get('data').split('\n'):
# decode response data
update['discovery'] = "auto"
# update['owned']='1'
# update['master']= 1
# update['role']='master'
if "Content-Type:" in each:
update['content-type'] = each.split(':')[1].strip()
elif "Resource-Identifier:" in each:
update['uuid'] = each.split(':')[1].strip()
elif "Name:" in each:
update['serverName'] = tryDecode(
each.split(':')[1].strip())
elif "Port:" in each:
update['port'] = each.split(':')[1].strip()
elif "Updated-At:" in each:
update['updated'] = each.split(':')[1].strip()
elif "Version:" in each:
update['version'] = each.split(':')[1].strip()
pmsList[update['uuid']] = update
return pmsList
def getPMSListFromMyPlex(self, token):
"""
getPMSListFromMyPlex
get Plex media Server List from plex.tv/pms/resources
"""
xml = self.doUtils('https://plex.tv/api/resources',
authenticate=False,
parameters={'includeHttps': 1},
headerOptions={'X-Plex-Token': token})
try:
xml.attrib
except AttributeError:
log.error('Could not get list of PMS from plex.tv')
return
import Queue
queue = Queue.Queue()
threadQueue = []
maxAgeSeconds = 2*60*60*24
for Dir in xml.findall('Device'):
if 'server' not in Dir.get('provides'):
# No PMS - skip
continue
if Dir.find('Connection') is None:
# no valid connection - skip
continue
# check MyPlex data age - skip if >2 days
PMS = {}
PMS['name'] = Dir.get('name')
infoAge = time() - int(Dir.get('lastSeenAt'))
if infoAge > maxAgeSeconds:
log.debug("Server %s not seen for 2 days - skipping."
% PMS['name'])
continue
PMS['uuid'] = Dir.get('clientIdentifier')
PMS['token'] = Dir.get('accessToken', token)
PMS['owned'] = Dir.get('owned', '1')
PMS['local'] = Dir.get('publicAddressMatches')
PMS['ownername'] = Dir.get('sourceTitle', '')
PMS['path'] = '/'
PMS['options'] = None
# Try a local connection first
# Backup to remote connection, if that failes
PMS['connections'] = []
for Con in Dir.findall('Connection'):
if Con.get('local') == '1':
PMS['connections'].append(Con)
# Append non-local
for Con in Dir.findall('Connection'):
if Con.get('local') != '1':
PMS['connections'].append(Con)
t = Thread(target=self.pokePMS,
args=(PMS, queue))
threadQueue.append(t)
maxThreads = 5
threads = []
# poke PMS, own thread for each PMS
while True:
# Remove finished threads
for t in threads:
if not t.isAlive():
threads.remove(t)
if len(threads) < maxThreads:
try:
t = threadQueue.pop()
except IndexError:
# We have done our work
break
else:
t.start()
threads.append(t)
else:
sleep(50)
# wait for requests being answered
for t in threads:
t.join()
# declare new PMSs
while not queue.empty():
PMS = queue.get()
self.declarePMS(PMS['uuid'], PMS['name'],
PMS['protocol'], PMS['ip'], PMS['port'])
self.updatePMSProperty(
PMS['uuid'], 'accesstoken', PMS['token'])
self.updatePMSProperty(
PMS['uuid'], 'owned', PMS['owned'])
self.updatePMSProperty(
PMS['uuid'], 'local', PMS['local'])
# set in declarePMS, overwrite for https encryption
self.updatePMSProperty(
PMS['uuid'], 'baseURL', PMS['baseURL'])
self.updatePMSProperty(
PMS['uuid'], 'ownername', PMS['ownername'])
log.debug('Found PMS %s: %s'
% (PMS['uuid'], self.g_PMS[PMS['uuid']]))
queue.task_done()
def pokePMS(self, PMS, queue):
data = PMS['connections'][0].attrib
if data['local'] == '1':
protocol = data['protocol']
address = data['address']
port = data['port']
url = '%s://%s:%s' % (protocol, address, port)
else:
url = data['uri']
if url.count(':') == 1:
url = '%s:%s' % (url, data['port'])
protocol, address, port = url.split(':', 2)
address = address.replace('/', '')
xml = self.doUtils('%s/identity' % url,
authenticate=False,
headerOptions={'X-Plex-Token': PMS['token']},
verifySSL=False,
timeout=10)
try:
xml.attrib['machineIdentifier']
except (AttributeError, KeyError):
# No connection, delete the one we just tested
del PMS['connections'][0]
if len(PMS['connections']) > 0:
# Still got connections left, try them
return self.pokePMS(PMS, queue)
return
else:
# Connection successful - correct PMS?
if xml.get('machineIdentifier') == PMS['uuid']:
# process later
PMS['baseURL'] = url
PMS['protocol'] = protocol
PMS['ip'] = address
PMS['port'] = port
queue.put(PMS)
return
log.info('Found a PMS at %s, but the expected machineIdentifier of '
'%s did not match the one we found: %s'
% (url, PMS['uuid'], xml.get('machineIdentifier')))
def returnServerList(self, data):
"""
Returns a nicer list of all servers found in data, where data is in
g_PMS format, for the client device with unique ID ATV_udid
Input:
data e.g. self.g_PMS
Output: List of all servers, with an entry of the form:
{
'name': friendlyName, the Plex server's name
'address': ip:port
'ip': ip, without http/https
'port': port
'scheme': 'http'/'https', nice for checking for secure connections
'local': '1'/'0', Is the server a local server?
'owned': '1'/'0', Is the server owned by the user?
'machineIdentifier': id, Plex server machine identifier
'accesstoken': token Access token to this server
'baseURL': baseURL scheme://ip:port
'ownername' Plex username of PMS owner
}
"""
serverlist = []
for key, value in data.items():
serverlist.append({
'name': value.get('name'),
'address': value.get('address'),
'ip': value.get('ip'),
'port': value.get('port'),
'scheme': value.get('scheme'),
'local': value.get('local'),
'owned': value.get('owned'),
'machineIdentifier': key,
'accesstoken': value.get('accesstoken'),
'baseURL': value.get('baseURL'),
'ownername': value.get('ownername')
})
return serverlist

View file

@ -1,136 +0,0 @@
# -*- coding: utf-8 -*-
##################################################################################################
import logging
import os
import xbmcgui
import xbmcaddon
from utils import language as lang
##################################################################################################
log = logging.getLogger("EMBY."+__name__)
addon = xbmcaddon.Addon('plugin.video.emby')
ACTION_PARENT_DIR = 9
ACTION_PREVIOUS_MENU = 10
ACTION_BACK = 92
SIGN_IN = 200
CANCEL = 201
ERROR_TOGGLE = 202
ERROR_MSG = 203
ERROR = {
'Invalid': 1,
'Empty': 2
}
##################################################################################################
class LoginConnect(xbmcgui.WindowXMLDialog):
_user = None
error = None
def __init__(self, *args, **kwargs):
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
def set_connect_manager(self, connect_manager):
self.connect_manager = connect_manager
def is_logged_in(self):
return True if self._user else False
def get_user(self):
return self._user
def onInit(self):
self.user_field = self._add_editcontrol(725, 385, 40, 500)
self.setFocus(self.user_field)
self.password_field = self._add_editcontrol(725, 470, 40, 500, password=1)
self.signin_button = self.getControl(SIGN_IN)
self.remind_button = self.getControl(CANCEL)
self.error_toggle = self.getControl(ERROR_TOGGLE)
self.error_msg = self.getControl(ERROR_MSG)
self.user_field.controlUp(self.remind_button)
self.user_field.controlDown(self.password_field)
self.password_field.controlUp(self.user_field)
self.password_field.controlDown(self.signin_button)
self.signin_button.controlUp(self.password_field)
self.remind_button.controlDown(self.user_field)
def onClick(self, control):
if control == SIGN_IN:
# Sign in to emby connect
self._disable_error()
user = self.user_field.getText()
password = self.password_field.getText()
if not user or not password:
# Display error
self._error(ERROR['Empty'], lang(30608))
log.error("Username or password cannot be null")
elif self._login(user, password):
self.close()
elif control == CANCEL:
# Remind me later
self.close()
def onAction(self, action):
if (self.error == ERROR['Empty']
and self.user_field.getText() and self.password_field.getText()):
self._disable_error()
if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU):
self.close()
def _add_editcontrol(self, x, y, height, width, password=0):
media = os.path.join(addon.getAddonInfo('path'), 'resources', 'skins', 'default', 'media')
control = xbmcgui.ControlEdit(0, 0, 0, 0,
label="User",
font="font10",
textColor="ff525252",
focusTexture=os.path.join(media, "button-focus.png"),
noFocusTexture=os.path.join(media, "button-focus.png"),
isPassword=password)
control.setPosition(x, y)
control.setHeight(height)
control.setWidth(width)
self.addControl(control)
return control
def _login(self, username, password):
result = self.connect_manager.loginToConnect(username, password)
if result is False:
self._error(ERROR['Invalid'], lang(33009))
return False
else:
self._user = result
return True
def _error(self, state, message):
self.error = state
self.error_msg.setLabel(message)
self.error_toggle.setVisibleCondition('True')
def _disable_error(self):
self.error = None
self.error_toggle.setVisibleCondition('False')

View file

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
################################################################################################## ###############################################################################
import logging from logging import getLogger
import xbmc import xbmc
import xbmcgui import xbmcgui
@ -10,11 +10,11 @@ import xbmcgui
import connect.connectionmanager as connectionmanager import connect.connectionmanager as connectionmanager
from utils import language as lang from utils import language as lang
################################################################################################## ###############################################################################
log = logging.getLogger("EMBY."+__name__) log = getLogger("PLEX."+__name__)
CONN_STATE = connectionmanager.ConnectionState CONN_STATE = connectionmanager.CONNECTIONSTATE
ACTION_PARENT_DIR = 9 ACTION_PARENT_DIR = 9
ACTION_PREVIOUS_MENU = 10 ACTION_PREVIOUS_MENU = 10
ACTION_BACK = 92 ACTION_BACK = 92
@ -27,10 +27,10 @@ CANCEL = 201
MESSAGE_BOX = 202 MESSAGE_BOX = 202
MESSAGE = 203 MESSAGE = 203
BUSY = 204 BUSY = 204
EMBY_CONNECT = 205 PLEX_CONNECT = 205
MANUAL_SERVER = 206 MANUAL_SERVER = 206
################################################################################################## ###############################################################################
class ServerConnect(xbmcgui.WindowXMLDialog): class ServerConnect(xbmcgui.WindowXMLDialog):
@ -43,13 +43,8 @@ class ServerConnect(xbmcgui.WindowXMLDialog):
_connect_login = False _connect_login = False
_manual_server = False _manual_server = False
def __init__(self, *args, **kwargs):
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
def set_args(self, **kwargs): def set_args(self, **kwargs):
# connect_manager, username, user_image, servers, emby_connect # connect_manager, username, user_image, servers, plex_connect
for key, value in kwargs.iteritems(): for key, value in kwargs.iteritems():
setattr(self, key, value) setattr(self, key, value)
@ -65,7 +60,6 @@ class ServerConnect(xbmcgui.WindowXMLDialog):
def is_manual_server(self): def is_manual_server(self):
return self._manual_server return self._manual_server
def onInit(self): def onInit(self):
self.message = self.getControl(MESSAGE) self.message = self.getControl(MESSAGE)
@ -74,7 +68,7 @@ class ServerConnect(xbmcgui.WindowXMLDialog):
self.list_ = self.getControl(LIST) self.list_ = self.getControl(LIST)
for server in self.servers: for server in self.servers:
server_type = "wifi" if server.get('ExchangeToken') else "network" server_type = "wifi" if server.get('local') == '0' else "network"
self.list_.addItem(self._add_listitem(server['Name'], server['Id'], server_type)) self.list_.addItem(self._add_listitem(server['Name'], server['Id'], server_type))
self.getControl(USER_NAME).setLabel("%s %s" % (lang(33000), self.username.decode('utf-8'))) self.getControl(USER_NAME).setLabel("%s %s" % (lang(33000), self.username.decode('utf-8')))
@ -82,8 +76,8 @@ class ServerConnect(xbmcgui.WindowXMLDialog):
if self.user_image is not None: if self.user_image is not None:
self.getControl(USER_IMAGE).setImage(self.user_image) self.getControl(USER_IMAGE).setImage(self.user_image)
if not self.emby_connect: # Change connect user if not self.plex_connect: # Change connect user
self.getControl(EMBY_CONNECT).setLabel("[UPPERCASE][B]"+lang(30618)+"[/B][/UPPERCASE]") self.getControl(PLEX_CONNECT).setLabel("[UPPERCASE][B]"+'plex.tv user change'+"[/B][/UPPERCASE]")
if self.servers: if self.servers:
self.setFocus(self.list_) self.setFocus(self.list_)
@ -115,7 +109,7 @@ class ServerConnect(xbmcgui.WindowXMLDialog):
def onClick(self, control): def onClick(self, control):
if control == EMBY_CONNECT: if control == PLEX_CONNECT:
self.connect_manager.clearData() self.connect_manager.clearData()
self._connect_login = True self._connect_login = True
self.close() self.close()

View file

@ -1,22 +1,16 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
###############################################################################
################################################################################################## from logging import getLogger
import logging
import os
import xbmcgui import xbmcgui
import xbmcaddon
import connect.connectionmanager as connectionmanager import connect.connectionmanager as connectionmanager
from utils import language as lang from utils import language as lang, tryDecode
################################################################################################## ###############################################################################
log = getLogger("PLEX."+__name__)
log = logging.getLogger("EMBY."+__name__) CONN_STATE = connectionmanager.CONNECTIONSTATE
addon = xbmcaddon.Addon('plugin.video.emby')
CONN_STATE = connectionmanager.ConnectionState
ACTION_PARENT_DIR = 9 ACTION_PARENT_DIR = 9
ACTION_PREVIOUS_MENU = 10 ACTION_PREVIOUS_MENU = 10
ACTION_BACK = 92 ACTION_BACK = 92
@ -24,23 +18,50 @@ CONNECT = 200
CANCEL = 201 CANCEL = 201
ERROR_TOGGLE = 202 ERROR_TOGGLE = 202
ERROR_MSG = 203 ERROR_MSG = 203
VERIFY_SSL = 204
HOST_SSL_PATH = 205
PMS_IP = 208
PMS_PORT = 209
ERROR = { ERROR = {
'Invalid': 1, 'Invalid': 1,
'Empty': 2 'Empty': 2
} }
###############################################################################
##################################################################################################
class ServerManual(xbmcgui.WindowXMLDialog): class ServerManual(xbmcgui.WindowXMLDialog):
_server = None _server = None
error = None error = None
def onInit(self):
self.connect_button = self.getControl(CONNECT)
self.cancel_button = self.getControl(CANCEL)
self.error_toggle = self.getControl(ERROR_TOGGLE)
self.error_msg = self.getControl(ERROR_MSG)
def __init__(self, *args, **kwargs): self.host_field = self.getControl(PMS_IP)
self.port_field = self.getControl(PMS_PORT)
self.verify_ssl_radio = self.getControl(VERIFY_SSL)
self.host_ssl_path_radio = self.getControl(HOST_SSL_PATH)
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) self.port_field.setText('32400')
self.setFocus(self.host_field)
self.verify_ssl_radio.setSelected(True)
self.host_ssl_path_radio.setSelected(False)
self.host_ssl_path = None
self.host_field.controlUp(self.cancel_button)
self.host_field.controlDown(self.port_field)
self.port_field.controlUp(self.host_field)
self.port_field.controlDown(self.verify_ssl_radio)
self.verify_ssl_radio.controlUp(self.port_field)
self.verify_ssl_radio.controlDown(self.host_ssl_path_radio)
self.host_ssl_path_radio.controlUp(self.verify_ssl_radio)
self.host_ssl_path_radio.controlDown(self.connect_button)
self.connect_button.controlUp(self.host_ssl_path_radio)
self.connect_button.controlDown(self.cancel_button)
self.cancel_button.controlUp(self.connect_button)
self.cancel_button.controlDown(self.host_field)
def set_connect_manager(self, connect_manager): def set_connect_manager(self, connect_manager):
self.connect_manager = connect_manager self.connect_manager = connect_manager
@ -51,29 +72,8 @@ class ServerManual(xbmcgui.WindowXMLDialog):
def get_server(self): def get_server(self):
return self._server return self._server
def onInit(self):
self.connect_button = self.getControl(CONNECT)
self.cancel_button = self.getControl(CANCEL)
self.error_toggle = self.getControl(ERROR_TOGGLE)
self.error_msg = self.getControl(ERROR_MSG)
self.host_field = self._add_editcontrol(725, 400, 40, 500)
self.port_field = self._add_editcontrol(725, 525, 40, 500)
self.port_field.setText('8096')
self.setFocus(self.host_field)
self.host_field.controlUp(self.cancel_button)
self.host_field.controlDown(self.port_field)
self.port_field.controlUp(self.host_field)
self.port_field.controlDown(self.connect_button)
self.connect_button.controlUp(self.port_field)
self.cancel_button.controlDown(self.host_field)
def onClick(self, control): def onClick(self, control):
if control == CONNECT: if control == CONNECT:
# Sign in to emby connect
self._disable_error() self._disable_error()
server = self.host_field.getText() server = self.host_field.getText()
@ -81,65 +81,66 @@ class ServerManual(xbmcgui.WindowXMLDialog):
if not server or not port: if not server or not port:
# Display error # Display error
self._error(ERROR['Empty'], lang(30617)) self._error(ERROR['Empty'], lang(30021))
log.error("Server or port cannot be null") log.error("Server or port cannot be null")
elif self._connect_to_server(server, port): elif self._connect_to_server(server, port):
self.close() self.close()
elif control == CANCEL: elif control == CANCEL:
# Remind me later
self.close() self.close()
elif control == HOST_SSL_PATH:
if self.host_ssl_path_radio.isSelected():
# Let the user choose path to the certificate (=file)
self.host_ssl_path = xbmcgui.Dialog().browse(
1, lang(29999), 'files', '', False, False, '', False)
log.debug('Host SSL file path chosen: %s' % self.host_ssl_path)
if not self.host_ssl_path:
self.host_ssl_path_radio.setSelected(False)
else:
self.host_ssl_path = tryDecode(self.host_ssl_path)
else:
# User disabled
# Ensure that we don't have a host certificate set
self.host_ssl_path = None
def onAction(self, action): def onAction(self, action):
if (self.error == ERROR['Empty'] and
if self.error == ERROR['Empty'] and self.host_field.getText() and self.port_field.getText(): self.host_field.getText() and self.port_field.getText()):
self._disable_error() self._disable_error()
if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU): if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU):
self.close() self.close()
def _add_editcontrol(self, x, y, height, width):
media = os.path.join(addon.getAddonInfo('path'), 'resources', 'skins', 'default', 'media')
control = xbmcgui.ControlEdit(0, 0, 0, 0,
label="User",
font="font10",
textColor="ffc2c2c2",
focusTexture=os.path.join(media, "button-focus.png"),
noFocusTexture=os.path.join(media, "button-focus.png"))
control.setPosition(x, y)
control.setHeight(height)
control.setWidth(width)
self.addControl(control)
return control
def _connect_to_server(self, server, port): def _connect_to_server(self, server, port):
"""Returns True if we could connect, False otherwise"""
server_address = "%s:%s" % (server, port) url = "%s:%s" % (server, port)
self._message("%s %s..." % (lang(30610), server_address)) self._message("%s %s..." % (lang(30023), url))
result = self.connect_manager.connectToAddress(server_address) options = {
'verify': True if self.verify_ssl_radio.isSelected() else False
}
if self.host_ssl_path:
options['cert'] = self.host_ssl_path
result = self.connect_manager.connectToAddress(url, options)
log.debug('Received the following results: %s' % result)
if result['State'] == CONN_STATE['Unavailable']: if result['State'] == CONN_STATE['Unavailable']:
self._message(lang(30609)) self._message(lang(30204))
return False return False
else: else:
self._server = result['Servers'][0] self._server = result['Servers'][0]
return True return True
def _message(self, message): def _message(self, message):
"""Displays a message popup just underneath the dialog"""
self.error_msg.setLabel(message) self.error_msg.setLabel(message)
self.error_toggle.setVisibleCondition('True') self.error_toggle.setVisibleCondition('True')
def _error(self, state, message): def _error(self, state, message):
"""Displays an error message just underneath the dialog"""
self.error = state self.error = state
self.error_msg.setLabel(message) self.error_msg.setLabel(message)
self.error_toggle.setVisibleCondition('True') self.error_toggle.setVisibleCondition('True')
def _disable_error(self): def _disable_error(self):
"""Disables the message popup just underneath the dialog"""
self.error = None self.error = None
self.error_toggle.setVisibleCondition('False') self.error_toggle.setVisibleCondition('False')

View file

@ -142,13 +142,16 @@ class DownloadUtils():
def downloadUrl(self, url, action_type="GET", postBody=None, def downloadUrl(self, url, action_type="GET", postBody=None,
parameters=None, authenticate=True, headerOptions=None, parameters=None, authenticate=True, headerOptions=None,
verifySSL=True, timeout=None, return_response=False): verifySSL=True, timeout=None, return_response=False,
auth=None):
""" """
Override SSL check with verifySSL=False Override SSL check with verifySSL=False
If authenticate=True, existing request session will be used/started If authenticate=True, existing request session will be used/started
Otherwise, 'empty' request will be made Otherwise, 'empty' request will be made
auth=None or auth=('user', 'password')
Returns: Returns:
None If an error occured None If an error occured
True If connection worked but no body was received True If connection worked but no body was received
@ -190,6 +193,8 @@ class DownloadUtils():
kwargs['params'] = parameters kwargs['params'] = parameters
if timeout is not None: if timeout is not None:
kwargs['timeout'] = timeout kwargs['timeout'] = timeout
if auth is not None:
kwargs['auth'] = auth
# ACTUAL DOWNLOAD HAPPENING HERE # ACTUAL DOWNLOAD HAPPENING HERE
try: try:

View file

@ -31,15 +31,15 @@ except IndexError:
############################################################################### ###############################################################################
def chooseServer(): def choose_server():
""" """
Lets user choose from list of PMS Lets user choose from list of PMS
""" """
log.info("Choosing PMS server requested, starting") log.info("Choosing PMS server requested, starting")
import initialsetup from connectmanager import ConnectManager
setup = initialsetup.InitialSetup() connectmanager = ConnectManager()
server = setup.PickPMS(showDialog=True) server = connectmanager.pick_pms(show_dialog=True)
if server is None: if server is None:
log.error('We did not connect to a new PMS, aborting') log.error('We did not connect to a new PMS, aborting')
plex_command('SUSPEND_USER_CLIENT', 'False') plex_command('SUSPEND_USER_CLIENT', 'False')
@ -47,7 +47,7 @@ def chooseServer():
return return
log.info("User chose server %s" % server['name']) log.info("User chose server %s" % server['name'])
setup.WritePMStoSettings(server) connectmanager.write_pms_to_settings(server)
if not __LogOut(): if not __LogOut():
return return
@ -85,8 +85,8 @@ def togglePlexTV():
plex_command('PLEX_USERNAME', '') plex_command('PLEX_USERNAME', '')
else: else:
log.info('Login to plex.tv') log.info('Login to plex.tv')
import initialsetup from connectmanager import ConnectManager
initialsetup.InitialSetup().PlexTVSignIn() ConnectManager().plex_tv_signin()
dialog('notification', dialog('notification',
lang(29999), lang(29999),
lang(39221), lang(39221),

View file

@ -1,406 +1,29 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
############################################################################### ###############################################################################
from logging import getLogger
from xbmc import executebuiltin
import logging from utils import settings, language as lang, advancedsettings_xml, dialog
import xbmc from connectmanager import ConnectManager
import xbmcgui
from utils import settings, window, language as lang, tryEncode, \
advancedsettings_xml
import downloadutils
from userclient import UserClient
from PlexAPI import PlexAPI
from PlexFunctions import GetMachineIdentifier, get_PMS_settings
import state import state
from migration import check_migration from migration import check_migration
############################################################################### ###############################################################################
log = getLogger("PLEX."+__name__)
log = logging.getLogger("PLEX."+__name__)
############################################################################### ###############################################################################
class InitialSetup(): def setup():
def __init__(self):
log.debug('Entering initialsetup class')
self.doUtils = downloadutils.DownloadUtils().downloadUrl
self.plx = PlexAPI()
self.dialog = xbmcgui.Dialog()
self.server = UserClient().getServer()
self.serverid = settings('plex_machineIdentifier')
# Get Plex credentials from settings file, if they exist
plexdict = self.plx.GetPlexLoginFromSettings()
self.myplexlogin = plexdict['myplexlogin'] == 'true'
self.plexLogin = plexdict['plexLogin']
self.plexToken = plexdict['plexToken']
self.plexid = plexdict['plexid']
# Token for the PMS, not plex.tv
self.pms_token = settings('accessToken')
if self.plexToken:
log.debug('Found a plex.tv token in the settings')
def PlexTVSignIn(self):
"""
Signs (freshly) in to plex.tv (will be saved to file settings)
Returns True if successful, or False if not
"""
result = self.plx.PlexTvSignInWithPin()
if result:
self.plexLogin = result['username']
self.plexToken = result['token']
self.plexid = result['plexid']
return True
return False
def CheckPlexTVSignIn(self):
"""
Checks existing connection to plex.tv. If not, triggers sign in
Returns True if signed in, False otherwise
"""
answer = True
chk = self.plx.CheckConnection('plex.tv', token=self.plexToken)
if chk in (401, 403):
# HTTP Error: unauthorized. Token is no longer valid
log.info('plex.tv connection returned HTTP %s' % str(chk))
# Delete token in the settings
settings('plexToken', value='')
settings('plexLogin', value='')
# Could not login, please try again
self.dialog.ok(lang(29999), lang(39009))
answer = self.PlexTVSignIn()
elif chk is False or chk >= 400:
# Problems connecting to plex.tv. Network or internet issue?
log.info('Problems connecting to plex.tv; connection returned '
'HTTP %s' % str(chk))
self.dialog.ok(lang(29999), lang(39010))
answer = False
else:
log.info('plex.tv connection with token successful')
settings('plex_status', value=lang(39227))
# Refresh the info from Plex.tv
xml = self.doUtils('https://plex.tv/users/account',
authenticate=False,
headerOptions={'X-Plex-Token': self.plexToken})
try:
self.plexLogin = xml.attrib['title']
except (AttributeError, KeyError):
log.error('Failed to update Plex info from plex.tv')
else:
settings('plexLogin', value=self.plexLogin)
home = 'true' if xml.attrib.get('home') == '1' else 'false'
settings('plexhome', value=home)
settings('plexAvatar', value=xml.attrib.get('thumb'))
settings('plexHomeSize', value=xml.attrib.get('homeSize', '1'))
log.info('Updated Plex info from plex.tv')
return answer
def CheckPMS(self):
"""
Check the PMS that was set in file settings.
Will return False if we need to reconnect, because:
PMS could not be reached (no matter the authorization)
machineIdentifier did not match
Will also set the PMS machineIdentifier in the file settings if it was
not set before
"""
answer = True
chk = self.plx.CheckConnection(self.server, verifySSL=False)
if chk is False:
log.warn('Could not reach PMS %s' % self.server)
answer = False
if answer is True and not self.serverid:
log.info('No PMS machineIdentifier found for %s. Trying to '
'get the PMS unique ID' % self.server)
self.serverid = GetMachineIdentifier(self.server)
if self.serverid is None:
log.warn('Could not retrieve machineIdentifier')
answer = False
else:
settings('plex_machineIdentifier', value=self.serverid)
elif answer is True:
tempServerid = GetMachineIdentifier(self.server)
if tempServerid != self.serverid:
log.warn('The current PMS %s was expected to have a '
'unique machineIdentifier of %s. But we got '
'%s. Pick a new server to be sure'
% (self.server, self.serverid, tempServerid))
answer = False
return answer
def _getServerList(self):
"""
Returns a list of servers from GDM and possibly plex.tv
"""
self.plx.discoverPMS(xbmc.getIPAddress(),
plexToken=self.plexToken)
serverlist = self.plx.returnServerList(self.plx.g_PMS)
log.debug('PMS serverlist: %s' % serverlist)
return serverlist
def _checkServerCon(self, server):
"""
Checks for server's connectivity. Returns CheckConnection result
"""
# Re-direct via plex if remote - will lead to the correct SSL
# certificate
if server['local'] == '1':
url = '%s://%s:%s' \
% (server['scheme'], server['ip'], server['port'])
# Deactive SSL verification if the server is local!
verifySSL = False
else:
url = server['baseURL']
verifySSL = True
chk = self.plx.CheckConnection(url,
token=server['accesstoken'],
verifySSL=verifySSL)
return chk
def PickPMS(self, showDialog=False):
"""
Searches for PMS in local Lan and optionally (if self.plexToken set)
also on plex.tv
showDialog=True: let the user pick one
showDialog=False: automatically pick PMS based on machineIdentifier
Returns the picked PMS' detail as a dict:
{
'name': friendlyName, the Plex server's name
'address': ip:port
'ip': ip, without http/https
'port': port
'scheme': 'http'/'https', nice for checking for secure connections
'local': '1'/'0', Is the server a local server?
'owned': '1'/'0', Is the server owned by the user?
'machineIdentifier': id, Plex server machine identifier
'accesstoken': token Access token to this server
'baseURL': baseURL scheme://ip:port
'ownername' Plex username of PMS owner
}
or None if unsuccessful
"""
server = None
# If no server is set, let user choose one
if not self.server or not self.serverid:
showDialog = True
if showDialog is True:
server = self._UserPickPMS()
else:
server = self._AutoPickPMS()
if server is not None:
self._write_PMS_settings(server['baseURL'], server['accesstoken'])
return server
def _write_PMS_settings(self, url, token):
"""
Sets certain settings for server by asking for the PMS' settings
Call with url: scheme://ip:port
"""
xml = get_PMS_settings(url, token)
try:
xml.attrib
except AttributeError:
log.error('Could not get PMS settings for %s' % url)
return
for entry in xml:
if entry.attrib.get('id', '') == 'allowMediaDeletion':
settings('plex_allows_mediaDeletion',
value=entry.attrib.get('value', 'true'))
window('plex_allows_mediaDeletion',
value=entry.attrib.get('value', 'true'))
def _AutoPickPMS(self):
"""
Will try to pick PMS based on machineIdentifier saved in file settings
but only once
Returns server or None if unsuccessful
"""
httpsUpdated = False
checkedPlexTV = False
server = None
while True:
if httpsUpdated is False:
serverlist = self._getServerList()
for item in serverlist:
if item.get('machineIdentifier') == self.serverid:
server = item
if server is None:
name = settings('plex_servername')
log.warn('The PMS you have used before with a unique '
'machineIdentifier of %s and name %s is '
'offline' % (self.serverid, name))
return
chk = self._checkServerCon(server)
if chk == 504 and httpsUpdated is False:
# Not able to use HTTP, try HTTPs for now
server['scheme'] = 'https'
httpsUpdated = True
continue
if chk == 401:
log.warn('Not yet authorized for Plex server %s'
% server['name'])
if self.CheckPlexTVSignIn() is True:
if checkedPlexTV is False:
# Try again
checkedPlexTV = True
httpsUpdated = False
continue
else:
log.warn('Not authorized even though we are signed '
' in to plex.tv correctly')
self.dialog.ok(lang(29999), '%s %s'
% (lang(39214),
tryEncode(server['name'])))
return
else:
return
# Problems connecting
elif chk >= 400 or chk is False:
log.warn('Problems connecting to server %s. chk is %s'
% (server['name'], chk))
return
log.info('We found a server to automatically connect to: %s'
% server['name'])
return server
def _UserPickPMS(self):
"""
Lets user pick his/her PMS from a list
Returns server or None if unsuccessful
"""
httpsUpdated = False
while True:
if httpsUpdated is False:
serverlist = self._getServerList()
# Exit if no servers found
if len(serverlist) == 0:
log.warn('No plex media servers found!')
self.dialog.ok(lang(29999), lang(39011))
return
# Get a nicer list
dialoglist = []
for server in serverlist:
if server['local'] == '1':
# server is in the same network as client.
# Add"local"
msg = lang(39022)
else:
# Add 'remote'
msg = lang(39054)
if server.get('ownername'):
# Display username if its not our PMS
dialoglist.append('%s (%s, %s)'
% (server['name'],
server['ownername'],
msg))
else:
dialoglist.append('%s (%s)'
% (server['name'], msg))
# Let user pick server from a list
resp = self.dialog.select(lang(39012), dialoglist)
if resp == -1:
# User cancelled
return
server = serverlist[resp]
chk = self._checkServerCon(server)
if chk == 504 and httpsUpdated is False:
# Not able to use HTTP, try HTTPs for now
serverlist[resp]['scheme'] = 'https'
httpsUpdated = True
continue
httpsUpdated = False
if chk == 401:
log.warn('Not yet authorized for Plex server %s'
% server['name'])
# Please sign in to plex.tv
self.dialog.ok(lang(29999),
lang(39013) + server['name'],
lang(39014))
if self.PlexTVSignIn() is False:
# Exit while loop if user cancels
return
# Problems connecting
elif chk >= 400 or chk is False:
# Problems connecting to server. Pick another server?
answ = self.dialog.yesno(lang(29999),
lang(39015))
# Exit while loop if user chooses No
if not answ:
return
# Otherwise: connection worked!
else:
return server
def WritePMStoSettings(self, server):
"""
Saves server to file settings. server is a dict of the form:
{
'name': friendlyName, the Plex server's name
'address': ip:port
'ip': ip, without http/https
'port': port
'scheme': 'http'/'https', nice for checking for secure connections
'local': '1'/'0', Is the server a local server?
'owned': '1'/'0', Is the server owned by the user?
'machineIdentifier': id, Plex server machine identifier
'accesstoken': token Access token to this server
'baseURL': baseURL scheme://ip:port
'ownername' Plex username of PMS owner
}
"""
settings('plex_machineIdentifier', server['machineIdentifier'])
settings('plex_servername', server['name'])
settings('plex_serverowned',
'true' if server['owned'] == '1'
else 'false')
# Careful to distinguish local from remote PMS
if server['local'] == '1':
scheme = server['scheme']
settings('ipaddress', server['ip'])
settings('port', server['port'])
log.debug("Setting SSL verify to false, because server is "
"local")
settings('sslverify', 'false')
else:
baseURL = server['baseURL'].split(':')
scheme = baseURL[0]
settings('ipaddress', baseURL[1].replace('//', ''))
settings('port', baseURL[2])
log.debug("Setting SSL verify to true, because server is not "
"local")
settings('sslverify', 'true')
if scheme == 'https':
settings('https', 'true')
else:
settings('https', 'false')
# And finally do some logging
log.debug("Writing to Kodi user settings file")
log.debug("PMS machineIdentifier: %s, ip: %s, port: %s, https: %s "
% (server['machineIdentifier'], server['ip'],
server['port'], server['scheme']))
def setup(self):
""" """
Initial setup. Run once upon startup. Initial setup. Run once upon startup.
Check server, user, direct paths, music, direct stream if not direct Check server, user, direct paths, music, direct stream if not direct
path. path.
""" """
log.info("Initial setup called.") log.info("Initial setup called")
dialog = self.dialog connectmanager = ConnectManager()
# Get current Kodi video cache setting # Get current Kodi video cache setting
cache, _ = advancedsettings_xml(['cache', 'memorysize']) cache, _ = advancedsettings_xml(['cache', 'memorysize'])
@ -418,28 +41,31 @@ class InitialSetup():
# Optionally sign into plex.tv. Will not be called on very first run # Optionally sign into plex.tv. Will not be called on very first run
# as plexToken will be '' # as plexToken will be ''
settings('plex_status', value=lang(39226)) settings('plex_status', value=lang(39226))
if self.plexToken and self.myplexlogin: if connectmanager.plexToken and connectmanager.myplexlogin:
self.CheckPlexTVSignIn() connectmanager.check_plex_tv_signin()
# If a Plex server IP has already been set # If a Plex server IP has already been set
# return only if the right machine identifier is found # return only if the right machine identifier is found
if self.server: if connectmanager.server:
log.info("PMS is already set: %s. Checking now..." % self.server) log.info("PMS is already set: %s. Checking now..."
if self.CheckPMS(): % connectmanager.server)
if connectmanager.check_pms():
log.info("Using PMS %s with machineIdentifier %s" log.info("Using PMS %s with machineIdentifier %s"
% (self.server, self.serverid)) % (connectmanager.server, connectmanager.serverid))
self._write_PMS_settings(self.server, self.pms_token) connectmanager.write_pms_settings(connectmanager.server,
connectmanager.pms_token)
connectmanager.pick_pms(show_dialog=True)
return return
# If not already retrieved myplex info, optionally let user sign in # If not already retrieved myplex info, optionally let user sign in
# to plex.tv. This DOES get called on very first install run # to plex.tv. This DOES get called on very first install run
if not self.plexToken and self.myplexlogin: if not connectmanager.plexToken and connectmanager.myplexlogin:
self.PlexTVSignIn() connectmanager.plex_tv_signin()
server = self.PickPMS() server = connectmanager.connectmanager.pick_pms()
if server is not None: if server is not None:
# Write our chosen server to Kodi settings file # Write our chosen server to Kodi settings file
self.WritePMStoSettings(server) connectmanager.write_pms_to_settings(server)
# User already answered the installation questions # User already answered the installation questions
if settings('InstallQuestionsAnswered') == 'true': if settings('InstallQuestionsAnswered') == 'true':
@ -448,7 +74,8 @@ class InitialSetup():
# Additional settings where the user needs to choose # Additional settings where the user needs to choose
# Direct paths (\\NAS\mymovie.mkv) or addon (http)? # Direct paths (\\NAS\mymovie.mkv) or addon (http)?
goToSettings = False goToSettings = False
if dialog.yesno(lang(29999), if dialog('yesno',
lang(29999),
lang(39027), lang(39027),
lang(39028), lang(39028),
nolabel="Addon (Default)", nolabel="Addon (Default)",
@ -458,55 +85,55 @@ class InitialSetup():
state.DIRECT_PATHS = True state.DIRECT_PATHS = True
# Are you on a system where you would like to replace paths # Are you on a system where you would like to replace paths
# \\NAS\mymovie.mkv with smb://NAS/mymovie.mkv? (e.g. Windows) # \\NAS\mymovie.mkv with smb://NAS/mymovie.mkv? (e.g. Windows)
if dialog.yesno(heading=lang(29999), line1=lang(39033)): if dialog('yesno', heading=lang(29999), line1=lang(39033)):
log.debug("User chose to replace paths with smb") log.debug("User chose to replace paths with smb")
else: else:
settings('replaceSMB', value="false") settings('replaceSMB', value="false")
# complete replace all original Plex library paths with custom SMB # complete replace all original Plex library paths with custom SMB
if dialog.yesno(heading=lang(29999), line1=lang(39043)): if dialog('yesno', heading=lang(29999), line1=lang(39043)):
log.debug("User chose custom smb paths") log.debug("User chose custom smb paths")
settings('remapSMB', value="true") settings('remapSMB', value="true")
# Please enter your custom smb paths in the settings under # Please enter your custom smb paths in the settings under
# "Sync Options" and then restart Kodi # "Sync Options" and then restart Kodi
dialog.ok(heading=lang(29999), line1=lang(39044)) dialog('ok', heading=lang(29999), line1=lang(39044))
goToSettings = True goToSettings = True
# Go to network credentials? # Go to network credentials?
if dialog.yesno(heading=lang(29999), if dialog('yesno',
heading=lang(29999),
line1=lang(39029), line1=lang(39029),
line2=lang(39030)): line2=lang(39030)):
log.debug("Presenting network credentials dialog.") log.debug("Presenting network credentials dialog.")
from utils import passwordsXML from utils import passwordsXML
passwordsXML() passwordsXML()
# Disable Plex music? # Disable Plex music?
if dialog.yesno(heading=lang(29999), line1=lang(39016)): if dialog('yesno', heading=lang(29999), line1=lang(39016)):
log.debug("User opted to disable Plex music library.") log.debug("User opted to disable Plex music library.")
settings('enableMusic', value="false") settings('enableMusic', value="false")
# Download additional art from FanArtTV # Download additional art from FanArtTV
if dialog.yesno(heading=lang(29999), line1=lang(39061)): if dialog('yesno', heading=lang(29999), line1=lang(39061)):
log.debug("User opted to use FanArtTV") log.debug("User opted to use FanArtTV")
settings('FanartTV', value="true") settings('FanartTV', value="true")
# Do you want to replace your custom user ratings with an indicator of # Do you want to replace your custom user ratings with an indicator of
# how many versions of a media item you posses? # how many versions of a media item you posses?
if dialog.yesno(heading=lang(29999), line1=lang(39718)): if dialog('yesno', heading=lang(29999), line1=lang(39718)):
log.debug("User opted to replace user ratings with version number") log.debug("User opted to replace user ratings with version number")
settings('indicate_media_versions', value="true") settings('indicate_media_versions', value="true")
# If you use several Plex libraries of one kind, e.g. "Kids Movies" and # If you use several Plex libraries of one kind, e.g. "Kids Movies" and
# "Parents Movies", be sure to check https://goo.gl/JFtQV9 # "Parents Movies", be sure to check https://goo.gl/JFtQV9
dialog.ok(heading=lang(29999), line1=lang(39076)) dialog('ok', heading=lang(29999), line1=lang(39076))
# Need to tell about our image source for collections: themoviedb.org # Need to tell about our image source for collections: themoviedb.org
dialog.ok(heading=lang(29999), line1=lang(39717)) dialog('ok', heading=lang(29999), line1=lang(39717))
# Make sure that we only ask these questions upon first installation # Make sure that we only ask these questions upon first installation
settings('InstallQuestionsAnswered', value='true') settings('InstallQuestionsAnswered', value='true')
if goToSettings is False: if goToSettings is False:
# Open Settings page now? You will need to restart! # Open Settings page now? You will need to restart!
goToSettings = dialog.yesno(heading=lang(29999), line1=lang(39017)) goToSettings = dialog('yesno', heading=lang(29999), line1=lang(39017))
if goToSettings: if goToSettings:
state.PMS_STATUS = 'Stop' state.PMS_STATUS = 'Stop'
xbmc.executebuiltin( executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)')
'Addon.OpenSettings(plugin.video.plexkodiconnect)')

View file

@ -29,6 +29,8 @@ INDICATE_MEDIA_VERSIONS = False
AUTHENTICATED = False AUTHENTICATED = False
# plex.tv username # plex.tv username
PLEX_USERNAME = None PLEX_USERNAME = None
# plex.tv image for this user
PLEX_USER_IMAGE = None
# Token for that user for plex.tv # Token for that user for plex.tv
PLEX_TOKEN = None PLEX_TOKEN = None
# Plex ID of that user (e.g. for plex.tv) as a STRING # Plex ID of that user (e.g. for plex.tv) as a STRING
@ -36,3 +38,6 @@ PLEX_USER_ID = None
# Token passed along, e.g. if playback initiated by Plex Companion. Might be # Token passed along, e.g. if playback initiated by Plex Companion. Might be
# another user playing something! Token identifies user # another user playing something! Token identifies user
PLEX_TRANSIENT_TOKEN = None PLEX_TRANSIENT_TOKEN = None
# Used by connectmanager.py
CONNECT_STATE = {}

View file

@ -14,7 +14,9 @@ from utils import window, settings, language as lang, thread_methods
import downloadutils import downloadutils
import PlexAPI import PlexAPI
from PlexFunctions import GetMachineIdentifier from connect.plex_tv import get_user_artwork_url
from PlexFunctions import GetMachineIdentifier, check_connection
import state import state
############################################################################### ###############################################################################
@ -106,9 +108,10 @@ class UserClient(threading.Thread):
log.debug('Setting user preferences') log.debug('Setting user preferences')
# Only try to get user avatar if there is a token # Only try to get user avatar if there is a token
if self.currToken: if self.currToken:
url = PlexAPI.PlexAPI().GetUserArtworkURL(self.currUser) url = get_user_artwork_url(self.currUser)
if url: if url:
window('PlexUserImage', value=url) window('PlexUserImage', value=url)
state.PLEX_USER_IMAGE = url
# Set resume point max # Set resume point max
# url = "{server}/emby/System/Configuration?format=json" # url = "{server}/emby/System/Configuration?format=json"
# result = doUtils.downloadUrl(url) # result = doUtils.downloadUrl(url)
@ -130,7 +133,7 @@ class UserClient(threading.Thread):
if self.currServer is None: if self.currServer is None:
return False return False
log.debug('Testing validity of current token') log.debug('Testing validity of current token')
res = PlexAPI.PlexAPI().CheckConnection(self.currServer, res = check_connection(self.currServer,
token=self.currToken, token=self.currToken,
verifySSL=self.ssl) verifySSL=self.ssl)
if res is False: if res is False:
@ -280,6 +283,7 @@ class UserClient(threading.Thread):
window('plex_servername', clear=True) window('plex_servername', clear=True)
state.PLEX_USER_ID = None state.PLEX_USER_ID = None
state.PLEX_USERNAME = None state.PLEX_USERNAME = None
state.PLEX_USER_IMAGE = None
window('plex_restricteduser', clear=True) window('plex_restricteduser', clear=True)
state.RESTRICTED_USER = False state.RESTRICTED_USER = False

View file

@ -34,6 +34,26 @@ log = logging.getLogger("PLEX."+__name__)
WINDOW = xbmcgui.Window(10000) WINDOW = xbmcgui.Window(10000)
ADDON = xbmcaddon.Addon(id='plugin.video.plexkodiconnect') ADDON = xbmcaddon.Addon(id='plugin.video.plexkodiconnect')
# For use with xbmcgui.dialog
ICONS = {
'{plex}': 'special://home/addons/plugin.video.plexkodiconnect/icon.png',
'{info}': xbmcgui.NOTIFICATION_INFO,
'{warning}': xbmcgui.NOTIFICATION_WARNING,
'{error}': xbmcgui.NOTIFICATION_ERROR
}
TYPES = {
'{alphanum}': xbmcgui.INPUT_ALPHANUM,
'{numeric}': xbmcgui.INPUT_NUMERIC,
'{date}': xbmcgui.INPUT_DATE,
'{time}': xbmcgui.INPUT_TIME,
'{ipaddress}': xbmcgui.INPUT_IPADDRESS,
'{password}': xbmcgui.INPUT_PASSWORD
}
OPTIONS = {
'{hide_input}': xbmcgui.ALPHANUM_HIDE_INPUT,
'{password_verify}': xbmcgui.PASSWORD_VERIFY
}
############################################################################### ###############################################################################
# Main methods # Main methods
@ -154,27 +174,19 @@ def dialog(typus, *args, **kwargs):
type='{ipaddress}' xbmcgui.INPUT_IPADDRESS (format: #.#.#.#) type='{ipaddress}' xbmcgui.INPUT_IPADDRESS (format: #.#.#.#)
type='{password}' xbmcgui.INPUT_PASSWORD type='{password}' xbmcgui.INPUT_PASSWORD
(return md5 hash of input, input is masked) (return md5 hash of input, input is masked)
Input Options:
option='{hide_input}': xbmcgui.ALPHANUM_HIDE_INPUT
option='{password_verify}': xbmcgui.PASSWORD_VERIFY
""" """
d = xbmcgui.Dialog() d = xbmcgui.Dialog()
if "icon" in kwargs: if "icon" in kwargs:
types = { for key, value in ICONS.iteritems():
'{plex}': 'special://home/addons/plugin.video.plexkodiconnect/icon.png',
'{info}': xbmcgui.NOTIFICATION_INFO,
'{warning}': xbmcgui.NOTIFICATION_WARNING,
'{error}': xbmcgui.NOTIFICATION_ERROR
}
for key, value in types.iteritems():
kwargs['icon'] = kwargs['icon'].replace(key, value) kwargs['icon'] = kwargs['icon'].replace(key, value)
if 'type' in kwargs: if 'type' in kwargs:
types = { kwargs['type'] = TYPES[kwargs['type']]
'{alphanum}': xbmcgui.INPUT_ALPHANUM, if 'option' in kwargs:
'{numeric}': xbmcgui.INPUT_NUMERIC, kwargs['option'] = OPTIONS[kwargs['option']]
'{date}': xbmcgui.INPUT_DATE,
'{time}': xbmcgui.INPUT_TIME,
'{ipaddress}': xbmcgui.INPUT_IPADDRESS,
'{password}': xbmcgui.INPUT_PASSWORD
}
kwargs['type'] = types[kwargs['type']]
if "heading" in kwargs: if "heading" in kwargs:
kwargs['heading'] = kwargs['heading'].replace("{plex}", kwargs['heading'] = kwargs['heading'].replace("{plex}",
language(29999)) language(29999))

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import xbmc import xbmc
from xbmcaddon import Addon from xbmcaddon import Addon
from os.path import join
# Paths are in unicode, otherwise Windows will throw fits # Paths are in unicode, otherwise Windows will throw fits
# For any file operations with KODI function, use encoded strings! # For any file operations with KODI function, use encoded strings!
@ -26,11 +27,14 @@ _ADDON = Addon()
ADDON_NAME = 'PlexKodiConnect' ADDON_NAME = 'PlexKodiConnect'
ADDON_ID = 'plugin.video.plexkodiconnect' ADDON_ID = 'plugin.video.plexkodiconnect'
ADDON_VERSION = _ADDON.getAddonInfo('version') ADDON_VERSION = _ADDON.getAddonInfo('version')
ADDON_PATH = tryDecode(_ADDON.getAddonInfo('path'))
MEDIA_PATH = join(ADDON_PATH, 'resources', 'skins', 'default', 'media')
KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1) KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1)
KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2]) KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
KODILONGVERSION = xbmc.getInfoLabel('System.BuildVersion') KODILONGVERSION = xbmc.getInfoLabel('System.BuildVersion')
KODI_PROFILE = tryDecode(xbmc.translatePath("special://profile")) KODI_PROFILE = tryDecode(xbmc.translatePath("special://profile"))
ADDON_PATH_DATA = join(KODI_PROFILE, 'addon_data', ADDON_ID, '')
if xbmc.getCondVisibility('system.platform.osx'): if xbmc.getCondVisibility('system.platform.osx'):
PLATFORM = "MacOSX" PLATFORM = "MacOSX"

View file

@ -1,179 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<window>
<defaultcontrol always="true">200</defaultcontrol>
<zorder>0</zorder>
<include>dialogeffect</include>
<controls>
<control type="image">
<description>Background fade</description>
<width>100%</width>
<height>100%</height>
<texture>emby-bg-fade.png</texture>
</control>
<control type="group">
<width>600</width>
<left>35%</left>
<top>15%</top>
<control type="image">
<description>Background box</description>
<texture colordiffuse="ff111111">white.png</texture>
<width>600</width>
<height>700</height>
</control>
<control type="group" id="202">
<top>705</top>
<visible>False</visible>
<control type="image">
<description>Error box</description>
<texture colordiffuse="ff222222">white.png</texture>
<width>100%</width>
<height>50</height>
</control>
<control type="label" id="203">
<description>Error message</description>
<textcolor>white</textcolor>
<font>font10</font>
<aligny>center</aligny>
<align>center</align>
<height>50</height>
</control>
</control>
<control type="image">
<description>Emby logo</description>
<texture>logo-white.png</texture>
<width>160</width>
<height>49</height>
<top>30</top>
<left>25</left>
</control>
<control type="group">
<width>500</width>
<left>50</left>
<control type="label">
<description>Sign in emby connect</description>
<label>$ADDON[plugin.video.emby 30600]</label>
<textcolor>white</textcolor>
<font>font12</font>
<aligny>top</aligny>
<top>115</top>
</control>
<control type="group">
<top>190</top>
<control type="label">
<description>Username email</description>
<label>$ADDON[plugin.video.emby 30543]</label>
<textcolor>ffa6a6a6</textcolor>
<font>font10</font>
<aligny>top</aligny>
</control>
<control type="image">
<description>separator</description>
<width>102%</width>
<height>0.5</height>
<top>66</top>
<left>-10</left>
<texture colordiffuse="ff525252" border="90,3,90,3">emby-separator.png</texture>
</control>
</control>
<control type="group">
<description>Password</description>
<top>275</top>
<control type="label">
<description>Password label</description>
<label>$ADDON[plugin.video.emby 30602]</label>
<textcolor>ffa6a6a6</textcolor>
<font>font10</font>
<aligny>top</aligny>
</control>
<control type="image">
<description>separator</description>
<width>102%</width>
<height>0.5</height>
<top>66</top>
<left>-10</left>
<texture colordiffuse="ff525252" border="90,3,90,3">emby-separator.png</texture>
</control>
</control>
<control type="group">
<description>Buttons</description>
<top>385</top>
<control type="button" id="200">
<description>Sign in</description>
<texturenofocus border="5" colordiffuse="ff0b8628">box.png</texturenofocus>
<texturefocus border="5" colordiffuse="ff13a134">box.png</texturefocus>
<label>[UPPERCASE][B]$ADDON[plugin.video.emby 30605][/B][/UPPERCASE]</label>
<font>font10</font>
<textcolor>ffa6a6a6</textcolor>
<focusedcolor>white</focusedcolor>
<align>center</align>
<width>100%</width>
<height>50</height>
<ondown>201</ondown>
</control>
<control type="button" id="201">
<description>Cancel</description>
<texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus>
<texturefocus border="5" colordiffuse="ff525252">box.png</texturefocus>
<label>[UPPERCASE][B]$ADDON[plugin.video.emby 30606][/B][/UPPERCASE]</label>
<font>font10</font>
<textcolor>ffa6a6a6</textcolor>
<focusedcolor>white</focusedcolor>
<align>center</align>
<width>100%</width>
<height>50</height>
<top>55</top>
<onup>200</onup>
</control>
</control>
<control type="group">
<description>Disclaimer</description>
<top>510</top>
<control type="label">
<description>Disclaimer label</description>
<label>$ADDON[plugin.video.emby 30603]</label>
<font>font10</font>
<textcolor>ff464646</textcolor>
<wrapmultiline>true</wrapmultiline>
<aligny>top</aligny>
<width>340</width>
<height>100%</height>
</control>
<control type="group">
<control type="label">
<description>Scan me</description>
<label>[UPPERCASE]$ADDON[plugin.video.emby 30604][/UPPERCASE]</label>
<font>font12</font>
<textcolor>ff0b8628</textcolor>
<aligny>top</aligny>
<width>200</width>
<top>120</top>
<left>230</left>
</control>
<control type="image">
<description>qrcode</description>
<texture>qrcode_disclaimer.png</texture>
<width>140</width>
<height>140</height>
<top>10</top>
<left>360</left>
</control>
</control>
</control>
</control>
</control>
</controls>
</window>

View file

@ -1,154 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<window>
<defaultcontrol always="true">200</defaultcontrol>
<zorder>0</zorder>
<include>dialogeffect</include>
<controls>
<control type="image">
<description>Background fade</description>
<width>100%</width>
<height>100%</height>
<texture>emby-bg-fade.png</texture>
</control>
<control type="group">
<width>600</width>
<left>35%</left>
<top>20%</top>
<control type="image">
<description>Background box</description>
<texture colordiffuse="ff111111">white.png</texture>
<width>600</width>
<height>525</height>
</control>
<control type="group" id="202">
<top>530</top>
<visible>False</visible>
<control type="image">
<description>Error box</description>
<texture colordiffuse="ff222222">white.png</texture>
<width>100%</width>
<height>50</height>
</control>
<control type="label" id="203">
<description>Error message</description>
<textcolor>white</textcolor>
<font>font10</font>
<aligny>center</aligny>
<align>center</align>
<height>50</height>
</control>
</control>
<control type="image">
<description>Emby logo</description>
<texture>logo-white.png</texture>
<aspectratio>keep</aspectratio>
<width>120</width>
<height>49</height>
<top>30</top>
<left>25</left>
</control>
<control type="group">
<width>500</width>
<left>50</left>
<control type="label">
<description>Connect to server</description>
<label>$ADDON[plugin.video.emby 30614]</label>
<textcolor>white</textcolor>
<font>font12</font>
<aligny>top</aligny>
<align>center</align>
<width>100%</width>
<top>100</top>
</control>
<control type="group">
<top>150</top>
<control type="label">
<description>Host</description>
<label>$ADDON[plugin.video.emby 30615]</label>
<textcolor>ffa6a6a6</textcolor>
<font>font10</font>
<aligny>top</aligny>
</control>
<control type="image">
<description>separator</description>
<width>102%</width>
<height>0.5</height>
<top>66</top>
<left>-10</left>
<texture colordiffuse="ff525252" border="90,3,90,3">emby-separator.png</texture>
</control>
<control type="label">
<description>Host example</description>
<label>192.168.1.100 or https://myserver.com</label>
<textcolor>ff464646</textcolor>
<font>font10</font>
<aligny>top</aligny>
<top>70</top>
</control>
</control>
<control type="group">
<description>Port</description>
<top>275</top>
<control type="label">
<description>Port label</description>
<label>$ADDON[plugin.video.emby 30030]</label>
<textcolor>ffa6a6a6</textcolor>
<font>font10</font>
<aligny>top</aligny>
</control>
<control type="image">
<description>separator</description>
<width>102%</width>
<height>0.5</height>
<top>66</top>
<left>-10</left>
<texture colordiffuse="ff525252" border="90,3,90,3">emby-separator.png</texture>
</control>
</control>
<control type="group">
<description>Buttons</description>
<top>380</top>
<control type="button" id="200">
<description>Connect</description>
<texturenofocus border="5" colordiffuse="ff0b8628">box.png</texturenofocus>
<texturefocus border="5" colordiffuse="ff13a134">box.png</texturefocus>
<label>[UPPERCASE][B]$ADDON[plugin.video.emby 30616][/B][/UPPERCASE]</label>
<font>font10</font>
<textcolor>ffa6a6a6</textcolor>
<focusedcolor>white</focusedcolor>
<align>center</align>
<width>100%</width>
<height>50</height>
<ondown>201</ondown>
</control>
<control type="button" id="201">
<description>Cancel</description>
<texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus>
<texturefocus border="5" colordiffuse="ff525252">box.png</texturefocus>
<label>[UPPERCASE][B]$ADDON[plugin.video.emby 30606][/B][/UPPERCASE]</label>
<font>font10</font>
<textcolor>ffa6a6a6</textcolor>
<focusedcolor>white</focusedcolor>
<align>center</align>
<width>100%</width>
<height>50</height>
<top>55</top>
<onup>200</onup>
</control>
</control>
</control>
</control>
</controls>
</window>

View file

@ -0,0 +1,148 @@
<?xml version="1.0" encoding="UTF-8"?>
<window>
<defaultcontrol always="true">200</defaultcontrol>
<controls>
<control type="image">
<description>Background fade</description>
<width>100%</width>
<height>100%</height>
<texture>emby-bg-fade.png</texture>
</control>
<control type="group">
<centerleft>50%</centerleft>
<centertop>50%</centertop>
<width>560</width>
<height>656</height>
<textcolor>white</textcolor>
<control type="image">
<top>-20</top>
<left>-20</left>
<description>Background box</description>
<texture colordiffuse="ff111111">white.png</texture>
<width>600</width>
<height>636</height>
</control>
<control type="image">
<description>Plex logo</description>
<texture>logo-white.png</texture>
<width>64</width>
<height>64</height>
<aspectratio>keep</aspectratio>
</control>
<control type="label">
<description>Connect to PMS</description>
<label>$ADDON[plugin.video.plexkodiconnect 30018]</label>
<left>84</left>
<width>476</width>
<height>64</height>
<align>left</align>
<aligny>center</aligny>
<font>font12</font>
</control>
<control type="grouplist">
<description>Container for spacing</description>
<top>80</top>
<left>0</left>
<height>400</height>
<width>560</width>
<aligny>top</aligny>
<control type="label">
<height>50</height>
<width>560</width>
<description>PMS IP address or host name</description>
<label>$ADDON[plugin.video.plexkodiconnect 30019]:</label>
<font>font10</font>
</control>
<control type="edit" id="208">
<height>50</height>
<width>560</width>
<hinttext>https://192.168.1.2, https://myserver.com</hinttext>
<font>font10</font>
</control>
<control type="label">
<height>50</height>
<width>560</width>
<description>Port Number</description>
<label>$ADDON[plugin.video.plexkodiconnect 30030]:</label>
<font>font10</font>
</control>
<control type="edit" id="209">
<height>50</height>
<width>560</width>
<font>font10</font>
</control>
<control type="label">
<description>Advanced</description>
<height>50</height>
<width>560</width>
<label>$ADDON[plugin.video.plexkodiconnect 30022]:</label>
<font>font10</font>
</control>
<control type="radiobutton" id="204">
<description>Verify SSL Certificate</description>
<width>560</width>
<label>$ADDON[plugin.video.plexkodiconnect 30500]</label>
<font>font10</font>
</control>
<control type="radiobutton" id="205">
<description>Client SSL certificate</description>
<width>560</width>
<label>$ADDON[plugin.video.plexkodiconnect 30501]</label>
<font>font10</font>
</control>
</control>
<control type="group">
<description>Buttons group</description>
<top>480</top>
<left>50</left>
<width>460</width>
<height>100</height>
<control type="button" id="200">
<description>Connect</description>
<label>[UPPERCASE][B]$ADDON[plugin.video.plexkodiconnect 30020][/UPPERCASE][/B]</label>
<height>50</height>
<width>460</width>
<font>font10</font>
<texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus>
<texturefocus border="5" colordiffuse="ff525252">box.png</texturefocus>
<textcolor>ffa6a6a6</textcolor>
<focusedcolor>white</focusedcolor>
<align>center</align>
</control>
<control type="button" id="201">
<description>Cancel</description>
<label>[UPPERCASE][B]$ADDON[plugin.video.plexkodiconnect 30602][/UPPERCASE][/B]</label>
<top>55</top>
<height>50</height>
<width>460</width>
<font>font10</font>
<texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus>
<texturefocus border="5" colordiffuse="ff525252">box.png</texturefocus>
<textcolor>ffa6a6a6</textcolor>
<focusedcolor>white</focusedcolor>
<align>center</align>
</control>
</control>
<control type="group" id="202">
<top>626</top>
<left>-20</left>
<width>600</width>
<visible>False</visible>
<control type="image">
<description>Error box</description>
<texture colordiffuse="ff222222">white.png</texture>
<width>100%</width>
<height>50</height>
</control>
<control type="label" id="203">
<description>Error message</description>
<textcolor>white</textcolor>
<font>font10</font>
<aligny>center</aligny>
<align>center</align>
<height>50</height>
</control>
</control>
</control>
</controls>
</window>

View file

@ -27,7 +27,7 @@
<texture>logo-white.png</texture> <texture>logo-white.png</texture>
<aspectratio>keep</aspectratio> <aspectratio>keep</aspectratio>
<width>120</width> <width>120</width>
<height>49</height> <height>120</height>
<top>30</top> <top>30</top>
<left>25</left> <left>25</left>
</control> </control>
@ -80,10 +80,10 @@
</control> </control>
<control type="label"> <control type="label">
<description>Select server</description> <description>Select Main PMS</description>
<textcolor>ffa6a6a6</textcolor> <textcolor>ffa6a6a6</textcolor>
<label>$ADDON[plugin.video.emby 30607]</label> <label>$ADDON[plugin.video.plexkodiconnect 30607]</label>
<font>font10</font> <font>font12</font>
<align>center</align> <align>center</align>
<aligny>top</aligny> <aligny>top</aligny>
<top>170</top> <top>170</top>
@ -202,10 +202,10 @@
<height>150</height> <height>150</height>
<control type="button" id="205"> <control type="button" id="205">
<visible>True</visible> <visible>True</visible>
<description>Sign in Connect</description> <description>Toggle plex.tv sign-in</description>
<texturenofocus border="5" colordiffuse="ff0b8628">box.png</texturenofocus> <texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus>
<texturefocus border="5" colordiffuse="ff13a134">box.png</texturefocus> <texturefocus border="5" colordiffuse="ff525252">box.png</texturefocus>
<label>[UPPERCASE][B]$ADDON[plugin.video.emby 30600][/B][/UPPERCASE]</label> <label>[UPPERCASE][B]$ADDON[plugin.video.plexkodiconnect 30600][/UPPERCASE][/B]</label>
<font>font10</font> <font>font10</font>
<textcolor>ffa6a6a6</textcolor> <textcolor>ffa6a6a6</textcolor>
<focusedcolor>white</focusedcolor> <focusedcolor>white</focusedcolor>
@ -221,7 +221,7 @@
<description>Manually add server</description> <description>Manually add server</description>
<texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus> <texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus>
<texturefocus border="5" colordiffuse="ff525252">box.png</texturefocus> <texturefocus border="5" colordiffuse="ff525252">box.png</texturefocus>
<label>[UPPERCASE][B]$ADDON[plugin.video.emby 30611][/B][/UPPERCASE]</label> <label>[UPPERCASE][B]$ADDON[plugin.video.plexkodiconnect 30601][/UPPERCASE][/B]</label>
<font>font10</font> <font>font10</font>
<textcolor>ffa6a6a6</textcolor> <textcolor>ffa6a6a6</textcolor>
<focusedcolor>white</focusedcolor> <focusedcolor>white</focusedcolor>
@ -239,7 +239,7 @@
<description>Cancel</description> <description>Cancel</description>
<texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus> <texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus>
<texturefocus border="5" colordiffuse="ff525252">box.png</texturefocus> <texturefocus border="5" colordiffuse="ff525252">box.png</texturefocus>
<label>[UPPERCASE][B]$ADDON[plugin.video.emby 30606][/B][/UPPERCASE]</label> <label>[UPPERCASE][B]$ADDON[plugin.video.plexkodiconnect 30602][/UPPERCASE][/B]</label>
<font>font10</font> <font>font10</font>
<textcolor>ffa6a6a6</textcolor> <textcolor>ffa6a6a6</textcolor>
<focusedcolor>white</focusedcolor> <focusedcolor>white</focusedcolor>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

View file

@ -30,8 +30,7 @@ sys_path.append(_base_resource)
############################################################################### ###############################################################################
from utils import settings, window, language as lang, dialog, tryEncode, \ from utils import settings, window, language as lang, dialog, tryDecode
tryDecode
from userclient import UserClient from userclient import UserClient
import initialsetup import initialsetup
from kodimonitor import KodiMonitor from kodimonitor import KodiMonitor
@ -40,8 +39,8 @@ import videonodes
from websocket_client import PMS_Websocket, Alexa_Websocket from websocket_client import PMS_Websocket, Alexa_Websocket
import downloadutils import downloadutils
from playqueue import Playqueue from playqueue import Playqueue
from connectmanager import ConnectManager, check_connection
import PlexAPI
from PlexCompanion import PlexCompanion from PlexCompanion import PlexCompanion
from command_pipeline import Monitor_Window from command_pipeline import Monitor_Window
from playback_starter import Playback_Starter from playback_starter import Playback_Starter
@ -152,7 +151,7 @@ class Service():
self.command_pipeline.start() self.command_pipeline.start()
# Server auto-detect # Server auto-detect
initialsetup.InitialSetup().setup() initialsetup.setup()
# Initialize important threads, handing over self for callback purposes # Initialize important threads, handing over self for callback purposes
self.user = UserClient(self) self.user = UserClient(self)
@ -165,8 +164,6 @@ class Service():
if settings('enableTextureCache') == "true": if settings('enableTextureCache') == "true":
self.image_cache_thread = Image_Cache_Thread() self.image_cache_thread = Image_Cache_Thread()
plx = PlexAPI.PlexAPI()
welcome_msg = True welcome_msg = True
counter = 0 counter = 0
while not __stop_PKC(): while not __stop_PKC():
@ -260,7 +257,7 @@ class Service():
if server is False: if server is False:
# No server info set in add-on settings # No server info set in add-on settings
pass pass
elif plx.CheckConnection(server, verifySSL=True) is False: elif check_connection(server, verifySSL=True) is False:
# Server is offline or cannot be reached # Server is offline or cannot be reached
# Alert the user and suppress future warning # Alert the user and suppress future warning
if self.server_online: if self.server_online:
@ -279,10 +276,10 @@ class Service():
# Periodically check if the IP changed, e.g. per minute # Periodically check if the IP changed, e.g. per minute
if counter > 20: if counter > 20:
counter = 0 counter = 0
setup = initialsetup.InitialSetup() connectmanager = ConnectManager()
tmp = setup.PickPMS() tmp = connectmanager.pick_pms()
if tmp is not None: if tmp is not None:
setup.WritePMStoSettings(tmp) connectmanager.write_pms_to_settings(tmp)
else: else:
# Server is online # Server is online
counter = 0 counter = 0