Compare commits
24 commits
master
...
pms-dialog
Author | SHA1 | Date | |
---|---|---|---|
|
47c1b51014 | ||
|
583512eb89 | ||
|
3310e0ad25 | ||
|
0628b40b8f | ||
|
04e1fb4ba8 | ||
|
8a4b1c00f3 | ||
|
0f84836533 | ||
|
b698d2e0e1 | ||
|
af0ac26045 | ||
|
c1098f22a4 | ||
|
76ca66b38b | ||
|
87775072c6 | ||
|
ac3016c84d | ||
|
d1346b2cd6 | ||
|
7a9e0611ed | ||
|
962ce6da1e | ||
|
73ce4eeacb | ||
|
10942558cc | ||
|
78f6ad7da8 | ||
|
92a5eac7be | ||
|
61b9bbee8f | ||
|
40ba9a495f | ||
|
051006a1ef | ||
|
32ace844aa |
26 changed files with 3027 additions and 2185 deletions
16
default.py
16
default.py
|
@ -137,7 +137,7 @@ class Main():
|
|||
window('plex_runLibScan', value='del_textures')
|
||||
|
||||
elif mode == 'chooseServer':
|
||||
entrypoint.chooseServer()
|
||||
self.__exec('function=choose_server')
|
||||
|
||||
elif mode == 'refreshplaylist':
|
||||
log.info('Requesting playlist/nodes refresh')
|
||||
|
@ -172,8 +172,7 @@ class Main():
|
|||
# Put the request into the 'queue'
|
||||
while window('plex_command'):
|
||||
sleep(50)
|
||||
window('plex_command',
|
||||
value='play_%s' % argv[2])
|
||||
window('plex_command', value='play_%s' % argv[2])
|
||||
# Wait for the result
|
||||
while not pickl_window('plex_result'):
|
||||
sleep(50)
|
||||
|
@ -190,6 +189,17 @@ class Main():
|
|||
listitem = convert_PKC_to_listitem(result.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):
|
||||
deviceId_old = window('plex_client_Id')
|
||||
from clientinfo import getDeviceId
|
||||
|
|
|
@ -83,10 +83,35 @@ msgctxt "#30017"
|
|||
msgid "Unauthorized for PMS"
|
||||
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"
|
||||
msgid "Advanced"
|
||||
msgstr ""
|
||||
|
||||
# Used to show that PKC is currently trying to connect to a PMS
|
||||
msgctxt "#30023"
|
||||
msgid "Connecting to"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30024"
|
||||
msgid "Username"
|
||||
msgstr ""
|
||||
|
@ -96,7 +121,7 @@ msgid "Display message if PMS goes offline"
|
|||
msgstr ""
|
||||
|
||||
msgctxt "#30030"
|
||||
msgid "Port Number"
|
||||
msgid "Port number"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30031"
|
||||
|
@ -977,7 +1002,7 @@ msgstr ""
|
|||
# add-on settings
|
||||
|
||||
msgctxt "#30500"
|
||||
msgid "Verify Host SSL Certificate (more secure)"
|
||||
msgid "Verify SSL Certificate"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30501"
|
||||
|
@ -1160,6 +1185,28 @@ msgctxt "#30545"
|
|||
msgid "Force transcode pictures"
|
||||
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
|
||||
|
||||
msgctxt "#33000"
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -3,11 +3,12 @@ from logging import getLogger
|
|||
from urllib import urlencode
|
||||
from ast import literal_eval
|
||||
from urlparse import urlparse, parse_qsl
|
||||
from urllib import quote_plus
|
||||
import re
|
||||
from copy import deepcopy
|
||||
|
||||
import downloadutils
|
||||
from utils import settings
|
||||
from downloadutils import DownloadUtils
|
||||
from utils import settings, tryEncode
|
||||
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
|
||||
chosen.
|
||||
"""
|
||||
downloadutils.DownloadUtils().downloadUrl(
|
||||
DownloadUtils().downloadUrl(
|
||||
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):
|
||||
"""
|
||||
Returns raw API metadata for key as an etree XML.
|
||||
|
@ -129,7 +180,7 @@ def GetPlexMetadata(key):
|
|||
# 'includeConcerts': 1
|
||||
}
|
||||
url = url + '?' + urlencode(arguments)
|
||||
xml = downloadutils.DownloadUtils().downloadUrl(url)
|
||||
xml = DownloadUtils().downloadUrl(url)
|
||||
if xml == 401:
|
||||
# Either unauthorized (taken care of by doUtils) or PMS under strain
|
||||
return 401
|
||||
|
@ -186,8 +237,7 @@ def DownloadChunks(url):
|
|||
'X-Plex-Container-Size': CONTAINERSIZE,
|
||||
'X-Plex-Container-Start': pos
|
||||
}
|
||||
xmlpart = downloadutils.DownloadUtils().downloadUrl(
|
||||
url + urlencode(args))
|
||||
xmlpart = DownloadUtils().downloadUrl(url + urlencode(args))
|
||||
# If something went wrong - skip in the hope that it works next time
|
||||
try:
|
||||
xmlpart.attrib
|
||||
|
@ -262,8 +312,7 @@ def get_plex_sections():
|
|||
"""
|
||||
Returns all Plex sections (libraries) of the PMS as an etree xml
|
||||
"""
|
||||
return downloadutils.DownloadUtils().downloadUrl(
|
||||
'{server}/library/sections')
|
||||
return DownloadUtils().downloadUrl('{server}/library/sections')
|
||||
|
||||
|
||||
def init_plex_playqueue(itemid, librarySectionUUID, mediatype='movie',
|
||||
|
@ -282,7 +331,7 @@ def init_plex_playqueue(itemid, librarySectionUUID, mediatype='movie',
|
|||
}
|
||||
if trailers is True:
|
||||
args['extrasPrefixCount'] = settings('trailerNumber')
|
||||
xml = downloadutils.DownloadUtils().downloadUrl(
|
||||
xml = DownloadUtils().downloadUrl(
|
||||
url + '?' + urlencode(args), action_type="POST")
|
||||
try:
|
||||
xml[0].tag
|
||||
|
@ -313,7 +362,7 @@ def PMSHttpsEnabled(url):
|
|||
|
||||
Prefers HTTPS over HTTP
|
||||
"""
|
||||
doUtils = downloadutils.DownloadUtils().downloadUrl
|
||||
doUtils = DownloadUtils().downloadUrl
|
||||
res = doUtils('https://%s/identity' % url,
|
||||
authenticate=False,
|
||||
verifySSL=False)
|
||||
|
@ -343,10 +392,10 @@ def GetMachineIdentifier(url):
|
|||
|
||||
Returns None if something went wrong
|
||||
"""
|
||||
xml = downloadutils.DownloadUtils().downloadUrl('%s/identity' % url,
|
||||
authenticate=False,
|
||||
verifySSL=False,
|
||||
timeout=10)
|
||||
xml = DownloadUtils().downloadUrl('%s/identity' % url,
|
||||
authenticate=False,
|
||||
verifySSL=False,
|
||||
timeout=10)
|
||||
try:
|
||||
machineIdentifier = xml.attrib['machineIdentifier']
|
||||
except (AttributeError, KeyError):
|
||||
|
@ -373,7 +422,7 @@ def GetPMSStatus(token):
|
|||
or an empty dict.
|
||||
"""
|
||||
answer = {}
|
||||
xml = downloadutils.DownloadUtils().downloadUrl(
|
||||
xml = DownloadUtils().downloadUrl(
|
||||
'{server}/status/sessions',
|
||||
headerOptions={'X-Plex-Token': token})
|
||||
try:
|
||||
|
@ -413,7 +462,7 @@ def scrobble(ratingKey, state):
|
|||
url = "{server}/:/unscrobble?" + urlencode(args)
|
||||
else:
|
||||
return
|
||||
downloadutils.DownloadUtils().downloadUrl(url)
|
||||
DownloadUtils().downloadUrl(url)
|
||||
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
|
||||
"""
|
||||
if downloadutils.DownloadUtils().downloadUrl(
|
||||
if DownloadUtils().downloadUrl(
|
||||
'{server}/library/metadata/%s' % plexid,
|
||||
action_type="DELETE") is True:
|
||||
log.info('Successfully deleted Plex id %s from the PMS' % plexid)
|
||||
|
@ -434,14 +483,53 @@ def delete_item_from_pms(plexid):
|
|||
return False
|
||||
|
||||
|
||||
def get_PMS_settings(url, token):
|
||||
def get_pms_settings(url, token):
|
||||
"""
|
||||
Retrieve the PMS' settings via <url>/:/
|
||||
|
||||
Call with url: scheme://ip:port
|
||||
"""
|
||||
return downloadutils.DownloadUtils().downloadUrl(
|
||||
return DownloadUtils().downloadUrl(
|
||||
'%s/:/prefs' % url,
|
||||
authenticate=False,
|
||||
verifySSL=False,
|
||||
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)
|
||||
|
|
|
@ -3,11 +3,13 @@
|
|||
import logging
|
||||
from threading import Thread
|
||||
from Queue import Queue
|
||||
from urlparse import parse_qsl
|
||||
|
||||
from xbmc import sleep
|
||||
|
||||
from utils import window, thread_methods
|
||||
import state
|
||||
import entrypoint
|
||||
|
||||
###############################################################################
|
||||
log = logging.getLogger("PLEX."+__name__)
|
||||
|
@ -32,6 +34,30 @@ class Monitor_Window(Thread):
|
|||
self.playback_queue = Queue()
|
||||
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):
|
||||
thread_stopped = self.thread_stopped
|
||||
queue = self.playback_queue
|
||||
|
@ -42,7 +68,9 @@ class Monitor_Window(Thread):
|
|||
window('plex_command', clear=True)
|
||||
if value.startswith('play_'):
|
||||
queue.put(value)
|
||||
|
||||
elif value.startswith('exec_'):
|
||||
t = Thread(target=self.__execute, args=(value[5:], ))
|
||||
t.start()
|
||||
elif value == 'SUSPEND_LIBRARY_THREAD-True':
|
||||
state.SUSPEND_LIBRARY_THREAD = True
|
||||
elif value == 'SUSPEND_LIBRARY_THREAD-False':
|
||||
|
|
1
resources/lib/connect/__init__.py
Normal file
1
resources/lib/connect/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
# Dummy file to make this directory a package.
|
831
resources/lib/connect/connectionmanager.py
Normal file
831
resources/lib/connect/connectionmanager.py
Normal 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("&", '&')
|
||||
password = password.replace("/", '\')
|
||||
password = password.replace("!", '!')
|
||||
password = password.replace("$", '$')
|
||||
password = password.replace("\"", '"')
|
||||
password = password.replace("<", '<')
|
||||
password = password.replace(">", '>')
|
||||
password = password.replace("'", ''')
|
||||
|
||||
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)
|
149
resources/lib/connect/credentials.py
Normal file
149
resources/lib/connect/credentials.py
Normal 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
|
449
resources/lib/connect/plex_tv.py
Normal file
449
resources/lib/connect/plex_tv.py
Normal 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
|
977
resources/lib/connectmanager.py
Normal file
977
resources/lib/connectmanager.py
Normal 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
|
|
@ -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')
|
|
@ -1,8 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
##################################################################################################
|
||||
###############################################################################
|
||||
|
||||
import logging
|
||||
from logging import getLogger
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
|
@ -10,11 +10,11 @@ import xbmcgui
|
|||
import connect.connectionmanager as connectionmanager
|
||||
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_PREVIOUS_MENU = 10
|
||||
ACTION_BACK = 92
|
||||
|
@ -27,10 +27,10 @@ CANCEL = 201
|
|||
MESSAGE_BOX = 202
|
||||
MESSAGE = 203
|
||||
BUSY = 204
|
||||
EMBY_CONNECT = 205
|
||||
PLEX_CONNECT = 205
|
||||
MANUAL_SERVER = 206
|
||||
|
||||
##################################################################################################
|
||||
###############################################################################
|
||||
|
||||
|
||||
class ServerConnect(xbmcgui.WindowXMLDialog):
|
||||
|
@ -43,13 +43,8 @@ class ServerConnect(xbmcgui.WindowXMLDialog):
|
|||
_connect_login = False
|
||||
_manual_server = False
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
xbmcgui.WindowXMLDialog.__init__(self, *args, **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():
|
||||
setattr(self, key, value)
|
||||
|
||||
|
@ -65,7 +60,6 @@ class ServerConnect(xbmcgui.WindowXMLDialog):
|
|||
def is_manual_server(self):
|
||||
return self._manual_server
|
||||
|
||||
|
||||
def onInit(self):
|
||||
|
||||
self.message = self.getControl(MESSAGE)
|
||||
|
@ -74,7 +68,7 @@ class ServerConnect(xbmcgui.WindowXMLDialog):
|
|||
self.list_ = self.getControl(LIST)
|
||||
|
||||
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.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:
|
||||
self.getControl(USER_IMAGE).setImage(self.user_image)
|
||||
|
||||
if not self.emby_connect: # Change connect user
|
||||
self.getControl(EMBY_CONNECT).setLabel("[UPPERCASE][B]"+lang(30618)+"[/B][/UPPERCASE]")
|
||||
if not self.plex_connect: # Change connect user
|
||||
self.getControl(PLEX_CONNECT).setLabel("[UPPERCASE][B]"+'plex.tv user change'+"[/B][/UPPERCASE]")
|
||||
|
||||
if self.servers:
|
||||
self.setFocus(self.list_)
|
||||
|
@ -115,7 +109,7 @@ class ServerConnect(xbmcgui.WindowXMLDialog):
|
|||
|
||||
def onClick(self, control):
|
||||
|
||||
if control == EMBY_CONNECT:
|
||||
if control == PLEX_CONNECT:
|
||||
self.connect_manager.clearData()
|
||||
self._connect_login = True
|
||||
self.close()
|
||||
|
|
|
@ -1,22 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
##################################################################################################
|
||||
|
||||
import logging
|
||||
import os
|
||||
###############################################################################
|
||||
from logging import getLogger
|
||||
|
||||
import xbmcgui
|
||||
import xbmcaddon
|
||||
|
||||
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__)
|
||||
addon = xbmcaddon.Addon('plugin.video.emby')
|
||||
|
||||
CONN_STATE = connectionmanager.ConnectionState
|
||||
CONN_STATE = connectionmanager.CONNECTIONSTATE
|
||||
ACTION_PARENT_DIR = 9
|
||||
ACTION_PREVIOUS_MENU = 10
|
||||
ACTION_BACK = 92
|
||||
|
@ -24,23 +18,50 @@ CONNECT = 200
|
|||
CANCEL = 201
|
||||
ERROR_TOGGLE = 202
|
||||
ERROR_MSG = 203
|
||||
VERIFY_SSL = 204
|
||||
HOST_SSL_PATH = 205
|
||||
PMS_IP = 208
|
||||
PMS_PORT = 209
|
||||
ERROR = {
|
||||
'Invalid': 1,
|
||||
'Empty': 2
|
||||
}
|
||||
|
||||
##################################################################################################
|
||||
###############################################################################
|
||||
|
||||
|
||||
class ServerManual(xbmcgui.WindowXMLDialog):
|
||||
|
||||
_server = 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):
|
||||
self.connect_manager = connect_manager
|
||||
|
@ -51,29 +72,8 @@ class ServerManual(xbmcgui.WindowXMLDialog):
|
|||
def get_server(self):
|
||||
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):
|
||||
|
||||
if control == CONNECT:
|
||||
# Sign in to emby connect
|
||||
self._disable_error()
|
||||
|
||||
server = self.host_field.getText()
|
||||
|
@ -81,65 +81,66 @@ class ServerManual(xbmcgui.WindowXMLDialog):
|
|||
|
||||
if not server or not port:
|
||||
# Display error
|
||||
self._error(ERROR['Empty'], lang(30617))
|
||||
self._error(ERROR['Empty'], lang(30021))
|
||||
log.error("Server or port cannot be null")
|
||||
|
||||
elif self._connect_to_server(server, port):
|
||||
self.close()
|
||||
|
||||
elif control == CANCEL:
|
||||
# Remind me later
|
||||
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):
|
||||
|
||||
if self.error == ERROR['Empty'] and self.host_field.getText() and self.port_field.getText():
|
||||
if (self.error == ERROR['Empty'] and
|
||||
self.host_field.getText() and self.port_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):
|
||||
|
||||
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):
|
||||
|
||||
server_address = "%s:%s" % (server, port)
|
||||
self._message("%s %s..." % (lang(30610), server_address))
|
||||
result = self.connect_manager.connectToAddress(server_address)
|
||||
|
||||
"""Returns True if we could connect, False otherwise"""
|
||||
url = "%s:%s" % (server, port)
|
||||
self._message("%s %s..." % (lang(30023), url))
|
||||
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']:
|
||||
self._message(lang(30609))
|
||||
self._message(lang(30204))
|
||||
return False
|
||||
else:
|
||||
self._server = result['Servers'][0]
|
||||
return True
|
||||
|
||||
def _message(self, message):
|
||||
|
||||
"""Displays a message popup just underneath the dialog"""
|
||||
self.error_msg.setLabel(message)
|
||||
self.error_toggle.setVisibleCondition('True')
|
||||
|
||||
def _error(self, state, message):
|
||||
|
||||
"""Displays an error message just underneath the dialog"""
|
||||
self.error = state
|
||||
self.error_msg.setLabel(message)
|
||||
self.error_toggle.setVisibleCondition('True')
|
||||
|
||||
def _disable_error(self):
|
||||
|
||||
"""Disables the message popup just underneath the dialog"""
|
||||
self.error = None
|
||||
self.error_toggle.setVisibleCondition('False')
|
||||
|
|
|
@ -142,13 +142,16 @@ class DownloadUtils():
|
|||
|
||||
def downloadUrl(self, url, action_type="GET", postBody=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
|
||||
|
||||
If authenticate=True, existing request session will be used/started
|
||||
Otherwise, 'empty' request will be made
|
||||
|
||||
auth=None or auth=('user', 'password')
|
||||
|
||||
Returns:
|
||||
None If an error occured
|
||||
True If connection worked but no body was received
|
||||
|
@ -190,6 +193,8 @@ class DownloadUtils():
|
|||
kwargs['params'] = parameters
|
||||
if timeout is not None:
|
||||
kwargs['timeout'] = timeout
|
||||
if auth is not None:
|
||||
kwargs['auth'] = auth
|
||||
|
||||
# ACTUAL DOWNLOAD HAPPENING HERE
|
||||
try:
|
||||
|
|
|
@ -31,15 +31,15 @@ except IndexError:
|
|||
###############################################################################
|
||||
|
||||
|
||||
def chooseServer():
|
||||
def choose_server():
|
||||
"""
|
||||
Lets user choose from list of PMS
|
||||
"""
|
||||
log.info("Choosing PMS server requested, starting")
|
||||
|
||||
import initialsetup
|
||||
setup = initialsetup.InitialSetup()
|
||||
server = setup.PickPMS(showDialog=True)
|
||||
from connectmanager import ConnectManager
|
||||
connectmanager = ConnectManager()
|
||||
server = connectmanager.pick_pms(show_dialog=True)
|
||||
if server is None:
|
||||
log.error('We did not connect to a new PMS, aborting')
|
||||
plex_command('SUSPEND_USER_CLIENT', 'False')
|
||||
|
@ -47,7 +47,7 @@ def chooseServer():
|
|||
return
|
||||
|
||||
log.info("User chose server %s" % server['name'])
|
||||
setup.WritePMStoSettings(server)
|
||||
connectmanager.write_pms_to_settings(server)
|
||||
|
||||
if not __LogOut():
|
||||
return
|
||||
|
@ -85,8 +85,8 @@ def togglePlexTV():
|
|||
plex_command('PLEX_USERNAME', '')
|
||||
else:
|
||||
log.info('Login to plex.tv')
|
||||
import initialsetup
|
||||
initialsetup.InitialSetup().PlexTVSignIn()
|
||||
from connectmanager import ConnectManager
|
||||
ConnectManager().plex_tv_signin()
|
||||
dialog('notification',
|
||||
lang(29999),
|
||||
lang(39221),
|
||||
|
|
|
@ -1,512 +1,139 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
###############################################################################
|
||||
from logging import getLogger
|
||||
from xbmc import executebuiltin
|
||||
|
||||
import logging
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
from utils import settings, language as lang, advancedsettings_xml, dialog
|
||||
from connectmanager import ConnectManager
|
||||
|
||||
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
|
||||
from migration import check_migration
|
||||
|
||||
###############################################################################
|
||||
|
||||
log = logging.getLogger("PLEX."+__name__)
|
||||
log = getLogger("PLEX."+__name__)
|
||||
|
||||
###############################################################################
|
||||
|
||||
|
||||
class InitialSetup():
|
||||
def setup():
|
||||
"""
|
||||
Initial setup. Run once upon startup.
|
||||
|
||||
def __init__(self):
|
||||
log.debug('Entering initialsetup class')
|
||||
self.doUtils = downloadutils.DownloadUtils().downloadUrl
|
||||
self.plx = PlexAPI()
|
||||
self.dialog = xbmcgui.Dialog()
|
||||
Check server, user, direct paths, music, direct stream if not direct
|
||||
path.
|
||||
"""
|
||||
log.info("Initial setup called")
|
||||
connectmanager = ConnectManager()
|
||||
|
||||
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')
|
||||
# Get current Kodi video cache setting
|
||||
cache, _ = advancedsettings_xml(['cache', 'memorysize'])
|
||||
if cache is None:
|
||||
# Kodi default cache
|
||||
cache = '20971520'
|
||||
else:
|
||||
cache = str(cache.text)
|
||||
log.info('Current Kodi video memory cache in bytes: %s' % cache)
|
||||
settings('kodi_video_cache', value=cache)
|
||||
|
||||
def PlexTVSignIn(self):
|
||||
"""
|
||||
Signs (freshly) in to plex.tv (will be saved to file settings)
|
||||
# Do we need to migrate stuff?
|
||||
check_migration()
|
||||
|
||||
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
|
||||
# Optionally sign into plex.tv. Will not be called on very first run
|
||||
# as plexToken will be ''
|
||||
settings('plex_status', value=lang(39226))
|
||||
if connectmanager.plexToken and connectmanager.myplexlogin:
|
||||
connectmanager.check_plex_tv_signin()
|
||||
|
||||
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.
|
||||
|
||||
Check server, user, direct paths, music, direct stream if not direct
|
||||
path.
|
||||
"""
|
||||
log.info("Initial setup called.")
|
||||
dialog = self.dialog
|
||||
|
||||
# Get current Kodi video cache setting
|
||||
cache, _ = advancedsettings_xml(['cache', 'memorysize'])
|
||||
if cache is None:
|
||||
# Kodi default cache
|
||||
cache = '20971520'
|
||||
else:
|
||||
cache = str(cache.text)
|
||||
log.info('Current Kodi video memory cache in bytes: %s' % cache)
|
||||
settings('kodi_video_cache', value=cache)
|
||||
|
||||
# Do we need to migrate stuff?
|
||||
check_migration()
|
||||
|
||||
# Optionally sign into plex.tv. Will not be called on very first run
|
||||
# as plexToken will be ''
|
||||
settings('plex_status', value=lang(39226))
|
||||
if self.plexToken and self.myplexlogin:
|
||||
self.CheckPlexTVSignIn()
|
||||
|
||||
# If a Plex server IP has already been set
|
||||
# return only if the right machine identifier is found
|
||||
if self.server:
|
||||
log.info("PMS is already set: %s. Checking now..." % self.server)
|
||||
if self.CheckPMS():
|
||||
log.info("Using PMS %s with machineIdentifier %s"
|
||||
% (self.server, self.serverid))
|
||||
self._write_PMS_settings(self.server, self.pms_token)
|
||||
return
|
||||
|
||||
# If not already retrieved myplex info, optionally let user sign in
|
||||
# to plex.tv. This DOES get called on very first install run
|
||||
if not self.plexToken and self.myplexlogin:
|
||||
self.PlexTVSignIn()
|
||||
|
||||
server = self.PickPMS()
|
||||
if server is not None:
|
||||
# Write our chosen server to Kodi settings file
|
||||
self.WritePMStoSettings(server)
|
||||
|
||||
# User already answered the installation questions
|
||||
if settings('InstallQuestionsAnswered') == 'true':
|
||||
# If a Plex server IP has already been set
|
||||
# return only if the right machine identifier is found
|
||||
if connectmanager.server:
|
||||
log.info("PMS is already set: %s. Checking now..."
|
||||
% connectmanager.server)
|
||||
if connectmanager.check_pms():
|
||||
log.info("Using PMS %s with machineIdentifier %s"
|
||||
% (connectmanager.server, connectmanager.serverid))
|
||||
connectmanager.write_pms_settings(connectmanager.server,
|
||||
connectmanager.pms_token)
|
||||
connectmanager.pick_pms(show_dialog=True)
|
||||
return
|
||||
|
||||
# Additional settings where the user needs to choose
|
||||
# Direct paths (\\NAS\mymovie.mkv) or addon (http)?
|
||||
goToSettings = False
|
||||
if dialog.yesno(lang(29999),
|
||||
lang(39027),
|
||||
lang(39028),
|
||||
nolabel="Addon (Default)",
|
||||
yeslabel="Native (Direct Paths)"):
|
||||
log.debug("User opted to use direct paths.")
|
||||
settings('useDirectPaths', value="1")
|
||||
state.DIRECT_PATHS = True
|
||||
# Are you on a system where you would like to replace paths
|
||||
# \\NAS\mymovie.mkv with smb://NAS/mymovie.mkv? (e.g. Windows)
|
||||
if dialog.yesno(heading=lang(29999), line1=lang(39033)):
|
||||
log.debug("User chose to replace paths with smb")
|
||||
else:
|
||||
settings('replaceSMB', value="false")
|
||||
# If not already retrieved myplex info, optionally let user sign in
|
||||
# to plex.tv. This DOES get called on very first install run
|
||||
if not connectmanager.plexToken and connectmanager.myplexlogin:
|
||||
connectmanager.plex_tv_signin()
|
||||
|
||||
# complete replace all original Plex library paths with custom SMB
|
||||
if dialog.yesno(heading=lang(29999), line1=lang(39043)):
|
||||
log.debug("User chose custom smb paths")
|
||||
settings('remapSMB', value="true")
|
||||
# Please enter your custom smb paths in the settings under
|
||||
# "Sync Options" and then restart Kodi
|
||||
dialog.ok(heading=lang(29999), line1=lang(39044))
|
||||
goToSettings = True
|
||||
server = connectmanager.connectmanager.pick_pms()
|
||||
if server is not None:
|
||||
# Write our chosen server to Kodi settings file
|
||||
connectmanager.write_pms_to_settings(server)
|
||||
|
||||
# Go to network credentials?
|
||||
if dialog.yesno(heading=lang(29999),
|
||||
line1=lang(39029),
|
||||
line2=lang(39030)):
|
||||
log.debug("Presenting network credentials dialog.")
|
||||
from utils import passwordsXML
|
||||
passwordsXML()
|
||||
# Disable Plex music?
|
||||
if dialog.yesno(heading=lang(29999), line1=lang(39016)):
|
||||
log.debug("User opted to disable Plex music library.")
|
||||
settings('enableMusic', value="false")
|
||||
# User already answered the installation questions
|
||||
if settings('InstallQuestionsAnswered') == 'true':
|
||||
return
|
||||
|
||||
# Download additional art from FanArtTV
|
||||
if dialog.yesno(heading=lang(29999), line1=lang(39061)):
|
||||
log.debug("User opted to use FanArtTV")
|
||||
settings('FanartTV', value="true")
|
||||
# Do you want to replace your custom user ratings with an indicator of
|
||||
# how many versions of a media item you posses?
|
||||
if dialog.yesno(heading=lang(29999), line1=lang(39718)):
|
||||
log.debug("User opted to replace user ratings with version number")
|
||||
settings('indicate_media_versions', value="true")
|
||||
# Additional settings where the user needs to choose
|
||||
# Direct paths (\\NAS\mymovie.mkv) or addon (http)?
|
||||
goToSettings = False
|
||||
if dialog('yesno',
|
||||
lang(29999),
|
||||
lang(39027),
|
||||
lang(39028),
|
||||
nolabel="Addon (Default)",
|
||||
yeslabel="Native (Direct Paths)"):
|
||||
log.debug("User opted to use direct paths.")
|
||||
settings('useDirectPaths', value="1")
|
||||
state.DIRECT_PATHS = True
|
||||
# Are you on a system where you would like to replace paths
|
||||
# \\NAS\mymovie.mkv with smb://NAS/mymovie.mkv? (e.g. Windows)
|
||||
if dialog('yesno', heading=lang(29999), line1=lang(39033)):
|
||||
log.debug("User chose to replace paths with smb")
|
||||
else:
|
||||
settings('replaceSMB', value="false")
|
||||
|
||||
# If you use several Plex libraries of one kind, e.g. "Kids Movies" and
|
||||
# "Parents Movies", be sure to check https://goo.gl/JFtQV9
|
||||
dialog.ok(heading=lang(29999), line1=lang(39076))
|
||||
# complete replace all original Plex library paths with custom SMB
|
||||
if dialog('yesno', heading=lang(29999), line1=lang(39043)):
|
||||
log.debug("User chose custom smb paths")
|
||||
settings('remapSMB', value="true")
|
||||
# Please enter your custom smb paths in the settings under
|
||||
# "Sync Options" and then restart Kodi
|
||||
dialog('ok', heading=lang(29999), line1=lang(39044))
|
||||
goToSettings = True
|
||||
|
||||
# Need to tell about our image source for collections: themoviedb.org
|
||||
dialog.ok(heading=lang(29999), line1=lang(39717))
|
||||
# Make sure that we only ask these questions upon first installation
|
||||
settings('InstallQuestionsAnswered', value='true')
|
||||
# Go to network credentials?
|
||||
if dialog('yesno',
|
||||
heading=lang(29999),
|
||||
line1=lang(39029),
|
||||
line2=lang(39030)):
|
||||
log.debug("Presenting network credentials dialog.")
|
||||
from utils import passwordsXML
|
||||
passwordsXML()
|
||||
# Disable Plex music?
|
||||
if dialog('yesno', heading=lang(29999), line1=lang(39016)):
|
||||
log.debug("User opted to disable Plex music library.")
|
||||
settings('enableMusic', value="false")
|
||||
|
||||
if goToSettings is False:
|
||||
# Open Settings page now? You will need to restart!
|
||||
goToSettings = dialog.yesno(heading=lang(29999), line1=lang(39017))
|
||||
if goToSettings:
|
||||
state.PMS_STATUS = 'Stop'
|
||||
xbmc.executebuiltin(
|
||||
'Addon.OpenSettings(plugin.video.plexkodiconnect)')
|
||||
# Download additional art from FanArtTV
|
||||
if dialog('yesno', heading=lang(29999), line1=lang(39061)):
|
||||
log.debug("User opted to use FanArtTV")
|
||||
settings('FanartTV', value="true")
|
||||
# Do you want to replace your custom user ratings with an indicator of
|
||||
# how many versions of a media item you posses?
|
||||
if dialog('yesno', heading=lang(29999), line1=lang(39718)):
|
||||
log.debug("User opted to replace user ratings with version number")
|
||||
settings('indicate_media_versions', value="true")
|
||||
|
||||
# If you use several Plex libraries of one kind, e.g. "Kids Movies" and
|
||||
# "Parents Movies", be sure to check https://goo.gl/JFtQV9
|
||||
dialog('ok', heading=lang(29999), line1=lang(39076))
|
||||
|
||||
# Need to tell about our image source for collections: themoviedb.org
|
||||
dialog('ok', heading=lang(29999), line1=lang(39717))
|
||||
# Make sure that we only ask these questions upon first installation
|
||||
settings('InstallQuestionsAnswered', value='true')
|
||||
|
||||
if goToSettings is False:
|
||||
# Open Settings page now? You will need to restart!
|
||||
goToSettings = dialog('yesno', heading=lang(29999), line1=lang(39017))
|
||||
if goToSettings:
|
||||
state.PMS_STATUS = 'Stop'
|
||||
executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)')
|
||||
|
|
|
@ -29,6 +29,8 @@ INDICATE_MEDIA_VERSIONS = False
|
|||
AUTHENTICATED = False
|
||||
# plex.tv username
|
||||
PLEX_USERNAME = None
|
||||
# plex.tv image for this user
|
||||
PLEX_USER_IMAGE = None
|
||||
# Token for that user for plex.tv
|
||||
PLEX_TOKEN = None
|
||||
# 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
|
||||
# another user playing something! Token identifies user
|
||||
PLEX_TRANSIENT_TOKEN = None
|
||||
|
||||
# Used by connectmanager.py
|
||||
CONNECT_STATE = {}
|
||||
|
|
|
@ -14,7 +14,9 @@ from utils import window, settings, language as lang, thread_methods
|
|||
import downloadutils
|
||||
|
||||
import PlexAPI
|
||||
from PlexFunctions import GetMachineIdentifier
|
||||
from connect.plex_tv import get_user_artwork_url
|
||||
|
||||
from PlexFunctions import GetMachineIdentifier, check_connection
|
||||
import state
|
||||
|
||||
###############################################################################
|
||||
|
@ -106,9 +108,10 @@ class UserClient(threading.Thread):
|
|||
log.debug('Setting user preferences')
|
||||
# Only try to get user avatar if there is a token
|
||||
if self.currToken:
|
||||
url = PlexAPI.PlexAPI().GetUserArtworkURL(self.currUser)
|
||||
url = get_user_artwork_url(self.currUser)
|
||||
if url:
|
||||
window('PlexUserImage', value=url)
|
||||
state.PLEX_USER_IMAGE = url
|
||||
# Set resume point max
|
||||
# url = "{server}/emby/System/Configuration?format=json"
|
||||
# result = doUtils.downloadUrl(url)
|
||||
|
@ -130,9 +133,9 @@ class UserClient(threading.Thread):
|
|||
if self.currServer is None:
|
||||
return False
|
||||
log.debug('Testing validity of current token')
|
||||
res = PlexAPI.PlexAPI().CheckConnection(self.currServer,
|
||||
token=self.currToken,
|
||||
verifySSL=self.ssl)
|
||||
res = check_connection(self.currServer,
|
||||
token=self.currToken,
|
||||
verifySSL=self.ssl)
|
||||
if res is False:
|
||||
# PMS probably offline
|
||||
return False
|
||||
|
@ -280,6 +283,7 @@ class UserClient(threading.Thread):
|
|||
window('plex_servername', clear=True)
|
||||
state.PLEX_USER_ID = None
|
||||
state.PLEX_USERNAME = None
|
||||
state.PLEX_USER_IMAGE = None
|
||||
window('plex_restricteduser', clear=True)
|
||||
state.RESTRICTED_USER = False
|
||||
|
||||
|
|
|
@ -34,6 +34,26 @@ log = logging.getLogger("PLEX."+__name__)
|
|||
WINDOW = xbmcgui.Window(10000)
|
||||
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
|
||||
|
||||
|
@ -154,27 +174,19 @@ def dialog(typus, *args, **kwargs):
|
|||
type='{ipaddress}' xbmcgui.INPUT_IPADDRESS (format: #.#.#.#)
|
||||
type='{password}' xbmcgui.INPUT_PASSWORD
|
||||
(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()
|
||||
if "icon" in kwargs:
|
||||
types = {
|
||||
'{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():
|
||||
for key, value in ICONS.iteritems():
|
||||
kwargs['icon'] = kwargs['icon'].replace(key, value)
|
||||
if 'type' in kwargs:
|
||||
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
|
||||
}
|
||||
kwargs['type'] = types[kwargs['type']]
|
||||
kwargs['type'] = TYPES[kwargs['type']]
|
||||
if 'option' in kwargs:
|
||||
kwargs['option'] = OPTIONS[kwargs['option']]
|
||||
if "heading" in kwargs:
|
||||
kwargs['heading'] = kwargs['heading'].replace("{plex}",
|
||||
language(29999))
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import xbmc
|
||||
from xbmcaddon import Addon
|
||||
from os.path import join
|
||||
|
||||
# Paths are in unicode, otherwise Windows will throw fits
|
||||
# For any file operations with KODI function, use encoded strings!
|
||||
|
@ -26,11 +27,14 @@ _ADDON = Addon()
|
|||
ADDON_NAME = 'PlexKodiConnect'
|
||||
ADDON_ID = 'plugin.video.plexkodiconnect'
|
||||
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)
|
||||
KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
|
||||
KODILONGVERSION = xbmc.getInfoLabel('System.BuildVersion')
|
||||
KODI_PROFILE = tryDecode(xbmc.translatePath("special://profile"))
|
||||
ADDON_PATH_DATA = join(KODI_PROFILE, 'addon_data', ADDON_ID, '')
|
||||
|
||||
if xbmc.getCondVisibility('system.platform.osx'):
|
||||
PLATFORM = "MacOSX"
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -27,7 +27,7 @@
|
|||
<texture>logo-white.png</texture>
|
||||
<aspectratio>keep</aspectratio>
|
||||
<width>120</width>
|
||||
<height>49</height>
|
||||
<height>120</height>
|
||||
<top>30</top>
|
||||
<left>25</left>
|
||||
</control>
|
||||
|
@ -80,10 +80,10 @@
|
|||
</control>
|
||||
|
||||
<control type="label">
|
||||
<description>Select server</description>
|
||||
<description>Select Main PMS</description>
|
||||
<textcolor>ffa6a6a6</textcolor>
|
||||
<label>$ADDON[plugin.video.emby 30607]</label>
|
||||
<font>font10</font>
|
||||
<label>$ADDON[plugin.video.plexkodiconnect 30607]</label>
|
||||
<font>font12</font>
|
||||
<align>center</align>
|
||||
<aligny>top</aligny>
|
||||
<top>170</top>
|
||||
|
@ -202,10 +202,10 @@
|
|||
<height>150</height>
|
||||
<control type="button" id="205">
|
||||
<visible>True</visible>
|
||||
<description>Sign in 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 30600][/B][/UPPERCASE]</label>
|
||||
<description>Toggle plex.tv sign-in</description>
|
||||
<texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus>
|
||||
<texturefocus border="5" colordiffuse="ff525252">box.png</texturefocus>
|
||||
<label>[UPPERCASE][B]$ADDON[plugin.video.plexkodiconnect 30600][/UPPERCASE][/B]</label>
|
||||
<font>font10</font>
|
||||
<textcolor>ffa6a6a6</textcolor>
|
||||
<focusedcolor>white</focusedcolor>
|
||||
|
@ -221,7 +221,7 @@
|
|||
<description>Manually add server</description>
|
||||
<texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus>
|
||||
<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>
|
||||
<textcolor>ffa6a6a6</textcolor>
|
||||
<focusedcolor>white</focusedcolor>
|
||||
|
@ -239,7 +239,7 @@
|
|||
<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>
|
||||
<label>[UPPERCASE][B]$ADDON[plugin.video.plexkodiconnect 30602][/UPPERCASE][/B]</label>
|
||||
<font>font10</font>
|
||||
<textcolor>ffa6a6a6</textcolor>
|
||||
<focusedcolor>white</focusedcolor>
|
Binary file not shown.
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 8.2 KiB |
17
service.py
17
service.py
|
@ -30,8 +30,7 @@ sys_path.append(_base_resource)
|
|||
|
||||
###############################################################################
|
||||
|
||||
from utils import settings, window, language as lang, dialog, tryEncode, \
|
||||
tryDecode
|
||||
from utils import settings, window, language as lang, dialog, tryDecode
|
||||
from userclient import UserClient
|
||||
import initialsetup
|
||||
from kodimonitor import KodiMonitor
|
||||
|
@ -40,8 +39,8 @@ import videonodes
|
|||
from websocket_client import PMS_Websocket, Alexa_Websocket
|
||||
import downloadutils
|
||||
from playqueue import Playqueue
|
||||
from connectmanager import ConnectManager, check_connection
|
||||
|
||||
import PlexAPI
|
||||
from PlexCompanion import PlexCompanion
|
||||
from command_pipeline import Monitor_Window
|
||||
from playback_starter import Playback_Starter
|
||||
|
@ -152,7 +151,7 @@ class Service():
|
|||
self.command_pipeline.start()
|
||||
|
||||
# Server auto-detect
|
||||
initialsetup.InitialSetup().setup()
|
||||
initialsetup.setup()
|
||||
|
||||
# Initialize important threads, handing over self for callback purposes
|
||||
self.user = UserClient(self)
|
||||
|
@ -165,8 +164,6 @@ class Service():
|
|||
if settings('enableTextureCache') == "true":
|
||||
self.image_cache_thread = Image_Cache_Thread()
|
||||
|
||||
plx = PlexAPI.PlexAPI()
|
||||
|
||||
welcome_msg = True
|
||||
counter = 0
|
||||
while not __stop_PKC():
|
||||
|
@ -260,7 +257,7 @@ class Service():
|
|||
if server is False:
|
||||
# No server info set in add-on settings
|
||||
pass
|
||||
elif plx.CheckConnection(server, verifySSL=True) is False:
|
||||
elif check_connection(server, verifySSL=True) is False:
|
||||
# Server is offline or cannot be reached
|
||||
# Alert the user and suppress future warning
|
||||
if self.server_online:
|
||||
|
@ -279,10 +276,10 @@ class Service():
|
|||
# Periodically check if the IP changed, e.g. per minute
|
||||
if counter > 20:
|
||||
counter = 0
|
||||
setup = initialsetup.InitialSetup()
|
||||
tmp = setup.PickPMS()
|
||||
connectmanager = ConnectManager()
|
||||
tmp = connectmanager.pick_pms()
|
||||
if tmp is not None:
|
||||
setup.WritePMStoSettings(tmp)
|
||||
connectmanager.write_pms_to_settings(tmp)
|
||||
else:
|
||||
# Server is online
|
||||
counter = 0
|
||||
|
|
Loading…
Reference in a new issue