Code refactoring

This commit is contained in:
croneter 2018-02-10 17:59:20 +01:00
parent 4fca4ecf63
commit 1151076660
8 changed files with 1086 additions and 1404 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,21 +1,31 @@
# -*- coding: utf-8 -*-
from logging import getLogger
from urllib import urlencode
from urllib import urlencode, quote_plus
from ast import literal_eval
from urlparse import urlparse, parse_qsl
import re
from re import compile as re_compile
from copy import deepcopy
from time import time
from threading import Thread
import downloadutils
from utils import settings
from xbmc import sleep
from downloadutils import DownloadUtils as DU
from utils import settings, tryEncode, tryDecode
from variables import PLEX_TO_KODI_TIMEFACTOR
import plex_tv
###############################################################################
log = getLogger("PLEX."+__name__)
LOG = getLogger("PLEX." + __name__)
CONTAINERSIZE = int(settings('limitindex'))
REGEX_PLEX_KEY = re.compile(r'''/(.+)/(\d+)$''')
REGEX_PLEX_KEY = re_compile(r'''/(.+)/(\d+)$''')
# For discovery of PMS in the local LAN
PLEX_GDM_IP = '239.0.0.250' # multicast to PMS
PLEX_GDM_PORT = 32414
PLEX_GDM_MSG = 'M-SEARCH * HTTP/1.0'
###############################################################################
@ -76,6 +86,374 @@ def GetMethodFromPlexType(plexType):
return methods[plexType]
def GetPlexLoginFromSettings():
"""
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')
}
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
"""
# Add '/clients' to URL because then an authentication is necessary
# If a plex.tv URL was passed, this does not work.
header_options = None
if token is not None:
header_options = {'X-Plex-Token': token}
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 = DU().downloadUrl(url,
authenticate=False,
headerOptions=header_options,
verifySSL=verifySSL,
timeout=10)
if answer is None:
LOG.debug("Could not connect to %s", url)
return False
try:
# xml received?
answer.attrib
except AttributeError:
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 discover_pms(token=None):
"""
Optional parameter:
token token for plex.tv
Returns a list of available PMS to connect to, one entry is the dict:
{
'machineIdentifier' [str] unique identifier of the PMS
'name' [str] name of the PMS
'token' [str] token needed to access that PMS
'ownername' [str] name of the owner of this PMS or None if
the owner itself supplied tries to connect
'product' e.g. 'Plex Media Server' or None
'version' e.g. '1.11.2.4772-3e...' or None
'device': e.g. 'PC' or 'Windows' or None
'platform': e.g. 'Windows', 'Android' or None
'local' [bool] True if plex.tv supplied
'publicAddressMatches'='1'
or if found using Plex GDM in the local LAN
'owned' [bool] True if it's the owner's PMS
'relay' [bool] True if plex.tv supplied 'relay'='1'
'presence' [bool] True if plex.tv supplied 'presence'='1'
'httpsRequired' [bool] True if plex.tv supplied
'httpsRequired'='1'
'scheme' [str] either 'http' or 'https'
'ip': [str] IP of the PMS, e.g. '192.168.1.1'
'port': [str] Port of the PMS, e.g. '32400'
'baseURL': [str] <scheme>://<ip>:<port> of the PMS
}
"""
LOG.info('Start discovery of Plex Media Servers')
# Look first for local PMS in the LAN
local_pms_list = _plex_gdm()
LOG.debug('PMS found in the local LAN using Plex GDM: %s', local_pms_list)
# Get PMS from plex.tv
if token:
LOG.info('Checking with plex.tv for more PMS to connect to')
plex_pms_list = _pms_list_from_plex_tv(token)
LOG.debug('PMS found on plex.tv: %s', plex_pms_list)
else:
LOG.info('No plex token supplied, only checked LAN for available PMS')
plex_pms_list = []
# See if we found a PMS both locally and using plex.tv. If so, use local
# connection data
all_pms = []
for pms in local_pms_list:
for i, plex_pms in enumerate(plex_pms_list):
if pms['machineIdentifier'] == plex_pms['machineIdentifier']:
# Update with GDM data - potentially more reliable than plex.tv
LOG.debug('Found this PMS also in the LAN: %s', plex_pms)
plex_pms['ip'] = pms['ip']
plex_pms['port'] = pms['port']
plex_pms['local'] = True
# Use all the other data we know from plex.tv
pms = plex_pms
# Remove this particular pms since we already know it
plex_pms_list.pop(i)
break
https = _pms_https_enabled('%s:%s' % (pms['ip'], pms['port']))
if https is None:
# Error contacting url. Skip and ignore this PMS for now
continue
elif https is True:
pms['scheme'] = 'https'
pms['baseURL'] = 'https://%s:%s' % (pms['ip'], pms['port'])
else:
pms['scheme'] = 'http'
pms['baseURL'] = 'http://%s:%s' % (pms['ip'], pms['port'])
all_pms.append(pms)
# Now add the remaining PMS from plex.tv (where we already checked connect.)
for plex_pms in plex_pms_list:
all_pms.append(plex_pms)
LOG.debug('Found the following PMS in total: %s', all_pms)
return all_pms
def _plex_gdm():
"""
PlexGDM - looks for PMS in the local LAN and returns a list of the PMS found
"""
# Import here because we might not need to do gdm because we already
# connected to a PMS successfully in the past
import struct
import socket
# 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)
return_data = []
try:
# Send data to the multicast group
gdm.sendto(PLEX_GDM_MSG, (PLEX_GDM_IP, PLEX_GDM_PORT))
# Look for responses from all recipients
while True:
try:
data, server = gdm.recvfrom(1024)
return_data.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()
LOG.debug('Plex GDM returned the data: %s', return_data)
pms_list = []
for response in return_data:
# Check if we had a positive HTTP response
if '200 OK' not in response['data']:
continue
pms = {
'ip': response['from'][0],
'scheme': None,
'local': True, # Since we found it using GDM
'product': None,
'baseURL': None,
'name': None,
'version': None,
'token': None,
'ownername': None,
'device': None,
'platform': None,
'owned': None,
'relay': None,
'presence': True, # Since we're talking to the PMS
'httpsRequired': None,
}
for line in response['data'].split('\n'):
if 'Content-Type:' in line:
pms['product'] = tryDecode(line.split(':')[1].strip())
elif 'Host:' in line:
pms['baseURL'] = line.split(':')[1].strip()
elif 'Name:' in line:
pms['name'] = tryDecode(line.split(':')[1].strip())
elif 'Port:' in line:
pms['port'] = line.split(':')[1].strip()
elif 'Resource-Identifier:' in line:
pms['machineIdentifier'] = line.split(':')[1].strip()
elif 'Version:' in line:
pms['version'] = line.split(':')[1].strip()
pms_list.append(pms)
return pms_list
def _pms_list_from_plex_tv(token):
"""
get Plex media Server List from plex.tv/pms/resources
"""
xml = DU().downloadUrl('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
from Queue import Queue
queue = Queue()
thread_queue = []
max_age_in_seconds = 2*60*60*24
for device in xml.findall('Device'):
if 'server' not in device.get('provides'):
# No PMS - skip
continue
if device.find('Connection') is None:
# no valid connection - skip
continue
# check MyPlex data age - skip if >2 days
info_age = time() - int(device.get('lastSeenAt'))
if info_age > max_age_in_seconds:
LOG.debug("Skip server %s not seen for 2 days", device.get('name'))
continue
pms = {
'machineIdentifier': device.get('clientIdentifier'),
'name': device.get('name'),
'token': device.get('accessToken'),
'ownername': device.get('sourceTitle'),
'product': device.get('product'), # e.g. 'Plex Media Server'
'version': device.get('productVersion'), # e.g. '1.11.2.4772-3e...'
'device': device.get('device'), # e.g. 'PC' or 'Windows'
'platform': device.get('platform'), # e.g. 'Windows', 'Android'
'local': device.get('publicAddressMatches') == '1',
'owned': device.get('owned') == '1',
'relay': device.get('relay') == '1',
'presence': device.get('presence') == '1',
'httpsRequired': device.get('httpsRequired') == '1',
'connections': []
}
# Try a local connection first, no matter what plex.tv tells us
for connection in device.findall('Connection'):
if connection.get('local') == '1':
pms['connections'].append(connection)
# Then try non-local
for connection in device.findall('Connection'):
if connection.get('local') != '1':
pms['connections'].append(connection)
# Spawn threads to ping each PMS simultaneously
thread = Thread(target=_poke_pms, args=(pms, queue))
thread_queue.append(thread)
max_threads = 5
threads = []
# poke PMS, own thread for each PMS
while True:
# Remove finished threads
for thread in threads:
if not thread.isAlive():
threads.remove(thread)
if len(threads) < max_threads:
try:
thread = thread_queue.pop()
except IndexError:
# We have done our work
break
else:
thread.start()
threads.append(thread)
else:
sleep(50)
# wait for requests being answered
for thread in threads:
thread.join()
# declare new PMSs
pms_list = []
while not queue.empty():
pms = queue.get()
del pms['connections']
pms_list.append(pms)
queue.task_done()
return pms_list
def _poke_pms(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 = DU().downloadUrl('%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 pms['connections']:
# Still got connections left, try them
return _poke_pms(pms, queue)
return
else:
# Connection successful - correct pms?
if xml.get('machineIdentifier') == pms['machineIdentifier']:
# 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 GetPlexMetadata(key):
"""
Returns raw API metadata for key as an etree XML.
@ -102,7 +480,7 @@ def GetPlexMetadata(key):
# 'includeConcerts': 1
}
url = url + '?' + urlencode(arguments)
xml = downloadutils.DownloadUtils().downloadUrl(url)
xml = DU().downloadUrl(url)
if xml == 401:
# Either unauthorized (taken care of by doUtils) or PMS under strain
return 401
@ -111,7 +489,7 @@ def GetPlexMetadata(key):
xml.attrib
# Nope we did not receive a valid XML
except AttributeError:
log.error("Error retrieving metadata for %s" % url)
LOG.error("Error retrieving metadata for %s", url)
xml = None
return xml
@ -153,22 +531,21 @@ def DownloadChunks(url):
"""
xml = None
pos = 0
errorCounter = 0
while errorCounter < 10:
error_counter = 0
while error_counter < 10:
args = {
'X-Plex-Container-Size': CONTAINERSIZE,
'X-Plex-Container-Start': pos
}
xmlpart = downloadutils.DownloadUtils().downloadUrl(
url + urlencode(args))
xmlpart = DU().downloadUrl(url + urlencode(args))
# If something went wrong - skip in the hope that it works next time
try:
xmlpart.attrib
except AttributeError:
log.error('Error while downloading chunks: %s'
% (url + urlencode(args)))
LOG.error('Error while downloading chunks: %s',
url + urlencode(args))
pos += CONTAINERSIZE
errorCounter += 1
error_counter += 1
continue
# Very first run: starting xml (to retain data in xml's root!)
@ -186,8 +563,8 @@ def DownloadChunks(url):
if len(xmlpart) < CONTAINERSIZE:
break
pos += CONTAINERSIZE
if errorCounter == 10:
log.error('Fatal error while downloading chunks for %s' % url)
if error_counter == 10:
LOG.error('Fatal error while downloading chunks for %s', url)
return None
return xml
@ -235,8 +612,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 DU().downloadUrl('{server}/library/sections')
def init_plex_playqueue(itemid, librarySectionUUID, mediatype='movie',
@ -255,17 +631,16 @@ def init_plex_playqueue(itemid, librarySectionUUID, mediatype='movie',
}
if trailers is True:
args['extrasPrefixCount'] = settings('trailerNumber')
xml = downloadutils.DownloadUtils().downloadUrl(
url + '?' + urlencode(args), action_type="POST")
xml = DU().downloadUrl(url + '?' + urlencode(args), action_type="POST")
try:
xml[0].tag
except (IndexError, TypeError, AttributeError):
log.error("Error retrieving metadata for %s" % url)
LOG.error("Error retrieving metadata for %s", url)
return None
return xml
def PMSHttpsEnabled(url):
def _pms_https_enabled(url):
"""
Returns True if the PMS can talk https, False otherwise.
None if error occured, e.g. the connection timed out
@ -277,21 +652,20 @@ def PMSHttpsEnabled(url):
Prefers HTTPS over HTTP
"""
doUtils = downloadutils.DownloadUtils().downloadUrl
res = doUtils('https://%s/identity' % url,
authenticate=False,
verifySSL=False)
res = DU().downloadUrl('https://%s/identity' % url,
authenticate=False,
verifySSL=False)
try:
res.attrib
except AttributeError:
# Might have SSL deactivated. Try with http
res = doUtils('http://%s/identity' % url,
authenticate=False,
verifySSL=False)
res = DU().downloadUrl('http://%s/identity' % url,
authenticate=False,
verifySSL=False)
try:
res.attrib
except AttributeError:
log.error("Could not contact PMS %s" % url)
LOG.error("Could not contact PMS %s", url)
return None
else:
# Received a valid XML. Server wants to talk HTTP
@ -307,17 +681,17 @@ def GetMachineIdentifier(url):
Returns None if something went wrong
"""
xml = downloadutils.DownloadUtils().downloadUrl('%s/identity' % url,
authenticate=False,
verifySSL=False,
timeout=10)
xml = DU().downloadUrl('%s/identity' % url,
authenticate=False,
verifySSL=False,
timeout=10)
try:
machineIdentifier = xml.attrib['machineIdentifier']
except (AttributeError, KeyError):
log.error('Could not get the PMS machineIdentifier for %s' % url)
LOG.error('Could not get the PMS machineIdentifier for %s', url)
return None
log.debug('Found machineIdentifier %s for the PMS %s'
% (machineIdentifier, url))
LOG.debug('Found machineIdentifier %s for the PMS %s',
machineIdentifier, url)
return machineIdentifier
@ -337,9 +711,8 @@ def GetPMSStatus(token):
or an empty dict.
"""
answer = {}
xml = downloadutils.DownloadUtils().downloadUrl(
'{server}/status/sessions',
headerOptions={'X-Plex-Token': token})
xml = DU().downloadUrl('{server}/status/sessions',
headerOptions={'X-Plex-Token': token})
try:
xml.attrib
except AttributeError:
@ -377,8 +750,8 @@ def scrobble(ratingKey, state):
url = "{server}/:/unscrobble?" + urlencode(args)
else:
return
downloadutils.DownloadUtils().downloadUrl(url)
log.info("Toggled watched state for Plex item %s" % ratingKey)
DU().downloadUrl(url)
LOG.info("Toggled watched state for Plex item %s", ratingKey)
def delete_item_from_pms(plexid):
@ -388,24 +761,76 @@ def delete_item_from_pms(plexid):
Returns True if successful, False otherwise
"""
if downloadutils.DownloadUtils().downloadUrl(
'{server}/library/metadata/%s' % plexid,
action_type="DELETE") is True:
log.info('Successfully deleted Plex id %s from the PMS' % plexid)
if DU().downloadUrl('{server}/library/metadata/%s' % plexid,
action_type="DELETE") is True:
LOG.info('Successfully deleted Plex id %s from the PMS', plexid)
return True
else:
log.error('Could not delete Plex id %s from the PMS' % plexid)
return False
LOG.error('Could not delete Plex id %s from the PMS', plexid)
return False
def get_PMS_settings(url, token):
"""
Retrieve the PMS' settings via <url>/:/
Retrieve the PMS' settings via <url>/:/prefs
Call with url: scheme://ip:port
"""
return downloadutils.DownloadUtils().downloadUrl(
return DU().downloadUrl(
'%s/:/prefs' % url,
authenticate=False,
verifySSL=False,
headerOptions={'X-Plex-Token': token} if token else None)
def GetUserArtworkURL(username):
"""
Returns the URL for the user's Avatar. Or False if something went
wrong.
"""
users = plex_tv.list_home_users(settings('plexToken'))
url = ''
# If an error is encountered, set to False
if not users:
LOG.info("Couldnt get user from plex.tv. No URL for user avatar")
return False
for user in users:
if username in user['title']:
url = user['thumb']
LOG.debug("Avatar url for user %s is: %s", username, url)
return url
def transcode_image_path(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...
transcode_path = ('/photo/:/transcode/%sx%s/%s'
% (width, height, quote_plus(path)))
args = {
'width': width,
'height': height,
'url': path
}
if AuthToken:
args['X-Plex-Token'] = AuthToken
return transcode_path + '?' + urlencode(args)

View file

@ -40,7 +40,7 @@ def chooseServer():
import initialsetup
setup = initialsetup.InitialSetup()
server = setup.PickPMS(showDialog=True)
server = setup.pick_pms(showDialog=True)
if server is None:
log.error('We did not connect to a new PMS, aborting')
plex_command('SUSPEND_USER_CLIENT', 'False')
@ -48,7 +48,7 @@ def chooseServer():
return
log.info("User chose server %s" % server['name'])
setup.WritePMStoSettings(server)
setup.write_pms_to_settings(server)
if not __LogOut():
return
@ -87,7 +87,7 @@ def togglePlexTV():
else:
log.info('Login to plex.tv')
import initialsetup
initialsetup.InitialSetup().PlexTVSignIn()
initialsetup.InitialSetup().plex_tv_sign_in()
dialog('notification',
lang(29999),
lang(39221),

View file

@ -4,17 +4,16 @@ from logging import getLogger
from Queue import Queue
import xml.etree.ElementTree as etree
import xbmc
import xbmcgui
from xbmc import executebuiltin, translatePath
from utils import settings, window, language as lang, tryEncode, tryDecode, \
XmlKodiSetting, reboot_kodi
XmlKodiSetting, reboot_kodi, dialog
from migration import check_migration
from downloadutils import DownloadUtils as DU
from userclient import UserClient
from clientinfo import getDeviceId
from PlexAPI import PlexAPI
from PlexFunctions import GetMachineIdentifier, get_PMS_settings
import PlexFunctions as PF
import plex_tv
from json_rpc import get_setting, set_setting
import playqueue as PQ
from videonodes import VideoNodes
@ -77,7 +76,7 @@ def reload_pkc():
set_webserver()
# To detect Kodi profile switches
window('plex_kodiProfile',
value=tryDecode(xbmc.translatePath("special://profile")))
value=tryDecode(translatePath("special://profile")))
getDeviceId()
# Initialize the PKC playqueues
PQ.init_playqueues()
@ -115,48 +114,67 @@ def set_webserver():
state.WEBSERVER_PASSWORD = get_setting('services.webserverpassword')
class InitialSetup():
def _write_pms_settings(url, token):
"""
Sets certain settings for server by asking for the PMS' settings
Call with url: scheme://ip:port
"""
xml = PF.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'))
class InitialSetup(object):
"""
Will load Plex PMS settings (e.g. address) and token
Will ask the user initial questions on first PKC boot
"""
def __init__(self):
LOG.debug('Entering initialsetup class')
self.plx = PlexAPI()
self.dialog = xbmcgui.Dialog()
self.server = UserClient().getServer()
self.serverid = settings('plex_machineIdentifier')
# Get Plex credentials from settings file, if they exist
plexdict = self.plx.GetPlexLoginFromSettings()
plexdict = PF.GetPlexLoginFromSettings()
self.myplexlogin = plexdict['myplexlogin'] == 'true'
self.plexLogin = plexdict['plexLogin']
self.plexToken = plexdict['plexToken']
self.plex_login = plexdict['plexLogin']
self.plex_token = plexdict['plexToken']
self.plexid = plexdict['plexid']
# Token for the PMS, not plex.tv
self.pms_token = settings('accessToken')
if self.plexToken:
if self.plex_token:
LOG.debug('Found a plex.tv token in the settings')
def PlexTVSignIn(self):
def plex_tv_sign_in(self):
"""
Signs (freshly) in to plex.tv (will be saved to file settings)
Returns True if successful, or False if not
"""
result = self.plx.PlexTvSignInWithPin()
result = plex_tv.sign_in_with_pin()
if result:
self.plexLogin = result['username']
self.plexToken = result['token']
self.plex_login = result['username']
self.plex_token = result['token']
self.plexid = result['plexid']
return True
return False
def CheckPlexTVSignIn(self):
def check_plex_tv_sign_in(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)
chk = PF.check_connection('plex.tv', token=self.plex_token)
if chk in (401, 403):
# HTTP Error: unauthorized. Token is no longer valid
LOG.info('plex.tv connection returned HTTP %s', str(chk))
@ -164,13 +182,13 @@ class InitialSetup():
settings('plexToken', value='')
settings('plexLogin', value='')
# Could not login, please try again
self.dialog.ok(lang(29999), lang(39009))
answer = self.PlexTVSignIn()
dialog('ok', lang(29999), lang(39009))
answer = self.plex_tv_sign_in()
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))
dialog('ok', lang(29999), lang(39010))
answer = False
else:
LOG.info('plex.tv connection with token successful')
@ -178,13 +196,13 @@ class InitialSetup():
# Refresh the info from Plex.tv
xml = DU().downloadUrl('https://plex.tv/users/account',
authenticate=False,
headerOptions={'X-Plex-Token': self.plexToken})
headerOptions={'X-Plex-Token': self.plex_token})
try:
self.plexLogin = xml.attrib['title']
self.plex_login = xml.attrib['title']
except (AttributeError, KeyError):
LOG.error('Failed to update Plex info from plex.tv')
else:
settings('plexLogin', value=self.plexLogin)
settings('plexLogin', value=self.plex_login)
home = 'true' if xml.attrib.get('home') == '1' else 'false'
settings('plexhome', value=home)
settings('plexAvatar', value=xml.attrib.get('thumb'))
@ -192,7 +210,7 @@ class InitialSetup():
LOG.info('Updated Plex info from plex.tv')
return answer
def CheckPMS(self):
def check_existing_pms(self):
"""
Check the PMS that was set in file settings.
Will return False if we need to reconnect, because:
@ -203,80 +221,80 @@ class InitialSetup():
not set before
"""
answer = True
chk = self.plx.CheckConnection(self.server, verifySSL=False)
chk = PF.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)
self.serverid = PF.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:
temp_server_id = PF.GetMachineIdentifier(self.server)
if temp_server_id != 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)
self.server, self.serverid, temp_server_id)
answer = False
return answer
def _getServerList(self):
@staticmethod
def _check_pms_connectivity(server):
"""
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
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'])
if server['local']:
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)
chk = PF.check_connection(url,
token=server['token'],
verifySSL=verifySSL)
return chk
def PickPMS(self, showDialog=False):
def pick_pms(self, showDialog=False):
"""
Searches for PMS in local Lan and optionally (if self.plexToken set)
Searches for PMS in local Lan and optionally (if self.plex_token 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
'machineIdentifier' [str] unique identifier of the PMS
'name' [str] name of the PMS
'token' [str] token needed to access that PMS
'ownername' [str] name of the owner of this PMS or None if
the owner itself supplied tries to connect
'product' e.g. 'Plex Media Server' or None
'version' e.g. '1.11.2.4772-3e...' or None
'device': e.g. 'PC' or 'Windows' or None
'platform': e.g. 'Windows', 'Android' or None
'local' [bool] True if plex.tv supplied
'publicAddressMatches'='1'
or if found using Plex GDM in the local LAN
'owned' [bool] True if it's the owner's PMS
'relay' [bool] True if plex.tv supplied 'relay'='1'
'presence' [bool] True if plex.tv supplied 'presence'='1'
'httpsRequired' [bool] True if plex.tv supplied
'httpsRequired'='1'
'scheme' [str] either 'http' or 'https'
'ip': [str] IP of the PMS, e.g. '192.168.1.1'
'port': [str] Port of the PMS, e.g. '32400'
'baseURL': [str] <scheme>://<ip>:<port> of the PMS
}
or None if unsuccessful
"""
server = None
@ -284,44 +302,26 @@ class InitialSetup():
if not self.server or not self.serverid:
showDialog = True
if showDialog is True:
server = self._UserPickPMS()
server = self._user_pick_pms()
else:
server = self._AutoPickPMS()
server = self._auto_pick_pms()
if server is not None:
self._write_PMS_settings(server['baseURL'], server['accesstoken'])
_write_pms_settings(server['baseURL'], server['token'])
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):
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
https_updated = False
checked_plex_tv = False
server = None
while True:
if httpsUpdated is False:
serverlist = self._getServerList()
if https_updated is False:
serverlist = PF.discover_pms(self.plex_token)
for item in serverlist:
if item.get('machineIdentifier') == self.serverid:
server = item
@ -331,26 +331,30 @@ class InitialSetup():
'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
chk = self._check_pms_connectivity(server)
if chk == 504 and https_updated is False:
# switch HTTPS to HTTP or vice-versa
if server['scheme'] == 'https':
server['scheme'] = 'http'
else:
server['scheme'] = 'https'
https_updated = 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:
if self.check_plex_tv_sign_in() is True:
if checked_plex_tv is False:
# Try again
checkedPlexTV = True
httpsUpdated = False
checked_plex_tv = True
https_updated = 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),
dialog('ok',
lang(29999),
'%s %s' % (lang(39214),
tryEncode(server['name'])))
return
else:
@ -364,25 +368,25 @@ class InitialSetup():
server['name'])
return server
def _UserPickPMS(self):
def _user_pick_pms(self):
"""
Lets user pick his/her PMS from a list
Returns server or None if unsuccessful
"""
httpsUpdated = False
https_updated = False
while True:
if httpsUpdated is False:
serverlist = self._getServerList()
if https_updated is False:
serverlist = PF.discover_pms(self.plex_token)
# Exit if no servers found
if len(serverlist) == 0:
if not serverlist:
LOG.warn('No plex media servers found!')
self.dialog.ok(lang(29999), lang(39011))
dialog('ok', lang(29999), lang(39011))
return
# Get a nicer list
dialoglist = []
for server in serverlist:
if server['local'] == '1':
if server['local']:
# server is in the same network as client.
# Add"local"
msg = lang(39022)
@ -399,34 +403,34 @@ class InitialSetup():
dialoglist.append('%s (%s)'
% (server['name'], msg))
# Let user pick server from a list
resp = self.dialog.select(lang(39012), dialoglist)
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:
chk = self._check_pms_connectivity(server)
if chk == 504 and https_updated is False:
# Not able to use HTTP, try HTTPs for now
serverlist[resp]['scheme'] = 'https'
httpsUpdated = True
https_updated = True
continue
httpsUpdated = False
https_updated = 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:
dialog('ok',
lang(29999),
lang(39013) + server['name'],
lang(39014))
if self.plex_tv_sign_in() 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))
answ = dialog('yesno', lang(29999), lang(39015))
# Exit while loop if user chooses No
if not answ:
return
@ -434,30 +438,16 @@ class InitialSetup():
else:
return server
def WritePMStoSettings(self, 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
}
Saves server to file settings
"""
settings('plex_machineIdentifier', server['machineIdentifier'])
settings('plex_servername', server['name'])
settings('plex_serverowned',
'true' if server['owned'] == '1'
else 'false')
settings('plex_serverowned', 'true' if server['owned'] else 'false')
# Careful to distinguish local from remote PMS
if server['local'] == '1':
if server['local']:
scheme = server['scheme']
settings('ipaddress', server['ip'])
settings('port', server['port'])
@ -491,7 +481,6 @@ class InitialSetup():
path.
"""
LOG.info("Initial setup called.")
dialog = self.dialog
try:
with XmlKodiSetting('advancedsettings.xml',
force_create=True,
@ -537,11 +526,13 @@ class InitialSetup():
# Missing smb:// occurences, re-add.
for _ in range(0, count):
source = etree.SubElement(root, 'source')
etree.SubElement(source,
'name').text = "PlexKodiConnect Masterlock Hack"
etree.SubElement(source,
'path',
attrib={'pathversion': "1"}).text = "smb://"
etree.SubElement(
source,
'name').text = "PlexKodiConnect Masterlock Hack"
etree.SubElement(
source,
'path',
attrib={'pathversion': "1"}).text = "smb://"
etree.SubElement(source, 'allowsharing').text = "true"
if reboot is False:
reboot = xml.write_xml
@ -555,23 +546,23 @@ class InitialSetup():
# 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():
if self.check_existing_pms():
LOG.info("Using PMS %s with machineIdentifier %s",
self.server, self.serverid)
self._write_PMS_settings(self.server, self.pms_token)
_write_pms_settings(self.server, self.pms_token)
if reboot is True:
reboot_kodi()
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()
if not self.plex_token and self.myplexlogin:
self.plex_tv_sign_in()
server = self.PickPMS()
server = self.pick_pms()
if server is not None:
# Write our chosen server to Kodi settings file
self.WritePMStoSettings(server)
self.write_pms_to_settings(server)
# User already answered the installation questions
if settings('InstallQuestionsAnswered') == 'true':
@ -581,50 +572,52 @@ class InitialSetup():
# 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)"):
goto_settings = 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)):
if dialog('yesno', heading=lang(29999), line1=lang(39033)):
LOG.debug("User chose to replace paths with smb")
else:
settings('replaceSMB', value="false")
# complete replace all original Plex library paths with custom SMB
if dialog.yesno(heading=lang(29999), line1=lang(39043)):
if dialog('yesno', heading=lang(29999), line1=lang(39043)):
LOG.debug("User chose custom smb paths")
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
dialog('ok', heading=lang(29999), line1=lang(39044))
goto_settings = True
# Go to network credentials?
if dialog.yesno(heading=lang(29999),
line1=lang(39029),
line2=lang(39030)):
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)):
if dialog('yesno', heading=lang(29999), line1=lang(39016)):
LOG.debug("User opted to disable Plex music library.")
settings('enableMusic', value="false")
# Download additional art from FanArtTV
if dialog.yesno(heading=lang(29999), line1=lang(39061)):
if dialog('yesno', heading=lang(29999), line1=lang(39061)):
LOG.debug("User opted to use FanArtTV")
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)):
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")
@ -637,12 +630,13 @@ class InitialSetup():
# Make sure that we only ask these questions upon first installation
settings('InstallQuestionsAnswered', value='true')
if goToSettings is False:
if goto_settings is False:
# Open Settings page now? You will need to restart!
goToSettings = dialog.yesno(heading=lang(29999), line1=lang(39017))
if goToSettings:
goto_settings = dialog('yesno',
heading=lang(29999),
line1=lang(39017))
if goto_settings:
state.PMS_STATUS = 'Stop'
xbmc.executebuiltin(
'Addon.OpenSettings(plugin.video.plexkodiconnect)')
executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)')
elif reboot is True:
reboot_kodi()

338
resources/lib/plex_tv.py Normal file
View file

@ -0,0 +1,338 @@
# -*- coding: utf-8 -*-
from logging import getLogger
from xbmc import sleep, executebuiltin
from downloadutils import DownloadUtils as DU
from utils import dialog, language as lang, settings, tryEncode
import variables as v
import state
###############################################################################
LOG = getLogger("PLEX." + __name__)
###############################################################################
def choose_home_user(token):
"""
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_home_users(token)
if not users:
LOG.error("User download failed.")
return False
userlist = []
userlist_coded = []
for user in users:
username = user['title']
userlist.append(username)
# To take care of non-ASCII usernames
userlist_coded.append(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),
userlist_coded)
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}')
# User chose to cancel
# Plex bug: don't call url for protected user with empty PIN
if not pin:
trials += 1
continue
# Switch to this Plex Home user, if applicable
result = switch_home_user(user['id'],
pin,
token,
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',
heading='{plex}',
line1=lang(39308) + selected_user,
line2=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 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 = DU().downloadUrl(url,
authenticate=False,
action_type="POST",
headerOptions={'X-Plex-Token': token})
try:
answer.attrib
except AttributeError:
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 = DU().downloadUrl(url,
authenticate=False,
headerOptions={'X-Plex-Token': token})
try:
xml.attrib
except AttributeError:
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 == machineIdentifier:
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 list_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 = DU().downloadUrl('https://plex.tv/api/home/users/',
authenticate=False,
headerOptions={'X-Plex-Token': token})
try:
xml.attrib
except AttributeError:
LOG.error('Download of Plex home users failed.')
return False
users = []
for user in xml:
users.append(user.attrib)
return users
def sign_in_with_pin():
"""
Prompts user to sign in by visiting https://plex.tv/pin
Writes to Kodi settings file. Also returns:
{
'plexhome': 'true' if Plex Home, 'false' otherwise
'username':
'avatar': URL to user avator
'token':
'plexid': Plex user ID
'homesize': Number of Plex home users (defaults to '1')
}
Returns False if authentication did not work.
"""
code, identifier = get_pin()
if not code:
# Problems trying to contact plex.tv. Try again later
dialog('ok', heading='{plex}', line1=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',
heading='{plex}',
line1=lang(39304) + "\n\n",
line2=code + "\n\n",
line3=lang(39311))
if not answer:
return False
count = 0
# Wait for approx 30 seconds (since the PIN is not visible anymore :-))
while count < 30:
xml = check_pin(identifier)
if xml is not False:
break
# Wait for 1 seconds
sleep(1000)
count += 1
if xml is False:
# Could not sign in to plex.tv Try again later
dialog('ok', heading='{plex}', line1=lang(39305))
return False
# Parse xml
userid = xml.attrib.get('id')
home = xml.get('home', '0')
if home == '1':
home = 'true'
else:
home = 'false'
username = xml.get('username', '')
avatar = xml.get('thumb', '')
token = xml.findtext('authentication-token')
home_size = xml.get('homeSize', '1')
result = {
'plexhome': home,
'username': username,
'avatar': avatar,
'token': token,
'plexid': userid,
'homesize': home_size
}
settings('plexLogin', username)
settings('plexToken', token)
settings('plexhome', home)
settings('plexid', userid)
settings('plexAvatar', avatar)
settings('plexHomeSize', home_size)
# Let Kodi log into plex.tv on startup from now on
settings('myplexlogin', 'true')
settings('plex_status', value=lang(39227))
return result
def get_pin():
"""
For plex.tv sign-in: returns 4-digit code and identifier as 2 str
"""
code = None
identifier = None
# Download
xml = DU().downloadUrl('https://plex.tv/pins.xml',
authenticate=False,
action_type="POST")
try:
xml.attrib
except AttributeError:
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 check_pin(identifier):
"""
Checks with plex.tv whether user entered the correct PIN on plex.tv/pin
Returns False if not yet done so, or the XML response file as etree
"""
# Try to get a temporary token
xml = DU().downloadUrl('https://plex.tv/pins/%s.xml' % identifier,
authenticate=False)
try:
temp_token = xml.find('auth_token').text
except AttributeError:
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 = DU().downloadUrl('https://plex.tv/users/account',
authenticate=False,
parameters={'X-Plex-Token': temp_token})
return xml

View file

@ -1,25 +1,21 @@
# -*- coding: utf-8 -*-
###############################################################################
from logging import getLogger
from threading import Thread
import xbmc
import xbmcgui
from xbmc import sleep, executebuiltin, translatePath
import xbmcaddon
from xbmcvfs import exists
from utils import window, settings, language as lang, thread_methods
import downloadutils
import PlexAPI
from PlexFunctions import GetMachineIdentifier
from utils import window, settings, language as lang, thread_methods, dialog
from downloadutils import DownloadUtils as DU
import plex_tv
import PlexFunctions as PF
import state
###############################################################################
log = getLogger("PLEX."+__name__)
LOG = getLogger("PLEX." + __name__)
###############################################################################
@ -45,7 +41,7 @@ class UserClient(Thread):
self.userSettings = None
self.addon = xbmcaddon.Addon()
self.doUtils = downloadutils.DownloadUtils()
self.doUtils = DU()
Thread.__init__(self)
@ -55,10 +51,10 @@ class UserClient(Thread):
"""
username = settings('username')
if not username:
log.debug("No username saved, trying to get Plex username")
LOG.debug("No username saved, trying to get Plex username")
username = settings('plexLogin')
if not username:
log.debug("Also no Plex username found")
LOG.debug("Also no Plex username found")
return ""
return username
@ -73,7 +69,7 @@ class UserClient(Thread):
server = host + ":" + port
if not host:
log.debug("No server information saved.")
LOG.debug("No server information saved.")
return False
# If https is true
@ -84,11 +80,11 @@ class UserClient(Thread):
server = "http://%s" % server
# User entered IP; we need to get the machineIdentifier
if self.machineIdentifier == '' and prefix is True:
self.machineIdentifier = GetMachineIdentifier(server)
self.machineIdentifier = PF.GetMachineIdentifier(server)
if self.machineIdentifier is None:
self.machineIdentifier = ''
settings('plex_machineIdentifier', value=self.machineIdentifier)
log.debug('Returning active server: %s' % server)
LOG.debug('Returning active server: %s', server)
return server
def getSSLverify(self):
@ -101,10 +97,10 @@ class UserClient(Thread):
else settings('sslcert')
def setUserPref(self):
log.debug('Setting user preferences')
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 = PF.GetUserArtworkURL(self.currUser)
if url:
window('PlexUserImage', value=url)
# Set resume point max
@ -116,7 +112,7 @@ class UserClient(Thread):
return True
def loadCurrUser(self, username, userId, usertoken, authenticated=False):
log.debug('Loading current user')
LOG.debug('Loading current user')
doUtils = self.doUtils
self.currToken = usertoken
@ -127,18 +123,18 @@ class UserClient(Thread):
if authenticated is False:
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)
LOG.debug('Testing validity of current token')
res = PF.check_connection(self.currServer,
token=self.currToken,
verifySSL=self.ssl)
if res is False:
# PMS probably offline
return False
elif res == 401:
log.error('Token is no longer valid')
LOG.error('Token is no longer valid')
return 401
elif res >= 400:
log.error('Answer from PMS is not as expected. Retrying')
LOG.error('Answer from PMS is not as expected. Retrying')
return False
# Set to windows property
@ -182,31 +178,29 @@ class UserClient(Thread):
return True
def authenticate(self):
log.debug('Authenticating user')
dialog = xbmcgui.Dialog()
LOG.debug('Authenticating user')
# Give attempts at entering password / selecting user
if self.retry >= 2:
log.error("Too many retries to login.")
LOG.error("Too many retries to login.")
state.PMS_STATUS = 'Stop'
dialog.ok(lang(33001),
lang(39023))
xbmc.executebuiltin(
dialog('ok', lang(33001), lang(39023))
executebuiltin(
'Addon.OpenSettings(plugin.video.plexkodiconnect)')
return False
# Get /profile/addon_data
addondir = xbmc.translatePath(self.addon.getAddonInfo('profile'))
addondir = translatePath(self.addon.getAddonInfo('profile'))
# If there's no settings.xml
if not exists("%ssettings.xml" % addondir):
log.error("Error, no settings.xml found.")
LOG.error("Error, no settings.xml found.")
self.auth = False
return False
server = self.getServer()
# If there is no server we can connect to
if not server:
log.info("Missing server information.")
LOG.info("Missing server information.")
self.auth = False
return False
@ -217,7 +211,7 @@ class UserClient(Thread):
enforceLogin = settings('enforceUserLogin')
# Found a user in the settings, try to authenticate
if username and enforceLogin == 'false':
log.debug('Trying to authenticate with old settings')
LOG.debug('Trying to authenticate with old settings')
answ = self.loadCurrUser(username,
userId,
usertoken,
@ -226,21 +220,19 @@ class UserClient(Thread):
# SUCCESS: loaded a user from the settings
return True
elif answ == 401:
log.error("User token no longer valid. Sign user out")
LOG.error("User token no longer valid. Sign user out")
settings('username', value='')
settings('userid', value='')
settings('accessToken', value='')
else:
log.debug("Could not yet authenticate user")
LOG.debug("Could not yet authenticate user")
return False
plx = PlexAPI.PlexAPI()
# Could not use settings - try to get Plex user list from plex.tv
plextoken = settings('plexToken')
if plextoken:
log.info("Trying to connect to plex.tv to get a user list")
userInfo = plx.ChoosePlexHomeUser(plextoken)
LOG.info("Trying to connect to plex.tv to get a user list")
userInfo = plex_tv.choose_home_user(plextoken)
if userInfo is False:
# FAILURE: Something went wrong, try again
self.auth = True
@ -250,7 +242,7 @@ class UserClient(Thread):
userId = userInfo['userid']
usertoken = userInfo['token']
else:
log.info("Trying to authenticate without a token")
LOG.info("Trying to authenticate without a token")
username = ''
userId = ''
usertoken = ''
@ -258,14 +250,13 @@ class UserClient(Thread):
if self.loadCurrUser(username, userId, usertoken, authenticated=False):
# SUCCESS: loaded a user from the settings
return True
else:
# FAILUR: Something went wrong, try again
self.auth = True
self.retry += 1
return False
# Something went wrong, try again
self.auth = True
self.retry += 1
return False
def resetClient(self):
log.debug("Reset UserClient authentication.")
LOG.debug("Reset UserClient authentication.")
self.doUtils.stopSession()
window('plex_authenticated', clear=True)
@ -295,17 +286,17 @@ class UserClient(Thread):
self.retry = 0
def run(self):
log.info("----===## Starting UserClient ##===----")
LOG.info("----===## Starting UserClient ##===----")
thread_stopped = self.thread_stopped
thread_suspended = self.thread_suspended
while not thread_stopped():
while thread_suspended():
if thread_stopped():
break
xbmc.sleep(1000)
sleep(1000)
if state.PMS_STATUS == "Stop":
xbmc.sleep(500)
sleep(500)
continue
# Verify the connection status to server
@ -318,7 +309,7 @@ class UserClient(Thread):
state.PMS_STATUS = 'Auth'
window('plex_serverStatus', value='Auth')
self.resetClient()
xbmc.sleep(3000)
sleep(3000)
if self.auth and (self.currUser is None):
# Try to authenticate user
@ -328,9 +319,9 @@ class UserClient(Thread):
self.auth = False
if self.authenticate():
# Successfully authenticated and loaded a user
log.info("Successfully authenticated!")
log.info("Current user: %s" % self.currUser)
log.info("Current userId: %s" % state.PLEX_USER_ID)
LOG.info("Successfully authenticated!")
LOG.info("Current user: %s", self.currUser)
LOG.info("Current userId: %s", state.PLEX_USER_ID)
self.retry = 0
state.SUSPEND_LIBRARY_THREAD = False
window('plex_serverStatus', clear=True)
@ -344,10 +335,10 @@ class UserClient(Thread):
# Or retried too many times
if server and state.PMS_STATUS != "Stop":
# Only if there's information found to login
log.debug("Server found: %s" % server)
LOG.debug("Server found: %s", server)
self.auth = True
# Minimize CPU load
xbmc.sleep(100)
sleep(100)
log.info("##===---- UserClient Stopped ----===##")
LOG.info("##===---- UserClient Stopped ----===##")

View file

@ -129,22 +129,19 @@ def dialog(typus, *args, **kwargs):
"""
Displays xbmcgui Dialog. Pass a string as typus:
'yesno', 'ok', 'notification', 'input', 'select', 'numeric'
kwargs:
heading='{plex}' title bar (here PlexKodiConnect)
message=lang(30128), Actual dialog content. Don't use with OK
message=lang(30128), Dialog content. Don't use with 'OK', 'yesno'
line1=str(), For 'OK' and 'yesno' dialogs use line1...line3!
time=5000,
sound=True,
nolabel=str(), For 'yesno' dialogs
yeslabel=str(), For 'yesno' dialogs
Icons:
icon='{plex}' Display Plex standard icon
icon='{info}' xbmcgui.NOTIFICATION_INFO
icon='{warning}' xbmcgui.NOTIFICATION_WARNING
icon='{error}' xbmcgui.NOTIFICATION_ERROR
Input Types:
type='{alphanum}' xbmcgui.INPUT_ALPHANUM (standard keyboard)
type='{numeric}' xbmcgui.INPUT_NUMERIC (format: #)
@ -153,9 +150,12 @@ def dialog(typus, *args, **kwargs):
type='{ipaddress}' xbmcgui.INPUT_IPADDRESS (format: #.#.#.#)
type='{password}' xbmcgui.INPUT_PASSWORD
(return md5 hash of input, input is masked)
Options:
option='{password}' xbmcgui.PASSWORD_VERIFY (verifies an existing
(default) md5 hashed password)
option='{hide}' xbmcgui.ALPHANUM_HIDE_INPUT (masks input)
"""
d = xbmcgui.Dialog()
if "icon" in kwargs:
if 'icon' in kwargs:
types = {
'{plex}': 'special://home/addons/plugin.video.plexkodiconnect/icon.png',
'{info}': xbmcgui.NOTIFICATION_INFO,
@ -174,16 +174,23 @@ def dialog(typus, *args, **kwargs):
'{password}': xbmcgui.INPUT_PASSWORD
}
kwargs['type'] = types[kwargs['type']]
if "heading" in kwargs:
if 'option' in kwargs:
types = {
'{password}': xbmcgui.PASSWORD_VERIFY,
'{hide}': xbmcgui.ALPHANUM_HIDE_INPUT
}
kwargs['option'] = types[kwargs['option']]
if 'heading' in kwargs:
kwargs['heading'] = kwargs['heading'].replace("{plex}",
language(29999))
dia = xbmcgui.Dialog()
types = {
'yesno': d.yesno,
'ok': d.ok,
'notification': d.notification,
'input': d.input,
'select': d.select,
'numeric': d.numeric
'yesno': dia.yesno,
'ok': dia.ok,
'notification': dia.notification,
'input': dia.input,
'select': dia.select,
'numeric': dia.numeric
}
return types[typus](*args, **kwargs)

View file

@ -35,7 +35,7 @@ from kodimonitor import KodiMonitor, SpecialMonitor
from librarysync import LibrarySync
from websocket_client import PMS_Websocket, Alexa_Websocket
import PlexAPI
from PlexFunctions import check_connection
from PlexCompanion import PlexCompanion
from command_pipeline import Monitor_Window
from playback_starter import Playback_Starter
@ -116,8 +116,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():
@ -208,7 +206,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:
@ -228,9 +226,9 @@ class Service():
if counter > 20:
counter = 0
setup = initialsetup.InitialSetup()
tmp = setup.PickPMS()
tmp = setup.pick_pms()
if tmp is not None:
setup.WritePMStoSettings(tmp)
setup.write_pms_to_settings(tmp)
else:
# Server is online
counter = 0