Files
DevOps/Kodi/Lenovo/addons/plugin.video.pseudotv.live/resources/lib/manager.py

974 lines
52 KiB
Python

# 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 <http://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from globals import *
from cache import Cache
from channels import Channels
from jsonrpc import JSONRPC
from rules import RulesList
from resources import Resources
from xsp import XSP
from infotagger.listitem import ListItemInfoTag
# Actions
ACTION_MOVE_LEFT = 1
ACTION_MOVE_RIGHT = 2
ACTION_MOVE_UP = 3
ACTION_MOVE_DOWN = 4
ACTION_SELECT_ITEM = 7
ACTION_INVALID = 999
ACTION_SHOW_INFO = [11,24,401]
ACTION_PREVIOUS_MENU = [92, 10,110,521] #+ [9, 92, 216, 247, 257, 275, 61467, 61448]
class Manager(xbmcgui.WindowXMLDialog):
monitor = MONITOR()
focusIndex = -1
newChannels = []
def __init__(self, *args, **kwargs):
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
def __get1stChannel(channelList):
for channel in channelList:
if not channel.get('id'): return channel.get('number')
return 1
def __findChannel(chnum, retitem=False, channels=[]):
for idx, channel in enumerate(channels):
if channel.get('number') == (chnum or 1):
if retitem: return channel
else: return idx
with BUILTIN.busy_dialog():
self.server = {}
self.lockAutotune = True
self.madeChanges = False
self.madeItemchange = False
self.lastActionTime = time.time()
self.cntrlStates = {}
self.showingList = True
self.startChannel = kwargs.get('channel',-1)
self.openChannel = kwargs.get('open')
self.cache = SETTINGS.cache
self.channels = Channels()
self.rule = RulesList()
self.jsonRPC = JSONRPC()
self.resource = Resources()
self.host = PROPERTIES.getRemoteHost()
self.friendly = SETTINGS.getFriendlyName()
self.newChannel = self.channels.getTemplate()
self.eChannels = self.loadChannels(SETTINGS.getSetting('Default_Channels'))
try:
if self.eChannels is None: raise Exception("No Channels Found!")
else:
self.channelList = self.channels.sortChannels(self.createChannelList(self.buildArray(), self.eChannels))
self.newChannels = self.channelList.copy()
if self.startChannel == -1: self.startChannel = __get1stChannel(self.channelList)
if self.startChannel <= CHANNEL_LIMIT: self.focusIndex = (self.startChannel - 1) #Convert from Channel number to array index
else: self.focusIndex = __findChannel(self.startChannel,channels=self.channelList)
if self.openChannel: self.openChannel = self.channelList[self.focusIndex]
self.log('Manager, startChannel = %s, focusIndex = %s, openChannel = %s'%(self.startChannel, self.focusIndex, self.openChannel))
if kwargs.get('start',True): self.doModal()
except Exception as e:
self.log('Manager failed! %s'%(e), xbmc.LOGERROR)
self.closeManager()
def log(self, msg, level=xbmc.LOGDEBUG):
return log('%s: %s'%(self.__class__.__name__,msg),level)
def onInit(self):
try:
self.focusItems = dict()
self.spinner = self.getControl(4)
self.chanList = self.getControl(5)
self.itemList = self.getControl(6)
self.right_button1 = self.getControl(9001)
self.right_button2 = self.getControl(9002)
self.right_button3 = self.getControl(9003)
self.right_button4 = self.getControl(9004)
self.fillChanList(self.newChannels,focus=self.focusIndex,channel=self.openChannel)
except Exception as e:
log("onInit, failed! %s"%(e), xbmc.LOGERROR)
self.closeManager()
def getServers(self):
from multiroom import Multiroom
return Multiroom().getDiscovery()
def loadChannels(self, name=''):
self.log('loadChannels, name = %s'%(name))
channels = self.channels.getChannels()
if name == self.friendly: return channels
elif name == LANGUAGE(30022):#Auto
if len(channels) > 0: return channels
else: return self.loadChannels('Ask')
elif name == 'Ask':
def __buildItem(servers, server):
if server.get('online',False):
return self.buildListItem(server.get('name'),'%s - %s: Channels (%s)'%(LANGUAGE(32211)%({True:'green',False:'red'}[server.get('online',False)],{True:LANGUAGE(32158),False:LANGUAGE(32253)}[server.get('online',False)]),server.get('host'),len(server.get('channels',[]))),icon=DUMMY_ICON.format(text=str(servers.index(server)+1)))
servers = self.getServers()
lizlst = poolit(__buildItem)(*(list(servers.values()),list(servers.values())))
lizlst.insert(0,self.buildListItem(self.friendly,'%s - %s: Channels (%s)'%('[B]Local[/B]',self.host,len(channels)),icon=ICON))
select = DIALOG.selectDialog(lizlst, LANGUAGE(30173), None, True, SELECT_DELAY, False)
if not select is None: return self.loadChannels(lizlst[select].getLabel())
else: return
elif name:
self.server = self.getServers().get(name,{})
return self.server.get('channels',[])
return channels
@cacheit(json_data=True)
def buildArray(self):
self.log('buildArray') # Create blank array of citem templates.
def __create(idx):
newChannel = self.newChannel.copy()
newChannel['number'] = idx + 1
return newChannel
return poolit(__create)(list(range(CHANNEL_LIMIT)))
def createChannelList(self, channelArray, channelList):
self.log('createChannelList') # Fill blank array with citems from channels.json
def __update(item):
try: channelArray[item["number"]-1].update(item) #CUSTOM
except: channelArray.append(item) #AUTOTUNE
checksum = getMD5(dumpJSON(channelList))
cacheName = 'createChannelList.%s'%(checksum)
cacheResponse = self.cache.get(cacheName, checksum=checksum, json_data=True)
if not cacheResponse:
poolit(__update)(channelList)
cacheResponse = self.cache.set(cacheName, channelArray, checksum=checksum, json_data=True)
return cacheResponse
def buildListItem(self, label: str="", label2: str="", icon: str="", paths: list=[], items: dict={}):
if not icon: icon = (items.get('citem',{}).get('logo') or COLOR_LOGO)
if not paths: paths = (items.get('citem',{}).get("path") or [])
return LISTITEMS.buildMenuListItem(label, label2, icon, url='|'.join(paths), props=items)
def fillChanList(self, channelList, refresh=False, focus=None, channel=None):
self.log('fillChanList, focus = %s, channel = %s'%(focus,channel))
def __buildItem(citem):
isPredefined = citem["number"] > CHANNEL_LIMIT
isFavorite = citem.get('favorite',False)
isRadio = citem.get('radio',False)
isLocked = isPredefined #todo parse channel lock rule
channelColor = COLOR_UNAVAILABLE_CHANNEL
labelColor = COLOR_UNAVAILABLE_CHANNEL
if citem.get("path"):
if isPredefined: channelColor = COLOR_LOCKED_CHANNEL
else:
labelColor = COLOR_AVAILABLE_CHANNEL
if isLocked: channelColor = COLOR_LOCKED_CHANNEL
elif isFavorite: channelColor = COLOR_FAVORITE_CHANNEL
elif isRadio: channelColor = COLOR_RADIO_CHANNEL
else: channelColor = COLOR_AVAILABLE_CHANNEL
return self.buildListItem('[COLOR=%s][B]%s|[/COLOR][/B]'%(channelColor,citem["number"]),'[COLOR=%s]%s[/COLOR]'%(labelColor,citem.get("name",'')),items={'citem':citem,'chname':citem["name"],'chnum':'%i'%(citem["number"]),'radio':citem.get('radio',False),'description':LANGUAGE(32169)%(citem["number"],self.server.get('name',self.friendly))})
self.togglechanList(reset=refresh)
with self.toggleSpinner():
listitems = poolit(__buildItem)(channelList)
self.chanList.addItems(listitems)
if focus is None: self.chanList.selectItem(self.setFocusPOS(listitems))
else: self.chanList.selectItem(focus)
self.setFocus(self.chanList)
if channel: self.buildChannelItem(channel)
@contextmanager
def toggleSpinner(self, state=True, allow=True):
if allow:
self.setVisibility(self.spinner,state)
try: yield
finally: self.setVisibility(self.spinner,False)
else: yield
def togglechanList(self, state=True, focus=0, reset=False):
self.log('togglechanList, state = %s, focus = %s, reset = %s'%(state,focus,reset))
with self.toggleSpinner():
if state: # channellist
if reset:
self.setVisibility(self.chanList,False)
self.chanList.reset()
self.setVisibility(self.itemList,False)
self.setVisibility(self.chanList,True)
self.setFocus(self.chanList)
self.chanList.selectItem(focus)
if self.madeChanges:
self.setLabels(self.right_button1,LANGUAGE(32059))#Save
self.setLabels(self.right_button2,LANGUAGE(32060))#Cancel
self.setLabels(self.right_button3,LANGUAGE(32136))#Move
self.setLabels(self.right_button4,LANGUAGE(32061))#Delete
self.setEnableCondition(self.right_button1,'[!String.IsEmpty(Container(5).ListItem(Container(5).Position).Property(chnum))]')
self.setEnableCondition(self.right_button2,'[!String.IsEmpty(Container(5).ListItem(Container(5).Position).Property(chnum))]')
else:
self.setLabels(self.right_button1,LANGUAGE(32062))#Close
self.setLabels(self.right_button2,LANGUAGE(32235))#Preview
self.setLabels(self.right_button3,LANGUAGE(32136))#Move
self.setLabels(self.right_button4,LANGUAGE(32061))#Delete
self.setEnableCondition(self.right_button1,'[!String.IsEmpty(Container(5).ListItem(Container(5).Position).Property(chnum))]')
self.setEnableCondition(self.right_button2,'[!String.IsEmpty(Container(5).ListItem(Container(5).Position).Path) + String.IsEqual(Container(5).ListItem(Container(5).Position).Property(radio),False)]')
self.setFocus(self.right_button1)
self.setEnableCondition(self.right_button3,'[!String.IsEmpty(Container(5).ListItem(Container(5).Position).Path)]')# + Integer.IsLessOrEqual(Container(5).ListItem(Container(5).Position).Property(chnum),CHANNEL_LIMIT)]')
self.setEnableCondition(self.right_button4,'[!String.IsEmpty(Container(5).ListItem(Container(5).Position).Path)]')# + Integer.IsLessOrEqual(Container(5).ListItem(Container(5).Position).Property(chnum),CHANNEL_LIMIT)]')
else: # channelitems
self.itemList.reset()
self.setVisibility(self.chanList,False)
self.setVisibility(self.itemList,True)
self.itemList.selectItem(focus)
self.setFocus(self.itemList)
if self.madeItemchange:
self.setLabels(self.right_button1,LANGUAGE(32240))#Confirm
self.setLabels(self.right_button2,LANGUAGE(32060))#Cancel
self.setEnableCondition(self.right_button1,'[!String.IsEmpty(Container(6).ListItem(Container(6).Position).Label) + !String.IsEmpty(Container(6).ListItem(Container(6).Position).Path)]')
self.setEnableCondition(self.right_button2,'[!String.IsEmpty(Container(6).ListItem(Container(6).Position).Property(chnum))]')
else:
self.setLabels(self.right_button1,LANGUAGE(32062))#Close
self.setLabels(self.right_button2,LANGUAGE(32060))#Cancel
self.setEnableCondition(self.right_button1,'[!String.IsEmpty(Container(6).ListItem(Container(6).Position).Property(chnum))]')
self.setEnableCondition(self.right_button2,'[!String.IsEmpty(Container(6).ListItem(Container(6).Position).Path)]')
self.setLabels(self.right_button3,LANGUAGE(32235))#Preview
self.setLabels(self.right_button4,LANGUAGE(32239))#Clear
self.setEnableCondition(self.right_button3,'[!String.IsEmpty(Container(6).ListItem(Container(6).Position).Path) + String.IsEqual(Container(6).ListItem(Container(6).Position).Property(radio),False)]')
self.setEnableCondition(self.right_button4,'[!String.IsEmpty(Container(6).ListItem(Container(6).Position).Path)]')
def setFocusPOS(self, listitems, chnum=None, ignore=True):
for idx, listitem in enumerate(listitems):
chnumber = int(cleanLabel(listitem.getLabel()).strip('|'))
if ignore and chnumber > CHANNEL_LIMIT: continue
elif chnum is not None and chnum == chnumber: return idx
elif chnum is None and cleanLabel(listitem.getLabel2()): return idx
return 0
def getRuleAbbr(self, citem, myId, optionindex):
value = citem.get('rules',{}).get(str(myId),{}).get('values',{}).get(str(optionindex))
self.log('getRuleAbbr, id = %s, myId = %s, optionindex = %s, optionvalue = %s'%(citem.get('id',-1),myId,optionindex,value))
return value
def getLogoColor(self, citem):
self.log('getLogoColor, id = %s'%(citem.get('id',-1)))
if (citem.get('logo') and citem.get('name')) is None: return 'FFFFFFFF'
elif citem.get('rules',{}).get("1"):
if (self.getRuleAbbr(citem,1,4) or self.resource.isMono(citem['logo'])):
return self.getRuleAbbr(citem,1,3)
return SETTINGS.getSetting('ChannelBug_Color')
def buildChannelItem(self, citem: dict={}, focuskey: str='path'):
self.log('buildChannelItem, id = %s, focuskey = %s'%(citem.get('id'),focuskey))
def __buildItem(key):
key = key.lower()
value = citem.get(key,' ')
if key in ["number","type","logo","id","catchup"]: return # keys to ignore, internal use only.
elif isinstance(value,(list,dict)):
if key == "group" : value = ('|'.join(sorted(set(value))) or LANGUAGE(30127))
elif key == "path" : value = '|'.join(value)
elif key == "rules" : value = '|'.join([rule.name for key, rule in list(self.rule.loadRules([citem]).get(citem['id'],{}).items())])#todo load rule names
elif not isinstance(value,str): value = str(value)
elif not value: value = ' '
return self.buildListItem(LABEL.get(key,' '),value,items={'key':key,'value':value,'citem':citem,'chname':citem["name"],'chnum':'%i'%(citem["number"]),'radio':citem.get('radio',False),'description':DESC.get(key,''),'colorDiffuse':self.getLogoColor(citem)})
self.togglechanList(False)
with self.toggleSpinner():
LABEL = {'name' : LANGUAGE(32092),
'path' : LANGUAGE(32093),
'group' : LANGUAGE(32094),
'rules' : LANGUAGE(32095),
'radio' : LANGUAGE(32091),
'favorite': LANGUAGE(32090)}
DESC = {'name' : LANGUAGE(32085),
'path' : LANGUAGE(32086),
'group' : LANGUAGE(32087),
'rules' : LANGUAGE(32088),
'radio' : LANGUAGE(32084),
'favorite': LANGUAGE(32083)}
listitems = poolit(__buildItem)(list(self.newChannel.keys()))
self.itemList.addItems(listitems)
self.itemList.selectItem([idx for idx, liz in enumerate(listitems) if liz.getProperty('key')== focuskey][0])
self.setFocus(self.itemList)
def itemInput(self, channelListItem=xbmcgui.ListItem()):
def __getName(citem: dict={}, name: str=''):
return DIALOG.inputDialog(message=LANGUAGE(32079),default=name), citem
def __getPath(citem: dict={}, paths: list=[]):
return self.getPaths(citem, paths)
def __getGroups(citem: dict={}, groups: list=[]):
groups = list([_f for _f in groups if _f])
ngroups = sorted([_f for _f in set(SETTINGS.getSetting('User_Groups').split('|') + GROUP_TYPES + groups) if _f])
ngroups.insert(0, '-%s'%(LANGUAGE(30064)))
selects = DIALOG.selectDialog(ngroups,header=LANGUAGE(32081),preselect=findItemsInLST(ngroups,groups),useDetails=False)
if 0 in selects:
SETTINGS.setSetting('User_Groups',DIALOG.inputDialog(LANGUAGE(32044), default=SETTINGS.getSetting('User_Groups')))
return __getGroups(citem, groups)
elif len(ngroups) > 0: groups = [ngroups[idx] for idx in selects]
if not groups: groups = [LANGUAGE(30127)]
return groups, citem
def __getRule(citem: dict={}, rules: dict={}):
return self.getRules(citem, rules)
def __getBool(citem: dict={}, state: bool=False):
return not bool(state), citem
key = channelListItem.getProperty('key')
value = channelListItem.getProperty('value')
citem = loadJSON(channelListItem.getProperty('citem'))
self.log('itemInput, In value = %s, key = %s\ncitem = %s'%(value,key,citem))
KEY_INPUT = {"name" : {'func':__getName , 'kwargs':{'citem':citem, 'name' :citem.get('name','')}},
"path" : {'func':__getPath , 'kwargs':{'citem':citem, 'paths' :citem.get('path',[])}},
"group" : {'func':__getGroups, 'kwargs':{'citem':citem, 'groups':citem.get('group',[])}},
"rules" : {'func':__getRule , 'kwargs':{'citem':citem, 'rules' :self.rule.loadRules([citem],incRez=False).get(citem['id'],{})}},
"radio" : {'func':__getBool , 'kwargs':{'citem':citem, 'state' :citem.get('radio',False)}},
"favorite" : {'func':__getBool , 'kwargs':{'citem':citem, 'state' :citem.get('favorite',False)}}}
action = KEY_INPUT.get(key)
retval, citem = action['func'](*action.get('args',()),**action.get('kwargs',{}))
retval, citem = self.validateInputs(key,retval,citem)
if not retval is None:
self.madeItemchange = True
if key in list(self.newChannel.keys()): citem[key] = retval
self.log('itemInput, Out value = %s, key = %s\ncitem = %s'%(retval,key,citem))
return citem
def getPaths(self, citem: dict={}, paths: list=[]):
select = -1
epaths = paths.copy()
pathLST = list([_f for _f in paths if _f])
lastOPT = None
if not citem.get('radio',False) and isRadio({'path':paths}): citem['radio'] = True #set radio on music paths
if citem.get('radio',False): excLST = [10,12,21,22]
else: excLST = [11,13,21]
while not self.monitor.abortRequested() and not select is None:
with self.toggleSpinner():
npath = None
lizLST = [self.buildListItem('%s|'%(idx+1),path,paths=[path],icon=DUMMY_ICON.format(text=str(idx+1)),items={'citem':citem,'idx':idx+1}) for idx, path in enumerate(pathLST) if path]
lizLST.insert(0,self.buildListItem('[COLOR=white][B]%s[/B][/COLOR]'%(LANGUAGE(32100)),LANGUAGE(33113),icon=ICON,items={'key':'add','citem':citem,'idx':0}))
if len(pathLST) > 0 and epaths != pathLST: lizLST.insert(1,self.buildListItem('[COLOR=white][B]%s[/B][/COLOR]'%(LANGUAGE(32101)),LANGUAGE(33114),icon=ICON,items={'key':'save','citem':citem}))
select = DIALOG.selectDialog(lizLST, header=LANGUAGE(32086), preselect=lastOPT, multi=False)
with self.toggleSpinner():
if not select is None:
key, path = lizLST[select].getProperty('key'), lizLST[select].getPath()
try: lastOPT = int(lizLST[select].getProperty('idx'))
except: lastOPT = None
if key == 'add':
retval = DIALOG.browseSources(heading=LANGUAGE(32080), exclude=excLST, monitor=True)
if not retval is None:
npath, citem = self.validatePaths(retval,citem)
if npath: pathLST.append(npath)
elif key == 'save':
paths = pathLST
break
elif path in pathLST:
retval = DIALOG.yesnoDialog(LANGUAGE(32102), customlabel=LANGUAGE(32103))
if retval in [1,2]: pathLST.pop(pathLST.index(path))
if retval == 2:
with self.toggleSpinner():
npath, citem = self.validatePaths(DIALOG.browseSources(heading=LANGUAGE(32080), default=path, monitor=True, exclude=excLST), citem)
pathLST.append(npath)
self.log('getPaths, paths = %s'%(paths))
return paths, citem
def getRules(self, citem: dict={}, rules: dict={}):
if citem.get('id') is None or len(citem.get('path',[])) == 0: DIALOG.notificationDialog(LANGUAGE(32071))
else:
select = -1
erules = rules.copy()
ruleLST = rules.copy()
lastIDX = None
lastXID = None
while not self.monitor.abortRequested() and not select is None:
with self.toggleSpinner():
nrule = None
crules = self.rule.loadRules([citem],append=True,incRez=False).get(citem['id'],{}) #all rule instances w/ channel rules
arules = [rule for key, rule in list(crules.items()) if not ruleLST.get(key)] #all unused rule instances
lizLST = [self.buildListItem(rule.name,rule.getTitle(),icon=DUMMY_ICON.format(text=str(rule.myId)),items={'myId':rule.myId,'citem':citem,'idx':list(ruleLST.keys()).index(key)+1}) for key, rule in list(ruleLST.items()) if rule.myId]
lizLST.insert(0,self.buildListItem('[COLOR=white][B]%s[/B][/COLOR]'%(LANGUAGE(32173)),"",icon=ICON,items={'key':'add' ,'citem':citem,'idx':0}))
if len(ruleLST) > 0 and erules != ruleLST: lizLST.insert(1,self.buildListItem('[COLOR=white][B]%s[/B][/COLOR]'%(LANGUAGE(32174)),"",icon=ICON,items={'key':'save','citem':citem}))
select = DIALOG.selectDialog(lizLST, header=LANGUAGE(32095), preselect=lastIDX, multi=False)
if not select is None:
key, myId = lizLST[select].getProperty('key'), int(lizLST[select].getProperty('myId') or '-1')
try: lastIDX = int(lizLST[select].getProperty('idx'))
except: lastIDX = None
if key == 'add':
with self.toggleSpinner():
lizLST = [self.buildListItem(rule.name,rule.description,icon=DUMMY_ICON.format(text=str(rule.myId)),items={'idx':idx,'myId':rule.myId,'citem':citem}) for idx, rule in enumerate(arules) if rule.myId]
select = DIALOG.selectDialog(lizLST, header=LANGUAGE(32072), preselect=lastXID, multi=False)
try: lastXID = int(lizLST[select].getProperty('idx'))
except: lastXID = -1
nrule, citem = self.getRule(citem, arules[lastXID])
if not nrule is None: ruleLST.update({str(nrule.myId):nrule})
elif key == 'save':
rules = ruleLST
break
elif ruleLST.get(str(myId)):
retval = DIALOG.yesnoDialog(LANGUAGE(32175), customlabel=LANGUAGE(32176))
if retval in [1,2]: ruleLST.pop(str(myId))
if retval == 2:
nrule, citem = self.getRule(citem, crules.get(str(myId),{}))
if not nrule is None: ruleLST.update({str(nrule.myId):nrule})
# elif not ruleLST.get(str(myId)):
# nrule, citem = self.getRule(citem, crules.get(str(myId),{}))
# if not nrule is None: ruleLST.update({str(nrule.myId):nrule})
self.log('getRules, rules = %s'%(len(rules)))
return self.rule.dumpRules(rules), citem
def getRule(self, citem={}, rule={}):
self.log('getRule, name = %s'%(rule.name))
if rule.exclude and True in list(set([True for p in citem.get('path',[]) if p.endswith('.xsp')])): return DIALOG.notificationDialog(LANGUAGE(32178))
else:
select = -1
while not self.monitor.abortRequested() and not select is None:
with self.toggleSpinner():
lizLST = [self.buildListItem('%s = %s'%(rule.optionLabels[idx],rule.optionValues[idx]),rule.optionDescriptions[idx],DUMMY_ICON.format(text=str(idx+1)),[str(rule.myId)],{'value':optionValue,'idx':idx,'myId':rule.myId,'citem':citem}) for idx, optionValue in enumerate(rule.optionValues)]
select = DIALOG.selectDialog(lizLST, header='%s %s - %s'%(LANGUAGE(32176),rule.myId,rule.name), multi=False)
if not select is None:
try: rule.onAction(int(lizLST[select].getProperty('idx') or "0"))
except Exception as e:
self.log("getRule, onAction failed! %s"%(e), xbmc.LOGERROR)
DIALOG.okDialog(LANGUAGE(32000))
return rule, citem
def setID(self, citem: dict={}) -> dict:
if not citem.get('id') and citem.get('name') and citem.get('path') and citem.get('number'):
citem['id'] = getChannelID(citem['name'], citem['path'], citem['number'])
self.log('setID, id = %s'%(citem['id']))
return citem
def setName(self, path, citem: dict={}) -> dict:
with self.toggleSpinner():
if citem.get('name'): return citem
elif path.strip('/').endswith(('.xml','.xsp')): citem['name'] = XSP().getName(path)
elif path.startswith(tuple(DB_TYPES+WEB_TYPES+VFS_TYPES)): citem['name'] = self.getMontiorList().getLabel()
else: citem['name'] = os.path.basename(os.path.dirname(path)).strip('/')
self.log('setName, id = %s, name = %s'%(citem['id'],citem['name']))
return citem
def setLogo(self, name=None, citem={}, force=False):
name = (name or citem.get('name'))
if name:
if force: logo = ''
else: logo = citem.get('logo')
if not logo or logo in [LOGO,COLOR_LOGO,ICON]:
with self.toggleSpinner():
citem['logo'] = self.resource.getLogo(citem,auto=True)
self.log('setLogo, id = %s, logo = %s, force = %s'%(citem.get('id'),citem.get('logo'),force))
return citem
def validateInputs(self, key, value, citem):
self.log('validateInputs, key = %s, value = %s'%(key,value))
def __validateName(name, citem):
if name and (len(name) > 1 or len(name) < 128):
citem['name'] = validString(name)
self.log('__validateName, name = %s'%(citem['name']))
return citem['name'], self.setLogo(name, citem, force=True)
return None, citem
def __validatePath(paths, citem):
if len(paths) > 0:
name, citem = __validateName(citem.get('name',''),self.setName(paths[0], citem))
self.log('__validatePath, name = %s, paths = %s'%(name,paths))
return paths, citem
return None, citem
def __validateGroup(groups, citem):
return groups, citem #todo check values
def __validateRules(rules, citem):
return rules, citem #todo check values
def __validateBool(state, citem):
if isinstance(state,bool): return state, citem
return None, citem
KEY_VALIDATION = {'name' :__validateName,
'path' :__validatePath,
'group' :__validateGroup,
'rules' :__validateRules,
'radio' :__validateBool,
'favorite':__validateBool}.get(key,None)
try:
with toggleSpinner():
retval, citem = KEY_VALIDATION(value,citem)
if retval is None:
DIALOG.notificationDialog(LANGUAGE(32077)%key.title())
return None , citem
return retval, self.setID(citem)
except Exception as e:
self.log("validateInputs, key = %s no action! %s"%(key,e))
return value, citem
def validatePaths(self, path, citem, spinner=True):
self.log('validatePaths, path = %s'%path)
def __set(path, citem):
citem = self.setName(path, citem)
return path, self.setLogo(citem.get('name'),citem)
def __seek(item, citem, cnt, dia, passed=False):
player = PLAYER()
if player.isPlaying(): return DIALOG.notificationDialog(LANGUAGE(30136))
# todo test seek for support disable via adv. rule if fails.
# todo set seeklock rule if seek == False
liz = xbmcgui.ListItem('Seek Test', path=item.get('file'))
liz.setProperty('startoffset', str(int(item.get('duration')//8)))
infoTag = ListItemInfoTag(liz, 'video')
infoTag.set_resume_point({'ResumeTime':int(item.get('duration')/4),'TotalTime':int(item.get('duration')*60)})
getTime = 0
waitTime = FIFTEEN
player.play(item.get('file'),liz)
while not self.monitor.abortRequested():
waitTime -= 1
self.log('validatePaths _seek, waiting (%s) to seek %s'%(waitTime, item.get('file')))
if self.monitor.waitForAbort(1.0) or waitTime < 1: break
elif not player.isPlaying(): continue
elif ((int(player.getTime()) > getTime) or BUILTIN.getInfoBool('SeekEnabled','Player')):
self.log('validatePaths _seek, found playable and seek-able file %s'%(item.get('file')))
passed = True
break
player.stop()
del player
if not passed:
retval = DIALOG.yesnoDialog(LANGUAGE(30202),customlabel='Try Again (%s)'%(cnt))
if retval == 1: passed = True
elif retval == 2: passed = None
self.log('validatePaths _seek, passed = %s'%(passed))
return passed
def __vfs(path, citem, valid=False):
if isRadio({'path':[path]}) or isMixed_XSP({'path':[path]}): return True #todo check mixed xsp.
with BUILTIN.busy_dialog():
items = self.jsonRPC.walkFileDirectory(path, 'music' if isRadio({'path':[path]}) else 'video', depth=5, retItem=True)
if len(items) > 0:
cnt = 3
msg = '%s %s, %s..\n%s'%(LANGUAGE(32098),'Path',LANGUAGE(32099),'%s...'%(str(path)))
dia = DIALOG.progressDialog(message=msg)
for idx, dir in enumerate(items):
if self.monitor.waitForAbort(0.0001): break
elif cnt <= 3 and cnt > 0:
item = random.choice(items.get(dir,[]))
msg = '%s %s...\n%s\n%s'%(LANGUAGE(32098),'Duration','%s...'%(dir),'%s...'%(item.get('file','')))
dia = DIALOG.progressDialog(int((idx*100)//len(items)), control=dia, message=msg)
item.update({'duration':self.jsonRPC.getDuration(item.get('file'), item, accurate=bool(SETTINGS.getSettingInt('Duration_Type')))})
if item.get('duration',0) == 0: continue
msg = '%s %s...\n%s\n%s'%(LANGUAGE(32098),'Seeking','%s...'%(str(dir)),'%s...'%(str(item.get('file',''))))
dia = DIALOG.progressDialog(int((idx*100)//len(items)), control=dia, message=msg)
valid = __seek(item, citem, cnt, dia)
if valid is None: cnt -=1
else: break
DIALOG.progressDialog(100,control=dia)
return valid
with self.toggleSpinner(allow=spinner):
if __vfs(path, citem): return __set(path, citem)
DIALOG.notificationDialog(LANGUAGE(32030))
return None, citem
def openEditor(self, path):
self.log('openEditor, path = %s'%(path))
if '|' in path:
path = path.split('|')
path = path[0]#prompt user to select:
media = 'video' if 'video' in path else 'music'
if '.xsp' in path: return self.openEditor(path,media)
elif '.xml' in path: return self.openNode(path,media)
def previewChannel(self, citem, retCntrl=None):
def __buildItem(fileList, fitem):
return self.buildListItem('%s| %s'%(fileList.index(fitem),fitem.get('showlabel',fitem.get('label'))), fitem.get('file') ,icon=(getThumb(fitem,opt=EPG_ARTWORK) or {0:FANART,1:COLOR_LOGO}[EPG_ARTWORK]))
def __fileList(citem):
from builder import Builder
builder = Builder()
fileList = []
start_time = 0
end_time = 0
if PROPERTIES.isInterruptActivity(): PROPERTIES.setInterruptActivity(False)
while not self.monitor.abortRequested() and PROPERTIES.isRunning('OVERLAY_MANAGER'):
if self.monitor.waitForAbort(1.0): break
elif not PROPERTIES.isRunning('builder.build') and not PROPERTIES.isInterruptActivity():
DIALOG.notificationDialog('%s: [B]%s[/B]\n%s'%(LANGUAGE(32236),citem.get('name','Untitled'),LANGUAGE(32140)))
tmpcitem = citem.copy()
tmpcitem['id'] = getChannelID(citem['name'], citem['path'], random.random())
start_time = time.time()
fileList = builder.build([tmpcitem],preview=True)
end_time = time.time()
if not fileList or isinstance(fileList,list): break
del builder
if not PROPERTIES.isInterruptActivity(): PROPERTIES.setInterruptActivity(True)
return fileList, round(abs(end_time-start_time),2)
if not PROPERTIES.isRunning('previewChannel'):
with PROPERTIES.chkRunning('previewChannel'), self.toggleSpinner():
listitems = []
fileList, run_time = __fileList(citem)
if not isinstance(fileList,list) and not fileList: DIALOG.notificationDialog('%s or\n%s'%(LANGUAGE(32030),LANGUAGE(32000)))
else:
listitems = poolit(__buildItem)(*(fileList,fileList))
self.log('previewChannel, id = %s, listitems = %s'%(citem['id'],len(listitems)))
if len(listitems) > 0: return DIALOG.selectDialog(listitems, header='%s: [B]%s[/B] - Build Time: [B]%ss[/B]'%(LANGUAGE(32235),citem.get('name','Untitled'),f"{run_time:.2f}"))
if retCntrl: self.setFocusId(retCntrl)
def getMontiorList(self):
self.log('getMontiorList')
try:
with self.toggleSpinner():
labels = sorted(set([cleanLabel(value).title() for info in DIALOG.getInfoMonitor() for key, value in list(info.items()) if value not in ['','..'] and key not in ['path','logo']]))
itemLST = [self.buildListItem(label,icon=ICON) for label in labels]
if len(itemLST) == 0: raise Exception()
itemSEL = DIALOG.selectDialog(itemLST,LANGUAGE(32078)%('Name'),useDetails=True,multi=False)
if itemSEL is not None: return itemLST[itemSEL]
else: raise Exception()
except: return xbmcgui.ListItem(LANGUAGE(32079))
def clearChannel(self, item, prompt=True, open=False):
self.log('clearChannel, channelPOS = %s'%(item['number'] - 1))
with self.toggleSpinner():
if item['number'] > CHANNEL_LIMIT: return DIALOG.notificationDialog(LANGUAGE(32238))
elif prompt and not DIALOG.yesnoDialog(LANGUAGE(32073)): return item
self.madeItemchange = True
nitem = self.newChannel.copy()
nitem['number'] = item['number'] #preserve channel number
self.saveChannelItems(nitem, open)
def moveChannel(self, citem, channelPOS):
self.log('moveChannel, channelPOS = %s'%(channelPOS))
if citem['number'] > CHANNEL_LIMIT: return DIALOG.notificationDialog(LANGUAGE(32064))
retval = DIALOG.inputDialog(LANGUAGE(32137), key=xbmcgui.INPUT_NUMERIC, opt=citem['number'])
if retval:
retval = int(retval)
if (retval > 0 and retval < CHANNEL_LIMIT) and retval != channelPOS + 1:
if DIALOG.yesnoDialog('%s %s %s from [B]%s[/B] to [B]%s[/B]?'%(LANGUAGE(32136),citem['name'],LANGUAGE(32023),citem['number'],retval)):
with self.toggleSpinner():
if retval in [channel.get('number') for channel in self.newChannels if channel.get('path')]: DIALOG.notificationDialog(LANGUAGE(32138))
else:
self.madeItemchange = True
nitem = self.newChannel.copy()
nitem['number'] = channelPOS + 1
self.newChannels[channelPOS] = nitem
citem['number'] = retval
self.saveChannelItems(citem)
def switchLogo(self, channelData, channelPOS):
def __cleanLogo(chlogo):
#todo convert resource from vfs to fs
# return chlogo.replace('resource://','special://home/addons/')
# resource = path.replace('/resources','').replace(,)
# resource://resource.images.studios.white/Amazon.png
return chlogo
def __select():
def _build(logos, logo):
label = os.path.splitext(os.path.basename(logo))[0]
return self.buildListItem('%s| %s'%(logos.index(logo)+1, label.upper() if len(label) <= 4 else label.title()), unquoteString(logo), logo, [logo])
DIALOG.notificationDialog(LANGUAGE(32140))
with self.toggleSpinner():
chname = channelData.get('name')
logos = self.resource.selectLogo(channelData)
listitems = poolit(_build)(*(logos,logos))
select = DIALOG.selectDialog(listitems,'%s (%s)'%(LANGUAGE(32066).split('[CR]')[1],chname),useDetails=True,multi=False)
if select is not None: return listitems[select].getPath()
def __browse():
with self.toggleSpinner():
chname = channelData.get('name')
retval = DIALOG.browseSources(type=1,heading='%s (%s)'%(LANGUAGE(32066).split('[CR]')[0],chname), default=channelData.get('icon',''), shares='files', mask=xbmc.getSupportedMedia('picture'), exclude=[12,13,14,15,16,17,21,22])
if FileAccess.copy(__cleanLogo(retval), os.path.join(LOGO_LOC,'%s%s'%(chname,retval[-4:])).replace('\\','/')):
if FileAccess.exists(os.path.join(LOGO_LOC,'%s%s'%(chname,retval[-4:])).replace('\\','/')):
return os.path.join(LOGO_LOC,'%s%s'%(chname,retval[-4:])).replace('\\','/')
return retval
def __match():
with self.toggleSpinner():
return self.resource.getLogo(channelData,auto=True)
if not channelData.get('name'): return DIALOG.notificationDialog(LANGUAGE(32065))
chlogo = None
retval = DIALOG.yesnoDialog(LANGUAGE(32066), heading ='%s - %s'%(ADDON_NAME,LANGUAGE(32172)),
nolabel = LANGUAGE(32067), #Select
yeslabel = LANGUAGE(32068), #Browse
customlabel = LANGUAGE(30022)) #Auto
if retval == 0: chlogo = __select()
elif retval == 1: chlogo = __browse()
elif retval == 2: chlogo = __match()
else: DIALOG.notificationDialog(LANGUAGE(32070))
if chlogo and chlogo != LOGO:
self.log('switchLogo, chname = %s, chlogo = %s'%(channelData.get('name'),chlogo))
DIALOG.notificationDialog(LANGUAGE(32139))
self.madeChanges = True
channelData['logo'] = chlogo
self.newChannels[channelPOS] = channelData
self.fillChanList(self.newChannels,refresh=True,focus=channelPOS)
def isVisible(self, cntrl):
try:
if isinstance(cntrl, int): cntrl = self.getControl(cntrl)
state = cntrl.isVisible()
except: state = self.cntrlStates.get(cntrl.getId(),False)
self.log('isVisible, cntrl = %s, state = %s'%(cntrl.getId(),state))
return state
def setVisibility(self, cntrl, state):
try:
if isinstance(cntrl, int): cntrl = self.getControl(cntrl)
cntrl.setVisible(state)
self.cntrlStates[cntrl.getId()] = state
self.log('setVisibility, cntrl = ' + str(cntrl.getId()) + ', state = ' + str(state))
except Exception as e: self.log("setVisibility, failed! %s"%(e), xbmc.LOGERROR)
def getLabels(self, cntrl):
try:
if isinstance(cntrl, int): cntrl = self.getControl(cntrl)
return cntrl.getLabel(), cntrl.getLabel2()
except Exception as e: return '',''
def setImages(self, cntrl, image='NA.png'):
try:
if isinstance(cntrl, int): cntrl = self.getControl(cntrl)
cntrl.setImage(image)
except Exception as e: self.log("setImages, failed! %s"%(e), xbmc.LOGERROR)
def setLabels(self, cntrl, label='', label2=''):
try:
if isinstance(cntrl, int): cntrl = self.getControl(cntrl)
cntrl.setLabel(str(label), str(label2))
self.setVisibility(cntrl,(len(label) > 0 or len(label2) > 0))
except Exception as e: self.log("setLabels, failed! %s"%(e), xbmc.LOGERROR)
def setEnableCondition(self, cntrl, condition):
try:
if isinstance(cntrl, int): cntrl = self.getControl(cntrl)
cntrl.setEnableCondition(condition)
except Exception as e: self.log("setEnableCondition, failed! %s"%(e), xbmc.LOGERROR)
def resetPagination(self, citem):
if isinstance(citem, list): [self.resetPagination(item) for item in citem]
else:
with self.toggleSpinner():
self.log('resetPagination, citem = %s'%(citem))
[self.jsonRPC.resetPagination(citem.get('id'), path) for path in citem.get('path',[]) if citem.get('id')]
def saveChannelItems(self, citem: dict={}, open=False):
self.log('saveChannelItems [%s], open = %s'%(citem.get('id'),open))
if self.madeItemchange:
self.madeChanges = True
self.newChannels[citem['number'] - 1] = citem
self.fillChanList(self.newChannels,True,(citem['number'] - 1),citem if open else None)
self.madeItemchange = False
return citem
def closeChannel(self, citem, focus=0, open=False):
self.log('closeChannel')
if self.madeItemchange:
if DIALOG.yesnoDialog(LANGUAGE(32243)): return self.saveChannelItems(citem, open)
self.togglechanList(focus=focus)
def saveChanges(self):
self.log("saveChanges")
def __validateChannels(channelList):
def _validate(citem):
if citem.get('name') and citem.get('path'):
if citem['number'] <= CHANNEL_LIMIT: citem['type'] = "Custom"
return self.setID(citem)
channelList = setDictLST(self.channels.sortChannels([_f for _f in [_validate(channel) for channel in channelList] if _f]))
self.log('__validateChannels, channelList = %s'%(len(channelList)))
return channelList
if self.madeChanges:
if DIALOG.yesnoDialog(LANGUAGE(32076)):
with self.toggleSpinner():
channels = __validateChannels(self.newChannels)
changes = diffLSTDICT(__validateChannels(self.channelList),channels)
added = [citem.get('id') for citem in changes.get('added',[])]
removed = [citem.get('id') for citem in changes.get('removed',[])]
ids = added+removed
citems = changes.get('added',[])+changes.get('removed',[])
self.log("saveChanges, channels = %s, added = %s, removed = %s"%(len(channels), len(added), len(removed)))
if self.server:
payload = {'uuid':SETTINGS.getMYUUID(),'name':self.friendly,'payload':channels}
requestURL('http://%s/%s'%(self.server.get('host'),CHANNELFLE), data=payload, header=HEADER, json_data=True)
else:
self.channels.setChannels(channels) #save changes
self.resetPagination(citems) #clear auto pagination cache
SETTINGS.setResetChannels(ids) #clear guidedata
PROPERTIES.setUpdateChannels(ids) #update channel meta.
self.madeChanges = False
self.closeManager()
def closeManager(self):
self.log('closeManager')
if self.madeChanges: self.saveChanges()
else: self.close()
def __exit__(self):
self.log('__exit__')
del self.resource
del self.jsonRPC
del self.rule
del self.channels
def getFocusItems(self, controlId=None):
focusItems = dict()
if controlId in [5,6,7,9001,9002,9003,9004]:
label, label2 = self.getLabels(controlId)
try: snum = int(cleanLabel(label.replace("|",'')))
except: snum = 1
if self.isVisible(self.chanList):
cntrl = controlId
sitem = self.chanList.getSelectedItem()
citem = loadJSON(sitem.getProperty('citem'))
chnum = (citem.get('number') or snum)
chpos = self.chanList.getSelectedPosition()
itpos = -1
elif self.isVisible(self.itemList):
cntrl = controlId
sitem = self.itemList.getSelectedItem()
citem = loadJSON(sitem.getProperty('citem'))
chnum = (citem.get('number') or snum)
chpos = chnum - 1
itpos = self.itemList.getSelectedPosition()
else:
sitem = xbmcgui.ListItem()
cntrl = (self.focusItems.get('cntrl') or controlId)
citem = (self.focusItems.get('citem') or {})
chnum = (self.focusItems.get('number') or snum)
chpos = (self.focusItems.get('chpos') or chnum - 1)
itpos = (self.focusItems.get('itpos') or -1)
self.focusItems.update({'retCntrl':cntrl,'label':label,'label2':label2,'number':chnum,'chpos':chpos,'itpos':itpos,'item':sitem,'citem':citem})
self.log('getFocusItems, controlId = %s, focusItems = %s'%(controlId,self.focusItems))
return self.focusItems
def onAction(self, act):
actionId = act.getId()
if (time.time() - self.lastActionTime) < .5 and actionId not in ACTION_PREVIOUS_MENU: action = ACTION_INVALID # during certain times we just want to discard all input
else:
if actionId in ACTION_PREVIOUS_MENU:
self.log('onAction: actionId = %s'%(actionId))
if xbmcgui.getCurrentWindowDialogId() == "13001": BUILTIN.executebuiltin("Action(Back)")
elif self.isVisible(self.itemList): self.closeChannel(self.getFocusItems().get('citem'),self.getFocusItems().get('position'))
elif self.isVisible(self.chanList): self.closeManager()
def onFocus(self, controlId):
self.log('onFocus: controlId = %s'%(controlId))
def onClick(self, controlId):
focusItems = self.getFocusItems(controlId)
focusID = focusItems.get('retCntrl')
focusLabel = focusItems.get('label')
focusNumber = focusItems.get('number',0)
focusCitem = focusItems.get('citem')
focusPOS = focusItems.get('chpos',0)
autoTuned = focusNumber > CHANNEL_LIMIT
self.log('onClick: controlId = %s\nitems = %s'%(controlId,focusItems))
if controlId == 0: self.closeManager()
elif controlId == 5: self.buildChannelItem(focusCitem) #item list
elif controlId == 6:
if self.lockAutotune and autoTuned: DIALOG.notificationDialog(LANGUAGE(32064))
else: self.buildChannelItem(self.itemInput(focusItems.get('item')),focusItems.get('item').getProperty('key'))
elif controlId == 10: #logo button
if self.lockAutotune and autoTuned: DIALOG.notificationDialog(LANGUAGE(32064))
else: self.switchLogo(focusCitem,focusPOS)
elif controlId in [9001,9002,9003,9004]: #side buttons
if focusLabel == LANGUAGE(32059): self.saveChanges() #Save
elif focusLabel == LANGUAGE(32061): self.clearChannel(focusCitem) #Delete
elif focusLabel == LANGUAGE(32239): self.clearChannel(focusCitem,open=True)#Clear
elif focusLabel == LANGUAGE(32136): self.moveChannel(focusCitem,focusPOS) #Move
elif focusLabel == LANGUAGE(32062): #Close
if self.isVisible(self.itemList): self.closeChannel(focusCitem,focus=focusPOS)
elif self.isVisible(self.chanList): self.closeManager()
elif focusLabel == LANGUAGE(32060): #Cancel
if self.isVisible(self.itemList): self.closeChannel(focusCitem)
elif self.isVisible(self.chanList): self.closeManager()
elif focusLabel == LANGUAGE(32240): #Confirm
if self.isVisible(self.itemList): self.saveChannelItems(focusCitem)
elif self.isVisible(self.chanList): self.saveChanges()
elif focusLabel == LANGUAGE(32235): #Preview
if self.isVisible(self.itemList) and self.madeItemchange: self.closeChannel(focusCitem, open=True)
self.previewChannel(focusCitem, focusID)