977 lines
36 KiB
Python
977 lines
36 KiB
Python
# -*- 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
|