# Copyright (C) 2024 Lunatixz
#
#
# This file is part of PseudoTV Live.
#
# PseudoTV Live is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PseudoTV Live is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with PseudoTV Live. If not, see .
#
# -*- coding: utf-8 -*-
import json, uuid, zlib, base64
from globals import *
from six.moves import urllib
from fileaccess import FileAccess
from json2html import Json2Html
from collections import Counter, OrderedDict
from contextlib import contextmanager, closing
from infotagger.listitem import ListItemInfoTag
def slugify(s, lowercase=False):
if lowercase: s = s.lower()
s = s.strip()
s = re.sub(r'[^\w\s-]', '', s)
s = re.sub(r'[\s_-]+', '_', s)
s = re.sub(r'^-+|-+$', '', s)
return s
def convertString2Num(value):
from ast import literal_eval
try: return literal_eval(value)
except Exception as e:
log("convertString2Num, failed! value = %s %s"%(value,e), xbmc.LOGERROR)
return value
def encodeString(text):
base64_bytes = base64.b64encode(zlib.compress(text.encode(DEFAULT_ENCODING)))
return base64_bytes.decode(DEFAULT_ENCODING)
def decodeString(base64_bytes):
try:
message_bytes = zlib.decompress(base64.b64decode(base64_bytes.encode(DEFAULT_ENCODING)))
return message_bytes.decode(DEFAULT_ENCODING)
except Exception as e: return ''
def getAbbr(text):
words = text.split(' ')
if len(words) > 1: return '%s.%s.'%(words[0][0].upper(),words[1][0].upper())
else: return words[0][0].upper()
def getThumb(item={},opt=0): #unify thumbnail artwork
keys = {0:['landscape','fanart','thumb','thumbnail','poster','clearlogo','logo','logos','clearart','keyart,icon'],
1:['poster','clearlogo','logo','logos','clearart','keyart','landscape','fanart','thumb','thumbnail','icon']}[opt]
for key in keys:
art = (item.get('art',{}).get('album.%s'%(key)) or
item.get('art',{}).get('albumartist.%s'%(key)) or
item.get('art',{}).get('artist.%s'%(key)) or
item.get('art',{}).get('season.%s'%(key)) or
item.get('art',{}).get('tvshow.%s'%(key)) or
item.get('art',{}).get(key) or
item.get(key) or '')
if art: return art
def setDictLST(lst=[]): #set lst of dicts then return
return [loadJSON(s) for s in list(OrderedDict.fromkeys([dumpJSON(d) for d in lst]))]
def dumpJSON(item, idnt=None, sortkey=False, separators=(',', ':')):
try:
if not isinstance(item,str):
return json.dumps(item, indent=idnt, sort_keys=sortkey, separators=separators)
elif isinstance(item,str):
return item
except Exception as e: log("dumpJSON, failed! %s"%(e), xbmc.LOGERROR)
return ''
def loadJSON(item):
try:
if hasattr(item, 'read'):
return json.load(item)
elif item and isinstance(item,str):
return json.loads(item)
elif item and isinstance(item,dict):
return item
except Exception as e: log("loadJSON, failed! %s\n%s"%(e,item), xbmc.LOGERROR)
return {}
def unquoteString(text):
return urllib.parse.unquote(text)
def quoteString(text):
return urllib.parse.quote(text)
def genUUID(seed=None):
if seed:
m = hashlib.md5()
m.update(seed.encode(DEFAULT_ENCODING))
return str(uuid.UUID(m.hexdigest()))
return str(uuid.uuid1(clock_seq=70420))
def getMD5(text,hash=0,hexit=True):
if isinstance(text,dict): text = dumpJSON(text)
elif not isinstance(text,str):text = str(text)
for ch in text: hash = (hash*281 ^ ord(ch)*997) & 0xFFFFFFFF
if hexit: return hex(hash)[2:].upper().zfill(8)
else: return hash
def getCRC32(text):
return binascii.crc32(text.encode('utf8'))
def findItemsInLST(items, values, item_key='getLabel', val_key='', index=True):
if not values: return [-1]
if not isinstance(values,list): values = [values]
matches = []
def _match(fkey,fvalue):
if str(fkey).lower() == str(fvalue).lower():
matches.append(idx if index else item)
for value in values:
if isinstance(value,dict):
value = value.get(val_key,'')
for idx, item in enumerate(items):
if isinstance(item,xbmcgui.ListItem):
if item_key == 'getLabel':
_match(item.getLabel() ,value)
elif item_key == 'getLabel2':
_match(item.getLabel2(),value)
elif item_key == 'getPath':
_match(item.getPath(),value)
elif isinstance(item,dict):
_match(item.get(item_key,''),value)
else: _match(item,value)
return matches
class Settings:
monitor = MONITOR()
def __init__(self):
self.cacheDB = Cache()
self.cache = Cache(mem_cache=True)
def log(self, msg, level=xbmc.LOGDEBUG):
log('%s: %s'%(self.__class__.__name__,msg),level)
@cacheit(expiration=datetime.timedelta(minutes=FIFTEEN))
def getIP(self, default='127.0.0.1'):
IP = (xbmc.getIPAddress() or gethostbyname(gethostname()) or default)
log('getIP, IP = %s'%(IP))
return IP
def getRealSettings(self):
try: return xbmcaddon.Addon(id=ADDON_ID)
except: return REAL_SETTINGS
def updateSettings(self):
self.log('updateSettings')
#todo build json of third-party addon settings
# self.pluginMeta.setdefault(addonID,{})['settings'] = [{'key':'value'}]
def openSettings(self, ctl=(0,1), id=ADDON_ID):
builtin = Builtin()
builtin.closeBusyDialog()
with builtin.busy_dialog():
builtin.executebuiltin(f'Addon.OpenSettings({id})')
xbmc.sleep(100)
builtin.executebuiltin('SetFocus(%i)'%(ctl[0]-200))
xbmc.sleep(50)
builtin.executebuiltin('SetFocus(%i)'%(ctl[1]-180))
del builtin
def openGuide(self, instance=ADDON_NAME):
def __match(label):
items = jsonRPC.getDirectory({"directory":baseURL}).get('files',[])
for item in items:
if label.lower() == item.get('label','').lower(): return item
for item in items:
if item.get('label','').lower().startswith(instance.lower()): return item
with self.builtin.busy_dialog():
from jsonrpc import JSONRPC
jsonRPC = JSONRPC()
baseURL = 'pvr://channels/tv/'
for name in ['%s [All channels]'%(instance), instance, 'All channels']:
item = __match(name)
if item: break
del jsonRPC
if not item: item = {'file':baseURL}
self.log('openGuide, opening %s'%(item.get('file',baseURL)))
self.builtin.executebuiltin("Dialog.Close(all)")
self.builtin.executebuiltin("ReplaceWindow(TVGuide,%s)"%(item.get('file',baseURL)))
#GET
def _getSetting(self, func, key):
try:
value = func(key)
self.log('%s, key = %s, value = %s'%(func.__name__,key,'%s...'%((str(value)[:128]))))
return value
except Exception as e:
self.log("_getSetting, failed! %s - key = %s"%(e,key), xbmc.LOGERROR)
def getSetting(self, key):
return self._getSetting(self.getRealSettings().getSetting,key)
def getSettingBool(self, key):
return self.getSetting(key).lower() == "true"
def getSettingBoolList(self, key):
return [value.lower() == "true" for value in self.getSetting(key).split('|')]
def getSettingInt(self, key):
return convertString2Num(self.getSetting(key))
def getSettingIntList(self, key):
return [convertString2Num(value) for value in self.getSetting(key).split('|')]
def getSettingNumber(self, key):
return convertString2Num(self.getSetting(key))
def getSettingNumberList(self, key):
return [convertString2Num(value) for value in self.getSetting(key).split('|')]
def getSettingString(self, key):
return self.getSetting(key)
def getSettingList(self, key):
return [value for value in self.getSetting(key).split('|')]
def getSettingFloat(self, key):
return float(convertString2Num(self.getSetting(key)))
def getSettingFloatList(self, key):
return [convertString2Num(value) for value in self.getSetting(key).split('|')]
def getSettingDict(self, key):
return loadJSON(decodeString(self.getSetting(key)))
def getCacheSetting(self, key, checksum=ADDON_VERSION, json_data=False, revive=True):
value = self.cacheDB.get(key, checksum, json_data)
if revive: return self.setCacheSetting(key, value, checksum, json_data)
else: return value
def getAddonDetails(self, id):
try:
addon = xbmcaddon.Addon(id)
properties = ['name', 'version', 'summary', 'description', 'path', 'author', 'icon', 'disclaimer', 'fanart', 'changelog', 'id', 'profile', 'stars', 'type']
return dict([(property,addon.getAddonInfo(property)) for property in properties])
except:
from jsonrpc import JSONRPC
return JSONRPC().getAddonDetails(id)
def getEXTSetting(self, id, key):
return xbmcaddon.Addon(id).getSetting(key)
def getFriendlyName(self):
friendly = Properties().getProperty('INSTANCE_NAME')
if not friendly:
from jsonrpc import JSONRPC
friendly = Properties().setProperty('INSTANCE_NAME', JSONRPC().inputFriendlyName())
return friendly
def getMYUUID(self):
friendly = self.getFriendlyName()
uuid = self.getCacheSetting('MY_UUID', checksum=friendly)
if not uuid: uuid = self.setCacheSetting('MY_UUID', genUUID(seed=self.getFriendlyName()), checksum=friendly)
return uuid
def getResetChannels(self):
return (self.getCacheSetting('clearChannels') or [])
#CLR
def clrCacheSetting(self, key):
self.cache.clear(key)
#SET
def _setSetting(self, func, key, value):
try:
self.log('%s, key = %s, value = %s'%(func.__name__,key,'%s...'%((str(value)[:128]))))
return func(key, value)
except Exception as e:
self.log("_setSetting, failed! %s - key = %s"%(e,key), xbmc.LOGERROR)
return False
def setSetting(self, key, value=""):
if not isinstance(value,str): value = str(value)
if self.getSetting(key) != value: #Kodi setsetting() can tax system performance. i/o issue? block redundant saves.
return self._setSetting(self.getRealSettings().setSetting,key,value)
def setSettingBool(self, key, value):
return self.setSetting(key,value)
def setSettingBoolList(self, key, value):
return self.setSetting(key,('|').join(value))
def setSettingInt(self, key, value):
return self.setSetting(key,value)
def setSettingIntList(self, key, value):
return self.setSetting(key,('|').join(value))
def setSettingNumber(self, key, value):
return self.setSetting(key,value)
def setSettingNumberList(self, key, value):
return self.setSetting(key,('|').join(value))
def setSettingString(self, key, value):
return self.setSetting(key,value)
def setSettingList(self, key, values):
return self.setSetting(key,('|').join(value))
def setSettingFloat(self, key, value):
return self.setSetting(key,value)
def setSettingDict(self, key, values):
return self.setSetting(key,encodeString(dumpJSON(values)))
def setCacheSetting(self, key, value, checksum=ADDON_VERSION, json_data=False):
return self.cacheDB.set(key, value, checksum, datetime.timedelta(days=84), json_data)
def setEXTSetting(self, id, key, value):
return xbmcaddon.Addon(id).setSetting(key,value)
def setResetChannels(self, id):
ids = self.getResetChannels()
if isinstance(id, list): ids.extend(id)
else: ids.append(id)
return self.setCacheSetting('clearChannels',list(set(ids)))
@cacheit(expiration=datetime.timedelta(minutes=5), json_data=True)
def getBonjour(self, inclChannels=False):
self.log("getBonjour, inclChannels = %s"%(inclChannels))
payload = {'id' :ADDON_ID,
'uuid' :self.getMYUUID(),
'version' :ADDON_VERSION,
'python' :platform.python_version(),
'machine' :platform.machine(),
'platform':self.builtin.getInfoLabel('OSVersionInfo','System'),
'build' :self.builtin.getInfoLabel('BuildVersion','System'),
'name' :self.getFriendlyName(),
'host' :self.property.getRemoteHost()}
payload['remotes'] = {'bonjour':'http://%s/%s'%(payload['host'],BONJOURFLE),
'remote' :'http://%s/%s'%(payload['host'],REMOTEFLE),
'm3u' :'http://%s/%s'%(payload['host'],M3UFLE),
'xmltv' :'http://%s/%s'%(payload['host'],XMLTVFLE),
'genre' :'http://%s/%s'%(payload['host'],GENREFLE)}
payload['settings'] = {'Resource_Logos' :self.getSetting('Resource_Logos').split('|'),
'Resource_Bumpers' :self.getSetting('Resource_Bumpers').split('|'),
'Resource_Ratings' :self.getSetting('Resource_Ratings').split('|'),
'Resource_Adverts' :self.getSetting('Resource_Adverts').split('|'),
'Resource_Trailers' :self.getSetting('Resource_Trailers').split('|')}
if inclChannels:
from channels import Channels
payload['channels'] = Channels().getChannels()
payload['updated'] = datetime.datetime.fromtimestamp(time.time()).strftime(DTFORMAT)
payload['md5'] = getMD5(dumpJSON(payload))
return payload
@cacheit(expiration=datetime.timedelta(minutes=5), json_data=True)
def getPayload(self, inclDebug: bool=False):
self.log("getPayload, inclDebug! %s"%(inclDebug))
def __getMeta(payload):
from m3u import M3U
from xmltvs import XMLTVS
from library import Library
from multiroom import Multiroom
xmltv = XMLTVS()
payload.pop('updated')
payload.pop('md5')
payload['m3u'] = M3U().getM3U()
stations = xmltv.getChannels()
recordings = xmltv.getRecordings()
payload['xmltv'] = {'stations' :[{'id':station.get('id'),'display-name':station.get('display-name',[['','']])[0][0],'icon':station.get('icon',[{'src':LOGO}])[0].get('src',LOGO)} for station in stations],
'recordings':[{'id':recording.get('id'),'display-name':recording.get('display-name',[['','']])[0][0],'icon':recording.get('icon',[{'src':LOGO}])[0].get('src',LOGO)} for recording in recordings],
'programmes':[{'id':key,'end-time':datetime.datetime.fromtimestamp(time.time()).strftime(DTFORMAT)} for key, value in list(dict(xmltv.loadStopTimes()).items())]}
payload['library'] = Library().getLibrary()
payload['servers'] = Multiroom().getDiscovery()
del xmltv
return payload
payload = __getMeta(self.getBonjour(inclChannels=True))
if inclDebug: payload['debug'] = loadJSON(self.property.getEXTProperty('%s.debug.log'%(ADDON_ID))).get('DEBUG',{})
payload['updated'] = datetime.datetime.fromtimestamp(time.time()).strftime(DTFORMAT)
payload['md5'] = getMD5(dumpJSON(payload))
return payload
@cacheit(expiration=datetime.timedelta(minutes=5))
def getPayloadUI(self):
return Json2Html().convert(self.getPayload(inclDebug=True))
def IPTV_SIMPLE_SETTINGS(self): #recommended IPTV Simple settings
return {'kodi_addon_instance_name' :ADDON_NAME,
'kodi_addon_instance_enabled' :'false',
'm3uPathType' :'0',
'm3uPath' :'0',
'm3uUrl' :'0',
'm3uCache' :'true',
'startNum' :'1',
'numberByOrder' :'false',
'm3uRefreshMode' :'1',
'm3uRefreshIntervalMins' :'15',
'm3uRefreshHour' :'0',
'connectioncheckinterval' :'10',
'connectionchecktimeout' :'20',
'defaultProviderName' :ADDON_NAME,
'enableProviderMappings' :'true',
# 'providerMappingFile' :PROVIDERFLEPATH,#todo
# 'tvGroupMode' :'0',
# 'customTvGroupsFile' :(TVGROUPFLE),#todo
# 'radioGroupMode' :'0',
# 'customRadioGroupsFile' :(RADIOGROUPFLE),#todo
'epgPathType' :'0',
'epgPath' :'0',
'epgUrl' :'0',
'epgCache' :'true',
'genresPathType' :'0',
'genresPath' :'0',
'genresUrl' :'0',
'useEpgGenreText' :'true',
'logoPathType' :'0',
'logoPath' :LOGO_LOC,
'logoFromEpg' :'2',
'mediaTitleSeasonEpisode' :'true',
'timeshiftEnabled' :'false',
'catchupEnabled' :'true',
'catchupPlayEpgAsLive' :'false',
'catchupWatchEpgEndBufferMins' :'0',
'catchupWatchEpgBeginBufferMins':'0'}
def setPVRPath(self, path, instance=ADDON_NAME, prompt=False, force=False): #local instance
settings = self.IPTV_SIMPLE_SETTINGS()
nsettings = {'m3uPathType' :'0',
'm3uPath' :os.path.join(path,M3UFLE),
'epgPathType' :'0',
'epgPath' :os.path.join(path,XMLTVFLE),
'genresPathType' :'0',
'genresPath' :os.path.join(path,GENREFLE),
'logoPathType' :'0',
'logoPath' :os.path.join(path,'logos'),
'kodi_addon_instance_name' : '%s - %s'%(ADDON_NAME,instance),
'kodi_addon_instance_enabled':'true'}
settings.update(nsettings)
self.log('setPVRPath, new settings = %s'%(nsettings))
if self.hasPVRInstance(instance) and not force:
return self.log('setPVRPath, instance (%s) settings exists.'%(instance))
return self.chkPluginSettings(PVR_CLIENT_ID,settings,instance,prompt)
def setPVRRemote(self, host, instance=ADDON_NAME, prompt=False): #multi-room instances
settings = self.IPTV_SIMPLE_SETTINGS()
nsettings = {'m3uPathType' :'1',
'm3uUrl' :'http://%s/%s'%(host,M3UFLE),
'epgPathType' :'1',
'epgUrl' :'http://%s/%s'%(host,XMLTVFLE),
'genresPathType' :'1',
'genresUrl' :'http://%s/%s'%(host,GENREFLE),
'kodi_addon_instance_name' : '%s - %s'%(ADDON_NAME,instance),
'kodi_addon_instance_enabled':'true'}
settings.update(nsettings)
self.log('setPVRRemote, new settings = %s'%(nsettings))
return self.chkPluginSettings(PVR_CLIENT_ID,settings,instance,prompt)
def hasPVRInstance(self, instance=ADDON_NAME):
instancePath = os.path.join(PVR_CLIENT_LOC,'instance-settings-%s.xml'%(self.gePVRInstance(instance)))
if FileAccess.exists(instancePath):
self.log('hasPVRInstance, instance = %s, instancePath = %s'%(instance, instancePath))
return instancePath
def setPVRInstance(self, instance=ADDON_NAME):
# todo https://github.com/xbmc/xbmc/pull/23648
if not self.getSettingBool('Enable_PVR_SETTINGS'): self.dialog.notificationDialog(LANGUAGE(32186))
elif not FileAccess.exists(os.path.join(PVR_CLIENT_LOC,'settings.xml')):
self.log('setPVRInstance, creating missing default settings.xml')
return self.chkPluginSettings(PVR_CLIENT_ID,self.IPTV_SIMPLE_SETTINGS(),False)
else:
newFile = os.path.join(PVR_CLIENT_LOC,'instance-settings-%s.xml'%(self.gePVRInstance(instance)))
if FileAccess.exists(newFile): FileAccess.delete(newFile)
else: #todo remove after migration to new instances
pvrFile = self.chkPVRInstance(instance)
if pvrFile: FileAccess.delete(pvrFile)
#new instance settings
self.log('setPVRInstance, creating %s'%(newFile))
return FileAccess.move(os.path.join(PVR_CLIENT_LOC,'settings.xml'),newFile)
def gePVRInstance(self, instance=ADDON_NAME):
return int(re.sub("[^0-9]", "", getMD5(instance))) * 2
def chkPVRInstance(self, instance=ADDON_NAME):
found = False
for file in [filename for filename in FileAccess.listdir(PVR_CLIENT_LOC)[1] if filename.endswith('.xml')]:
if self.monitor.waitForAbort(0.0001): break
elif file.startswith('instance-settings-'):
try:
xml = FileAccess.open(os.path.join(PVR_CLIENT_LOC,file), "r")
txt = xml.read()
xml.close()
except Exception as e:
self.log('chkPVRInstance, path = %s, failed to open file = %s\n%s'%(PVR_CLIENT_LOC,file,e))
continue
match = re.compile(r'(.*?)\', re.IGNORECASE).search(txt)
try: name = match.group(1)
except:
match = re.compile(r'(.*?)\', re.IGNORECASE).search(txt)
try: name = match.group(1)
except: name = None
if name == instance:
if found == False: found = os.path.join(PVR_CLIENT_LOC,file)
else: #auto remove any duplicate entries with the same instance name.
FileAccess.delete(os.path.join(PVR_CLIENT_LOC,file))
self.log('chkPVRInstance, removing duplicate entry %s'%(file))
self.log('chkPVRInstance, found %s file = %s'%(name,found))
return found
@cacheit(expiration=datetime.timedelta(minutes=5),json_data=True)
def getPVRInstanceSettings(self, instance):
instanceConf = dict()
instancePath = self.hasPVRInstance(instance)
if instancePath:
fle = FileAccess.open(instancePath,'r')
lines = fle.readlines()
for line in lines:
if not 'id=' in line: continue
match = re.compile(r'(.*?)\', re.IGNORECASE).search(line)
try: instanceConf.update({match.group(1):(match.group(2),match.group(3))})
except:
match = re.compile(r'(.*?)\', re.IGNORECASE).search(line)
try: instanceConf.update({match.group(1):('',match.group(2))})
except:
match = re.compile(r'', re.IGNORECASE).search(line)
try: instanceConf.update({match.group(1):(match.group(2),None)})
except: pass
fle.close()
self.log('getPVRInstanceSettings, returning instance = %s\n%s'%(instance,instanceConf))
return instanceConf
def chkPluginSettings(self, id, nsettings, instance=ADDON_NAME, prompt=False):
self.log('chkPluginSettings, id = %s, instance = %s, prompt=%s'%(id,instance,prompt))
addon = xbmcaddon.Addon(id)
dialog = Dialog()
if addon is None: dialog.notificationDialog(LANGUAGE(32034)%(id))
else:
changes = {}
name = addon.getAddonInfo('name')
osettings = self.getPVRInstanceSettings(instance)
for setting, newvalue in list(nsettings.items()):
if self.monitor.waitForAbort(0.0001): return False
default, oldvalue = osettings.get(setting,({},{}))
if str(newvalue).lower() != str(oldvalue).lower():
changes[setting] = (oldvalue, newvalue)
if not changes:
self.log('chkPluginSettings, no changes detected!')
return False
elif prompt:
modified = '\n'.join(['Modifying %s: [COLOR=dimgray][B]%s[/B][/COLOR] => [COLOR=green][B]%s[/B][/COLOR]'%(setting,newvalue[0],newvalue[1]) for setting, newvalue in list(changes.items())])
dialog.textviewer('%s\n\n%s'%(LANGUAGE(32035)%(name),modified))
if not dialog.yesnoDialog((LANGUAGE(32036)%name)):
dialog.notificationDialog(LANGUAGE(32046))
return False
for s, v in list(changes.items()):
if self.monitor.waitForAbort(0.0001): return False
addon.setSetting(s, v[1])
self.log('chkPluginSettings, setting = %s, current value = %s => %s'%(s,oldvalue,v[1]))
self.setPVRInstance(instance)
dialog.notificationDialog((LANGUAGE(32037)%(name)))
del dialog
return True
del dialog
def getCurrentSettings(self):
settings = ['User_Folder',
'Debug_Enable',
'Overlay_Enable',
'Enable_OnInfo',
'Disable_Trakt',
'Rollback_Watched',
'Store_Duration',
'Seek_Tolerance',
'Seek_Threshold',
'Idle_Timer',
'Run_While_Playing',
'Restart_Percentage',]
for setting in settings:
yield (setting,self.getSetting(setting))
def hasAutotuned(self):
return self.getCacheSetting('has.Autotuned')
def setAutotuned(self, state=True):
return self.setCacheSetting('has.Autotuned',state)
def setWizardRun(self, state=True):
return self.setCacheSetting('has.wizardRun',state)
def hasWizardRun(self):
return self.getCacheSetting('has.wizardRun')
class Properties:
monitor = MONITOR()
def __init__(self, winID=10000):
self.winID = winID
self.window = xbmcgui.Window(winID)
def log(self, msg, level=xbmc.LOGDEBUG):
log('%s: %s'%(self.__class__.__name__,msg),level)
def setEpochTimer(self, key, time=0):
return self.setPropertyInt(key,time)
def setPropTimer(self, key, state=True):
return self.setEXTPropertyBool('%s.%s'%(ADDON_ID,key),state)
def setRemoteHost(self, value):
return self.setProperty('%s.Remote_Host'%(ADDON_ID),value)
def getRemoteHost(self):
remote = self.getProperty('%s.Remote_Host'%(ADDON_ID))
if not remote: remote = self.setRemoteHost('%s:%s'%(Settings().getIP(),Settings().getSettingInt('TCP_PORT')))
return remote
@contextmanager
def chkRunning(self, key):
if not self.isRunning(key):
self.setRunning(key,True)
try: yield
finally: self.setRunning(key,False)
else: yield
def setUpdateChannels(self, id):
ids = self.getUpdateChannels()
if isinstance(id, list): ids.extend(id)
else: ids.append(id)
timerit(self.setPropTimer)(FIFTEEN,['chkUpdate'])
return self.setPropertyList('updateChannels',list(set(ids)))
def getUpdateChannels(self):
ids = (self.getPropertyList('updateChannels') or [])
self.clrProperty('updateChannels')
return ids
def setRunning(self, key, state=True):
return self.setEXTPropertyBool('%s.Running.%s'%(ADDON_ID,key),state)
def isRunning(self, key):
return self.getEXTPropertyBool('%s.Running.%s'%(ADDON_ID,key))
def setInitRun(self, state=True):
return self.setEXTPropertyBool('%s.Init.Run'%(ADDON_ID),state)
def hasInitRun(self):
return self.getEXTPropertyBool('%s.Init.Run'%(ADDON_ID))
def setChannels(self, state=True):
return self.setEXTPropertyBool('%s.has.Channels'%(ADDON_ID),state)
def hasChannels(self):
return self.getEXTPropertyBool('%s.has.Channels'%(ADDON_ID))
def setBackup(self, state=True):
return self.setEXTPropertyBool('%s.has.Backup'%(ADDON_ID),state)
def hasBackup(self):
return self.getEXTPropertyBool('%s.has.Backup'%(ADDON_ID))
def setServers(self, state=True):
return self.setEXTPropertyBool('%s.has.Servers'%(ADDON_ID),state)
def hasServers(self):
return self.getEXTPropertyBool('%s.has.Servers'%(ADDON_ID))
def setEnabledServers(self, state=True):
return self.setEXTPropertyBool('%s.has.Enabled_Servers'%(ADDON_ID),state)
def hasEnabledServers(self):
return self.getEXTPropertyBool('%s.has.Enabled_Servers'%(ADDON_ID))
def hasEnabledLibrary(self, type):
return self.getEXTPropertyBool('%s.has.%s.enabled'%(ADDON_ID,slugify(type)))
def hasLibrary(self):
return True in list(set([self.hasEnabledLibrary(type) for type in AUTOTUNE_TYPES]))
def setPendingShutdown(self, state=True):
return self.setEXTPropertyBool('%s.pendingShutdown'%(ADDON_ID),state)
def isPendingShutdown(self):
value = self.getEXTPropertyBool('%s.pendingShutdown'%(ADDON_ID))
self.clrEXTProperty('%s.pendingShutdown'%(ADDON_ID))
return value
def setPendingRestart(self, state=True):
return self.setEXTPropertyBool('%s.pendingRestart'%(ADDON_ID),state)
def isPendingRestart(self):
value = self.getEXTPropertyBool('%s.pendingRestart'%(ADDON_ID))
self.clrEXTProperty('%s.pendingRestart'%(ADDON_ID))
return value
def setFirstRun(self, state=True):
return self.setEXTPropertyBool('%s.has.firstRun'%(ADDON_ID),state)
def hasFirstRun(self):
return self.getEXTPropertyBool('%s.has.firstRun'%(ADDON_ID))
@contextmanager
def interruptActivity(self): #quit background task
self.setInterruptActivity(True)
try: yield
finally: self.setInterruptActivity(False)
def setInterruptActivity(self, state=True):
return self.setEXTPropertyBool('interruptActivity',state)
def isInterruptActivity(self):
return self.getEXTPropertyBool('interruptActivity')
def setPendingInterrupt(self, state=True):
return self.setEXTPropertyBool('pendingInterrupt',state)
def isPendingInterrupt(self):
return self.getEXTPropertyBool('pendingInterrupt')
@contextmanager
def suspendActivity(self): #pause background task.
self.setSuspendActivity(True)
try: yield
finally: self.setSuspendActivity(False)
def setSuspendActivity(self, state=True):
return self.setEXTPropertyBool('suspendActivity',state)
def isSuspendActivity(self):
return self.getEXTPropertyBool('suspendActivity')
def setPendingSuspend(self, state=True):
return self.setEXTPropertyBool('pendingSuspend',state)
def isPendingSuspend(self):
return self.getEXTPropertyBool('pendingSuspend')
@contextmanager
def legacy(self):
if not self.isPseudoTVRunning():
self.setEXTPropertyBool('PseudoTVRunning',True)
try: yield
finally: self.setEXTPropertyBool('PseudoTVRunning',False)
else: yield
def isPseudoTVRunning(self):
return self.getEXTPropertyBool('PseudoTVRunning')
def setInstanceID(self):
self.clearTrash(self.getEXTProperty('%s.InstanceID'%(ADDON_ID)))
return self.setEXTProperty('%s.InstanceID'%(ADDON_ID),getMD5(uuid.uuid4()))
def getInstanceID(self):
instanceID = self.getEXTProperty('%s.InstanceID'%(ADDON_ID))
if not instanceID: instanceID = self.setInstanceID()
return instanceID
def getKey(self, key, useInstance=True):
if not isinstance(key,str): key = str(key)
if self.winID == 10000 and not key.startswith(ADDON_ID): #create unique id
if useInstance: return self.setTrash('%s.%s.%s'%(ADDON_ID,key,self.getInstanceID()))
else: return '%s.%s'%(ADDON_ID,key)
return key
#CLEAR
def clrEXTProperty(self, key):
self.log('clrEXTProperty, id = %s, key = %s'%(10000,key))
return xbmcgui.Window(10000).clearProperty(key)
def clrProperties(self):
self.log('clrProperties')
return self.window.clearProperties()
def clrProperty(self, key):
key = self.getKey(key)
self.log('clrProperty, id = %s, key = %s'%(self.winID,key))
return self.window.clearProperty(key)
#GET
def getEXTProperty(self, key):
value = xbmcgui.Window(10000).getProperty(key)
if not '.TRASH' in key: self.log('getEXTProperty, id = %s, key = %s, value = %s'%(10000,key,'%s...'%(str(value)[:128])))
return value
def getEXTPropertyBool(self, key):
return (self.getEXTProperty(key) or '').lower() == "true"
def getProperty(self, key):
key = self.getKey(key)
value = self.window.getProperty(key)
self.log('getProperty, id = %s, key = %s, value = %s'%(self.winID,key,'%s...'%(str(value)[:128])))
return value
def getPropertyList(self, key):
return self.getProperty(key).split('|')
def getPropertyBool(self, key):
return self.getProperty(key).lower() == "true"
def getPropertyDict(self, key=''):
return loadJSON(decodeString(self.getProperty(key)))
def getPropertyInt(self, key, default=-1):
value = self.getProperty(key)
if value: return int(convertString2Num(value))
else: return default
def getPropertyFloat(self, key, default=-1):
value = self.getProperty(key)
if value: return float(convertString2Num(value))
else: return default
#SET
def setEXTProperty(self, key, value):
if not '.TRASH' in key: self.log('setEXTProperty, id = %s, key = %s, value = %s'%(10000,key,'%s...'%((str(value)[:128]))))
xbmcgui.Window(10000).setProperty(key,str(value))
return value
def setEXTPropertyBool(self, key, value):
if value: self.setEXTProperty(key,str(value).lower())
else: self.clrEXTProperty(key)
return str(value).lower() == 'true'
def setProperty(self, key, value):
key = self.getKey(key)
self.log('setProperty, id = %s, key = %s, value = %s'%(self.winID,key,'%s...'%((str(value)[:128]))))
self.window.setProperty(key, str(value))
return value
def setPropertyList(self, key, values):
return self.setProperty(key, '|'.join(values))
def setPropertyBool(self, key, value):
if value: self.setProperty(key, value)
else: self.clrProperty(key)
return str(value).lower() == 'true'
def setPropertyDict(self, key, value={}):
return self.setProperty(key, encodeString(dumpJSON(value)))
def setPropertyInt(self, key, value):
return self.setProperty(key, int(value))
def setPropertyFloat(self, key, value):
return self.setProperty(key, float(value))
def setTrakt(self, state=False):
self.log('setTrakt, disable trakt = %s'%(state))
# https://github.com/trakt/script.trakt/blob/d45f1363c49c3e1e83dabacb70729cc3dec6a815/resources/lib/kodiUtilities.py#L104
if state: self.setEXTPropertyBool('script.trakt.paused',state)
else: self.clrEXTProperty('script.trakt.paused')
def setTrash(self, key): #catalog instance properties that may become abandoned
instanceID = self.getInstanceID()
tmpDCT = loadJSON(self.getEXTProperty('%s.TRASH'%(ADDON_ID)))
if key not in tmpDCT.setdefault(instanceID,[]): tmpDCT.setdefault(instanceID,[]).append(key)
self.setEXTProperty('%s.TRASH'%(ADDON_ID),dumpJSON(tmpDCT))
return key
def clearTrash(self, instanceID=None): #clear abandoned properties after instanceID change
if instanceID is None: instanceID = self.getInstanceID()
tmpDCT = loadJSON(self.getEXTProperty('%s.TRASH'%(ADDON_ID)))
if instanceID in tmpDCT:
self.log('clearTrash, instanceID = %s'%(instanceID))
tmpLST = tmpDCT.pop(instanceID)
for prop in tmpLST:
self.clrProperty(prop)
self.clrEXTProperty(prop)
def __exit__(self):
self.log('__exit__')
self.clearTrash(self.getInstanceID())
class ListItems:
def log(self, msg, level=xbmc.LOGDEBUG):
log('%s: %s'%(self.__class__.__name__,msg),level)
def getListItem(self, label='', label2='', path='', offscreen=False):
return xbmcgui.ListItem(label,label2,path,offscreen)
def infoTagVideo(self, offscreen=False):
return xbmc.InfoTagVideo(offscreen)
def InfoTagMusic(self, offscreen=False):
return xbmc.InfoTagVideo(offscreen)
def buildItemListItem(self, item, media='video', oscreen=False, playable=True):
info = item.copy()
art = (info.pop('art' ,{}) or {})
cast = (info.pop('cast' ,[]) or [])
uniqueid = (info.pop('uniqueid' ,{}) or {})
streamInfo = (info.pop('streamdetails' ,{}) or {})
properties = (info.pop('customproperties' ,{}) or {})
if 'citem' in info: properties.update({'citem' :info.pop('citem')}) # write dump to single key
if 'pvritem' in info: properties.update({'pvritem':info.pop('pvritem')}) # write dump to single key
if media != 'video': #unify default artwork for music.
art['poster'] = (getThumb(info,opt=1) or COLOR_LOGO)
art['fanart'] = (getThumb(info) or FANART)
listitem = self.getListItem(info.pop('label',''), info.pop('label2',''), info.pop('file',''), offscreen=oscreen)
listitem.setArt(art)
infoTag = ListItemInfoTag(listitem, media)
info, properties = self.cleanInfo(info,media,properties)
infoTag.set_info(info)
if not media.lower() == 'music':
infoTag.set_cast(cast)
infoTag.set_unique_ids(uniqueid)
for ainfo in streamInfo.get('audio',[]): infoTag.add_stream_info('audio' , ainfo)
for vinfo in streamInfo.get('video',[]): infoTag.add_stream_info('video' , vinfo)
for sinfo in streamInfo.get('subtitle',[]): infoTag.add_stream_info('subtitle', sinfo)
for key, pvalue in list(properties.items()): listitem.setProperty(key, self.cleanProp(pvalue))
if playable: listitem.setProperty("IsPlayable","true")
else: listitem.setIsFolder(True)
return listitem
def buildMenuListItem(self, label="", label2="", icon=COLOR_LOGO, url="", info={}, art={}, props={}, oscreen=False, media='video'):
if not art: art = {'thumb':icon,'logo':icon,'icon':icon}
listitem = self.getListItem(label, label2, url, offscreen=oscreen)
listitem.setIsFolder(True)
listitem.setArt(art)
if info:
infoTag = ListItemInfoTag(listitem, media)
infoTag.set_info(self.cleanInfo(info,media))
[listitem.setProperty(key, self.cleanProp(pvalue)) for key, pvalue in list(props.items())]
return listitem
def cleanInfo(self, ninfo, media='video', properties={}):
LISTITEM_TYPES = MUSIC_LISTITEM_TYPES if media == 'music' else VIDEO_LISTITEM_TYPES
tmpInfo = ninfo.copy()
for key, value in list(tmpInfo.items()):
types = LISTITEM_TYPES.get(key,None)
if not types:# key not in json enum schema, add to customproperties
ninfo.pop(key)
properties[key] = value
continue
elif not isinstance(value,types):# convert to schema type
for type in types:
try: ninfo[key] = type(value)
except Exception as e: self.log("cleanInfo failed! %s\nkey = %s, value = %s, type = %s\n%s"%(e,key,value,type,ninfo), xbmc.LOGWARNING)
if isinstance(ninfo[key],list):
for n in ninfo[key]:
if isinstance(n,dict): n, properties = self.cleanInfo(n,media,properties)
if isinstance(ninfo[key],dict): ninfo[key], properties = self.cleanInfo(ninfo[key],media,properties)
return ninfo, properties
def cleanProp(self, pvalue):
if isinstance(pvalue,dict): return dumpJSON(pvalue)
elif isinstance(pvalue,list): return '|'.join(map(str, pvalue))
elif not isinstance(pvalue,str): return str(pvalue)
else: return pvalue
class Builtin:
busy = None
monitor = MONITOR()
properties = Properties()
def __init__(self):
...
def log(self, msg, level=xbmc.LOGDEBUG):
log('%s: %s'%(self.__class__.__name__,msg),level)
def hasSearches(self):
from jsonrpc import JSONRPC
return len(JSONRPC().getPVRSearches()) > 0
def hasRecordings(self):
from jsonrpc import JSONRPC
return len(JSONRPC().getPVRRecordings()) > 0
def hasPVR(self):
return self.getInfoBool('HasTVChannels','Pvr')
def hasRadio(self):
return self.getInfoBool('HasRadioChannels','Pvr')
def hasMusic(self):
return self.getInfoBool('HasContent(Music)','Library')
def hasTV(self):
return self.getInfoBool('HasContent(TVShows)','Library')
def hasMovie(self):
return self.getInfoBool('HasContent(Movies)','Library')
def hasSubtitle(self):
return self.getInfoBool('HasSubtitles','VideoPlayer')
def isSubtitle(self):
return self.getInfoBool('SubtitlesEnabled','VideoPlayer')
def isPlaylistRandom(self):
return self.getInfoLabel('Random','Playlist').lower() == 'on' # Disable auto playlist shuffling if it's on
def isPlaylistRepeat(self):
return self.getInfoLabel('IsRepeat','Playlist').lower() == 'true' # Disable auto playlist repeat if it's on #todo
def isPaused(self):
return self.getInfoBool('Player.Paused')
def isRecording(self):
return self.getInfoBool('IsRecording','Pvr')
def isScanning(self):
return (self.getInfoBool('IsScanningVideo','Library') & self.getInfoBool('IsScanningMusic','Library'))
def isSettingsOpened(self) -> bool:
return (self.getInfoBool('IsVisible(addonsettings)','Window') | self.getInfoBool('IsVisible(selectdialog)' ,'Window'))
def isBusyDialog(self):
return (self.properties.isRunning('OVERLAY_BUSY') | self.getInfoBool('IsActive(busydialognocancel)','Window') | self.getInfoBool('IsActive(busydialog)','Window'))
def closeBusyDialog(self):
if hasattr(self.busy, 'close'):
self.busy = self.busy.close()
elif self.getInfoBool('IsActive(busydialognocancel)','Window'):
self.executebuiltin('Dialog.Close(busydialognocancel)')
elif self.getInfoBool('IsActive(busydialog)','Window'):
self.executebuiltin('Dialog.Close(busydialog)')
@contextmanager
def busy_dialog(self, cancel=False):
if not self.isBusyDialog() and not cancel:
try:
if self.busy is None:
from overlay import Busy
self.busy = Busy(BUSY_XML, ADDON_PATH, "default")
self.busy.show()
yield
finally:
if hasattr(self.busy, 'close'):
self.busy = self.busy.close()
else: yield
def getIdle(self):
try: return int(xbmc.getGlobalIdleTime() or '0')
except: return 0
def getInfoLabel(self, key, param='ListItem', default=''):
value = xbmc.getInfoLabel('%s.%s'%(param,key))
if value == "Busy":
if not MONITOR().waitForAbort(1.0): return self.getInfoLabel(key,param,default)
self.log('getInfoLabel, key = %s.%s, value = %s'%(param,key,value))
return (value or default)
def getInfoBool(self, key, param='Library'):
value = (xbmc.getCondVisibility('%s.%s'%(param,key)) or False)
self.log('getInfoBool, key = %s.%s, value = %s'%(param,key,value))
return value
def executeWindow(self, key):
if self.getInfoBool('Playing','Player'):
self.executebuiltin(key)
def executebuiltin(self, key, wait=False):
self.log('executebuiltin, key = %s, wait = %s'%(key,wait))
xbmc.executebuiltin('%s'%(key),wait)
return True
def executescript(self, path):
self.log('executescript, path = %s'%(path))
xbmc.executescript('%s'%(path))
return True
@contextmanager
def sendLocker(self, wait=0.0001):
while not self.monitor.abortRequested(): #try and make kodi api thread safe / thread locks not working? todo debug.
if self.monitor.waitForAbort(wait) or self.properties.getEXTPropertyBool('%s.pendingInterrupt'%(ADDON_ID)): break
elif not self.properties.getEXTPropertyBool('%s.sendLocker'%(ADDON_ID)): break
else: log('sendLocker, avoiding collision')
self.properties.setEXTPropertyBool('%s.sendLocker'%(ADDON_ID),True)
try: yield
finally: self.properties.setEXTPropertyBool('%s.sendLocker'%(ADDON_ID),False)
def executeJSONRPC(self, command):
self.log('executeJSONRPC, command = %s'%(command))
# with self.sendLocker():
return xbmc.executeJSONRPC(command)
def getResolution(self):
WH, WIN = self.getInfoLabel('ScreenResolution','System').split(' - ')
return (1920,1080), WIN #tuple(int(x) for x in WH.split('x')), WIN
class Dialog:
settings = Settings()
properties = Properties()
listitems = ListItems()
builtin = Builtin()
dialog = xbmcgui.Dialog()
def __init__(self):
self.settings.dialog = self
self.settings.property = self.properties
self.settings.builtin = self.builtin
self.monitor = self.builtin.monitor
def log(self, msg, level=xbmc.LOGDEBUG):
log('%s: %s'%(self.__class__.__name__,msg),level)
def toggleInfoMonitor(self, state, wait=0.5):
self.log('toggleInfoMonitor, state = %s'%(state))
self.properties.setPropertyBool('chkInfoMonitor',state)
if state:
self.properties.clrProperty('monitor.montiorList')
timerit(self.doInfoMonitor)(0.0001)
def doInfoMonitor(self):
while not self.monitor.abortRequested():
if not self.fillInfoMonitor(): break
elif self.monitor.waitForAbort(float(self.settings.getSettingInt('RPC_Delay')/1000)): break
def fillInfoMonitor(self, type='ListItem'):
#todo catch full listitem not singular properties.
try:
item = {'label' :self.builtin.getInfoLabel('Label' ,type),
'label2' :self.builtin.getInfoLabel('Label2' ,type),
'set' :self.builtin.getInfoLabel('Set' ,type),
'path' :self.builtin.getInfoLabel('Path' ,type),
'genre' :self.builtin.getInfoLabel('Genre' ,type),
'studio' :self.builtin.getInfoLabel('Studio' ,type),
'title' :self.builtin.getInfoLabel('Title' ,type),
'tvshowtitle' :self.builtin.getInfoLabel('TVShowTitle' ,type),
'plot' :self.builtin.getInfoLabel('Plot' ,type),
'addonname' :self.builtin.getInfoLabel('AddonName' ,type),
'artist' :self.builtin.getInfoLabel('Artist' ,type),
'album' :self.builtin.getInfoLabel('Album' ,type),
'albumartist' :self.builtin.getInfoLabel('AlbumArtist' ,type),
'foldername' :self.builtin.getInfoLabel('FolderName' ,type),
'logo' :(self.builtin.getInfoLabel('Art(tvshow.clearlogo)',type) or
self.builtin.getInfoLabel('Art(clearlogo)' ,type) or
self.builtin.getInfoLabel('Icon' ,type) or
self.builtin.getInfoLabel('Thumb' ,type))}
if item.get('label'):
montiorList = self.getInfoMonitor()
if item.get('label') not in montiorList: montiorList.insert(0,item)
self.setInfoMonitor(montiorList)
return self.properties.getPropertyBool('chkInfoMonitor')
except Exception as e:
self.log("fillInfoMonitor, failed! %s"%(e), xbmc.LOGERROR)
return False
def getInfoMonitor(self):
return self.properties.getPropertyDict('monitor.montiorList').get('info',[])
def setInfoMonitor(self, items):
return self.properties.setPropertyDict('monitor.montiorList',{'info':list(setDictLST(items))})
def colorDialog(self, colorlist=[], preselect="", colorfile="", heading=ADDON_NAME):
return self.dialog.colorpicker(heading, preselect, colorfile, colorlist)
def _closeOkDialog(self):
if self.builtin.getInfoBool('IsActive(okdialog)','Window'):
self.builtin.executebuiltin('Dialog.Close(okdialog)')
def _okDialog(self, msg, heading, autoclose, url):
return timerit(self.okDialog)(0.1,[msg, heading, autoclose])
def okDialog(self, msg, heading=ADDON_NAME, autoclose=AUTOCLOSE_DELAY, usethread=False):
if usethread: return self._okDialog(msg, heading, autoclose)
else:
if autoclose > 0: timerit(self._closeOkDialog)(autoclose)
return self.dialog.ok(heading, msg)
def qrDialog(self, url, msg, heading='%s - %s'%(ADDON_NAME,LANGUAGE(30158)), autoclose=AUTOCLOSE_DELAY):
class QRCode(xbmcgui.WindowXMLDialog):
def __init__(self, *args, **kwargs):
self.header = kwargs["header"]
self.image = kwargs["image"]
self.text = kwargs["text"]
self.acThread = Timer(kwargs["atclose"], self.onClose)
def onInit(self):
self.getControl(40000).setLabel(self.header)
self.getControl(40001).setImage(self.image)
self.getControl(40002).setText(self.text)
self.getControl(40003).setLabel(LANGUAGE(32062))
self.setFocus(self.getControl(40003))
self.acThread.name = "acThread"
self.acThread.daemon=True
self.acThread.start()
def onClick(self, controlId):
if controlId == 40003:
self.onClose()
def onClose(self):
if self.acThread.is_alive():
if hasattr(self.acThread, 'cancel'): self.acThread.cancel()
try: self.acThread.join()
except: pass
self.close()
if not self.properties.isRunning('qrDialog'):
with self.properties.chkRunning('qrDialog'):
with self.builtin.busy_dialog():
imagefile = os.path.join(FileAccess.translatePath(TEMP_LOC),'%s.png'%(getMD5(str(url.split('/')[-1]))))
if not FileAccess.exists(imagefile):
qrIMG = pyqrcode.create(url)
qrIMG.png(imagefile, scale=10)
qr = QRCode( "plugin.video.pseudotv.live.qrcode.xml" , ADDON_PATH, "default", image=imagefile, text=msg, header=heading, atclose=autoclose)
qr.doModal()
del qr
return True
def _closeTextViewer(self):
if self.builtin.getInfoBool('IsActive(textviewer)','Window'):
self.builtin.executebuiltin('Dialog.Close(textviewer)')
def _customTextViewer():
class TEXTVIEW(xbmcgui.WindowXMLDialog):
textbox = None
def __init__(self, *args, **kwargs):
self.head = kwargs.get('head','')
self.text = kwargs.get('text','')
self.doModal()
def onInit(self):
self.getControl(1).setLabel(self.head)
self.textbox = self.getControl(5)
def onClick(self, control_id): pass
def onFocus(self, control_id): pass
def onAction(self, action):
if action in [xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK]: self.close()
def _updateText(self, txt):
try:
self.textbox.setText(txt)
xbmc.executebuiltin('SetFocus(3000)')
xbmc.executebuiltin('AlarmClock(down,Action(down),.5,true,false)')
except: pass
return TEXTVIEW("DialogTextViewer.xml", os.getcwd(), "Default")
def _textViewer(self, msg, heading, usemono, autoclose):
return timerit(self.textviewer)(0.1,[msg, heading, usemono, autoclose])
def textviewer(self, msg, heading=ADDON_NAME, usemono=False, autoclose=AUTOCLOSE_DELAY, usethread=False, custom=False):
# if custom: return self._customTextViewer(msg,heading,autoclose)
if usethread: return self._textViewer(msg, heading, usemono, autoclose)
else:
if autoclose > 0: timerit(self._closeTextViewer)(autoclose)
self.dialog.textviewer(heading, msg, usemono)
return True
def yesnoDialog(self, message, heading=ADDON_NAME, nolabel='', yeslabel='', customlabel='', autoclose=AUTOCLOSE_DELAY):
if customlabel:
# Returns the integer value for the selected button (-1:cancelled, 0:no, 1:yes, 2:custom)
return self.dialog.yesnocustom(heading, message, customlabel, nolabel, yeslabel, (autoclose*1000))
else:
# Returns True if 'Yes' was pressed, else False.
return self.dialog.yesno(heading, message, nolabel, yeslabel, (autoclose*1000))
def _notificationWait(self, message, header, wait):
return timerit(self.notificationWait)(0.1,[message, header, wait])
def notificationWait(self, message, header=ADDON_NAME, wait=4, usethread=False):
if usethread: return self._notificationWait(message, header, wait)
else:
pDialog = self.progressBGDialog(message=message,header=header)
for idx in range(int(wait)):
pDialog = self.progressBGDialog((((idx+1) * 100)//int(wait)),pDialog,header=header)
if pDialog is None or MONITOR().waitForAbort(1.0): break
if hasattr(pDialog, 'close'): pDialog.close()
return True
def updateProgress(self, percent=-1, control=None, message='', header=ADDON_NAME):
if isinstance(control,xbmcgui.DialogProgressBG): return self.progressBGDialog(percent, control, message, header)
elif isinstance(control,xbmcgui.DialogProgress):
title = header.replace('%s, '%(ADDON_NAME),'')
match = re.compile(r'(.*?): (.*?)\%', re.IGNORECASE).search(message)
try:
message = match.group(1)
percent = int(match.group(2))
except: pass
if title: message = '%s: %s'%(title,message)
return self.progressDialog(percent, control, message)
def progressDialog(self, percent=0, control=None, message='', header=ADDON_NAME):
if control is None and int(percent) == 0:
control = xbmcgui.DialogProgress()
control.create(header, message)
elif control:
if int(percent) == 100 or control.iscanceled(): return control.close()
elif hasattr(control, 'update'): control.update(int(percent), message)
return control
def progressBGDialog(self, percent=0, control=None, message='', header=ADDON_NAME):
if control is None and int(percent) == 0:
control = xbmcgui.DialogProgressBG()
control.create(header, message)
elif control:
if int(percent) == 100 or control.isFinished(): return control.close()
elif hasattr(control,'update'): control.update(int(percent), header, message)
return control
def infoDialog(self, listitem):
self.dialog.info(listitem)
def notificationDialog(self, message, header=ADDON_NAME, sound=False, time=PROMPT_DELAY, icon=COLOR_LOGO, show=True):
self.log('notificationDialog: %s'%(message))
## - Builtin Icons:
## - xbmcgui.NOTIFICATION_INFO
## - xbmcgui.NOTIFICATION_WARNING
## - xbmcgui.NOTIFICATION_ERROR
if show:
try: self.dialog.notification(header, message, icon, time, sound=False)
except: self.builtin.executebuiltin("Notification(%s, %s, %d, %s)" % (header, message, time, icon))
return True
def customSelect(self, items, header, preselect, useDetails, autoclose, multi): #todo
class DialogSelect(xbmcgui.WindowXMLDialog):
def __init__(self, *args, **kwargs):
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
def onInit(self):
log("DialogSelect: onInit")
#todo get list control, convert items to listitems, additems to list control.
def onAction(self, act):
actionId = act.getId()
log('DialogSelect: onAction: actionId = %s'%(actionId))
if actionId in ACTION_PREVIOUS_MENU: self.close()
else: pass
if not self.properties.isRunning('SELECT_DIALOG'):
dialogSelect = DialogSelect(DIALOG_SELECT, ADDON_PATH, "default")
dialogSelect.doModal()
del dialogSelect
def selectDialog(self, items, header=ADDON_NAME, preselect=None, useDetails=True, autoclose=SELECT_DELAY, multi=True, custom=False):
self.log('selectDialog, items = %s, header = %s, preselect = %s, useDetails = %s, autoclose = %s, multi = %s, custom = %s'%(len(items),header,preselect,useDetails,autoclose,multi,custom))
if custom: return self.customSelect(items, header, preselect, useDetails, autoclose, multi)
elif multi == True:
if not preselect: preselect = [-1]
select = self.dialog.multiselect(header, items, (autoclose*1000), preselect, useDetails)
if select == [-1]: return
else:
if not preselect: preselect = -1
elif isinstance(preselect,list) and len(preselect) > 0: preselect = preselect[0]
select = self.dialog.select(header, items, (autoclose*1000), preselect, useDetails)
if select == -1: return
return select
def inputDialog(self, message, default='', key=xbmcgui.INPUT_ALPHANUM, opt=0, close=0):
## - xbmcgui.INPUT_ALPHANUM (standard keyboard)
## - xbmcgui.INPUT_NUMERIC (format: #)
## - xbmcgui.INPUT_DATE (format: DD/MM/YYYY)
## - xbmcgui.INPUT_TIME (format: HH:MM)
## - xbmcgui.INPUT_IPADDRESS (format: #.#.#.#)
## - xbmcgui.INPUT_PASSWORD (return md5 hash of input, input is masked)
return self.dialog.input(message, default, key, opt, close)
def importSTRM(self, strm):
try:
with self.builtin.busy_dialog():
fle = FileAccess.open(strm,'r')
paths = [line for line in fle.readlines() if not line.startswith('#') and '://' in line]
fle.close()
if len(paths) == 0: return self.notificationDialog(LANGUAGE(32018)%(LANGUAGE(30047)))
select = self.selectDialog(paths, LANGUAGE(32080), useDetails=False, multi=False)
self.log("importSTRM, strm = %s paths = %s"%(strm,paths))
if not select is None: return paths[select]
except Exception as e: self.log("importSTRM, failed! %s\n%s"%(e,strm), xbmc.LOGERROR)
def _resourcePath(self, id=[], content='videos', ftype=''):
if not id: id = self.browseResources(id, content, ftype, multi=False)
path = 'special://home/addons/%s/resources/'%(id)
self.log("_resourcePath, id = %s, content = %s, ftype = %s, path = %s"%(id, content, ftype,path))
return path
def browseResources(self, ids=[], content='videos', ftype='', multi=True):
#todo when no resources avail take user to Image Collections repo.
self.log("browseResources, ids = %s, content = %s, ftype = %s, multi = %s"%(ids, content, ftype, multi))
#todo selectDialog content and ftype.
def __buildMenuItem(resource):
return self.listitems.buildMenuListItem(resource['name'],resource['description'],resource['thumbnail'],url=resource['addonid'])
def __getResources():
return jsonRPC.getAddons({"enabled":True})
from jsonrpc import JSONRPC
jsonRPC = JSONRPC()
with self.builtin.busy_dialog():
lizLST = poolit(__buildMenuItem)([result for result in __getResources() if result.get('addonid').startswith('resource.%s.%s'%(content,ftype))])
del jsonRPC
selects = self.selectDialog(lizLST, 'Select one or more resources', preselect=findItemsInLST(lizLST,ids,'getPath'), multi=multi)
if selects is None: return
elif not isinstance(selects,list): return lizLST[selects].getPath()
else: return [lizLST[select].getPath() for select in selects]
def browseSources(self, type=0, heading=ADDON_NAME, default='', shares='', mask='', useThumbs=True, treatAsFolder=False, multi=False, monitor=False, include=[], exclude=[]):
self.log('browseSources, type = %s, heading= %s, shares= %s, useThumbs= %s, treatAsFolder= %s, default= %s, mask= %s, include= %s, exclude= %s'%(type,heading,shares,useThumbs,treatAsFolder,default,mask,len(include),exclude))
def __buildMenuItem(option):
return self.listitems.buildMenuListItem(option['label'],option['label2'],DUMMY_ICON.format(text=getAbbr(option['label'])))
with self.builtin.busy_dialog():
optlabel = "%s"%({'0':'Folders','1':'Files'}[str(type)]) if multi else "%s"%({'0':'Folder','1':'File'}[str(type)])
opts = [{"idx":10, "label":'%s %s'%(LANGUAGE(32196),optlabel) , "label2":"library://video/" , "default":"library://video/" , "shares":"video" , "mask":xbmc.getSupportedMedia('video') , "type":0 , "multi":multi},
{"idx":11, "label":'%s %s'%(LANGUAGE(32207),optlabel) , "label2":"library://music/" , "default":"library://music/" , "shares":"music" , "mask":xbmc.getSupportedMedia('music') , "type":0 , "multi":multi},
{"idx":12, "label":LANGUAGE(32191) , "label2":"special://profile/playlists/video/" , "default":"special://profile/playlists/video/" , "shares":"" , "mask":".xsp" , "type":1 , "multi":False},
{"idx":13, "label":LANGUAGE(32192) , "label2":"special://profile/playlists/music/" , "default":"special://profile/playlists/music/" , "shares":"" , "mask":".xsp" , "type":1 , "multi":False},
{"idx":14, "label":LANGUAGE(32193) , "label2":"special://profile/playlists/mixed/" , "default":"special://profile/playlists/mixed/" , "shares":"" , "mask":".xsp" , "type":1 , "multi":False},
{"idx":15, "label":LANGUAGE(32195) , "label2":"Create Dynamic Smartplaylist" , "default":"" , "shares":"" , "mask":"" , "type":1 , "multi":False},
{"idx":16, "label":LANGUAGE(32194) , "label2":"Import directory paths from STRM" , "default":"" , "shares":"files" , "mask":".strm" , "type":1 , "multi":False},
{"idx":17, "label":LANGUAGE(32206) , "label2":"Media from basic playlists" , "default":"" , "shares":"" , "mask":"|".join(ALT_PLAYLISTS) , "type":1 , "multi":False},
{"idx":18, "label":'%s %s'%(LANGUAGE(32198),'Folders'), "label2":"" , "default":"" , "shares":"files" , "mask":mask , "type":type , "multi":multi},
{"idx":19, "label":'%s %s'%(LANGUAGE(32199),'Folders'), "label2":"" , "default":"" , "shares":"local" , "mask":mask , "type":type , "multi":multi},
{"idx":20, "label":'%s %s'%(LANGUAGE(32200),'Folders'), "label2":"" , "default":"" , "shares":shares , "mask":mask , "type":type , "multi":multi},
{"idx":21, "label":LANGUAGE(32201) , "label2":"" , "default":"" , "shares":"pictures", "mask":xbmc.getSupportedMedia('picture') , "type":1 , "multi":False},
{"idx":22, "label":LANGUAGE(32202) , "label2":"Resource Plugin" , "default":"" , "shares":shares , "mask":mask , "type":type , "multi":multi}]
options = include.copy()
options.extend([opt for opt in opts if not opt.get('idx',-1) in exclude])
options = setDictLST(options)
if default: options.insert(0,{"idx":0, "label":LANGUAGE(32203), "label2":default, "default":default, "shares":shares, "mask":mask, "type":type, "multi":multi})
lizLST = poolit(__buildMenuItem)(sorted(options, key=itemgetter('idx')))
select = self.selectDialog(lizLST, LANGUAGE(32089), multi=False)
if select is None: return
default = options[select]['default']
shares = options[select]['shares']
mask = options[select]['mask']
type = options[select]['type']
multi = options[select]['multi']
if type == 0:
if "resource." in default or options[select]["idx"] == 22: return self._resourcePath(default, {xbmc.getSupportedMedia('video'):'videos',xbmc.getSupportedMedia('picture'):'images'}.get(mask,xbmc.getSupportedMedia('video')))
elif type == 1:
if "?xsp=" in default or options[select]["idx"] == 15: return self.buildDXSP(default)
elif ".strm" in default or options[select]["idx"] == 16: return self.importSTRM(default)
elif "resource." in default or options[select]["idx"] == 22: default = self._resourcePath(default, {xbmc.getSupportedMedia('video'):'videos',xbmc.getSupportedMedia('picture'):'images'}.get(mask,xbmc.getSupportedMedia('video')))
return self.browseDialog(type, heading, default, shares, mask, useThumbs, treatAsFolder, multi, monitor)
def browseDialog(self, type=0, heading=ADDON_NAME, default='', shares='', mask='', useThumbs=True, treatAsFolder=False, multi=False, monitor=False):
self.log('browseDialog, type = %s, heading= %s, shares= %s, useThumbs= %s, treatAsFolder= %s, default= %s\nmask= %s'%(type,heading,shares,useThumbs,treatAsFolder,default,mask))
# https://xbmc.github.io/docs.kodi.tv/master/kodi-base/d6/de8/group__python___dialog.html#ga856f475ecd92b1afa37357deabe4b9e4
# https://xbmc.github.io/docs.kodi.tv/master/kodi-base/d6/de8/group__python___dialog.html#gafa1e339e5a98ae4ea4e3d3bb3e1d028c
if monitor: self.toggleInfoMonitor(True)
if multi == True and type > 0: retval = self.dialog.browseMultiple(type, heading, shares, mask, useThumbs, treatAsFolder, default)
else: retval = self.dialog.browseSingle(type, heading, shares, mask, useThumbs, treatAsFolder, default)
if monitor: self.toggleInfoMonitor(False)
if not retval is None and retval != default:
return retval
def multiBrowse(self, paths: list=[], header=ADDON_NAME, exclude=[], monitor=True):
self.log('multiBrowse, IN paths = %s'%(paths))
def __buildListItem(label: str="", label2: str="", icon: str=COLOR_LOGO, paths: list=[], items: dict={}):
return self.listitems.buildMenuListItem(label, label2, icon, url='|'.join(paths), props=items)
select = -1
epaths = paths.copy()
pathLST = list([_f for _f in paths if _f])
lastOPT = None
while not MONITOR().abortRequested() and not select is None:
with self.builtin.busy_dialog():
npath = None
lizLST = [__buildListItem('%s|'%(idx+1),path,paths=[path],icon=DUMMY_ICON.format(text=str(idx+1)),items={'idx':idx+1}) for idx, path in enumerate(pathLST) if path]
lizLST.insert(0,__buildListItem('[COLOR=white][B]%s[/B][/COLOR]'%(LANGUAGE(32100)),LANGUAGE(33113),icon=ICON,items={'key':'add','idx':0}))
if len(pathLST) > 0 and epaths != pathLST: lizLST.insert(1,__buildListItem('[COLOR=white][B]%s[/B][/COLOR]'%(LANGUAGE(32101)),LANGUAGE(33114),icon=ICON,items={'key':'save'}))
select = self.selectDialog(lizLST, header, preselect=lastOPT, multi=False)
if not select is None:
key, path = lizLST[select].getProperty('key'), lizLST[select].getPath()
try: lastOPT = int(lizLST[select].getProperty('idx'))
except: lastOPT = -1
if key == 'add':
with self.builtin.busy_dialog():
npath = self.browseSources(heading=LANGUAGE(32080), exclude=exclude, monitor=monitor)
if npath: pathLST.append(npath)
elif key == 'save':
paths = pathLST
break
elif path in pathLST:
retval = self.yesnoDialog(LANGUAGE(32102), customlabel=LANGUAGE(32103))
if retval in [1,2]: pathLST.pop(pathLST.index(path))
if retval == 2:
with self.builtin.busy_dialog():
npath = self.browseSources(heading=LANGUAGE(32080), default=path, monitor=monitor, exclude=exclude)
pathLST.append(npath)
self.log('multiBrowse, OUT paths = %s'%(paths))
return paths
def buildDXSP(self, path=''):
# https://github.com/xbmc/xbmc/blob/master/xbmc/playlists/SmartPlayList.cpp
def mtype(params={"type":"","order":{'direction':'ascending','method':'random','ignorearticle':True,'useartistsortname':True},"rules":{}}):
path = ''
enumLST = list(sorted(['albums', 'artists', 'episodes', 'mixed', 'movies', 'musicvideos', 'songs', 'tvshows']))
enumSEL = self.selectDialog(list(sorted([l.title() for l in enumLST])),header="Select Media Type",preselect=(enumLST.index(params.get('type','')) if params.get('type') else -1),useDetails=False, multi=False)
if not enumSEL is None:
params['type'] = enumLST[enumSEL]
if params['type'] in MUSIC_TYPES: db = 'musicdb'
else: db = 'videodb'
if params['type'] == 'episodes': path = "%s://tvshows//titles/-1/-1/-1/"%(db)
elif params['type'] in ['movies','tvshows','musicvideos']: path = "%s://%s/titles/"%(db,params['type'])
elif params['type'] in ['albums','artists','songs']: path = "%s://songs/"%(db)
else: path = ''
return path, params
def andor(params={}):
enumLST = list(sorted(['and', 'or']))
enumSEL = self.selectDialog(list(sorted([l.title() for l in enumLST])),header="Select Conjunction",preselect=(enumLST.index(list(params.get('rules',{}).keys())) if params.get('rules',{}) else -1),useDetails=False, multi=False)
if not enumSEL is None: return enumLST[enumSEL]
def order(params={}):
enums = jsonRPC.getEnums("List.Sort",type="order")
enumLST = list(sorted([_f for _f in enums if _f]))
enumSEL = self.selectDialog(list(sorted([l.title() for l in enumLST])),header="Select order",preselect=enumLST.index(params.get('order',{}).get('direction','ascending')),useDetails=False, multi=False)
if not enumSEL is None: return enumLST[enumSEL]
def method(params={}):
enums = jsonRPC.getEnums("List.Sort",type="method")
enumLST = list(sorted([_f for _f in enums if _f]))
enumSEL = self.selectDialog(list(sorted([l.title() for l in enumLST])),header="Select method",preselect=enumLST.index(params.get('order',{}).get('method','random')),useDetails=False, multi=False)
if not enumSEL is None: return enumLST[enumSEL]
def field(params={}, rule={}):
if params.get('type') == 'songs': enums = jsonRPC.getEnums("List.Filter.Fields.Songs" , type='items')
elif params.get('type') == 'albums': enums = jsonRPC.getEnums("List.Filter.Fields.Albums" , type='items')
elif params.get('type') == 'artists': enums = jsonRPC.getEnums("List.Filter.Fields.Artists" , type='items')
elif params.get('type') == 'tvshows': enums = jsonRPC.getEnums("List.Filter.Fields.TVShows" , type='items')
elif params.get('type') == 'episodes': enums = jsonRPC.getEnums("List.Filter.Fields.Episodes", type='items')
elif params.get('type') == 'movies': enums = jsonRPC.getEnums("List.Filter.Fields.Movies" , type='items')
elif params.get('type') == 'musicvideos': enums = jsonRPC.getEnums("List.Filter.Fields.MusicVideos")
elif params.get('type') == 'mixed': enums = ['playlist', 'virtualfolder']
else: return
enumLST = list(sorted([_f for _f in enums if _f]))
enumSEL = self.selectDialog(list(sorted([l.title() for l in enumLST])),header="Select Filter",preselect=(enumLST.index(rule.get('field')) if rule.get('field') else -1),useDetails=False, multi=False)
if not enumSEL is None: return enumLST[enumSEL]
def operator(params={}, rule={}):
enumLST = sorted(jsonRPC.getEnums("List.Filter.Operators"))
if rule.get("field") != 'date':
if 'inthelast' in enumLST: enumLST.remove('inthelast')
if 'notinthelast' in enumLST: enumLST.remove('notinthelast')
enumSEL = self.selectDialog(list(sorted([l.title() for l in enumLST])),header="Select Operator",preselect=(enumLST.index(rule.get('operator')) if rule.get('operator') else -1),useDetails=False, multi=False)
if not enumSEL is None: return enumLST[enumSEL]
def value(params={}, rule={}):
return self.getValue(params, rule)
def getRule(params={}, rule={"field":"","operator":"","value":[]}):
enumSEL = -1
while not self.monitor.abortRequested() and not enumSEL is None:
enumLST = [self.listitems.buildMenuListItem(key.title(),str(value),icon=DUMMY_ICON.format(text=getAbbr(key.title())),props={'key':key,'value':value}) for key, value in list(rule.items())]
enumSEL = self.selectDialog(enumLST,header="Select method",preselect=-1, multi=False)
if not enumSEL is None: rule.update({enumLST[enumSEL].getProperty('key'):({"field":field,"operator":operator,"value":value}[enumLST[enumSEL].getProperty('key')])(params,rule)})
return rule
def getRules(params={}):
enumSEL = -1
eparams = params.copy()
while not self.monitor.abortRequested() and not enumSEL is None:
enumLST = [self.listitems.buildMenuListItem(key.title(),dumpJSON(params.get('rules',{}).get(key,[])),icon=DUMMY_ICON.format(text=getAbbr(key.title())),props={'key':key}) for key in ["and","or"]]
enumSEL = self.selectDialog(enumLST,header="Edit Rules",multi=False)
if not enumSEL is None:
if enumLST[enumSEL].getLabel() in ['And','Or']:
CONSEL = -1
CONLKEY = enumLST[enumSEL].getProperty('key')
ruleLST = params.get('rules',{}).get(CONLKEY,[])
while not self.monitor.abortRequested() and not CONSEL is None:
andLST = [self.listitems.buildMenuListItem('%s|'%(idx+1),dumpJSON(value),icon=DUMMY_ICON.format(text=str(idx+1)),props={'idx':str(idx)}) for idx, value in enumerate(ruleLST)]
andLST.insert(0,self.listitems.buildMenuListItem('[COLOR=white][B]%s[/B][/COLOR]'%(LANGUAGE(32173)),"",icon=ICON,props={'key':'add'}))
if len(ruleLST) > 0 and eparams != params: andLST.insert(1,self.listitems.buildMenuListItem('[COLOR=white][B]%s[/B][/COLOR]'%(LANGUAGE(32174)),"",icon=ICON,props={'key':'save'}))
CONSEL = self.selectDialog(andLST,header="Edit Rules",multi=False)
if not CONSEL is None:
if andLST[CONSEL].getProperty('key') == 'add': ruleLST.append(getRule(params,{"field":"","operator":"","value":[]}))
elif andLST[CONSEL].getProperty('key') == 'save':
params.setdefault('rules',{})[CONLKEY] = ruleLST
break
elif sorted(loadJSON(andLST[CONSEL].getLabel2())) in [sorted(andd) for andd in ruleLST]:
retval = self.yesnoDialog(LANGUAGE(32175), customlabel=LANGUAGE(32176))
if retval in [1,2]: ruleLST.pop(int(andLST[CONSEL].getProperty('idx')))
if retval == 2: ruleLST.append(getRule(params,loadJSON(andLST[CONSEL].getLabel2())))
else: ruleLST.append(getRule(params,loadJSON(andLST[CONSEL].getLabel2())))
return params
def getOrder(params={}):
enumSEL = -1
while not self.monitor.abortRequested() and not enumSEL is None:
enumLST = [self.listitems.buildMenuListItem(key.title(),str(value).title(),icon=DUMMY_ICON.format(text=getAbbr(key.title()))) for key, value in list(params.get('order',{}).items())]
enumLST.insert(0,self.listitems.buildMenuListItem('[COLOR=white][B]%s[/B][/COLOR]'%(LANGUAGE(32174)),"",icon=ICON,props={'key':'save'}))
enumSEL = self.selectDialog(enumLST,header="Edit Selection",preselect=-1,multi=False)
if not enumSEL is None:
if enumLST[enumSEL].getLabel() == 'Direction': params['order'].update({'direction':order(params)})
elif enumLST[enumSEL].getLabel() == 'Method': params['order'].update({'method':method(params)})
elif enumLST[enumSEL].getProperty('key') == 'save': break
else: params['order'].update({enumLST[enumSEL].getLabel().lower(): not enumLST[enumSEL].getLabel2() == 'True'})
return params
from jsonrpc import JSONRPC
jsonRPC = JSONRPC()
try:
path, params = path.split('?xsp=')
params = loadJSON(params)
except:
path, params = mtype()
self.log('buildDXSP, path = %s, params = %s'%(path,params))
enumSEL = -1
while not self.monitor.abortRequested() and not enumSEL is None:
enumLST = [self.listitems.buildMenuListItem('Path',path,icon=ICON),self.listitems.buildMenuListItem('Order',dumpJSON(params.get('order',{})),icon=ICON),self.listitems.buildMenuListItem('Rules',dumpJSON(params.get('rules',{})),icon=ICON)]
enumSEL = self.selectDialog(enumLST,header="Edit Dynamic Path", multi=False)
if not enumSEL is None:
if enumLST[enumSEL].getLabel() == 'Path': path, params = mtype(params)
elif enumLST[enumSEL].getLabel() == 'Order': params = getOrder(params)
elif enumLST[enumSEL].getLabel() == 'Rules': params = getRules(params)
del jsonRPC
if len(params.get('rules',{}).get('and',[]) or params.get('rules',{}).get('and',[])) > 0:
url = '%s?xsp=%s'%(path,dumpJSON(params))
self.log('buildDXSP, returning %s'%(url))
return url
def getValue(self, params={}, rule={}):
# etype = params.get("type")
# efield = str(rule.get("field")).lower()
# evalue = ','.join([unquoteString(value) for value in rule.get('value',[])])
# LIST_SORT_ACTIONS = {"none" : (),
# "label" : (self.inputDialog,{message='Enter %s Value (Separate multiple values by ',' ex. Action,Comedy)'%(efield.title()),default=evalue,key=xbmcgui.INPUT_ALPHANUM}),
# "albumtype" : (self.inputDialog,{message='Enter %s Value (Separate multiple values by ',' ex. Action,Comedy)'%(efield.title()),default=evalue,key=xbmcgui.INPUT_ALPHANUM}),
# "country" : (self.inputDialog,{message='Enter %s Value (Separate multiple values by ',' ex. Action,Comedy)'%(efield.title()),default=evalue,key=xbmcgui.INPUT_ALPHANUM}),
# "originaltitle" : (self.inputDialog,{message='Enter %s Value (Separate multiple values by ',' ex. Action,Comedy)'%(efield.title()),default=evalue,key=xbmcgui.INPUT_ALPHANUM}),
# "productioncode" : (self.inputDialog,{message='Enter %s Value (Separate multiple values by ',' ex. Action,Comedy)'%(efield.title()),default=evalue,key=xbmcgui.INPUT_ALPHANUM}),
# "mpaa" : (self.inputDialog,{message='Enter %s Value (Separate multiple values by ',' ex. Action,Comedy)'%(efield.title()),default=evalue,key=xbmcgui.INPUT_ALPHANUM}),
# "votes" : (self.inputDialog,{message='Enter %s Value (Separate multiple values by ',' ex. Action,Comedy)'%(efield.title()),default=evalue,key=xbmcgui.INPUT_ALPHANUM}),
# "sorttitle" : (self.inputDialog,{message='Enter %s Value (Separate multiple values by ',' ex. Action,Comedy)'%(efield.title()),default=evalue,key=xbmcgui.INPUT_ALPHANUM}),
# "time" : (self.inputDialog,{message='Enter %s Value (Format ex. ####)'%(efield.title()),default=evalue,key=xbmcgui.INPUT_TIME}),
# "date" : (self.inputDialog,{message='Enter %s Value (Format ex. ####)'%(efield.title()),default=evalue,key=xbmcgui.INPUT_DATE})
# "originaldate" : (self.inputDialog,{message='Enter %s Value (Format ex. ####)'%(efield.title()),default=evalue,key=xbmcgui.INPUT_DATE}),
# "dateadded" : (self.inputDialog,{message='Enter %s Value (Format ex. ####)'%(efield.title()),default=evalue,key=xbmcgui.INPUT_DATE}),
# "lastplayed" : (self.inputDialog,{message='Enter %s Value (Format ex. ####)'%(efield.title()),default=evalue,key=xbmcgui.INPUT_NUMERIC}),
# "listeners" : (self.inputDialog,{message='Enter %s Value (Format ex. ####)'%(efield.title()),default=evalue,key=xbmcgui.INPUT_NUMERIC}),
# "size" : (self.inputDialog,{message='Enter %s Value (Format ex. ####)'%(efield.title()),default=evalue,key=xbmcgui.INPUT_NUMERIC}),
# "track" : (self.inputDialog,{message='Enter %s Value (Format ex. ####)'%(efield.title()),default=evalue,key=xbmcgui.INPUT_NUMERIC}),
# "programcount" : (self.inputDialog,{message='Enter %s Value (Format ex. ####)'%(efield.title()),default=evalue,key=xbmcgui.INPUT_NUMERIC}),
# "totalepisodes" : (self.inputDialog,{message='Enter %s Value (Format ex. ####)'%(efield.title()),default=evalue,key=xbmcgui.INPUT_NUMERIC}),
# "watchedepisodes": (self.inputDialog,{message='Enter %s Value (Format ex. ####)'%(efield.title()),default=evalue,key=xbmcgui.INPUT_NUMERIC}),
# "episode" : (self.inputDialog,{message='Enter %s Value (Format ex. ####)'%(efield.title()),default=evalue,key=xbmcgui.INPUT_NUMERIC}),
# "season" : (self.inputDialog,{message='Enter %s Value (Format ex. ####)'%(efield.title()),default=evalue,key=xbmcgui.INPUT_NUMERIC}),
# "playcount" : (self.inputDialog,{message='Enter %s Value (Format ex. ####)'%(efield.title()),default=evalue,key=xbmcgui.INPUT_NUMERIC}),
# "year" : (self.inputDialog,{message='Enter %s Value (Format ex. ####)'%(efield.title()),default=evalue,key=xbmcgui.INPUT_NUMERIC}),
# "rating" : (self.inputDialog,{message='Enter %s Value (Format ex. ####)'%(efield.title()),default=evalue,key=xbmcgui.INPUT_NUMERIC}),
# "userrating" : (self.inputDialog,{message='Enter %s Value (Format ex. ####)'%(efield.title()),default=evalue,key=xbmcgui.INPUT_NUMERIC}),
# "bpm" : (self.inputDialog,{message='Enter %s Value (Format ex. ####)'%(efield.title()),default=evalue,key=xbmcgui.INPUT_NUMERIC}),
# "totaldiscs" : (self.inputDialog,{message='Enter %s Value (Format ex. ####)'%(efield.title()),default=evalue,key=xbmcgui.INPUT_NUMERIC}),
# "bitrate" : (self.inputDialog,{message='Enter %s Value (Format ex. ####)'%(efield.title()),default=evalue,key=xbmcgui.INPUT_NUMERIC}),
# "top250" : (self.inputDialog,{message='Enter %s Value (Format ex. ####)'%(efield.title()),default=evalue,key=xbmcgui.INPUT_NUMERIC}),
# "file" : (self.browseDialog,{type=1,heading="",default=rule.get('value',[]),multi=True}),
# "path" : (self.browseDialog,{type=0,heading="",default=rule.get('value',[]),multi=True}),
# "drivetype" : (self.browseDialog,{type=0,heading="",default=rule.get('value',[]),multi=True}),
# "random" : (),
# "tvshowstatus" : (),
# "playlist" : (),
# "genre" : (self.selectDialog,({list=jsonRPC.getVideoGenres,jsonRPC.getMusicGenres)),
# "tvshowtitle" : (self.selectDialog,(jsonRPC.getTVshows,)),
# "title" : (self.selectDialog,(jsonRPC.getMovies)),
# "artist" : (self.selectDialog,jsonRPC.getArtists),
# "album" : (self.selectDialog,jsonRPC.getAlbums),
# "studio" : (self.selectDialog,(jsonRPC.getMovieStudios,jsonRPC.getNetworks))}
def __getInput(): return self.inputDialog("Enter Value\nSeparate by ',' ex. Action,Comedy",','.join([unquoteString(value) for value in rule.get('value',[])]))
def __getBrowse(): return self.browseSources(default='|'.join([unquoteString(value) for value in rule.get('value',[])]))
def __getSelect(): return self.notificationDialog(LANGUAGE(32020))
enumLST = sorted(['Enter', 'Browse', 'Select'])
enumKEY = {'Enter':{'func':__getInput},'Browse':{'func':__getBrowse},'Select':{'func':__getSelect}}
enumSEL = self.selectDialog(enumLST,header="Select Input",useDetails=False, multi=False)
if not enumSEL is None: return [quoteString(value) for value in (enumKEY[enumLST[enumSEL]].get('func')()).split(',')]