#!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import absolute_import, division, unicode_literals import logging import sys import platform import uuid import json import threading import time import requests import xbmc from .plexnet import plexapp, myplex from . import util LOG = logging.getLogger('PLEX.plex') class PlexTimer(plexapp.Timer): def shouldAbort(self): return xbmc.abortRequested def abortFlag(): return util.MONITOR.abortRequested() plexapp.setTimer(PlexTimer) plexapp.setAbortFlagFunction(abortFlag) maxVideoRes = plexapp.Res((3840, 2160)) # INTERFACE.globals["supports4k"] and plexapp.Res((3840, 2160)) or plexapp.Res((1920, 1080)) CLIENT_ID = util.getSetting('client.ID') if not CLIENT_ID: CLIENT_ID = str(uuid.uuid4()) util.setSetting('client.ID', CLIENT_ID) def defaultUserAgent(): """Return a string representing the default user agent.""" _implementation = platform.python_implementation() if _implementation == 'CPython': _implementation_version = platform.python_version() elif _implementation == 'PyPy': _implementation_version = '%s.%s.%s' % (sys.pypy_version_info.major, sys.pypy_version_info.minor, sys.pypy_version_info.micro) if sys.pypy_version_info.releaselevel != 'final': _implementation_version = ''.join([_implementation_version, sys.pypy_version_info.releaselevel]) elif _implementation == 'Jython': _implementation_version = platform.python_version() # Complete Guess elif _implementation == 'IronPython': _implementation_version = platform.python_version() # Complete Guess else: _implementation_version = 'Unknown' try: p_system = platform.system() p_release = platform.release() except IOError: p_system = 'Unknown' p_release = 'Unknown' return " ".join(['%s/%s' % ('Plex-for-Kodi', util.ADDON.getAddonInfo('version')), '%s/%s' % ('Kodi', xbmc.getInfoLabel('System.BuildVersion').replace(' ', '-')), '%s/%s' % (_implementation, _implementation_version), '%s/%s' % (p_system, p_release)]) class PlexInterface(plexapp.AppInterface): _regs = { None: {}, } _globals = { 'platform': 'Kodi', 'appVersionStr': util.ADDON.getAddonInfo('version'), 'clientIdentifier': CLIENT_ID, 'platformVersion': xbmc.getInfoLabel('System.BuildVersion'), 'product': 'Plex for Kodi', 'provides': 'player', 'device': util.getPlatform() or plexapp.PLATFORM, 'model': 'Unknown', 'friendlyName': 'Kodi Add-on ({0})'.format(platform.node()), 'supports1080p60': True, 'vp9Support': True, 'transcodeVideoQualities': [ "10", "20", "30", "30", "40", "60", "60", "75", "100", "60", "75", "90", "100", "100" ], 'transcodeVideoResolutions': [ plexapp.Res((220, 180)), plexapp.Res((220, 128)), plexapp.Res((284, 160)), plexapp.Res((420, 240)), plexapp.Res((576, 320)), plexapp.Res((720, 480)), plexapp.Res((1024, 768)), plexapp.Res((1280, 720)), plexapp.Res((1280, 720)), maxVideoRes, maxVideoRes, maxVideoRes, maxVideoRes, maxVideoRes ], 'transcodeVideoBitrates': [ "64", "96", "208", "320", "720", "1500", "2000", "3000", "4000", "8000", "10000", "12000", "20000", "200000" ], 'deviceInfo': plexapp.DeviceInfo() } def getPreference(self, pref, default=None): if pref == 'manual_connections': return self.getManualConnections() else: return util.getSetting(pref, default) def getManualConnections(self): conns = [] for i in range(2): ip = util.getSetting('manual_ip_{0}'.format(i)) if not ip: continue port = util.getSetting('manual_port_{0}'.format(i), 32400) conns.append({'connection': ip, 'port': port}) return json.dumps(conns) def setPreference(self, pref, value): util.setSetting(pref, value) def getRegistry(self, reg, default=None, sec=None): if sec == 'myplex' and reg == 'MyPlexAccount': ret = util.getSetting('{0}.{1}'.format(sec, reg), default) if ret: return ret return json.dumps({'authToken': util.getSetting('auth.token')}) else: return util.getSetting('{0}.{1}'.format(sec, reg), default) def setRegistry(self, reg, value, sec=None): util.setSetting('{0}.{1}'.format(sec, reg), value) def clearRegistry(self, reg, sec=None): util.setSetting('{0}.{1}'.format(sec, reg), '') def addInitializer(self, sec): pass def clearInitializer(self, sec): pass def getGlobal(self, glbl, default=None): if glbl == 'transcodeVideoResolutions': maxres = self.getPreference('allow_4k', True) and plexapp.Res((3840, 2160)) or plexapp.Res((1920, 1080)) self._globals['transcodeVideoResolutions'][-5:] = [maxres] * 5 return self._globals.get(glbl, default) def getCapabilities(self): return '' def LOG(self, msg): LOG.debug('API: %s', msg) def DEBUG_LOG(self, msg): LOG.debug('API: %s', msg) def WARN_LOG(self, msg): LOG.warn('API: %s', msg) def ERROR_LOG(self, msg): LOG.error('API: %s', msg) def ERROR(self, msg=None, err=None): if err: LOG.error('%s - %s', msg, err.message) else: util.ERROR() def supportsAudioStream(self, codec, channels): return True # if codec = invalid then return true # canDownmix = (m.globals["audioDownmix"][codec] <> invalid) # supportsSurroundSound = m.SupportsSurroundSound() # if not supportsSurroundSound and canDownmix then # maxChannels = m.globals["audioDownmix"][codec] # else # maxChannels = firstOf(m.globals["audioDecoders"][codec], 0) # end if # if maxChannels > 2 and not canDownmix and not supportsSurroundSound then # ' It's a surround sound codec and we can't do surround sound # supported = false # else if maxChannels = 0 or maxChannels < channels then # ' The codec is either unsupported or can't handle the requested channels # supported = false # else # supported = true # return supported def supportsSurroundSound(self): return True def getQualityIndex(self, qualityType): if qualityType == self.QUALITY_LOCAL: return self.getPreference("local_quality", 13) elif qualityType == self.QUALITY_ONLINE: return self.getPreference("online_quality", 8) else: return self.getPreference("remote_quality", 13) def getMaxResolution(self, quality_type, allow4k=False): qualityIndex = self.getQualityIndex(quality_type) if qualityIndex >= 9: if self.getPreference('allow_4k', True): return allow4k and 2160 or 1088 else: return 1088 elif qualityIndex >= 6: return 720 elif qualityIndex >= 5: return 480 else: return 360 plexapp.setInterface(PlexInterface()) plexapp.setUserAgent(defaultUserAgent()) class CallbackEvent(plexapp.CompatEvent): def __init__(self, context, signal, timeout=15, *args, **kwargs): threading._Event.__init__(self, *args, **kwargs) self.start = time.time() self.context = context self.signal = signal self.timeout = timeout self.context.on(self.signal, self.set) def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.wait() def __repr__(self): return '<{0}:{1}>'.format(self.__class__.__name__, self.signal) def set(self, **kwargs): threading._Event.set(self) def wait(self): if not threading._Event.wait(self, self.timeout): LOG.debug('%s: TIMED-OUT', self) self.close() def triggeredOrTimedOut(self, timeout=None): try: if time.time() - self.start() > self.timeout: LOG.debug('%s: TIMED-OUT', self) return True if timeout: threading._Event.wait(self, timeout) finally: return self.isSet() def close(self): self.set() self.context.off(self.signal, self.set) def init(): LOG.info('Initializing') with CallbackEvent(plexapp.APP, 'init'): plexapp.init() LOG.info('Waiting for account initialization...') retry = True while retry: retry = False if not plexapp.ACCOUNT.authToken: token = authorize() if not token: LOG.info('FAILED TO AUTHORIZE') return False with CallbackEvent(plexapp.APP, 'account:response'): plexapp.ACCOUNT.validateToken(token) LOG.info('Waiting for account initialization') # if not PLEX: # util.messageDialog('Connection Error', u'Unable to connect to any servers') # util.DEBUG_LOG('SIGN IN: Failed to connect to any servers') # return False # util.DEBUG_LOG('SIGN IN: Connected to server: {0} - {1}'.format(PLEX.friendlyName, PLEX.baseuri)) success = requirePlexPass() if success == 'RETRY': retry = True continue return success def requirePlexPass(): return True # if not plexapp.ACCOUNT.hasPlexPass(): # from windows import signin, background # background.setSplash(False) # w = signin.SignInPlexPass.open() # retry = w.retry # del w # util.DEBUG_LOG('PlexPass required. Signing out...') # plexapp.ACCOUNT.signOut() # plexapp.SERVERMANAGER.clearState() # if retry: # return 'RETRY' # else: # return False # return True def authorize(): from .windows import background with background.BackgroundContext(function=_authorize) as win: return win.result def _authorize(): from .windows import signin, background background.setSplash(False) back = signin.Background.create() pre = signin.PreSignInWindow.open() try: if not pre.doSignin: return None finally: del pre try: while True: pinLoginWindow = signin.PinLoginWindow.create() try: pl = myplex.PinLogin() except requests.ConnectionError: util.ERROR() util.messageDialog(util.T(32427, 'Failed'), util.T(32449, 'Sign-in failed. Cound not connect to plex.tv')) return pinLoginWindow.setPin(pl.pin) try: pl.startTokenPolling() while not pl.finished(): if pinLoginWindow.abort: util.DEBUG_LOG('SIGN IN: Pin login aborted') pl.abort() return None xbmc.sleep(100) else: if not pl.expired(): if pl.authenticationToken: pinLoginWindow.setLinking() return pl.authenticationToken else: return None finally: pinLoginWindow.doClose() del pinLoginWindow if pl.expired(): util.DEBUG_LOG('SIGN IN: Pin expired') expiredWindow = signin.ExpiredWindow.open() try: if not expiredWindow.refresh: util.DEBUG_LOG('SIGN IN: Pin refresh aborted') return None finally: del expiredWindow finally: back.doClose() del back