Updated kodi settings on Lenovo
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,204 @@
|
||||
# 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 library import Library
|
||||
from channels import Channels
|
||||
|
||||
class Autotune:
|
||||
def __init__(self, sysARG=sys.argv):
|
||||
self.log('__init__, sysARG = %s'%(sysARG))
|
||||
self.sysARG = sysARG
|
||||
self.channels = Channels()
|
||||
self.library = Library()
|
||||
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
return log('%s: %s'%(self.__class__.__name__,msg),level)
|
||||
|
||||
|
||||
def getCustom(self) -> dict:
|
||||
#return autotuned channels ie. channels > CHANNEL_LIMIT
|
||||
channels = self.channels.getCustom()
|
||||
self.log('getCustom, channels = %s'%(len(channels)))
|
||||
return channels
|
||||
|
||||
|
||||
def getAutotuned(self) -> dict:
|
||||
#return autotuned channels ie. channels > CHANNEL_LIMIT
|
||||
channels = self.channels.getAutotuned()
|
||||
self.log('getAutotuned, channels = %s'%(len(channels)))
|
||||
return channels
|
||||
|
||||
|
||||
def _runTune(self, prompt: bool=False, rebuild: bool=False, dia=None):
|
||||
customChannels = self.getCustom()
|
||||
autoChannels = self.getAutotuned()
|
||||
hasLibrary = PROPERTIES.hasLibrary()
|
||||
if len(autoChannels) > 0 or hasLibrary: rebuild = PROPERTIES.setEXTPropertyBool('%s.has.Predefined'%(ADDON_ID),True) #rebuild existing autotune, no prompt needed, refresh paths and logos
|
||||
if len(customChannels) == 0: prompt = True #begin check if prompt or recovery is needed
|
||||
self.log('_runTune, customChannels = %s, autoChannels = %s'%(len(customChannels),len(autoChannels)))
|
||||
|
||||
if prompt:
|
||||
opt = ''
|
||||
msg = '%s?'%(LANGUAGE(32042)%(ADDON_NAME))
|
||||
hasBackup = PROPERTIES.hasBackup()
|
||||
hasServers = PROPERTIES.hasServers()
|
||||
hasM3U = FileAccess.exists(M3UFLEPATH) if not hasLibrary else False
|
||||
|
||||
if (hasBackup or hasServers or hasM3U):
|
||||
opt = LANGUAGE(32254)
|
||||
msg = '%s\n%s'%(LANGUAGE(32042)%(ADDON_NAME),LANGUAGE(32255))
|
||||
|
||||
retval = DIALOG.yesnoDialog(message=msg,customlabel=opt)
|
||||
if retval == 1: dia = DIALOG.progressBGDialog(header='%s, %s'%(ADDON_NAME,LANGUAGE(32021))) #Yes
|
||||
elif retval == 2: #Custom
|
||||
with BUILTIN.busy_dialog(), PROPERTIES.interruptActivity():
|
||||
menu = [LISTITEMS.buildMenuListItem(LANGUAGE(30107),LANGUAGE(33310),url='special://home/addons/%s/resources/lib/utilities.py, Channel_Manager'%(ADDON_ID))]
|
||||
if hasM3U: menu.append(LISTITEMS.buildMenuListItem(LANGUAGE(32257),LANGUAGE(32256),url='special://home/addons/%s/resources/lib/autotune.py, Recover_M3U'%(ADDON_ID)))
|
||||
if hasBackup: menu.append(LISTITEMS.buildMenuListItem(LANGUAGE(32112),LANGUAGE(32111),url='special://home/addons/%s/resources/lib/backup.py, Recover_Backup'%(ADDON_ID)))
|
||||
if hasServers: menu.append(LISTITEMS.buildMenuListItem(LANGUAGE(30173),LANGUAGE(32215),url='special://home/addons/%s/resources/lib/multiroom.py, Select_Server'%(ADDON_ID)))
|
||||
select = DIALOG.selectDialog(menu,multi=False)
|
||||
if not select is None: return BUILTIN.executescript(menu[select].getPath())
|
||||
else: return True #No
|
||||
else: return True
|
||||
|
||||
for idx, ATtype in enumerate(AUTOTUNE_TYPES):
|
||||
if dia: dia = DIALOG.progressBGDialog(int((idx+1)*100//len(AUTOTUNE_TYPES)),dia,ATtype,'%s, %s'%(ADDON_NAME,LANGUAGE(32021)))
|
||||
self.selectAUTOTUNE(ATtype, autoSelect=prompt, rebuildChannels=rebuild)
|
||||
return True
|
||||
|
||||
|
||||
def selectAUTOTUNE(self, ATtype: str, autoSelect: bool=False, rebuildChannels: bool=False):
|
||||
self.log('selectAUTOTUNE, ATtype = %s, autoSelect = %s, rebuildChannels = %s'%(ATtype,autoSelect,rebuildChannels))
|
||||
def __buildMenuItem(item):
|
||||
return LISTITEMS.buildMenuListItem(item['name'],item['type'],item['logo'])
|
||||
|
||||
def _match(enabledItems):
|
||||
for item in enabledItems:
|
||||
for idx, liz in enumerate(lizlst):
|
||||
if item.get('name','').lower() == liz.getLabel().lower():
|
||||
yield idx
|
||||
|
||||
def _set(ATtype, selects=[]):
|
||||
for item in items:
|
||||
item['enabled'] = False #disable everything before selecting new items.
|
||||
for select in selects:
|
||||
if item.get('name','').lower() == lizlst[select].getLabel().lower():
|
||||
item['enabled'] = True
|
||||
self.library.setLibrary(ATtype, items)
|
||||
|
||||
items = self.library.getLibrary(ATtype)
|
||||
if len(items) == 0 and (not rebuildChannels and not autoSelect):
|
||||
if SETTINGS.getSettingBool('Debug_Enable'): DIALOG.notificationDialog(LANGUAGE(32018)%(ATtype))
|
||||
return
|
||||
|
||||
lizlst = poolit(__buildMenuItem)(items)
|
||||
if rebuildChannels:#rebuild channels.json entries
|
||||
selects = list(_match(self.library.getEnabled(ATtype)))
|
||||
elif autoSelect:#build sample channels
|
||||
if len(items) >= AUTOTUNE_LIMIT:
|
||||
selects = sorted(set(random.sample(list(set(range(0,len(items)))),AUTOTUNE_LIMIT)))
|
||||
else:
|
||||
selects = list(range(0,len(items)))
|
||||
else:
|
||||
selects = DIALOG.selectDialog(lizlst,LANGUAGE(32017)%(ATtype),preselect=list(_match(self.library.getEnabled(ATtype))))
|
||||
|
||||
if not selects is None: _set(ATtype, selects)
|
||||
return self.buildAUTOTUNE(ATtype, self.library.getEnabled(ATtype))
|
||||
|
||||
|
||||
def buildAUTOTUNE(self, ATtype: str, items: list=[]):
|
||||
if not list: return
|
||||
def buildAvailableRange(existing):
|
||||
# create number array for given type, excluding existing channel numbers.
|
||||
if existing: existingNUMBERS = [eitem.get('number') for eitem in existing if eitem.get('number',0) > 0] # existing channel numbers
|
||||
else: existingNUMBERS = []
|
||||
|
||||
start = ((CHANNEL_LIMIT+1)*(AUTOTUNE_TYPES.index(ATtype)+1))
|
||||
stop = (start + CHANNEL_LIMIT)
|
||||
self.log('buildAUTOTUNE, ATtype = %s, range = %s-%s, existingNUMBERS = %s'%(ATtype,start,stop,existingNUMBERS))
|
||||
return [num for num in range(start,stop) if num not in existingNUMBERS]
|
||||
|
||||
existingAUTOTUNE = self.channels.popChannels(ATtype,self.getAutotuned())
|
||||
usesableNUMBERS = iter(buildAvailableRange(existingAUTOTUNE)) # available channel numbers
|
||||
for item in items:
|
||||
music = isRadio(item)
|
||||
citem = self.channels.getTemplate()
|
||||
citem.update({"id" : "",
|
||||
"type" : ATtype,
|
||||
"number" : 0,
|
||||
"name" : getChannelSuffix(item['name'], ATtype),
|
||||
"logo" : item.get('logo',LOGO),
|
||||
"path" : item.get('path',''),
|
||||
"group" : [item.get('type','')],
|
||||
"rules" : item.get('rules',{}),
|
||||
"catchup" : ('vod' if not music else ''),
|
||||
"radio" : music,
|
||||
"favorite": True})
|
||||
|
||||
match, eitem = self.channels.findAutotuned(citem, channels=existingAUTOTUNE)
|
||||
if match is None: #new autotune
|
||||
citem['id'] = getChannelID(citem['name'],citem['path'],citem['number']) #generate new channelid
|
||||
citem['number'] = next(usesableNUMBERS,0) #first available channel number
|
||||
PROPERTIES.setUpdateChannels(citem['id'])
|
||||
else: #update existing autotune
|
||||
citem['id'] = eitem.get('id')
|
||||
citem['number'] = eitem.get('number')
|
||||
citem['logo'] = chkLogo(eitem.get('logo',''),citem.get('logo',LOGO))
|
||||
citem['favorite'] = eitem.get('favorite',False)
|
||||
self.log('[%s] buildAUTOTUNE, number = %s, match = %s'%(citem['id'],citem['number'],match))
|
||||
self.channels.addChannel(citem)
|
||||
return self.channels.setChannels()
|
||||
|
||||
|
||||
def recoverM3U(self, autotune={}):
|
||||
from m3u import M3U
|
||||
stations = M3U().getStations()
|
||||
[autotune.setdefault(AUTOTUNE_TYPES[station.get('number')//1000],[]).append(station.get('name')) for station in stations if station.get('number') > CHANNEL_LIMIT]
|
||||
[self.library.enableByName(type, names) for type, names in list(autotune.items()) if len(names) > 0]
|
||||
return BUILTIN.executescript('special://home/addons/%s/resources/lib/utilities.py, Run_Autotune'%(ADDON_ID))
|
||||
|
||||
|
||||
def clearLibrary(self):
|
||||
self.library.resetLibrary()
|
||||
DIALOG.notificationDialog(LANGUAGE(32025))
|
||||
|
||||
|
||||
def clearBlacklist(self):
|
||||
SETTINGS.setSetting('Clear_BlackList','')
|
||||
DIALOG.notificationDialog(LANGUAGE(32025))
|
||||
|
||||
|
||||
def run(self):
|
||||
with BUILTIN.busy_dialog():
|
||||
ctl = (1,1) #settings return focus
|
||||
try: param = self.sysARG[1]
|
||||
except: param = None
|
||||
if param.replace('_',' ') in AUTOTUNE_TYPES:
|
||||
ctl = (1,AUTOTUNE_TYPES.index(param.replace('_',' '))+1)
|
||||
self.selectAUTOTUNE(param.replace('_',' '))
|
||||
elif param == 'Clear_Autotune' : self.clearLibrary()
|
||||
elif param == 'Clear_BlackList': self.clearBlacklist()
|
||||
elif param == 'Recover_M3U': self.recoverM3U()
|
||||
elif param == None: return
|
||||
return SETTINGS.openSettings(ctl)
|
||||
|
||||
if __name__ == '__main__': timerit(Autotune(sys.argv).run)(0.1)
|
||||
@@ -0,0 +1,98 @@
|
||||
# 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/>.
|
||||
# https://github.com/kodi-community-addons/script.module.simplecache/blob/master/README.md
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from globals import *
|
||||
from library import Library
|
||||
from channels import Channels
|
||||
|
||||
class Backup:
|
||||
def __init__(self, sysARG=sys.argv):
|
||||
self.sysARG = sysARG
|
||||
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
return log('%s: %s'%(self.__class__.__name__,msg),level)
|
||||
|
||||
|
||||
def getFileDate(self, file: str) -> str:
|
||||
try: return datetime.datetime.fromtimestamp(pathlib.Path(FileAccess.translatePath(file)).stat().st_mtime).strftime(BACKUP_TIME_FORMAT)
|
||||
except: return LANGUAGE(32105) #Unknown
|
||||
|
||||
|
||||
def hasBackup(self, file: str=CHANNELFLE_BACKUP) -> bool:
|
||||
self.log('hasBackup')
|
||||
if PROPERTIES.setBackup(FileAccess.exists(file)):
|
||||
if file == CHANNELFLE_BACKUP:#main backup file, set meta.
|
||||
if (SETTINGS.getSetting('Backup_Channels') or 'Last Backup: Unknown') == 'Last Backup: Unknown':
|
||||
SETTINGS.setSetting('Backup_Channels' ,'%s: %s'%(LANGUAGE(32106),self.getFileDate(file)))
|
||||
if not SETTINGS.getSetting('Recover_Backup'):
|
||||
SETTINGS.setSetting('Recover_Backup','%s [B]%s[/B] Channels?'%(LANGUAGE(32107),len(self.getChannels())))
|
||||
return True
|
||||
SETTINGS.setSetting('Backup_Channels' ,'')
|
||||
SETTINGS.setSetting('Recover_Backup','')
|
||||
return False
|
||||
|
||||
|
||||
def getChannels(self, file: str=CHANNELFLE_BACKUP) -> list:
|
||||
self.log('getChannels')
|
||||
channels = Channels()
|
||||
citems = channels._load(file).get('channels',[])
|
||||
del channels
|
||||
return citems
|
||||
|
||||
|
||||
def backupChannels(self, file: str=CHANNELFLE_BACKUP) -> bool:
|
||||
self.log('backupChannels')
|
||||
if FileAccess.exists(file):
|
||||
if not DIALOG.yesnoDialog('%s\n%s?'%(LANGUAGE(32108),SETTINGS.getSetting('Backup_Channels'))):
|
||||
return False
|
||||
|
||||
with BUILTIN.busy_dialog(), PROPERTIES.interruptActivity():
|
||||
if FileAccess.copy(CHANNELFLEPATH,file):
|
||||
if file == CHANNELFLE_BACKUP: #main backup file, set meta.
|
||||
PROPERTIES.setBackup(True)
|
||||
SETTINGS.setSetting('Backup_Channels' ,'%s: %s'%(LANGUAGE(32106),datetime.datetime.now().strftime(BACKUP_TIME_FORMAT)))
|
||||
SETTINGS.setSetting('Recover_Backup','%s [B]%s[/B] Channels?'%(LANGUAGE(32107),len(self.getChannels())))
|
||||
return DIALOG.notificationDialog('%s %s'%(LANGUAGE(32110),LANGUAGE(32025)))
|
||||
self.hasBackup()
|
||||
SETTINGS.openSettings(ctl)
|
||||
|
||||
|
||||
def recoverChannels(self, file: str=CHANNELFLE_BACKUP) -> bool:
|
||||
self.log('recoverChannels, file = %s'%(file))
|
||||
if not DIALOG.yesnoDialog('%s'%(LANGUAGE(32109)%(SETTINGS.getSetting('Recover_Backup').replace(LANGUAGE(30216),''),SETTINGS.getSetting('Backup_Channels')))):
|
||||
return False
|
||||
|
||||
with BUILTIN.busy_dialog(), PROPERTIES.interruptActivity():
|
||||
FileAccess.move(CHANNELFLEPATH,CHANNELFLE_RESTORE)
|
||||
if FileAccess.copy(file,CHANNELFLEPATH):
|
||||
Library().resetLibrary()
|
||||
PROPERTIES.setPendingRestart()
|
||||
|
||||
|
||||
def run(self):
|
||||
with BUILTIN.busy_dialog():
|
||||
ctl = (0,1) #settings return focus
|
||||
try: param = self.sysARG[1]
|
||||
except: param = None
|
||||
if param == 'Recover_Backup': self.recoverChannels()
|
||||
elif param == 'Backup_Channels': self.backupChannels()
|
||||
|
||||
if __name__ == '__main__': timerit(Backup(sys.argv).run)(0.1)
|
||||
@@ -0,0 +1,590 @@
|
||||
# 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 channels import Channels
|
||||
from xmltvs import XMLTVS
|
||||
from xsp import XSP
|
||||
from m3u import M3U
|
||||
from fillers import Fillers
|
||||
from resources import Resources
|
||||
from seasonal import Seasonal
|
||||
from rules import RulesList
|
||||
|
||||
class Service:
|
||||
from jsonrpc import JSONRPC
|
||||
player = PLAYER()
|
||||
monitor = MONITOR()
|
||||
jsonRPC = JSONRPC()
|
||||
def _interrupt(self) -> bool:
|
||||
return PROPERTIES.isPendingInterrupt()
|
||||
def _suspend(self) -> bool:
|
||||
return PROPERTIES.isPendingSuspend()
|
||||
|
||||
|
||||
class Builder:
|
||||
loopback = {}
|
||||
|
||||
def __init__(self, service=None):
|
||||
if service is None: service = Service()
|
||||
self.service = service
|
||||
self.jsonRPC = service.jsonRPC
|
||||
self.cache = service.jsonRPC.cache
|
||||
self.channels = Channels()
|
||||
|
||||
#global dialog
|
||||
self.pDialog = None
|
||||
self.pCount = 0
|
||||
self.pMSG = ''
|
||||
self.pName = ''
|
||||
self.pErrors = []
|
||||
|
||||
#global rules
|
||||
self.accurateDuration = bool(SETTINGS.getSettingInt('Duration_Type'))
|
||||
self.enableEven = bool(SETTINGS.getSettingInt('Enable_Even'))
|
||||
self.interleaveValue = SETTINGS.getSettingInt('Interleave_Value')
|
||||
self.incStrms = SETTINGS.getSettingBool('Enable_Strms')
|
||||
self.inc3D = SETTINGS.getSettingBool('Enable_3D')
|
||||
self.incExtras = SETTINGS.getSettingBool('Enable_Extras')
|
||||
self.fillBCTs = SETTINGS.getSettingBool('Enable_Fillers')
|
||||
self.saveDuration = SETTINGS.getSettingBool('Store_Duration')
|
||||
self.epgArt = SETTINGS.getSettingInt('EPG_Artwork')
|
||||
self.enableGrouping = SETTINGS.getSettingBool('Enable_Grouping')
|
||||
self.minDuration = SETTINGS.getSettingInt('Seek_Tolerance')
|
||||
self.limit = SETTINGS.getSettingInt('Page_Limit')
|
||||
self.filelistQuota = False
|
||||
self.schedulingQuota = True
|
||||
|
||||
self.filters = {}#{"and": [{"operator": "contains", "field": "title", "value": "Star Wars"},{"operator": "contains", "field": "tag", "value": "Good"}],"or":[]}
|
||||
self.sort = {}#{"ignorearticle":True,"method":"random","order":"ascending","useartistsortname":True}
|
||||
self.limits = {"end":-1,"start":0,"total":0}
|
||||
|
||||
self.bctTypes = {"ratings" :{"min":-1, "max":SETTINGS.getSettingInt('Enable_Preroll'), "auto":SETTINGS.getSettingInt('Enable_Preroll') == -1, "enabled":bool(SETTINGS.getSettingInt('Enable_Preroll')), "chance":SETTINGS.getSettingInt('Random_Pre_Chance'),
|
||||
"sources" :{"ids":SETTINGS.getSetting('Resource_Ratings').split('|'),"paths":[os.path.join(FILLER_LOC,'Ratings' ,'')]},"items":{}},
|
||||
|
||||
"bumpers" :{"min":-1, "max":SETTINGS.getSettingInt('Enable_Preroll'), "auto":SETTINGS.getSettingInt('Enable_Preroll') == -1, "enabled":bool(SETTINGS.getSettingInt('Enable_Preroll')), "chance":SETTINGS.getSettingInt('Random_Pre_Chance'),
|
||||
"sources" :{"ids":SETTINGS.getSetting('Resource_Bumpers').split('|'),"paths":[os.path.join(FILLER_LOC,'Bumpers' ,'')]},"items":{}},
|
||||
|
||||
"adverts" :{"min":SETTINGS.getSettingInt('Enable_Postroll'), "max":PAGE_LIMIT, "auto":SETTINGS.getSettingInt('Enable_Postroll') == -1, "enabled":bool(SETTINGS.getSettingInt('Enable_Postroll')), "chance":SETTINGS.getSettingInt('Random_Post_Chance'),
|
||||
"sources" :{"ids":SETTINGS.getSetting('Resource_Adverts').split('|'),"paths":[os.path.join(FILLER_LOC,'Adverts' ,'')]},"items":{}},
|
||||
|
||||
"trailers":{"min":SETTINGS.getSettingInt('Enable_Postroll'), "max":PAGE_LIMIT, "auto":SETTINGS.getSettingInt('Enable_Postroll') == -1, "enabled":bool(SETTINGS.getSettingInt('Enable_Postroll')), "chance":SETTINGS.getSettingInt('Random_Post_Chance'),
|
||||
"sources" :{"ids":SETTINGS.getSetting('Resource_Trailers').split('|'),"paths":[os.path.join(FILLER_LOC,'Trailers','')]},"items":{}, "incKODI":SETTINGS.getSettingBool('Include_Trailers_KODI')}}
|
||||
|
||||
self.xsp = XSP()
|
||||
self.xmltv = XMLTVS()
|
||||
self.m3u = M3U()
|
||||
self.resources = Resources(service=self.service)
|
||||
self.runActions = RulesList(self.channels.getChannels()).runActions
|
||||
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
return log('%s: %s'%(self.__class__.__name__,msg),level)
|
||||
|
||||
|
||||
def updateProgress(self, percent, message, header):
|
||||
if self.pDialog: self.pDialog = DIALOG.updateProgress(percent, self.pDialog, message=message, header=header)
|
||||
|
||||
|
||||
def getVerifiedChannels(self):
|
||||
return sorted(self.verify(), key=itemgetter('number'))
|
||||
|
||||
|
||||
def verify(self, channels=[]):
|
||||
if not channels: channels = self.channels.getChannels()
|
||||
for idx, citem in enumerate(channels):
|
||||
if not citem.get('name') or len(citem.get('path',[])) == 0 or not citem.get('number'):
|
||||
self.log('[%s] SKIPPING - missing necessary channel meta\n%s'%(citem.get('id'),citem))
|
||||
continue
|
||||
elif not citem.get('id'): citem['id'] = getChannelID(citem['name'],citem['path'],citem['number']) #generate new channelid
|
||||
citem['logo'] = self.resources.getLogo(citem,citem.get('logo',LOGO))
|
||||
self.log('[%s] VERIFIED - channel %s: %s'%(citem['id'],citem['number'],citem['name']))
|
||||
yield self.runActions(RULES_ACTION_CHANNEL_CITEM, citem, citem, inherited=self) #inject persistent citem changes here
|
||||
|
||||
|
||||
def build(self, channels: list=[], preview=False):
|
||||
def __hasProgrammes(citem: dict) -> bool:
|
||||
try: return dict(self.xmltv.hasProgrammes([citem])).get(citem['id'],False)
|
||||
except: return False
|
||||
|
||||
def __hasFileList(fileList: list) -> bool:
|
||||
if isinstance(fileList,list):
|
||||
if len(fileList) > 0: return True
|
||||
return False
|
||||
|
||||
def __clrChannel(citem: dict) -> bool:
|
||||
self.log('[%s] __clrChannel'%(citem['id']))
|
||||
return self.m3u.delStation(citem) & self.xmltv.delBroadcast(citem)
|
||||
|
||||
def __addStation(citem: dict) -> bool:
|
||||
self.log('[%s] __addStation'%(citem['id']))
|
||||
citem['logo'] = self.resources.buildWebImage(cleanImage(citem['logo']))
|
||||
citem['group'] = cleanGroups(citem, self.enableGrouping)
|
||||
sitem = self.m3u.getStationItem(citem)
|
||||
return self.m3u.addStation(sitem) & self.xmltv.addChannel(sitem)
|
||||
|
||||
def __addProgrammes(citem: dict, fileList: list) -> bool:
|
||||
self.log('[%s] __addProgrammes, fileList = %s'%(citem['id'],len(fileList)))
|
||||
for idx, item in enumerate(fileList): self.xmltv.addProgram(citem['id'], self.xmltv.getProgramItem(citem, item))
|
||||
return True
|
||||
|
||||
def __setChannels():
|
||||
self.log('__setChannels')
|
||||
return self.xmltv._save() & self.m3u._save()
|
||||
|
||||
if not PROPERTIES.isRunning('builder.build'):
|
||||
with PROPERTIES.legacy(), PROPERTIES.chkRunning('builder.build'):
|
||||
try:
|
||||
if len(channels) == 0: raise Exception('No individual channels to update, updating all!')
|
||||
else: channels = sorted(self.verify(channels), key=itemgetter('number'))
|
||||
except: channels = self.getVerifiedChannels()
|
||||
|
||||
if len(channels) > 0:
|
||||
complete = True
|
||||
updated = set()
|
||||
now = getUTCstamp()
|
||||
start = roundTimeDown(now,offset=60)#offset time to start bottom of the hour
|
||||
fallback = datetime.datetime.fromtimestamp(start).strftime(DTFORMAT)
|
||||
clrIDS = SETTINGS.getResetChannels()
|
||||
|
||||
if preview: self.pDialog = DIALOG.progressDialog()
|
||||
else: self.pDialog = DIALOG.progressBGDialog()
|
||||
|
||||
for idx, citem in enumerate(channels):
|
||||
self.pCount = int(idx*100//len(channels))
|
||||
|
||||
citem = self.runActions(RULES_ACTION_CHANNEL_TEMP_CITEM, citem, citem, inherited=self) #inject temporary citem changes here
|
||||
self.log('[%s] build, preview = %s, rules = %s'%(citem['id'],preview,citem.get('rules',{})))
|
||||
if self.service._interrupt():
|
||||
self.log("[%s] build, _interrupt"%(citem['id']))
|
||||
complete = False
|
||||
self.pErrors = [LANGUAGE(32160)]
|
||||
self.updateProgress(self.pCount, message='%s: %s'%(LANGUAGE(32144),LANGUAGE(32213)), header=ADDON_NAME)
|
||||
break
|
||||
else:
|
||||
self.pMSG = '%s: %s'%(LANGUAGE(32144),LANGUAGE(32212))
|
||||
self.pName = citem['name']
|
||||
self.runActions(RULES_ACTION_CHANNEL_START, citem, inherited=self)
|
||||
|
||||
if not preview and citem['id'] in clrIDS: __clrChannel({'id':clrIDS.pop(clrIDS.index(citem['id']))}) #clear channel xmltv
|
||||
stopTimes = dict(self.xmltv.loadStopTimes([citem], fallback=fallback)) #check last stop times
|
||||
|
||||
if preview: self.pMSG = LANGUAGE(32236) #Preview
|
||||
elif (stopTimes.get(citem['id']) or start) > (now + ((MAX_GUIDEDAYS * 86400) - 43200)): self.pMSG = '%s %s'%(LANGUAGE(32028),LANGUAGE(32023)) #Checking
|
||||
elif (stopTimes.get(citem['id']) or fallback) == fallback: self.pMSG = '%s %s'%(LANGUAGE(30014),LANGUAGE(32023)) #Building
|
||||
elif stopTimes.get(citem['id']): self.pMSG = '%s %s'%(LANGUAGE(32022),LANGUAGE(32023)) #Updating
|
||||
else: self.pMSG = '%s %s'%(LANGUAGE(32245),LANGUAGE(32023)) #Parsing
|
||||
|
||||
self.updateProgress(self.pCount, message='%s: %s'%(LANGUAGE(32248),self.pName), header='%s, %s'%(ADDON_NAME,self.pMSG))
|
||||
response = self.getFileList(citem, now, (stopTimes.get(citem['id']) or start))# {False:'In-Valid Channel', True:'Valid Channel w/o programmes', list:'Valid Channel w/ programmes}
|
||||
if preview: return response
|
||||
elif response:
|
||||
if __addStation(citem) and __hasFileList(response): updated.add(__addProgrammes(citem, response)) #added xmltv lineup entries.
|
||||
else:
|
||||
if complete: self.pErrors.append(LANGUAGE(32026))
|
||||
chanErrors = ' | '.join(list(sorted(set(self.pErrors))))
|
||||
self.log('[%s] build, In-Valid Channel (%s) %s'%(citem['id'],self.pName,chanErrors))
|
||||
self.updateProgress(self.pCount, message='%s: %s'%(self.pName,chanErrors),header='%s, %s'%(ADDON_NAME,'%s %s'%(LANGUAGE(32027),LANGUAGE(32023))))
|
||||
if not __hasProgrammes(citem):
|
||||
self.updateProgress(self.pCount, message=self.pName,header='%s, %s'%(ADDON_NAME,'%s %s'%(LANGUAGE(32244),LANGUAGE(32023))))
|
||||
__clrChannel(citem) #remove m3u/xmltv references when no valid programmes found. # todo del citem causes issues down the road with citem missing params. reeval need to remove here
|
||||
self.runActions(RULES_ACTION_CHANNEL_STOP, citem, inherited=self)
|
||||
|
||||
SETTINGS.setResetChannels(clrIDS)
|
||||
self.pDialog = DIALOG.updateProgress(100, self.pDialog, message='%s %s'%(self.pMSG,LANGUAGE(32025) if complete else LANGUAGE(32135)))
|
||||
self.log('build, complete = %s, updated = %s, saved = %s'%(complete,bool(updated),__setChannels()))
|
||||
return complete, bool(updated)
|
||||
else: self.log('build, no verified channels found!')
|
||||
return False, False
|
||||
|
||||
|
||||
def getFileList(self, citem: dict, now: time, start: time) -> bool and list:
|
||||
self.log('[%s] getFileList, start = %s'%(citem['id'],start))
|
||||
try:
|
||||
if start > (now + ((MAX_GUIDEDAYS * 86400) - 43200)): #max guidedata days to seconds, minus fill buffer (12hrs) in seconds.
|
||||
self.updateProgress(self.pCount, message=self.pName, header='%s, %s'%(ADDON_NAME,self.pMSG))
|
||||
self.log('[%s] getFileList, programmes over MAX_DAYS! start = %s'%(citem['id'],datetime.datetime.fromtimestamp(start)),xbmc.LOGINFO)
|
||||
return True# prevent over-building
|
||||
|
||||
multi = len(citem.get('path',[])) > 1 #multi-path source
|
||||
radio = True if citem.get('radio',False) else False
|
||||
media = 'music' if radio else 'video'
|
||||
self.log('[%s] getFileList, multipath = %s, radio = %s, media = %s, path = %s'%(citem['id'],multi,radio,media,citem.get('path')),xbmc.LOGINFO)
|
||||
|
||||
if radio: response = self.buildRadio(citem)
|
||||
else: response = self.buildChannel(citem)
|
||||
|
||||
if isinstance(response,list): return sorted(self.addScheduling(citem, response, now, start), key=itemgetter('start'))
|
||||
elif self.service._interrupt():
|
||||
self.log("[%s] getFileList, _interrupt"%(citem['id']))
|
||||
self.updateProgress(self.pCount, message='%s: %s'%(LANGUAGE(32144),LANGUAGE(32213)), header=ADDON_NAME)
|
||||
return True
|
||||
else:
|
||||
return response
|
||||
except Exception as e: self.log("[%s] getFileList, failed! %s"%(citem['id'],e), xbmc.LOGERROR)
|
||||
return False
|
||||
|
||||
|
||||
def buildCells(self, citem: dict={}, duration: int=10800, type: str='video', entries: int=3, info: dict={}) -> list:
|
||||
tmpItem = {'label' : (info.get('title') or citem['name']),
|
||||
'episodetitle': (info.get('episodetitle') or '|'.join(citem['group'])),
|
||||
'plot' : (info.get('plot') or LANGUAGE(32020)),
|
||||
'genre' : (info.get('genre') or ['Undefined']),
|
||||
'file' : (info.get('path') or info.get('file') or info.get('originalpath') or '|'.join(citem.get('path'))),
|
||||
'art' : (info.get('art') or {"thumb":COLOR_LOGO,"fanart":FANART,"logo":LOGO,"icon":LOGO}),
|
||||
'type' : type,
|
||||
'duration' : duration,
|
||||
'start' : 0,
|
||||
'stop' : 0}
|
||||
info.update(tmpItem)
|
||||
return [info.copy() for idx in range(entries)]
|
||||
|
||||
|
||||
def addScheduling(self, citem: dict, fileList: list, now: time, start: time) -> list: #quota meet MIN_EPG_DURATION requirements.
|
||||
self.log("[%s] addScheduling, IN fileList = %s, now = %s, start = %s"%(citem['id'],len(fileList),now,start))
|
||||
totDur = 0
|
||||
tmpList = []
|
||||
fileList = self.runActions(RULES_ACTION_CHANNEL_BUILD_TIME_PRE, citem, fileList, inherited=self)
|
||||
for idx, item in enumerate(fileList):
|
||||
item["idx"] = idx
|
||||
item['start'] = start
|
||||
item['stop'] = start + item['duration']
|
||||
start = item['stop']
|
||||
tmpList.append(item)
|
||||
|
||||
if len(tmpList) > 0:
|
||||
iters = cycle(fileList)
|
||||
while not self.service.monitor.abortRequested() and tmpList[-1].get('stop') <= (now + MIN_EPG_DURATION):
|
||||
if self.service.monitor.waitForAbort(0.0001): break
|
||||
elif tmpList[-1].get('stop') >= (now + MIN_EPG_DURATION):
|
||||
self.log("[%s] addScheduling, OUT fileList = %s, stop = %s"%(citem['id'],len(tmpList),tmpList[-1].get('stop')))
|
||||
break
|
||||
else:
|
||||
idx += 1
|
||||
item = next(iters).copy()
|
||||
item["idx"] = idx
|
||||
item['start'] = start
|
||||
item['stop'] = start + item['duration']
|
||||
start = item['stop']
|
||||
totDur += item['duration']
|
||||
tmpList.append(item)
|
||||
self.updateProgress(self.pCount, message="%s: %s %s/%s"%(self.pName,LANGUAGE(33085),totDur,MIN_EPG_DURATION),header='%s, %s'%(ADDON_NAME,self.pMSG))
|
||||
self.log("[%s] addScheduling, ADD fileList = %s, totDur = %s/%s, stop = %s"%(citem['id'],len(tmpList),totDur,MIN_EPG_DURATION,tmpList[-1].get('stop')))
|
||||
return self.runActions(RULES_ACTION_CHANNEL_BUILD_TIME_POST, citem, tmpList, inherited=self) #adv. scheduling second pass and cleanup.
|
||||
|
||||
|
||||
def buildRadio(self, citem: dict) -> list:
|
||||
self.log("[%s] buildRadio"%(citem['id']))
|
||||
#todo insert custom radio labels,plots based on genre type?
|
||||
# https://www.musicgenreslist.com/
|
||||
# https://www.musicgateway.com/blog/how-to/what-are-the-different-genres-of-music
|
||||
return self.buildCells(citem, MIN_EPG_DURATION, 'music', ((MAX_GUIDEDAYS * 8)), info={'genre':["Music"],'art':{'thumb':citem['logo'],'icon':citem['logo'],'fanart':citem['logo']},'plot':LANGUAGE(32029)%(citem['name'])})
|
||||
|
||||
|
||||
def buildChannel(self, citem: dict) -> bool and list:
|
||||
def _validFileList(fileArray):
|
||||
for fileList in fileArray:
|
||||
if len(fileList) > 0: return True
|
||||
|
||||
def _injectFillers(citem, fileList, enable=False):
|
||||
self.log("[%s] buildChannel: _injectFillers, fileList = %s, enable = %s"%(citem['id'],len(fileList),enable))
|
||||
if enable: return Fillers(self,citem).injectBCTs(fileList)
|
||||
else: return fileList
|
||||
|
||||
def _injectRules(citem):
|
||||
def __chkEvenDistro(citem):
|
||||
if self.enableEven and not citem.get('rules',{}).get("1000"):
|
||||
nrules = {"1000":{"values":{"0":SETTINGS.getSettingInt('Enable_Even'),"1":SETTINGS.getSettingInt('Page_Limit'),"2":SETTINGS.getSettingBool('Enable_Force_Episode')}}}
|
||||
self.log(" [%s] buildChannel: _injectRules, __chkEvenDistro, new rules = %s"%(citem['id'],nrules))
|
||||
citem.setdefault('rules',{}).update(nrules)
|
||||
return citem
|
||||
return __chkEvenDistro(citem)
|
||||
|
||||
citem = _injectRules(citem) #inject temporary adv. channel rules here
|
||||
fileArray = self.runActions(RULES_ACTION_CHANNEL_BUILD_FILEARRAY_PRE, citem, list(), inherited=self) #inject fileArray thru adv. channel rules here
|
||||
self.log("[%s] buildChannel, channel pre fileArray items = %s"%(citem['id'],len(fileArray)),xbmc.LOGINFO)
|
||||
|
||||
#Primary rule for handling fileList injection bypassing channel building below.
|
||||
if not _validFileList(fileArray): #if valid array bypass channel building
|
||||
for idx, file in enumerate(citem.get('path',[])):
|
||||
if self.service._interrupt():
|
||||
self.log("[%s] buildChannel, _interrupt"%(citem['id']))
|
||||
self.updateProgress(self.pCount, message='%s: %s'%(LANGUAGE(32144),LANGUAGE(32213)), header=ADDON_NAME)
|
||||
return []
|
||||
else:
|
||||
if len(citem.get('path',[])) > 1: self.pName = '%s %s/%s'%(citem['name'],idx+1,len(citem.get('path',[])))
|
||||
fileList = self.buildFileList(citem, self.runActions(RULES_ACTION_CHANNEL_BUILD_PATH, citem, file, inherited=self), 'video', self.limit, self.sort, self.limits)
|
||||
fileArray.append(fileList)
|
||||
self.log("[%s] buildChannel, path = %s, fileList = %s"%(citem['id'],file,len(fileList)))
|
||||
fileArray = self.runActions(RULES_ACTION_CHANNEL_BUILD_FILEARRAY_POST, citem, fileArray, inherited=self) #flatten fileArray here to pass as fileList below
|
||||
|
||||
#Primary rule for handling adv. interleaving, must return single list to avoid default interleave() below. Add adv. rule to setDictLST duplicates
|
||||
if isinstance(fileArray, list):
|
||||
self.log("[%s] buildChannel, channel post fileArray items = %s"%(citem['id'],len(fileArray)),xbmc.LOGINFO)
|
||||
if not _validFileList(fileArray):#check that at least one fileList in array contains meta
|
||||
self.log("[%s] buildChannel, channel fileArray In-Valid!"%(citem['id']),xbmc.LOGINFO)
|
||||
return False
|
||||
self.log("[%s] buildChannel, fileArray = %s"%(citem['id'],','.join(['[%s]'%(len(fileList)) for fileList in fileArray])))
|
||||
fileList = self.runActions(RULES_ACTION_CHANNEL_BUILD_FILELIST_PRE, citem, interleave(fileArray, self.interleaveValue), inherited=self)
|
||||
self.log('[%s] buildChannel, pre fileList items = %s'%(citem['id'],len(fileList)),xbmc.LOGINFO)
|
||||
fileList = self.runActions(RULES_ACTION_CHANNEL_BUILD_FILELIST_POST, citem, _injectFillers(citem, fileList, self.fillBCTs), inherited=self)
|
||||
self.log('[%s] buildChannel, post fileList items = %s'%(citem['id'],len(fileList)),xbmc.LOGINFO)
|
||||
else: fileList = fileArray
|
||||
return self.runActions(RULES_ACTION_CHANNEL_BUILD_FILELIST_RETURN, citem, fileList, inherited=self)
|
||||
|
||||
|
||||
def buildFileList(self, citem: dict, path: str, media: str='video', page: int=SETTINGS.getSettingInt('Page_Limit'), sort: dict={}, limits: dict={}) -> list: #build channel via vfs path.
|
||||
self.log("[%s] buildFileList, media = %s, path = %s\nlimit = %s, sort = %s, page = %s"%(citem['id'],media,path,page,sort,limits))
|
||||
self.loopback = {}
|
||||
|
||||
def __padFileList(fileItems, page):
|
||||
if page > len(fileItems):
|
||||
tmpList = fileItems * (page // len(fileItems))
|
||||
tmpList.extend(fileItems[:page % len(fileItems)])
|
||||
return tmpList
|
||||
return fileItems
|
||||
|
||||
fileArray = []
|
||||
if path.endswith('.xsp'): #smartplaylist - parse xsp for path, sort info
|
||||
paths, media, sort, page = self.xsp.parseXSP(citem.get('id',''), path, media, sort, page)
|
||||
if len(paths) > 0:
|
||||
for idx, npath in enumerate(paths):
|
||||
self.pName = '%s %s/%s'%(citem['name'],idx+1,len(paths))
|
||||
fileArray.append(self.buildFileList(citem, npath, media, page, sort, limits))
|
||||
return interleave(fileArray, self.interleaveValue)
|
||||
|
||||
elif 'db://' in path and '?xsp=' in path: #dynamicplaylist - parse xsp for path, filter and sort info
|
||||
path, media, sort, filter = self.xsp.parseDXSP(citem.get('id',''), path, sort, {}, self.incExtras) #todo filter adv. rules
|
||||
|
||||
fileList = []
|
||||
dirList = [{'file':path}]
|
||||
npath = path
|
||||
nlimits = limits
|
||||
self.log("[%s] buildFileList, page = %s, sort = %s, limits = %s\npath = %s"%(citem['id'],page,sort,limits,path))
|
||||
|
||||
while not self.service.monitor.abortRequested() and len(fileList) < page:
|
||||
#Not all results are flat hierarchies; walk all paths until fileList page is reached. ie. folders with pagination and/or directories
|
||||
if self.service._interrupt():
|
||||
self.log("[%s] buildFileList, _interrupt"%(citem['id']))
|
||||
self.updateProgress(self.pCount, message='%s: %s'%(LANGUAGE(32144),LANGUAGE(32213)), header=ADDON_NAME)
|
||||
return []
|
||||
elif self.service._suspend():
|
||||
self.log("[%s] buildFileList, _suspend"%(citem['id']))
|
||||
self.updateProgress(self.pCount, message='%s: %s'%(LANGUAGE(32144),LANGUAGE(32145)), header=ADDON_NAME)
|
||||
self.service.monitor.waitForAbort(SUSPEND_TIMER)
|
||||
continue
|
||||
elif len(dirList) > 0:
|
||||
dir = dirList.pop(0)
|
||||
npath = dir.get('file')
|
||||
subfileList, subdirList, nlimits, errors = self.buildList(citem, npath, media, abs(page - len(fileList)), sort, limits, dir) #parse all directories under root. Flattened hierarchies required to stream line channel building.
|
||||
fileList += subfileList
|
||||
dirList = setDictLST(dirList + subdirList)
|
||||
self.log('[%s] buildFileList, adding = %s/%s remaining dirs (%s)\npath = %s, limits = %s'%(citem['id'],len(fileList),page,len(dirList),npath,nlimits))
|
||||
elif len(dirList) == 0:
|
||||
if len(fileList) > 0 and nlimits.get('total',0) > 0:
|
||||
dirList.insert(0,{'file':npath})
|
||||
self.log('[%s] buildFileList, reparse path %s'%(citem['id'],npath))
|
||||
else:
|
||||
self.log('[%s] buildFileList, no more folders to parse'%(citem['id']))
|
||||
break
|
||||
self.log("[%s] buildFileList, returning fileList %s/%s"%(citem['id'],len(fileList),page))
|
||||
return fileList
|
||||
|
||||
|
||||
def buildList(self, citem: dict, path: str, media: str='video', page: int=SETTINGS.getSettingInt('Page_Limit'), sort: dict={}, limits: dict={}, dirItem: dict={}, query: dict={}):
|
||||
self.log("[%s] buildList, media = %s, path = %s\npage = %s, sort = %s, query = %s, limits = %s\ndirItem = %s"%(citem['id'],media,path,page,sort,query,limits,dirItem))
|
||||
dirList, fileList, seasoneplist, trailersdict = [], [], [], {}
|
||||
items, nlimits, errors = self.jsonRPC.requestList(citem, path, media, page, sort, limits, query)
|
||||
|
||||
if errors.get('message'):
|
||||
self.pErrors.append(errors['message'])
|
||||
return fileList, dirList, nlimits, errors
|
||||
|
||||
elif items == self.loopback and limits != nlimits:# malformed jsonrpc queries will return root response, catch a re-parse and return.
|
||||
self.log("[%s] buildList, loopback detected using path = %s\nreturning: fileList (%s), dirList (%s)"%(citem['id'],path,len(fileList),len(dirList)))
|
||||
self.pErrors.append(LANGUAGE(32030))
|
||||
return fileList, dirList, nlimits, errors
|
||||
|
||||
elif not items and len(fileList) == 0:
|
||||
self.log("[%s] buildList, no request items found using path = %s\nreturning: fileList (%s), dirList (%s)"%(citem['id'],path,len(fileList),len(dirList)))
|
||||
self.pErrors.append(LANGUAGE(32026))
|
||||
return fileList, dirList, nlimits, errors
|
||||
|
||||
elif items:
|
||||
self.loopback = items
|
||||
|
||||
for idx, item in enumerate(items):
|
||||
file = item.get('file','')
|
||||
fileType = item.get('filetype','file')
|
||||
if not item.get('type'): item['type'] = query.get('key','files')
|
||||
|
||||
if self.service._interrupt():
|
||||
self.log("[%s] buildList, _interrupt"%(citem['id']))
|
||||
self.updateProgress(self.pCount, message='%s: %s'%(LANGUAGE(32144),LANGUAGE(32213)), header=ADDON_NAME)
|
||||
self.jsonRPC.autoPagination(citem['id'], path, query, limits) #rollback pagination limits
|
||||
return [], [], nlimits, errors
|
||||
|
||||
elif fileType == 'directory':
|
||||
dirList.append(item)
|
||||
# self.updateProgress(self.pCount, message=f'{self.pName}: {int(idx*100)//page}% appending: {item.get("label")}',header=f'{ADDON_NAME}, {self.pMSG}')
|
||||
self.log("[%s] buildList, IDX = %s, appending directory: %s"%(citem['id'],idx,file),xbmc.LOGINFO)
|
||||
|
||||
elif fileType == 'file':
|
||||
if file.startswith('pvr://'): #parse encoded fileitem otherwise no relevant meta provided via org. query. playable pvr:// paths are limited in Kodi.
|
||||
self.log("[%s] buildList, IDX = %s, PVR item => FileItem! file = %s"%(citem['id'],idx,file),xbmc.LOGINFO)
|
||||
item = decodePlot(item.get('plot',''))
|
||||
file = item.get('file')
|
||||
|
||||
if not file:
|
||||
self.pErrors.append(LANGUAGE(32031))
|
||||
self.log("[%s] buildList, IDX = %s, skipping missing playable file! path = %s"%(citem['id'],idx,path),xbmc.LOGINFO)
|
||||
continue
|
||||
|
||||
elif (file.lower().endswith('strm') and not self.incStrms):
|
||||
self.pErrors.append('%s STRM'%(LANGUAGE(32027)))
|
||||
self.log("[%s] buildList, IDX = %s, skipping strm file! file = %s"%(citem['id'],idx,file),xbmc.LOGINFO)
|
||||
continue
|
||||
|
||||
if not item.get('streamdetails',{}).get('video',[]) and not file.startswith(tuple(VFS_TYPES)): #parsing missing meta, kodi rpc bug fails to return streamdetails during Files.GetDirectory.
|
||||
item['streamdetails'] = self.jsonRPC.getStreamDetails(file, media)
|
||||
|
||||
if (self.is3D(item) and not self.inc3D):
|
||||
self.pErrors.append('%s 3D'%(LANGUAGE(32027)))
|
||||
self.log("[%s] buildList, IDX = %s skipping 3D file! file = %s"%(citem['id'],idx,file),xbmc.LOGINFO)
|
||||
continue
|
||||
|
||||
title = (item.get("title") or item.get("label") or dirItem.get('label') or '')
|
||||
tvtitle = (item.get("showtitle") or item.get("label") or dirItem.get('label') or '')
|
||||
if (item['type'].startswith(tuple(TV_TYPES)) or item.get("showtitle")):# This is a TV show
|
||||
season = int(item.get("season","0"))
|
||||
episode = int(item.get("episode","0"))
|
||||
if not file.startswith(tuple(VFS_TYPES)) and not self.incExtras and (season == 0 or episode == 0):
|
||||
self.pErrors.append('%s Extras'%(LANGUAGE(32027)))
|
||||
self.log("[%s] buildList, IDX = %s skipping extras! file = %s"%(citem['id'],idx,file),xbmc.LOGINFO)
|
||||
continue
|
||||
|
||||
label = tvtitle
|
||||
item["tvshowtitle"] = tvtitle
|
||||
item["episodetitle"] = title
|
||||
item["episodelabel"] = '%s%s'%(title,' (%sx%s)'%(season,str(episode).zfill(2))) #Episode Title (SSxEE) Mimic Kodi's PVR label format
|
||||
item["showlabel"] = '%s%s'%(item["tvshowtitle"],' - %s'%(item['episodelabel']) if item['episodelabel'] else '')
|
||||
else: # This is a Movie
|
||||
label = title
|
||||
item["episodetitle"] = item.get("tagline","")
|
||||
item["episodelabel"] = item.get("tagline","")
|
||||
item["showlabel"] = '%s%s'%(item["title"], ' - %s'%(item['episodelabel']) if item['episodelabel'] else '')
|
||||
|
||||
if not label:
|
||||
self.pErrors.append(LANGUAGE(32018)(LANGUAGE(30188)))
|
||||
continue
|
||||
|
||||
dur = self.jsonRPC.getDuration(file, item, self.accurateDuration, self.saveDuration)
|
||||
if dur > self.minDuration: #include media that's duration is above the players seek tolerance & users adv. rule
|
||||
self.updateProgress(self.pCount, message='%s: %s'%(self.pName,int(idx*100)//page)+'%',header='%s, %s'%(ADDON_NAME,self.pMSG))
|
||||
|
||||
item['duration'] = dur
|
||||
item['media'] = media
|
||||
item['originalpath'] = path #use for path sorting/playback verification
|
||||
item['friendly'] = SETTINGS.getFriendlyName()
|
||||
item['remote'] = PROPERTIES.getRemoteHost()
|
||||
|
||||
if item.get("year",0) == 1601: item['year'] = 0 #detect kodi bug that sets a fallback year to 1601 https://github.com/xbmc/xbmc/issues/15554
|
||||
spTitle, spYear = splitYear(label)
|
||||
item['label'] = spTitle
|
||||
if item.get('year',0) == 0 and spYear: item['year'] = spYear #replace missing item year with one parsed from show title
|
||||
|
||||
item['plot'] = (item.get("plot","") or item.get("plotoutline","") or item.get("description","") or LANGUAGE(32020)).strip()
|
||||
if query.get('holiday'):
|
||||
citem['holiday'] = query.get('holiday')
|
||||
holiday = "[B]%s[/B] - [I]%s[/I]"%(query["holiday"]["name"],query["holiday"]["tagline"]) if query["holiday"]["tagline"] else "[B]%s[/B]"%(query["holiday"]["name"])
|
||||
item["plot"] = "%s \n%s"%(holiday,item["plot"])
|
||||
|
||||
item['art'] = (item.get('art',{}) or dirItem.get('art',{}))
|
||||
item.get('art',{})['icon'] = citem['logo']
|
||||
|
||||
if item.get('trailer') and self.bctTypes['trailers'].get('enabled',False):
|
||||
titem = item.copy()
|
||||
tdur = self.jsonRPC.getDuration(titem.get('trailer'), accurate=True, save=False)
|
||||
if tdur > 0:
|
||||
titem.update({'label':'%s - %s'%(item["label"],LANGUAGE(30187)),'episodetitle':'%s - %s'%(item["episodetitle"],LANGUAGE(30187)),'episodelabel':'%s - %s'%(item["episodelabel"],LANGUAGE(30187)),'duration':tdur, 'runtime':tdur, 'file':titem['trailer'], 'streamdetails':{}})
|
||||
[trailersdict.setdefault(genre.lower(),[]).append(titem) for genre in (titem.get('genre',[]) or ['resources'])]
|
||||
|
||||
if sort.get("method","") == 'episode' and (int(item.get("season","0")) + int(item.get("episode","0"))) > 0:
|
||||
seasoneplist.append([int(item.get("season","0")), int(item.get("episode","0")), item])
|
||||
else:
|
||||
fileList.append(item)
|
||||
else:
|
||||
self.pErrors.append(LANGUAGE(32032))
|
||||
self.log("[%s] buildList, IDX = %s skipping content no duration meta found! or runtime below minDuration (%s/%s) file = %s"%(citem['id'],idx,dur,self.minDuration,file),xbmc.LOGINFO)
|
||||
|
||||
if sort.get("method","").startswith('episode'):
|
||||
self.log("[%s] buildList, sorting by episode"%(citem['id']))
|
||||
seasoneplist.sort(key=lambda seep: seep[1])
|
||||
seasoneplist.sort(key=lambda seep: seep[0])
|
||||
for seepitem in seasoneplist:
|
||||
fileList.append(seepitem[2])
|
||||
|
||||
elif sort.get("method","") == 'random':
|
||||
self.log("[%s] buildList, random shuffling"%(citem['id']))
|
||||
dirList = randomShuffle(dirList)
|
||||
fileList = randomShuffle(fileList)
|
||||
|
||||
self.getTrailers(trailersdict)
|
||||
self.log("[%s] buildList, returning (%s) files, (%s) dirs; parsed (%s) trailers"%(citem['id'],len(fileList),len(dirList),len(trailersdict)))
|
||||
return fileList, dirList, nlimits, errors
|
||||
|
||||
|
||||
def isHD(self, item: dict) -> bool:
|
||||
if 'isHD' in item: return item['isHD']
|
||||
elif not item.get('streamdetails',{}).get('video',[]) and not item.get('file','').startswith(tuple(VFS_TYPES)):
|
||||
item['streamdetails'] = self.jsonRPC.getStreamDetails(item.get('file'), item.get('media','video'))
|
||||
details = item.get('streamdetails',{})
|
||||
if 'video' in details and len(details.get('video')) > 0:
|
||||
videowidth = int(details['video'][0]['width'] or '0')
|
||||
videoheight = int(details['video'][0]['height'] or '0')
|
||||
if videowidth >= 1280 or videoheight >= 720: return True
|
||||
return False
|
||||
|
||||
|
||||
def isUHD(self, item: dict) -> bool:
|
||||
if 'isUHD' in item: return item['isUHD']
|
||||
elif not item.get('streamdetails',{}).get('video',[]) and not item.get('file','').startswith(tuple(VFS_TYPES)):
|
||||
item['streamdetails'] = self.jsonRPC.getStreamDetails(item.get('file'), item.get('media','video'))
|
||||
details = item.get('streamdetails',{})
|
||||
if 'video' in details and len(details.get('video')) > 0:
|
||||
videowidth = int(details['video'][0]['width'] or '0')
|
||||
videoheight = int(details['video'][0]['height'] or '0')
|
||||
if videowidth > 1920 or videoheight > 1080: return True
|
||||
return False
|
||||
|
||||
|
||||
def is3D(self, item: dict) -> bool:
|
||||
if 'is3D' in item: return item['is3D']
|
||||
elif not item.get('streamdetails',{}).get('video',[]) and not item.get('file','').startswith(tuple(VFS_TYPES)):
|
||||
item['streamdetails'] = self.jsonRPC.getStreamDetails(item.get('file'), item.get('media','video'))
|
||||
details = item.get('streamdetails',{})
|
||||
if 'video' in details and details.get('video') != [] and len(details.get('video')) > 0:
|
||||
stereomode = (details['video'][0]['stereomode'] or [])
|
||||
if len(stereomode) > 0: return True
|
||||
return False
|
||||
|
||||
|
||||
def getTrailers(self, nitems: dict={}) -> dict:
|
||||
return self.cache.set('kodiTrailers', mergeDictLST((self.cache.get('kodiTrailers', json_data=True) or {}),nitems), expiration=datetime.timedelta(days=28), json_data=True)
|
||||
@@ -0,0 +1,115 @@
|
||||
# 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 functools import wraps
|
||||
from fileaccess import FileAccess
|
||||
|
||||
try: from simplecache import SimpleCache
|
||||
except: from simplecache.simplecache import SimpleCache #pycharm stub
|
||||
|
||||
def cacheit(expiration=datetime.timedelta(days=MIN_GUIDEDAYS), checksum=ADDON_VERSION, json_data=False):
|
||||
def internal(method):
|
||||
@wraps(method)
|
||||
def wrapper(*args, **kwargs):
|
||||
method_class = args[0]
|
||||
cacheName = "%s.%s"%(method_class.__class__.__name__, method.__name__)
|
||||
for item in args[1:]: cacheName += u".%s"%item
|
||||
for k, v in list(kwargs.items()): cacheName += u".%s"%(v)
|
||||
results = method_class.cache.get(cacheName.lower(), checksum, json_data)
|
||||
if results: return results
|
||||
return method_class.cache.set(cacheName.lower(), method(*args, **kwargs), checksum, expiration, json_data)
|
||||
return wrapper
|
||||
return internal
|
||||
|
||||
class Service:
|
||||
monitor = MONITOR()
|
||||
def _interrupt(self) -> bool:
|
||||
return xbmcgui.Window(10000).getProperty('%s.pendingInterrupt'%(ADDON_ID)) == "true"
|
||||
def _suspend(self) -> bool:
|
||||
return xbmcgui.Window(10000).getProperty('%s.suspendActivity'%(ADDON_ID)) == "true"
|
||||
|
||||
class Cache:
|
||||
lock = Lock()
|
||||
cache = SimpleCache()
|
||||
service = Service()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def cacheLocker(self, wait=0.0001): #simplecache is not thread safe, threadlock not avoiding collisions? Hack/Lazy avoidance.
|
||||
while not self.service.monitor.abortRequested():
|
||||
if self.service.monitor.waitForAbort(wait) or self.service._interrupt(): break
|
||||
elif xbmcgui.Window(10000).getProperty('%s.cacheLocker'%(ADDON_ID)) != 'true': break
|
||||
xbmcgui.Window(10000).setProperty('%s.cacheLocker'%(ADDON_ID),'true')
|
||||
try: yield
|
||||
finally:
|
||||
xbmcgui.Window(10000).setProperty('%s.cacheLocker'%(ADDON_ID),'false')
|
||||
|
||||
|
||||
def __init__(self, mem_cache=False, is_json=False, disable_cache=False):
|
||||
self.cache.enable_mem_cache = mem_cache
|
||||
self.cache.data_is_json = is_json
|
||||
self.disable_cache = (disable_cache | REAL_SETTINGS.getSettingBool('Disable_Cache'))
|
||||
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
log('%s: %s'%(self.__class__.__name__,msg),level)
|
||||
|
||||
|
||||
def getname(self, name):
|
||||
if not name.startswith(ADDON_ID): name = '%s.%s'%(ADDON_ID,name)
|
||||
return name.lower()
|
||||
|
||||
|
||||
def set(self, name, value, checksum=ADDON_VERSION, expiration=datetime.timedelta(minutes=15), json_data=False):
|
||||
if not self.disable_cache or (not isinstance(value,(bool,list,dict)) and not value):
|
||||
with self.cacheLocker():
|
||||
self.log('set, name = %s, value = %s'%(self.getname(name),'%s...'%(str(value)[:128])))
|
||||
self.cache.set(self.getname(name),value,checksum,expiration,json_data)
|
||||
return value
|
||||
|
||||
|
||||
def get(self, name, checksum=ADDON_VERSION, json_data=False):
|
||||
if not self.disable_cache:
|
||||
with self.cacheLocker():
|
||||
try:
|
||||
value = self.cache.get(self.getname(name),checksum,json_data)
|
||||
self.log('get, name = %s, value = %s'%(self.getname(name),'%s...'%(str(value)[:128])))
|
||||
return value
|
||||
except Exception as e:
|
||||
self.log("get, name = %s failed! simplecacheDB %s"%(self.getname(name),e), xbmc.LOGERROR)
|
||||
self.clear(name)
|
||||
|
||||
|
||||
def clear(self, name, wait=15):
|
||||
import sqlite3
|
||||
self.log('clear, name = %s'%self.getname(name))
|
||||
sc = FileAccess.translatePath(xbmcaddon.Addon(id='script.module.simplecache').getAddonInfo('profile'))
|
||||
dbpath = os.path.join(sc, 'simplecache.db')
|
||||
try:
|
||||
connection = sqlite3.connect(dbpath, timeout=wait, isolation_level=None)
|
||||
connection.execute('DELETE FROM simplecache WHERE id LIKE ?', (self.getname(name) + '%',))
|
||||
connection.commit()
|
||||
except sqlite3.Error as e: self.log('clear, failed! %s' % e, xbmc.LOGERROR)
|
||||
finally:
|
||||
if connection:
|
||||
connection.close()
|
||||
del connection
|
||||
del sqlite3
|
||||
@@ -0,0 +1,162 @@
|
||||
# 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 *
|
||||
#todo create dataclasses for all jsons
|
||||
# https://pypi.org/project/dataclasses-json/
|
||||
class Channels:
|
||||
|
||||
def __init__(self):
|
||||
self.channelDATA = getJSON(CHANNELFLE_DEFAULT)
|
||||
self.channelTEMP = getJSON(CHANNEL_ITEM)
|
||||
self.channelDATA.update(self._load())
|
||||
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
return log('%s: %s'%(self.__class__.__name__,msg),level)
|
||||
|
||||
|
||||
def _load(self, file=CHANNELFLEPATH) -> dict:
|
||||
channelDATA = getJSON(file)
|
||||
self.log('_load, channels = %s'%(len(channelDATA.get('channels',[]))))
|
||||
return channelDATA
|
||||
|
||||
|
||||
def _verify(self, channels: list=[]):
|
||||
for idx, citem in enumerate(self.channelDATA.get('channels',[])):
|
||||
if not citem.get('name') or not citem.get('id') or len(citem.get('path',[])) == 0:
|
||||
self.log('_verify, in-valid citem [%s]\n%s'%(citem.get('id'),citem))
|
||||
continue
|
||||
else: yield citem
|
||||
|
||||
|
||||
def _save(self, file=CHANNELFLEPATH) -> bool:
|
||||
self.channelDATA['uuid'] = SETTINGS.getMYUUID()
|
||||
self.channelDATA['channels'] = self.sortChannels(self.channelDATA['channels'])
|
||||
self.log('_save, channels = %s'%(len(self.channelDATA['channels'])))
|
||||
return setJSON(file,self.channelDATA)
|
||||
|
||||
|
||||
def getTemplate(self) -> dict:
|
||||
return self.channelTEMP.copy()
|
||||
|
||||
|
||||
def getChannels(self) -> list:
|
||||
return sorted(self.channelDATA['channels'], key=itemgetter('number'))
|
||||
|
||||
|
||||
def popChannels(self, type: str, channels: list=[]) -> list:
|
||||
return [self.channelDATA['channels'].pop(self.channelDATA['channels'].index(citem)) for citem in list([c for c in channels if c.get('type') == type])]
|
||||
|
||||
|
||||
def getCustom(self) -> list:
|
||||
channels = self.getChannels()
|
||||
return list([citem for citem in channels if citem.get('number') <= CHANNEL_LIMIT])
|
||||
|
||||
|
||||
def getAutotuned(self) -> list:
|
||||
channels = self.getChannels()
|
||||
return list([citem for citem in channels if citem.get('number') > CHANNEL_LIMIT])
|
||||
|
||||
|
||||
def getChannelbyID(self, id: str) -> list:
|
||||
channels = self.getChannels()
|
||||
return list([c for c in channels if c.get('id') == id])
|
||||
|
||||
|
||||
def getType(self, type: str):
|
||||
channels = self.getChannels()
|
||||
return list([citem for citem in channels if citem.get('type') == type])
|
||||
|
||||
|
||||
def sortChannels(self, channels: list) -> list:
|
||||
try: return sorted(channels, key=itemgetter('number'))
|
||||
except: return channels
|
||||
|
||||
|
||||
def setChannels(self, channels: list=[]) -> bool:
|
||||
if len(channels) == 0: channels = self.channelDATA['channels']
|
||||
self.channelDATA['channels'] = channels
|
||||
SETTINGS.setSetting('Select_Channels','[B]%s[/B] Channels'%(len(channels)))
|
||||
PROPERTIES.setChannels(len(channels)>0)
|
||||
return self._save()
|
||||
|
||||
|
||||
def getImports(self) -> list:
|
||||
return self.channelDATA.get('imports',[])
|
||||
|
||||
|
||||
def setImports(self, data: list=[]) -> bool:
|
||||
self.channelDATA['imports'] = data
|
||||
return self.setChannels()
|
||||
|
||||
|
||||
def clearChannels(self):
|
||||
self.channelDATA['channels'] = []
|
||||
|
||||
|
||||
def delChannel(self, citem: dict={}) -> bool:
|
||||
self.log('delChannel,[%s]'%(citem['id']), xbmc.LOGINFO)
|
||||
idx, channel = self.findChannel(citem)
|
||||
if idx is not None: self.channelDATA['channels'].pop(idx)
|
||||
return True
|
||||
|
||||
|
||||
def delChannels(self, channels: list=[]) -> bool:
|
||||
return [self.delChannel(channel) for channel in channels]
|
||||
|
||||
|
||||
def addChannel(self, citem: dict={}) -> bool:
|
||||
idx, channel = self.findChannel(citem)
|
||||
if idx is not None:
|
||||
for key in ['id','rules','number','favorite','logo']:
|
||||
if channel.get(key): citem[key] = channel[key] # existing id found, reuse channel meta.
|
||||
|
||||
if citem.get('favorite',False):
|
||||
citem['group'].append(LANGUAGE(32019))
|
||||
citem['group'] = sorted(set(citem['group']))
|
||||
|
||||
self.log('addChannel, [%s] updating channel %s'%(citem["id"],citem["name"]), xbmc.LOGINFO)
|
||||
self.channelDATA['channels'][idx] = citem
|
||||
else:
|
||||
self.log('addChannel, [%s] adding channel %s'%(citem["id"],citem["name"]), xbmc.LOGINFO)
|
||||
self.channelDATA.setdefault('channels',[]).append(citem)
|
||||
return True
|
||||
|
||||
|
||||
def addChannels(self, channels: list=[]) -> bool:
|
||||
return [self.addChannel(channel) for channel in channels]
|
||||
|
||||
|
||||
def findChannel(self, citem: dict={}, channels: list=[]) -> tuple:
|
||||
if len(channels) == 0: channels = self.getChannels()
|
||||
for idx, eitem in enumerate(channels):
|
||||
if citem.get('id') == eitem.get('id',str(random.random())):
|
||||
return idx, eitem
|
||||
return None, {}
|
||||
|
||||
|
||||
def findAutotuned(self, citem: dict={}, channels: list=[]) -> tuple:
|
||||
if len(channels) == 0: channels = self.getAutotuned()
|
||||
for idx, eitem in enumerate(channels):
|
||||
if (citem.get('id') == eitem.get('id',str(random.random()))) or (citem.get('type') == eitem.get('type',str(random.random())) and citem.get('name','').lower() == eitem.get('name',str(random.random())).lower()):
|
||||
return idx, eitem
|
||||
return None, {}
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
# 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 -*-
|
||||
|
||||
import os
|
||||
|
||||
from kodi_six import xbmc, xbmcaddon
|
||||
|
||||
#info
|
||||
ADDON_ID = 'plugin.video.pseudotv.live'
|
||||
REAL_SETTINGS = xbmcaddon.Addon(id=ADDON_ID)
|
||||
ADDON_NAME = REAL_SETTINGS.getAddonInfo('name')
|
||||
ADDON_VERSION = REAL_SETTINGS.getAddonInfo('version')
|
||||
ICON = REAL_SETTINGS.getAddonInfo('icon')
|
||||
FANART = REAL_SETTINGS.getAddonInfo('fanart')
|
||||
SETTINGS_LOC = REAL_SETTINGS.getAddonInfo('profile')
|
||||
ADDON_PATH = REAL_SETTINGS.getAddonInfo('path')
|
||||
ADDON_AUTHOR = REAL_SETTINGS.getAddonInfo('author')
|
||||
ADDON_URL = 'https://raw.githubusercontent.com/PseudoTV/PseudoTV_Live/master/plugin.video.pseudotv.live/addon.xml'
|
||||
LANGUAGE = REAL_SETTINGS.getLocalizedString
|
||||
|
||||
#api
|
||||
MONITOR = xbmc.Monitor
|
||||
PLAYER = xbmc.Player
|
||||
|
||||
#constants
|
||||
FIFTEEN = 15 #unit
|
||||
DISCOVERY_TIMER = 60 #secs
|
||||
SUSPEND_TIMER = 2 #secs
|
||||
DISCOVER_INTERVAL = 30 #secs
|
||||
MIN_EPG_DURATION = 10800 #secs
|
||||
ONNEXT_TIMER = 15
|
||||
DTFORMAT = '%Y%m%d%H%M%S'
|
||||
DTZFORMAT = '%Y%m%d%H%M%S +%z'
|
||||
DTJSONFORMAT = '%Y-%m-%d %H:%M:%S'
|
||||
BACKUP_TIME_FORMAT = '%Y-%m-%d %I:%M %p'
|
||||
|
||||
LANG = 'en' #todo parse kodi region settings
|
||||
DEFAULT_ENCODING = "utf-8"
|
||||
PROMPT_DELAY = 4000 #msecs
|
||||
AUTOCLOSE_DELAY = 300 #secs
|
||||
SELECT_DELAY = 900 #secs
|
||||
RADIO_ITEM_LIMIT = 250
|
||||
CHANNEL_LIMIT = 999
|
||||
AUTOTUNE_LIMIT = 3
|
||||
FILLER_LIMIT = 250
|
||||
QUEUE_CHUNK = 25
|
||||
|
||||
FILLER_TYPE = ['Rating',
|
||||
'Bumper',
|
||||
'Advert',
|
||||
'Trailer',
|
||||
'Pre-Roll',
|
||||
'Post-Roll']
|
||||
|
||||
FILLER_TYPES = ['Ratings',
|
||||
'Bumpers',
|
||||
'Adverts',
|
||||
'Trailers']
|
||||
|
||||
AUTOTUNE_TYPES = ["Playlists",
|
||||
"TV Networks",
|
||||
"TV Shows",
|
||||
"TV Genres",
|
||||
"Movie Genres",
|
||||
"Movie Studios",
|
||||
"Mixed Genres",
|
||||
"Music Genres",
|
||||
"Mixed",
|
||||
"Recommended",
|
||||
"Services"]
|
||||
|
||||
GROUP_TYPES = ['Addon',
|
||||
'Custom',
|
||||
'Directory',
|
||||
'TV',
|
||||
'Movies',
|
||||
'Music',
|
||||
'Miscellaneous',
|
||||
'PVR',
|
||||
'Plugin',
|
||||
'Radio',
|
||||
'Smartplaylist',
|
||||
'UPNP',
|
||||
'IPTV'] + AUTOTUNE_TYPES
|
||||
|
||||
DB_TYPES = ["videodb://",
|
||||
"musicdb://",
|
||||
"library://",
|
||||
"special://"]
|
||||
|
||||
WEB_TYPES = ["http",
|
||||
"ftp://",
|
||||
"pvr://"
|
||||
"upnp://",]
|
||||
|
||||
VFS_TYPES = ["plugin://",
|
||||
"pvr://",
|
||||
"resource://",
|
||||
"special://home/addons/resource"]
|
||||
|
||||
TV_TYPES = ['episode',
|
||||
'episodes',
|
||||
'tvshow',
|
||||
'tvshows']
|
||||
|
||||
MOVIE_TYPES = ['movie',
|
||||
'movies']
|
||||
|
||||
MUSIC_TYPES = ['songs',
|
||||
'albums',
|
||||
'artists',
|
||||
'music']
|
||||
|
||||
HTML_ESCAPE = {"&": "&",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
">": ">",
|
||||
"<": "<"}
|
||||
|
||||
ALT_PLAYLISTS = [".cue",
|
||||
".m3u",
|
||||
".m3u8",
|
||||
".strm",
|
||||
".pls",
|
||||
".wpl"]
|
||||
|
||||
IGNORE_CHTYPE = ['TV Shows',
|
||||
'Mixed',
|
||||
'Recommended',
|
||||
'Services',
|
||||
'Music Genres']
|
||||
|
||||
MOVIE_CHTYPE = ["Movie Genres",
|
||||
"Movie Studios"]
|
||||
|
||||
TV_CHTYPE = ["TV Networks",
|
||||
"TV Genres",
|
||||
"Mixed Genre"]
|
||||
|
||||
TV_URL = 'plugin://{addon}/?mode=tv&name={name}&chid={chid}.pvr'
|
||||
RESUME_URL = 'plugin://{addon}/?mode=resume&name={name}&chid={chid}.pvr'
|
||||
RADIO_URL = 'plugin://{addon}/?mode=radio&name={name}&chid={chid}&radio={radio}&vid={vid}.pvr'
|
||||
LIVE_URL = 'plugin://{addon}/?mode=live&name={name}&chid={chid}&vid={vid}&now={now}&start={start}&duration={duration}&stop={stop}.pvr'
|
||||
BROADCAST_URL = 'plugin://{addon}/?mode=broadcast&name={name}&chid={chid}&vid={vid}.pvr'
|
||||
VOD_URL = 'plugin://{addon}/?mode=vod&title={title}&chid={chid}&vid={vid}&name={name}.pvr'
|
||||
DVR_URL = 'plugin://{addon}/?mode=dvr&title={title}&chid={chid}&vid={vid}&seek={seek}&duration={duration}.pvr'
|
||||
|
||||
PTVL_REPO = 'repository.pseudotv'
|
||||
PVR_CLIENT_ID = 'pvr.iptvsimple'
|
||||
PVR_CLIENT_NAME = 'IPTV Simple Client'
|
||||
PVR_CLIENT_LOC = 'special://profile/addon_data/%s'%(PVR_CLIENT_ID)
|
||||
|
||||
#docs
|
||||
README_FLE = os.path.join(ADDON_PATH,'README.md')
|
||||
CHANGELOG_FLE = os.path.join(ADDON_PATH,'changelog.txt')
|
||||
LICENSE_FLE = os.path.join(ADDON_PATH,'LICENSE')
|
||||
|
||||
#files
|
||||
M3UFLE = 'pseudotv.m3u'
|
||||
XMLTVFLE = 'pseudotv.xml'
|
||||
GENREFLE = 'genres.xml'
|
||||
REMOTEFLE = 'remote.json'
|
||||
BONJOURFLE = 'bonjour.json'
|
||||
SERVERFLE = 'servers.json'
|
||||
CHANNELFLE = 'channels.json'
|
||||
LIBRARYFLE = 'library.json'
|
||||
TVGROUPFLE = 'tv_groups.xml'
|
||||
RADIOGROUPFLE = 'radio_groups.xml'
|
||||
PROVIDERFLE = 'providers.xml'
|
||||
CHANNELBACKUPFLE = 'channels.backup'
|
||||
CHANNELRESTOREFLE = 'channels.restore'
|
||||
|
||||
#exts
|
||||
VIDEO_EXTS = xbmc.getSupportedMedia('video').split('|')[:-1]
|
||||
MUSIC_EXTS = xbmc.getSupportedMedia('music').split('|')[:-1]
|
||||
IMAGE_EXTS = xbmc.getSupportedMedia('picture').split('|')[:-1]
|
||||
IMG_EXTS = ['.png','.jpg','.gif']
|
||||
TEXTURES = 'Textures.xbt'
|
||||
|
||||
#folders
|
||||
IMAGE_LOC = os.path.join(ADDON_PATH,'resources','images')
|
||||
MEDIA_LOC = os.path.join(ADDON_PATH,'resources','skins','default','media')
|
||||
SFX_LOC = os.path.join(MEDIA_LOC,'sfx')
|
||||
SERVER_LOC = os.path.join(SETTINGS_LOC,SERVERFLE)
|
||||
BACKUP_LOC = os.path.join(SETTINGS_LOC,'backup')
|
||||
CACHE_LOC = os.path.join(SETTINGS_LOC,'cache')
|
||||
TEMP_LOC = os.path.join(SETTINGS_LOC,'temp')
|
||||
|
||||
#file paths
|
||||
SETTINGS_FLE = os.path.join(SETTINGS_LOC,'settings.xml')
|
||||
CHANNELFLE_BACKUP = os.path.join(BACKUP_LOC,CHANNELBACKUPFLE)
|
||||
CHANNELFLE_RESTORE = os.path.join(BACKUP_LOC,CHANNELRESTOREFLE)
|
||||
|
||||
#sfx
|
||||
BING_WAV = os.path.join(SFX_LOC,'bing.wav')
|
||||
NOTE_WAV = os.path.join(SFX_LOC,'notify.wav')
|
||||
|
||||
#remotes
|
||||
IMPORT_ASSET = os.path.join(ADDON_PATH,'remotes','asset.json')
|
||||
RULEFLE_ITEM = os.path.join(ADDON_PATH,'remotes','rule.json')
|
||||
CHANNEL_ITEM = os.path.join(ADDON_PATH,'remotes','channel.json')
|
||||
M3UFLE_DEFAULT = os.path.join(ADDON_PATH,'remotes','m3u.json')
|
||||
SEASONS = os.path.join(ADDON_PATH,'remotes','seasons.json')
|
||||
HOLIDAYS = os.path.join(ADDON_PATH,'remotes','holidays.json')
|
||||
GROUPFLE_DEFAULT = os.path.join(ADDON_PATH,'remotes','groups.xml')
|
||||
LIBRARYFLE_DEFAULT = os.path.join(ADDON_PATH,'remotes',LIBRARYFLE)
|
||||
CHANNELFLE_DEFAULT = os.path.join(ADDON_PATH,'remotes',CHANNELFLE)
|
||||
GENREFLE_DEFAULT = os.path.join(ADDON_PATH,'remotes',GENREFLE)
|
||||
PROVIDERFLE_DEFAULT = os.path.join(ADDON_PATH,'remotes',PROVIDERFLE)
|
||||
|
||||
#colors
|
||||
PRIMARY_BACKGROUND = 'FF11375C'
|
||||
SECONDARY_BACKGROUND = '334F4F9E'
|
||||
DIALOG_TINT = 'FF181B1E'
|
||||
BUTTON_FOCUS = 'FF2866A4'
|
||||
SELECTED = 'FF5BE5EE'
|
||||
|
||||
COLOR_BACKGROUND = '01416b'
|
||||
COLOR_TEXT = 'FFFFFF'
|
||||
COLOR_UNAVAILABLE_CHANNEL = 'dimgray'
|
||||
COLOR_AVAILABLE_CHANNEL = 'white'
|
||||
COLOR_LOCKED_CHANNEL = 'orange'
|
||||
COLOR_WARNING_CHANNEL = 'red'
|
||||
COLOR_NEW_CHANNEL = 'green'
|
||||
COLOR_RADIO_CHANNEL = 'cyan'
|
||||
COLOR_FAVORITE_CHANNEL = 'yellow'
|
||||
#https://github.com/xbmc/xbmc/blob/656052d108297e4dd8c5c6fc7db86606629e457e/system/colors.xml
|
||||
|
||||
#urls
|
||||
URL_WIKI = 'https://github.com/PseudoTV/PseudoTV_Live/wiki'
|
||||
URL_SUPPORT = 'https://forum.kodi.tv/showthread.php?tid=346803'
|
||||
URL_WIN_BONJOUR = 'https://support.apple.com/en-us/106380'
|
||||
URL_README = 'https://github.com/PseudoTV/PseudoTV_Live/blob/master/plugin.video.pseudotv.live/README.md'
|
||||
|
||||
|
||||
# https://github.com/xbmc/xbmc/blob/master/system/colors.xml
|
||||
|
||||
#images
|
||||
LOGO = os.path.join(MEDIA_LOC,'wlogo.png')
|
||||
DIM_LOGO = os.path.join(MEDIA_LOC,'dimlogo.png')
|
||||
COLOR_LOGO = os.path.join(MEDIA_LOC,'logo.png')
|
||||
COLOR_FANART = os.path.join(MEDIA_LOC,'fanart.jpg')
|
||||
HOST_LOGO = 'http://github.com/PseudoTV/PseudoTV_Live/blob/master/plugin.video.pseudotv.live/resources/skins/default/media/logo.png?raw=true'
|
||||
DUMMY_ICON = 'https://dummyimage.com/512x512/%s/%s.png&text={text}'%(COLOR_BACKGROUND,COLOR_TEXT)
|
||||
|
||||
#skins
|
||||
BUSY_XML = '%s.busy.xml'%(ADDON_ID)
|
||||
ONNEXT_XML = '%s.onnext.xml'%(ADDON_ID)
|
||||
RESTART_XML = '%s.restart.xml'%(ADDON_ID)
|
||||
ONNEXT_XML = '%s.onnext.xml'%(ADDON_ID)
|
||||
BACKGROUND_XML = '%s.background.xml'%(ADDON_ID)
|
||||
MANAGER_XML = '%s.manager.xml'%(ADDON_ID)
|
||||
OVERLAYTOOL_XML = '%s.overlaytool.xml'%(ADDON_ID)
|
||||
DIALOG_SELECT = '%s.dialogselect.xml'%(ADDON_ID)
|
||||
|
||||
# https://github.com/xbmc/xbmc/blob/master/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/input/action_ids.h
|
||||
|
||||
# Actions
|
||||
ACTION_MOVE_LEFT = 1
|
||||
ACTION_MOVE_RIGHT = 2
|
||||
ACTION_MOVE_UP = 3
|
||||
ACTION_MOVE_DOWN = 4
|
||||
ACTION_INVALID = 999
|
||||
ACTION_SELECT_ITEM = [7,135]
|
||||
ACTION_PREVIOUS_MENU = [92,10,110,521,ACTION_SELECT_ITEM]
|
||||
|
||||
#rules
|
||||
##builder
|
||||
RULES_ACTION_CHANNEL_CITEM = 1 #Persistent citem changes
|
||||
RULES_ACTION_CHANNEL_START = 2 #Set channel global
|
||||
RULES_ACTION_CHANNEL_BUILD_FILEARRAY_PRE = 3 #Initial FileArray (build bypass)
|
||||
RULES_ACTION_CHANNEL_BUILD_PATH = 4 #Alter parsing directory prior to build
|
||||
RULES_ACTION_CHANNEL_BUILD_FILELIST_PRE = 5 #Initial FileList prior to fillers, after interleaving
|
||||
RULES_ACTION_CHANNEL_BUILD_FILELIST_POST = 6 #Final FileList after fillers
|
||||
RULES_ACTION_CHANNEL_BUILD_TIME_PRE = 7 #FileList prior to scheduling
|
||||
RULES_ACTION_CHANNEL_BUILD_TIME_POST = 8 #FileList after scheduling
|
||||
RULES_ACTION_CHANNEL_BUILD_FILEARRAY_POST = 9 #FileArray prior to interleaving and fillers
|
||||
RULES_ACTION_CHANNEL_STOP = 10#restore channel global
|
||||
RULES_ACTION_CHANNEL_TEMP_CITEM = 11 #Temporary citem changes, rule injection
|
||||
RULES_ACTION_CHANNEL_BUILD_FILELIST_RETURN = 12
|
||||
##player
|
||||
RULES_ACTION_PLAYER_START = 20 #Playback started
|
||||
RULES_ACTION_PLAYER_CHANGE = 21 #Playback changed/ended
|
||||
RULES_ACTION_PLAYER_STOP = 22 #Playback stopped
|
||||
##overlay/background
|
||||
RULES_ACTION_OVERLAY_OPEN = 30 #Overlay opened
|
||||
RULES_ACTION_OVERLAY_CLOSE = 31 #Overlay closed
|
||||
##playback
|
||||
RULES_ACTION_PLAYBACK_RESUME = 40 #Prior to playback trigger resume to received a FileList
|
||||
|
||||
HEADER = {'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246"}
|
||||
|
||||
MUSIC_LISTITEM_TYPES = {'tracknumber' : (int,), #integer (8)
|
||||
'discnumber' : (int,), #integer (2)
|
||||
'duration' : (int,), #integer (245) - duration in seconds
|
||||
'year' : (int,), #integer (1998)
|
||||
'genre' : (tuple,list),
|
||||
'album' : (str,),
|
||||
'artist' : (str,),
|
||||
'title' : (str,),
|
||||
'rating' : (float,),#float - range is between 0 and 10
|
||||
'userrating' : (int,), #integer - range is 1..10
|
||||
'lyrics' : (str,),
|
||||
'playcount' : (int,), #integer (2) - number of times this item has been played
|
||||
'lastplayed' : (str,), #string (Y-m-d h:m:s = 2009-04-05 23:16:04)
|
||||
'mediatype' : (str,), #string - "music", "song", "album", "artist"
|
||||
'dbid' : (int,), #integer (23) - Only add this for items which are part of the local db. You also need to set the correct 'mediatype'!
|
||||
'listeners' : (int,), #integer (25614)
|
||||
'musicbrainztrackid' : (str,list),
|
||||
'musicbrainzartistid' : (str,list),
|
||||
'musicbrainzalbumid' : (str,list),
|
||||
'musicbrainzalbumartistid': (str,list),
|
||||
'comment' : (str,),
|
||||
'count' : (int,), #integer (12) - can be used to store an id for later, or for sorting purposes
|
||||
# 'size' : (int,), #long (1024) - size in bytes
|
||||
'date' : (str,),} #string (d.m.Y / 01.01.2009) - file date
|
||||
|
||||
VIDEO_LISTITEM_TYPES = {'genre' : (tuple,list),
|
||||
'country' : (str,list),
|
||||
'year' : (int,), #integer (2009)
|
||||
'episode' : (int,), #integer (4)
|
||||
'season' : (int,), #integer (1)
|
||||
'sortepisode' : (int,), #integer (4)
|
||||
'sortseason' : (int,), #integer (1)
|
||||
'episodeguide' : (str,),
|
||||
'showlink' : (str,list),
|
||||
'top250' : (int,), #integer (192)
|
||||
'setid' : (int,), #integer (14)
|
||||
'tracknumber' : (int,), #integer (3)
|
||||
'rating' : (float,),#float (6.4) - range is 0..10
|
||||
'userrating' : (int,), #integer (9) - range is 1..10 (0 to reset)
|
||||
'playcount' : (int,), #integer (2) - number of times this item has been played
|
||||
'overlay' : (int,), #integer (2) - range is 0..7. See Overlay icon types for values
|
||||
'cast' : (list,),
|
||||
'castandrole' : (list,tuple),
|
||||
'director' : (str,list),
|
||||
'mpaa' : (str,),
|
||||
'plot' : (str,),
|
||||
'plotoutline' : (str,),
|
||||
'title' : (str,),
|
||||
'originaltitle' : (str,),
|
||||
'sorttitle' : (str,),
|
||||
'duration' : (int,), #integer (245) - duration in seconds
|
||||
'studio' : (str,list),
|
||||
'tagline' : (str,),
|
||||
'writer' : (str,list),
|
||||
'tvshowtitle' : (str,list),
|
||||
'premiered' : (str,), #string (2005-03-04)
|
||||
'status' : (str,),
|
||||
'set' : (str,),
|
||||
'setoverview' : (str,),
|
||||
'tag' : (str,list),
|
||||
'imdbnumber' : (str,), #string (tt0110293) - IMDb code
|
||||
'code' : (str,), #string (101) - Production code
|
||||
'aired' : (str,), #string (2008-12-07)
|
||||
'credits' : (str,list),
|
||||
'lastplayed' : (str,), #string (Y-m-d h:m:s = 2009-04-05 23:16:04)
|
||||
'album' : (str,),
|
||||
'artist' : (list,),
|
||||
'votes' : (str,),
|
||||
'path' : (str,),
|
||||
'trailer' : (str,),
|
||||
'dateadded' : (str,), #string (Y-m-d h:m:s = 2009-04-05 23:16:04)
|
||||
'mediatype' : (str,), #mediatype string - "video", "movie", "tvshow", "season", "episode" or "musicvideo"
|
||||
'dbid' : (int,), #integer (23) - Only add this for items which are part of the local db. You also need to set the correct 'mediatype'!
|
||||
'count' : (int,), #integer (12) - can be used to store an id for later, or for sorting purposes
|
||||
# 'size' : (int,), #long (1024) - size in bytes
|
||||
'date' : (str,),} #string (d.m.Y / 01.01.2009) - file date
|
||||
@@ -0,0 +1,68 @@
|
||||
# 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 manager import Manager
|
||||
|
||||
class Create:
|
||||
def __init__(self, sysARG: dict={}, listitem: xbmcgui.ListItem=xbmcgui.ListItem(), fitem: dict={}):
|
||||
log('Create: __init__, sysARG = %s, fitem = %s\npath = %s'%(sysARG,fitem,listitem.getPath()))
|
||||
self.sysARG = sysARG
|
||||
self.fitem = fitem
|
||||
self.listitem = listitem
|
||||
|
||||
|
||||
def add(self):
|
||||
if not self.listitem.getPath(): return DIALOG.notificationDialog(LANGUAGE(32030))
|
||||
elif DIALOG.yesnoDialog('Would you like to add:\n[B]%s[/B]\nto the first available %s channel?'%(self.listitem.getLabel(),ADDON_NAME)):
|
||||
if not PROPERTIES.isRunning('Create.add'):
|
||||
with PROPERTIES.chkRunning('Create.add'), BUILTIN.busy_dialog(), PROPERTIES.interruptActivity():
|
||||
manager = Manager(MANAGER_XML, ADDON_PATH, "default", start=False, channel=-1)
|
||||
channelData = manager.newChannel
|
||||
channelData['type'] = 'Custom'
|
||||
channelData['favorite'] = True
|
||||
channelData['number'] = manager.getFirstAvailChannel()
|
||||
channelData['radio'] = True if self.listitem.getPath().startswith('musicdb://') else False
|
||||
channelData['name'], channelData = manager.validateLabel(cleanLabel(self.listitem.getLabel()),channelData)
|
||||
path, channelData = manager.validatePath(unquoteString(self.listitem.getPath()),channelData,spinner=False)
|
||||
if path is None: return
|
||||
channelData['path'] = [path.strip('/')]
|
||||
channelData['id'] = getChannelID(channelData['name'], channelData['path'], channelData['number'])
|
||||
manager.channels.addChannel(channelData)
|
||||
manager.channels.setChannels()
|
||||
PROPERTIES.setUpdateChannels(channelData['id'])
|
||||
manager.closeManager()
|
||||
del manager
|
||||
manager = Manager(MANAGER_XML, ADDON_PATH, "default", channel=channelData['number'])
|
||||
del manager
|
||||
|
||||
|
||||
def open(self):
|
||||
if not PROPERTIES.isRunning('Create.open'):
|
||||
with PROPERTIES.chkRunning('Create.open'), BUILTIN.busy_dialog(), PROPERTIES.interruptActivity():
|
||||
manager = Manager(MANAGER_XML, ADDON_PATH, "default", channel=self.fitem.get('citem',{}).get('number',1))
|
||||
del manager
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
log('Create: __main__, param = %s'%(sys.argv))
|
||||
try: mode = sys.argv[1]
|
||||
except: mode = ''
|
||||
if mode == 'manage': Create(sys.argv,listitem=sys.listitem,fitem=decodePlot(BUILTIN.getInfoLabel('Plot'))).open()
|
||||
else: Create(sys.argv,listitem=sys.listitem,fitem=decodePlot(BUILTIN.getInfoLabel('Plot'))).add()
|
||||
@@ -0,0 +1,87 @@
|
||||
# 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 seasonal import Seasonal
|
||||
|
||||
class Info:
|
||||
def __init__(self, sysARG: dict={}, listitem: xbmcgui.ListItem=xbmcgui.ListItem(), fitem: dict={}):
|
||||
with BUILTIN.busy_dialog():
|
||||
log('Info: __init__, sysARG = %s'%(sysARG))
|
||||
listitem = LISTITEMS.buildItemListItem(fitem,fitem.get('media','video'),oscreen=True)
|
||||
DIALOG.infoDialog(listitem)
|
||||
|
||||
class Browse:
|
||||
def __init__(self, sysARG: dict={}, listitem: xbmcgui.ListItem=xbmcgui.ListItem(), fitem: dict={}):
|
||||
log('Browse: __init__, sysARG = %s'%(sysARG))
|
||||
with BUILTIN.busy_dialog():
|
||||
media = '%ss'%(fitem.get('media','video'))
|
||||
path = fitem.get('citem',{}).get('path')
|
||||
if isinstance(path,list): path = path[0]
|
||||
if '?xsp=' in path:
|
||||
path, params = path.split('?xsp=')
|
||||
path = '%s?xsp=%s'%(path,quoteString(unquoteString(params)))
|
||||
#todo create custom container window with channel listitems.
|
||||
log('Browse: target = %s, path = %s'%(media,path))
|
||||
BUILTIN.executebuiltin('ReplaceWindow(%s,%s,return)'%(media,path))
|
||||
|
||||
class Match:
|
||||
SEARCH_SCRIPT = None
|
||||
GLOBAL_SCRIPT = 'script.globalsearch'
|
||||
SIMILAR_SCRIPT = 'script.embuary.helper'
|
||||
|
||||
def __init__(self, sysARG: dict={}, listitem: xbmcgui.ListItem=xbmcgui.ListItem(), fitem: dict={}):
|
||||
with BUILTIN.busy_dialog():
|
||||
title = BUILTIN.getInfoLabel('Title')
|
||||
name = BUILTIN.getInfoLabel('EpisodeName')
|
||||
dbtype = fitem.get('type').replace('episodes','tvshow').replace('tvshows','tvshow').replace('movies','movie')
|
||||
dbid = (fitem.get('tvshowid') or fitem.get('movieid'))
|
||||
log('Match: __init__, sysARG = %s, title = %s, dbtype = %s, dbid = %s'%(sysARG,'%s - %s'%(title,name),dbtype,dbid))
|
||||
|
||||
if hasAddon(self.SIMILAR_SCRIPT,install=True) and dbid:
|
||||
self.SEARCH_SCRIPT = self.SIMILAR_SCRIPT
|
||||
elif hasAddon(self.GLOBAL_SCRIPT,install=True):
|
||||
self.SEARCH_SCRIPT = self.GLOBAL_SCRIPT
|
||||
else:
|
||||
DIALOG.notificationDialog(LANGUAGE(32000))
|
||||
log('Match: SEARCH_SCRIPT = %s'%(self.SEARCH_SCRIPT))
|
||||
hasAddon(self.SEARCH_SCRIPT,enable=True)
|
||||
|
||||
if self.SEARCH_SCRIPT == self.SIMILAR_SCRIPT:
|
||||
# plugin://script.embuary.helper/?info=getsimilar&dbid=$INFO[ListItem.DBID]&type=tvshow&tag=HDR
|
||||
# plugin://script.embuary.helper/?info=getsimilar&dbid=$INFO[ListItem.DBID]&type=movie&tag=HDR
|
||||
# tag = optional, additional filter option to filter by library tag
|
||||
BUILTIN.executebuiltin('ReplaceWindow(%s,%s,return)'%('%ss'%(fitem.get('media','video')),'plugin://%s/?info=getsimilar&dbid=%d&type=%s'%(self.SEARCH_SCRIPT,dbid,dbtype)))
|
||||
else:
|
||||
# - the addon is executed by another addon/skin: RunScript(script.globalsearch,searchstring=foo)
|
||||
# You can specify which categories should be searched (this overrides the user preferences set in the addon settings):
|
||||
# RunScript(script.globalsearch,movies=true)
|
||||
# RunScript(script.globalsearch,tvshows=true&musicvideos=true&songs=true)
|
||||
# availableeoptions: movies, tvshows, episodes, musicvideos, artists, albums, songs, livetv, actors, directors
|
||||
BUILTIN.executebuiltin('RunScript(%s)'%('%s,searchstring=%s'%(self.SEARCH_SCRIPT,escapeString('%s,movies=True,episodes=True,tvshows=True,livetv=True'%(quoteString(title))))))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
param = sys.argv[1]
|
||||
log('Info: __main__, param = %s'%(param))
|
||||
if param == 'info': Info(sys.argv ,listitem=sys.listitem,fitem=decodePlot(BUILTIN.getInfoLabel('Plot')))
|
||||
elif param == 'browse': Browse(sys.argv,listitem=sys.listitem,fitem=decodePlot(BUILTIN.getInfoLabel('Plot')))
|
||||
elif param == 'match': Match(sys.argv ,listitem=sys.listitem,fitem=decodePlot(BUILTIN.getInfoLabel('Plot')))
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# 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 plugin import Plugin
|
||||
|
||||
def run(sysARG, fitem: dict={}, nitem: dict={}):
|
||||
with BUILTIN.busy_dialog():
|
||||
mode = sysARG[1]
|
||||
params = {}
|
||||
params['fitem'] = fitem
|
||||
params['nitem'] = nitem
|
||||
params['vid'] = decodeString(params.get("vid",''))
|
||||
params["chid"] = (params.get("chid") or fitem.get('citem',{}).get('id'))
|
||||
params['title'] = (params.get('title') or BUILTIN.getInfoLabel('label'))
|
||||
params['name'] = (unquoteString(params.get("name",'')) or fitem.get('citem',{}).get('name') or BUILTIN.getInfoLabel('ChannelName'))
|
||||
params['isPlaylist'] = (mode == 'playlist')
|
||||
log("Context_Play: run, params = %s"%(params))
|
||||
|
||||
if mode == 'play': threadit(Plugin(sysARG, sysInfo=params).playTV)(params["name"],params["chid"])
|
||||
elif mode == 'playlist': threadit(Plugin(sysARG, sysInfo=params).playPlaylist)(params["name"],params["chid"])
|
||||
|
||||
if __name__ == '__main__': run(sys.argv, fitem=decodePlot(BUILTIN.getInfoLabel('Plot')), nitem=decodePlot(BUILTIN.getInfoLabel('NextPlot')))
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# 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 m3u import M3U
|
||||
from xmltvs import XMLTVS
|
||||
|
||||
class Record:
|
||||
def __init__(self, sysARG: dict={}, listitem: xbmcgui.ListItem=xbmcgui.ListItem(), fitem: dict={}):
|
||||
with BUILTIN.busy_dialog():
|
||||
log('Record: __init__, sysARG = %s, fitem = %s\npath = %s'%(sysARG,fitem,listitem.getPath()))
|
||||
self.sysARG = sysARG
|
||||
self.fitem = fitem
|
||||
self.listitem = listitem
|
||||
self.fitem['label'] = (fitem.get('label') or listitem.getLabel())
|
||||
|
||||
|
||||
def add(self):
|
||||
if not PROPERTIES.isRunning('Record.add'):
|
||||
with PROPERTIES.chkRunning('Record.add'):
|
||||
now = timeString2Seconds(BUILTIN.getInfoLabel('Time(hh:mm:ss)','System'))
|
||||
start = timeString2Seconds(BUILTIN.getInfoLabel('StartTime').split(' ')[0] +':00')
|
||||
stop = timeString2Seconds(BUILTIN.getInfoLabel('EndTime').split(' ')[0] +':00')
|
||||
if (now > start and now < stop):
|
||||
opt ='Incl. Resume'
|
||||
seek = (now - start) - OSD_TIMER #add rollback buffer
|
||||
msg = '%s or %s'%(LANGUAGE(30119),LANGUAGE(30152))
|
||||
else:
|
||||
opt = ''
|
||||
seek = 0
|
||||
msg = LANGUAGE(30119)
|
||||
retval = DIALOG.yesnoDialog('Would you like to add:\n[B]%s[/B]\nto %s recordings?'%(self.fitem['label'],msg),customlabel=opt)
|
||||
if retval or int(retval) > 0:
|
||||
with BUILTIN.busy_dialog(), PROPERTIES.interruptActivity():
|
||||
m3u = M3U()
|
||||
ritem = m3u.getRecordItem(self.fitem, seek)
|
||||
added = (m3u.addRecording(ritem), XMLTVS().addRecording(ritem,self.fitem))
|
||||
del m3u
|
||||
if added:
|
||||
log('Record: add, ritem = %s'%(ritem))
|
||||
DIALOG.notificationDialog('%s\n%s'%(ritem['label'],LANGUAGE(30116)))
|
||||
PROPERTIES.setPropTimer('chkPVRRefresh')
|
||||
else: DIALOG.notificationDialog(LANGUAGE(32000))
|
||||
|
||||
|
||||
def remove(self):
|
||||
if not PROPERTIES.isRunning('Record.remove'):
|
||||
with PROPERTIES.chkRunning('Record.remove'):
|
||||
if DIALOG.yesnoDialog('Would you like to remove:\n[B]%s[/B]\nfrom recordings?'%(self.fitem['label'])):
|
||||
with BUILTIN.busy_dialog(), PROPERTIES.interruptActivity():
|
||||
ritem = (self.fitem.get('citem') or {"name":self.fitem['label'],"path":self.listitem.getPath()})
|
||||
removed = (M3U().delRecording(ritem), XMLTVS().delRecording(ritem))
|
||||
if removed:
|
||||
log('Record: remove, ritem = %s'%(ritem))
|
||||
DIALOG.notificationDialog('%s\n%s'%(ritem['name'],LANGUAGE(30118)))
|
||||
PROPERTIES.setPropTimer('chkPVRRefresh')
|
||||
else: DIALOG.notificationDialog(LANGUAGE(32000))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try: param = sys.argv[1]
|
||||
except: param = None
|
||||
log('Record: __main__, param = %s'%(param))
|
||||
if param == 'add': Record(sys.argv,listitem=sys.listitem,fitem=decodePlot(BUILTIN.getInfoLabel('Plot'))).add()
|
||||
elif param == 'del': Record(sys.argv,listitem=sys.listitem,fitem=decodePlot(BUILTIN.getInfoLabel('Plot'))).remove()
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
# 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 pool import ExecutorPool
|
||||
from collections import defaultdict
|
||||
|
||||
class LlNode:
|
||||
def __init__(self, package: tuple, priority: int=0, delay: int=0):
|
||||
self.prev = None
|
||||
self.next = None
|
||||
self.package = package
|
||||
self.priority = priority
|
||||
self.wait = delay
|
||||
|
||||
|
||||
class CustomQueue:
|
||||
isRunning = False
|
||||
|
||||
def __init__(self, fifo: bool=False, lifo: bool=False, priority: bool=False, delay: bool=False, service=None):
|
||||
self.log("__init__, fifo = %s, lifo = %s, priority = %s, delay = %s"%(fifo, lifo, priority, delay))
|
||||
self.service = service
|
||||
self.lock = Lock()
|
||||
self.fifo = fifo
|
||||
self.lifo = lifo
|
||||
self.priority = priority
|
||||
self.delay = delay
|
||||
self.head = None
|
||||
self.tail = None
|
||||
self.qsize = 0
|
||||
self.min_heap = []
|
||||
self.itemCount = defaultdict(int)
|
||||
self.popThread = Thread(target=self.__pop)
|
||||
self.pool = ExecutorPool()
|
||||
self.executor = SETTINGS.getSettingBool('Enable_Executors')
|
||||
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
return log('%s: %s'%(self.__class__.__name__,msg),level)
|
||||
|
||||
|
||||
def __manage(self, thread, target_function, name, daemon=True):
|
||||
if thread.is_alive():
|
||||
if hasattr(thread, 'cancel'): thread.cancel()
|
||||
try: thread.join()
|
||||
except Exception: pass
|
||||
|
||||
new_thread = Thread(target=target_function)
|
||||
new_thread.name = name
|
||||
new_thread.daemon = daemon
|
||||
new_thread.start()
|
||||
return new_thread
|
||||
|
||||
|
||||
def __start(self):
|
||||
self.log("__starting popThread")
|
||||
self.popThread = self.__manage(self.popThread, self.__pop, "popThread")
|
||||
|
||||
|
||||
def __run(self, func, *args, **kwargs):
|
||||
self.log(f"__run, func = {func.__name__}, executor = {self.executor}")
|
||||
try:
|
||||
if self.executor:
|
||||
return self.pool.executor(func, None, *args, **kwargs)
|
||||
else:
|
||||
thread = Thread(target=func, args=args, kwargs=kwargs)
|
||||
thread.start()
|
||||
except Exception as e:
|
||||
self.log(f"__run, func = {func.__name__} failed! {e}\nargs = {args}, kwargs = {kwargs}", xbmc.LOGERROR)
|
||||
|
||||
|
||||
def __exists(self, priority, package):
|
||||
for idx, item in enumerate(self.min_heap):
|
||||
epriority,_,epackage = item
|
||||
if package == epackage:
|
||||
if priority < epriority:
|
||||
try:
|
||||
self.min_heap.pop(idx)
|
||||
heapq.heapify(self.min_heap) # Ensure heap property is maintained
|
||||
self.log("__exists, replacing queue: func = %s, priority %s => %s"%(epackage[0].__name__,epriority,priority))
|
||||
except: self.log("__exists, replacing queue: func = %s, idx = %s failed!"%(epackage[0].__name__,idx))
|
||||
else: return True
|
||||
return False
|
||||
|
||||
|
||||
def _push(self, package: tuple, priority: int = 0, delay: int = 0):
|
||||
node = LlNode(package, priority, delay)
|
||||
if self.priority:
|
||||
if not self.__exists(priority, package):
|
||||
try:
|
||||
self.qsize += 1
|
||||
self.itemCount[priority] += 1
|
||||
self.log(f"_push, func = {package[0].__name__}, priority = {priority}")
|
||||
heapq.heappush(self.min_heap, (priority, self.itemCount[priority], package))
|
||||
except Exception as e:
|
||||
self.log(f"_push, func = {package[0].__name__} failed! {e}", xbmc.LOGFATAL)
|
||||
else:
|
||||
if self.head:
|
||||
self.tail.next = node
|
||||
node.prev = self.tail
|
||||
self.tail = node
|
||||
else:
|
||||
self.head = node
|
||||
self.tail = node
|
||||
self.log(f"_push, func = {package[0].__name__}")
|
||||
|
||||
if not self.isRunning:
|
||||
self.log("_push, starting __pop")
|
||||
self.__start()
|
||||
|
||||
|
||||
def __process(self, node, fifo=True):
|
||||
package = node.package
|
||||
self.log(f"process_node, package = {package}")
|
||||
next_node = node.__next__ if fifo else node.prev
|
||||
if next_node: next_node.prev = None if fifo else next_node.prev
|
||||
if node.prev: node.prev.next = None if fifo else node.prev
|
||||
if fifo: self.head = next_node
|
||||
else: self.tail = next_node
|
||||
return package
|
||||
|
||||
|
||||
def __pop(self):
|
||||
self.isRunning = True
|
||||
self.log("__pop, starting")
|
||||
self.executor = SETTINGS.getSettingBool('Enable_Executors')
|
||||
while not self.service.monitor.abortRequested():
|
||||
if self.service.monitor.waitForAbort(0.0001):
|
||||
self.log("__pop, waitForAbort")
|
||||
break
|
||||
elif self.service._interrupt():
|
||||
self.log("__pop, _interrupt")
|
||||
break
|
||||
elif self.service._suspend():
|
||||
self.log("__pop, _suspend")
|
||||
self.service.monitor.waitForAbort(SUSPEND_TIMER)
|
||||
continue
|
||||
elif not self.head and not self.priority:
|
||||
self.log("__pop, The queue is empty!")
|
||||
break
|
||||
elif self.priority:
|
||||
if not self.min_heap:
|
||||
self.log("__pop, The priority queue is empty!")
|
||||
break
|
||||
else:
|
||||
try: priority, _, package = heapq.heappop(self.min_heap)
|
||||
except Exception as e: continue
|
||||
self.qsize -= 1
|
||||
self.__run(package[0],*package[1],**package[2])
|
||||
elif self.fifo or self.lifo:
|
||||
curr_node = self.head if self.fifo else self.tail
|
||||
if curr_node is None:
|
||||
break
|
||||
else:
|
||||
package = self.__process(curr_node, fifo=self.fifo)
|
||||
if not self.delay: self.__run(*package)
|
||||
else: timerit(curr_node.wait, [*package])
|
||||
else:
|
||||
self.log("__pop, queue undefined!")
|
||||
break
|
||||
|
||||
self.isRunning = False
|
||||
self.log("__pop, finished: shutting down!")
|
||||
|
||||
|
||||
# def quePriority(package: tuple, priority: int=0):
|
||||
# q_priority = CustomQueue(priority=True)
|
||||
# q_priority.log("quePriority")
|
||||
# q_priority._push(package, priority)
|
||||
|
||||
# def queFIFO(package: tuple, delay: int=0):
|
||||
# q_fifo = CustomQueue(fifo=True, delay=bool(delay))
|
||||
# q_fifo.log("queFIFO")
|
||||
# q_fifo._push(package, delay)
|
||||
|
||||
# def queLIFO(package: tuple, delay: int=0):
|
||||
# q_lifo = CustomQueue(lifo=True, delay=bool(delay))
|
||||
# q_lifo.log("queLIFO")
|
||||
# q_lifo._push(package, delay)
|
||||
|
||||
# def queThread(packages, delay=0):
|
||||
# q_fifo = CustomQueue(fifo=True)
|
||||
# q_fifo.log("queThread")
|
||||
|
||||
# def thread_function(*package):
|
||||
# q_fifo._push(package)
|
||||
|
||||
# for package in packages:
|
||||
# t = Thread(target=thread_function, args=(package))
|
||||
# t.daemon = True
|
||||
# t.start()
|
||||
@@ -0,0 +1,37 @@
|
||||
# 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 *
|
||||
|
||||
class Channel():
|
||||
def __init__(self, id: str="", type: str="", number: int=0, name: str="", logo: str="", path: list=[], group: list=[], rules: list=[], catchup: str="vod", radio: bool=False, favorite: bool=False):
|
||||
self.id = id
|
||||
self.type = type
|
||||
self.number = number
|
||||
self.name = name
|
||||
self.logo = logo
|
||||
self.path = path
|
||||
self.group = group
|
||||
self.rules = rules
|
||||
self.catchup = catchup
|
||||
self.radio = radio
|
||||
self.favorite = favorite
|
||||
|
||||
|
||||
#todo convert json to dataclasses https://dataclass-wizard.readthedocs.io/en/latest/
|
||||
@@ -0,0 +1,137 @@
|
||||
from dataclasses import asdict
|
||||
from typing import List, Dict
|
||||
import random
|
||||
from operator import itemgetter
|
||||
from globals import getJSON, setJSON, SETTINGS, PROPERTIES, LANGUAGE, xbmc, log
|
||||
|
||||
@dataclass
|
||||
class Channel:
|
||||
id: str
|
||||
type: str
|
||||
number: int
|
||||
name: str
|
||||
logo: str
|
||||
path: List[str] = field(default_factory=list)
|
||||
group: List[str] = field(default_factory=list)
|
||||
rules: Dict = field(default_factory=dict)
|
||||
catchup: str = "vod"
|
||||
radio: bool = False
|
||||
favorite: bool = False
|
||||
|
||||
class Channels:
|
||||
def __init__(self):
|
||||
self.channelDATA: Dict[str, List[Channel]] = getJSON(CHANNELFLE_DEFAULT)
|
||||
self.channelTEMP: Dict = getJSON(CHANNEL_ITEM)
|
||||
self.channelDATA.update(self._load())
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
return log('%s: %s' % (self.__class__.__name__, msg), level)
|
||||
|
||||
def _load(self, file=CHANNELFLEPATH) -> Dict[str, List[Channel]]:
|
||||
channelDATA = getJSON(file)
|
||||
self.log('_load, channels = %s' % (len(channelDATA.get('channels', []))))
|
||||
return channelDATA
|
||||
|
||||
def _verify(self, channels: List[Channel] = []):
|
||||
for idx, citem in enumerate(self.channelDATA.get('channels', [])):
|
||||
if not citem.name or not citem.id or len(citem.path) == 0:
|
||||
self.log('_verify, in-valid citem [%s]\n%s' % (citem.id, citem))
|
||||
continue
|
||||
else:
|
||||
yield citem
|
||||
|
||||
def _save(self, file=CHANNELFLEPATH) -> bool:
|
||||
self.channelDATA['uuid'] = SETTINGS.getMYUUID()
|
||||
self.channelDATA['channels'] = self.sortChannels(self.channelDATA['channels'])
|
||||
self.log('_save, channels = %s' % (len(self.channelDATA['channels'])))
|
||||
return setJSON(file, self.channelDATA)
|
||||
|
||||
def getTemplate(self) -> Dict:
|
||||
return self.channelTEMP.copy()
|
||||
|
||||
def getChannels(self) -> List[Channel]:
|
||||
return sorted(self.channelDATA['channels'], key=itemgetter('number'))
|
||||
|
||||
def popChannels(self, type: str, channels: List[Channel] = []) -> List[Channel]:
|
||||
return [self.channelDATA['channels'].pop(self.channelDATA['channels'].index(citem)) for citem in list([c for c in channels if c.type == type])]
|
||||
|
||||
def getCustom(self) -> List[Channel]:
|
||||
channels = self.getChannels()
|
||||
return list([citem for citem in channels if citem.number <= CHANNEL_LIMIT])
|
||||
|
||||
def getAutotuned(self) -> List[Channel]:
|
||||
channels = self.getChannels()
|
||||
return list([citem for citem in channels if citem.number > CHANNEL_LIMIT])
|
||||
|
||||
def getChannelbyID(self, id: str) -> List[Channel]:
|
||||
channels = self.getChannels()
|
||||
return list([c for c in channels if c.id == id])
|
||||
|
||||
def getType(self, type: str) -> List[Channel]:
|
||||
channels = self.getChannels()
|
||||
return list([citem for citem in channels if citem.type == type])
|
||||
|
||||
def sortChannels(self, channels: List[Channel]) -> List[Channel]:
|
||||
try:
|
||||
return sorted(channels, key=itemgetter('number'))
|
||||
except:
|
||||
return channels
|
||||
|
||||
def setChannels(self, channels: List[Channel] = []) -> bool:
|
||||
if len(channels) == 0:
|
||||
channels = self.channelDATA['channels']
|
||||
self.channelDATA['channels'] = channels
|
||||
SETTINGS.setSetting('Select_Channels', '[B]%s[/B] Channels' % (len(channels)))
|
||||
PROPERTIES.setChannels(len(channels) > 0)
|
||||
return self._save()
|
||||
|
||||
def getImports(self) -> List:
|
||||
return self.channelDATA.get('imports', [])
|
||||
|
||||
def setImports(self, data: List = []) -> bool:
|
||||
self.channelDATA['imports'] = data
|
||||
return self.setChannels()
|
||||
|
||||
def clearChannels(self):
|
||||
self.channelDATA['channels'] = []
|
||||
|
||||
def delChannel(self, citem: Channel) -> bool:
|
||||
self.log('delChannel,[%s]' % (citem.id), xbmc.LOGINFO)
|
||||
idx, channel = self.findChannel(citem)
|
||||
if idx is not None:
|
||||
self.channelDATA['channels'].pop(idx)
|
||||
return True
|
||||
|
||||
def addChannel(self, citem: Channel) -> bool:
|
||||
idx, channel = self.findChannel(citem)
|
||||
if idx is not None:
|
||||
for key in ['id', 'rules', 'number', 'favorite', 'logo']:
|
||||
if getattr(channel, key):
|
||||
setattr(citem, key, getattr(channel, key)) # existing id found, reuse channel meta.
|
||||
|
||||
if citem.favorite:
|
||||
citem.group.append(LANGUAGE(32019))
|
||||
citem.group = sorted(set(citem.group))
|
||||
|
||||
self.log('addChannel, [%s] updating channel %s' % (citem.id, citem.name), xbmc.LOGINFO)
|
||||
self.channelDATA['channels'][idx] = citem
|
||||
else:
|
||||
self.log('addChannel, [%s] adding channel %s' % (citem.id, citem.name), xbmc.LOGINFO)
|
||||
self.channelDATA.setdefault('channels', []).append(citem)
|
||||
return True
|
||||
|
||||
def findChannel(self, citem: Channel, channels: List[Channel] = []) -> tuple:
|
||||
if len(channels) == 0:
|
||||
channels = self.getChannels()
|
||||
for idx, eitem in enumerate(channels):
|
||||
if citem.id == eitem.id:
|
||||
return idx, eitem
|
||||
return None, {}
|
||||
|
||||
def findAutotuned(self, citem: Channel, channels: List[Channel] = []) -> tuple:
|
||||
if len(channels) == 0:
|
||||
channels = self.getAutotuned()
|
||||
for idx, eitem in enumerate(channels):
|
||||
if citem.id == eitem.id or (citem.type == eitem.type and citem.name.lower() == eitem.name.lower()):
|
||||
return idx, eitem
|
||||
return None, {}
|
||||
@@ -0,0 +1,26 @@
|
||||
# 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 *
|
||||
|
||||
class Program():
|
||||
def __init__(self):
|
||||
...
|
||||
|
||||
#todo convert json to dataclasses https://dataclass-wizard.readthedocs.io/en/latest/
|
||||
@@ -0,0 +1,52 @@
|
||||
# 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 *
|
||||
|
||||
class Station():
|
||||
def __init__(self):
|
||||
self.id = id
|
||||
self.number = number
|
||||
self.name = name
|
||||
self.logo = logo
|
||||
self.group = group
|
||||
self.catchup = catchup
|
||||
self.radio = radio
|
||||
self.favorite = favorite
|
||||
self.realtime = realtime
|
||||
self.media = media
|
||||
self.label = label
|
||||
self.url = url
|
||||
self.tvg-shift = tvg-shift
|
||||
self.x-tvg-url = x-tvg-url
|
||||
self.media-dir = media-dir
|
||||
self.media-size = media-size
|
||||
self.media-type = media-type
|
||||
self.catchup-source = catchup-source
|
||||
self.catchup-days = catchup-days
|
||||
self.catchup-correction = catchup-correction
|
||||
self.provider = provider
|
||||
self.provider-type = provider-type
|
||||
self.provider-logo = provider-logo
|
||||
self.provider-countries = provider-countries
|
||||
self.provider-languages = provider-languages
|
||||
self.x-playlist-type = x-playlist-type
|
||||
self.kodiprops = kodiprops
|
||||
|
||||
#todo convert json to dataclasses https://dataclass-wizard.readthedocs.io/en/latest/
|
||||
@@ -0,0 +1,95 @@
|
||||
# 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 plugin import Plugin
|
||||
|
||||
def run(sysARG, fitem: dict={}, nitem: dict={}):
|
||||
"""
|
||||
Main entry point for PseudoTV Live's functionality.
|
||||
|
||||
This function handles various modes of playback and interaction based on the parameters passed.
|
||||
These modes include live TV, video-on-demand (VOD), DVR playback, guide display, and more.
|
||||
It also processes system arguments and settings to determine the appropriate behavior.
|
||||
|
||||
Args:
|
||||
sysARG (list): System arguments passed by the Kodi interface.
|
||||
fitem (dict, optional): Dictionary containing information about the current (featured) item. Defaults to an empty dictionary.
|
||||
nitem (dict, optional): Dictionary containing information about the next item. Defaults to an empty dictionary.
|
||||
|
||||
Behavior:
|
||||
- Parses system arguments and determines the mode of operation.
|
||||
- Depending on the mode, it invokes the appropriate plugin functionality (e.g., play live TV, VOD, DVR, etc.).
|
||||
- Utilizes utility functions like `threadit` for threading and `PROPERTIES` for managing app states.
|
||||
|
||||
Supported Modes:
|
||||
- 'live': Plays live TV or a playlist based on the provided parameters.
|
||||
- 'vod': Plays video-on-demand content.
|
||||
- 'dvr': Plays DVR recordings.
|
||||
- 'resume': Resumes paused playback.
|
||||
- 'broadcast': Simulates broadcast playback.
|
||||
- 'radio': Plays radio streams.
|
||||
- 'guide': Opens the TV guide using the PVR client.
|
||||
- 'settings': Opens the settings menu.
|
||||
|
||||
Notifications:
|
||||
- Displays notification dialogs for unsupported modes or errors.
|
||||
|
||||
"""
|
||||
with BUILTIN.busy_dialog(), PROPERTIES.suspendActivity():
|
||||
params = dict(urllib.parse.parse_qsl(sysARG[2][1:].replace('.pvr','')))
|
||||
mode = (params.get("mode") or 'guide')
|
||||
params['fitem'] = fitem
|
||||
params['nitem'] = nitem
|
||||
params['vid'] = decodeString(params.get("vid",''))
|
||||
params["chid"] = (params.get("chid") or fitem.get('citem',{}).get('id'))
|
||||
params['title'] = (params.get('title') or BUILTIN.getInfoLabel('label'))
|
||||
params['name'] = (unquoteString(params.get("name",'')) or BUILTIN.getInfoLabel('ChannelName'))
|
||||
params['isPlaylist'] = bool(SETTINGS.getSettingInt('Playback_Method'))
|
||||
log("Default: run, params = %s"%(params))
|
||||
|
||||
if PROPERTIES.isRunning('togglePVR'): DIALOG.notificationDialog(LANGUAGE(32166))
|
||||
elif mode == 'live':
|
||||
if params.get('start') == '{utc}':
|
||||
PROPERTIES.setPropTimer('chkPVRRefresh')
|
||||
params.update({'start':0,'stop':0,'duration':0})
|
||||
if params['isPlaylist']: threadit(Plugin(sysARG, sysInfo=params).playPlaylist)(params["name"],params["chid"])
|
||||
elif params['vid'] : threadit(Plugin(sysARG, sysInfo=params).playTV)(params["name"],params["chid"])
|
||||
elif params['isPlaylist']: threadit(Plugin(sysARG, sysInfo=params).playPlaylist)(params["name"],params["chid"])
|
||||
elif params['vid'] : threadit(Plugin(sysARG, sysInfo=params).playLive)(params["name"],params["chid"],params["vid"])
|
||||
else: threadit(Plugin(sysARG, sysInfo=params).playTV)(params["name"],params["chid"])
|
||||
elif mode == 'vod': threadit(Plugin(sysARG, sysInfo=params).playVOD)(params["title"],params["vid"])
|
||||
elif mode == 'dvr': threadit(Plugin(sysARG, sysInfo=params).playDVR)(params["title"],params["vid"])
|
||||
elif mode == 'resume': threadit(Plugin(sysARG, sysInfo=params).playPaused)(params["name"],params["chid"])
|
||||
elif mode == 'broadcast': threadit(Plugin(sysARG, sysInfo=params).playBroadcast)(params["name"],params["chid"],params["vid"])
|
||||
elif mode == 'radio': threadit(Plugin(sysARG, sysInfo=params).playRadio)(params["name"],params["chid"],params["vid"])
|
||||
elif mode == 'guide' and hasAddon(PVR_CLIENT_ID,install=True,enable=True): SETTINGS.openGuide()
|
||||
elif mode == 'settings' and hasAddon(PVR_CLIENT_ID,install=True,enable=True): SETTINGS.openSettings()
|
||||
else: DIALOG.notificationDialog(LANGUAGE(32000))
|
||||
MONITOR().waitForAbort(float(SETTINGS.getSettingInt('RPC_Delay')/1000)) #delay to avoid thread crashes when fast channel changing.
|
||||
|
||||
if __name__ == '__main__':
|
||||
"""
|
||||
Runs the script when executed as the main module.
|
||||
|
||||
It decodes information about the current and next items using the `decodePlot` function
|
||||
and then invokes the `run` function with the appropriate arguments.
|
||||
"""
|
||||
run(sys.argv, fitem=decodePlot(BUILTIN.getInfoLabel('Plot')), nitem=decodePlot(BUILTIN.getInfoLabel('NextPlot')))
|
||||
@@ -0,0 +1,382 @@
|
||||
# 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 *
|
||||
|
||||
#constants
|
||||
DEFAULT_ENCODING = "utf-8"
|
||||
FILE_LOCK_MAX_FILE_TIMEOUT = 10
|
||||
FILE_LOCK_NAME = "pseudotv"
|
||||
|
||||
class FileAccess:
|
||||
@staticmethod
|
||||
def open(filename, mode, encoding=DEFAULT_ENCODING):
|
||||
fle = 0
|
||||
try: return VFSFile(filename, mode)
|
||||
except UnicodeDecodeError: return FileAccess.open(filename, mode, encoding)
|
||||
return fle
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _getFolderPath(path):
|
||||
head, tail = os.path.split(path)
|
||||
last_folder = os.path.basename(head)
|
||||
return os.path.join(last_folder, tail)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def listdir(path):
|
||||
return xbmcvfs.listdir(path)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def translatePath(path):
|
||||
if '@' in path: path = path.split('@')[1]
|
||||
return xbmcvfs.translatePath(path)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def copyFolder(src, dir, dia=None, delete=False):
|
||||
log('FileAccess: copyFolder %s to %s'%(src,dir))
|
||||
if not FileAccess.exists(dir): FileAccess.makedirs(dir)
|
||||
if dia:
|
||||
from kodi import Dialog
|
||||
DIALOG = Dialog()
|
||||
|
||||
subs, files = FileAccess.listdir(src)
|
||||
pct = 0
|
||||
if dia: dia = DIALOG.progressDialog(pct, control=dia, message='%s\n%s'%(LANGUAGE(32051),src))
|
||||
for fidx, file in enumerate(files):
|
||||
if dia: dia = DIALOG.progressDialog(pct, control=dia, message='%s: (%s%)\n%s'%(LANGUAGE(32051),(int(fidx*100)//len(files)),FileAccess._getFolderPath(file)))
|
||||
if delete: FileAccess.move(os.path.join(src, file), os.path.join(dir, file))
|
||||
else: FileAccess.copy(os.path.join(src, file), os.path.join(dir, file))
|
||||
|
||||
for sidx, sub in enumerate(subs):
|
||||
pct = int(sidx)//len(subs)
|
||||
FileAccess.copyFolder(os.path.join(src, sub), os.path.join(dir, sub), dia, delete)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def copy(orgfilename, newfilename):
|
||||
log('FileAccess: copying %s to %s'%(orgfilename,newfilename))
|
||||
dir, file = os.path.split(newfilename)
|
||||
if not FileAccess.exists(dir): FileAccess.makedirs(dir)
|
||||
return xbmcvfs.copy(orgfilename, newfilename)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def move(orgfilename, newfilename):
|
||||
log('FileAccess: moving %s to %s'%(orgfilename,newfilename))
|
||||
if FileAccess.copy(orgfilename, newfilename):
|
||||
return FileAccess.delete(orgfilename)
|
||||
return False
|
||||
|
||||
|
||||
@staticmethod
|
||||
def delete(filename):
|
||||
return xbmcvfs.delete(filename)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def exists(filename):
|
||||
if filename.startswith('stack://'):
|
||||
try: filename = (filename.split('stack://')[1].split(' , '))[0]
|
||||
except Exception as e: log('FileAccess: exists failed! %s'%(e), xbmc.LOGERROR)
|
||||
try:
|
||||
return xbmcvfs.exists(filename)
|
||||
except UnicodeDecodeError:
|
||||
return os.path.exists(xbmcvfs.translatePath(filename))
|
||||
return False
|
||||
|
||||
|
||||
@staticmethod
|
||||
def openSMB(filename, mode, encoding=DEFAULT_ENCODING):
|
||||
fle = 0
|
||||
if os.name.lower() == 'nt':
|
||||
newname = '\\\\' + filename[6:]
|
||||
try: fle = codecs.open(newname, mode, encoding)
|
||||
except: fle = 0
|
||||
return fle
|
||||
|
||||
|
||||
@staticmethod
|
||||
def existsSMB(filename):
|
||||
if os.name.lower() == 'nt':
|
||||
filename = '\\\\' + filename[6:]
|
||||
return FileAccess.exists(filename)
|
||||
return False
|
||||
|
||||
|
||||
@staticmethod
|
||||
def rename(path, newpath):
|
||||
log("FileAccess: rename %s to %s"%(path,newpath))
|
||||
if not FileAccess.exists(path):
|
||||
return False
|
||||
|
||||
try:
|
||||
if xbmcvfs.rename(path, newpath):
|
||||
return True
|
||||
except Exception as e:
|
||||
log("FileAccess: rename, failed! %s"%(e), xbmc.LOGERROR)
|
||||
|
||||
try:
|
||||
if FileAccess.move(path, newpath):
|
||||
return True
|
||||
except Exception as e:
|
||||
log("FileAccess: move, failed! %s"%(e), xbmc.LOGERROR)
|
||||
|
||||
if path[0:6].lower() == 'smb://' or newpath[0:6].lower() == 'smb://':
|
||||
if os.name.lower() == 'nt':
|
||||
log("FileAccess: Modifying name")
|
||||
if path[0:6].lower() == 'smb://':
|
||||
path = '\\\\' + path[6:]
|
||||
|
||||
if newpath[0:6].lower() == 'smb://':
|
||||
newpath = '\\\\' + newpath[6:]
|
||||
|
||||
if not os.path.exist(xbmcvfs.translatePath(path)):
|
||||
return False
|
||||
|
||||
try:
|
||||
log("FileAccess: os.rename")
|
||||
os.rename(xbmcvfs.translatePath(path), xbmcvfs.translatePath(newpath))
|
||||
return True
|
||||
except Exception as e:
|
||||
log("FileAccess: os.rename, failed! %s"%(e), xbmc.LOGERROR)
|
||||
|
||||
try:
|
||||
log("FileAccess: shutil.move")
|
||||
shutil.move(xbmcvfs.translatePath(path), xbmcvfs.translatePath(newpath))
|
||||
return True
|
||||
except Exception as e:
|
||||
log("FileAccess: shutil.move, failed! %s"%(e), xbmc.LOGERROR)
|
||||
|
||||
log("FileAccess: OSError")
|
||||
raise OSError()
|
||||
|
||||
|
||||
@staticmethod
|
||||
def removedirs(path, force=True):
|
||||
if len(path) == 0: return False
|
||||
elif(xbmcvfs.exists(path)):
|
||||
return True
|
||||
try:
|
||||
success = xbmcvfs.rmdir(dir, force=force)
|
||||
if success: return True
|
||||
else: raise
|
||||
except:
|
||||
try:
|
||||
os.rmdir(xbmcvfs.translatePath(path))
|
||||
if os.path.exists(xbmcvfs.translatePath(path)):
|
||||
return True
|
||||
except: log("FileAccess: removedirs failed!", xbmc.LOGERROR)
|
||||
return False
|
||||
|
||||
|
||||
@staticmethod
|
||||
def makedirs(directory):
|
||||
try:
|
||||
os.makedirs(xbmcvfs.translatePath(directory))
|
||||
return os.path.exists(xbmcvfs.translatePath(directory))
|
||||
except:
|
||||
return FileAccess._makedirs(directory)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _makedirs(path):
|
||||
if len(path) == 0:
|
||||
return False
|
||||
|
||||
if(xbmcvfs.exists(path)):
|
||||
return True
|
||||
|
||||
success = xbmcvfs.mkdir(path)
|
||||
if success == False:
|
||||
if path == os.path.dirname(xbmcvfs.translatePath(path)):
|
||||
return False
|
||||
|
||||
if FileAccess._makedirs(os.path.dirname(xbmcvfs.translatePath(path))):
|
||||
return xbmcvfs.mkdir(path)
|
||||
return xbmcvfs.exists(path)
|
||||
|
||||
|
||||
class VFSFile:
|
||||
monitor = MONITOR()
|
||||
|
||||
def __init__(self, filename, mode):
|
||||
if mode == 'w':
|
||||
if not FileAccess.exists(filename):
|
||||
FileAccess.makedirs(os.path.split(filename)[0])
|
||||
self.currentFile = xbmcvfs.File(filename, 'wb')
|
||||
else:
|
||||
self.currentFile = xbmcvfs.File(filename, 'r')
|
||||
log("VFSFile: Opening %s"%filename, xbmc.LOGDEBUG)
|
||||
|
||||
if self.currentFile == None:
|
||||
log("VFSFile: Couldnt open %s"%filename, xbmc.LOGERROR)
|
||||
|
||||
|
||||
def read(self, bytes=0):
|
||||
try: return self.currentFile.read(bytes)
|
||||
except: return self.currentFile.readBytes(bytes)
|
||||
|
||||
|
||||
def readBytes(self, bytes=0):
|
||||
return self.currentFile.readBytes(bytes)
|
||||
|
||||
|
||||
def write(self, data):
|
||||
if isinstance(data,bytes):
|
||||
data = data.decode(DEFAULT_ENCODING, 'backslashreplace')
|
||||
return self.currentFile.write(data)
|
||||
|
||||
|
||||
def close(self):
|
||||
return self.currentFile.close()
|
||||
|
||||
|
||||
def seek(self, bytes, offset=1):
|
||||
return self.currentFile.seek(bytes, offset)
|
||||
|
||||
|
||||
def size(self):
|
||||
return self.currentFile.size()
|
||||
|
||||
|
||||
def readlines(self):
|
||||
return self.read().split('\n')
|
||||
# return list(self.readline())
|
||||
|
||||
|
||||
def readline(self):
|
||||
for line in self.read_in_chunks():
|
||||
yield line
|
||||
|
||||
|
||||
def tell(self):
|
||||
try: return self.currentFile.tell()
|
||||
except: return self.currentFile.seek(0, 1)
|
||||
|
||||
|
||||
def read_in_chunks(self, chunk_size=1024):
|
||||
"""Lazy function (generator) to read a file piece by piece."""
|
||||
while not self.monitor.abortRequested():
|
||||
data = self.read(chunk_size)
|
||||
if not data: break
|
||||
yield data
|
||||
|
||||
|
||||
class FileLock(object):
|
||||
monitor = MONITOR()
|
||||
|
||||
# https://github.com/dmfrey/FileLock
|
||||
""" A file locking mechanism that has context-manager support so
|
||||
you can use it in a with statement. This should be relatively cross
|
||||
compatible as it doesn't rely on msvcrt or fcntl for the locking.
|
||||
"""
|
||||
|
||||
def __init__(self, file_name=FILE_LOCK_NAME, timeout=FILE_LOCK_MAX_FILE_TIMEOUT, delay: float=0.5):
|
||||
""" Prepare the file locker. Specify the file to lock and optionally
|
||||
the maximum timeout and the delay between each attempt to lock.
|
||||
"""
|
||||
if timeout is not None and delay is None:
|
||||
raise ValueError("If timeout is not None, then delay must not be None.")
|
||||
|
||||
self.is_locked = False
|
||||
self.file_name = file_name
|
||||
self.lockpath = self.checkpath()
|
||||
self.lockfile = os.path.join(self.lockpath, "%s.lock" % self.file_name)
|
||||
self.timeout = timeout
|
||||
self.delay = delay
|
||||
|
||||
|
||||
def checkpath(self):
|
||||
lockpath = os.path.join(REAL_SETTINGS.getSetting('User_Folder'))
|
||||
if not FileAccess.exists(lockpath):
|
||||
if FileAccess.makedirs(lockpath):
|
||||
return lockpath
|
||||
else:#fallback to local folder.
|
||||
#todo log error with lock path
|
||||
lockpath = os.path.join(SETTINGS_LOC,'cache')
|
||||
if not FileAccess.exists(lockpath):
|
||||
FileAccess.makedirs(lockpath)
|
||||
return lockpath
|
||||
|
||||
|
||||
def acquire(self):
|
||||
""" Acquire the lock, if possible. If the lock is in use, it check again
|
||||
every `wait` seconds. It does this until it either gets the lock or
|
||||
exceeds `timeout` number of seconds, in which case it throws
|
||||
an exception.
|
||||
"""
|
||||
start_time = time.time()
|
||||
while not self.monitor.abortRequested():
|
||||
if self.monitor.waitForAbort(self.delay): break
|
||||
else:
|
||||
try:
|
||||
self.fd = FileAccess.open(self.lockfile, 'w')
|
||||
self.is_locked = True #moved to ensure tag only when locked
|
||||
break;
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
raise
|
||||
if self.timeout is None:
|
||||
raise FileLockException("Could not acquire lock on {}".format(self.file_name))
|
||||
if (time.time() - start_time) >= self.timeout:
|
||||
raise FileLockException("Timeout occured.")
|
||||
|
||||
|
||||
def release(self):
|
||||
""" Get rid of the lock by deleting the lockfile.
|
||||
When working in a `with` statement, this gets automatically
|
||||
called at the end.
|
||||
"""
|
||||
if self.is_locked:
|
||||
self.fd.close()
|
||||
self.is_locked = False
|
||||
|
||||
|
||||
def __enter__(self):
|
||||
""" Activated when used in the with statement.
|
||||
Should automatically acquire a lock to be used in the with block.
|
||||
"""
|
||||
if not self.is_locked:
|
||||
self.acquire()
|
||||
return self
|
||||
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
""" Activated at the end of the with statement.
|
||||
It automatically releases the lock if it isn't locked.
|
||||
"""
|
||||
if self.is_locked:
|
||||
self.release()
|
||||
|
||||
|
||||
def __del__(self):
|
||||
""" Make sure that the FileLock instance doesn't leave a lockfile
|
||||
lying around.
|
||||
"""
|
||||
self.release()
|
||||
FileAccess.delete(self.lockfile)
|
||||
|
||||
|
||||
class FileLockException(Exception):
|
||||
pass
|
||||
@@ -0,0 +1,209 @@
|
||||
# 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 resources import Resources
|
||||
|
||||
#Ratings - resource only, Movie Type only any channel type
|
||||
#Bumpers - plugin, path only, tv type, tv network, custom channel type
|
||||
#Adverts - plugin, path only, tv type, any tv channel type
|
||||
#Trailers - plug, path only, movie type, any movie channel.
|
||||
|
||||
class Fillers:
|
||||
def __init__(self, builder, citem={}):
|
||||
self.builder = builder
|
||||
self.bctTypes = builder.bctTypes
|
||||
self.runActions = builder.runActions
|
||||
self.jsonRPC = builder.jsonRPC
|
||||
self.cache = builder.jsonRPC.cache
|
||||
self.citem = citem
|
||||
self.resources = Resources(service=builder.service)
|
||||
self.log('[%s] __init__, bctTypes = %s'%(self.citem.get('id'),builder.bctTypes))
|
||||
self.fillSources(citem, builder.bctTypes)
|
||||
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
return log('%s: %s'%(self.__class__.__name__,msg),level)
|
||||
|
||||
|
||||
def fillSources(self, citem={}, bctTypes={}):
|
||||
for ftype, values in list(bctTypes.items()):
|
||||
if values.get('enabled',False):
|
||||
self.builder.updateProgress(self.builder.pCount,message='%s %s'%(LANGUAGE(30014),ftype.title()),header='%s, %s'%(ADDON_NAME,self.builder.pMSG))
|
||||
if values.get('incKODI',False): values["items"] = mergeDictLST(values.get('items',{}), self.builder.getTrailers())
|
||||
|
||||
for id in values["sources"].get("ids",[]):
|
||||
values['items'] = mergeDictLST(values.get('items',{}),self.buildSource(ftype,id)) #parse resource packs
|
||||
|
||||
for path in values["sources"].get("paths",[]):
|
||||
values['items'] = mergeDictLST(values.get('items',{}),self.buildSource(ftype,path)) #parse vfs paths
|
||||
|
||||
values['items'] = lstSetDictLst(values['items'])
|
||||
self.log('[%s] fillSources, type = %s, items = %s'%(self.citem.get('id'),ftype,len(values['items'])))
|
||||
|
||||
|
||||
@cacheit(expiration=datetime.timedelta(minutes=30),json_data=False)
|
||||
def buildSource(self, ftype, path=''):
|
||||
self.log('[%s] buildSource, type = %s, path = %s'%(self.citem.get('id'),ftype, path))
|
||||
def _parseResource(id):
|
||||
if hasAddon(id, install=True): return self.jsonRPC.walkListDirectory(os.path.join('special://home/addons/%s'%id,'resources'),exts=VIDEO_EXTS,depth=CHANNEL_LIMIT,checksum=SETTINGS.getAddonDetails(id).get('version',ADDON_VERSION),expiration=datetime.timedelta(days=MAX_GUIDEDAYS))
|
||||
|
||||
def _parseVFS(path):
|
||||
if path.startswith('plugin://'):
|
||||
if not hasAddon(path, install=True): return {}
|
||||
return self.jsonRPC.walkFileDirectory(path, depth=CHANNEL_LIMIT, chkDuration=True, retItem=True)
|
||||
|
||||
def _parseLocal(path):
|
||||
if FileAccess.exists(path): return self.jsonRPC.walkListDirectory(path,exts=VIDEO_EXTS,depth=CHANNEL_LIMIT,chkDuration=True)
|
||||
|
||||
def __sortItems(data, stype='folder'):
|
||||
tmpDCT = {}
|
||||
if data:
|
||||
for path, files in list(data.items()):
|
||||
if stype == 'file': key = file.split('.')[0].lower()
|
||||
elif stype == 'folder': key = (os.path.basename(os.path.normpath(path)).replace('\\','/').strip('/').split('/')[-1:][0]).lower()
|
||||
for file in files:
|
||||
if isinstance(file,dict): [tmpDCT.setdefault(key.lower(),[]).append(file) for key in (file.get('genre',[]) or ['resources'])]
|
||||
else:
|
||||
dur = self.jsonRPC.getDuration(os.path.join(path,file), accurate=True)
|
||||
if dur > 0: tmpDCT.setdefault(key.lower(),[]).append({'file':os.path.join(path,file),'duration':dur,'label':'%s - %s'%(path.strip('/').split('/')[-1:][0],file.split('.')[0])})
|
||||
self.log('[%s] buildSource, __sortItems: stype = %s, items = %s'%(self.citem.get('id'),stype,len(tmpDCT)))
|
||||
return tmpDCT
|
||||
|
||||
try:
|
||||
if path.startswith('resource.'): return __sortItems(_parseResource(path))
|
||||
elif path.startswith(tuple(VFS_TYPES+DB_TYPES)): return __sortItems(_parseVFS(path))
|
||||
else: return __sortItems(_parseLocal(path))
|
||||
except Exception as e: self.log("[%s] buildSource, failed! %s\n path = %s"%(self.citem.get('id'),e,path), xbmc.LOGERROR)
|
||||
return {}
|
||||
|
||||
|
||||
def convertMPAA(self, ompaa):
|
||||
try:
|
||||
ompaa = ompaa.upper()
|
||||
mpaa = re.compile(":(.*?)/", re.IGNORECASE).search(ompaa).group(1).strip()
|
||||
except: return ompaa
|
||||
mpaa = mpaa.replace('TV-Y','G').replace('TV-Y7','G').replace('TV-G','G').replace('NA','NR').replace('TV-PG','PG').replace('TV-14','PG-13').replace('TV-MA','R')
|
||||
return mpaa
|
||||
|
||||
#todo always add a bumper for pseudo/kodi (based on build ver.)
|
||||
# resource.videos.bumpers.kodi
|
||||
# resource.videos.bumpers.pseudotv
|
||||
|
||||
def getSingle(self, type, keys=['resources'], chance=False):
|
||||
items = [random.choice(tmpLST) for key in keys if (tmpLST := self.bctTypes.get(type, {}).get('items', {}).get(key.lower(), []))]
|
||||
if not items and chance:
|
||||
items.extend(self.getSingle(type))
|
||||
self.log('[%s] getSingle, type = %s, keys = %s, chance = %s, returning = %s' % (self.citem.get('id'),type, keys, chance, len(items)))
|
||||
return setDictLST(items)
|
||||
|
||||
|
||||
def getMulti(self, type, keys=['resources'], count=1, chance=False):
|
||||
items = []
|
||||
tmpLST = []
|
||||
for key in keys:
|
||||
tmpLST.extend(self.bctTypes.get(type, {}).get('items', {}).get(key.lower(), []))
|
||||
if len(tmpLST) >= count:
|
||||
items = random.sample(tmpLST, count)
|
||||
elif tmpLST:
|
||||
items = setDictLST(random.choices(tmpLST, k=count))
|
||||
if len(items) < count and chance:
|
||||
items.extend(self.getMulti(type, count=(count - len(items))))
|
||||
self.log('[%s] getMulti, type = %s, keys = %s, count = %s, chance = %s, returning = %s' % (self.citem.get('id'),type, keys, count, chance, len(items)))
|
||||
return setDictLST(items)
|
||||
|
||||
|
||||
def injectBCTs(self, fileList):
|
||||
nfileList = []
|
||||
for idx, fileItem in enumerate(fileList):
|
||||
if not fileItem: continue
|
||||
else:
|
||||
runtime = fileItem.get('duration',0)
|
||||
if runtime == 0: continue
|
||||
|
||||
chtype = self.citem.get('type','')
|
||||
chname = self.citem.get('name','')
|
||||
fitem = fileItem.copy()
|
||||
dbtype = fileItem.get('type','')
|
||||
fmpaa = (self.convertMPAA(fileItem.get('mpaa')) or 'NR')
|
||||
fcodec = (fileItem.get('streamdetails',{}).get('audio') or [{}])[0].get('codec','')
|
||||
fgenre = (fileItem.get('genre') or self.citem.get('group') or '')
|
||||
if isinstance(fgenre,list) and len(fgenre) > 0: fgenre = fgenre[0]
|
||||
|
||||
#pre roll - bumpers/ratings
|
||||
if dbtype.startswith(tuple(MOVIE_TYPES)):
|
||||
ftype = 'ratings'
|
||||
preKeys = [fmpaa, fcodec]
|
||||
elif dbtype.startswith(tuple(TV_TYPES)):
|
||||
ftype = 'bumpers'
|
||||
preKeys = [chname, fgenre]
|
||||
else:
|
||||
ftype = None
|
||||
|
||||
if ftype:
|
||||
preFileList = []
|
||||
if self.bctTypes[ftype].get('enabled',False) and chtype not in IGNORE_CHTYPE:
|
||||
preFileList.extend(self.getSingle(ftype, preKeys, chanceBool(self.bctTypes[ftype].get('chance',0))))
|
||||
|
||||
for i, item in enumerate(setDictLST(preFileList)):
|
||||
if (item.get('duration') or 0) > 0:
|
||||
runtime += item.get('duration')
|
||||
self.log('[%s] injectBCTs, adding pre-roll %s - %s'%(self.citem.get('id'),item.get('duration'),item.get('file')))
|
||||
self.builder.updateProgress(self.builder.pCount,message='Filling Pre-Rolls %s%%'%(int(i*100//len(preFileList))),header='%s, %s'%(ADDON_NAME,self.builder.pMSG))
|
||||
item.update({'title':'Pre-Roll','episodetitle':item.get('label'),'genre':['Pre-Roll'],'plot':item.get('plot',item.get('file')),'path':item.get('file')})
|
||||
nfileList.append(self.builder.buildCells(self.citem,item.get('duration'),entries=1,info=item)[0])
|
||||
|
||||
# original media
|
||||
nfileList.append(fileItem)
|
||||
self.log('[%s] injectBCTs, adding media %s - %s'%(self.citem.get('id'),fileItem.get('duration'),fileItem.get('file')))
|
||||
|
||||
# post roll - adverts/trailers
|
||||
postFileList = []
|
||||
for ftype in ['adverts','trailers']:
|
||||
postIgnoreTypes = {'adverts':IGNORE_CHTYPE + MOVIE_CHTYPE,'trailers':IGNORE_CHTYPE}[ftype]
|
||||
postFillRuntime = diffRuntime(runtime) if self.bctTypes[ftype]['auto'] else MIN_EPG_DURATION
|
||||
if self.bctTypes[ftype].get('enabled',False) and chtype not in postIgnoreTypes:
|
||||
postFileList.extend(self.getMulti(ftype, [chname, fgenre], self.bctTypes[ftype]['max'] if self.bctTypes[ftype]['auto'] else self.bctTypes[ftype]['min'], chanceBool(self.bctTypes[ftype].get('chance',0))))
|
||||
|
||||
postAuto = (self.bctTypes['adverts']['auto'] | self.bctTypes['trailers']['auto'])
|
||||
postCounter = 0
|
||||
if len(postFileList) > 0:
|
||||
i = 0
|
||||
postFileList = randomShuffle(postFileList)
|
||||
self.log('[%s] injectBCTs, post-roll current runtime %s, available runtime %s, available content %s'%(self.citem.get('id'),runtime, postFillRuntime,len(postFileList)))
|
||||
while not self.builder.service.monitor.abortRequested() and postFillRuntime > 0 and len(postFileList) > 0:
|
||||
if self.builder.service.monitor.waitForAbort(0.0001): break
|
||||
else:
|
||||
i += 1
|
||||
item = postFileList.pop(0)
|
||||
if (item.get('duration') or 0) == 0: continue
|
||||
elif postAuto and postCounter >= len(postFileList):
|
||||
self.log('[%s] injectBCTs, unused post roll runtime %s %s/%s'%(self.citem.get('id'),postFillRuntime,postCounter,len(postFileList)))
|
||||
break
|
||||
elif postFillRuntime >= item.get('duration'):
|
||||
postFillRuntime -= item.get('duration')
|
||||
self.log('[%s] injectBCTs, adding post-roll %s - %s'%(self.citem.get('id'),item.get('duration'),item.get('file')))
|
||||
self.builder.updateProgress(self.builder.pCount,message='Filling Post-Rolls %s%%'%(int(i*100//len(postFileList))),header='%s, %s'%(ADDON_NAME,self.builder.pMSG))
|
||||
item.update({'title':'Post-Roll','episodetitle':item.get('label'),'genre':['Post-Roll'],'plot':item.get('plot',item.get('file')),'path':item.get('file')})
|
||||
nfileList.append(self.builder.buildCells(self.citem,item.get('duration'),entries=1,info=item)[0])
|
||||
elif postFillRuntime < item.get('duration'):
|
||||
postFileList.append(item)
|
||||
postCounter += 1
|
||||
self.log('[%s] injectBCTs, finished'%(self.citem.get('id')))
|
||||
return nfileList
|
||||
@@ -0,0 +1,509 @@
|
||||
# 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 -*-
|
||||
|
||||
import os, sys, re, json, struct, errno, zlib
|
||||
import shutil, subprocess, io, platform
|
||||
import codecs, random
|
||||
import uuid, base64, binascii, hashlib
|
||||
import time, datetime, calendar
|
||||
import heapq, requests, pyqrcode
|
||||
import xml.sax.saxutils
|
||||
|
||||
from six.moves import urllib
|
||||
from io import StringIO, BytesIO
|
||||
from threading import Lock, Thread, Event, Timer, BoundedSemaphore
|
||||
from threading import enumerate as thread_enumerate
|
||||
from xml.dom.minidom import parse, parseString, Document
|
||||
from xml.etree.ElementTree import ElementTree, Element, SubElement, XMLParser, fromstringlist, fromstring, tostring
|
||||
from xml.etree.ElementTree import parse as ETparse
|
||||
from typing import Dict, List, Union, Optional
|
||||
|
||||
from variables import *
|
||||
from kodi_six import xbmc, xbmcaddon, xbmcplugin, xbmcgui, xbmcvfs
|
||||
from contextlib import contextmanager, closing
|
||||
from socket import gethostbyname, gethostname
|
||||
from itertools import cycle, chain, zip_longest, islice
|
||||
from xml.sax.saxutils import escape, unescape
|
||||
from operator import itemgetter
|
||||
|
||||
from logger import *
|
||||
from cache import Cache, cacheit
|
||||
from pool import killit, timeit, poolit, executeit, timerit, threadit
|
||||
from kodi import *
|
||||
from fileaccess import FileAccess, FileLock
|
||||
from collections import defaultdict, Counter, OrderedDict
|
||||
from six.moves import urllib
|
||||
from math import ceil, floor
|
||||
from infotagger.listitem import ListItemInfoTag
|
||||
from requests.adapters import HTTPAdapter, Retry
|
||||
|
||||
DIALOG = Dialog()
|
||||
PROPERTIES = DIALOG.properties
|
||||
SETTINGS = DIALOG.settings
|
||||
LISTITEMS = DIALOG.listitems
|
||||
BUILTIN = DIALOG.builtin
|
||||
|
||||
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 validString(s):
|
||||
return "".join(x for x in s if (x.isalnum() or x not in '\\/:*?"<>|'))
|
||||
|
||||
def stripNumber(s):
|
||||
return re.sub(r'\d+','',s)
|
||||
|
||||
def stripRegion(s):
|
||||
match = re.compile(r'(.*) \((.*)\)', re.IGNORECASE).search(s)
|
||||
try: return match.group(1)
|
||||
except: return s
|
||||
|
||||
def chanceBool(percent=25):
|
||||
return random.randrange(100) <= percent
|
||||
|
||||
def decodePlot(text: str = '') -> dict:
|
||||
plot = re.search(r'\[COLOR item=\"(.+?)\"]\[/COLOR]', text)
|
||||
if plot: return loadJSON(decodeString(plot.group(1)))
|
||||
return {}
|
||||
|
||||
def encodePlot(plot, text):
|
||||
return '%s [COLOR item="%s"][/COLOR]'%(plot,encodeString(dumpJSON(text)))
|
||||
|
||||
def escapeString(text, table=HTML_ESCAPE):
|
||||
return escape(text,table)
|
||||
|
||||
def unescapeString(text, table=HTML_ESCAPE):
|
||||
return unescape(text,{v:k for k, v in list(table.items())})
|
||||
|
||||
def getJSON(file):
|
||||
data = {}
|
||||
try:
|
||||
fle = FileAccess.open(file,'r')
|
||||
data = loadJSON(fle.read())
|
||||
except Exception as e: log('Globals: getJSON failed! %s\nfile = %s'%(e,file), xbmc.LOGERROR)
|
||||
fle.close()
|
||||
return data
|
||||
|
||||
def setJSON(file, data):
|
||||
with FileLock():
|
||||
fle = FileAccess.open(file, 'w')
|
||||
fle.write(dumpJSON(data, idnt=4, sortkey=False))
|
||||
fle.close()
|
||||
return True
|
||||
|
||||
def requestURL(url, params={}, payload={}, header=HEADER, timeout=FIFTEEN, json_data=False, cache=None, checksum=ADDON_VERSION, life=datetime.timedelta(minutes=15)):
|
||||
def __error(json_data):
|
||||
return {} if json_data else ""
|
||||
|
||||
def __getCache(key,json_data,cache,checksum):
|
||||
return (cache.get('requestURL.%s'%(key), checksum, json_data) or __error(json_data))
|
||||
|
||||
def __setCache(key,results,json_data,cache,checksum,life):
|
||||
return cache.set('requestURL.%s'%(key), results, checksum, life, json_data)
|
||||
|
||||
complete = False
|
||||
cacheKey = '.'.join([url,dumpJSON(params),dumpJSON(payload),dumpJSON(header)])
|
||||
session = requests.Session()
|
||||
retries = Retry(total=3, backoff_factor=1, status_forcelist=[500, 502, 503, 504])
|
||||
adapter = HTTPAdapter(max_retries=retries)
|
||||
session.mount("http://", adapter)
|
||||
session.mount("https://", adapter)
|
||||
try:
|
||||
headers = HEADER.copy()
|
||||
headers.update(header)
|
||||
if payload: response = session.post(url, data=dumpJSON(payload), headers=headers, timeout=timeout)
|
||||
else: response = session.get(url, params=params, headers=headers, timeout=timeout)
|
||||
response.raise_for_status() # Raise an exception for HTTP errors
|
||||
log("Globals: requestURL, url = %s, status = %s"%(url,response.status_code))
|
||||
complete = True
|
||||
|
||||
if json_data: results = response.json()
|
||||
else: results = response.content
|
||||
if results and cache: return __setCache(cacheKey,results,json_data,cache,checksum,life)
|
||||
else: return results
|
||||
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
log("Globals: requestURL, failed! Error connecting to the server: %s"%('Returning cache' if cache else 'No Response'))
|
||||
return __getCache(cacheKey,json_data,cache,checksum) if cache else __error(json_data)
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
log("Globals: requestURL, failed! HTTP error occurred: %s"%('Returning cache' if cache else 'No Response'))
|
||||
return __getCache(cacheKey,json_data,cache,checksum) if cache else __error(json_data)
|
||||
|
||||
except Exception as e:
|
||||
log("Globals: requestURL, failed! An error occurred: %s"%(e), xbmc.LOGERROR)
|
||||
return __error(json_data)
|
||||
|
||||
finally:
|
||||
if not complete and payload:
|
||||
queueURL({"url":url, "params":params, "payload":payload, "header":header, "timeout":timeout, "json_data":json_data, "cache":cache, "checksum":checksum, "life":life}) #retry post
|
||||
|
||||
def queueURL(param):
|
||||
queuePool = (SETTINGS.getCacheSetting('queueURL', json_data=True) or {})
|
||||
params = queuePool.setdefault('params',[])
|
||||
params.append(param)
|
||||
queuePool['params'] = setDictLST(params)
|
||||
log("Globals: queueURL, saving = %s\n%s"%(len(queuePool['params']),param))
|
||||
SETTINGS.setCacheSetting('queueURL', queuePool, json_data=True)
|
||||
|
||||
def setURL(url, file):
|
||||
try:
|
||||
contents = requestURL(url)
|
||||
fle = FileAccess.open(file, 'w')
|
||||
fle.write(contents)
|
||||
fle.close()
|
||||
return FileAccess.exists(file)
|
||||
except Exception as e: log('Globals: setURL failed! %s\nurl = %s'%(e,url), xbmc.LOGERROR)
|
||||
|
||||
def diffLSTDICT(old, new):
|
||||
set1 = {dumpJSON(d, sortkey=True) for d in old}
|
||||
set2 = {dumpJSON(d, sortkey=True) for d in new}
|
||||
return {"added": [loadJSON(s) for s in set2 - set1], "removed": [loadJSON(s) for s in set1 - set2]}
|
||||
|
||||
def getChannelID(name, path, number):
|
||||
if isinstance(path, list): path = '|'.join(path)
|
||||
tmpid = '%s.%s.%s.%s'%(number, name, hashlib.md5(path.encode(DEFAULT_ENCODING)),SETTINGS.getMYUUID())
|
||||
return '%s@%s'%((binascii.hexlify(tmpid.encode(DEFAULT_ENCODING))[:32]).decode(DEFAULT_ENCODING),slugify(ADDON_NAME))
|
||||
|
||||
def getRecordID(name, path, number):
|
||||
if isinstance(path, list): path = '|'.join(path)
|
||||
tmpid = '%s.%s.%s.%s'%(number, name, hashlib.md5(path.encode(DEFAULT_ENCODING)),SETTINGS.getMYUUID())
|
||||
return '%s@%s'%((binascii.hexlify(tmpid.encode(DEFAULT_ENCODING))[:16]).decode(DEFAULT_ENCODING),slugify(ADDON_NAME))
|
||||
|
||||
def splitYear(label):
|
||||
try:
|
||||
match = re.compile(r'(.*) \((.*)\)', re.IGNORECASE).search(label)
|
||||
if match and match.group(2):
|
||||
label, year = match.groups()
|
||||
if year.isdigit():
|
||||
return label, int(year)
|
||||
except: pass
|
||||
return label, None
|
||||
|
||||
def getChannelSuffix(name, type):
|
||||
name = validString(name)
|
||||
if type == "TV Genres" and not LANGUAGE(32014).lower() in name.lower(): suffix = LANGUAGE(32014) #TV
|
||||
elif type == "Movie Genres" and not LANGUAGE(32015).lower() in name.lower(): suffix = LANGUAGE(32015) #Movies
|
||||
elif type == "Mixed Genres" and not LANGUAGE(32010).lower() in name.lower(): suffix = LANGUAGE(32010) #Mixed
|
||||
elif type == "Music Genres" and not LANGUAGE(32016).lower() in name.lower(): suffix = LANGUAGE(32016) #Music
|
||||
else: return name
|
||||
return '%s %s'%(name,suffix)
|
||||
|
||||
def cleanChannelSuffix(name, type):
|
||||
if type == "TV Genres" : name = name.split(' %s'%LANGUAGE(32014))[0]#TV
|
||||
elif type == "Movie Genres" : name = name.split(' %s'%LANGUAGE(32015))[0]#Movies
|
||||
elif type == "Mixed Genres" : name = name.split(' %s'%LANGUAGE(32010))[0]#Mixed
|
||||
elif type == "Music Genres" : name = name.split(' %s'%LANGUAGE(32016))[0]#Music
|
||||
return name
|
||||
|
||||
def getLabel(item, addYear=False):
|
||||
label = (item.get('name') or item.get('label') or item.get('showtitle') or item.get('title'))
|
||||
if not label: return ''
|
||||
label, year = splitYear(label)
|
||||
year = (item.get('year') or year)
|
||||
if year and addYear: return '%s (%s)'%(label, year)
|
||||
return label
|
||||
|
||||
def hasFile(file):
|
||||
if not file.startswith(tuple(VFS_TYPES + WEB_TYPES)): state = FileAccess.exists(file)
|
||||
elif file.startswith('plugin://'): state = hasAddon(file)
|
||||
else: state = True
|
||||
log("Globals: hasFile, file = %s (%s)"%(file,state))
|
||||
return state
|
||||
|
||||
def hasAddon(id, install=False, enable=False, force=False, notify=False):
|
||||
if '://' in id: id = getIDbyPath(id)
|
||||
if BUILTIN.getInfoBool('HasAddon(%s)'%(id),'System'):
|
||||
if BUILTIN.getInfoBool('AddonIsEnabled(%s)'%(id),'System'): return True
|
||||
elif enable:
|
||||
if not force:
|
||||
if not DIALOG.yesnoDialog(message=LANGUAGE(32156)%(id)): return False
|
||||
return BUILTIN.executebuiltin('EnableAddon(%s)'%(id),wait=True)
|
||||
elif install: return BUILTIN.executebuiltin('InstallAddon(%s)'%(id),wait=True)
|
||||
if notify: DIALOG.notificationDialog(LANGUAGE(32034)%(id))
|
||||
return False
|
||||
|
||||
def diffRuntime(dur, roundto=15):
|
||||
def ceil_dt(dt, delta):
|
||||
return dt + (datetime.datetime.min - dt) % delta
|
||||
now = datetime.datetime.fromtimestamp(dur)
|
||||
return (ceil_dt(now, datetime.timedelta(minutes=roundto)) - now).total_seconds()
|
||||
|
||||
def roundTimeDown(dt, offset=30): # round the given time down to the nearest
|
||||
n = datetime.datetime.fromtimestamp(dt)
|
||||
delta = datetime.timedelta(minutes=offset)
|
||||
if n.minute > (offset-1): n = n.replace(minute=offset, second=0, microsecond=0)
|
||||
else: n = n.replace(minute=0, second=0, microsecond=0)
|
||||
return time.mktime(n.timetuple())
|
||||
|
||||
def roundTimeUp(dt=None, roundTo=60):
|
||||
if dt == None : dt = datetime.datetime.now()
|
||||
seconds = (dt.replace(tzinfo=None) - dt.min).seconds
|
||||
rounding = (seconds+roundTo/2) // roundTo * roundTo
|
||||
return dt + datetime.timedelta(0,rounding-seconds,-dt.microsecond)
|
||||
|
||||
def strpTime(datestring, format=DTJSONFORMAT): #convert pvr infolabel datetime string to datetime obj, thread safe!
|
||||
try: return datetime.datetime.strptime(datestring, format)
|
||||
except TypeError: return datetime.datetime.fromtimestamp(time.mktime(time.strptime(datestring, format)))
|
||||
except: return ''
|
||||
|
||||
def epochTime(timestamp, tz=True): #convert pvr json datetime string to datetime obj
|
||||
if tz: timestamp -= getTimeoffset()
|
||||
return datetime.datetime.fromtimestamp(timestamp)
|
||||
|
||||
def getTimeoffset():
|
||||
return (int((datetime.datetime.now() - datetime.datetime.utcnow()).days * 86400 + round((datetime.datetime.now() - datetime.datetime.utcnow()).seconds, -1)))
|
||||
|
||||
def getUTCstamp():
|
||||
return time.time() - getTimeoffset()
|
||||
|
||||
def getGMTstamp():
|
||||
return time.time()
|
||||
|
||||
def randomShuffle(items=[]):
|
||||
if len(items) > 0:
|
||||
#reseed random for a "greater sudo random"
|
||||
random.seed(random.randint(0,999999999999))
|
||||
random.shuffle(items)
|
||||
return items
|
||||
|
||||
def isStack(path): #is path a stack
|
||||
return path.startswith('stack://')
|
||||
|
||||
def splitStacks(path): #split stack path for indv. files.
|
||||
if not isStack(path): return [path]
|
||||
return [_f for _f in ((path.split('stack://')[1]).split(' , ')) if _f]
|
||||
|
||||
def escapeDirJSON(path):
|
||||
mydir = path
|
||||
if (mydir.find(":")): mydir = mydir.replace("\\", "\\\\")
|
||||
return mydir
|
||||
|
||||
def KODI_LIVETV_SETTINGS(): #recommended Kodi LiveTV settings
|
||||
return {'pvrmanager.preselectplayingchannel' :'true',
|
||||
'pvrmanager.syncchannelgroups' :'true',
|
||||
'pvrmanager.backendchannelorder' :'true',
|
||||
'pvrmanager.usebackendchannelnumbers':'true',
|
||||
'pvrplayback.autoplaynextprogramme' :'true',
|
||||
# 'pvrmenu.iconpath':'',
|
||||
# 'pvrplayback.switchtofullscreenchanneltypes':1,
|
||||
# 'pvrplayback.confirmchannelswitch':'true',
|
||||
# 'epg.selectaction':2,
|
||||
# 'epg.epgupdate':120,
|
||||
'pvrmanager.startgroupchannelnumbersfromone':'false'}
|
||||
|
||||
def togglePVR(state=True, reverse=False, wait=FIFTEEN):
|
||||
if SETTINGS.getSettingBool('Enable_PVR_RELOAD'):
|
||||
isEnabled = BUILTIN.getInfoBool('AddonIsEnabled(%s)'%(PVR_CLIENT_ID),'System')
|
||||
if (state and isEnabled) or (not state and not isEnabled): return
|
||||
elif not PROPERTIES.isRunning('togglePVR'):
|
||||
with PROPERTIES.chkRunning('togglePVR'):
|
||||
BUILTIN.executebuiltin("Dialog.Close(all)")
|
||||
log('globals: togglePVR, state = %s, reverse = %s, wait = %s'%(state,reverse,wait))
|
||||
BUILTIN.executeJSONRPC('{"jsonrpc":"2.0","method":"Addons.SetAddonEnabled","params":{"addonid":"%s","enabled":%s}, "id": 1}'%(PVR_CLIENT_ID,str(state).lower()))
|
||||
if reverse:
|
||||
with BUILTIN.busy_dialog():
|
||||
MONITOR().waitForAbort(1.0)
|
||||
timerit(togglePVR)(wait,[not bool(state)])
|
||||
DIALOG.notificationWait('%s: %s'%(PVR_CLIENT_NAME,LANGUAGE(32125)),wait=wait)
|
||||
else: DIALOG.notificationWait(LANGUAGE(30023)%(PVR_CLIENT_NAME))
|
||||
|
||||
def isRadio(item):
|
||||
if item.get('radio',False) or item.get('type') == "Music Genres": return True
|
||||
for path in item.get('path',[item.get('file','')]):
|
||||
if path.lower().startswith(('musicdb://','special://profile/playlists/music/','special://musicplaylists/')): return True
|
||||
return False
|
||||
|
||||
def isMixed_XSP(item):
|
||||
for path in item.get('path',[item.get('file','')]):
|
||||
if path.lower().startswith('special://profile/playlists/mixed/'): return True
|
||||
return False
|
||||
|
||||
def cleanLabel(text):
|
||||
text = re.sub(r'\[COLOR=(.+?)\]', '', text)
|
||||
text = re.sub(r'\[/COLOR\]', '', text)
|
||||
text = text.replace("[B]",'').replace("[/B]",'')
|
||||
text = text.replace("[I]",'').replace("[/I]",'')
|
||||
return text.replace(":",'')
|
||||
|
||||
def cleanImage(image=LOGO):
|
||||
if not image: image = LOGO
|
||||
if not image.startswith(('image://','resource://','special://','smb://','nfs://','https://','http://')):
|
||||
realPath = FileAccess.translatePath('special://home/addons/')
|
||||
if image.startswith(realPath):# convert real path. to vfs
|
||||
image = image.replace(realPath,'special://home/addons/').replace('\\','/')
|
||||
elif image.startswith(realPath.replace('\\','/')):
|
||||
image = image.replace(realPath.replace('\\','/'),'special://home/addons/').replace('\\','/')
|
||||
return image
|
||||
|
||||
def cleanGroups(citem, enableGrouping=SETTINGS.getSettingBool('Enable_Grouping')):
|
||||
if not enableGrouping:
|
||||
citem['group'] = [ADDON_NAME]
|
||||
else:
|
||||
citem['group'].append(ADDON_NAME)
|
||||
if citem.get('favorite',False) and not LANGUAGE(32019) in citem['group']:
|
||||
citem['group'].append(LANGUAGE(32019))
|
||||
elif not citem.get('favorite',False) and LANGUAGE(32019) in citem['group']:
|
||||
citem['group'].remove(LANGUAGE(32019))
|
||||
return sorted(set(citem['group']))
|
||||
|
||||
def cleanMPAA(mpaa):
|
||||
orgMPA = mpaa
|
||||
mpaa = mpaa.lower()
|
||||
if ':' in mpaa: mpaa = re.split(':',mpaa)[1] #todo prop. regex
|
||||
if 'rated ' in mpaa: mpaa = re.split('rated ',mpaa)[1] #todo prop. regex
|
||||
#todo regex, detect other region rating formats
|
||||
# re.compile(':(.*)', re.IGNORECASE).search(text))
|
||||
text = mpaa.upper()
|
||||
try:
|
||||
text = re.sub('/ US', '' , text)
|
||||
text = re.sub('Rated ', '', text)
|
||||
mpaa = text.strip()
|
||||
except:
|
||||
mpaa = mpaa.strip()
|
||||
return mpaa
|
||||
|
||||
def getIDbyPath(url):
|
||||
try:
|
||||
if url.startswith('special://'): return re.compile('special://home/addons/(.*?)/resources', re.IGNORECASE).search(url).group(1)
|
||||
elif url.startswith('plugin://'): return re.compile('plugin://(.*?)/', re.IGNORECASE).search(url).group(1)
|
||||
except Exception as e: log('Globals: getIDbyPath failed! url = %s, %s'%(url,e), xbmc.LOGERROR)
|
||||
return url
|
||||
|
||||
def combineDicts(dict1={}, dict2={}):
|
||||
for k,v in list(dict1.items()):
|
||||
if dict2.get(k): k = dict2.pop(k)
|
||||
dict1.update(dict2)
|
||||
return dict1
|
||||
|
||||
def mergeDictLST(dict1={},dict2={}):
|
||||
for k, v in list(dict2.items()):
|
||||
dict1.setdefault(k,[]).extend(v)
|
||||
setDictLST()
|
||||
return dict1
|
||||
|
||||
def lstSetDictLst(lst=[]):
|
||||
items = dict()
|
||||
for key, dictlst in list(lst.items()):
|
||||
if isinstance(dictlst, list): dictlst = setDictLST(dictlst)
|
||||
items[key] = dictlst
|
||||
return items
|
||||
|
||||
def compareDict(dict1,dict2,sortKey):
|
||||
a = sorted(dict1, key=itemgetter(sortKey))
|
||||
b = sorted(dict2, key=itemgetter(sortKey))
|
||||
return a == b
|
||||
|
||||
def subZoom(number,percentage,multi=100):
|
||||
return round(number * (percentage*multi) / 100)
|
||||
|
||||
def addZoom(number,percentage,multi=100):
|
||||
return round((number - (number * (percentage*multi) / 100)) + number)
|
||||
|
||||
def frange(start,stop,inc):
|
||||
return [x/10.0 for x in range(start,stop,inc)]
|
||||
|
||||
def timeString2Seconds(string): #hh:mm:ss
|
||||
try: return int(sum(x*y for x, y in zip(list(map(float, string.split(':')[::-1])), (1, 60, 3600, 86400))))
|
||||
except: return -1
|
||||
|
||||
def chunkLst(lst, n):
|
||||
for i in range(0, len(lst), n):
|
||||
yield lst[i:i + n]
|
||||
|
||||
def chunkDict(items, n):
|
||||
it = iter(items)
|
||||
for i in range(0, len(items), n):
|
||||
yield {k:items[k] for k in islice(it, n)}
|
||||
|
||||
def roundupDIV(p, q):
|
||||
try:
|
||||
d, r = divmod(p, q)
|
||||
if r: d += 1
|
||||
return d
|
||||
except ZeroDivisionError:
|
||||
return 1
|
||||
|
||||
def interleave(seqs, sets=1, repeats=False):
|
||||
#evenly interleave multi-lists of different sizes, while preserving seq order and by sets of x
|
||||
# In [[1,2,3,4],['a','b','c'],['A','B','C','D','E']]
|
||||
# repeats = False
|
||||
# Out sets=0 [1, 2, 3, 4, 'a', 'b', 'c', 'A', 'B', 'C', 'D', 'E']
|
||||
# Out sets=1 [1, 'a', 'A', 2, 'b', 'B', 3, 'c', 'C', 4, 'D', 'E']
|
||||
# Out sets=2 [1, 2, 'a', 'b', 'A', 'B', 3, 4, 'c', 'C', 'D', 'E']
|
||||
# repeats = True
|
||||
# Out sets=0 [1, 2, 3, 4, 'a', 'b', 'c', 'A', 'B', 'C', 'D', 'E']
|
||||
# Out sets=1 [1, 'a', 'A', 2, 'b', 'B', 3, 'c', 'C', 4, 'a', 'D', 1, 'b', 'E']
|
||||
# Out sets=2 [1, 2, 'a', 'b', 'A', 'B', 3, 4, 'c', 'a', 'C', 'D', 1, 2, 'b', 'c', 'E', 'A']
|
||||
if sets > 0:
|
||||
# if repeats:
|
||||
# # Create cyclical iterators for each list
|
||||
# cyclical_iterators = [cycle(lst) for lst in seqs]
|
||||
# interleaved = []
|
||||
# # Determine the length of the longest list
|
||||
# max_len = max(len(lst) for lst in seqs)
|
||||
# # Calculate the number of blocks needed
|
||||
# num_blocks = (max_len + sets - 1) // sets
|
||||
# # Interleave in blocks
|
||||
# for i in range(num_blocks):
|
||||
# for iterator in cyclical_iterators:
|
||||
# # Use islice to take a block of elements from the current iterator
|
||||
# block = list(islice(iterator, sets))
|
||||
# interleaved.extend(block)
|
||||
# return interleaved
|
||||
# else:
|
||||
seqs = [list(zip_longest(*[iter(seqs)] * sets, fillvalue=None)) for seqs in seqs]
|
||||
return list([_f for _f in sum([_f for _f in chain.from_iterable(zip_longest(*seqs)) if _f], ()) if _f])
|
||||
else: return list(chain.from_iterable(seqs))
|
||||
|
||||
def percentDiff(org, new):
|
||||
try: return (abs(float(org) - float(new)) / float(new)) * 100.0
|
||||
except ZeroDivisionError: return -1
|
||||
|
||||
def pagination(list, end):
|
||||
for start in range(0, len(list), end):
|
||||
yield seq[start:start+end]
|
||||
|
||||
def isCenterlized():
|
||||
default = 'special://profile/addon_data/plugin.video.pseudotv.live/cache'
|
||||
if REAL_SETTINGS.getSetting('User_Folder') == default:
|
||||
return False
|
||||
return True
|
||||
|
||||
def isFiller(item={}):
|
||||
for genre in item.get('genre',[]):
|
||||
if genre.lower() in ['pre-roll','post-roll']: return True
|
||||
return False
|
||||
|
||||
def isShort(item={}, minDuration=SETTINGS.getSettingInt('Seek_Tolerance')):
|
||||
if item.get('duration', minDuration) < minDuration: return True
|
||||
else: return False
|
||||
|
||||
def isEnding(progress=100):
|
||||
if progress >= SETTINGS.getSettingInt('Seek_Threshold'): return True
|
||||
else: return False
|
||||
|
||||
def chkLogo(old, new=LOGO):
|
||||
if new.endswith('wlogo.png') and not old.endswith('wlogo.png'): return old
|
||||
return new
|
||||
@@ -0,0 +1,176 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
JSON 2 HTML Converter
|
||||
=====================
|
||||
|
||||
(c) Varun Malhotra 2013-2024
|
||||
Source Code: https://github.com/softvar/json2html
|
||||
|
||||
|
||||
Contributors:
|
||||
-------------
|
||||
1. Michel Müller (@muellermichel), https://github.com/softvar/json2html/pull/2
|
||||
2. Daniel Lekic (@lekic), https://github.com/softvar/json2html/pull/17
|
||||
|
||||
LICENSE: MIT
|
||||
--------
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from collections import OrderedDict
|
||||
import json as json_parser
|
||||
|
||||
if sys.version_info[:2] < (3, 0):
|
||||
from cgi import escape as html_escape
|
||||
text = unicode
|
||||
text_types = (unicode, str)
|
||||
else:
|
||||
from html import escape as html_escape
|
||||
text = str
|
||||
text_types = (str,)
|
||||
|
||||
|
||||
class Json2Html:
|
||||
def convert(self, json="", table_attributes='border="1"', clubbing=True, encode=False, escape=True):
|
||||
"""
|
||||
Convert JSON to HTML Table format
|
||||
"""
|
||||
# table attributes such as class, id, data-attr-*, etc.
|
||||
# eg: table_attributes = 'class = "table table-bordered sortable"'
|
||||
self.table_init_markup = "<table %s>" % table_attributes
|
||||
self.clubbing = clubbing
|
||||
self.escape = escape
|
||||
json_input = None
|
||||
if not json:
|
||||
json_input = {}
|
||||
elif type(json) in text_types:
|
||||
try:
|
||||
json_input = json_parser.loads(json, object_pairs_hook=OrderedDict)
|
||||
except ValueError as e:
|
||||
#so the string passed here is actually not a json string
|
||||
# - let's analyze whether we want to pass on the error or use the string as-is as a text node
|
||||
if u"Expecting property name" in text(e):
|
||||
#if this specific json loads error is raised, then the user probably actually wanted to pass json, but made a mistake
|
||||
raise e
|
||||
json_input = json
|
||||
else:
|
||||
json_input = json
|
||||
converted = self.convert_json_node(json_input)
|
||||
if encode:
|
||||
return converted.encode('ascii', 'xmlcharrefreplace')
|
||||
return converted
|
||||
|
||||
def column_headers_from_list_of_dicts(self, json_input):
|
||||
"""
|
||||
This method is required to implement clubbing.
|
||||
It tries to come up with column headers for your input
|
||||
"""
|
||||
if not json_input \
|
||||
or not hasattr(json_input, '__getitem__') \
|
||||
or not hasattr(json_input[0], 'keys'):
|
||||
return None
|
||||
column_headers = list(json_input[0].keys())
|
||||
for entry in json_input:
|
||||
if not hasattr(entry, 'keys') \
|
||||
or not hasattr(entry, '__iter__') \
|
||||
or len(list(entry.keys())) != len(column_headers):
|
||||
return None
|
||||
for header in column_headers:
|
||||
if header not in entry:
|
||||
return None
|
||||
return column_headers
|
||||
|
||||
def convert_json_node(self, json_input):
|
||||
"""
|
||||
Dispatch JSON input according to the outermost type and process it
|
||||
to generate the super awesome HTML format.
|
||||
We try to adhere to duck typing such that users can just pass all kinds
|
||||
of funky objects to json2html that *behave* like dicts and lists and other
|
||||
basic JSON types.
|
||||
"""
|
||||
if type(json_input) in text_types:
|
||||
if self.escape:
|
||||
return html_escape(text(json_input))
|
||||
else:
|
||||
return text(json_input)
|
||||
if hasattr(json_input, 'items'):
|
||||
return self.convert_object(json_input)
|
||||
if hasattr(json_input, '__iter__') and hasattr(json_input, '__getitem__'):
|
||||
return self.convert_list(json_input)
|
||||
return text(json_input)
|
||||
|
||||
def convert_list(self, list_input):
|
||||
"""
|
||||
Iterate over the JSON list and process it
|
||||
to generate either an HTML table or a HTML list, depending on what's inside.
|
||||
If suppose some key has array of objects and all the keys are same,
|
||||
instead of creating a new row for each such entry,
|
||||
club such values, thus it makes more sense and more readable table.
|
||||
|
||||
@example:
|
||||
jsonObject = {
|
||||
"sampleData": [
|
||||
{"a":1, "b":2, "c":3},
|
||||
{"a":5, "b":6, "c":7}
|
||||
]
|
||||
}
|
||||
OUTPUT:
|
||||
_____________________________
|
||||
| | | | |
|
||||
| | a | c | b |
|
||||
| sampleData |---|---|---|
|
||||
| | 1 | 3 | 2 |
|
||||
| | 5 | 7 | 6 |
|
||||
-----------------------------
|
||||
|
||||
@contributed by: @muellermichel
|
||||
"""
|
||||
if not list_input:
|
||||
return ""
|
||||
converted_output = ""
|
||||
column_headers = None
|
||||
if self.clubbing:
|
||||
column_headers = self.column_headers_from_list_of_dicts(list_input)
|
||||
if column_headers is not None:
|
||||
converted_output += self.table_init_markup
|
||||
converted_output += '<thead>'
|
||||
converted_output += '<tr><th>' + '</th><th>'.join(column_headers) + '</th></tr>'
|
||||
converted_output += '</thead>'
|
||||
converted_output += '<tbody>'
|
||||
for list_entry in list_input:
|
||||
converted_output += '<tr><td>'
|
||||
converted_output += '</td><td>'.join([self.convert_json_node(list_entry[column_header]) for column_header in
|
||||
column_headers])
|
||||
converted_output += '</td></tr>'
|
||||
converted_output += '</tbody>'
|
||||
converted_output += '</table>'
|
||||
return converted_output
|
||||
|
||||
#so you don't want or need clubbing eh? This makes @muellermichel very sad... ;(
|
||||
#alright, let's fall back to a basic list here...
|
||||
converted_output = '<ul><li>'
|
||||
converted_output += '</li><li>'.join([self.convert_json_node(child) for child in list_input])
|
||||
converted_output += '</li></ul>'
|
||||
return converted_output
|
||||
|
||||
def convert_object(self, json_input):
|
||||
"""
|
||||
Iterate over the JSON object and process it
|
||||
to generate the super awesome HTML Table format
|
||||
"""
|
||||
if not json_input:
|
||||
return "" #avoid empty tables
|
||||
converted_output = self.table_init_markup + "<tr>"
|
||||
converted_output += "</tr><tr>".join([
|
||||
"<th>%s</th><td>%s</td>" %(
|
||||
self.convert_json_node(k),
|
||||
self.convert_json_node(v)
|
||||
)
|
||||
for k, v in list(json_input.items())
|
||||
])
|
||||
converted_output += '</tr></table>'
|
||||
return converted_output
|
||||
|
||||
json2html = Json2Html()
|
||||
@@ -0,0 +1,668 @@
|
||||
# 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 videoparser import VideoParser
|
||||
|
||||
class Service:
|
||||
player = PLAYER()
|
||||
monitor = MONITOR()
|
||||
def _interrupt(self) -> bool:
|
||||
return PROPERTIES.isPendingInterrupt()
|
||||
def _suspend(self) -> bool:
|
||||
return PROPERTIES.isPendingSuspend()
|
||||
|
||||
|
||||
class JSONRPC:
|
||||
def __init__(self, service=None):
|
||||
if service is None: service = Service()
|
||||
self.service = service
|
||||
self.cache = SETTINGS.cacheDB
|
||||
self.videoParser = VideoParser()
|
||||
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
return log('%s: %s' % (self.__class__.__name__, msg), level)
|
||||
|
||||
|
||||
def sendJSON(self, param, timeout=-1):
|
||||
command = param
|
||||
command["jsonrpc"] = "2.0"
|
||||
command["id"] = ADDON_ID
|
||||
self.log('sendJSON, timeout = %s, command = %s'%(timeout,dumpJSON(command)))
|
||||
if timeout > 0: response = loadJSON((killit(BUILTIN.executeJSONRPC)(timeout,dumpJSON(command))) or {'error':{'message':'JSONRPC timed out!'}})
|
||||
else: response = loadJSON(BUILTIN.executeJSONRPC(dumpJSON(command)))
|
||||
if response.get('error'):
|
||||
self.log('sendJSON, failed! error = %s\n%s'%(dumpJSON(response.get('error')),command), xbmc.LOGWARNING)
|
||||
response.setdefault('result',{})['error'] = response.pop('error')
|
||||
#throttle calls, low power devices suffer segfault during rpc flood
|
||||
self.service.monitor.waitForAbort(float(SETTINGS.getSettingInt('RPC_Delay')/1000))
|
||||
return response
|
||||
|
||||
|
||||
def queueJSON(self, param):
|
||||
queuePool = (SETTINGS.getCacheSetting('queueJSON', json_data=True) or {})
|
||||
params = queuePool.setdefault('params',[])
|
||||
params.append(param)
|
||||
queuePool['params'] = sorted(setDictLST(params), key=lambda d: d.get('params',{}).get('setting',''))
|
||||
queuePool['params'] = sorted(setDictLST(params), key=lambda d: d.get('params',{}).get('playcount',0))
|
||||
queuePool['params'].reverse() #prioritize setsetting,playcount rollback over duration amendments.
|
||||
self.log("queueJSON, saving = %s\n%s"%(len(queuePool['params']),param))
|
||||
SETTINGS.setCacheSetting('queueJSON', queuePool, json_data=True)
|
||||
|
||||
|
||||
def cacheJSON(self, param, life=datetime.timedelta(minutes=15), checksum=ADDON_VERSION, timeout=-1):
|
||||
cacheName = 'cacheJSON.%s'%(getMD5(dumpJSON(param)))
|
||||
cacheResponse = self.cache.get(cacheName, checksum=checksum, json_data=True)
|
||||
if not cacheResponse:
|
||||
cacheResponse = self.sendJSON(param, timeout)
|
||||
if cacheResponse.get('result',{}): self.cache.set(cacheName, cacheResponse, checksum=checksum, expiration=life, json_data=True)
|
||||
return cacheResponse
|
||||
|
||||
|
||||
def walkFileDirectory(self, path, media='video', depth=5, chkDuration=False, retItem=False, checksum=ADDON_VERSION, expiration=datetime.timedelta(minutes=15)):
|
||||
walk = dict()
|
||||
self.log('walkFileDirectory, walking %s, depth = %s'%(path,depth))
|
||||
items = self.getDirectory({"directory":path,"media":media},True,checksum,expiration).get('files',[])
|
||||
for idx, item in enumerate(items):
|
||||
if item.get('filetype') == 'file':
|
||||
if chkDuration:
|
||||
item['duration'] = self.getDuration(item.get('file'),item, accurate=bool(SETTINGS.getSettingInt('Duration_Type')))
|
||||
if item['duration'] == 0: continue
|
||||
walk.setdefault(path,[]).append(item if retItem else item.get('file'))
|
||||
elif item.get('filetype') == 'directory' and depth > 0:
|
||||
depth -= 1
|
||||
walk.update(self.walkFileDirectory(item.get('file'), media, depth, chkDuration, retItem, checksum, expiration))
|
||||
return walk
|
||||
|
||||
|
||||
def walkListDirectory(self, path, exts='', depth=5, chkDuration=False, appendPath=False, checksum=ADDON_VERSION, expiration=datetime.timedelta(minutes=15)):
|
||||
def _chkfile(path, f):
|
||||
if exts and not f.lower().endswith(tuple(exts)): return
|
||||
if chkDuration:
|
||||
dur = self.getDuration(os.path.join(path,f), accurate=bool(SETTINGS.getSettingInt('Duration_Type')))
|
||||
if dur == 0: return
|
||||
return {True:os.path.join(path,f).replace('\\','/'),False:f}[appendPath]
|
||||
|
||||
def _parseXBT(resource):
|
||||
self.log('walkListDirectory, parsing XBT = %s'%(resource))
|
||||
walk.setdefault(resource,[]).extend(self.getListDirectory(resource,checksum,expiration)[1])
|
||||
return walk
|
||||
|
||||
walk = dict()
|
||||
path = path.replace('\\','/')
|
||||
subs, files = self.getListDirectory(path,checksum,expiration)
|
||||
if len(files) > 0 and TEXTURES in files: return _parseXBT(re.sub('/resources','',path).replace('special://home/addons/','resource://'))
|
||||
nfiles = [_f for _f in [_chkfile(path, file) for file in files] if _f]
|
||||
self.log('walkListDirectory, walking %s, found = %s, appended = %s, depth = %s, ext = %s'%(path,len(files),len(nfiles),depth,exts))
|
||||
walk.setdefault(path,[]).extend(nfiles)
|
||||
|
||||
for sub in subs:
|
||||
if depth == 0: break
|
||||
depth -= 1
|
||||
walk.update(self.walkListDirectory(os.path.join(path,sub), exts, depth, chkDuration, appendPath, checksum, expiration))
|
||||
return walk
|
||||
|
||||
|
||||
def getListDirectory(self, path, checksum=ADDON_VERSION, expiration=datetime.timedelta(minutes=15)):
|
||||
cacheName = 'getListDirectory.%s'%(getMD5(path))
|
||||
results = self.cache.get(cacheName, checksum)
|
||||
if not results:
|
||||
try:
|
||||
results = self.cache.set(cacheName, FileAccess.listdir(path), checksum, expiration)
|
||||
self.log('getListDirectory path = %s, checksum = %s'%(path, checksum))
|
||||
except Exception as e:
|
||||
self.log("getListDirectory, failed! %s\npath = %s"%(e,path), xbmc.LOGERROR)
|
||||
results = [],[]
|
||||
self.log('getListDirectory return dirs = %s, files = %s\n%s'%(len(results[0]), len(results[1]),path))
|
||||
return results
|
||||
|
||||
|
||||
def getIntrospect(self, id):
|
||||
param = {"method":"JSONRPC.Introspect","params":{"filter":{"id":id,"type":"method"}}}
|
||||
return self.cacheJSON(param,datetime.timedelta(days=28),BUILTIN.getInfoLabel('BuildVersion','System')).get('result',{})
|
||||
|
||||
|
||||
def getEnums(self, id, type='', key='enums'):
|
||||
self.log('getEnums id = %s, type = %s, key = %s' % (id, type, key))
|
||||
param = {"method":"JSONRPC.Introspect","params":{"getmetadata":True,"filterbytransport":True,"filter":{"getreferences":False,"id":id,"type":"type"}}}
|
||||
json_response = self.cacheJSON(param,datetime.timedelta(days=28),BUILTIN.getInfoLabel('BuildVersion','System')).get('result',{}).get('types',{}).get(id,{})
|
||||
return (json_response.get('properties',{}).get(type,{}).get(key) or json_response.get(type,{}).get(key) or json_response.get(key,[]))
|
||||
|
||||
|
||||
def notifyAll(self, message, data, sender=ADDON_ID):
|
||||
param = {"method":"JSONRPC.NotifyAll","params":{"sender":sender,"message":message,"data":[data]}}
|
||||
return self.sendJSON(param).get('result') == 'OK'
|
||||
|
||||
|
||||
def playerOpen(self, params={}):
|
||||
param = {"method":"Player.Open","params":params}
|
||||
return self.sendJSON(param).get('result') == 'OK'
|
||||
|
||||
|
||||
def getSetting(self, category, section, cache=False):
|
||||
param = {"method":"Settings.GetSettings","params":{"filter":{"category":category,"section":section}}}
|
||||
if cache: return self.cacheJSON(param).get('result',{}).get('settings',[])
|
||||
else: return self.sendJSON(param).get('result', {}).get('settings',[])
|
||||
|
||||
|
||||
def getSettingValue(self, key, default='', cache=False):
|
||||
param = {"method":"Settings.GetSettingValue","params":{"setting":key}}
|
||||
if cache: return (self.cacheJSON(param).get('result',{}).get('value') or default)
|
||||
else: return (self.sendJSON(param).get('result',{}).get('value') or default)
|
||||
|
||||
|
||||
def setSettingValue(self, key, value, queue=False):
|
||||
param = {"method":"Settings.SetSettingValue","params":{"setting":key,"value":value}}
|
||||
if queue: self.queueJSON(param)
|
||||
else: self.sendJSON(param)
|
||||
|
||||
|
||||
def getSources(self, media='video', cache=True):
|
||||
param = {"method":"Files.GetSources","params":{"media":media}}
|
||||
if cache: return self.cacheJSON(param).get('result', {}).get('sources', [])
|
||||
else: return self.sendJSON(param).get('result', {}).get('sources', [])
|
||||
|
||||
|
||||
def getAddonDetails(self, addonid=ADDON_ID, cache=True):
|
||||
param = {"method":"Addons.GetAddonDetails","params":{"addonid":addonid,"properties":self.getEnums("Addon.Fields", type='items')}}
|
||||
if cache: return self.cacheJSON(param).get('result', {}).get('addon', {})
|
||||
else: return self.sendJSON(param).get('result', {}).get('addon', {})
|
||||
|
||||
|
||||
def getAddons(self, param={"content":"video","enabled":True,"installed":True}, cache=True):
|
||||
param["properties"] = self.getEnums("Addon.Fields", type='items')
|
||||
param = {"method":"Addons.GetAddons","params":param}
|
||||
if cache: return self.cacheJSON(param).get('result', {}).get('addons', [])
|
||||
else: return self.sendJSON(param).get('result', {}).get('addons', [])
|
||||
|
||||
|
||||
def getSongs(self, cache=True):
|
||||
param = {"method":"AudioLibrary.GetSongs","params":{"properties":self.getEnums("Audio.Fields.Song", type='items')}}
|
||||
if cache: return self.cacheJSON(param).get('result', {}).get('songs', [])
|
||||
else: return self.sendJSON(param).get('result', {}).get('songs', [])
|
||||
|
||||
|
||||
def getArtists(self, cache=True):
|
||||
param = {"method":"AudioLibrary.GetArtists","params":{"properties":self.getEnums("Audio.Fields.Artist", type='items')}}
|
||||
if cache: return self.cacheJSON(param).get('result', {}).get('artists', [])
|
||||
else: return self.sendJSON(param).get('result', {}).get('artists', [])
|
||||
|
||||
|
||||
def getAlbums(self, cache=True):
|
||||
param = {"method":"AudioLibrary.GetAlbums","params":{"properties":self.getEnums("Audio.Fields.Album", type='items')}}
|
||||
if cache: return self.cacheJSON(param).get('result', {}).get('albums', [])
|
||||
else: return self.sendJSON(param).get('result', {}).get('albums', [])
|
||||
|
||||
|
||||
def getEpisodes(self, cache=True):
|
||||
param = {"method":"VideoLibrary.GetEpisodes","params":{"properties":self.getEnums("Video.Fields.Episode", type='items')}}
|
||||
if cache: return self.cacheJSON(param).get('result', {}).get('episodes', [])
|
||||
else: return self.sendJSON(param).get('result', {}).get('episodes', [])
|
||||
|
||||
|
||||
def getTVshows(self, cache=True):
|
||||
param = {"method":"VideoLibrary.GetTVShows","params":{"properties":self.getEnums("Video.Fields.TVShow", type='items')}}
|
||||
if cache: return self.cacheJSON(param).get('result', {}).get('tvshows', [])
|
||||
else: return self.sendJSON(param).get('result', {}).get('tvshows', [])
|
||||
|
||||
|
||||
def getMovies(self, cache=True):
|
||||
param = {"method":"VideoLibrary.GetMovies","params":{"properties":self.getEnums("Video.Fields.Movie", type='items')}}
|
||||
if cache: return self.cacheJSON(param).get('result', {}).get('movies', [])
|
||||
else: return self.sendJSON(param).get('result', {}).get('movies', [])
|
||||
|
||||
|
||||
def getVideoGenres(self, type="movie", cache=True): #type = "movie"/"tvshow"
|
||||
param = {"method":"VideoLibrary.GetGenres","params":{"type":type,"properties":self.getEnums("Library.Fields.Genre", type='items')}}
|
||||
if cache: return self.cacheJSON(param).get('result', {}).get('genres', [])
|
||||
else: return self.sendJSON(param).get('result', {}).get('genres', [])
|
||||
|
||||
|
||||
def getMusicGenres(self, cache=True):
|
||||
param = {"method":"AudioLibrary.GetGenres","params":{"properties":self.getEnums("Library.Fields.Genre", type='items')}}
|
||||
if cache: return self.cacheJSON(param).get('result', {}).get('genres', [])
|
||||
else: return self.sendJSON(param).get('result', {}).get('genres', [])
|
||||
|
||||
|
||||
def getDirectory(self, param={}, cache=True, checksum=ADDON_VERSION, expiration=datetime.timedelta(minutes=15), timeout=-1):
|
||||
param["properties"] = self.getEnums("List.Fields.Files", type='items')
|
||||
param = {"method":"Files.GetDirectory","params":param}
|
||||
if cache: return self.cacheJSON(param, expiration, checksum, timeout).get('result', {})
|
||||
else: return self.sendJSON(param, timeout).get('result', {})
|
||||
|
||||
|
||||
def getLibrary(self, method, param={}, cache=True):
|
||||
param = {"method":method,"params":param}
|
||||
if cache: return self.cacheJSON(param).get('result', {})
|
||||
else: return self.sendJSON(param).get('result', {})
|
||||
|
||||
|
||||
def getStreamDetails(self, path, media='video'):
|
||||
if isStack(path): path = splitStacks(path)[0]
|
||||
param = {"method":"Files.GetFileDetails","params":{"file":path,"media":media,"properties":["streamdetails"]}}
|
||||
return self.cacheJSON(param, life=datetime.timedelta(days=MAX_GUIDEDAYS), checksum=getMD5(path)).get('result',{}).get('filedetails',{}).get('streamdetails',{})
|
||||
|
||||
|
||||
def getFileDetails(self, file, media='video', properties=["duration","runtime"]):
|
||||
return self.cacheJSON({"method":"Files.GetFileDetails","params":{"file":file,"media":media,"properties":properties}})
|
||||
|
||||
|
||||
def getViewMode(self):
|
||||
default = {"nonlinearstretch":False,"pixelratio":1,"verticalshift":0,"viewmode":"custom","zoom": 1.0}
|
||||
return self.cacheJSON({"method":"Player.GetViewMode","params":{}},datetime.timedelta(seconds=FIFTEEN)).get('result',default)
|
||||
|
||||
|
||||
def setViewMode(self, params={}):
|
||||
return self.sendJSON({"method":"Player.SetViewMode","params":params})
|
||||
|
||||
|
||||
def getPlayerItem(self, playlist=False):
|
||||
self.log('getPlayerItem, playlist = %s' % (playlist))
|
||||
if playlist: param = {"method":"Playlist.GetItems","params":{"playlistid":self.getActivePlaylist(),"properties":self.getEnums("List.Fields.All", type='items')}}
|
||||
else: param = {"method":"Player.GetItem" ,"params":{"playerid":self.getActivePlayer() ,"properties":self.getEnums("List.Fields.All", type='items')}}
|
||||
result = self.sendJSON(param).get('result', {})
|
||||
return (result.get('item', {}) or result.get('items', []))
|
||||
|
||||
|
||||
def getPVRChannels(self, radio=False):
|
||||
param = {"method":"PVR.GetChannels","params":{"channelgroupid":{True:'allradio',False:'alltv'}[radio],"properties":self.getEnums("PVR.Fields.Channel", type='items')}}
|
||||
return self.sendJSON(param).get('result', {}).get('channels', [])
|
||||
|
||||
|
||||
def getPVRChannelsDetails(self, id):
|
||||
param = {"method":"PVR.GetChannelDetails","params":{"channelid":id,"properties":self.getEnums("PVR.Fields.Channel", type='items')}}
|
||||
return self.sendJSON(param).get('result', {}).get('channels', [])
|
||||
|
||||
|
||||
def getPVRBroadcasts(self, id):
|
||||
param = {"method":"PVR.GetBroadcasts","params":{"channelid":id,"properties":self.getEnums("PVR.Fields.Broadcast", type='items')}}
|
||||
return self.sendJSON(param).get('result', {}).get('broadcasts', [])
|
||||
|
||||
|
||||
def getPVRBroadcastDetails(self, id):
|
||||
param = {"method":"PVR.GetBroadcastDetails","params":{"broadcastid":id,"properties":self.getEnums("PVR.Fields.Broadcast", type='items')}}
|
||||
return self.sendJSON(param).get('result', {}).get('broadcastdetails', [])
|
||||
|
||||
|
||||
def getPVRRecordings(self, media='video', cache=True):
|
||||
param = {"method":"Files.GetDirectory","params":{"directory":"pvr://recordings/tv/active/","media":media,"properties":self.getEnums("List.Fields.Files", type='items')}}
|
||||
if cache: return self.cacheJSON(param).get('result', {}).get('files', [])
|
||||
else: return self.sendJSON(param).get('result', {}).get('files', [])
|
||||
|
||||
|
||||
def getPVRSearches(self, media='video', cache=True):
|
||||
param = {"method":"Files.GetDirectory","params":{"directory":"pvr://search/tv/savedsearches/","media":media,"properties":self.getEnums("List.Fields.Files", type='items')}}
|
||||
if cache: return self.cacheJSON(param).get('result', {}).get('files', [])
|
||||
else: return self.sendJSON(param).get('result', {}).get('files', [])
|
||||
|
||||
|
||||
def getPVRSearchItems(self, id, media='video', cache=True):
|
||||
param = {"method":"Files.GetDirectory","params":{"directory":f"pvr://search/tv/savedsearches/{id}/","media":media,"properties":self.getEnums("List.Fields.Files", type='items')}}
|
||||
if cache: return self.cacheJSON(param).get('result', {}).get('files', [])
|
||||
else: return self.sendJSON(param).get('result', {}).get('files', [])
|
||||
|
||||
|
||||
def getSmartPlaylists(self, type='video', cache=True):
|
||||
param = {"method":"Files.GetDirectory","params":{"directory":f"special://profile/playlists/{type}/","media":"video","properties":self.getEnums("List.Fields.Files", type='items')}}
|
||||
if cache: return self.cacheJSON(param).get('result', {}).get('files', [])
|
||||
else: return self.sendJSON(param).get('result', {}).get('files', [])
|
||||
|
||||
|
||||
def getInfoLabel(self, key, cache=False):
|
||||
param = {"method":"XBMC.GetInfoLabels","params":{"labels":[key]}}
|
||||
if cache: return self.cacheJSON(param).get('result', {}).get(key)
|
||||
else: return self.sendJSON(param).get('result', {}).get(key)
|
||||
|
||||
|
||||
def getInfoBool(self, key, cache=False):
|
||||
param = {"method":"XBMC.GetInfoBooleans","params":{"booleans":[key]}}
|
||||
if cache: return self.cacheJSON(param).get('result', {}).get(key)
|
||||
else: return self.sendJSON(param).get('result', {}).get(key)
|
||||
|
||||
|
||||
def _setRuntime(self, item={}, runtime=0, save=SETTINGS.getSettingBool('Store_Duration')): #set runtime collected by player, accurate meta.
|
||||
self.cache.set('getRuntime.%s'%(getMD5(item.get('file'))), runtime, checksum=getMD5(item.get('file')), expiration=datetime.timedelta(days=28), json_data=False)
|
||||
if not item.get('file','plugin://').startswith(tuple(VFS_TYPES)) and save and runtime > 0: self.queDuration(item, runtime=runtime)
|
||||
|
||||
|
||||
def _getRuntime(self, item={}): #get runtime collected by player, else less accurate provider meta
|
||||
runtime = self.cache.get('getRuntime.%s'%(getMD5(item.get('file'))), checksum=getMD5(item.get('file')), json_data=False)
|
||||
return (runtime or item.get('resume',{}).get('total') or item.get('runtime') or item.get('duration') or (item.get('streamdetails',{}).get('video',[]) or [{}])[0].get('duration') or 0)
|
||||
|
||||
|
||||
def _setDuration(self, path, item={}, duration=0, save=SETTINGS.getSettingBool('Store_Duration')):#set VideoParser cache
|
||||
self.cache.set('getDuration.%s'%(getMD5(path)), duration, checksum=getMD5(path), expiration=datetime.timedelta(days=28), json_data=False)
|
||||
if save and item: self.queDuration(item, duration)
|
||||
return duration
|
||||
|
||||
|
||||
def _getDuration(self, path): #get VideoParser cache
|
||||
return (self.cache.get('getDuration.%s'%(getMD5(path)), checksum=getMD5(path), json_data=False) or 0)
|
||||
|
||||
|
||||
def getDuration(self, path, item={}, accurate=bool(SETTINGS.getSettingInt('Duration_Type')), save=SETTINGS.getSettingBool('Store_Duration')):
|
||||
self.log("getDuration, accurate = %s, path = %s, save = %s" % (accurate, path, save))
|
||||
if not item: item = {'file':path}
|
||||
runtime = self._getRuntime(item)
|
||||
if runtime == 0 or accurate:
|
||||
duration = 0
|
||||
if isStack(path):# handle "stacked" videos
|
||||
for file in splitStacks(path): duration += self.__parseDuration(runtime, file)
|
||||
else: duration = self.__parseDuration(runtime, path, item, save)
|
||||
if duration > 0: runtime = duration
|
||||
self.log("getDuration, returning path = %s, runtime = %s" % (path, runtime))
|
||||
return runtime
|
||||
|
||||
|
||||
def getTotRuntime(self, items=[]):
|
||||
total = sum([self._getRuntime(item) for item in items])
|
||||
self.log("getTotRuntime, items = %s, total = %s" % (len(items), total))
|
||||
return total
|
||||
|
||||
|
||||
def getTotDuration(self, items=[]):
|
||||
total = sum([self.getDuration(item.get('file'),item) for item in items])
|
||||
self.log("getTotDuration, items = %s, total = %s" % (len(items), total))
|
||||
return total
|
||||
|
||||
|
||||
def __parseDuration(self, runtime, path, item={}, save=SETTINGS.getSettingBool('Store_Duration')):
|
||||
self.log("__parseDuration, runtime = %s, path = %s, save = %s" % (runtime, path, save))
|
||||
duration = self.videoParser.getVideoLength(path.replace("\\\\", "\\"), item, self)
|
||||
if not path.startswith(tuple(VFS_TYPES)):
|
||||
## duration diff. safe guard, how different are the two values? if > 45% don't save to Kodi.
|
||||
rundiff = int(percentDiff(runtime, duration))
|
||||
runsafe = False
|
||||
if (rundiff <= 45 and rundiff > 0) or (rundiff == 100 and (duration == 0 or runtime == 0)) or (rundiff == 0 and (duration > 0 and runtime > 0)) or (duration > runtime): runsafe = True
|
||||
self.log("__parseDuration, path = %s, runtime = %s, duration = %s, difference = %s%%, safe = %s" % (path, runtime, duration, rundiff, runsafe))
|
||||
## save parsed duration to Kodi database, if enabled.
|
||||
if runsafe:
|
||||
runtime = duration
|
||||
if save and not path.startswith(tuple(VFS_TYPES)): self.queDuration(item, duration)
|
||||
else: runtime = duration
|
||||
self.log("__parseDuration, returning runtime = %s" % (runtime))
|
||||
return runtime
|
||||
|
||||
|
||||
def queDuration(self, item={}, duration=0, runtime=0):
|
||||
mtypes = {'video' : {},
|
||||
'movie' : {"method":"VideoLibrary.SetMovieDetails" ,"params":{"movieid" :item.get('id',-1) ,"runtime": runtime,"resume": {"position": item.get('position',0.0),"total": duration}}},
|
||||
'movies' : {"method":"VideoLibrary.SetMovieDetails" ,"params":{"movieid" :item.get('movieid',-1) ,"runtime": runtime,"resume": {"position": item.get('position',0.0),"total": duration}}},
|
||||
'episode' : {"method":"VideoLibrary.SetEpisodeDetails" ,"params":{"episodeid" :item.get('id',-1) ,"runtime": runtime,"resume": {"position": item.get('position',0.0),"total": duration}}},
|
||||
'episodes' : {"method":"VideoLibrary.SetEpisodeDetails" ,"params":{"episodeid" :item.get('episodeid',-1) ,"runtime": runtime,"resume": {"position": item.get('position',0.0),"total": duration}}},
|
||||
'musicvideo' : {"method":"VideoLibrary.SetMusicVideoDetails","params":{"musicvideoid":item.get('id',-1) ,"runtime": runtime,"resume": {"position": item.get('position',0.0),"total": duration}}},
|
||||
'musicvideos': {"method":"VideoLibrary.SetMusicVideoDetails","params":{"musicvideoid":item.get('musicvideoid',-1) ,"runtime": runtime,"resume": {"position": item.get('position',0.0),"total": duration}}},
|
||||
'song' : {"method":"AudioLibrary.SetSongDetails" ,"params":{"songid" :item.get('id',-1) ,"runtime": runtime,"resume": {"position": item.get('position',0.0),"total": duration}}},
|
||||
'songs' : {"method":"AudioLibrary.SetSongDetails" ,"params":{"songid" :item.get('songid',-1) ,"runtime": runtime,"resume": {"position": item.get('position',0.0),"total": duration}}}}
|
||||
try:
|
||||
mtype = mtypes.get(item.get('type'))
|
||||
if mtype.get('params'):
|
||||
if duration == 0: mtype['params'].pop('resume') #save file duration meta
|
||||
elif runtime == 0: mtype['params'].pop('runtime') #save player runtime meta
|
||||
id = (item.get('id') or item.get('movieid') or item.get('episodeid') or item.get('musicvideoid') or item.get('songid'))
|
||||
self.log('[%s] queDuration, media = %s, duration = %s, runtime = %s'%(id,item['type'],duration,runtime))
|
||||
self.queueJSON(mtype['params'])
|
||||
except Exception as e: self.log("queDuration, failed! %s\nmtype = %s\nitem = %s"%(e,mtype,item), xbmc.LOGERROR)
|
||||
|
||||
|
||||
def quePlaycount(self, item, save=SETTINGS.getSettingBool('Rollback_Watched')):
|
||||
param = {'video' : {},
|
||||
'movie' : {"method":"VideoLibrary.SetMovieDetails" ,"params":{"movieid" :item.get('id',-1) ,"playcount": item.get('playcount',0),"resume": {"position": item.get('position',0.0),"total": item.get('total',0.0)}}},
|
||||
'movies' : {"method":"VideoLibrary.SetMovieDetails" ,"params":{"movieid" :item.get('movieid',-1) ,"playcount": item.get('playcount',0),"resume": {"position": item.get('position',0.0),"total": item.get('total',0.0)}}},
|
||||
'episode' : {"method":"VideoLibrary.SetEpisodeDetails" ,"params":{"episodeid" :item.get('id',-1) ,"playcount": item.get('playcount',0),"resume": {"position": item.get('position',0.0),"total": item.get('total',0.0)}}},
|
||||
'episodes' : {"method":"VideoLibrary.SetEpisodeDetails" ,"params":{"episodeid" :item.get('episodeid',-1) ,"playcount": item.get('playcount',0),"resume": {"position": item.get('position',0.0),"total": item.get('total',0.0)}}},
|
||||
'musicvideo' : {"method":"VideoLibrary.SetMusicVideoDetails","params":{"musicvideoid":item.get('id',-1) ,"playcount": item.get('playcount',0),"resume": {"position": item.get('position',0.0),"total": item.get('total',0.0)}}},
|
||||
'musicvideos': {"method":"VideoLibrary.SetMusicVideoDetails","params":{"musicvideoid":item.get('musicvideoid',-1) ,"playcount": item.get('playcount',0),"resume": {"position": item.get('position',0.0),"total": item.get('total',0.0)}}},
|
||||
'song' : {"method":"AudioLibrary.SetSongDetails" ,"params":{"songid" :item.get('id',-1) ,"playcount": item.get('playcount',0),"resume": {"position": item.get('position',0.0),"total": item.get('total',0.0)}}},
|
||||
'songs' : {"method":"AudioLibrary.SetSongDetails" ,"params":{"songid" :item.get('songid',-1) ,"playcount": item.get('playcount',0),"resume": {"position": item.get('position',0.0),"total": item.get('total',0.0)}}}}
|
||||
|
||||
if not item.get('file','plugin://').startswith(tuple(VFS_TYPES)):
|
||||
try:
|
||||
params = param.get(item.get('type'))
|
||||
self.log('quePlaycount, params = %s'%(params.get('params',{})))
|
||||
self.queueJSON(params)
|
||||
except: pass
|
||||
|
||||
|
||||
def requestList(self, citem, path, media='video', page=SETTINGS.getSettingInt('Page_Limit'), sort={}, limits={}, query={}):
|
||||
# {"method": "VideoLibrary.GetEpisodes",
|
||||
# "params": {
|
||||
# "properties": ["title"],
|
||||
# "sort": {"ignorearticle": true,
|
||||
# "method": "label",
|
||||
# "order": "ascending",
|
||||
# "useartistsortname": true},
|
||||
# "limits": {"end": 0, "start": 0},
|
||||
# "filter": {"and": [{"field": "title", "operator": "contains", "value": "Star Wars"}]}}}
|
||||
|
||||
##################################
|
||||
|
||||
# {"method": "Files.GetDirectory",
|
||||
# "params": {
|
||||
# "directory": "videodb://tvshows/studios/-1/-1/-1/",
|
||||
# "media": "video",
|
||||
# "properties": ["title"],
|
||||
# "sort": {"ignorearticle": true,
|
||||
# "method": "label",
|
||||
# "order": "ascending",
|
||||
# "useartistsortname": true},
|
||||
# "limits": {"end": 25, "start": 0}}}
|
||||
|
||||
param = {}
|
||||
if query: #library query
|
||||
getDirectory = False
|
||||
param['filter'] = query.get('filter',{})
|
||||
param["properties"] = self.getEnums(query['enum'], type='items')
|
||||
else: #vfs path
|
||||
getDirectory = True
|
||||
param["media"] = media
|
||||
param["directory"] = escapeDirJSON(path)
|
||||
param["properties"] = self.getEnums("List.Fields.Files", type='items')
|
||||
self.log("requestList, id: %s, getDirectory = %s, media = %s, limit = %s, sort = %s, query = %s, limits = %s\npath = %s"%(citem['id'],getDirectory,media,page,sort,query,limits,path))
|
||||
|
||||
if limits.get('end',-1) == -1: #-1 unlimited pagination, replace with autoPagination.
|
||||
limits = self.autoPagination(citem['id'], path, query) #get
|
||||
self.log('[%s] requestList, autoPagination limits = %s'%(citem['id'],limits))
|
||||
if limits.get('total',0) > page and sort.get("method","") == "random":
|
||||
limits = self.randomPagination(page,limits)
|
||||
self.log('[%s] requestList, generating random limits = %s'%(citem['id'],limits))
|
||||
|
||||
param["limits"] = {}
|
||||
param["limits"]["start"] = 0 if limits.get('end', 0) == -1 else limits.get('end', 0)
|
||||
param["limits"]["end"] = abs(limits.get('end', 0) + page)
|
||||
param["sort"] = sort
|
||||
self.log('[%s] requestList, page = %s\nparam = %s'%(citem['id'], page, param))
|
||||
|
||||
if getDirectory:
|
||||
results = self.getDirectory(param,timeout=float(SETTINGS.getSettingInt('RPC_Timer')*60))
|
||||
if 'filedetails' in results: key = 'filedetails'
|
||||
else: key = 'files'
|
||||
else:
|
||||
results = self.getLibrary(query['method'],param, cache=False)
|
||||
key = query.get('key',list(results.keys())[0])
|
||||
|
||||
items, limits, errors = results.get(key,[]), results.get('limits',param["limits"]), results.get('error',{})
|
||||
if (limits.get('end',0) >= limits.get('total',0) or limits.get('start',0) >= limits.get('total',0)):
|
||||
# restart page to 0, exceeding boundaries.
|
||||
self.log('[%s] requestList, resetting limits to 0'%(citem['id']))
|
||||
limits = {"end": 0, "start": 0, "total": limits.get('total',0)}
|
||||
|
||||
if len(items) == 0 and limits.get('total',0) > 0:
|
||||
# retry last request with fresh limits when no items are returned.
|
||||
self.log("[%s] requestList, trying again with start limits at 0"%(citem['id']))
|
||||
return self.requestList(citem, path, media, page, sort, {"end": 0, "start": 0, "total": limits.get('total',0)}, query)
|
||||
else:
|
||||
self.autoPagination(citem['id'], path, query, limits) #set
|
||||
self.log("[%s] requestList, return items = %s" % (citem['id'], len(items)))
|
||||
return items, limits, errors
|
||||
|
||||
|
||||
def resetPagination(self, id, path, query={}, limits={"end": 0, "start": 0, "total":0}):
|
||||
return self.autoPagination(id, path, query, limits)
|
||||
|
||||
|
||||
def autoPagination(self, id, path, query={}, limits={}):
|
||||
if not limits: return (self.cache.get('autoPagination.%s.%s.%s'%(id,getMD5(path),getMD5(dumpJSON(query))), checksum=id, json_data=True) or {"end": 0, "start": 0, "total":0})
|
||||
else: return self.cache.set('autoPagination.%s.%s.%s'%(id,getMD5(path),getMD5(dumpJSON(query))), limits, checksum=id, expiration=datetime.timedelta(days=28), json_data=True)
|
||||
|
||||
|
||||
def randomPagination(self, page=SETTINGS.getSettingInt('Page_Limit'), limits={}, start=0):
|
||||
if limits.get('total',0) > page: start = random.randrange(0, (limits.get('total',0)-page), page)
|
||||
return {"end": start, "start": start, "total":limits.get('total',0)}
|
||||
|
||||
|
||||
@cacheit(checksum=PROPERTIES.getInstanceID())
|
||||
def buildWebBase(self, local=False):
|
||||
port = 80
|
||||
username = 'kodi'
|
||||
password = ''
|
||||
secure = False
|
||||
enabled = True
|
||||
settings = self.getSetting('control','services')
|
||||
for setting in settings:
|
||||
if setting.get('id','').lower() == 'services.webserver' and not setting.get('value'):
|
||||
enabled = False
|
||||
DIALOG.notificationDialog(LANGUAGE(32131))
|
||||
break
|
||||
if setting.get('id','').lower() == 'services.webserverusername': username = setting.get('value')
|
||||
elif setting.get('id','').lower() == 'services.webserverport': port = setting.get('value')
|
||||
elif setting.get('id','').lower() == 'services.webserverpassword': password = setting.get('value')
|
||||
elif setting.get('id','').lower() == 'services.webserverssl' and setting.get('value'): secure = True
|
||||
username = '{0}:{1}@'.format(username, password) if username and password else ''
|
||||
protocol = 'https' if secure else 'http'
|
||||
if local: ip = 'localhost'
|
||||
else: ip = SETTINGS.getIP()
|
||||
webURL = '{0}://{1}{2}:{3}'.format(protocol,username,ip, port)
|
||||
self.log("buildWebBase; returning %s"%(webURL))
|
||||
return webURL
|
||||
|
||||
|
||||
def padItems(self, files, page=SETTINGS.getSettingInt('Page_Limit')):
|
||||
# Balance media limits, by filling with duplicates to meet min. pagination.
|
||||
self.log("padItems; files In = %s"%(len(files)))
|
||||
if len(files) < page:
|
||||
iters = cycle(files)
|
||||
while not self.service.monitor.abortRequested() and (len(files) < page and len(files) > 0):
|
||||
item = next(iters).copy()
|
||||
if self.service.monitor.waitForAbort(0.0001): break
|
||||
elif self.getDuration(item.get('file'),item) == 0:
|
||||
try: files.pop(files.index(item))
|
||||
except: break
|
||||
else: files.append(item)
|
||||
self.log("padItems; files Out = %s"%(len(files)))
|
||||
return files
|
||||
|
||||
|
||||
def inputFriendlyName(self):
|
||||
friendly = self.getSettingValue("services.devicename")
|
||||
self.log("inputFriendlyName, name = %s"%(friendly))
|
||||
if not friendly or friendly.lower() == 'kodi':
|
||||
with PROPERTIES.interruptActivity():
|
||||
if DIALOG.okDialog(LANGUAGE(32132)%(friendly)):
|
||||
friendly = DIALOG.inputDialog(LANGUAGE(30122), friendly)
|
||||
if not friendly or friendly.lower() == 'kodi':
|
||||
return self.inputFriendlyName()
|
||||
else:
|
||||
self.setSettingValue("services.devicename",friendly,queue=False)
|
||||
self.log('inputFriendlyName, setting device name = %s'%(friendly))
|
||||
return friendly
|
||||
|
||||
|
||||
def getCallback(self, sysInfo={}):
|
||||
self.log('[%s] getCallback, mode = %s, radio = %s, isPlaylist = %s'%(sysInfo.get('chid'),sysInfo.get('mode'),sysInfo.get('radio',False),sysInfo.get('isPlaylist',False)))
|
||||
def _matchJSON():#requires 'pvr://' json whitelisting
|
||||
results = self.getDirectory(param={"directory":"pvr://channels/{dir}/".format(dir={'True':'radio','False':'tv'}[str(sysInfo.get('radio',False))])}, cache=False).get('files',[])
|
||||
for dir in [ADDON_NAME,'All channels']: #todo "All channels" may not work with non-English translations!
|
||||
for result in results:
|
||||
if result.get('label','').lower().startswith(dir.lower()):
|
||||
self.log('getCallback: _matchJSON, found dir = %s'%(result.get('file')))
|
||||
channels = self.getDirectory(param={"directory":result.get('file')},checksum=PROPERTIES.getInstanceID(),expiration=datetime.timedelta(minutes=FIFTEEN)).get('files',[])
|
||||
for item in channels:
|
||||
if item.get('label','').lower() == sysInfo.get('name','').lower() and decodePlot(item.get('plot','')).get('citem',{}).get('id') == sysInfo.get('chid'):
|
||||
self.log('[%s] getCallback: _matchJSON, found file = %s'%(sysInfo.get('chid'),item.get('file')))
|
||||
return item.get('file')
|
||||
|
||||
if sysInfo.get('mode','').lower() == 'live' and sysInfo.get('chpath'):
|
||||
callback = sysInfo.get('chpath')
|
||||
elif sysInfo.get('isPlaylist'):
|
||||
callback = sysInfo.get('citem',{}).get('url')
|
||||
elif sysInfo.get('mode','').lower() == 'vod' and sysInfo.get('nitem',{}).get('file'):
|
||||
callback = sysInfo.get('nitem',{}).get('file')
|
||||
else:
|
||||
callback = sysInfo.get('callback','')
|
||||
if not callback: callback = _matchJSON()
|
||||
self.log('getCallback: returning callback = %s'%(callback))
|
||||
return callback# or (('%s%s'%(self.sysARG[0],self.sysARG[2])).split('%s&'%(slugify(ADDON_NAME))))[0])
|
||||
|
||||
|
||||
def matchChannel(self, chname: str, id: str, radio: bool=False, extend=True):
|
||||
self.log('[%s] matchChannel, chname = %s, radio = %s'%(id,chname,radio))
|
||||
def __match():
|
||||
channels = self.getPVRChannels(radio)
|
||||
for channel in channels:
|
||||
if channel.get('label','').lower() == chname.lower():
|
||||
for key in ['broadcastnow', 'broadcastnext']:
|
||||
if decodePlot(channel.get(key,{}).get('plot','')).get('citem',{}).get('id') == id:
|
||||
channel['broadcastnext'] = [channel.get('broadcastnext',{})]
|
||||
self.log('[%s] matchChannel: __match, found pvritem = %s'%(id,channel))
|
||||
return channel
|
||||
|
||||
def __extend(pvritem: dict={}) -> dict:
|
||||
channelItem = {}
|
||||
def _parseBroadcast(broadcast={}):
|
||||
if broadcast.get('progresspercentage',0) == 100:
|
||||
channelItem.setdefault('broadcastpast',[]).append(broadcast)
|
||||
elif broadcast.get('progresspercentage',0) > 0 and broadcast.get('progresspercentage',100) < 100:
|
||||
channelItem['broadcastnow'] = broadcast
|
||||
elif broadcast.get('progresspercentage',0) == 0 and broadcast.get('progresspercentage',100) < 100:
|
||||
channelItem.setdefault('broadcastnext',[]).append(broadcast)
|
||||
|
||||
broadcasts = self.getPVRBroadcasts(pvritem.get('channelid',{}))
|
||||
[_parseBroadcast(broadcast) for broadcast in broadcasts]
|
||||
pvritem['broadcastnext'] = channelItem.get('broadcastnext',pvritem['broadcastnext'])
|
||||
self.log('matchChannel: __extend, broadcastnext = %s entries'%(len(pvritem['broadcastnext'])))
|
||||
return pvritem
|
||||
|
||||
cacheName = 'matchChannel.%s'%(getMD5('%s.%s.%s.%s'%(chname,id,radio,extend)))
|
||||
cacheResponse = (self.cache.get(cacheName, checksum=PROPERTIES.getInstanceID(), json_data=True) or {})
|
||||
if not cacheResponse:
|
||||
pvrItem = __match()
|
||||
if pvrItem:
|
||||
if extend: pvrItem = __extend(pvrItem)
|
||||
cacheResponse = self.cache.set(cacheName, pvrItem, checksum=PROPERTIES.getInstanceID(), expiration=datetime.timedelta(seconds=FIFTEEN), json_data=True)
|
||||
else: return {}
|
||||
return cacheResponse
|
||||
|
||||
|
||||
def getNextItem(self, citem={}, nitem={}): #return next broadcast ignoring fillers
|
||||
if not nitem: nitem = decodePlot(BUILTIN.getInfoLabel('NextPlot','VideoPlayer'))
|
||||
nextitems = sorted(self.matchChannel(citem.get('name',''), citem.get('id',''), citem.get('radio',False)).get('broadcastnext',[]), key=itemgetter('starttime'))
|
||||
for nextitem in nextitems:
|
||||
if not isFiller(nextitem): return decodePlot(nextitem.get('plot',''))
|
||||
return nitem
|
||||
|
||||
|
||||
def toggleShowLog(self, state=False):
|
||||
self.log('toggleShowLog, state = %s'%(state))
|
||||
if SETTINGS.getSettingBool('Enable_PVR_RELOAD'): #check that users allow alternations to kodi.
|
||||
opState = not bool(state)
|
||||
if self.getSettingValue("debug.showloginfo") == opState:
|
||||
self.setSettingValue("debug.showloginfo",state,queue=False)
|
||||
|
||||
1894
Kodi/Lenovo/addons/plugin.video.pseudotv.live/resources/lib/kodi.py
Normal file
1894
Kodi/Lenovo/addons/plugin.video.pseudotv.live/resources/lib/kodi.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,507 @@
|
||||
# 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 predefined import Predefined
|
||||
from resources import Resources
|
||||
from channels import Channels
|
||||
|
||||
#constants
|
||||
REG_KEY = 'PseudoTV_Recommended.%s'
|
||||
|
||||
class Service:
|
||||
from jsonrpc import JSONRPC
|
||||
player = PLAYER()
|
||||
monitor = MONITOR()
|
||||
jsonRPC = JSONRPC()
|
||||
def _interrupt(self) -> bool:
|
||||
return PROPERTIES.isPendingInterrupt()
|
||||
def _suspend(self) -> bool:
|
||||
return PROPERTIES.isPendingSuspend()
|
||||
|
||||
|
||||
class Library:
|
||||
def __init__(self, service=None):
|
||||
if service is None: service = Service()
|
||||
self.service = service
|
||||
self.jsonRPC = service.jsonRPC
|
||||
self.cache = service.jsonRPC.cache
|
||||
self.predefined = Predefined()
|
||||
self.channels = Channels()
|
||||
self.resources = Resources(service=self.service)
|
||||
|
||||
self.pCount = 0
|
||||
self.pDialog = None
|
||||
self.pMSG = ''
|
||||
self.pHeader = ''
|
||||
|
||||
self.libraryDATA = getJSON(LIBRARYFLE_DEFAULT)
|
||||
self.libraryTEMP = self.libraryDATA['library'].pop('Item')
|
||||
self.libraryDATA.update(self._load())
|
||||
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
return log('%s: %s'%(self.__class__.__name__,msg),level)
|
||||
|
||||
|
||||
def _load(self, file=LIBRARYFLEPATH):
|
||||
return getJSON(file)
|
||||
|
||||
|
||||
def _save(self, file=LIBRARYFLEPATH):
|
||||
self.libraryDATA['uuid'] = SETTINGS.getMYUUID()
|
||||
return setJSON(file, self.libraryDATA)
|
||||
|
||||
|
||||
def getLibrary(self, type=None):
|
||||
self.log('getLibrary, type = %s'%(type))
|
||||
if type is None: return self.libraryDATA.get('library',{})
|
||||
else: return self.libraryDATA.get('library',{}).get(type,[])
|
||||
|
||||
|
||||
def enableByName(self, type, names=[]):
|
||||
self.log('enableByName, type = %s, names = %s'%(type, names))
|
||||
items = self.getLibrary(type)
|
||||
for name in names:
|
||||
for item in items:
|
||||
if name.lower() == item.get('name','').lower(): item['enabled'] = True
|
||||
else: item['enabled'] = False
|
||||
return self.setLibrary(type, items)
|
||||
|
||||
|
||||
def setLibrary(self, type, items=[]):
|
||||
self.log('setLibrary, type = %s, items = %s'%(type,len(items)))
|
||||
self.libraryDATA['library'][type] = items
|
||||
enabled = self.getEnabled(type, items)
|
||||
PROPERTIES.setEXTPropertyBool('%s.has.%s'%(ADDON_ID,slugify(type)),len(items) > 0)
|
||||
PROPERTIES.setEXTPropertyBool('%s.has.%s.enabled'%(ADDON_ID,slugify(type)),len(enabled) > 0)
|
||||
SETTINGS.setSetting('Select_%s'%(slugify(type)),'[COLOR=orange][B]%s[/COLOR][/B]/[COLOR=dimgray]%s[/COLOR]'%(len(enabled),len(items)))
|
||||
return self._save()
|
||||
|
||||
|
||||
def getEnabled(self, type, items=None):
|
||||
if items is None: items = self.getLibrary(type)
|
||||
return [item for item in items if item.get('enabled',False)]
|
||||
|
||||
|
||||
def updateLibrary(self, force: bool=False) -> bool:
|
||||
def __funcs():
|
||||
return {
|
||||
"Playlists" :{'func':self.getPlaylists ,'life':datetime.timedelta(minutes=FIFTEEN)},
|
||||
"TV Networks" :{'func':self.getNetworks ,'life':datetime.timedelta(days=MAX_GUIDEDAYS)},
|
||||
"TV Shows" :{'func':self.getTVShows ,'life':datetime.timedelta(hours=MAX_GUIDEDAYS)},
|
||||
"TV Genres" :{'func':self.getTVGenres ,'life':datetime.timedelta(days=MAX_GUIDEDAYS)},
|
||||
"Movie Genres" :{'func':self.getMovieGenres ,'life':datetime.timedelta(days=MAX_GUIDEDAYS)},
|
||||
"Movie Studios":{'func':self.getMovieStudios,'life':datetime.timedelta(days=MAX_GUIDEDAYS)},
|
||||
"Mixed Genres" :{'func':self.getMixedGenres ,'life':datetime.timedelta(days=MAX_GUIDEDAYS)},
|
||||
"Mixed" :{'func':self.getMixed ,'life':datetime.timedelta(minutes=FIFTEEN)},
|
||||
"Recommended" :{'func':self.getRecommend ,'life':datetime.timedelta(hours=MAX_GUIDEDAYS)},
|
||||
"Services" :{'func':self.getServices ,'life':datetime.timedelta(hours=MAX_GUIDEDAYS)},
|
||||
"Music Genres" :{'func':self.getMusicGenres ,'life':datetime.timedelta(days=MAX_GUIDEDAYS)}
|
||||
}
|
||||
|
||||
def __fill(type, func):
|
||||
try: items = func()
|
||||
except Exception as e:
|
||||
self.log("__fill, %s failed! %s"%(type,e), xbmc.LOGERROR)
|
||||
items = []
|
||||
self.log('__fill, returning %s (%s)'%(type,len(items)))
|
||||
return items
|
||||
|
||||
def __update(type, items, existing=[]):
|
||||
if not existing: existing = self.channels.getType(type)
|
||||
self.log('__update, type = %s, items = %s, existing = %s'%(type,len(items),len(existing)))
|
||||
for item in items:
|
||||
if not item.get('enabled',False):
|
||||
for eitem in existing:
|
||||
if getChannelSuffix(item.get('name'), type).lower() == eitem.get('name','').lower():
|
||||
if eitem['logo'] not in [LOGO,COLOR_LOGO] and item['logo'] in [LOGO,COLOR_LOGO]: item['logo'] = eitem['logo']
|
||||
item['enabled'] = True
|
||||
break
|
||||
item['logo'] = self.resources.getLogo(item,item.get('logo',LOGO)) #update logo
|
||||
entry = self.libraryTEMP.copy()
|
||||
entry.update(item)
|
||||
yield entry
|
||||
|
||||
if force: #clear library cache.
|
||||
with BUILTIN.busy_dialog():
|
||||
for label, params in list(__funcs().items()):
|
||||
DIALOG.notificationDialog(LANGUAGE(30070)%(label),time=5)
|
||||
self.cache.clear("%s.%s"%(self.__class__.__name__,params['func'].__name__),wait=5)
|
||||
|
||||
|
||||
complete = True
|
||||
types = list(__funcs().keys())
|
||||
|
||||
for idx, type in enumerate(types):
|
||||
self.pMSG = type
|
||||
self.pCount = int(idx*100//len(types))
|
||||
self.pHeader = '%s, %s %s'%(ADDON_NAME,LANGUAGE(32028),LANGUAGE(32041))
|
||||
self.pDialog = DIALOG.progressBGDialog(header=self.pHeader)
|
||||
|
||||
if (self.service._interrupt() or self.service._suspend()) and PROPERTIES.hasFirstRun():
|
||||
self.log("updateLibrary, _interrupt")
|
||||
complete = False
|
||||
self.pDialog = DIALOG.progressBGDialog(self.pCount, self.pDialog, '%s: %s'%(LANGUAGE(32144),LANGUAGE(32213)), self.pHeader)
|
||||
break
|
||||
|
||||
self.pDialog = DIALOG.progressBGDialog(self.pCount, self.pDialog, self.pMSG, self.pHeader)
|
||||
cacheResponse = self.cache.get("%s.%s"%(self.__class__.__name__,__funcs()[type]['func'].__name__))
|
||||
if not cacheResponse:
|
||||
self.pHeader = '%s, %s %s'%(ADDON_NAME,LANGUAGE(32022),LANGUAGE(32041))
|
||||
cacheResponse = self.cache.set("%s.%s"%(self.__class__.__name__,__funcs()[type]['func'].__name__), __fill(type, __funcs()[type]['func']), expiration=__funcs()[type]['life'])
|
||||
|
||||
if complete:
|
||||
self.setLibrary(type, list(__update(type,cacheResponse,self.getEnabled(type))))
|
||||
self.log("updateLibrary, type = %s, saved items = %s"%(type,len(cacheResponse)))
|
||||
|
||||
self.pDialog = DIALOG.progressBGDialog(100, self.pDialog, header='%s, %s %s'%(ADDON_NAME,LANGUAGE(32041),LANGUAGE(32025)))
|
||||
|
||||
self.log('updateLibrary, force = %s, complete = %s'%(force, complete))
|
||||
return complete
|
||||
|
||||
|
||||
def resetLibrary(self, ATtypes=AUTOTUNE_TYPES):
|
||||
self.log('resetLibrary')
|
||||
for ATtype in ATtypes:
|
||||
items = self.getLibrary(ATtype)
|
||||
for item in items:
|
||||
item['enabled'] = False #disable everything before selecting new items.
|
||||
self.setLibrary(ATtype, items)
|
||||
|
||||
|
||||
def updateProgress(self, percent, message, header):
|
||||
if self.pDialog: self.pDialog = DIALOG.progressBGDialog(percent, self.pDialog, message=message, header=header)
|
||||
|
||||
|
||||
def getNetworks(self):
|
||||
return self.getTVInfo().get('studios',[])
|
||||
|
||||
|
||||
def getTVGenres(self):
|
||||
return self.getTVInfo().get('genres',[])
|
||||
|
||||
|
||||
def getTVShows(self):
|
||||
return self.getTVInfo().get('shows',[])
|
||||
|
||||
|
||||
def getMovieStudios(self):
|
||||
return self.getMovieInfo().get('studios',[])
|
||||
|
||||
|
||||
def getMovieGenres(self):
|
||||
return self.getMovieInfo().get('genres',[])
|
||||
|
||||
|
||||
def getMusicGenres(self):
|
||||
return self.getMusicInfo().get('genres',[])
|
||||
|
||||
|
||||
def getMixedGenres(self):
|
||||
MixedGenreList = []
|
||||
tvGenres = self.getTVGenres()
|
||||
movieGenres = self.getMovieGenres()
|
||||
for tv in [tv for tv in tvGenres for movie in movieGenres if tv.get('name','').lower() == movie.get('name','').lower()]:
|
||||
MixedGenreList.append({'name':tv.get('name'),'type':"Mixed Genres",'path':self.predefined.createGenreMixedPlaylist(tv.get('name')),'logo':tv.get('logo'),'rules':{"800":{"values":{"0":tv.get('name')}}}})
|
||||
self.log('getMixedGenres, genres = %s' % (len(MixedGenreList)))
|
||||
return sorted(MixedGenreList,key=itemgetter('name'))
|
||||
|
||||
|
||||
def getMixed(self):
|
||||
MixedList = []
|
||||
MixedList.append({'name':LANGUAGE(32001), 'type':"Mixed",'path':self.predefined.createMixedRecent() ,'logo':self.resources.getLogo({'name':LANGUAGE(32001),'type':"Mixed"})}) #"Recently Added"
|
||||
MixedList.append({'name':LANGUAGE(32002), 'type':"Mixed",'path':self.predefined.createSeasonal() ,'logo':self.resources.getLogo({'name':LANGUAGE(32002),'type':"Mixed"}),'rules':{"800":{"values":{"0":LANGUAGE(32002)}}}}) #"Seasonal"
|
||||
MixedList.extend(self.getPVRRecordings())#"PVR Recordings"
|
||||
MixedList.extend(self.getPVRSearches()) #"PVR Searches"
|
||||
self.log('getMixed, mixed = %s' % (len(MixedList)))
|
||||
return sorted(MixedList,key=itemgetter('name'))
|
||||
|
||||
|
||||
def getPVRRecordings(self):
|
||||
recordList = []
|
||||
json_response = self.jsonRPC.getPVRRecordings()
|
||||
paths = [item.get('file') for idx, item in enumerate(json_response) if item.get('label','').endswith('(%s)'%(ADDON_NAME))]
|
||||
if len(paths) > 0: recordList.append({'name':LANGUAGE(32003),'type':"Mixed",'path':[paths],'logo':self.resources.getLogo({'name':LANGUAGE(32003),'type':"Mixed"})})
|
||||
self.log('getPVRRecordings, recordings = %s' % (len(recordList)))
|
||||
return sorted(recordList,key=itemgetter('name'))
|
||||
|
||||
|
||||
def getPVRSearches(self):
|
||||
searchList = []
|
||||
json_response = self.jsonRPC.getPVRSearches()
|
||||
for idx, item in enumerate(json_response):
|
||||
if not item.get('file'): continue
|
||||
searchList.append({'name':"%s (%s)"%(item.get('label',LANGUAGE(32241)),LANGUAGE(32241)),'type':"Mixed",'path':[item.get('file')],'logo':self.resources.getLogo({'name':item.get('label',LANGUAGE(32241)),'type':"Mixed"})})
|
||||
self.log('getPVRSearches, searches = %s' % (len(searchList)))
|
||||
return sorted(searchList,key=itemgetter('name'))
|
||||
|
||||
|
||||
def getPlaylists(self):
|
||||
PlayList = []
|
||||
for type in ['video','mixed','music']:
|
||||
self.updateProgress(self.pCount,'%s: %s'%(self.pMSG,LANGUAGE(32140)),self.pHeader)
|
||||
results = self.jsonRPC.getSmartPlaylists(type)
|
||||
for idx, result in enumerate(results):
|
||||
self.updateProgress(self.pCount,'%s (%s): %s%%'%(self.pMSG,type.title(),int((idx)*100//len(results))),self.pHeader)
|
||||
if not result.get('label'): continue
|
||||
logo = result.get('thumbnail')
|
||||
if not logo: logo = self.resources.getLogo({'name':result.get('label',''),'type':"Custom"})
|
||||
PlayList.append({'name':result.get('label'),'type':"%s Playlist"%(type.title()),'path':[result.get('file')],'logo':logo})
|
||||
self.log('getPlaylists, PlayList = %s' % (len(PlayList)))
|
||||
PlayList = sorted(PlayList,key=itemgetter('name'))
|
||||
PlayList = sorted(PlayList,key=itemgetter('type'))
|
||||
return PlayList
|
||||
|
||||
|
||||
@cacheit()
|
||||
def getTVInfo(self, sortbycount=True):
|
||||
self.log('getTVInfo')
|
||||
if BUILTIN.hasTV():
|
||||
NetworkList = Counter()
|
||||
ShowGenreList = Counter()
|
||||
TVShows = Counter()
|
||||
self.updateProgress(self.pCount,'%s: %s'%(self.pMSG,LANGUAGE(32140)),self.pHeader)
|
||||
json_response = self.jsonRPC.getTVshows()
|
||||
|
||||
for idx, info in enumerate(json_response):
|
||||
self.updateProgress(self.pCount,'%s: %s%%'%(self.pMSG,int((idx)*100//len(json_response))),self.pHeader)
|
||||
if not info.get('label'): continue
|
||||
TVShows.update({json.dumps({'name': info.get('label'), 'type':"TV Shows", 'path': self.predefined.createShowPlaylist(info.get('label')), 'logo': info.get('art', {}).get('clearlogo', ''),'rules':{"800":{"values":{"0":info.get('label')}}}}): info.get('episode', 0)})
|
||||
NetworkList.update([studio for studio in info.get('studio', [])])
|
||||
ShowGenreList.update([genre for genre in info.get('genre', [])])
|
||||
|
||||
if sortbycount:
|
||||
TVShows = [json.loads(x[0]) for x in sorted(TVShows.most_common(250))]
|
||||
NetworkList = [x[0] for x in sorted(NetworkList.most_common(50))]
|
||||
ShowGenreList = [x[0] for x in sorted(ShowGenreList.most_common(25))]
|
||||
else:
|
||||
TVShows = (sorted(map(json.loads, list(TVShows.keys())), key=itemgetter('name')))
|
||||
del TVShows[250:]
|
||||
NetworkList = (sorted(set(list(NetworkList.keys()))))
|
||||
del NetworkList[250:]
|
||||
ShowGenreList = (sorted(set(list(ShowGenreList.keys()))))
|
||||
|
||||
#search resources for studio/genre logos
|
||||
nNetworkList = []
|
||||
for idx, network in enumerate(NetworkList):
|
||||
self.updateProgress(self.pCount,'%s: %s%%'%(self.pMSG,int((idx)*100//len(NetworkList))),self.pHeader)
|
||||
nNetworkList.append({'name':network, 'type':"TV Networks", 'path': self.predefined.createNetworkPlaylist(network),'logo':self.resources.getLogo({'name':network,'type':"TV Networks"}),'rules':{"800":{"values":{"0":network}}}})
|
||||
NetworkList = nNetworkList
|
||||
|
||||
nShowGenreList = []
|
||||
for idx, tvgenre in enumerate(ShowGenreList):
|
||||
self.updateProgress(self.pCount,'%s: %s%%'%(self.pMSG,int((idx)*100//len(ShowGenreList))),self.pHeader)
|
||||
nShowGenreList.append({'name':tvgenre, 'type':"TV Genres" , 'path': self.predefined.createTVGenrePlaylist(tvgenre),'logo':self.resources.getLogo({'name':tvgenre,'type':"TV Genres"}),'rules':{"800":{"values":{"0":tvgenre}}}})
|
||||
ShowGenreList = nShowGenreList
|
||||
|
||||
else: NetworkList = ShowGenreList = TVShows = []
|
||||
self.log('getTVInfo, networks = %s, genres = %s, shows = %s' % (len(NetworkList), len(ShowGenreList), len(TVShows)))
|
||||
return {'studios':NetworkList,'genres':ShowGenreList,'shows':TVShows}
|
||||
|
||||
|
||||
@cacheit()
|
||||
def getMovieInfo(self, sortbycount=True):
|
||||
self.log('getMovieInfo')
|
||||
if BUILTIN.hasMovie():
|
||||
StudioList = Counter()
|
||||
MovieGenreList = Counter()
|
||||
self.updateProgress(self.pCount,'%s: %s'%(self.pMSG,LANGUAGE(32140)),self.pHeader)
|
||||
json_response = self.jsonRPC.getMovies() #we can't parse for genres directly from Kodi json ie.getGenres; because we need the weight of each genre to prioritize list.
|
||||
|
||||
for idx, info in enumerate(json_response):
|
||||
StudioList.update([studio for studio in info.get('studio', [])])
|
||||
MovieGenreList.update([genre for genre in info.get('genre', [])])
|
||||
|
||||
if sortbycount:
|
||||
StudioList = [x[0] for x in sorted(StudioList.most_common(25))]
|
||||
MovieGenreList = [x[0] for x in sorted(MovieGenreList.most_common(25))]
|
||||
else:
|
||||
StudioList = (sorted(set(list(StudioList.keys()))))
|
||||
del StudioList[250:]
|
||||
MovieGenreList = (sorted(set(list(MovieGenreList.keys()))))
|
||||
|
||||
#search resources for studio/genre logos
|
||||
nStudioList = []
|
||||
for idx, studio in enumerate(StudioList):
|
||||
self.updateProgress(self.pCount,'%s: %s%%'%(self.pMSG,int((idx)*100//len(StudioList))),self.pHeader)
|
||||
nStudioList.append({'name':studio, 'type':"Movie Studios", 'path': self.predefined.createStudioPlaylist(studio) ,'logo':self.resources.getLogo({'name':studio,'type':"Movie Studios"}),'rules':{"800":{"values":{"0":studio}}}})
|
||||
StudioList = nStudioList
|
||||
|
||||
nMovieGenreList = []
|
||||
for idx, genre in enumerate(MovieGenreList):
|
||||
self.updateProgress(self.pCount,'%s: %s%%'%(self.pMSG,int((idx)*100//len(MovieGenreList))),self.pHeader)
|
||||
nMovieGenreList.append({'name':genre, 'type':"Movie Genres" , 'path': self.predefined.createMovieGenrePlaylist(genre) ,'logo':self.resources.getLogo({'name':genre,'type':"Movie Genres"}) ,'rules':{"800":{"values":{"0":genre}}}})
|
||||
MovieGenreList = nMovieGenreList
|
||||
|
||||
else: StudioList = MovieGenreList = []
|
||||
self.log('getMovieInfo, studios = %s, genres = %s' % (len(StudioList), len(MovieGenreList)))
|
||||
return {'studios':StudioList,'genres':MovieGenreList}
|
||||
|
||||
|
||||
@cacheit()
|
||||
def getMusicInfo(self, sortbycount=True):
|
||||
self.log('getMusicInfo')
|
||||
if BUILTIN.hasMusic():
|
||||
MusicGenreList = Counter()
|
||||
self.updateProgress(self.pCount,'%s: %s'%(self.pMSG,LANGUAGE(32140)),self.pHeader)
|
||||
json_response = self.jsonRPC.getMusicGenres()
|
||||
|
||||
for idx, info in enumerate(json_response):
|
||||
MusicGenreList.update([genre.strip() for genre in info.get('label','').split(';')])
|
||||
|
||||
if sortbycount:
|
||||
MusicGenreList = [x[0] for x in sorted(MusicGenreList.most_common(50))]
|
||||
else:
|
||||
MusicGenreList = (sorted(set(list(MusicGenreList.keys()))))
|
||||
del MusicGenreList[250:]
|
||||
MusicGenreList = (sorted(set(list(MusicGenreList.keys()))))
|
||||
|
||||
#search resources for studio/genre logos
|
||||
nMusicGenreList = []
|
||||
for idx, genre in enumerate(MusicGenreList):
|
||||
self.updateProgress(self.pCount,'%s: %s%%'%(self.pMSG,int((idx)*100//len(MusicGenreList))),self.pHeader)
|
||||
nMusicGenreList.append({'name':genre, 'type':"Music Genres", 'path': self.predefined.createMusicGenrePlaylist(genre),'logo':self.resources.getLogo({'name':genre,'type':"Music Genres"})})
|
||||
MusicGenreList = nMusicGenreList
|
||||
|
||||
else: MusicGenreList = []
|
||||
self.log('getMusicInfo, found genres = %s' % (len(MusicGenreList)))
|
||||
return {'genres':MusicGenreList}
|
||||
|
||||
|
||||
def getRecommend(self):
|
||||
self.log('getRecommend')
|
||||
PluginList = []
|
||||
WhiteList = self.getWhiteList()
|
||||
AddonsList = self.searchRecommended()
|
||||
for addonid, item in list(AddonsList.items()):
|
||||
if addonid not in WhiteList: continue
|
||||
items = item.get('data',{}).get('vod',[])
|
||||
items.extend(item.get('data',{}).get('live',[]))
|
||||
for vod in items:
|
||||
path = vod.get('path')
|
||||
if not isinstance(path,list): path = [path]
|
||||
PluginList.append({'id':item['meta'].get('name'), 'name':vod.get('name'), 'type':"Recommended", 'path': path, 'logo':vod.get('icon',item['meta'].get('thumbnail'))})
|
||||
self.log('getRecommend, found (%s) vod items.' % (len(PluginList)))
|
||||
PluginList = sorted(PluginList,key=itemgetter('name'))
|
||||
PluginList = sorted(PluginList,key=itemgetter('id'))
|
||||
return PluginList
|
||||
|
||||
|
||||
def getRecommendInfo(self, addonid):
|
||||
self.log('getRecommendInfo, addonid = %s'%(addonid))
|
||||
return self.searchRecommended().get(addonid,{})
|
||||
|
||||
|
||||
def searchRecommended(self):
|
||||
return {} #todo
|
||||
# def _search(addonid):
|
||||
# cacheName = 'searchRecommended.%s'%(getMD5(addonid))
|
||||
# addonMeta = SETTINGS.getAddonDetails(addonid)
|
||||
# payload = PROPERTIES.getEXTProperty(REG_KEY%(addonid))
|
||||
# if not payload: #startup services may not be broadcasting beacon; use last cached beacon instead.
|
||||
# payload = self.cache.get(cacheName, checksum=addonMeta.get('version',ADDON_VERSION), json_data=True)
|
||||
# else:
|
||||
# payload = loadJSON(payload)
|
||||
# self.cache.set(cacheName, payload, checksum=addonMeta.get('version',ADDON_VERSION), expiration=datetime.timedelta(days=MAX_GUIDEDAYS), json_data=True)
|
||||
|
||||
# if payload:
|
||||
# self.log('searchRecommended, found addonid = %s, payload = %s'%(addonid,payload))
|
||||
# return addonid,{"data":payload,"meta":addonMeta}
|
||||
|
||||
# addonList = sorted(list(set([_f for _f in [addon.get('addonid') for addon in list([k for k in self.jsonRPC.getAddons() if k.get('addonid','') not in self.getBlackList()])] if _f])))
|
||||
# return dict([_f for _f in [_search(addonid) for addonid in addonList] if _f])
|
||||
|
||||
|
||||
def getServices(self):
|
||||
self.log('getServices')
|
||||
return []
|
||||
|
||||
|
||||
def getWhiteList(self):
|
||||
#whitelist - prompt shown, added to import list and/or manager dropdown.
|
||||
return self.libraryDATA.get('whitelist',[])
|
||||
|
||||
|
||||
def setWhiteList(self, data=[]):
|
||||
self.libraryDATA['whitelist'] = sorted(set(data))
|
||||
return self._save()
|
||||
|
||||
|
||||
def getBlackList(self):
|
||||
#blacklist - plugin ignored for the life of the list.
|
||||
return self.libraryDATA.get('blacklist',[])
|
||||
|
||||
|
||||
def setBlackList(self, data=[]):
|
||||
self.libraryDATA['blacklist'] = sorted(set(data))
|
||||
return self._save()
|
||||
|
||||
|
||||
def addWhiteList(self, addonid):
|
||||
self.log('addWhiteList, addonid = %s'%(addonid))
|
||||
whiteList = self.getWhiteList()
|
||||
whiteList.append(addonid)
|
||||
whiteList = sorted(set(whiteList))
|
||||
if len(whiteList) > 0: PROPERTIES.setEXTPropertyBool('%s.has.WhiteList'%(ADDON_ID),len(whiteList) > 0)
|
||||
return self.setWhiteList(whiteList)
|
||||
|
||||
|
||||
def addBlackList(self, addonid):
|
||||
self.log('addBlackList, addonid = %s'%(addonid))
|
||||
blackList = self.getBlackList()
|
||||
blackList.append(addonid)
|
||||
blackList = sorted(set(blackList))
|
||||
return self.setBlackList(blackList)
|
||||
|
||||
|
||||
def clearBlackList(self):
|
||||
return self.setBlackList()
|
||||
|
||||
|
||||
def importPrompt(self):
|
||||
addonList = self.searchRecommended()
|
||||
ignoreList = self.getWhiteList()
|
||||
ignoreList.extend(self.getBlackList()) #filter addons previously parsed.
|
||||
addonNames = sorted(list(set([_f for _f in [item.get('meta',{}).get('name') for addonid, item in list(addonList.items()) if not addonid in ignoreList] if _f])))
|
||||
self.log('importPrompt, addonNames = %s'%(len(addonNames)))
|
||||
|
||||
try:
|
||||
if len(addonNames) > 1:
|
||||
retval = DIALOG.yesnoDialog('%s'%(LANGUAGE(32055)%(ADDON_NAME,', '.join(addonNames))), customlabel=LANGUAGE(32056))
|
||||
self.log('importPrompt, prompt retval = %s'%(retval))
|
||||
if retval == 1: raise Exception('Single Entry')
|
||||
elif retval == 2:
|
||||
for addonid, item in list(addonList.items()):
|
||||
if item.get('meta',{}).get('name') in addonNames:
|
||||
self.addWhiteList(addonid)
|
||||
else: raise Exception('Single Entry')
|
||||
except Exception as e:
|
||||
self.log('importPrompt, %s'%(e))
|
||||
for addonid, item in list(addonList.items()):
|
||||
if item.get('meta',{}).get('name') in addonNames:
|
||||
if not DIALOG.yesnoDialog('%s'%(LANGUAGE(32055)%(ADDON_NAME,item['meta'].get('name','')))):
|
||||
self.addBlackList(addonid)
|
||||
else:
|
||||
self.addWhiteList(addonid)
|
||||
|
||||
PROPERTIES.setEXTPropertyBool('%s.has.WhiteList'%(ADDON_ID),len(self.getWhiteList()) > 0)
|
||||
PROPERTIES.setEXTPropertyBool('%s.has.BlackList'%(ADDON_ID),len(self.getBlackList()) > 0)
|
||||
SETTINGS.setSetting('Clear_BlackList','|'.join(self.getBlackList()))
|
||||
@@ -0,0 +1,92 @@
|
||||
# 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 -*-
|
||||
|
||||
import json,traceback
|
||||
from globals import *
|
||||
|
||||
def log(event, level=xbmc.LOGDEBUG):
|
||||
"""
|
||||
Logs an event or message using Kodi's logging system.
|
||||
|
||||
This function is designed to capture and log debug events based on the specified log level.
|
||||
If debugging is enabled or the log level is critical (e.g., errors), the event will be
|
||||
recorded in Kodi's log file. Additionally, it stores events in a custom debug window property
|
||||
for debugging purposes.
|
||||
|
||||
Args:
|
||||
event (str): The message or event to log.
|
||||
level (int, optional): The log level (default is xbmc.LOGDEBUG). Supported levels:
|
||||
- xbmc.LOGDEBUG: Debug messages (low priority).
|
||||
- xbmc.LOGINFO: Informational messages.
|
||||
- xbmc.LOGWARNING: Warnings.
|
||||
- xbmc.LOGERROR: Errors.
|
||||
- xbmc.LOGFATAL: Fatal errors.
|
||||
|
||||
Behavior:
|
||||
- Logs the event if debugging is enabled or if the log level is above the configured threshold.
|
||||
- Appends a traceback for error-level logs (level >= xbmc.LOGERROR).
|
||||
- Formats the log message with the add-on ID and version for context.
|
||||
- Stores the log entry in the global debug window property for later retrieval if debugging is enabled.
|
||||
|
||||
Example Usage:
|
||||
log("This is a debug message", xbmc.LOGDEBUG)
|
||||
log("An error occurred", xbmc.LOGERROR)
|
||||
|
||||
Notes:
|
||||
- The `REAL_SETTINGS.getSetting('Debug_Enable')` setting determines whether to log debug-level messages.
|
||||
- The log entries are stored in a JSON object with timestamps and log levels for easy parsing.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
if REAL_SETTINGS.getSetting('Debug_Enable') == 'true' or level >= 3:
|
||||
DEBUG_NAMES = {0: 'LOGDEBUG', 1: 'LOGINFO', 2: 'LOGWARNING', 3: 'LOGERROR', 4: 'LOGFATAL'}
|
||||
DEBUG_LEVELS = {0: xbmc.LOGDEBUG, 1: xbmc.LOGINFO, 2: xbmc.LOGWARNING, 3: xbmc.LOGERROR, 4: xbmc.LOGFATAL}
|
||||
DEBUG_LEVEL = DEBUG_LEVELS[int((REAL_SETTINGS.getSetting('Debug_Level') or "3"))]
|
||||
|
||||
# Add traceback for error-level events
|
||||
if level >= 3:
|
||||
event = '%s\n%s' % (event, traceback.format_exc())
|
||||
|
||||
# Format event with add-on ID and version
|
||||
event = '%s-%s-%s' % (ADDON_ID, ADDON_VERSION, event)
|
||||
|
||||
# Log the event if the level is above the configured debug level
|
||||
if level >= DEBUG_LEVEL:
|
||||
xbmc.log(event, level)
|
||||
try:
|
||||
entries = json.loads(xbmcgui.Window(10000).getProperty('%s.debug.log' % (ADDON_ID))).get('DEBUG', {})
|
||||
except:
|
||||
entries = {}
|
||||
|
||||
# Add the event to the debug entries
|
||||
entries.setdefault(DEBUG_NAMES[DEBUG_LEVEL], []).append(
|
||||
'%s - %s: %s' % (datetime.datetime.fromtimestamp(time.time()).strftime(DTFORMAT), DEBUG_NAMES[level], event)
|
||||
)
|
||||
|
||||
# Store the debug entries in the window property
|
||||
try:
|
||||
xbmcgui.Window(10000).setProperty('%s.debug.log' % (ADDON_ID), json.dumps({'DEBUG': entries}, indent=4))
|
||||
except:
|
||||
pass
|
||||
|
||||
# Mark the debug property as active
|
||||
if not xbmcgui.Window(10000).getProperty('%s.has.debug' % (ADDON_ID)) == 'true':
|
||||
xbmcgui.Window(10000).setProperty('%s.has.debug' % (ADDON_ID), 'true')
|
||||
@@ -0,0 +1,451 @@
|
||||
# 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/>.
|
||||
# https://github.com/kodi-pvr/pvr.iptvsimple#supported-m3u-and-xmltv-elements
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from globals import *
|
||||
from channels import Channels
|
||||
|
||||
M3U_TEMP = {"id" : "",
|
||||
"number" : 0,
|
||||
"name" : "",
|
||||
"logo" : "",
|
||||
"group" : [],
|
||||
"catchup" : "vod",
|
||||
"radio" : False,
|
||||
"favorite" : False,
|
||||
"realtime" : False,
|
||||
"media" : "",
|
||||
"label" : "",
|
||||
"url" : "",
|
||||
"tvg-shift" : "",
|
||||
"x-tvg-url" : "",
|
||||
"media-dir" : "",
|
||||
"media-size" : "",
|
||||
"media-type" : "",
|
||||
"catchup-source" : "",
|
||||
"catchup-days" : "",
|
||||
"catchup-correction": "",
|
||||
"provider" : "",
|
||||
"provider-type" : "",
|
||||
"provider-logo" : "",
|
||||
"provider-countries": "",
|
||||
"provider-languages": "",
|
||||
"x-playlist-type" : "",
|
||||
"kodiprops" : []}
|
||||
|
||||
M3U_MIN = {"id" : "",
|
||||
"number" : 0,
|
||||
"name" : "",
|
||||
"logo" : "",
|
||||
"group" : [],
|
||||
"catchup" : "vod",
|
||||
"radio" : False,
|
||||
"label" : "",
|
||||
"url" : ""}
|
||||
|
||||
class M3U:
|
||||
def __init__(self):
|
||||
stations, recordings = self.cleanSelf(list(self._load()))
|
||||
self.M3UDATA = {'data':'#EXTM3U tvg-shift="" x-tvg-url="" x-tvg-id="" catchup-correction=""', 'stations':stations, 'recordings':recordings}
|
||||
# self.M3UTEMP = getJSON(M3UFLE_DEFAULT)
|
||||
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
return log('%s: %s'%(self.__class__.__name__,msg),level)
|
||||
|
||||
|
||||
def _load(self, file=M3UFLEPATH):
|
||||
self.log('_load, file = %s'%file)
|
||||
if file.startswith('http'):
|
||||
url = file
|
||||
file = os.path.join(TEMP_LOC,slugify(url))
|
||||
saveURL(url,file)
|
||||
|
||||
if FileAccess.exists(file):
|
||||
fle = FileAccess.open(file, 'r')
|
||||
lines = (fle.readlines())
|
||||
fle.close()
|
||||
|
||||
chCount = 0
|
||||
data = {}
|
||||
filter = []
|
||||
|
||||
for idx, line in enumerate(lines):
|
||||
line = line.rstrip()
|
||||
|
||||
if line.startswith('#EXTM3U'):
|
||||
data = {'tvg-shift' :re.compile('tvg-shift=\"(.*?)\"' , re.IGNORECASE).search(line),
|
||||
'x-tvg-url' :re.compile('x-tvg-url=\"(.*?)\"' , re.IGNORECASE).search(line),
|
||||
'catchup-correction':re.compile('catchup-correction=\"(.*?)\"' , re.IGNORECASE).search(line)}
|
||||
|
||||
# if SETTINGS.getSettingInt('Import_XMLTV_TYPE') == 2 and file == os.path.join(TEMP_LOC,slugify(SETTINGS.getSetting('Import_M3U_URL'))):
|
||||
# if data.get('x-tvg-url').group(1):
|
||||
# self.log('_load, using #EXTM3U "x-tvg-url"')
|
||||
# SETTINGS.setSetting('Import_XMLTV_M3U',data.get('x-tvg-url').group(1))
|
||||
|
||||
elif line.startswith('#EXTINF:'):
|
||||
chCount += 1
|
||||
match = {'label' :re.compile(',(.*)' , re.IGNORECASE).search(line),
|
||||
'id' :re.compile('tvg-id=\"(.*?)\"' , re.IGNORECASE).search(line),
|
||||
'name' :re.compile('tvg-name=\"(.*?)\"' , re.IGNORECASE).search(line),
|
||||
'group' :re.compile('group-title=\"(.*?)\"' , re.IGNORECASE).search(line),
|
||||
'number' :re.compile('tvg-chno=\"(.*?)\"' , re.IGNORECASE).search(line),
|
||||
'logo' :re.compile('tvg-logo=\"(.*?)\"' , re.IGNORECASE).search(line),
|
||||
'radio' :re.compile('radio=\"(.*?)\"' , re.IGNORECASE).search(line),
|
||||
'tvg-shift' :re.compile('tvg-shift=\"(.*?)\"' , re.IGNORECASE).search(line),
|
||||
'catchup' :re.compile('catchup=\"(.*?)\"' , re.IGNORECASE).search(line),
|
||||
'catchup-source' :re.compile('catchup-source=\"(.*?)\"' , re.IGNORECASE).search(line),
|
||||
'catchup-days' :re.compile('catchup-days=\"(.*?)\"' , re.IGNORECASE).search(line),
|
||||
'catchup-correction':re.compile('catchup-correction=\"(.*?)\"' , re.IGNORECASE).search(line),
|
||||
'provider' :re.compile('provider=\"(.*?)\"' , re.IGNORECASE).search(line),
|
||||
'provider-type' :re.compile('provider-type=\"(.*?)\"' , re.IGNORECASE).search(line),
|
||||
'provider-logo' :re.compile('provider-logo=\"(.*?)\"' , re.IGNORECASE).search(line),
|
||||
'provider-countries':re.compile('provider-countries=\"(.*?)\"' , re.IGNORECASE).search(line),
|
||||
'provider-languages':re.compile('provider-languages=\"(.*?)\"' , re.IGNORECASE).search(line),
|
||||
'media' :re.compile('media=\"(.*?)\"' , re.IGNORECASE).search(line),
|
||||
'media-dir' :re.compile('media-dir=\"(.*?)\"' , re.IGNORECASE).search(line),
|
||||
'media-size' :re.compile('media-size=\"(.*?)\"' , re.IGNORECASE).search(line),
|
||||
'realtime' :re.compile('realtime=\"(.*?)\"' , re.IGNORECASE).search(line)}
|
||||
|
||||
if match['id'].group(1) in filter:
|
||||
self.log('_load, filtering duplicate %s'%(match['id'].group(1)))
|
||||
continue
|
||||
filter.append(match['id'].group(1)) #filter dups, todo find where dups originate from.
|
||||
|
||||
mitem = self.getMitem()
|
||||
mitem.update({'number' :chCount,
|
||||
'logo' :LOGO,
|
||||
'catchup':''}) #set default parameters
|
||||
|
||||
for key, value in list(match.items()):
|
||||
if value is None:
|
||||
if data.get(key,None) is not None:
|
||||
self.log('_load, using #EXTM3U "%s" value for #EXTINF'%(key))
|
||||
value = data[key] #no local EXTINF value found; use global EXTM3U if applicable.
|
||||
else: continue
|
||||
|
||||
if value.group(1) is None:
|
||||
continue
|
||||
elif key == 'logo':
|
||||
mitem[key] = value.group(1)
|
||||
elif key == 'number':
|
||||
try: mitem[key] = int(value.group(1))
|
||||
except: mitem[key] = float(value.group(1))#todo why was this needed?
|
||||
elif key == 'group':
|
||||
mitem[key] = [_f for _f in sorted(list(set((value.group(1)).split(';')))) if _f]
|
||||
elif key in ['radio','favorite','realtime','media']:
|
||||
mitem[key] = (value.group(1)).lower() == 'true'
|
||||
else:
|
||||
mitem[key] = value.group(1)
|
||||
|
||||
for nidx in range(idx+1,len(lines)):
|
||||
try:
|
||||
nline = lines[nidx].rstrip()
|
||||
if nline.startswith('#EXTINF:'): break
|
||||
elif nline.startswith('#EXTGRP'):
|
||||
grop = re.compile('^#EXTGRP:(.*)$', re.IGNORECASE).search(nline)
|
||||
if grop is not None:
|
||||
mitem['group'].append(grop.group(1).split(';'))
|
||||
mitem['group'] = sorted(set(mitem['group']))
|
||||
elif nline.startswith('#KODIPROP:'):
|
||||
prop = re.compile('^#KODIPROP:(.*)$', re.IGNORECASE).search(nline)
|
||||
if prop is not None: mitem.setdefault('kodiprops',[]).append(prop.group(1))
|
||||
elif nline.startswith('#EXTVLCOPT'):
|
||||
copt = re.compile('^#EXTVLCOPT:(.*)$', re.IGNORECASE).search(nline)
|
||||
if copt is not None: mitem.setdefault('extvlcopt',[]).append(copt.group(1))
|
||||
elif nline.startswith('#EXT-X-PLAYLIST-TYPE'):
|
||||
xplay = re.compile('^#EXT-X-PLAYLIST-TYPE:(.*)$', re.IGNORECASE).search(nline)
|
||||
if xplay is not None: mitem['x-playlist-type'] = xplay.group(1)
|
||||
elif nline.startswith('##'): continue
|
||||
elif not nline: continue
|
||||
else: mitem['url'] = nline
|
||||
except Exception as e: self.log('_load, error parsing m3u! %s'%(e))
|
||||
|
||||
#Fill missing with similar parameters.
|
||||
mitem['name'] = (mitem.get('name') or mitem.get('label') or '')
|
||||
mitem['label'] = (mitem.get('label') or mitem.get('name') or '')
|
||||
mitem['favorite'] = (mitem.get('favorite') or False)
|
||||
|
||||
#Set Fav. based on group value.
|
||||
if LANGUAGE(32019) in mitem['group'] and not mitem['favorite']:
|
||||
mitem['favorite'] = True
|
||||
|
||||
#Core m3u parameters missing, ignore entry.
|
||||
if not mitem.get('id') or not mitem.get('name') or not mitem.get('number'):
|
||||
self.log('_load, SKIPPED MISSING META m3u item = %s'%mitem)
|
||||
continue
|
||||
|
||||
self.log('_load, m3u item = %s'%mitem)
|
||||
yield mitem
|
||||
|
||||
|
||||
def _save(self, file=M3UFLEPATH):
|
||||
with FileLock():
|
||||
fle = FileAccess.open(file, 'w')
|
||||
fle.write('%s\n'%(self.M3UDATA['data']))
|
||||
|
||||
opts = list(self.getMitem().keys())
|
||||
mins = [opts.pop(opts.index(key)) for key in list(M3U_MIN.keys()) if key in opts] #min required m3u entries.
|
||||
line = '#EXTINF:-1 tvg-chno="%s" tvg-id="%s" tvg-name="%s" tvg-logo="%s" group-title="%s" radio="%s" catchup="%s" %s,%s\n'
|
||||
self.M3UDATA['stations'] = self.sortStations(self.M3UDATA.get('stations',[]))
|
||||
self.M3UDATA['recordings'] = self.sortStations(self.M3UDATA.get('recordings',[]), key='name')
|
||||
self.log('_save, saving %s stations and %s recordings to %s'%(len(self.M3UDATA['stations']),len(self.M3UDATA['recordings']),file))
|
||||
|
||||
for station in (self.M3UDATA['recordings'] + self.M3UDATA['stations']):
|
||||
optional = ''
|
||||
xplaylist = ''
|
||||
kodiprops = {}
|
||||
extvlcopt = {}
|
||||
|
||||
# write optional m3u parameters.
|
||||
if 'kodiprops' in station: kodiprops = station.pop('kodiprops')
|
||||
if 'extvlcopt' in station: extvlcopt = station.pop('extvlcopt')
|
||||
if 'x-playlist-type' in station: xplaylist = station.pop('x-playlist-type')
|
||||
for key, value in list(station.items()):
|
||||
if key in opts and str(value):
|
||||
optional += '%s="%s" '%(key,value)
|
||||
|
||||
fle.write(line%(station['number'],
|
||||
station['id'],
|
||||
station['name'],
|
||||
station['logo'],
|
||||
';'.join(station['group']),
|
||||
station['radio'],
|
||||
station['catchup'],
|
||||
optional,
|
||||
station['label']))
|
||||
|
||||
if kodiprops: fle.write('%s\n'%('\n'.join(['#KODIPROP:%s'%(prop) for prop in kodiprops])))
|
||||
if extvlcopt: fle.write('%s\n'%('\n'.join(['#EXTVLCOPT:%s'%(prop) for prop in extvlcopt])))
|
||||
if xplaylist: fle.write('%s\n'%('#EXT-X-PLAYLIST-TYPE:%s'%(xplaylist)))
|
||||
fle.write('%s\n'%(station['url']))
|
||||
fle.close()
|
||||
return self._reload()
|
||||
|
||||
|
||||
def _reload(self):
|
||||
self.log('_reload')
|
||||
self.__init__()
|
||||
return True
|
||||
|
||||
|
||||
def _verify(self, stations=[], recordings=[], chkPath=SETTINGS.getSettingBool('Clean_Recordings')):
|
||||
if stations: #remove abandoned m3u entries; Stations that are not found in the channel list
|
||||
channels = Channels().getChannels()
|
||||
stations = [station for station in stations for channel in channels if channel.get('id') == station.get('id',str(random.random()))]
|
||||
self.log('_verify, stations = %s'%(len(stations)))
|
||||
return stations
|
||||
elif recordings:#remove recordings that no longer exists on disk
|
||||
if chkPath: recordings = [recording for recording in recordings if hasFile(decodeString(dict(urllib.parse.parse_qsl(recording.get('url',''))).get('vid').replace('.pvr','')))]
|
||||
else: recordings = [recording for recording in recordings if recording.get('media',False)]
|
||||
self.log('_verify, recordings = %s, chkPath = %s'%(len(recordings),chkPath))
|
||||
return recordings
|
||||
return []
|
||||
|
||||
|
||||
def cleanSelf(self, items, key='id', slug='@%s'%(slugify(ADDON_NAME))): # remove m3u imports (Non PseudoTV Live)
|
||||
if not slug: return items
|
||||
stations = self.sortStations(self._verify(stations=[station for station in items if station.get(key,'').endswith(slug) and not station.get('media',False)]))
|
||||
recordings = self.sortStations(self._verify(recordings=[recording for recording in items if recording.get(key,'').endswith(slug) and recording.get('media',False)]), key='name')
|
||||
self.log('cleanSelf, slug = %s, key = %s: returning: stations = %s, recordings = %s'%(slug,key,len(stations),len(recordings)))
|
||||
return stations, recordings
|
||||
|
||||
|
||||
def sortStations(self, stations, key='number'):
|
||||
try: return sorted(stations, key=itemgetter(key))
|
||||
except: return stations
|
||||
|
||||
|
||||
def getM3U(self):
|
||||
return self.M3UDATA
|
||||
|
||||
|
||||
def getMitem(self):
|
||||
return M3U_TEMP.copy()
|
||||
|
||||
|
||||
def getTZShift(self):
|
||||
self.log('getTZShift')
|
||||
return ((time.mktime(time.localtime()) - time.mktime(time.gmtime())) / 60 / 60)
|
||||
|
||||
|
||||
def getStations(self):
|
||||
stations = self.sortStations(self.M3UDATA.get('stations',[]))
|
||||
self.log('getStations, stations = %s'%(len(stations)))
|
||||
return stations
|
||||
|
||||
|
||||
def getRecordings(self):
|
||||
recordings = self.sortStations(self.M3UDATA.get('recordings',[]), key='name')
|
||||
self.log('getRecordings, recordings = %s'%(len(recordings)))
|
||||
return recordings
|
||||
|
||||
|
||||
def findStation(self, citem):
|
||||
for idx, eitem in enumerate(self.M3UDATA.get('stations',[])):
|
||||
if (citem.get('id',str(random.random())) == eitem.get('id') or citem.get('url',str(random.random())).lower() == eitem.get('url','').lower()):
|
||||
self.log('findStation, found eitem = %s'%(eitem))
|
||||
return idx, eitem
|
||||
return None, {}
|
||||
|
||||
|
||||
def findRecording(self, ritem):
|
||||
for idx, eitem in enumerate(self.M3UDATA.get('recordings',[])):
|
||||
if (ritem.get('id',str(random.random())) == eitem.get('id')) or (ritem.get('label',str(random.random())).lower() == eitem.get('label','').lower()) or (ritem.get('path',str(random.random())).endswith('%s.pvr'%(eitem.get('name')))):
|
||||
self.log('findRecording, found eitem = %s'%(eitem))
|
||||
return idx, eitem
|
||||
return None, {}
|
||||
|
||||
|
||||
def getStationItem(self, sitem):
|
||||
if sitem.get('resume',False):
|
||||
sitem['url'] = RESUME_URL.format(addon=ADDON_ID,name=quoteString(sitem['name']),chid=quoteString(sitem['id']))
|
||||
elif sitem['catchup']:
|
||||
sitem['catchup-source'] = BROADCAST_URL.format(addon=ADDON_ID,name=quoteString(sitem['name']),chid=quoteString(sitem['id']),vid='{catchup-id}')
|
||||
sitem['url'] = LIVE_URL.format(addon=ADDON_ID,name=quoteString(sitem['name']),chid=quoteString(sitem['id']),vid='{catchup-id}',now='{lutc}',start='{utc}',duration='{duration}',stop='{utcend}')
|
||||
elif sitem['radio']: sitem['url'] = RADIO_URL.format(addon=ADDON_ID,name=quoteString(sitem['name']),chid=quoteString(sitem['id']),radio=str(sitem['radio']),vid='{catchup-id}')
|
||||
else: sitem['url'] = TV_URL.format(addon=ADDON_ID,name=quoteString(sitem['name']),chid=quoteString(sitem['id']))
|
||||
return sitem
|
||||
|
||||
def getRecordItem(self, fitem, seek=0):
|
||||
if seek <= 0: group = LANGUAGE(30119)
|
||||
else: group = LANGUAGE(30152)
|
||||
ritem = self.getMitem()
|
||||
ritem['provider'] = '%s (%s)'%(ADDON_NAME,SETTINGS.getFriendlyName())
|
||||
ritem['provider-type'] = 'addon'
|
||||
ritem['provider-logo'] = HOST_LOGO
|
||||
ritem['label'] = (fitem.get('showlabel') or '%s%s'%(fitem.get('label',''),' - %s'%(fitem.get('episodelabel','')) if fitem.get('episodelabel','') else ''))
|
||||
ritem['name'] = ritem['label']
|
||||
ritem['number'] = random.Random(str(fitem.get('id',1))).random()
|
||||
ritem['logo'] = cleanImage((getThumb(fitem,opt=EPG_ARTWORK) or {0:FANART,1:COLOR_LOGO}[EPG_ARTWORK]))
|
||||
ritem['media'] = True
|
||||
ritem['media-size'] = str(fitem.get('size',0))
|
||||
ritem['media-dir'] = ''#todo optional add parent directory via user prompt?
|
||||
ritem['group'] = ['%s (%s)'%(group,ADDON_NAME)]
|
||||
ritem['id'] = getRecordID(ritem['name'], (fitem.get('originalfile') or fitem.get('file','')), ritem['number'])
|
||||
ritem['url'] = DVR_URL.format(addon=ADDON_ID,title=quoteString(ritem['label']),chid=quoteString(ritem['id']),vid=quoteString(encodeString((fitem.get('originalfile') or fitem.get('file','')))),seek=seek,duration=fitem.get('duration',0))#fitem.get('catchup-id','')
|
||||
return ritem
|
||||
|
||||
|
||||
def addStation(self, citem):
|
||||
idx, line = self.findStation(citem)
|
||||
self.log('addStation,\nchannel item = %s\nfound existing = %s'%(citem,line))
|
||||
mitem = self.getMitem()
|
||||
mitem.update(citem)
|
||||
mitem['label'] = citem['name'] #todo channel manager opt to change channel 'label' leaving 'name' static for channelid purposes
|
||||
mitem['logo'] = citem['logo']
|
||||
mitem['realtime'] = False
|
||||
mitem['provider'] = '%s (%s)'%(ADDON_NAME,SETTINGS.getFriendlyName())
|
||||
mitem['provider-type'] = 'addon'
|
||||
mitem['provider-logo'] = HOST_LOGO
|
||||
|
||||
if not idx is None: self.M3UDATA['stations'].pop(idx)
|
||||
self.M3UDATA.get('stations',[]).append(mitem)
|
||||
self.log('addStation, channels = %s'%(len(self.M3UDATA.get('stations',[]))))
|
||||
return True
|
||||
|
||||
|
||||
def addRecording(self, ritem):
|
||||
# https://github.com/kodi-pvr/pvr.iptvsimple/blob/Omega/README.md#media
|
||||
idx, line = self.findRecording(ritem)
|
||||
self.log('addRecording,\nrecording ritem = %s\nfound existing = %s'%(ritem,idx))
|
||||
if not idx is None: self.M3UDATA['recordings'].pop(idx)
|
||||
self.M3UDATA.get('recordings',[]).append(ritem)
|
||||
return self._save()
|
||||
|
||||
|
||||
def delStation(self, citem):
|
||||
self.log('[%s] delStation'%(citem['id']))
|
||||
idx, line = self.findStation(citem)
|
||||
if not idx is None: self.M3UDATA['stations'].pop(idx)
|
||||
return True
|
||||
|
||||
|
||||
def delRecording(self, ritem):
|
||||
self.log('[%s] delRecording'%((ritem.get('id') or ritem.get('label'))))
|
||||
idx, line = self.findRecording(ritem)
|
||||
if not idx is None:
|
||||
self.M3UDATA['recordings'].pop(idx)
|
||||
return self._save()
|
||||
|
||||
|
||||
def importM3U(self, file, filters={}, multiplier=1):
|
||||
self.log('importM3U, file = %s, filters = %s, multiplier = %s'%(file,filters,multiplier))
|
||||
try:
|
||||
importChannels = []
|
||||
if file.startswith('http'):
|
||||
url = file
|
||||
file = os.path.join(TEMP_LOC,'%s'%(slugify(url)))
|
||||
setURL(url,file)
|
||||
|
||||
stations = self._load(file)
|
||||
for key, value in list(filters.items()):
|
||||
if key == 'slug' and value:
|
||||
importChannels.extend(self.cleanSelf(stations,'id',value)[0])
|
||||
elif key == 'providers' and value:
|
||||
for provider in value:
|
||||
importChannels.extend(self.cleanSelf(stations,'provider',provider)[0])
|
||||
|
||||
#no filter found, import all stations.
|
||||
if not importChannels: importChannels.extend(stations)
|
||||
importChannels = self.sortStations(list(self.chkImport(importChannels,multiplier)))
|
||||
self.log('importM3U, found import stations = %s'%(len(importChannels)))
|
||||
self.M3UDATA.get('stations',[]).extend(importChannels)
|
||||
except Exception as e: self.log("importM3U, failed! %s"%(e), xbmc.LOGERROR)
|
||||
return importChannels
|
||||
|
||||
|
||||
def chkImport(self, stations, multiplier=1):
|
||||
def roundup(x):
|
||||
return x if x % 1000 == 0 else x + 1000 - x % 1000
|
||||
|
||||
def frange(start, stop, step):
|
||||
while not MONITOR().abortRequested() and start < stop:
|
||||
yield float(start)
|
||||
start += decimal.Decimal(step)
|
||||
|
||||
stations = self.sortStations(stations)
|
||||
chstart = roundup((CHANNEL_LIMIT * len(CHAN_TYPES)+1))
|
||||
chmin = int(chstart + (multiplier*1000))
|
||||
chmax = int(chmin + (CHANNEL_LIMIT))
|
||||
chrange = list(frange(chmin,chmax,0.1))
|
||||
leftovers = []
|
||||
self.log('chkImport, stations = %s, multiplier = %s, chstart = %s, chmin = %s, chmax = %s'%(len(stations),multiplier,chstart,chmin,chmax))
|
||||
## check tvg-chno for conflict, use multiplier to modify org chnum.
|
||||
for mitem in stations:
|
||||
if len(chrange) == 0:
|
||||
self.log('chkImport, reached max import')
|
||||
break
|
||||
elif mitem['number'] < CHANNEL_LIMIT:
|
||||
newnumber = (chmin+mitem['number'])
|
||||
if newnumber in chrange:
|
||||
chrange.remove(newnumber)
|
||||
mitem['number'] = newnumber
|
||||
yield mitem
|
||||
else: leftovers.append(mitem)
|
||||
else: leftovers.append(mitem)
|
||||
|
||||
for mitem in leftovers:
|
||||
if len(chrange) == 0:
|
||||
self.log('chkImport, reached max import')
|
||||
break
|
||||
else:
|
||||
mitem['number'] = chrange.pop(0)
|
||||
yield mitem
|
||||
@@ -0,0 +1,974 @@
|
||||
# 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)
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
# 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 server import Discovery
|
||||
|
||||
class Service:
|
||||
from jsonrpc import JSONRPC
|
||||
player = PLAYER()
|
||||
monitor = MONITOR()
|
||||
jsonRPC = JSONRPC()
|
||||
def _interrupt(self) -> bool:
|
||||
return PROPERTIES.isPendingInterrupt()
|
||||
def _suspend(self) -> bool:
|
||||
return PROPERTIES.isPendingSuspend()
|
||||
|
||||
|
||||
class Multiroom:
|
||||
def __init__(self, sysARG=sys.argv, service=None):
|
||||
self.log('__init__, sysARG = %s'%(sysARG))
|
||||
if service is None: service = Service()
|
||||
self.service = service
|
||||
self.jsonRPC = service.jsonRPC
|
||||
self.cache = service.jsonRPC.cache
|
||||
self.sysARG = sysARG
|
||||
self.uuid = SETTINGS.getMYUUID()
|
||||
self.friendly = SETTINGS.getFriendlyName()
|
||||
self.remoteHost = PROPERTIES.getRemoteHost()
|
||||
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
return log('%s: %s'%(self.__class__.__name__,msg),level)
|
||||
|
||||
|
||||
@cacheit(checksum=PROPERTIES.getInstanceID(), expiration=datetime.timedelta(minutes=FIFTEEN))
|
||||
def _getStatus(self):
|
||||
return self.jsonRPC.getSettingValue("services.zeroconf",default=False)
|
||||
|
||||
|
||||
def _chkDiscovery(self):
|
||||
self.log('_chkDiscovery')
|
||||
Discovery(service=self.service, multiroom=self)
|
||||
|
||||
|
||||
def chkServers(self, servers={}):
|
||||
def __chkSettings(settings):
|
||||
[hasAddon(id,install=True,enable=True) for k,addons in list(settings.items()) for id in addons if id.startswith(('resource','plugin'))]
|
||||
|
||||
if isinstance(servers,bool): servers = {} #temp fix remove after a by next build
|
||||
if not servers: servers = self.getDiscovery()
|
||||
PROPERTIES.setServers(len(servers) > 0)
|
||||
for server in list(servers.values()):
|
||||
online = server.get('online',False)
|
||||
response = self.getRemote(server.get('remotes',{}).get('bonjour'))
|
||||
if response: server['online'] = True
|
||||
else: server['online'] = False
|
||||
if server.get('enabled',False):
|
||||
if online != server.get('online',False): DIALOG.notificationDialog('%s: %s'%(server.get('name'),LANGUAGE(32211)%({True:'green',False:'red'}[server.get('online',False)],{True:LANGUAGE(32158),False:LANGUAGE(32253)}[server.get('online',False)])))
|
||||
__chkSettings(loadJSON(server.get('settings')))
|
||||
SETTINGS.setSetting('Select_server','|'.join([LANGUAGE(32211)%({True:'green',False:'red'}[server.get('online',False)],server.get('name')) for server in self.getEnabled(servers)]))
|
||||
self.log('chkServers, servers = %s'%(len(servers)))
|
||||
self.setDiscovery(servers)
|
||||
return servers
|
||||
|
||||
|
||||
def getDiscovery(self):
|
||||
servers = getJSON(SERVER_LOC).get('servers',{})
|
||||
if isinstance(servers,bool): servers = {} #temp fix remove after a by next build
|
||||
return servers
|
||||
|
||||
|
||||
def setDiscovery(self, servers={}):
|
||||
return setJSON(SERVER_LOC,{"servers":servers})
|
||||
|
||||
|
||||
def getEnabled(self, servers={}):
|
||||
if not servers: servers = self.getDiscovery()
|
||||
enabled = [server for server in list(servers.values()) if server.get('enabled',False)]
|
||||
PROPERTIES.setEnabledServers(len(enabled) > 0)
|
||||
return enabled
|
||||
|
||||
|
||||
@cacheit(expiration=datetime.timedelta(minutes=FIFTEEN), json_data=True)
|
||||
def getRemote(self, remote):
|
||||
self.log("getRemote, remote = %s"%(remote))
|
||||
cacheName = 'getRemote.%s'%(remote)
|
||||
return requestURL(remote, header={'Accept':'application/json'}, json_data=True, cache=self.cache, checksum=self.uuid, life=datetime.timedelta(days=MAX_GUIDEDAYS))
|
||||
|
||||
|
||||
def addServer(self, payload={}):
|
||||
self.log('addServer, name = %s'%(payload.get('name')))
|
||||
if payload and payload.get('name') and payload.get('host'):
|
||||
payload['online'] = True
|
||||
servers = self.getDiscovery()
|
||||
server = servers.get(payload.get('name'),{})
|
||||
if not server:
|
||||
payload['enabled'] = not bool(SETTINGS.getSettingBool('Debug_Enable')) #set enabled by default when not debugging.
|
||||
self.log('addServer, adding server = %s'%(payload))
|
||||
DIALOG.notificationDialog('%s: %s'%(LANGUAGE(32047),payload.get('name')))
|
||||
servers[payload['name']] = payload
|
||||
else:
|
||||
payload['enabled'] = server.get('enabled',False)
|
||||
if payload.get('md5',server.get('md5')) != server.get('md5'):
|
||||
self.log('addServer, updating server = %s'%(server))
|
||||
servers.update({payload['name']:payload})
|
||||
|
||||
if self.setDiscovery(self.chkServers(servers)):
|
||||
instancePath = SETTINGS.hasPVRInstance(server.get('name'))
|
||||
if payload.get('enabled',False) and not instancePath: changed = SETTINGS.setPVRRemote(payload.get('host'),payload.get('name'))
|
||||
elif not payload.get('enabled',False) and instancePath: changed = FileAccess.delete(instancePath)
|
||||
else: changed = False
|
||||
if changed: PROPERTIES.setPropTimer('chkPVRRefresh')
|
||||
self.log('addServer, payload changed, chkPVRRefresh = %s'%(changed))
|
||||
return True
|
||||
|
||||
|
||||
def delServer(self, servers={}):
|
||||
self.log('delServer')
|
||||
def __build(idx, payload):
|
||||
return LISTITEMS.buildMenuListItem(payload.get('name'),'%s - %s: Channels (%s)'%(LANGUAGE(32211)%({True:'green',False:'red'}[payload.get('online',False)],{True:LANGUAGE(32158),False:LANGUAGE(32253)}[payload.get('online',False)]),payload.get('host'),len(payload.get('channels',[]))),icon=DUMMY_ICON.format(text=str(idx+1)),url=dumpJSON(payload))
|
||||
|
||||
with BUILTIN.busy_dialog():
|
||||
if not servers: servers = self.getDiscovery()
|
||||
lizlst = [__build(idx, server) for idx, server in enumerate(list(servers.values()))]
|
||||
|
||||
selects = DIALOG.selectDialog(lizlst,LANGUAGE(32183))
|
||||
if not selects is None:
|
||||
[servers.pop(liz.getLabel()) for idx, liz in enumerate(lizlst) if idx in selects]
|
||||
if self.setDiscovery(self.chkServers(servers)):
|
||||
return DIALOG.notificationDialog(LANGUAGE(30046))
|
||||
|
||||
|
||||
def selServer(self):
|
||||
self.log('selServer')
|
||||
def __build(idx, payload):
|
||||
return LISTITEMS.buildMenuListItem(payload.get('name'),'%s - %s: Channels (%s)'%(LANGUAGE(32211)%({True:'green',False:'red'}[payload.get('online',False)],{True:LANGUAGE(32158),False:LANGUAGE(32253)}[payload.get('online',False)]),payload.get('host'),len(payload.get('channels',[]))),icon=DUMMY_ICON.format(text=str(idx+1)),url=dumpJSON(payload))
|
||||
|
||||
with BUILTIN.busy_dialog():
|
||||
servers = self.getDiscovery()
|
||||
lizlst = [__build(idx, server) for idx, server in enumerate(list(servers.values()))]
|
||||
if len(lizlst) > 0: lizlst.insert(0,LISTITEMS.buildMenuListItem('[COLOR=white][B]- %s[/B][/COLOR]'%(LANGUAGE(30046)),LANGUAGE(33046)))
|
||||
else: return
|
||||
|
||||
selects = DIALOG.selectDialog(lizlst,LANGUAGE(30130),preselect=[idx for idx, listitem in enumerate(lizlst) if loadJSON(listitem.getPath()).get('enabled',False)])
|
||||
if not selects is None:
|
||||
if 0 in selects: return self.delServer(servers)
|
||||
else:
|
||||
for idx, liz in enumerate(lizlst):
|
||||
if idx == 0: continue
|
||||
instancePath = SETTINGS.hasPVRInstance(liz.getLabel())
|
||||
if idx in selects:
|
||||
if not servers[liz.getLabel()].get('enabled',False):
|
||||
DIALOG.notificationDialog(LANGUAGE(30099)%(liz.getLabel()))
|
||||
servers[liz.getLabel()]['enabled'] = True
|
||||
if not instancePath:
|
||||
if SETTINGS.setPVRRemote(servers[liz.getLabel()].get('host'),liz.getLabel()): PROPERTIES.setPropTimer('chkPVRRefresh')
|
||||
else:
|
||||
if servers[liz.getLabel()].get('enabled',False):
|
||||
DIALOG.notificationDialog(LANGUAGE(30100)%(liz.getLabel()))
|
||||
servers[liz.getLabel()]['enabled'] = False
|
||||
if instancePath: FileAccess.delete(instancePath)
|
||||
with BUILTIN.busy_dialog():
|
||||
return self.setDiscovery(self.chkServers(servers))
|
||||
|
||||
|
||||
def enableZeroConf(self):
|
||||
self.log('enableZeroConf')
|
||||
if SETTINGS.getSetting('ZeroConf_Status') == '[COLOR=red][B]%s[/B][/COLOR]'%(LANGUAGE(32253)):
|
||||
if BUILTIN.getInfoLabel('Platform.Windows','System'):
|
||||
BUILTIN.executescript('special://home/addons/%s/resources/lib/utilities.py, Show_ZeroConf_QR'%(ADDON_ID))
|
||||
if DIALOG.yesnoDialog(message=LANGUAGE(30129)):
|
||||
with PROPERTIES.interruptActivity():
|
||||
if self.jsonRPC.setSettingValue("services.zeroconf",True,queue=False):
|
||||
DIALOG.notificationDialog(LANGUAGE(32219)%(LANGUAGE(30035)))
|
||||
PROPERTIES.setEpochTimer('chkKodiSettings')
|
||||
else: DIALOG.notificationDialog(LANGUAGE(32219)%(LANGUAGE(30034)))
|
||||
|
||||
|
||||
def run(self):
|
||||
try: param = self.sysARG[1]
|
||||
except: param = None
|
||||
if param == 'Enable_ZeroConf':
|
||||
ctl = (5,1)
|
||||
self.enableZeroConf()
|
||||
elif param == 'Select_Server':
|
||||
ctl = (5,11)
|
||||
self.selServer()
|
||||
elif param == 'Remove_server':
|
||||
ctl = (5,12)
|
||||
return SETTINGS.openSettings(ctl)
|
||||
|
||||
|
||||
if __name__ == '__main__': timerit(Multiroom(sys.argv).run)(0.1)
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
# Copyright (C) 2025 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/>.
|
||||
# https://github.com/xbmc/xbmc/blob/master/xbmc/input/actions/ActionIDs.h
|
||||
# https://github.com/xbmc/xbmc/blob/master/xbmc/input/Key.h
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
from globals import *
|
||||
from resources import Resources
|
||||
|
||||
WH, WIN = BUILTIN.getResolution()
|
||||
|
||||
class Busy(xbmcgui.WindowXMLDialog):
|
||||
def __init__(self, *args, **kwargs):
|
||||
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
|
||||
|
||||
|
||||
def onAction(self, act):
|
||||
actionId = act.getId()
|
||||
log('Busy: onAction: actionId = %s'%(actionId))
|
||||
if actionId in ACTION_PREVIOUS_MENU: self.close()
|
||||
|
||||
|
||||
class Background(xbmcgui.WindowXMLDialog):
|
||||
def __init__(self, *args, **kwargs):
|
||||
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
|
||||
self.player = kwargs.get('player', None)
|
||||
self.sysInfo = kwargs.get('sysInfo', self.player.sysInfo)
|
||||
|
||||
self.citem = self.sysInfo.get('citem',{})
|
||||
self.fitem = self.sysInfo.get('fitem',{})
|
||||
|
||||
self.nitem = self.player.jsonRPC.getNextItem(self.citem,self.sysInfo.get('nitem'))
|
||||
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
return log('%s: %s'%(self.__class__.__name__,msg),level)
|
||||
|
||||
|
||||
def onInit(self):
|
||||
try:
|
||||
self.log('onInit, citem = %s\nfitem = %ss\nnitem = %s'%(self.citem,self.fitem,self.nitem))
|
||||
logo = (self.citem.get('logo') or BUILTIN.getInfoLabel('Art(icon)','Player') or LOGO)
|
||||
land = (getThumb(self.nitem) or COLOR_FANART)
|
||||
chname = (self.citem.get('name') or BUILTIN.getInfoLabel('ChannelName','VideoPlayer'))
|
||||
nowTitle = (self.fitem.get('label') or BUILTIN.getInfoLabel('Title','VideoPlayer'))
|
||||
nextTitle = (self.nitem.get('showlabel') or BUILTIN.getInfoLabel('NextTitle','VideoPlayer') or chname)
|
||||
|
||||
try: nextTime = epochTime(self.nitem['start']).strftime('%I:%M%p')
|
||||
except Exception as e:
|
||||
self.log("__init__, nextTime failed! %s\nstart = %s"%(e,self.nitem.get('start')), xbmc.LOGERROR)
|
||||
nextTime = BUILTIN.getInfoLabel('NextStartTime','VideoPlayer')
|
||||
|
||||
onNow = '%s on %s'%(nowTitle,chname) if chname not in validString(nowTitle) else nowTitle
|
||||
onNext = '@ %s: %s'%(nextTime,nextTitle)
|
||||
|
||||
window_w, window_h = WH # window_h, window_w = (self.getHeight(), self.getWidth())
|
||||
onNextX, onNextY = abs(int(window_w // 9)), abs(int(window_h // 16) - window_h) - 356 #auto
|
||||
|
||||
self.getControl(40001).setPosition(onNextX, onNextY)
|
||||
self.getControl(40001).setVisibleCondition('[Player.Playing + !Window.IsVisible(fullscreeninfo) + Window.IsVisible(fullscreenvideo)]')
|
||||
self.getControl(40001).setAnimations([('WindowOpen' , 'effect=zoom start=80 end=100 center=%s,%s delay=160 tween=back time=240 reversible=false'%(onNextX, onNextY)),
|
||||
('WindowOpen' , 'effect=fade start=0 end=100 delay=160 time=240 reversible=false'),
|
||||
('WindowClose', 'effect=zoom start=100 end=80 center=%s,%s delay=160 tween=back time=240 reversible=false'%(onNextX, onNextY)),
|
||||
('WindowClose', 'effect=fade start=100 end=0 time=240 reversible=false'),
|
||||
('Visible' , 'effect=zoom start=80 end=100 center=%s,%s delay=160 tween=back time=240 reversible=false'%(onNextX, onNextY)),
|
||||
('Visible' , 'effect=fade end=100 time=240 reversible=false')])
|
||||
self.getControl(40002).setImage(COLOR_LOGO if logo.endswith('wlogo.png') else logo)
|
||||
self.getControl(40003).setText('%s %s[CR]%s [B]%s[/B]'%(LANGUAGE(32104),onNow,LANGUAGE(32116),onNext))
|
||||
self.getControl(40004).setImage(land)
|
||||
except Exception as e:
|
||||
self.log("onInit, failed! %s"%(e), xbmc.LOGERROR)
|
||||
self.close()
|
||||
|
||||
|
||||
class Restart(xbmcgui.WindowXMLDialog):
|
||||
closing = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
|
||||
self.player = kwargs.get('player', None)
|
||||
self.sysInfo = kwargs.get('sysInfo', self.player.sysInfo)
|
||||
self.monitor = self.player.service.monitor
|
||||
|
||||
if bool(self.player.restartPercentage) and self.sysInfo.get('fitem'):
|
||||
progress = self.player.getPlayerProgress()
|
||||
self.log("__init__, restartPercentage = %s, progress = %s"%(self.player.restartPercentage, progress))
|
||||
if (progress >= self.player.restartPercentage and progress < self.player.maxProgress) and not isFiller(self.sysInfo.get('fitem',{})):
|
||||
self.doModal()
|
||||
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
return log('%s: %s'%(self.__class__.__name__,msg),level)
|
||||
|
||||
|
||||
def _isVisible(self, control):
|
||||
try: return control.isVisible()
|
||||
except: return (BUILTIN.getInfoBool('Playing','Player') and not bool(BUILTIN.getInfoBool('IsVisible(fullscreeninfo)','Window')) | BUILTIN.getInfoBool('IsVisible(fullscreenvideo)','Window'))
|
||||
|
||||
|
||||
def onInit(self):
|
||||
self.log("onInit")
|
||||
try:
|
||||
prog = 0
|
||||
wait = OSD_TIMER*2
|
||||
tot = wait
|
||||
control = self.getControl(40000)
|
||||
control.setVisibleCondition('[Player.Playing + !Window.IsVisible(fullscreeninfo) + Window.IsVisible(fullscreenvideo)]')
|
||||
xpos = control.getX()
|
||||
|
||||
while not self.monitor.abortRequested():
|
||||
if (self.monitor.waitForAbort(0.5) or self._isVisible(control) or self.closing): break
|
||||
|
||||
while not self.monitor.abortRequested():
|
||||
if (self.monitor.waitForAbort(0.5) or wait < 0 or self.closing or not self.player.isPlaying()): break
|
||||
else:
|
||||
prog = int((abs(wait-tot)*100)//tot)
|
||||
if prog > 0: control.setAnimations([('Conditional', 'effect=zoom start=%s,100 end=%s,100 time=1000 center=%s,100 condition=True'%((prog-20),(prog),xpos))])
|
||||
wait -= 1
|
||||
|
||||
control.setAnimations([('Conditional', 'effect=fade start=%s end=0 time=240 delay=0.240 condition=True'%(prog))])
|
||||
control.setVisible(False)
|
||||
self.setFocusId(40001)
|
||||
except Exception as e: self.log("onInit, failed! %s\ncitem = %s"%(e,self.sysInfo), xbmc.LOGERROR)
|
||||
self.log("onInit, closing")
|
||||
self.close()
|
||||
|
||||
|
||||
def onAction(self, act):
|
||||
actionId = act.getId()
|
||||
self.log('onAction: actionId = %s'%(actionId))
|
||||
self.closing = True
|
||||
if actionId in ACTION_SELECT_ITEM and self.getFocusId(40001):
|
||||
if self.sysInfo.get('isPlaylist',False): self.player.seekTime(0)
|
||||
elif self.sysInfo.get('fitem'):
|
||||
with BUILTIN.busy_dialog():
|
||||
liz = LISTITEMS.buildItemListItem(self.sysInfo.get('fitem',{}))
|
||||
liz.setProperty('sysInfo',encodeString(dumpJSON(self.sysInfo)))
|
||||
self.player.stop()
|
||||
timerit(self.player.play)(1.0,[self.sysInfo.get('fitem',{}).get('catchup-id'),liz])
|
||||
else: DIALOG.notificationDialog(LANGUAGE(30154))
|
||||
elif actionId == ACTION_MOVE_UP: timerit(BUILTIN.executebuiltin)(0.1,['AlarmClock(up,Action(up),.5,true,false)'])
|
||||
elif actionId == ACTION_MOVE_DOWN: timerit(BUILTIN.executebuiltin)(0.1,['AlarmClock(down,Action(down),.5,true,false)'])
|
||||
elif actionId in ACTION_PREVIOUS_MENU: timerit(BUILTIN.executebuiltin)(0.1,['AlarmClock(back,Action(back),.5,true,false)'])
|
||||
|
||||
|
||||
def onClose(self):
|
||||
self.log("onClose")
|
||||
self.closing = True
|
||||
|
||||
|
||||
class Overlay():
|
||||
channelBug = None
|
||||
vignette = None
|
||||
controlManager = dict()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.log("__init__")
|
||||
self.player = kwargs.get('player', None)
|
||||
self.sysInfo = kwargs.get('sysInfo', self.player.sysInfo)
|
||||
|
||||
self.service = self.player.service
|
||||
self.jsonRPC = self.player.jsonRPC
|
||||
self.runActions = self.player.runActions
|
||||
self.resources = Resources(service=self.service)
|
||||
|
||||
self.window = xbmcgui.Window(12005)
|
||||
self.window_w, self.window_h = WH
|
||||
|
||||
#vignette rules
|
||||
self.enableVignette = False
|
||||
self.defaultView = self.jsonRPC.getViewMode()
|
||||
self.vinView = self.defaultView
|
||||
self.vinImage = ''
|
||||
|
||||
#channelBug rules
|
||||
self.enableChannelBug = SETTINGS.getSettingBool('Enable_ChannelBug')
|
||||
self.forceBugDiffuse = SETTINGS.getSettingBool('Force_Diffuse')
|
||||
self.channelBugColor = '0x%s'%((SETTINGS.getSetting('ChannelBug_Color') or 'FFFFFFFF'))
|
||||
self.channelBugFade = SETTINGS.getSettingInt('ChannelBug_Transparency')
|
||||
|
||||
try: self.channelBugX, self.channelBugY = eval(SETTINGS.getSetting("Channel_Bug_Position_XY")) #user
|
||||
except: self.channelBugX, self.channelBugY = abs(int(self.window_w // 9) - self.window_w) - 128, abs(int(self.window_h // 16) - self.window_h) - 128 #auto
|
||||
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
return log('%s: %s'%(self.__class__.__name__,msg),level)
|
||||
|
||||
|
||||
def _hasControl(self, control):
|
||||
return control in self.controlManager
|
||||
|
||||
|
||||
def _isVisible(self, control):
|
||||
return self.controlManager.get(control,False)
|
||||
|
||||
|
||||
def _setVisible(self, control, state: bool=False):
|
||||
self.log('_setVisible, %s = %s'%(control,state))
|
||||
try:
|
||||
control.setVisible(state)
|
||||
return state
|
||||
except Exception as e:
|
||||
self.log('_setVisible, failed! control = %s %s'%(control,e))
|
||||
self._delControl(control)
|
||||
return False
|
||||
|
||||
|
||||
def _addControl(self, control):
|
||||
if not self._hasControl(control):
|
||||
try:
|
||||
self.log('_addControl, %s'%(control))
|
||||
self.window.addControl(control)
|
||||
self.controlManager[control] = self._setVisible(control,True)
|
||||
except Exception as e:
|
||||
self.log('_addControl failed! control = %s %s'%(control, e), xbmc.LOGERROR)
|
||||
self._delControl(control)
|
||||
|
||||
|
||||
def _delControl(self, control):
|
||||
if self._hasControl(control):
|
||||
self.log('_delControl, %s'%(control))
|
||||
try: self.window.removeControl(control)
|
||||
except Exception as e: self.log('_delControl failed! control = %s %s'%(control, e), xbmc.LOGERROR)
|
||||
self.controlManager.pop(control)
|
||||
|
||||
|
||||
def open(self):
|
||||
if self.sysInfo.get('citem',{}):
|
||||
self.runActions(RULES_ACTION_OVERLAY_OPEN, self.sysInfo.get('citem',{}), inherited=self)
|
||||
self.log("open, enableVignette = %s, enableChannelBug = %s"%(self.enableVignette, self.enableChannelBug))
|
||||
if self.enableVignette:
|
||||
window_h, window_w = (self.window.getHeight(), self.window.getWidth())
|
||||
self.vignette = xbmcgui.ControlImage(0, 0, window_w, window_h, ' ', aspectRatio=0)
|
||||
self._addControl(self.vignette)
|
||||
self.vignette.setImage(self.vinImage)
|
||||
if self.vinView != self.defaultView: timerit(self.jsonRPC.setViewMode)(0.5,[self.vinView])
|
||||
self.vignette.setAnimations([('Conditional', 'effect=fade start=0 end=100 time=240 delay=160 condition=True reversible=True')])
|
||||
self.log('enableVignette, vinImage = %s, vinView = %s'%(self.vinImage,self.vinView))
|
||||
|
||||
if self.enableChannelBug:
|
||||
self.channelBug = xbmcgui.ControlImage(self.channelBugX, self.channelBugY, 128, 128, ' ', aspectRatio=2)
|
||||
self._addControl(self.channelBug)
|
||||
|
||||
logo = self.sysInfo.get('citem',{}).get('logo',(BUILTIN.getInfoLabel('Art(icon)','Player') or LOGO))
|
||||
if self.forceBugDiffuse: self.channelBug.setColorDiffuse(self.channelBugColor)
|
||||
elif self.resources.isMono(logo): self.channelBug.setColorDiffuse(self.channelBugColor)
|
||||
self.channelBug.setImage(logo)
|
||||
self.channelBug.setAnimations([('Conditional', 'effect=fade start=0 end=100 time=2000 delay=1000 condition=Control.IsVisible(%i) reversible=false'%(self.channelBug.getId())),
|
||||
('Conditional', 'effect=fade start=100 end=%i time=1000 delay=3000 condition=Control.IsVisible(%i) reversible=false'%(self.channelBugFade,self.channelBug.getId()))])
|
||||
self.log('enableChannelBug, logo = %s, channelBugColor = %s, window = (%s,%s)'%(logo,self.channelBugColor,self.window_h, self.window_w))
|
||||
else: self.close()
|
||||
|
||||
|
||||
def close(self):
|
||||
self.log("close")
|
||||
self._delControl(self.vignette)
|
||||
self._delControl(self.channelBug)
|
||||
if self.vinView != self.defaultView: timerit(self.jsonRPC.setViewMode)(0.5,[self.defaultView])
|
||||
self.runActions(RULES_ACTION_OVERLAY_CLOSE, self.sysInfo.get('citem',{}), inherited=self)
|
||||
|
||||
|
||||
class OnNext(xbmcgui.WindowXMLDialog):
|
||||
closing = False
|
||||
totalTime = 0
|
||||
threshold = 0
|
||||
remaining = 0
|
||||
intTime = 0
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
|
||||
self.player = kwargs.get('player' , None)
|
||||
self.sysInfo = kwargs.get('sysInfo' , self.player.sysInfo)
|
||||
self.onNextMode = kwargs.get('mode' , SETTINGS.getSettingInt('OnNext_Mode'))
|
||||
self.onNextPosition = kwargs.get('position' , SETTINGS.getSetting("OnNext_Position_XY"))
|
||||
|
||||
self.jsonRPC = self.player.jsonRPC
|
||||
self.monitor = self.player.service.monitor
|
||||
|
||||
self.citem = self.sysInfo.get('citem',{})
|
||||
self.fitem = self.sysInfo.get('fitem',{})
|
||||
self.nitem = self.jsonRPC.getNextItem(self.citem,self.sysInfo.get('nitem'))
|
||||
|
||||
self.window = xbmcgui.Window(12005)
|
||||
self.window_w, self.window_h = WH #self.window_h, self.window_w = (self.window.getHeight(), self.window.getWidth())
|
||||
|
||||
try: self.onNextX, self.onNextY = eval(self.onNextPosition) #user
|
||||
except: self.onNextX, self.onNextY = abs(int(self.window_w // 9)), abs(int(self.window_h // 16) - self.window_h) - 356 #auto
|
||||
|
||||
self.log('__init__, enableOnNext = %s, onNextMode = %s, onNextX = %s, onNextY = %s'%(bool(self.onNextMode),self.onNextMode,self.onNextX,self.onNextY))
|
||||
|
||||
if not isFiller(self.fitem):
|
||||
self.totalTime = int(self.player.getPlayerTime() * (self.player.maxProgress / 100))
|
||||
self.threshold = abs((self.totalTime - (self.totalTime * .75)) - (ONNEXT_TIMER*3))
|
||||
self.remaining = floor(self.totalTime - self.player.getPlayedTime())
|
||||
self.intTime = roundupDIV(self.threshold,3)
|
||||
self.log('__init__, totalTime = %s, threshold = %s, remaining = %s, intTime = %s'%(self.totalTime,self.threshold,self.remaining,self.intTime))
|
||||
if self.remaining >= self.intTime:
|
||||
self.doModal()
|
||||
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
return log('%s: %s'%(self.__class__.__name__,msg),level)
|
||||
|
||||
|
||||
def onInit(self):
|
||||
def __chkCitem():
|
||||
return sorted(self.citem) == sorted(self.player.sysInfo.get('citem',{}))
|
||||
try:
|
||||
self.log('onInit, citem = %s\nfitem = %ss\nnitem = %s'%(self.citem,self.fitem,self.nitem))
|
||||
if self.onNextMode in [1,2]:
|
||||
logo = (self.citem.get('logo') or BUILTIN.getInfoLabel('Art(icon)','Player') or LOGO)
|
||||
land = (getThumb(self.nitem) or COLOR_FANART)
|
||||
chname = (self.citem.get('name') or BUILTIN.getInfoLabel('ChannelName','VideoPlayer'))
|
||||
nowTitle = (self.fitem.get('label') or BUILTIN.getInfoLabel('Title','VideoPlayer'))
|
||||
nextTitle = (self.nitem.get('showlabel') or BUILTIN.getInfoLabel('NextTitle','VideoPlayer') or chname)
|
||||
|
||||
try: nextTime = epochTime(self.nitem['start']).strftime('%I:%M%p')
|
||||
except Exception as e:
|
||||
self.log("__init__, nextTime failed! %s\nstart = %s"%(e,self.nitem.get('start')), xbmc.LOGERROR)
|
||||
nextTime = BUILTIN.getInfoLabel('NextStartTime','VideoPlayer')
|
||||
|
||||
onNow = '%s on %s'%(nowTitle,chname) if chname not in validString(nowTitle) else nowTitle
|
||||
onNext = '@ %s: %s'%(nextTime,nextTitle)
|
||||
|
||||
self.getControl(40001).setPosition(self.onNextX, self.onNextY)
|
||||
self.getControl(40001).setVisibleCondition('[Player.Playing + !Window.IsVisible(fullscreeninfo) + Window.IsVisible(fullscreenvideo)]')
|
||||
self.getControl(40001).setAnimations([('WindowOpen' , 'effect=zoom start=80 end=100 center=%s,%s delay=160 tween=back time=240 reversible=false'%(self.onNextX, self.onNextY)),
|
||||
('WindowOpen' , 'effect=fade start=0 end=100 delay=160 time=240 reversible=false'),
|
||||
('WindowClose', 'effect=zoom start=100 end=80 center=%s,%s delay=160 tween=back time=240 reversible=false'%(self.onNextX, self.onNextY)),
|
||||
('WindowClose', 'effect=fade start=100 end=0 time=240 reversible=false'),
|
||||
('Visible' , 'effect=zoom start=80 end=100 center=%s,%s delay=160 tween=back time=240 reversible=false'%(self.onNextX, self.onNextY)),
|
||||
('Visible' , 'effect=fade end=100 time=240 reversible=false')])
|
||||
self.onNext_Text = self.getControl(40003)
|
||||
self.onNext_Text.setVisible(False)
|
||||
self.onNext_Text.setText('%s %s[CR]%s [B]%s[/B]'%(LANGUAGE(32104),onNow,LANGUAGE(32116),onNext))
|
||||
|
||||
if self.onNextMode == 2:
|
||||
self.onNext_Artwork = self.getControl(40004)
|
||||
self.onNext_Artwork.setVisible(False)
|
||||
self.onNext_Artwork.setImage(land)
|
||||
|
||||
self.onNext_Text.setVisible(True)
|
||||
self.onNext_Artwork.setVisible(True)
|
||||
xbmc.playSFX(BING_WAV)
|
||||
|
||||
show = ONNEXT_TIMER*2
|
||||
while not self.monitor.abortRequested() and not self.closing:
|
||||
self.log('onInit, showing (%s)'%(show))
|
||||
if self.monitor.waitForAbort(0.5) or not self.player.isPlaying() or show < 1 or not __chkCitem(): break
|
||||
else: show -= 1
|
||||
|
||||
self.onNext_Text.setVisible(False)
|
||||
self.onNext_Artwork.setVisible(False)
|
||||
|
||||
elif self.onNextMode == 3: self.player.toggleInfo()
|
||||
elif self.onNextMode == 4: self._updateUpNext(self.fitem,self.nitem)
|
||||
|
||||
wait = self.intTime*2
|
||||
while not self.monitor.abortRequested() and not self.closing:
|
||||
self.log('onInit, waiting (%s)'%(wait))
|
||||
if self.monitor.waitForAbort(0.5) or not self.player.isPlaying() or wait < 1 or not __chkCitem(): break
|
||||
else: wait -= 1
|
||||
|
||||
except Exception as e: self.log("onInit, failed! %s"%(e), xbmc.LOGERROR)
|
||||
self.log("onInit, closing")
|
||||
self.close()
|
||||
|
||||
|
||||
def _updateUpNext(self, nowItem: dict={}, nextItem: dict={}):
|
||||
self.log('_updateUpNext')
|
||||
data = dict()
|
||||
try:# https://github.com/im85288/service.upnext/wiki/Example-source-code
|
||||
data.update({"notification_offset":int(floor(self.player.getRemainingTime())) + OSD_TIMER})
|
||||
current_episode = {"current_episode":{"episodeid" :(nowItem.get("id") or ""),
|
||||
"tvshowid" :(nowItem.get("tvshowid") or ""),
|
||||
"title" :(nowItem.get("title") or ""),
|
||||
"art" :(nowItem.get("art") or ""),
|
||||
"season" :(nowItem.get("season") or ""),
|
||||
"episode" :(nowItem.get("episode") or ""),
|
||||
"showtitle" :(nowItem.get("tvshowtitle") or ""),
|
||||
"plot" :(nowItem.get("plot") or ""),
|
||||
"playcount" :(nowItem.get("playcount") or ""),
|
||||
"rating" :(nowItem.get("rating") or ""),
|
||||
"firstaired":(nowItem.get("firstaired") or ""),
|
||||
"runtime" :(nowItem.get("runtime") or "")}}
|
||||
data.update(current_episode)
|
||||
except: pass
|
||||
try:
|
||||
next_episode = {"next_episode" :{"episodeid" :(nextItem.get("id") or ""),
|
||||
"tvshowid" :(nextItem.get("tvshowid") or ""),
|
||||
"title" :(nextItem.get("title") or ""),
|
||||
"art" :(nextItem.get("art") or ""),
|
||||
"season" :(nextItem.get("season") or ""),
|
||||
"episode" :(nextItem.get("episode") or ""),
|
||||
"showtitle" :(nextItem.get("tvshowtitle") or ""),
|
||||
"plot" :(nextItem.get("plot" ) or ""),
|
||||
"playcount" :(nextItem.get("playcount") or ""),
|
||||
"rating" :(nextItem.get("rating") or ""),
|
||||
"firstaired":(nextItem.get("firstaired") or ""),
|
||||
"runtime" :(nextItem.get("runtime") or "")}}
|
||||
data.update(next_episode)
|
||||
|
||||
except: pass
|
||||
if data: timerit(self.jsonRPC.notifyAll)(0.5,['upnext_data', binascii.hexlify(json.dumps(data).encode('utf-8')).decode('utf-8'), '%s.SIGNAL'%(ADDON_ID)])
|
||||
|
||||
|
||||
def onAction(self, act):
|
||||
actionId = act.getId()
|
||||
self.log('onAction: actionId = %s'%(actionId))
|
||||
self.closing = True
|
||||
if actionId == ACTION_MOVE_UP: timerit(BUILTIN.executebuiltin)(0.1,['AlarmClock(up,Action(up),.5,true,false)'])
|
||||
elif actionId == ACTION_MOVE_DOWN: timerit(BUILTIN.executebuiltin)(0.1,['AlarmClock(down,Action(down),.5,true,false)'])
|
||||
elif actionId in ACTION_PREVIOUS_MENU: timerit(BUILTIN.executebuiltin)(0.1,['AlarmClock(back,Action(back),.5,true,false)'])
|
||||
|
||||
|
||||
def onClose(self):
|
||||
self.log('onClose')
|
||||
self.closing = True
|
||||
@@ -0,0 +1,192 @@
|
||||
# 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/>.
|
||||
# https://github.com/xbmc/xbmc/blob/master/xbmc/input/actions/ActionIDs.h
|
||||
# https://github.com/xbmc/xbmc/blob/master/xbmc/input/Key.h
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
from globals import *
|
||||
from jsonrpc import JSONRPC
|
||||
|
||||
WH, WIN = BUILTIN.getResolution()
|
||||
|
||||
class OverlayTool(xbmcgui.WindowXMLDialog):
|
||||
focusControl = None
|
||||
focusCycle = None
|
||||
focusCycleLST = []
|
||||
focusCNTRLST = {}
|
||||
lastActionTime = time.time()
|
||||
posx, posy = 0, 0
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
|
||||
self.log('__init__, args = %s, kwargs = %s'%(args,kwargs))
|
||||
with BUILTIN.busy_dialog():
|
||||
self.jsonRPC = JSONRPC()
|
||||
if BUILTIN.getInfoBool('Playing','Player'): self.window = xbmcgui.Window(12005)
|
||||
else: self.window = xbmcgui.Window(10000)
|
||||
self.window_w, self.window_h = WH
|
||||
self.advRule = (kwargs.get("ADV_RULES") or False)
|
||||
self.focusIDX = (kwargs.get("Focus_IDX") or 1)
|
||||
|
||||
self.defaultView = {}#self.jsonRPC.getViewMode()
|
||||
self.vinView = (kwargs.get("Vignette_VideoMode") or self.defaultView)
|
||||
self.vinImage = (kwargs.get("Vignette_Image") or os.path.join(MEDIA_LOC,'overlays','ratio.png'))
|
||||
self.channelBugColor = '0x%s'%((kwargs.get("ChannelBug_Color") or SETTINGS.getSetting('ChannelBug_Color')))
|
||||
|
||||
try:
|
||||
self.autoBugX, self.autoBugY = abs(int(self.window_w // 9) - self.window_w) - 128, abs(int(self.window_h // 16) - self.window_h) - 128 #auto
|
||||
self.channelBugX, self.channelBugY = eval(SETTINGS.getSetting("Channel_Bug_Position_XY")) #user
|
||||
except:
|
||||
self.channelBugX, self.channelBugY = self.autoBugX, self.autoBugY
|
||||
|
||||
try:
|
||||
self.autoNextX, self.autoNextY = abs(int(self.window_w // 9)), abs(int(self.window_h // 16) - self.window_h) - 356 #auto
|
||||
self.onNextX, self.onNextY = eval(kwargs.get("OnNext_Position_XY",SETTINGS.getSetting("OnNext_Position_XY")))
|
||||
except: self.onNextX, self.onNextY = self.autoNextX, self.autoNextY
|
||||
|
||||
try:
|
||||
# self.runActions(RULES_ACTION_OVERLAY_OPEN, self.sysInfo.get('citem',{}), inherited=self)
|
||||
# if self.vinView != self.defaultView: timerit(self.jsonRPC.setViewMode)(0.1,[self.vinView])
|
||||
if BUILTIN.getInfoBool('Playing','Player'): BUILTIN.executebuiltin('ActivateWindow(fullscreenvideo)')
|
||||
self.doModal()
|
||||
except Exception as e:
|
||||
self.log("__init__, failed! %s"%(e), xbmc.LOGERROR)
|
||||
self.close()
|
||||
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
return log('%s: %s'%(self.__class__.__name__,msg),level)
|
||||
|
||||
|
||||
def onInit(self):
|
||||
if not BUILTIN.getInfoBool('IsFullscreen','System'):
|
||||
DIALOG.okDialog(LANGUAGE(32097)%(BUILTIN.getInfoLabel('ScreenResolution','System')))
|
||||
|
||||
self.posx, self.posy = 0, 0
|
||||
|
||||
self.vignetteControl = xbmcgui.ControlImage(0, 0, self.window_w, self.window_h, self.vinImage, aspectRatio=0) #IDX 0
|
||||
self.channelBug = xbmcgui.ControlImage(self.channelBugX, self.channelBugY, 128, 128, COLOR_LOGO, 2, self.channelBugColor) #IDX 1
|
||||
self.onNext_Artwork = xbmcgui.ControlImage(self.onNextX, self.onNextY, 256, 128, COLOR_FANART, 0) #IDX 2
|
||||
self.onNext_Text = xbmcgui.ControlTextBox(self.onNextX, (self.onNextY + 140), 960, 72, 'font27', '0xFFFFFFFF')
|
||||
self.onNext_Text.setText('%s %s[CR]%s: %s'%(LANGUAGE(32104),ADDON_NAME,LANGUAGE(32116),ADDON_NAME))
|
||||
|
||||
self._addCntrl(self.vignetteControl)
|
||||
self._addCntrl(self.channelBug)
|
||||
self._addCntrl(self.onNext_Artwork)
|
||||
self._addCntrl(self.onNext_Text, incl=False)
|
||||
|
||||
self.focusCycleLST.insert(0,self.focusCycleLST.pop(self.focusIDX))
|
||||
self.focusCycle = cycle(self.focusCycleLST).__next__
|
||||
self.focusControl = self.focusCycle()
|
||||
self.switch(self.focusControl)
|
||||
|
||||
|
||||
def _addCntrl(self, cntrl, incl=True):
|
||||
self.log('_addCntrl cntrl = %s'%(cntrl))
|
||||
self.addControl(cntrl)
|
||||
cntrl.setVisible(True)
|
||||
if incl and not cntrl in self.focusCycleLST: self.focusCycleLST.append(cntrl)
|
||||
if not cntrl in self.focusCNTRLST: self.focusCNTRLST[cntrl] = cntrl.getX(),cntrl.getY()
|
||||
|
||||
|
||||
def switch(self, cntrl=None):
|
||||
self.log('switch cntrl = %s'%(cntrl))
|
||||
if not self.focusCycle is None:
|
||||
if cntrl is None: self.focusControl = self.focusCycle()
|
||||
else: self.focusControl = cntrl
|
||||
self._setFocus(self.focusControl)
|
||||
|
||||
for cntrl in self.focusCNTRLST:
|
||||
if self.focusControl != cntrl: cntrl.setAnimations([('Conditional', 'effect=fade start=100 end=25 time=240 delay=160 condition=True reversible=False')])
|
||||
else:
|
||||
self.posx, self.posy = cntrl.getX(),cntrl.getY()
|
||||
cntrl.setAnimations([('Conditional', 'effect=fade start=25 end=100 time=240 delay=160 condition=True reversible=False')])
|
||||
|
||||
|
||||
def move(self, cntrl):
|
||||
posx, posy = self.focusCNTRLST[cntrl]
|
||||
if (self.posx != posx or self.posy != posy):
|
||||
cntrl.setPosition(self.posx, self.posy)
|
||||
if cntrl == self.onNext_Artwork:
|
||||
self.onNext_Text.setPosition(self.posx, (self.posy + 140))
|
||||
|
||||
|
||||
def save(self):
|
||||
changes = {}
|
||||
for cntrl in self.focusCNTRLST:
|
||||
posx, posy = cntrl.getX(), cntrl.getY()
|
||||
if cntrl == self.channelBug:
|
||||
if (posx != self.channelBugX or posy != self.channelBugY):
|
||||
changes[cntrl] = posx, posy, (posx == self.autoBugX & posy == self.autoBugY)
|
||||
elif cntrl == self.onNext_Artwork:
|
||||
if (posx != self.onNextX or posy != self.onNextY):
|
||||
changes[cntrl] = posx, posy, (posx == self.autoNextX & posy == self.autoNextY)
|
||||
|
||||
if changes:
|
||||
self.log('save, saving %s'%(changes))
|
||||
if DIALOG.yesnoDialog(LANGUAGE(32096)):
|
||||
for cntrl, value in list(changes.items()): self.set(cntrl,value[0],value[1],value[2])
|
||||
# if self.vinView != self.defaultView: timerit(self.jsonRPC.setViewMode)(0.1,[self.defaultView])
|
||||
self.close()
|
||||
|
||||
|
||||
def set(self, cntrl, posx, posy, auto=False):
|
||||
self.log('set, cntrl = %s, posx,posy = (%s,%s) %s? %s'%(cntrl, posx, posy, LANGUAGE(30022), auto))
|
||||
if self.advRule: save = PROPERTIES.setProperty
|
||||
else: save = SETTINGS.setSetting
|
||||
|
||||
if cntrl == self.channelBug:
|
||||
if auto: save("Channel_Bug_Position_XY",LANGUAGE(30022))
|
||||
else: save("Channel_Bug_Position_XY","(%s,%s)"%(posx, posy))
|
||||
elif cntrl == self.onNext_Artwork:
|
||||
if auto: save("OnNext_Position_XY",LANGUAGE(30022))
|
||||
else: save("OnNext_Position_XY","(%s,%s)"%(posx, posy))
|
||||
|
||||
|
||||
def _setFocus(self, cntrl):
|
||||
self.log('_setFocus cntrl = %s'%(cntrl))
|
||||
try: self.setFocus(cntrl)
|
||||
except: pass
|
||||
|
||||
|
||||
def _getFocus(self, cntrl):
|
||||
self.log('_getFocus cntrl = %s'%(cntrl))
|
||||
try: self.getFocus(cntrl)
|
||||
except: pass
|
||||
|
||||
|
||||
def onAction(self, act):
|
||||
actionId = act.getId()
|
||||
self.log('onAction: actionId = %s'%(actionId))
|
||||
lastaction = time.time() - self.lastActionTime
|
||||
# during certain times we just want to discard all input
|
||||
if lastaction < 3 and lastaction > 1 and actionId not in ACTION_PREVIOUS_MENU:
|
||||
self.log('Not allowing actions')
|
||||
action = ACTION_INVALID
|
||||
elif actionId in ACTION_SELECT_ITEM: self.switch(self.focusCycle())
|
||||
elif actionId in ACTION_PREVIOUS_MENU: self.save()
|
||||
else:
|
||||
if actionId == ACTION_MOVE_UP: self.posy-=1
|
||||
elif actionId == ACTION_MOVE_DOWN: self.posy+=1
|
||||
elif actionId == ACTION_MOVE_LEFT: self.posx-=1
|
||||
elif actionId == ACTION_MOVE_RIGHT: self.posx+=1
|
||||
else: return
|
||||
self.move((self.focusControl))
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
# Copyright (C) 2024 Jason Anderson, 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/>.
|
||||
|
||||
from globals import *
|
||||
|
||||
class AVIChunk:
|
||||
def __init__(self):
|
||||
self.empty()
|
||||
|
||||
|
||||
def empty(self):
|
||||
self.size = 0
|
||||
self.fourcc = ''
|
||||
self.datatype = 1
|
||||
self.chunk = ''
|
||||
|
||||
|
||||
def read(self, thefile):
|
||||
data = thefile.readBytes(4)
|
||||
try: self.size = struct.unpack('<i', data)[0]
|
||||
except: self.size = 0
|
||||
# Putting an upper limit on the chunk size, in case the file is corrupt
|
||||
if self.size > 0 and self.size < 10000:
|
||||
self.chunk = thefile.readBytes(self.size)
|
||||
else:
|
||||
self.chunk = ''
|
||||
self.size = 0
|
||||
|
||||
|
||||
class AVIList:
|
||||
def __init__(self):
|
||||
self.empty()
|
||||
|
||||
|
||||
def empty(self):
|
||||
self.size = 0
|
||||
self.fourcc = ''
|
||||
self.datatype = 2
|
||||
|
||||
|
||||
def read(self, thefile):
|
||||
data = thefile.readBytes(4)
|
||||
self.size = struct.unpack('<i', data)[0]
|
||||
try: self.size = struct.unpack('<i', data)[0]
|
||||
except: self.size = 0
|
||||
self.fourcc = thefile.read(4)
|
||||
|
||||
|
||||
class AVIHeader:
|
||||
def __init__(self):
|
||||
self.empty()
|
||||
|
||||
|
||||
def empty(self):
|
||||
self.dwMicroSecPerFrame = 0
|
||||
self.dwMaxBytesPerSec = 0
|
||||
self.dwPaddingGranularity = 0
|
||||
self.dwFlags = 0
|
||||
self.dwTotalFrames = 0
|
||||
self.dwInitialFrames = 0
|
||||
self.dwStreams = 0
|
||||
self.dwSuggestedBufferSize = 0
|
||||
self.dwWidth = 0
|
||||
self.dwHeight = 0
|
||||
|
||||
|
||||
|
||||
class AVIStreamHeader:
|
||||
def __init__(self):
|
||||
self.empty()
|
||||
|
||||
|
||||
def empty(self):
|
||||
self.fccType = ''
|
||||
self.fccHandler = ''
|
||||
self.dwFlags = 0
|
||||
self.wPriority = 0
|
||||
self.wLanguage = 0
|
||||
self.dwInitialFrame = 0
|
||||
self.dwScale = 0
|
||||
self.dwRate = 0
|
||||
self.dwStart = 0
|
||||
self.dwLength = 0
|
||||
self.dwSuggestedBuffer = 0
|
||||
self.dwQuality = 0
|
||||
self.dwSampleSize = 0
|
||||
self.rcFrame = ''
|
||||
|
||||
|
||||
|
||||
class AVIParser:
|
||||
def __init__(self):
|
||||
self.Header = AVIHeader()
|
||||
self.StreamHeader = AVIStreamHeader()
|
||||
|
||||
|
||||
def determineLength(self, filename: str) -> int and float:
|
||||
log("AVIParser: determineLength %s"%filename)
|
||||
|
||||
try: self.File = FileAccess.open(filename, "rb", None)
|
||||
except:
|
||||
log("AVIParser: Unable to open the file")
|
||||
return 0
|
||||
|
||||
dur = int(self.readHeader())
|
||||
self.File.close()
|
||||
log('AVIParser: Duration is %s'%(dur))
|
||||
return dur
|
||||
|
||||
|
||||
def readHeader(self):
|
||||
# AVI Chunk
|
||||
data = self.getChunkOrList()
|
||||
|
||||
if data.datatype != 2:
|
||||
log("AVIParser: Not an avi")
|
||||
return 0
|
||||
#todo fix
|
||||
if data.fourcc[0:4] != "AVI ":
|
||||
log("AVIParser: Wrong FourCC")
|
||||
return 0
|
||||
|
||||
# Header List
|
||||
data = self.getChunkOrList()
|
||||
if data.fourcc != "hdrl":
|
||||
log("AVIParser: Header not found")
|
||||
return 0
|
||||
|
||||
# Header chunk
|
||||
data = self.getChunkOrList()
|
||||
|
||||
if data.fourcc != 'avih':
|
||||
log('Header chunk not found')
|
||||
return 0
|
||||
|
||||
self.parseHeader(data)
|
||||
# Stream list
|
||||
data = self.getChunkOrList()
|
||||
|
||||
if self.Header.dwStreams > 10:
|
||||
self.Header.dwStreams = 10
|
||||
|
||||
for i in range(self.Header.dwStreams):
|
||||
if data.datatype != 2:
|
||||
log("AVIParser: Unable to find streams")
|
||||
return 0
|
||||
|
||||
listsize = data.size
|
||||
# Stream chunk number 1, the stream header
|
||||
data = self.getChunkOrList()
|
||||
|
||||
if data.datatype != 1:
|
||||
log("AVIParser: Broken stream header")
|
||||
return 0
|
||||
|
||||
self.StreamHeader.empty()
|
||||
self.parseStreamHeader(data)
|
||||
|
||||
# If this is the video header, determine the duration
|
||||
if self.StreamHeader.fccType == 'vids':
|
||||
return self.getStreamDuration()
|
||||
|
||||
# If this isn't the video header, skip through the rest of these
|
||||
# stream chunks
|
||||
try:
|
||||
if listsize - data.size - 12 > 0:
|
||||
self.File.seek(listsize - data.size - 12, 1)
|
||||
|
||||
data = self.getChunkOrList()
|
||||
except:
|
||||
log("AVIParser: Unable to seek")
|
||||
|
||||
log("AVIParser: Video stream not found")
|
||||
return 0
|
||||
|
||||
|
||||
def getStreamDuration(self):
|
||||
try:
|
||||
return int(self.StreamHeader.dwLength / (float(self.StreamHeader.dwRate) / float(self.StreamHeader.dwScale)))
|
||||
except:
|
||||
return 0
|
||||
|
||||
|
||||
def parseHeader(self, data):
|
||||
try:
|
||||
header = struct.unpack('<iiiiiiiiiiiiii', data.chunk)
|
||||
self.Header.dwMicroSecPerFrame = header[0]
|
||||
self.Header.dwMaxBytesPerSec = header[1]
|
||||
self.Header.dwPaddingGranularity = header[2]
|
||||
self.Header.dwFlags = header[3]
|
||||
self.Header.dwTotalFrames = header[4]
|
||||
self.Header.dwInitialFrames = header[5]
|
||||
self.Header.dwStreams = header[6]
|
||||
self.Header.dwSuggestedBufferSize = header[7]
|
||||
self.Header.dwWidth = header[8]
|
||||
self.Header.dwHeight = header[9]
|
||||
except:
|
||||
self.Header.empty()
|
||||
log("AVIParser: Unable to parse the header")
|
||||
|
||||
|
||||
def parseStreamHeader(self, data):
|
||||
try:
|
||||
self.StreamHeader.fccType = data.chunk[0:4].decode('utf-8')
|
||||
self.StreamHeader.fccHandler = data.chunk[4:8].decode('utf-8')
|
||||
header = struct.unpack('<ihhiiiiiiiid', data.chunk[8:])
|
||||
self.StreamHeader.dwFlags = header[0]
|
||||
self.StreamHeader.wPriority = header[1]
|
||||
self.StreamHeader.wLanguage = header[2]
|
||||
self.StreamHeader.dwInitialFrame = header[3]
|
||||
self.StreamHeader.dwScale = header[4]
|
||||
self.StreamHeader.dwRate = header[5]
|
||||
self.StreamHeader.dwStart = header[6]
|
||||
self.StreamHeader.dwLength = header[7]
|
||||
self.StreamHeader.dwSuggestedBuffer = header[8]
|
||||
self.StreamHeader.dwQuality = header[9]
|
||||
self.StreamHeader.dwSampleSize = header[10]
|
||||
self.StreamHeader.rcFrame = ''
|
||||
except:
|
||||
self.StreamHeader.empty()
|
||||
log("AVIParser: Error reading stream header")
|
||||
|
||||
|
||||
def getChunkOrList(self):
|
||||
try: data = self.File.readBytes(4).decode('utf-8')
|
||||
except: data = self.File.read(4)
|
||||
|
||||
if data == "RIFF" or data == "LIST":
|
||||
dataclass = AVIList()
|
||||
elif len(data) == 0:
|
||||
dataclass = AVIChunk()
|
||||
dataclass.datatype = 3
|
||||
else:
|
||||
dataclass = AVIChunk()
|
||||
dataclass.fourcc = data
|
||||
|
||||
# Fill in the chunk or list info
|
||||
dataclass.read(self.File)
|
||||
return dataclass
|
||||
@@ -0,0 +1,31 @@
|
||||
# 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/>.
|
||||
|
||||
from globals import *
|
||||
|
||||
class FFProbe:
|
||||
def determineLength(self, filename: str) -> int and float :
|
||||
try:
|
||||
import ffmpeg
|
||||
log("FFProbe: determineLength %s"%(filename))
|
||||
dur = ffmpeg.probe(FileAccess.translatePath(filename))["format"]["duration"]
|
||||
log('FFProbe: Duration is %s'%(dur))
|
||||
return dur
|
||||
except Exception as e:
|
||||
log("FFProbe: failed! %s"%(e), xbmc.LOGERROR)
|
||||
return 0
|
||||
@@ -0,0 +1,144 @@
|
||||
# Copyright (C) 2024 Jason Anderson, 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/>.
|
||||
|
||||
from globals import *
|
||||
|
||||
class FLVTagHeader:
|
||||
def __init__(self):
|
||||
self.tagtype = 0
|
||||
self.datasize = 0
|
||||
self.timestamp = 0
|
||||
self.timestampext = 0
|
||||
|
||||
|
||||
def readHeader(self, thefile):
|
||||
try:
|
||||
data = struct.unpack('B', thefile.readBytes(1))[0]
|
||||
self.tagtype = (data & 0x1F)
|
||||
self.datasize = struct.unpack('>H', thefile.readBytes(2))[0]
|
||||
data = struct.unpack('>B', thefile.readBytes(1))[0]
|
||||
self.datasize = (self.datasize << 8) | data
|
||||
self.timestamp = struct.unpack('>H', thefile.readBytes(2))[0]
|
||||
data = struct.unpack('>B', thefile.readBytes(1))[0]
|
||||
self.timestamp = (self.timestamp << 8) | data
|
||||
self.timestampext = struct.unpack('>B', thefile.readBytes(1))[0]
|
||||
except:
|
||||
self.tagtype = 0
|
||||
self.datasize = 0
|
||||
self.timestamp = 0
|
||||
self.timestampext = 0
|
||||
|
||||
|
||||
|
||||
class FLVParser:
|
||||
def __init__(self):
|
||||
self.monitor = MONITOR()
|
||||
|
||||
|
||||
def determineLength(self, filename: str) -> int and float:
|
||||
log("FLVParser: determineLength %s"%filename)
|
||||
|
||||
try: self.File = FileAccess.open(filename, "rb", None)
|
||||
except:
|
||||
log("FLVParser: Unable to open the file")
|
||||
return 0
|
||||
|
||||
if self.verifyFLV() == False:
|
||||
log("FLVParser: Not a valid FLV")
|
||||
self.File.close()
|
||||
return 0
|
||||
|
||||
tagheader = self.findLastVideoTag()
|
||||
|
||||
if tagheader is None:
|
||||
log("FLVParser: Unable to find a video tag")
|
||||
self.File.close()
|
||||
return 0
|
||||
|
||||
dur = int(self.getDurFromTag(tagheader))
|
||||
self.File.close()
|
||||
log("FLVParser: Duration is %s"%(dur))
|
||||
return dur
|
||||
|
||||
|
||||
def verifyFLV(self):
|
||||
data = self.File.read(3)
|
||||
|
||||
if data != 'FLV':
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def findLastVideoTag(self):
|
||||
try:
|
||||
self.File.seek(0, 2)
|
||||
curloc = self.File.tell()
|
||||
except:
|
||||
log("FLVParser: Exception seeking in findLastVideoTag")
|
||||
return None
|
||||
|
||||
# Go through a limited amount of the file before quiting
|
||||
maximum = curloc - (2 * 1024 * 1024)
|
||||
|
||||
if maximum < 0:
|
||||
maximum = 8
|
||||
|
||||
while not self.monitor.abortRequested() and curloc > maximum:
|
||||
try:
|
||||
self.File.seek(-4, 1)
|
||||
data = int(struct.unpack('>I', self.File.readBytes(4))[0])
|
||||
|
||||
if data < 1:
|
||||
log('FLVParser: Invalid packet data')
|
||||
return None
|
||||
|
||||
if curloc - data <= 0:
|
||||
log('FLVParser: No video packet found')
|
||||
return None
|
||||
|
||||
self.File.seek(-4 - data, 1)
|
||||
curloc = curloc - data
|
||||
tag = FLVTagHeader()
|
||||
tag.readHeader(self.File)
|
||||
|
||||
if tag.datasize <= 0:
|
||||
log('FLVParser: Invalid packet header')
|
||||
return None
|
||||
|
||||
if curloc - 8 <= 0:
|
||||
log('FLVParser: No video packet found')
|
||||
return None
|
||||
|
||||
self.File.seek(-8, 1)
|
||||
log("FLVParser: detected tag type %s"%(tag.tagtype))
|
||||
curloc = self.File.tell()
|
||||
|
||||
if tag.tagtype == 9:
|
||||
return tag
|
||||
except:
|
||||
log('FLVParser: Exception in findLastVideoTag')
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def getDurFromTag(self, tag):
|
||||
tottime = tag.timestamp | (tag.timestampext << 24)
|
||||
tottime = int(tottime / 1000)
|
||||
return tottime
|
||||
@@ -0,0 +1,35 @@
|
||||
# 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/>.
|
||||
|
||||
from globals import *
|
||||
|
||||
class Hachoir:
|
||||
def determineLength(self, filename: str) -> int and float:
|
||||
try:
|
||||
meta = {}
|
||||
from hachoir.parser import createParser
|
||||
from hachoir.metadata import extractMetadata
|
||||
log("Hachoir: determineLength %s"%(filename))
|
||||
meta = extractMetadata(createParser(FileAccess.open(filename,'r')))
|
||||
if not meta: raise Exception('No meta found')
|
||||
dur = meta.get('duration').total_seconds()
|
||||
log('Hachoir: Duration is %s'%(dur))
|
||||
return dur
|
||||
except Exception as e:
|
||||
log("Hachoir: failed! %s\nmeta = %s"%(e,meta), xbmc.LOGERROR)
|
||||
return 0
|
||||
@@ -0,0 +1,210 @@
|
||||
# Copyright (C) 2024Jason Anderson, 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/>.
|
||||
|
||||
from globals import *
|
||||
|
||||
class MKVParser:
|
||||
monitor = xbmc.Monitor()
|
||||
|
||||
def determineLength(self, filename: str) -> int and float:
|
||||
log("MKVParser: determineLength %s"%filename)
|
||||
try: self.File = FileAccess.open(filename, "rb", None)
|
||||
except:
|
||||
log("MKVParser: Unable to open the file")
|
||||
log(traceback.format_exc(), xbmc.LOGERROR)
|
||||
return
|
||||
|
||||
size = self.findHeader()
|
||||
|
||||
if size == 0:
|
||||
log('MKVParser: Unable to find the segment info')
|
||||
dur = 0
|
||||
else:
|
||||
dur = int(round(self.parseHeader(size)))
|
||||
|
||||
log("MKVParser: Duration is %s"%(dur))
|
||||
return dur
|
||||
|
||||
|
||||
def parseHeader(self, size):
|
||||
duration = 0
|
||||
timecode = 0
|
||||
fileend = self.File.tell() + size
|
||||
datasize = 1
|
||||
data = 1
|
||||
|
||||
while not self.monitor.abortRequested() and self.File.tell() < fileend and datasize > 0 and data > 0:
|
||||
data = self.getEBMLId()
|
||||
datasize = self.getDataSize()
|
||||
|
||||
if data == 0x2ad7b1:
|
||||
timecode = 0
|
||||
|
||||
try:
|
||||
for x in range(datasize):
|
||||
timecode = (timecode << 8) + struct.unpack('B', self.getData(1))[0]
|
||||
except:
|
||||
timecode = 0
|
||||
|
||||
if duration != 0 and timecode != 0:
|
||||
break
|
||||
|
||||
elif data == 0x4489:
|
||||
try:
|
||||
if datasize == 4:
|
||||
duration = int(struct.unpack('>f', self.getData(datasize))[0])
|
||||
else:
|
||||
duration = int(struct.unpack('>d', self.getData(datasize))[0])
|
||||
except:
|
||||
log("MKVParser: Error getting duration in header, size is " + str(datasize))
|
||||
duration = 0
|
||||
|
||||
if timecode != 0 and duration != 0:
|
||||
break
|
||||
else:
|
||||
try:
|
||||
self.File.seek(datasize, 1)
|
||||
except:
|
||||
log('MKVParser: Error while seeking')
|
||||
return 0
|
||||
|
||||
if duration > 0 and timecode > 0:
|
||||
dur = (duration * timecode) / 1000000000
|
||||
return dur
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def findHeader(self):
|
||||
log("MKVParser: findHeader")
|
||||
filesize = self.getFileSize()
|
||||
if filesize == 0:
|
||||
log("MKVParser: Empty file")
|
||||
return 0
|
||||
|
||||
data = self.getEBMLId()
|
||||
|
||||
# Check for 1A 45 DF A3
|
||||
if data != 0x1A45DFA3:
|
||||
log("MKVParser: Not a proper MKV")
|
||||
return 0
|
||||
|
||||
datasize = self.getDataSize()
|
||||
|
||||
try:
|
||||
self.File.seek(datasize, 1)
|
||||
except:
|
||||
log('MKVParser: Error while seeking')
|
||||
return 0
|
||||
|
||||
data = self.getEBMLId()
|
||||
|
||||
# Look for the segment header
|
||||
while not self.monitor.abortRequested() and data != 0x18538067 and self.File.tell() < filesize and data > 0 and datasize > 0:
|
||||
datasize = self.getDataSize()
|
||||
|
||||
try:
|
||||
self.File.seek(datasize, 1)
|
||||
except:
|
||||
log('MKVParser: Error while seeking')
|
||||
return 0
|
||||
|
||||
data = self.getEBMLId()
|
||||
|
||||
datasize = self.getDataSize()
|
||||
data = self.getEBMLId()
|
||||
|
||||
# Find segment info
|
||||
while not self.monitor.abortRequested() and data != 0x1549A966 and self.File.tell() < filesize and data > 0 and datasize > 0:
|
||||
datasize = self.getDataSize()
|
||||
|
||||
try:
|
||||
self.File.seek(datasize, 1)
|
||||
except:
|
||||
log('MKVParser: Error while seeking')
|
||||
return 0
|
||||
|
||||
data = self.getEBMLId()
|
||||
|
||||
datasize = self.getDataSize()
|
||||
|
||||
if self.File.tell() < filesize:
|
||||
return datasize
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def getFileSize(self):
|
||||
size = 0
|
||||
try:
|
||||
pos = self.File.tell()
|
||||
self.File.seek(0, 2)
|
||||
size = self.File.tell()
|
||||
self.File.seek(pos, 0)
|
||||
except:
|
||||
pass
|
||||
return size
|
||||
|
||||
|
||||
def getData(self, datasize):
|
||||
data = self.File.readBytes(datasize)
|
||||
return data
|
||||
|
||||
|
||||
def getDataSize(self):
|
||||
data = self.File.readBytes(1)
|
||||
|
||||
try:
|
||||
firstbyte = struct.unpack('>B', data)[0]
|
||||
datasize = firstbyte
|
||||
mask = 0xFFFF
|
||||
|
||||
for i in range(8):
|
||||
if datasize >> (7 - i) == 1:
|
||||
mask = mask ^ (1 << (7 - i))
|
||||
break
|
||||
|
||||
datasize = datasize & mask
|
||||
|
||||
if firstbyte >> 7 != 1:
|
||||
for i in range(1, 8):
|
||||
datasize = (datasize << 8) + struct.unpack('>B', self.File.readBytes(1))[0]
|
||||
|
||||
if firstbyte >> (7 - i) == 1:
|
||||
break
|
||||
except:
|
||||
datasize = 0
|
||||
|
||||
return datasize
|
||||
|
||||
|
||||
def getEBMLId(self):
|
||||
data = self.File.readBytes(1)
|
||||
try:
|
||||
firstbyte = struct.unpack('>B', data)[0]
|
||||
ID = firstbyte
|
||||
|
||||
if firstbyte >> 7 != 1:
|
||||
for i in range(1, 4):
|
||||
ID = (ID << 8) + struct.unpack('>B', self.File.readBytes(1))[0]
|
||||
|
||||
if firstbyte >> (7 - i) == 1:
|
||||
break
|
||||
except:
|
||||
ID = 0
|
||||
return ID
|
||||
@@ -0,0 +1,185 @@
|
||||
# Copyright (C) 2024 Jason Anderson, 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/>.
|
||||
|
||||
from globals import *
|
||||
|
||||
class MP4DataBlock:
|
||||
def __init__(self):
|
||||
self.size = -1
|
||||
self.boxtype = ''
|
||||
self.data = ''
|
||||
|
||||
|
||||
class MP4MovieHeader:
|
||||
def __init__(self):
|
||||
self.version = 0
|
||||
self.flags = 0
|
||||
self.created = 0
|
||||
self.modified = 0
|
||||
self.scale = 0
|
||||
self.duration = 0
|
||||
|
||||
class MP4Parser:
|
||||
def __init__(self):
|
||||
self.MovieHeader = MP4MovieHeader()
|
||||
self.monitor = MONITOR()
|
||||
|
||||
|
||||
def determineLength(self, filename: str) -> int and float:
|
||||
log("MP4Parser: determineLength %s"%filename)
|
||||
try: self.File = FileAccess.open(filename, "rb", None)
|
||||
except:
|
||||
log("MP4Parser: Unable to open the file")
|
||||
return 0
|
||||
|
||||
dur = self.readHeader()
|
||||
if not dur:
|
||||
log("MP4Parser - Using New Parser")
|
||||
boxes = self.find_boxes(self.File)
|
||||
# Sanity check that this really is a movie file.
|
||||
if (boxes.get(b"ftyp",[-1])[0] == 0):
|
||||
try:
|
||||
moov_boxes = self.find_boxes(self.File, boxes[b"moov"][0] + 8, boxes[b"moov"][1])
|
||||
trak_boxes = self.find_boxes(self.File, moov_boxes[b"trak"][0] + 8, moov_boxes[b"trak"][1])
|
||||
udta_boxes = self.find_boxes(self.File, moov_boxes[b"udta"][0] + 8, moov_boxes[b"udta"][1])
|
||||
dur = self.scan_mvhd(self.File, moov_boxes[b"mvhd"][0])
|
||||
except Exception as e:
|
||||
log("MP4Parser, failed! %s\nboxes = %s"%(e,boxes), xbmc.LOGERROR)
|
||||
dur = 0
|
||||
self.File.close()
|
||||
log("MP4Parser: Duration is %s"%(dur))
|
||||
return dur
|
||||
|
||||
|
||||
def find_boxes(self, f, start_offset=0, end_offset=float("inf")):
|
||||
"""Returns a dictionary of all the data boxes and their absolute starting
|
||||
and ending offsets inside the mp4 file. Specify a start_offset and end_offset to read sub-boxes."""
|
||||
s = struct.Struct("> I 4s")
|
||||
boxes = {}
|
||||
offset = start_offset
|
||||
last_offset = -1
|
||||
f.seek(offset, 0)
|
||||
while not self.monitor.abortRequested() and offset < end_offset:
|
||||
try:
|
||||
if last_offset == offset: break
|
||||
else: last_offset = offset
|
||||
data = f.readBytes(8) # read box header
|
||||
if data == b"": break # EOF
|
||||
length, text = s.unpack(data)
|
||||
f.seek(length - 8, 1) # skip to next box
|
||||
boxes[text] = (offset, offset + length)
|
||||
offset += length
|
||||
except: pass
|
||||
return boxes
|
||||
|
||||
|
||||
def scan_mvhd(self, f, offset):
|
||||
f.seek(offset, 0)
|
||||
f.seek(8, 1) # skip box header
|
||||
data = f.readBytes(1) # read version number
|
||||
version = int.from_bytes(data, "big")
|
||||
word_size = 8 if version == 1 else 4
|
||||
f.seek(3, 1) # skip flags
|
||||
f.seek(word_size * 2, 1) # skip dates
|
||||
timescale = int.from_bytes(f.readBytes(4), "big")
|
||||
if timescale == 0: timescale = 600
|
||||
duration = int.from_bytes(f.readBytes(word_size), "big")
|
||||
duration = round(duration / timescale)
|
||||
return duration
|
||||
|
||||
|
||||
def readHeader(self):
|
||||
data = self.readBlock()
|
||||
|
||||
if data.boxtype != 'ftyp':
|
||||
log("MP4Parser: No file block")
|
||||
return 0
|
||||
|
||||
# Skip past the file header
|
||||
try:
|
||||
self.File.seek(data.size, 1)
|
||||
except:
|
||||
log('MP4Parser: Error while seeking')
|
||||
return 0
|
||||
|
||||
data = self.readBlock()
|
||||
|
||||
while not self.monitor.abortRequested() and data.boxtype != 'moov' and data.size > 0:
|
||||
try: self.File.seek(data.size, 1)
|
||||
except:
|
||||
log('MP4Parser: Error while seeking')
|
||||
return 0
|
||||
|
||||
data = self.readBlock()
|
||||
|
||||
data = self.readBlock()
|
||||
|
||||
while not self.monitor.abortRequested() and data.boxtype != 'mvhd' and data.size > 0:
|
||||
try: self.File.seek(data.size, 1)
|
||||
except:
|
||||
log('MP4Parser: Error while seeking')
|
||||
return 0
|
||||
|
||||
data = self.readBlock()
|
||||
|
||||
self.readMovieHeader()
|
||||
|
||||
if self.MovieHeader.scale > 0 and self.MovieHeader.duration > 0:
|
||||
return int(self.MovieHeader.duration / self.MovieHeader.scale)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def readMovieHeader(self):
|
||||
try:
|
||||
self.MovieHeader.version = struct.unpack('>b', self.File.readBytes(1))[0]
|
||||
self.File.read(3) #skip flags for now
|
||||
|
||||
if self.MovieHeader.version == 1:
|
||||
data = struct.unpack('>QQIQQ', self.File.readBytes(36))
|
||||
else:
|
||||
data = struct.unpack('>IIIII', self.File.readBytes(20))
|
||||
|
||||
self.MovieHeader.created = data[0]
|
||||
self.MovieHeader.modified = data[1]
|
||||
self.MovieHeader.scale = data[2]
|
||||
self.MovieHeader.duration = data[3]
|
||||
except:
|
||||
self.MovieHeader.duration = 0
|
||||
|
||||
|
||||
def readBlock(self):
|
||||
box = MP4DataBlock()
|
||||
|
||||
try:
|
||||
data = self.File.readBytes(4)
|
||||
box.size = struct.unpack('>I', data)[0]
|
||||
box.boxtype = self.File.read(4)
|
||||
|
||||
if box.size == 1:
|
||||
box.size = struct.unpack('>q', self.File.readBytes(8))[0]
|
||||
box.size -= 8
|
||||
box.size -= 8
|
||||
|
||||
if box.boxtype == 'uuid':
|
||||
box.boxtype = self.File.read(16)
|
||||
box.size -= 16
|
||||
except:
|
||||
pass
|
||||
|
||||
return box
|
||||
@@ -0,0 +1,50 @@
|
||||
# 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/>.
|
||||
|
||||
from globals import *
|
||||
|
||||
class MediaInfo:
|
||||
def determineLength(self, filename: str) -> int and float:
|
||||
try:
|
||||
from pymediainfo import MediaInfo
|
||||
dur = 0
|
||||
mi = None
|
||||
fileXML = filename.replace('.%s'%(filename.rsplit('.',1)[1]),'-mediainfo.xml')
|
||||
if FileAccess.exists(fileXML):
|
||||
log("MediaInfo: parsing XML %s"%(fileXML))
|
||||
fle = FileAccess.open(fileXML, 'rb')
|
||||
mi = MediaInfo(fle.read())
|
||||
fle.close()
|
||||
else:
|
||||
log("MediaInfo: parsing %s"%(FileAccess.translatePath(filename)))
|
||||
mi = MediaInfo.parse(FileAccess.translatePath(filename))
|
||||
|
||||
if not mi is None and mi.tracks:
|
||||
for track in mi.tracks:
|
||||
if track.track_type == 'General':
|
||||
dur = track.duration / 1000
|
||||
break
|
||||
|
||||
log("MediaInfo: determineLength %s Duration is %s"%(filename,dur))
|
||||
return dur
|
||||
except Exception as e:
|
||||
log("MediaInfo: failed! %s"%(e), xbmc.LOGERROR)
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# 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/>.
|
||||
|
||||
from globals import *
|
||||
|
||||
class MoviePY:
|
||||
def determineLength(self, filename: str) -> int and float:
|
||||
try:
|
||||
from moviepy.editor import VideoFileClip
|
||||
log("MoviePY: determineLength %s"%(filename))
|
||||
dur = VideoFileClip(FileAccess.translatePath(filename)).duration
|
||||
log('MoviePY: Duration is %s'%(dur))
|
||||
return dur
|
||||
except Exception as e:
|
||||
log("MoviePY: failed! %s"%(e), xbmc.LOGERROR)
|
||||
return 0
|
||||
@@ -0,0 +1,73 @@
|
||||
# Copyright (C) 2024 Lunatixz
|
||||
#
|
||||
#
|
||||
# This file is part of PseudoTV Live.
|
||||
#
|
||||
# PseudoTV Live 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 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 Live. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from globals import *
|
||||
|
||||
class NFOParser:
|
||||
## NFO EXAMPLE ##
|
||||
##<episodedetails>
|
||||
## <runtime>25</runtime>
|
||||
## <duration>1575</duration>
|
||||
## <fileinfo>
|
||||
## <streamdetails>
|
||||
## <video>
|
||||
## <durationinseconds>1575</durationinseconds>
|
||||
## </video>
|
||||
## </streamdetails>
|
||||
## </fileinfo>
|
||||
##</episodedetails>
|
||||
|
||||
def determineLength(self, filename: str) -> int and float:
|
||||
duration = 0
|
||||
fleName, fleExt = os.path.splitext(filename)
|
||||
fleName += '.nfo'
|
||||
if not FileAccess.exists(fleName):
|
||||
log("NFOParser: Unable to locate NFO %s"%(fleName), xbmc.LOGERROR)
|
||||
return 0
|
||||
|
||||
log("NFOParser: determineLength, file = %s, nfo = %s"%(filename,fleName))
|
||||
try:
|
||||
File = FileAccess.open(fleName, "rb")
|
||||
dom = parse(File)
|
||||
File.close()
|
||||
except:
|
||||
log("NFOParser: Unable to open the file %s"%(fleName), xbmc.LOGERROR)
|
||||
return duration
|
||||
|
||||
try:
|
||||
xmldurationinseconds = dom.getElementsByTagName('durationinseconds')[0].toxml()
|
||||
duration = int(xmldurationinseconds.replace('<durationinseconds>','').replace('</durationinseconds>',''))
|
||||
except Exception as e:
|
||||
log("NFOParser: <durationinseconds> not found")
|
||||
|
||||
if duration == 0:
|
||||
try:
|
||||
xmlruntime = dom.getElementsByTagName('runtime')[0].toxml()
|
||||
duration = int(xmlruntime.replace('<runtime>','').replace('</runtime>','').replace(' min.','')) * 60
|
||||
except Exception as e:
|
||||
log("NFOParser: <runtime> not found")
|
||||
|
||||
if duration == 0:
|
||||
try:
|
||||
xmlruntime = dom.getElementsByTagName('duration')[0].toxml()
|
||||
duration = int(xmlruntime.replace('<duration>','').replace('</duration>','')) * 60
|
||||
except Exception as e:
|
||||
log("NFOParser: <duration> not found")
|
||||
|
||||
log("NFOParser: Duration is %s"%(duration))
|
||||
return duration
|
||||
@@ -0,0 +1,31 @@
|
||||
# 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/>.
|
||||
|
||||
from globals import *
|
||||
|
||||
class OpenCV:
|
||||
def determineLength(self, filename: str) -> int and float:
|
||||
try:
|
||||
import cv2
|
||||
log("OpenCV: determineLength %s"%(filename))
|
||||
dur = cv2.VideoCapture(FileAccess.translatePath(filename)).get(cv2.CAP_PROP_POS_MSEC)
|
||||
log('OpenCV: Duration is %s'%(dur))
|
||||
return dur
|
||||
except Exception as e:
|
||||
log("OpenCV: failed! %s"%(e), xbmc.LOGERROR)
|
||||
return 0
|
||||
@@ -0,0 +1,247 @@
|
||||
# Copyright (C) 2024 Jason Anderson, 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/>.
|
||||
|
||||
from globals import *
|
||||
|
||||
class TSPacket:
|
||||
def __init__(self):
|
||||
self.pid = 0
|
||||
self.errorbit = 1
|
||||
self.pesstartbit = 0
|
||||
self.adaption = 1
|
||||
self.adaptiondata = ''
|
||||
self.pesdata = ''
|
||||
|
||||
|
||||
class TSParser:
|
||||
def __init__(self):
|
||||
self.monitor = MONITOR()
|
||||
|
||||
|
||||
def determineLength(self, filename: str) -> int and float:
|
||||
log("TSParser: determineLength %s"%filename)
|
||||
self.pid = -1
|
||||
|
||||
try: self.File = FileAccess.open(filename, "rb", None)
|
||||
except:
|
||||
log("TSParser: Unable to open the file")
|
||||
return 0
|
||||
|
||||
self.filesize = self.getFileSize()
|
||||
self.packetLength = self.findPacketLength()
|
||||
|
||||
if self.packetLength <= 0:
|
||||
return 0
|
||||
|
||||
start = self.getStartTime()
|
||||
log('TSParser: Start %s'%(start))
|
||||
end = self.getEndTime()
|
||||
log('TSParser: End - %s'%(end))
|
||||
|
||||
if end > start:
|
||||
dur = int((end - start) / 90000)
|
||||
else:
|
||||
dur = 0
|
||||
|
||||
self.File.close()
|
||||
log("TSParser: Duration is %s"%(dur))
|
||||
return dur
|
||||
|
||||
|
||||
def findPacketLength(self):
|
||||
log('TSParser: findPacketLength')
|
||||
maxbytes = 600
|
||||
start = 0
|
||||
self.packetLength = 0
|
||||
|
||||
|
||||
while not self.monitor.abortRequested() and maxbytes > 0:
|
||||
maxbytes -= 1
|
||||
|
||||
try:
|
||||
data = self.File.readBytes(1)
|
||||
data = struct.unpack('B', data)
|
||||
|
||||
if data[0] == 71:
|
||||
if start > 0:
|
||||
end = self.File.tell()
|
||||
break
|
||||
else:
|
||||
start = self.File.tell()
|
||||
# A minimum of 188, so skip the rest
|
||||
self.File.seek(187, 1)
|
||||
except:
|
||||
log('TSParser: Exception in findPacketLength')
|
||||
return
|
||||
|
||||
if (start > 0) and (end > start):
|
||||
log('TSParser: Packet Length: %s'%(end - start))
|
||||
return (end - start)
|
||||
|
||||
return
|
||||
|
||||
|
||||
def getFileSize(self):
|
||||
size = 0
|
||||
try:
|
||||
pos = self.File.tell()
|
||||
self.File.seek(0, 2)
|
||||
size = self.File.tell()
|
||||
self.File.seek(pos, 0)
|
||||
except:
|
||||
pass
|
||||
|
||||
return size
|
||||
|
||||
|
||||
def getStartTime(self):
|
||||
# A reasonably high number of retries in case the PES starts in the middle
|
||||
# and is it's maximum length
|
||||
maxpackets = 12000
|
||||
log('TSParser: getStartTime')
|
||||
|
||||
try:
|
||||
self.File.seek(0, 0)
|
||||
except:
|
||||
return 0
|
||||
|
||||
while not self.monitor.abortRequested() and maxpackets > 0:
|
||||
packet = self.readTSPacket()
|
||||
maxpackets -= 1
|
||||
|
||||
if packet == None:
|
||||
return 0
|
||||
|
||||
if packet.errorbit == 0 and packet.pesstartbit == 1:
|
||||
ret = self.getPTS(packet)
|
||||
|
||||
if ret > 0:
|
||||
self.pid = packet.pid
|
||||
log('TSParser: PID: %s'%(self.pid))
|
||||
return ret
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def getEndTime(self):
|
||||
log('TSParser: getEndTime')
|
||||
packetcount = int(self.filesize / self.packetLength)
|
||||
|
||||
try:
|
||||
self.File.seek((packetcount * self.packetLength)- self.packetLength, 0)
|
||||
except:
|
||||
return 0
|
||||
|
||||
maxpackets = 12000
|
||||
|
||||
while not self.monitor.abortRequested() and maxpackets > 0:
|
||||
packet = self.readTSPacket()
|
||||
maxpackets -= 1
|
||||
|
||||
if packet == None:
|
||||
log('TSParser: getEndTime got a null packet')
|
||||
return 0
|
||||
|
||||
if packet.errorbit == 0 and packet.pesstartbit == 1 and packet.pid == self.pid:
|
||||
ret = self.getPTS(packet)
|
||||
|
||||
if ret > 0:
|
||||
log('TSParser: getEndTime returning time')
|
||||
return ret
|
||||
else:
|
||||
try:
|
||||
self.File.seek(-1 * (self.packetLength * 2), 1)
|
||||
except:
|
||||
log('TSParser: exception')
|
||||
return 0
|
||||
|
||||
log('TSParser: getEndTime no found end time')
|
||||
return 0
|
||||
|
||||
|
||||
def getPTS(self, packet):
|
||||
timestamp = 0
|
||||
log('TSParser: getPTS')
|
||||
|
||||
try:
|
||||
data = struct.unpack('19B', packet.pesdata[:19])
|
||||
|
||||
# start code
|
||||
if data[0] == 0 and data[1] == 0 and data[2] == 1:
|
||||
# cant be a navigation packet
|
||||
if data[3] != 190 and data[3] != 191:
|
||||
offset = 0
|
||||
|
||||
if (data[9] >> 4) == 3:
|
||||
offset = 5
|
||||
|
||||
# a little dangerous...ignoring the LSB of the timestamp
|
||||
timestamp = ((data[9 + offset] >> 1) & 7) << 30
|
||||
timestamp = timestamp | (data[10 + offset] << 22)
|
||||
timestamp = timestamp | ((data[11 + offset] >> 1) << 15)
|
||||
timestamp = timestamp | (data[12 + offset] << 7)
|
||||
timestamp = timestamp | (data[13 + offset] >> 1)
|
||||
return timestamp
|
||||
except:
|
||||
log('TSParser: exception in getPTS')
|
||||
pass
|
||||
|
||||
log('TSParser: getPTS returning 0')
|
||||
return 0
|
||||
|
||||
|
||||
def readTSPacket(self):
|
||||
packet = TSPacket()
|
||||
pos = 0
|
||||
|
||||
try:
|
||||
data = self.File.readBytes(4)
|
||||
pos = 4
|
||||
data = struct.unpack('4B', data)
|
||||
|
||||
if data[0] == 71:
|
||||
packet.pid = (data[1] & 31) << 8
|
||||
packet.pid = packet.pid | data[2]
|
||||
|
||||
# skip tables and null packets
|
||||
if packet.pid < 21 or packet.pid == 8191:
|
||||
self.File.seek(self.packetLength - 4, 1)
|
||||
else:
|
||||
packet.adaption = (data[3] >> 4) & 3
|
||||
packet.errorbit = data[1] >> 7
|
||||
packet.pesstartbit = (data[1] >> 6) & 1
|
||||
|
||||
if packet.adaption > 1:
|
||||
data = self.File.readBytes(1)
|
||||
length = struct.unpack('B', data)[0]
|
||||
|
||||
if length > 0:
|
||||
data = self.File.readBytes(length)
|
||||
else:
|
||||
length = 0
|
||||
|
||||
pos += length + 1
|
||||
|
||||
if pos < 188:
|
||||
# read the PES data
|
||||
packet.pesdata = self.File.readBytes(self.packetLength - pos)
|
||||
except:
|
||||
log('TSParser: readTSPacket exception')
|
||||
return None
|
||||
|
||||
return packet
|
||||
@@ -0,0 +1,29 @@
|
||||
# Copyright (C) 2024 Lunatixz
|
||||
#
|
||||
#
|
||||
# This file is part of PseudoTV Live.
|
||||
#
|
||||
# PseudoTV Live 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 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 Live. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from globals import *
|
||||
|
||||
class VFSParser:
|
||||
def determineLength(self, filename: str, fileitem: dict={}, jsonRPC=None)-> int and float:
|
||||
log("VFSParser: determineLength, file = %s\nitem = %s"%(filename,fileitem))
|
||||
duration = (fileitem.get('resume',{}).get('total') or fileitem.get('runtime') or fileitem.get('duration') or (fileitem.get('streamdetails',{}).get('video',[]) or [{}])[0].get('duration') or 0)
|
||||
if duration == 0 and not filename.lower().startswith(fileitem.get('originalpath','').lower()) and not filename.lower().startswith(tuple(self.VFSPaths)):
|
||||
metadata = self.jsonRPC.getFileDetails((fileitem.get('originalpath') or fileitem.get('file') or filename))
|
||||
duration = (metadata.get('resume',{}).get('total') or metadata.get('runtime') or metadata.get('duration') or (metadata.get('streamdetails',{}).get('video',[]) or [{}])[0].get('duration') or 0)
|
||||
log("VFSParser: Duration is %s"%(duration))
|
||||
return duration
|
||||
@@ -0,0 +1,38 @@
|
||||
# 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/>.
|
||||
|
||||
from globals import *
|
||||
|
||||
class YTParser:
|
||||
def determineLength(self, filename: str) -> int and float:
|
||||
try:
|
||||
dur = 0
|
||||
if hasAddon('script.module.youtube.dl'):
|
||||
from youtube_dl import YoutubeDL
|
||||
if 'videoid' in filename: vID = (re.compile(r'videoid\=(.*)' , re.IGNORECASE).search(filename)).group(1)
|
||||
elif 'video_id' in filename: vID = (re.compile(r'video_id\=(.*)', re.IGNORECASE).search(filename)).group(1)
|
||||
else: raise Exception('No video_id found!')
|
||||
log("YTParser: determineLength, file = %s, id = %s"%(filename,vID))
|
||||
ydl = YoutubeDL({'no_color': True, 'format': 'best', 'outtmpl': '%(id)s.%(ext)s', 'no-mtime': True, 'add-header': HEADER})
|
||||
with ydl:
|
||||
dur = ydl.extract_info("https://www.youtube.com/watch?v={sID}".format(sID=vID), download=False).get('duration',0)
|
||||
log('YTParser: Duration is %s'%(dur))
|
||||
return dur
|
||||
except Exception as e:
|
||||
log("YTParser: failed! %s\nfile = %s"%(e,filename), xbmc.LOGERROR)
|
||||
return 0
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,385 @@
|
||||
# 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 jsonrpc import JSONRPC
|
||||
from rules import RulesList
|
||||
|
||||
|
||||
class Plugin:
|
||||
@contextmanager
|
||||
def preparingPlayback(self):
|
||||
if self.playCheck(loadJSON(decodeString(PROPERTIES.getEXTProperty('%s.lastPlayed.sysInfo'%(ADDON_ID))))):
|
||||
try: yield
|
||||
finally: PROPERTIES.setEXTProperty('%s.lastPlayed.sysInfo'%(ADDON_ID),encodeString(dumpJSON(self.sysInfo)))
|
||||
else: #todo evaluate potential for error handling.
|
||||
if REAL_SETTINGS.getSetting('Debug_Enable').lower() == 'true':
|
||||
yield self.playError()
|
||||
|
||||
|
||||
def __init__(self, sysARG=sys.argv, sysInfo={}):
|
||||
with BUILTIN.busy_dialog():
|
||||
self.sysARG = sysARG
|
||||
self.sysInfo = sysInfo
|
||||
self.jsonRPC = JSONRPC()
|
||||
self.cache = SETTINGS.cache
|
||||
|
||||
self.debugEnabled = SETTINGS.getSettingBool('Debug_Enable')
|
||||
|
||||
self.sysInfo['radio'] = sysInfo.get('mode','').lower() == "radio"
|
||||
self.sysInfo['now'] = int(sysInfo.get('now') or int(getUTCstamp()))
|
||||
self.sysInfo['start'] = int(sysInfo.get('start') or '-1')
|
||||
self.sysInfo['stop'] = int(sysInfo.get('stop') or '-1')
|
||||
self.sysInfo['citem'] = (sysInfo.get('citem') or combineDicts({'id':sysInfo.get("chid")},sysInfo.get('fitem',{}).get('citem',{})))
|
||||
|
||||
if sysInfo.get('fitem'):
|
||||
if sysInfo.get("nitem"): self.sysInfo.update({'citem':combineDicts(self.sysInfo["nitem"].pop('citem'),self.sysInfo["fitem"].pop('citem'))})
|
||||
else: self.sysInfo.update({'citem':combineDicts(self.sysInfo["citem"],self.sysInfo["fitem"].pop('citem'))})
|
||||
|
||||
if self.sysInfo.get('start') == -1:
|
||||
self.sysInfo['start'] = self.sysInfo['fitem'].get('start')
|
||||
self.sysInfo['stop'] = self.sysInfo['fitem'].get('stop')
|
||||
|
||||
self.sysInfo['duration'] = float(sysInfo.get('duration') or self.jsonRPC._getRuntime(self.sysInfo['fitem']) or timeString2Seconds(BUILTIN.getInfoLabel('Duration(hh:mm:ss)')))
|
||||
else:
|
||||
self.sysInfo['duration'] = float((sysInfo.get('duration') or '-1'))
|
||||
|
||||
try:
|
||||
self.sysInfo['seek'] = int(sysInfo.get('seek') or (abs(self.sysInfo['start'] - self.sysInfo['now']) if self.sysInfo['start'] > 0 else -1))
|
||||
self.sysInfo["progresspercentage"] = -1 if self.sysInfo['seek'] == -1 else (self.sysInfo["seek"]/self.sysInfo["duration"]) * 100
|
||||
except:
|
||||
self.sysInfo['seek'] = int(sysInfo.get('seek','-1'))
|
||||
self.sysInfo["progresspercentage"] = -1
|
||||
|
||||
self.log('__init__, sysARG = %s\nsysInfo = %s'%(sysARG,self.sysInfo))
|
||||
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
return log('%s: %s'%(self.__class__.__name__,msg),level)
|
||||
|
||||
|
||||
def _resolveURL(self, found, listitem):
|
||||
xbmcplugin.setResolvedUrl(int(self.sysARG[1]), found, listitem)
|
||||
|
||||
|
||||
def _setResume(self, chid, liz):
|
||||
if self.sysInfo.get('seek',0) > SETTINGS.getSettingInt('Seek_Tolerance') and self.sysInfo.get('progresspercentage',100) < 100:
|
||||
self.log('[%s] _setResume, seek = %s, progresspercentage = %s\npath = %s'%(chid, self.sysInfo.get('seek',0), self.sysInfo.get('progresspercentage',100), liz.getPath()))
|
||||
liz.setProperty('startoffset', str(self.sysInfo['seek'])) #secs
|
||||
infoTag = ListItemInfoTag(liz,'video')
|
||||
infoTag.set_resume_point({'ResumeTime':self.sysInfo['seek'],'TotalTime':(self.sysInfo['duration'] * 60)})
|
||||
else:
|
||||
self.sysInfo["seek"] = -1
|
||||
self.sysInfo["progresspercentage"] = -1
|
||||
return liz
|
||||
|
||||
|
||||
def _quePlaylist(self, chid, listitems, pltype=xbmc.PLAYLIST_VIDEO, shuffle=BUILTIN.isPlaylistRandom()):
|
||||
self.log('[%s] _quePlaylist, listitems = %s, shuffle = %s'%(chid, len(listitems),shuffle))
|
||||
channelPlaylist = xbmc.PlayList(pltype)
|
||||
channelPlaylist.clear()
|
||||
xbmc.sleep(100) #give channelPlaylist.clear() enough time to clear queue.
|
||||
[channelPlaylist.add(liz.getPath(),liz,idx) for idx,liz in enumerate(listitems) if liz.getPath()]
|
||||
self.log('[%s] _quePlaylist, Playlist size = %s, shuffle = %s'%(chid, channelPlaylist.size(),shuffle))
|
||||
if shuffle: channelPlaylist.shuffle()
|
||||
else: channelPlaylist.unshuffle()
|
||||
return channelPlaylist
|
||||
|
||||
|
||||
def getRadioItems(self, name, chid, vid, limit=RADIO_ITEM_LIMIT):
|
||||
self.log('[%s] getRadioItems'%(chid))
|
||||
return interleave([self.jsonRPC.requestList({'id':chid}, path, 'music', page=limit, sort={"method":"random"})[0] for path in vid.split('|')], SETTINGS.getSettingInt('Interleave_Value'))
|
||||
|
||||
|
||||
def getPausedItems(self, name, chid):
|
||||
self.log('[%s] getPausedItems'%(chid))
|
||||
def __buildfItem(idx, item):
|
||||
if 'citem' in item: item.pop('citem')
|
||||
sysInfo = self.sysInfo.copy()
|
||||
sysInfo['isPlaylist'] = True
|
||||
liz = LISTITEMS.buildItemListItem(item,'video')
|
||||
|
||||
if item.get('file') == item.get('resume',{}).get('file',str(random.random())):
|
||||
seektime = int(item.get('resume',{}).get('position',0.0))
|
||||
runtime = int(item.get('resume',{}).get('total',0.0))
|
||||
self.log('[%s] getPausedItems, within seek tolerance setting seek totaltime = %s, resumetime = %s'%(chid, runtime, seektime))
|
||||
liz.setProperty('startoffset', str(seektime)) #secs
|
||||
infoTag = ListItemInfoTag(liz, 'video')
|
||||
infoTag.set_resume_point({'ResumeTime':seektime, 'TotalTime':runtime * 60})
|
||||
|
||||
sysInfo.update({'fitem':item,'resume':{"idx":idx}})
|
||||
liz.setProperty('sysInfo',encodeString(dumpJSON(sysInfo)))
|
||||
return liz
|
||||
|
||||
nextitems = RulesList([self.sysInfo.get('citem',{'name':name,'id':chid})]).runActions(RULES_ACTION_PLAYBACK_RESUME, self.sysInfo.get('citem',{'name':name,'id':chid}))
|
||||
if nextitems:
|
||||
nextitems = nextitems[:SETTINGS.getSettingInt('Page_Limit')]# list of upcoming items, truncate for speed
|
||||
self.log('[%s] getPausedItems, building nextitems (%s)'%(chid, len(nextitems)))
|
||||
return [__buildfItem(idx, nextitem) for idx, nextitem in enumerate(nextitems)]
|
||||
else: DIALOG.notificationDialog(LANGUAGE(32000))
|
||||
return []
|
||||
|
||||
|
||||
def getPVRItems(self, name: str, chid: str) -> list:
|
||||
self.log('[%s] getPVRItems, chname = %s'%(chid,name))
|
||||
def __buildfItem(idx, item):
|
||||
sysInfo = self.sysInfo.copy()
|
||||
nowitem = decodePlot(item.get('plot',''))
|
||||
if 'citem' in nowitem: nowitem.pop('citem')
|
||||
nowitem['pvritem'] = item
|
||||
sysInfo.update({'fitem':nowitem,'position':idx})
|
||||
|
||||
try: #next broadcast
|
||||
nextitem = decodePlot(nextitems[idx+1][1].get('plot',''))
|
||||
if 'citem' in nextitem: nextitem.pop('citem')
|
||||
nextitem.get('customproperties',{})['pvritem'] = nextitems[idx + 1]
|
||||
sysInfo.update({'nitem':nextitem})
|
||||
except: pass
|
||||
|
||||
liz = LISTITEMS.buildItemListItem(nowitem,'video')
|
||||
if (item.get('progress',0) > 0 and item.get('runtime',0) > 0):
|
||||
self.log('[%s] getPVRItems, within seek tolerance setting seek totaltime = %s, resumetime = %s'%(chid,(item['runtime'] * 60),item['progress']))
|
||||
liz.setProperty('startoffset', str(item['progress'])) #secs
|
||||
infoTag = ListItemInfoTag(liz, 'video')
|
||||
infoTag.set_resume_point({'ResumeTime':item['progress'],'TotalTime':(item['runtime'] * 60)})
|
||||
liz.setProperty('sysInfo',encodeString(dumpJSON(sysInfo)))
|
||||
return liz
|
||||
|
||||
found = False
|
||||
pvritem = self.jsonRPC.matchChannel(name,chid,radio=False)
|
||||
if pvritem:
|
||||
pastItems = pvritem.get('broadcastpast',[])
|
||||
nowitem = pvritem.get('broadcastnow',{})
|
||||
nextitems = pvritem.get('broadcastnext',[]) # upcoming items
|
||||
nextitems.insert(0,nowitem)
|
||||
nextitems = pastItems + nextitems
|
||||
|
||||
if (self.sysInfo.get('fitem') or self.sysInfo.get('vid')):
|
||||
for pos, nextitem in enumerate(nextitems):
|
||||
fitem = decodePlot(nextitem.get('plot',{}))
|
||||
file = self.sysInfo.get('fitem',{}).get('file') if self.sysInfo.get('fitem') else self.sysInfo.get('vid')
|
||||
if file == fitem.get('file') and self.sysInfo.get('citem',{}).get('id') == fitem.get('citem',{}).get('id',str(random.random())):
|
||||
found = True
|
||||
self.log('[%s] getPVRItems found matching fitem'%(chid))
|
||||
del nextitems[0:pos] # start array at correct position
|
||||
break
|
||||
|
||||
elif self.sysInfo.get('now') and self.sysInfo.get('vid'):
|
||||
for pos, nextitem in enumerate(nextitems):
|
||||
fitem = decodePlot(nextitem.get('plot',{}))
|
||||
ntime = datetime.datetime.fromtimestamp(float(self.sysInfo.get('now')))
|
||||
if ntime >= strpTime(nextitem.get('starttime')) and ntime < strpTime(nextitem.get('endtime')) and chid == fitem.get('citem',{}).get('id',str(random.random())):
|
||||
found = True
|
||||
self.log('[%s] getPVRItems found matching starttime'%(chid))
|
||||
del nextitems[0:pos] # start array at correct position
|
||||
break
|
||||
|
||||
elif nowitem: found = True
|
||||
|
||||
if found:
|
||||
nowitem = nextitems.pop(0)
|
||||
if round(nowitem['progresspercentage']) > SETTINGS.getSettingInt('Seek_Threshold'):
|
||||
self.log('[%s] getPVRItems, progress past threshold advance to nextitem'%(chid))
|
||||
nowitem = nextitems.pop(0)
|
||||
|
||||
if round(nowitem['progress']) < SETTINGS.getSettingInt('Seek_Tolerance'):
|
||||
self.log('[%s] getPVRItems, progress start at the beginning'%(chid))
|
||||
nowitem['progress'] = 0
|
||||
nowitem['progresspercentage'] = 0
|
||||
|
||||
self.sysInfo.update({'citem':decodePlot(nowitem.get('plot','')).get('citem',self.sysInfo.get('citem'))})
|
||||
self.sysInfo['callback'] = self.jsonRPC.getCallback(self.sysInfo)
|
||||
nextitems = nextitems[:SETTINGS.getSettingInt('Page_Limit')]# list of upcoming items, truncate for speed
|
||||
nextitems.insert(0,nowitem)
|
||||
self.log('[%s] getPVRItems, building nextitems (%s)'%(chid,len(nextitems)))
|
||||
return [__buildfItem(idx, item) for idx, item in enumerate(nextitems)]
|
||||
else: DIALOG.notificationDialog(LANGUAGE(32164))
|
||||
else: DIALOG.notificationDialog(LANGUAGE(32000))
|
||||
return [xbmcgui.ListItem()]
|
||||
|
||||
|
||||
def playTV(self, name: str, chid: str):
|
||||
self.log('[%s] playTV'%(chid))
|
||||
with self.preparingPlayback(), PROPERTIES.suspendActivity():
|
||||
if self.sysInfo.get('fitem') and (self.sysInfo.get('fitem').get('file','-1') == self.sysInfo.get('vid','0')): #-> live
|
||||
liz = self._setResume(chid, LISTITEMS.buildItemListItem(self.sysInfo['fitem']))
|
||||
else:
|
||||
liz = self.getPVRItems(name, chid)[0]
|
||||
liz.setProperty('sysInfo',encodeString(dumpJSON(self.sysInfo)))
|
||||
self._resolveURL(True, liz)
|
||||
|
||||
|
||||
def playLive(self, name: str, chid: str, vid: str):
|
||||
self.log('[%s] playLive, name = %s'%(chid, name))
|
||||
with self.preparingPlayback(), PROPERTIES.suspendActivity():
|
||||
if self.sysInfo.get('fitem').get('file','-1') == self.sysInfo.get('vid','0'):#-> live playback from UI incl. listitem
|
||||
liz = self._setResume(chid, LISTITEMS.buildItemListItem(self.sysInfo['fitem']))
|
||||
liz.setProperty('sysInfo',encodeString(dumpJSON(self.sysInfo)))
|
||||
self._resolveURL(True, liz)
|
||||
elif self.sysInfo.get('fitem') and self.sysInfo.get('start') <= self.sysInfo.get('now') <= self.sysInfo.get('stop'):#-> VOD called by non-current EPG cell. (Unreliable during playback)
|
||||
self.sysInfo['mode'] = 'vod'
|
||||
self.sysInfo['name'] = self.sysInfo['fitem'].get('label')
|
||||
self.sysInfo['vid'] = self.sysInfo['fitem'].get('file')
|
||||
self.sysInfo["seek"] = -1
|
||||
self.sysInfo["progresspercentage"] = -1
|
||||
self.log('[%s] playLive, VOD = %s'%(chid, self.sysInfo['vid']))
|
||||
DIALOG.notificationDialog(LANGUAGE(32185)%(self.sysInfo['name']))
|
||||
liz = LISTITEMS.buildItemListItem(self.sysInfo.get('fitem'))
|
||||
liz.setProperty("IsPlayable","true")
|
||||
liz.setProperty('sysInfo',encodeString(dumpJSON(self.sysInfo)))
|
||||
timerit(PLAYER().play)(1.0,[self.sysInfo['vid'],liz,True])
|
||||
self._resolveURL(False, liz)
|
||||
elif vid:#-> onChange callback from "live" or widget or channel switch (change via input not ui)
|
||||
self.log('[%s] playLive, VID = %s'%(chid, vid))
|
||||
liz = self._setResume(chid, xbmcgui.ListItem(name,path=vid))
|
||||
liz.setProperty("IsPlayable","true")
|
||||
liz.setProperty('sysInfo',encodeString(dumpJSON(self.sysInfo)))
|
||||
self._resolveURL(True, liz)
|
||||
else:
|
||||
self.playTV(name, chid)
|
||||
|
||||
|
||||
def playBroadcast(self, name: str, chid: str, vid: str): #-> catchup-source
|
||||
self.log('[%s] playBroadcast'%(chid))
|
||||
with self.preparingPlayback(), PROPERTIES.suspendActivity():
|
||||
if self.sysInfo.get('fitem'): #-> catchup-id called via ui "play programme"
|
||||
liz = LISTITEMS.buildItemListItem(self.sysInfo.get('fitem'))
|
||||
else:
|
||||
liz = xbmcgui.ListItem(name,path=vid)
|
||||
liz.setProperty("IsPlayable","true")
|
||||
self.sysInfo["seek"] = -1
|
||||
self.sysInfo["progresspercentage"] = -1
|
||||
liz.setProperty('sysInfo',encodeString(dumpJSON(self.sysInfo)))
|
||||
self._resolveURL(True, liz)
|
||||
|
||||
|
||||
def playVOD(self, title: str, vid: str): #-> catchup-id
|
||||
self.log('[%s] playVOD, title = %s'%(vid,title))
|
||||
with self.preparingPlayback(), PROPERTIES.suspendActivity():
|
||||
liz = LISTITEMS.buildItemListItem(self.sysInfo.get('fitem'))
|
||||
liz.setProperty("IsPlayable","true")
|
||||
liz.setProperty('sysInfo',encodeString(dumpJSON(self.sysInfo)))
|
||||
self._resolveURL(True, liz)
|
||||
|
||||
|
||||
def playDVR(self, title: str, vid: str): #-> catchup-id
|
||||
self.log('[%s] playDVR, title = %s'%(vid, title))
|
||||
with self.preparingPlayback(), PROPERTIES.suspendActivity():
|
||||
liz = self._setResume(vid, LISTITEMS.buildItemListItem(self.sysInfo.get('fitem')))
|
||||
liz.setProperty('sysInfo',encodeString(dumpJSON(self.sysInfo)))
|
||||
self._resolveURL(True, liz)
|
||||
|
||||
|
||||
def playRadio(self, name: str, chid: str, vid: str):
|
||||
self.log('[%s] playRadio'%(chid))
|
||||
def __buildfItem(idx, item: dict={}):
|
||||
return LISTITEMS.buildItemListItem(item, 'music')
|
||||
with self.preparingPlayback(), PROPERTIES.suspendActivity():
|
||||
items = randomShuffle(self.getRadioItems(name, chid, vid))
|
||||
listitems = [__buildfItem(idx, item) for idx, item in enumerate(items)]
|
||||
if len(listitems) > 0:
|
||||
playlist = self._quePlaylist(chid, listitems, pltype=xbmc.PLAYLIST_MUSIC, shuffle=True)
|
||||
timerit(BUILTIN.executeWindow)(OSD_TIMER,['ReplaceWindow(visualisation)'])
|
||||
BUILTIN.executebuiltin("Dialog.Close(all)")
|
||||
PLAYER().play(playlist,windowed=True)
|
||||
self._resolveURL(False, xbmcgui.ListItem())
|
||||
|
||||
|
||||
def playPlaylist(self, name: str, chid: str):
|
||||
self.log('[%s] playPlaylist'%(chid))
|
||||
with self.preparingPlayback(), PROPERTIES.suspendActivity():
|
||||
listitems = self.getPVRItems(name, chid)
|
||||
if len(listitems) > 0:
|
||||
playlist = self._quePlaylist(chid, listitems,shuffle=False)
|
||||
if BUILTIN.getInfoBool('Playing','Player'): BUILTIN.executebuiltin('PlayerControl(Stop)')
|
||||
timerit(BUILTIN.executeWindow)(OSD_TIMER,['ReplaceWindow(fullscreenvideo)'])
|
||||
BUILTIN.executebuiltin("Dialog.Close(all)")
|
||||
PLAYER().play(playlist,windowed=True)
|
||||
self._resolveURL(False, xbmcgui.ListItem())
|
||||
|
||||
|
||||
def playPaused(self, name: str, chid: str):
|
||||
self.log('[%s] playPaused'%(chid))
|
||||
with self.preparingPlayback(), PROPERTIES.suspendActivity():
|
||||
listitems = self.getPausedItems(name, chid)
|
||||
if len(listitems) > 0:
|
||||
playlist = self._quePlaylist(chid, listitems,shuffle=False)
|
||||
if BUILTIN.getInfoBool('Playing','Player'): BUILTIN.executebuiltin('PlayerControl(Stop)')
|
||||
timerit(BUILTIN.executeWindow)(OSD_TIMER,['ReplaceWindow(fullscreenvideo)'])
|
||||
BUILTIN.executebuiltin("Dialog.Close(all)")
|
||||
PLAYER().play(playlist,windowed=True)
|
||||
self._resolveURL(False, xbmcgui.ListItem())
|
||||
|
||||
|
||||
def playCheck(self, oldInfo: dict={}) -> bool:
|
||||
def _chkPath():
|
||||
status = True
|
||||
if oldInfo.get('isPlaylist') or not self.sysInfo.get('vid',''): status = True
|
||||
elif self.sysInfo.get('vid','').startswith(tuple(VFS_TYPES)): status = hasAddon(self.sysInfo['vid'])
|
||||
elif not self.sysInfo.get('vid','').startswith(tuple(WEB_TYPES)): status = FileAccess.exists(self.sysInfo['vid'])
|
||||
self.log('[%s] playCheck _chkPath, valid = %s\npath %s'%(self.sysInfo.get('citem',{}).get('id'),status,self.sysInfo.get('vid')))
|
||||
if not status: DIALOG.notificationDialog(LANGUAGE(32167),show=self.debugEnabled)
|
||||
return status
|
||||
|
||||
def _chkLoop():
|
||||
if self.sysInfo.get('chid') == oldInfo.get('chid',random.random()):
|
||||
if self.sysInfo.get('start') == oldInfo.get('start',random.random()):
|
||||
self.sysInfo['playcount'] = oldInfo.get('playcount',0) + 1 #carry over playcount
|
||||
self.sysInfo['runtime'] = oldInfo.get('runtime',0) #carry over previous player runtime
|
||||
|
||||
if self.sysInfo['mode'] == 'live':
|
||||
if self.sysInfo['now'] >= self.sysInfo['stop']:
|
||||
self.log('[%s] playCheck _chkLoop, failed! Current time (%s) is past the contents stop time (%s).'%(self.sysInfo.get('citem',{}).get('id'),self.sysInfo['now'],self.sysInfo['stop']))
|
||||
DIALOG.notificationDialog("Current time (%s) is past the contents stop time (%s)."%(self.sysInfo['now'],self.sysInfo['stop']),show=self.debugEnabled)
|
||||
return False
|
||||
elif self.sysInfo['runtime'] > 0 and self.sysInfo['duration'] > self.sysInfo['runtime']:
|
||||
self.log('[%s] playCheck _chkLoop, failed! Duration error between player (%s) and pvr (%s).'%(self.sysInfo.get('citem',{}).get('id'),self.sysInfo['duration'],self.sysInfo['runtime']))
|
||||
DIALOG.notificationDialog("Duration error between player (%s) and pvr (%s)."%(self.sysInfo['runtime'],self.sysInfo['duration']),show=self.debugEnabled)
|
||||
return False
|
||||
elif self.sysInfo['seek'] >= oldInfo.get('runtime',self.sysInfo['duration']):
|
||||
self.log('[%s] playCheck _chkLoop, failed! Seeking to a position (%s) past media runtime (%s).'%(self.sysInfo.get('citem',{}).get('id'),self.sysInfo['seek'],oldInfo.get('runtime',self.sysInfo['duration'])))
|
||||
DIALOG.notificationDialog("Seeking to a position (%s) past media runtime (%s)."%(self.sysInfo['seek'],oldInfo.get('runtime',self.sysInfo['duration'])),show=self.debugEnabled)
|
||||
return False
|
||||
elif self.sysInfo['seek'] == oldInfo.get('seek',self.sysInfo['seek']):
|
||||
self.log('[%s] playCheck _chkLoop, failed! Seeking to same position.'%(self.sysInfo.get('citem',{}).get('id')))
|
||||
DIALOG.notificationDialog("Playback Failed: Seeking to same position",show=self.debugEnabled)
|
||||
return False
|
||||
return True
|
||||
|
||||
status = _chkPath()
|
||||
if status: status = _chkLoop()
|
||||
self.log('[%s] playCheck, status = %s\nsysInfo=%s\noldInfo = %s'%(self.sysInfo.get('citem',{}).get('id'),status, self.sysInfo,oldInfo))
|
||||
return True
|
||||
|
||||
|
||||
def playError(self):
|
||||
PROPERTIES.setEXTProperty('%s.lastPlayed.sysInfo'%(ADDON_ID),encodeString(dumpJSON(self.sysInfo)))
|
||||
self.log('[%s] playError, attempt = %s\n%s'%(self.sysInfo.get('chid','-1'),self.sysInfo.get('playcount'),self.sysInfo))
|
||||
self._resolveURL(False, xbmcgui.ListItem()) #release pending playback
|
||||
if self.sysInfo.get('playcount',0) == 0:
|
||||
DIALOG.notificationWait(LANGUAGE(32038)%(self.sysInfo.get('playcount',0)))
|
||||
timerit(BUILTIN.executebuiltin)(0.1,['AlarmClock(last,Number(0),.5,true,false)']) #last channel
|
||||
elif self.sysInfo.get('playcount',3) < 3:
|
||||
DIALOG.notificationWait(LANGUAGE(32038)%(self.sysInfo.get('playcount',3)))
|
||||
timerit(BUILTIN.executebuiltin)(0.1,['PlayMedia(%s%s)'%(self.sysARG[0],self.sysARG[2])]) #retry channel
|
||||
else:
|
||||
DIALOG.notificationDialog(LANGUAGE(32000))
|
||||
PROPERTIES.setPropTimer('chkPVRRefresh')
|
||||
timerit(DIALOG.okDialog)(0.1,[LANGUAGE(32134)%(ADDON_NAME)])
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
# 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 concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, TimeoutError
|
||||
from itertools import repeat, count
|
||||
from functools import partial, wraps, reduce, update_wrapper
|
||||
|
||||
try:
|
||||
import multiprocessing
|
||||
cpu_count = multiprocessing.cpu_count()
|
||||
ENABLE_POOL = False #True force disable multiproc. until monkeypatch/wrapper to fix pickling error.
|
||||
except:
|
||||
ENABLE_POOL = False
|
||||
cpu_count = os.cpu_count()
|
||||
|
||||
def wrapped_partial(func, *args, **kwargs):
|
||||
partial_func = partial(func, *args, **kwargs)
|
||||
update_wrapper(partial_func, func)
|
||||
return partial_func
|
||||
|
||||
def timeit(method):
|
||||
@wraps(method)
|
||||
def wrapper(*args, **kwargs):
|
||||
start_time = time.time()
|
||||
result = method(*args, **kwargs)
|
||||
end_time = time.time()
|
||||
if REAL_SETTINGS.getSetting('Debug_Enable').lower() == 'true':
|
||||
log('%s timeit => %.2f ms'%(method.__qualname__.replace('.',': '),(end_time-start_time)*1000))
|
||||
return result
|
||||
return wrapper
|
||||
|
||||
def killit(method):
|
||||
@wraps(method)
|
||||
def wrapper(wait=30, *args, **kwargs):
|
||||
class waiter(Thread):
|
||||
def __init__(self):
|
||||
Thread.__init__(self)
|
||||
self.result = None
|
||||
self.error = None
|
||||
def run(self):
|
||||
try: self.result = method(*args, **kwargs)
|
||||
except: self.error = sys.exc_info()[0]
|
||||
timer = waiter()
|
||||
timer.name = '%s.%s'%('killit',method.__qualname__.replace('.',': '))
|
||||
timer.daemon=True
|
||||
timer.start()
|
||||
try: timer.join(wait)
|
||||
except: pass
|
||||
log('%s, starting %s waiting (%s)'%(method.__qualname__.replace('.',': => -:'),timer.name,wait))
|
||||
if (timer.is_alive() or timer.error): log('%s, Timed out! Errors: %s'%(method.__qualname__.replace('.',': '),timer.error), xbmc.LOGERROR)
|
||||
return timer.result
|
||||
return wrapper
|
||||
|
||||
def poolit(method):
|
||||
@wraps(method)
|
||||
def wrapper(items=[], *args, **kwargs):
|
||||
try:
|
||||
pool = ExecutorPool()
|
||||
name = '%s.%s'%('poolit',method.__qualname__.replace('.',': '))
|
||||
log('%s, starting %s'%(method.__qualname__.replace('.',': '),name))
|
||||
results = pool.executors(method, items, *args, **kwargs)
|
||||
except Exception as e:
|
||||
log('poolit, failed! %s'%(e), xbmc.LOGERROR)
|
||||
results = pool.generator(method, items, *args, **kwargs)
|
||||
log('%s poolit => %s'%(pool.__class__.__name__, method.__qualname__.replace('.',': ')))
|
||||
return list([_f for _f in results if _f])
|
||||
return wrapper
|
||||
|
||||
def threadit(method):
|
||||
@wraps(method)
|
||||
def wrapper(*args, **kwargs):
|
||||
thread_name = 'threadit.%s'%(method.__qualname__.replace('.',': '))
|
||||
for thread in thread_enumerate():
|
||||
if thread.name == thread_name and thread.is_alive():
|
||||
if hasattr(thread, 'cancel'):
|
||||
thread.cancel()
|
||||
log('%s, canceling %s'%(method.__qualname__.replace('.',': '),thread_name))
|
||||
thread = Thread(None, method, None, args, kwargs)
|
||||
thread.name = thread_name
|
||||
thread.daemon=True
|
||||
thread.start()
|
||||
log('%s, starting %s'%(method.__qualname__.replace('.',': '),thread.name))
|
||||
return thread
|
||||
return wrapper
|
||||
|
||||
def timerit(method):
|
||||
@wraps(method)
|
||||
def wrapper(wait, *args, **kwargs):
|
||||
thread_name = 'timerit.%s'%(method.__qualname__.replace('.',': '))
|
||||
for timer in thread_enumerate():
|
||||
if timer.name == thread_name and timer.is_alive():
|
||||
if hasattr(timer, 'cancel'):
|
||||
timer.cancel()
|
||||
log('%s, canceling %s'%(method.__qualname__.replace('.',': '),thread_name))
|
||||
try:
|
||||
timer.join()
|
||||
log('%s, joining %s'%(method.__qualname__.replace('.',': '),thread_name))
|
||||
except: pass
|
||||
timer = Timer(float(wait), method, *args, **kwargs)
|
||||
timer.name = thread_name
|
||||
timer.start()
|
||||
log('%s, starting %s wait = %s'%(method.__qualname__.replace('.',': '),thread_name,wait))
|
||||
return timer
|
||||
return wrapper
|
||||
|
||||
def executeit(method):
|
||||
@wraps(method)
|
||||
def wrapper(*args, **kwargs):
|
||||
pool = ExecutorPool()
|
||||
log('%s executeit => %s'%(pool.__class__.__name__, method.__qualname__.replace('.',': ')))
|
||||
return pool.executor(method, None, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
class ExecutorPool:
|
||||
def __init__(self):
|
||||
self.CPUCount = cpu_count
|
||||
if ENABLE_POOL: self.pool = ProcessPoolExecutor
|
||||
else: self.pool = ThreadPoolExecutor
|
||||
self.log(f"__init__, multiprocessing = {ENABLE_POOL}, CORES = {self.CPUCount}, THREADS = {self._calculate_thread_count()}")
|
||||
|
||||
|
||||
def _calculate_thread_count(self):
|
||||
if ENABLE_POOL: return self.CPUCount
|
||||
else: return int(os.getenv('THREAD_COUNT', self.CPUCount * 2))
|
||||
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
return log('%s: %s'%(self.__class__.__name__,msg),level)
|
||||
|
||||
|
||||
def executor(self, func, timeout=None, *args, **kwargs):
|
||||
self.log("executor, func = %s, timeout = %s"%(func.__name__,timeout))
|
||||
with self.pool(self._calculate_thread_count()) as executor:
|
||||
try: return executor.submit(func, *args, **kwargs).result(timeout)
|
||||
except Exception as e: self.log("executor, func = %s failed! %s\nargs = %s, kwargs = %s"%(func.__name__,e,args,kwargs), xbmc.LOGERROR)
|
||||
|
||||
|
||||
def executors(self, func, items=[], *args, **kwargs):
|
||||
self.log("executors, func = %s, items = %s"%(func.__name__,len(items)))
|
||||
with self.pool(self._calculate_thread_count()) as executor:
|
||||
try: return executor.map(wrapped_partial(func, *args, **kwargs), items)
|
||||
except Exception as e: self.log("executors, func = %s, items = %s failed! %s\nargs = %s, kwargs = %s"%(func.__name__,len(items),e,args,kwargs), xbmc.LOGERROR)
|
||||
|
||||
|
||||
def generator(self, func, items=[], *args, **kwargs):
|
||||
self.log("generator, items = %s"%(len(items)))
|
||||
try: return [wrapped_partial(func, *args, **kwargs)(i) for i in items]
|
||||
except Exception as e: self.log("generator, func = %s, items = %s failed! %s\nargs = %s, kwargs = %s"%(func.__name__,len(items),e,args,kwargs), xbmc.LOGERROR)
|
||||
@@ -0,0 +1,119 @@
|
||||
# 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 *
|
||||
|
||||
class Predefined:
|
||||
def __init__(self):
|
||||
...
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
return log('%s: %s'%(self.__class__.__name__,msg),level)
|
||||
|
||||
|
||||
def getParams(self) -> dict:
|
||||
params = {}
|
||||
params["order"] = {"direction" :"ascending",
|
||||
"method" :"random",
|
||||
"ignorearticle" :True,
|
||||
"useartistsortname":True}
|
||||
return params.copy()
|
||||
|
||||
|
||||
def createRECOMMENDED(self, type: str) -> list:
|
||||
return []
|
||||
|
||||
|
||||
def createMixedRecent(self) -> list:
|
||||
param = self.getParams()
|
||||
param["order"]["method"] = "episode"
|
||||
return ['videodb://recentlyaddedepisodes/?xsp=%s'%(dumpJSON(param)),
|
||||
'videodb://recentlyaddedmovies/?xsp=%s'%(dumpJSON(self.getParams()))]
|
||||
|
||||
|
||||
def createMusicRecent(self) -> list:
|
||||
return ['musicdb://recentlyaddedalbums/?xsp=%s'%(dumpJSON(self.getParams()))]
|
||||
|
||||
|
||||
def createNetworkPlaylist(self, network: str, method: str='episode') -> list:
|
||||
param = self.getParams()
|
||||
param["type"] = "episodes"
|
||||
param["order"]["method"] = method
|
||||
param.setdefault("rules",{}).setdefault("and",[]).append({"field":"studio","operator":"contains","value":[quoteString(network)]})
|
||||
return ['videodb://tvshows/studios/-1/-1/-1/?xsp=%s'%(dumpJSON(param))]
|
||||
|
||||
|
||||
def createShowPlaylist(self, show: str, method: str='episode') -> list:
|
||||
param = self.getParams()
|
||||
param["type"] = "episodes"
|
||||
param["order"]["method"] = method
|
||||
try:
|
||||
match = re.compile(r'(.*) \((.*)\)', re.IGNORECASE).search(show)
|
||||
year, title = int(match.group(2)), match.group(1)
|
||||
param.setdefault("rules",{}).setdefault("and",[]).extend([{"field":"year","operator":"is","value":[year]},{"field":"tvshow","operator":"is","value":[quoteString(title)]}])
|
||||
except:
|
||||
param.setdefault("rules",{}).setdefault("and",[]).append({"field":"tvshow","operator":"is","value":[quoteString(show)]})
|
||||
return ['videodb://tvshows/titles/-1/-1/-1/?xsp=%s'%(dumpJSON(param))]
|
||||
|
||||
|
||||
def createTVGenrePlaylist(self, genre: str, method: str='episode') -> list:
|
||||
param = self.getParams()
|
||||
param["type"] = "episodes"
|
||||
param["order"]["method"] = method
|
||||
param.setdefault("rules",{}).setdefault("and",[]).append({"field":"genre","operator":"contains","value":[quoteString(genre)]})
|
||||
return ['videodb://tvshows/genres/-1/-1/-1/?xsp=%s'%(dumpJSON(param))]
|
||||
|
||||
|
||||
def createMovieGenrePlaylist(self, genre: str, method: str='year') -> list:
|
||||
param = self.getParams()
|
||||
param["type"] = "movies"
|
||||
param["order"]["method"] = method
|
||||
param.setdefault("rules",{}).setdefault("and",[]).append({"field":"genre","operator":"contains","value":[quoteString(genre)]})
|
||||
return ['videodb://movies/genres/?xsp=%s'%(dumpJSON(param))]
|
||||
|
||||
|
||||
def createStudioPlaylist(self, studio: str, method: str='random') -> list:
|
||||
param = self.getParams()
|
||||
param["type"] = "movies"
|
||||
param["order"]["method"] = method
|
||||
param.setdefault("rules",{}).setdefault("and",[]).append({"field":"studio","operator":"contains","value":[quoteString(studio)]})
|
||||
return ['videodb://movies/studios/?xsp=%s'%(dumpJSON(param))]
|
||||
|
||||
|
||||
def createMusicGenrePlaylist(self, genre: str, method: str='random') -> list:
|
||||
param = self.getParams()
|
||||
param["type"] = "music"
|
||||
param["order"]["method"] = method
|
||||
param.setdefault("rules",{}).setdefault("and",[]).append({"field":"genre","operator":"contains","value":[quoteString(genre)]})
|
||||
return ['musicdb://songs/?xsp=%s'%(dumpJSON(param))]
|
||||
|
||||
|
||||
def createGenreMixedPlaylist(self, genre: str) -> list:
|
||||
mixed = self.createTVGenrePlaylist(genre)
|
||||
mixed.extend(self.createMovieGenrePlaylist(genre))
|
||||
return mixed
|
||||
|
||||
|
||||
def createSeasonal(self) -> list:
|
||||
return ["{Seasonal}"]
|
||||
|
||||
|
||||
def createProvisional(self, value: str) -> list:
|
||||
return ["{%s}"%(value)]
|
||||
@@ -0,0 +1,244 @@
|
||||
# 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 functools import reduce
|
||||
from difflib import SequenceMatcher
|
||||
from seasonal import Seasonal
|
||||
|
||||
LOCAL_FOLDERS = [LOGO_LOC, IMAGE_LOC]
|
||||
MUSIC_RESOURCE = ["resource.images.musicgenreicons.text"]
|
||||
GENRE_RESOURCE = ["resource.images.moviegenreicons.transparent"]
|
||||
STUDIO_RESOURCE = ["resource.images.studios.white"]
|
||||
|
||||
class Service:
|
||||
from jsonrpc import JSONRPC
|
||||
player = PLAYER()
|
||||
monitor = MONITOR()
|
||||
jsonRPC = JSONRPC()
|
||||
def _interrupt(self) -> bool:
|
||||
return PROPERTIES.isPendingInterrupt()
|
||||
def _suspend(self) -> bool:
|
||||
return PROPERTIES.isPendingSuspend()
|
||||
|
||||
|
||||
class Resources:
|
||||
queuePool = {}
|
||||
|
||||
def __init__(self, service=None):
|
||||
if service is None: service = Service()
|
||||
self.service = service
|
||||
self.jsonRPC = service.jsonRPC
|
||||
self.cache = service.jsonRPC.cache
|
||||
self.baseURL = service.jsonRPC.buildWebBase()
|
||||
self.remoteHost = PROPERTIES.getRemoteHost()
|
||||
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
return log('%s: %s'%(self.__class__.__name__,msg),level)
|
||||
|
||||
|
||||
def getLogo(self, citem: dict, fallback=LOGO, auto=False) -> str:
|
||||
if citem.get('name') == LANGUAGE(32002): logo = Seasonal().getHoliday().get('logo') #seasonal
|
||||
else: logo = self.getLocalLogo(citem.get('name')) #local
|
||||
if not logo: logo = self.getCachedLogo(citem) #cache
|
||||
if not logo and auto: logo = self.getLogoResources(citem) #resources
|
||||
if not logo and auto: logo = self.getTVShowLogo(citem.get('name')) #tvshow
|
||||
if not logo: logo = (fallback or LOGO) #fallback
|
||||
self.log('getLogo, name = %s, logo = %s, auto = %s'%(citem.get('name'), logo, auto))
|
||||
return logo
|
||||
|
||||
|
||||
@cacheit(expiration=datetime.timedelta(days=MAX_GUIDEDAYS), json_data=True)
|
||||
def selectLogo(self, citem: dict) -> list:
|
||||
logos = []
|
||||
logos.extend(self.getLocalLogo(citem.get('name'),select=True))
|
||||
logos.extend(self.getLogoResources(citem, select=True))
|
||||
logos.extend(self.getTVShowLogo(citem.get('name'), select=True))
|
||||
self.log('selectLogo, chname = %s, logos = %s'%(citem.get('name'), len(logos)))
|
||||
return list([_f for _f in logos if _f])
|
||||
|
||||
|
||||
def queueLOGO(self, param):
|
||||
params = self.queuePool.setdefault('params',[])
|
||||
params.append(param)
|
||||
self.queuePool['params'] = setDictLST(params)
|
||||
self.log("queueLOGO, saving = %s, param = %s"%(len(self.queuePool['params']),param))
|
||||
timerit(SETTINGS.setCacheSetting)(5.0,['queueLOGO', self.queuePool, ADDON_VERSION, True])
|
||||
|
||||
|
||||
def getCachedLogo(self, citem, select=False):
|
||||
cacheFuncs = [{'name':'getLogoResources.%s.%s'%(getMD5(citem.get('name')),select), 'args':(citem,select) ,'checksum':getMD5('|'.join([SETTINGS.getAddonDetails(id).get('version',ADDON_VERSION) for id in self.getResources(citem)]))},
|
||||
{'name':'getTVShowLogo.%s.%s'%(getMD5(citem.get('name')),select) , 'args':(citem.get('name'), select),'checksum':ADDON_VERSION}]
|
||||
for cacheItem in cacheFuncs:
|
||||
cacheResponse = self.cache.get(cacheItem.get('name',''),cacheItem.get('checksum',ADDON_VERSION))
|
||||
if cacheResponse:
|
||||
self.log('getCachedLogo, chname = %s, type = %s, logo = %s'%(citem.get('name'), citem.get('type'), cacheResponse))
|
||||
return cacheResponse
|
||||
else: self.queueLOGO(cacheItem)
|
||||
|
||||
|
||||
def getLocalLogo(self, chname: str, select: bool=False) -> list:
|
||||
logos = []
|
||||
for path in LOCAL_FOLDERS:
|
||||
for ext in IMG_EXTS:
|
||||
if FileAccess.exists(os.path.join(path,'%s%s'%(chname,ext))):
|
||||
self.log('getLocalLogo, found %s'%(os.path.join(path,'%s%s'%(chname,ext))))
|
||||
if select: logos.append(os.path.join(path,'%s%s'%(chname,ext)))
|
||||
else: return os.path.join(path,'%s%s'%(chname,ext))
|
||||
if select: return logos
|
||||
|
||||
|
||||
def fillLogoResource(self, id):
|
||||
results = {}
|
||||
response = self.jsonRPC.walkListDirectory(os.path.join('special://home/addons/%s/resources'%id), exts=IMG_EXTS, checksum=SETTINGS.getAddonDetails(id).get('version',ADDON_VERSION), expiration=datetime.timedelta(days=28))
|
||||
for path, images in list(response.items()):
|
||||
for image in images:
|
||||
name, ext = os.path.splitext(image)
|
||||
results[name] = '%s/%s'%(path,image)
|
||||
return results
|
||||
|
||||
|
||||
def getResources(self, citem={}):
|
||||
resources = SETTINGS.getSetting('Resource_Logos').split('|').copy()
|
||||
if citem.get('type') in ["TV Genres","Movie Genres"]: resources.extend(GENRE_RESOURCE)
|
||||
elif citem.get('type') in ["TV Networks","Movie Studios"]: resources.extend(STUDIO_RESOURCE)
|
||||
elif citem.get('type') in ["Music Genres","Radio"] or isRadio(citem): resources.extend(MUSIC_RESOURCE)
|
||||
else: resources.extend(GENRE_RESOURCE + STUDIO_RESOURCE)
|
||||
self.log('getResources, type = %s, resources = %s'%(citem.get('type'),resources))
|
||||
return resources
|
||||
|
||||
|
||||
def getLogoResources(self, citem: dict, select: bool=False) -> dict and None:
|
||||
self.log('getLogoResources, chname = %s, type = %s, select = %s'%(citem.get('name'), citem.get('type'),select))
|
||||
logos = []
|
||||
resources = self.getResources(citem)
|
||||
cacheName = 'getLogoResources.%s.%s'%(getMD5(citem.get('name')),select)
|
||||
cacheResponse = self.cache.get(cacheName, checksum=getMD5('|'.join([SETTINGS.getAddonDetails(id).get('version',ADDON_VERSION) for id in resources])))
|
||||
if not cacheResponse:
|
||||
for id in list(dict.fromkeys(resources)):
|
||||
if not hasAddon(id):
|
||||
self.log('getLogoResources, missing %s'%(id))
|
||||
continue
|
||||
else:
|
||||
results = self.fillLogoResource(id)
|
||||
self.log('getLogoResources, checking %s, results = %s'%(id,len(results)))
|
||||
for name, logo in list(results.items()):
|
||||
if self.matchName(citem.get('name'), name, auto=select):
|
||||
self.log('getLogoResources, found %s'%(logo))
|
||||
if select: logos.append(logo)
|
||||
else: return self.cache.set(cacheName, logo, checksum=getMD5('|'.join(resources)), expiration=datetime.timedelta(days=MAX_GUIDEDAYS))
|
||||
if select: return self.cache.set(cacheName, logos, checksum=getMD5('|'.join(resources)), expiration=datetime.timedelta(days=MAX_GUIDEDAYS))
|
||||
return cacheResponse
|
||||
|
||||
|
||||
def getTVShowLogo(self, chname: str, select: bool=False) -> dict and None:
|
||||
self.log('getTVShowLogo, chname = %s, select = %s'%(chname,select))
|
||||
logos = []
|
||||
items = self.jsonRPC.getTVshows()
|
||||
cacheName = 'getTVShowLogo.%s.%s'%(getMD5(chname),select)
|
||||
cacheResponse = self.cache.get(cacheName)
|
||||
if not cacheResponse:
|
||||
for item in items:
|
||||
if self.matchName(chname, item.get('title',''), auto=select):
|
||||
keys = ['clearlogo','logo','logos','clearart','icon']
|
||||
for key in keys:
|
||||
logo = item.get('art',{}).get(key,'').replace('image://DefaultFolder.png/','').rstrip('/')
|
||||
if logo:
|
||||
self.log('getTVShowLogo, found %s'%(logo))
|
||||
if select: logos.append(logo)
|
||||
else: return self.cache.set(cacheName, logo, expiration=datetime.timedelta(days=MAX_GUIDEDAYS))
|
||||
if select: return self.cache.set(cacheName, logos, expiration=datetime.timedelta(days=MAX_GUIDEDAYS))
|
||||
return cacheResponse
|
||||
|
||||
|
||||
#todo refactor this mess, proper pattern matching...
|
||||
def matchName(self, chname: str, name: str, type: str='Custom', auto: bool=False) -> bool and None: #todo auto setting SETTINGS.getSettingBool('')
|
||||
chnames = list(set([chname, splitYear(chname)[0], stripRegion(chname), getChannelSuffix(chname, type), cleanChannelSuffix(chname, type), chname.replace('and', '&'), chname.replace('&','and'), slugify(chname), validString(chname)]))
|
||||
renames = list(set([name, splitYear(name)[0], stripRegion(name), slugify(name), validString(name)]))
|
||||
for chname in chnames:
|
||||
if not chname: continue
|
||||
elif auto: return SequenceMatcher(None, chname.lower(), name.lower()).ratio() >= .75
|
||||
elif chname.lower() == name.lower(): return True
|
||||
for rename in renames:
|
||||
if not rename: continue
|
||||
elif chname.lower() == rename.lower(): return True
|
||||
return False
|
||||
|
||||
|
||||
def buildWebImage(self, image: str) -> str:
|
||||
#convert any local images to url via local server and/or kodi web server.
|
||||
if image.startswith(LOGO_LOC) and self.remoteHost:
|
||||
image = 'http://%s/images/%s'%(self.remoteHost,quoteString(os.path.split(image)[1]))
|
||||
elif image.startswith(('image://','image%3A')) and self.baseURL and not ('smb' in image or 'nfs' in image or 'http' in image):
|
||||
image = '%s/image/%s'%(self.baseURL,quoteString(image))
|
||||
self.log('buildWebImage, returning image = %s'%(image))
|
||||
return image
|
||||
|
||||
|
||||
def isMono(self, file: str) -> bool:
|
||||
if file.startswith('resource://') and (bool(set([match in file.lower() for match in ['transparent','white','mono']]))): return True
|
||||
elif hasAddon('script.module.pil'):
|
||||
try:
|
||||
from PIL import Image, ImageStat
|
||||
file = unquoteString(file.replace('resource://','special://home/addons/').replace('image://','')).replace('\\','/')
|
||||
mono = reduce(lambda x, y: x and y < 0.005, ImageStat.Stat(Image.open(FileAccess.open(file.encode('utf-8').strip(),'r'),mode='r')).var, True)
|
||||
self.log('isMono, mono = %s, file = %s'%(mono,file))
|
||||
return mono
|
||||
except Exception as e: self.log("isMono, failed! %s\nfile = %s"%(e,file), xbmc.LOGWARNING)
|
||||
return False
|
||||
|
||||
|
||||
def generate_placeholder(self, text, background_image_path=FileAccess.translatePath(os.path.join(MEDIA_LOC,'blank.png')), output_path=TEMP_LOC, font_path=FileAccess.translatePath(os.path.join('special://skin','fonts','NotoSans-Regular.ttf')), font_size=30, text_color=(255, 255, 255)):
|
||||
"""
|
||||
Generates a placeholder image with text on a background image.
|
||||
|
||||
Args:
|
||||
text: The text to display on the placeholder.
|
||||
background_image_path: Path to the background image.
|
||||
output_path: Path to save the generated placeholder image.
|
||||
font_path: Path to the font file (optional).
|
||||
font_size: Font size for the text (optional).
|
||||
text_color: Color of the text (optional).
|
||||
"""
|
||||
|
||||
if hasAddon('script.module.pil'):
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
# Open the background image
|
||||
background_image = Image.open(background_image_path)
|
||||
# Create a drawing object
|
||||
draw = ImageDraw.Draw(background_image)
|
||||
# Choose a font
|
||||
font = ImageFont.truetype(font_path, font_size)
|
||||
# Calculate text size
|
||||
text_width, text_height = draw.textsize(text, font)
|
||||
# Calculate text position for centering
|
||||
x = (background_image.width - text_width) // 2
|
||||
y = (background_image.height - text_height) // 2
|
||||
# Draw the text on the image
|
||||
draw.text((x, y), text, font=font, fill=text_color)
|
||||
# Save the image
|
||||
file_name = os.path.join(output_path,'%s.png'%(text))
|
||||
fle = FileAccess.open(file_name,'wb')
|
||||
background_image.save(fle,'png')
|
||||
fle.close()
|
||||
|
||||
# Example usage
|
||||
# generate_placeholder("Product Image", "background.jpg", "placeholder.jpg")
|
||||
1771
Kodi/Lenovo/addons/plugin.video.pseudotv.live/resources/lib/rules.py
Normal file
1771
Kodi/Lenovo/addons/plugin.video.pseudotv.live/resources/lib/rules.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,196 @@
|
||||
# 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 -*-
|
||||
# Adapted from https://github.com/sualfred/script.embuary.helper/blob/matrix
|
||||
|
||||
# https://www.holidaysmart.com/category/fandom
|
||||
# https://www.holidaysmart.com/holidays/daily/fandom
|
||||
# https://www.holidaysmart.com/holidays/daily/tv-movies
|
||||
# https://tvtropes.org/pmwiki/pmwiki.php/Main/PopCultureHoliday
|
||||
# https://fanlore.org/wiki/List_of_Annual_Holidays,_Observances,_and_Events_in_Fandom
|
||||
|
||||
from globals import *
|
||||
|
||||
KEY_QUERY = {"method":"","order":"","field":'',"operator":'',"value":[]}
|
||||
LIMITS = {"end":-1,"start":0,"total":0}
|
||||
FILTER = {"field":"","operator":"","value":[]}
|
||||
SORT = {"method":"","order":"","ignorearticle":True,"useartistsortname":True}
|
||||
TV_QUERY = {"path":"videodb://tvshows/titles/", "method":"VideoLibrary.GetEpisodes","enum":"Video.Fields.Episode","key":"episodes","limits":LIMITS,"sort":SORT,"filter":FILTER}
|
||||
MOVIE_QUERY = {"path":"videodb://movies/titles/" , "method":"VideoLibrary.GetMovies" ,"enum":"Video.Fields.Movie" ,"key":"movies" ,"limits":LIMITS,"sort":SORT,"filter":FILTER}
|
||||
|
||||
class Seasonal:
|
||||
def __init__(self):
|
||||
"""
|
||||
Initializes the Seasonal class. Sets up logging and caching.
|
||||
"""
|
||||
self.log('__init__')
|
||||
self.cache = SETTINGS.cacheDB
|
||||
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
"""
|
||||
Logs a message to the system log with the specified logging level.
|
||||
|
||||
:param msg: The message to log.
|
||||
:param level: The log level (default: xbmc.LOGDEBUG).
|
||||
"""
|
||||
return log('%s: %s' % (self.__class__.__name__, msg), level)
|
||||
|
||||
|
||||
def getYear(self):
|
||||
"""
|
||||
Get the current year.
|
||||
|
||||
This function retrieves the current year using the `datetime` module.
|
||||
|
||||
returns:
|
||||
int: The current year.
|
||||
"""
|
||||
return datetime.datetime.now().year
|
||||
|
||||
|
||||
def getMonth(self, name=False):
|
||||
"""
|
||||
Get the current month in either name or numeric format.
|
||||
|
||||
Args:
|
||||
name (bool): If True, returns the full name of the current month (e.g., 'April').
|
||||
If False, returns the numeric representation of the current month (e.g., 4).
|
||||
|
||||
Returns:
|
||||
str/int: The current month as a string (full name) or as an integer (numeric format).
|
||||
"""
|
||||
if name: return datetime.datetime.now().strftime('%B') # Full month name
|
||||
else: return datetime.datetime.now().month # Numeric month
|
||||
|
||||
|
||||
def getDay(self):
|
||||
"""
|
||||
Calculate and return the adjusted day of the month.
|
||||
|
||||
This function adds the current day of the month to the weekday of the first day of the current month.
|
||||
The result can be used to determine the week number or other date-based calculations.
|
||||
|
||||
Returns:
|
||||
int: Adjusted day of the month.
|
||||
"""
|
||||
return datetime.datetime.now().day
|
||||
|
||||
|
||||
def getDOM(self, year, month):
|
||||
"""
|
||||
Get all days of the specified month for a given year.
|
||||
|
||||
This function uses the `calendar.Calendar` class to iterate through all days of the specified month and year.
|
||||
It extracts only the valid days (ignoring placeholder zeros for days outside the month) and returns them as a list.
|
||||
|
||||
Args:
|
||||
year (int): The year of the desired month.
|
||||
month (int): The month (1-12) for which to retrieve the days.
|
||||
|
||||
Returns:
|
||||
list: A list of integers representing the days in the specified month.
|
||||
"""
|
||||
cal = calendar.Calendar()
|
||||
days_in_month = []
|
||||
for day in cal.itermonthdays2(year, month):
|
||||
if day[0] != 0: # Exclude placeholder days (zeros)
|
||||
days_in_month.append(day[0])
|
||||
return days_in_month
|
||||
|
||||
|
||||
def getSeason(self, key):
|
||||
self.log('getSeason, key = %s' % (key))
|
||||
return getJSON(HOLIDAYS).get(key,{})
|
||||
|
||||
|
||||
def getSeasons(self, month):
|
||||
self.log('getSeasons, month = %s' % (month))
|
||||
return getJSON(SEASONS).get(month,{})
|
||||
|
||||
|
||||
@cacheit(expiration=datetime.timedelta(minutes=15), checksum=PROPERTIES.getInstanceID())
|
||||
def getHoliday(self, nearest=SETTINGS.getSettingBool('Nearest_Holiday')):
|
||||
"""
|
||||
Retrieves the current or nearest holiday based on user settings.
|
||||
|
||||
:param nearest: Boolean indicating whether to return the nearest holiday (default: True).
|
||||
:return: A dictionary representing the holiday details.
|
||||
"""
|
||||
self.log('getHoliday, nearest = %s' % (nearest))
|
||||
if nearest: return self.getNearestHoliday()
|
||||
else: return self.getCurrentHoliday()
|
||||
|
||||
|
||||
def getCurrentHoliday(self):
|
||||
"""
|
||||
Retrieves the holiday for the current month and week.
|
||||
|
||||
:return: A dictionary representing the holiday details for the current month and week.
|
||||
"""
|
||||
return self.getSeasons(self.getMonth(name=True)).get(self.getDay(),{})
|
||||
|
||||
|
||||
def getSpecialHolidays(self, month, day): #todo check if month, day of week, day match holiday exceptions.
|
||||
return {"Friday":{"13":{ "name": "Friday The 13th", "tagline": "", "keyword": "", "logo": ""}}}
|
||||
|
||||
|
||||
def getNearestHoliday(self, fallback=True):
|
||||
"""
|
||||
Retrieves the nearest holiday. If no holiday is found in the current week, it searches
|
||||
forward and optionally backward for the nearest holiday.
|
||||
|
||||
:param fallback: Boolean indicating whether to search backward if no holiday is found forward (default: True).
|
||||
:return: A dictionary representing the nearest holiday.
|
||||
"""
|
||||
holiday = {}
|
||||
month = self.getMonth(name=True)
|
||||
day = self.getDay()
|
||||
dom = self.getDOM(self.getYear(),self.getMonth())
|
||||
curr = dom[day - 1:]
|
||||
days = curr
|
||||
if fallback:
|
||||
past = dom[:day - 1]
|
||||
past.reverse()
|
||||
days = days + past
|
||||
|
||||
for next in days:
|
||||
holiday = self.getSeasons(month).get(str(next),{})
|
||||
if holiday.get('keyword'): break
|
||||
self.log('getNearestHoliday, using fallback = %s, month = %s, day = %s, nearest day = %s, returning = %s' %(fallback, month, day, next, holiday))
|
||||
return holiday
|
||||
|
||||
|
||||
def buildSeasonal(self):
|
||||
"""
|
||||
Builds a generator that provides seasonal content queries. Each query is augmented
|
||||
with holiday-specific metadata, including sorting and filtering options.
|
||||
|
||||
:yield: A dictionary representing a seasonal content query.
|
||||
"""
|
||||
holiday = self.getHoliday()
|
||||
season = self.getSeason(holiday.get('keyword'))
|
||||
for type, params in list(season.items()):
|
||||
for param in params:
|
||||
item = {'episodes':TV_QUERY,'movies':MOVIE_QUERY}[type.lower()].copy()
|
||||
item["holiday"] = holiday
|
||||
item["sort"].update(param.get("sort"))
|
||||
item["filter"].update(param.get("filter"))
|
||||
self.log('buildSeasonal, %s - item = %s'%(holiday.get('name'),item))
|
||||
yield item
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
# 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 -*-
|
||||
import gzip, mimetypes, socket, time
|
||||
|
||||
from zeroconf import *
|
||||
from globals import *
|
||||
from functools import partial
|
||||
from six.moves.BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
|
||||
from six.moves.socketserver import ThreadingMixIn
|
||||
|
||||
#todo proper REST API to handle server/client communication incl. sync/update triggers.
|
||||
#todo incorporate experimental webserver UI to master branch.
|
||||
|
||||
ZEROCONF_SERVICE = "_xbmc-jsonrpc-h._tcp.local."
|
||||
|
||||
class Discovery:
|
||||
class MyListener(object):
|
||||
def __init__(self, multiroom=None):
|
||||
self.zServers = dict()
|
||||
self.zeroconf = Zeroconf()
|
||||
self.multiroom = multiroom
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
return log('%s: %s'%(self.__class__.__name__,msg),level)
|
||||
|
||||
def removeService(self, zeroconf, type, name):
|
||||
self.log("getService, type = %s, name = %s"%(type,name))
|
||||
|
||||
def addService(self, zeroconf, type, name):
|
||||
info = self.zeroconf.getServiceInfo(type, name)
|
||||
if info:
|
||||
IP = socket.inet_ntoa(info.getAddress())
|
||||
if IP != SETTINGS.getIP():
|
||||
server = info.getServer()
|
||||
self.zServers[server] = {'type':type,'name':name,'server':server,'host':'%s:%d'%(IP,info.getPort()),'bonjour':'http://%s:%s/%s'%(IP,SETTINGS.getSettingInt('TCP_PORT'),BONJOURFLE)}
|
||||
self.log("addService, found zeroconf %s @ %s using using bonjour %s"%(server,self.zServers[server]['host'],self.zServers[server]['bonjour']))
|
||||
self.multiroom.addServer(requestURL(self.zServers[server]['bonjour'],json_data=True))
|
||||
|
||||
|
||||
def __init__(self, service=None, multiroom=None):
|
||||
self.service = service
|
||||
self.multiroom = multiroom
|
||||
self._start()
|
||||
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
return log('%s: %s'%(self.__class__.__name__,msg),level)
|
||||
|
||||
|
||||
def _start(self):
|
||||
if not PROPERTIES.isRunning('Discovery'):
|
||||
with PROPERTIES.chkRunning('Discovery'):
|
||||
zconf = Zeroconf()
|
||||
zcons = self.multiroom._getStatus()
|
||||
self.log("_start, Multicast DNS Service Discovery (%s)"%(ZEROCONF_SERVICE))
|
||||
SETTINGS.setSetting('ZeroConf_Status','[COLOR=yellow][B]%s[/B][/COLOR]'%(LANGUAGE(32252)))
|
||||
ServiceBrowser(zconf, ZEROCONF_SERVICE, self.MyListener(multiroom=self.multiroom))
|
||||
self.service.monitor.waitForAbort(DISCOVER_INTERVAL)
|
||||
SETTINGS.setSetting('ZeroConf_Status',LANGUAGE(32211)%({True:'green',False:'red'}[zcons],{True:LANGUAGE(32158),False:LANGUAGE(32253)}[zcons]))
|
||||
zconf.close()
|
||||
|
||||
|
||||
class RequestHandler(BaseHTTPRequestHandler):
|
||||
|
||||
def __init__(self, request, client_address, server, monitor):
|
||||
self.monitor = monitor
|
||||
self.cache = SETTINGS.cache
|
||||
try: BaseHTTPRequestHandler.__init__(self, request, client_address, server)
|
||||
except: pass
|
||||
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
return log('%s: %s'%(self.__class__.__name__,msg),level)
|
||||
|
||||
|
||||
def _set_headers(self, content='*/*', size=None, gzip=False):
|
||||
self.send_response(200, "OK")
|
||||
self.send_header("Content-type",content)
|
||||
if size: self.send_header("Content-Length", len(size))
|
||||
if gzip: self.send_header("Content-Encoding", "gzip")
|
||||
self.end_headers()
|
||||
|
||||
|
||||
def _gzip_encode(self, content):
|
||||
out = BytesIO()
|
||||
f = gzip.GzipFile(fileobj=out, mode='w', compresslevel=5)
|
||||
f.write(content)
|
||||
f.close()
|
||||
return out.getvalue()
|
||||
|
||||
|
||||
def do_HEAD(self):
|
||||
return self._set_headers()
|
||||
|
||||
|
||||
def do_POST(self):
|
||||
def _verifyUUID(uuid):
|
||||
if uuid == SETTINGS.getMYUUID(): return True
|
||||
else:
|
||||
from multiroom import Multiroom
|
||||
for server in list(Multiroom().getDiscovery().values()):
|
||||
if server.get('uuid') == uuid: return True
|
||||
|
||||
self.log('do_POST, incoming path = %s'%(self.path))
|
||||
if not PROPERTIES.isRunning('do_POST'):
|
||||
with PROPERTIES.chkRunning('do_POST'), PROPERTIES.interruptActivity():
|
||||
if self.path.lower().endswith('.json'):
|
||||
try: incoming = loadJSON(self.rfile.read(int(self.headers['content-length'])).decode())
|
||||
except: incoming = {}
|
||||
if _verifyUUID(incoming.get('uuid')):
|
||||
self.log('do_POST incoming uuid [%s] verified!'%(incoming.get('uuid')))
|
||||
#channels - channel manager save
|
||||
if self.path.lower() == '/%s'%(CHANNELFLE.lower()) and incoming.get('payload'):
|
||||
from channels import Channels
|
||||
if Channels().setChannels(list(Channels()._verify(incoming.get('payload')))):
|
||||
DIALOG.notificationDialog(LANGUAGE(30085)%(LANGUAGE(30108),incoming.get('name',ADDON_NAME)))
|
||||
return self.send_response(200, "OK")
|
||||
#filelist w/resume - paused channel rule
|
||||
elif self.path.lower().startswith('/filelist') and incoming.get('payload'):
|
||||
if setJSON(os.path.join(TEMP_LOC,self.path.replace('/filelist/','')),incoming.get('payload')):
|
||||
DIALOG.notificationDialog(LANGUAGE(30085)%(LANGUAGE(30060),incoming.get('name',ADDON_NAME)))
|
||||
return self.send_response(200, "OK")
|
||||
else: self.send_error(401, "Path Not found")
|
||||
else: return self.send_error(401, "UUID Not verified!")
|
||||
else: return self.do_GET()
|
||||
|
||||
|
||||
def do_GET(self):
|
||||
def _sendChunk(path, content, chunk):
|
||||
self.log('do_GET, outgoing path = %s, content = %s'%(path, content))
|
||||
self._set_headers(content,chunk)
|
||||
self.log('do_GET, sending chunk, size = %s'%(len(chunk)))
|
||||
self.wfile.write(chunk)
|
||||
self.wfile.close()
|
||||
|
||||
def _sendChunks(path, content):
|
||||
self._set_headers(content)
|
||||
self.log('do_GET, outgoing path = %s, content = %s'%(path, content))
|
||||
while not self.monitor.abortRequested():
|
||||
chunk = fle.read(64 * 1024).encode(encoding=DEFAULT_ENCODING)
|
||||
if not chunk or self.monitor.waitForAbort(0.0001): break
|
||||
self.send_header('content-length', len(chunk))
|
||||
self.log('do_GET, sending = %s, chunk = %s'%(path, chunk))
|
||||
self.wfile.write(chunk)
|
||||
self.wfile.close()
|
||||
|
||||
def _sendFile(path, content):
|
||||
self.log('do_GET, outgoing path = %s, content = %s'%(path, content))
|
||||
with xbmcvfs.File(path, "r") as fle:
|
||||
chunk = fle.read().encode(encoding=DEFAULT_ENCODING)
|
||||
self._set_headers(content,chunk)
|
||||
self.log('do_GET, sending = %s, size = %s'%(path,len(chunk)))
|
||||
self.wfile.write(chunk)
|
||||
self.wfile.close()
|
||||
|
||||
def _sendZip(path, content):
|
||||
self.log('do_GET, outgoing path = %s, content = %s'%(path, content))
|
||||
with xbmcvfs.File(path, "r") as fle:
|
||||
if 'gzip' in self.headers.get('accept-encoding'):
|
||||
data = self._gzip_encode(fle.read().encode(encoding=DEFAULT_ENCODING))
|
||||
self._set_headers(content,data,True)
|
||||
self.log('do_GET, sending = %s, gzip compressing'%(path))
|
||||
self.wfile.write(data)
|
||||
self.wfile.close()
|
||||
else: self._sendChunks(path, content)
|
||||
|
||||
def _sendImage(path, content):
|
||||
self.log('do_GET, outgoing path = %s, content = %s'%(path, content))
|
||||
with xbmcvfs.File(path, "r") as fle:
|
||||
chunk = fle.readBytes()
|
||||
self._set_headers(content,chunk)
|
||||
self.log('do_GET, sending = %s, size = %s'%(path,len(chunk)))
|
||||
self.wfile.write(chunk)
|
||||
self.wfile.close()
|
||||
|
||||
self.log('do_GET, incoming path = %s'%(self.path))
|
||||
if not PROPERTIES.isRunning('do_GET'):
|
||||
with PROPERTIES.chkRunning('do_GET'), PROPERTIES.interruptActivity():
|
||||
#Bonjour json/html
|
||||
if self.path.lower() == '/%s'%(BONJOURFLE.lower()):
|
||||
chunk = dumpJSON(SETTINGS.getBonjour(inclChannels=True),idnt=4).encode(encoding=DEFAULT_ENCODING)
|
||||
_sendChunk(self.path.lower(), "application/json", chunk)
|
||||
#Remotes Json/jtml
|
||||
elif self.path.lower().startswith('/remote'):
|
||||
if self.path.lower().endswith('.json'):
|
||||
_sendChunk(self.path.lower(), "application/json", dumpJSON(SETTINGS.getPayload(),idnt=4).encode(encoding=DEFAULT_ENCODING))
|
||||
elif self.path.lower().endswith('.html'):
|
||||
_sendChunk(self.path.lower(), "text/html", SETTINGS.getPayloadUI().encode(encoding=DEFAULT_ENCODING))
|
||||
else: self.send_error(404, "Path Not found")
|
||||
#filelist - Paused Channels
|
||||
elif self.path.lower().startswith('/filelist') and self.path.lower().endswith('.json'):
|
||||
_sendChunk(self.path.lower(), "application/json", dumpJSON(getJSON((os.path.join(TEMP_LOC,self.path.replace('/filelist/',''))))).encode(encoding=DEFAULT_ENCODING))
|
||||
#M3U - MPEG
|
||||
elif self.path.lower() == '/%s'%(M3UFLE.lower()):
|
||||
_sendFile(M3UFLEPATH, "application/vnd.apple.mpegurl")
|
||||
#Genres - XML
|
||||
elif self.path.lower() == '/%s'%(GENREFLE.lower()):
|
||||
_sendFile(GENREFLEPATH, "text/plain")
|
||||
#XMLTV - XML (Large)
|
||||
elif self.path.lower() == '/%s'%(XMLTVFLE.lower()):
|
||||
_sendZip(XMLTVFLEPATH, "text/xml")
|
||||
#Images - image server
|
||||
elif self.path.lower().startswith("/images/"):
|
||||
_sendImage(os.path.join(LOGO_LOC,unquoteString(self.path.replace('/images/',''))), mimetypes.guess_type(self.path[1:])[0])
|
||||
else: self.send_error(404, "Path Not found")
|
||||
|
||||
class HTTP:
|
||||
isRunning = False
|
||||
|
||||
def __init__(self, service=None):
|
||||
self.log('__init__')
|
||||
self.service = service
|
||||
timerit(self._start)(0.1)
|
||||
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
return log('%s: %s'%(self.__class__.__name__,msg),level)
|
||||
|
||||
|
||||
def chkPort(self, port=0, redirect=False):
|
||||
try:
|
||||
state = False
|
||||
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
|
||||
s.bind(("127.0.0.1", port))
|
||||
state = True
|
||||
except Exception as e:
|
||||
self.log("chkPort, port = %s, failed! = %s"%(port,e))
|
||||
if redirect:
|
||||
try:
|
||||
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
|
||||
s.bind(("127.0.0.1", 0))
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
port = s.getsockname()[1]
|
||||
state = True
|
||||
except Exception as e: self.log("chkPort, port = %s, failed! = %s"%(port,e))
|
||||
else: port = None
|
||||
self.log("chkPort, port = %s, available = %s"%(port,state))
|
||||
return port
|
||||
|
||||
|
||||
def _start(self, wait=900):
|
||||
while not self.service.monitor.abortRequested():
|
||||
if not self.isRunning:
|
||||
try:
|
||||
IP = SETTINGS.getIP()
|
||||
TCP = SETTINGS.getSettingInt('TCP_PORT')
|
||||
PORT= self.chkPort(TCP,redirect=True)
|
||||
if PORT is None: raise Exception('Port: %s In-Use!'%(PORT))
|
||||
elif PORT != TCP: SETTINGS.setSettingInt('TCP_PORT',PORT)
|
||||
LOCAL_HOST = PROPERTIES.setRemoteHost('%s:%s'%(IP,PORT))
|
||||
self.log("_start, starting server @ %s"%(LOCAL_HOST),xbmc.LOGINFO)
|
||||
|
||||
SETTINGS.setSetting('Remote_NAME' ,SETTINGS.getFriendlyName())
|
||||
SETTINGS.setSetting('Remote_M3U' ,'http://%s/%s'%(LOCAL_HOST,M3UFLE))
|
||||
SETTINGS.setSetting('Remote_XMLTV','http://%s/%s'%(LOCAL_HOST,XMLTVFLE))
|
||||
SETTINGS.setSetting('Remote_GENRE','http://%s/%s'%(LOCAL_HOST,GENREFLE))
|
||||
|
||||
self.isRunning = True
|
||||
self._server = ThreadedHTTPServer((IP, PORT), partial(RequestHandler,monitor=self.service.monitor))
|
||||
self._server.allow_reuse_address = True
|
||||
self._httpd_thread = Thread(target=self._server.serve_forever)
|
||||
self._httpd_thread.daemon=True
|
||||
self._httpd_thread.start()
|
||||
except Exception as e: self.log("_start, Failed! %s"%(e), xbmc.LOGERROR)
|
||||
self._update()
|
||||
if self.service.monitor.waitForAbort(wait): break
|
||||
self._stop()
|
||||
|
||||
|
||||
def _stop(self):
|
||||
try:
|
||||
if self.isRunning:
|
||||
self.log('_stop, shutting server down',xbmc.LOGINFO)
|
||||
self._server.shutdown()
|
||||
self._server.server_close()
|
||||
self._server.socket.close()
|
||||
if self._httpd_thread.is_alive():
|
||||
try: self._httpd_thread.join(5)
|
||||
except: pass
|
||||
except Exception as e: self.log("_stop, Failed! %s"%(e), xbmc.LOGERROR)
|
||||
self.isRunning = False
|
||||
self._update()
|
||||
|
||||
|
||||
def _update(self):
|
||||
DIALOG.notificationDialog('%s: %s'%(SETTINGS.getSetting('Remote_NAME'),LANGUAGE(32211)%({True:'green',False:'red'}[self.isRunning],{True:LANGUAGE(32158),False:LANGUAGE(32253)}[self.isRunning])))
|
||||
SETTINGS.setSetting('Remote_Status',LANGUAGE(32211)%({True:'green',False:'red'}[self.isRunning],{True:LANGUAGE(32158),False:LANGUAGE(32253)}[self.isRunning]))
|
||||
|
||||
|
||||
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
|
||||
daemon_threads = True
|
||||
|
||||
@@ -0,0 +1,498 @@
|
||||
# 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 overlay import Background, Restart, Overlay, OnNext
|
||||
from rules import RulesList
|
||||
from tasks import Tasks
|
||||
from jsonrpc import JSONRPC
|
||||
|
||||
class Player(xbmc.Player):
|
||||
sysInfo = {}
|
||||
pendingItem = {}
|
||||
isPseudoTV = False
|
||||
pendingStop = False
|
||||
pendingPlay = -1
|
||||
lastSubState = False
|
||||
background = None
|
||||
restart = None
|
||||
onnext = None
|
||||
overlay = None
|
||||
runActions = None
|
||||
|
||||
"""
|
||||
Player() Trigger Order
|
||||
Player: onPlayBackStarted
|
||||
Player: onAVChange (if playing)
|
||||
Player: onAVStarted
|
||||
Player: onPlayBackSeek (if seek)
|
||||
Player: onAVChange (if changed)
|
||||
Player: onPlayBackError
|
||||
Player: onPlayBackEnded
|
||||
Player: onPlayBackStopped
|
||||
"""
|
||||
|
||||
def __init__(self, service=None):
|
||||
xbmc.Player.__init__(self)
|
||||
self.service = service
|
||||
self.jsonRPC = service.jsonRPC
|
||||
self.enableOverlay = SETTINGS.getSettingBool('Overlay_Enable')
|
||||
self.infoOnChange = SETTINGS.getSettingBool('Enable_OnInfo')
|
||||
self.disableTrakt = SETTINGS.getSettingBool('Disable_Trakt')
|
||||
self.rollbackPlaycount = SETTINGS.getSettingBool('Rollback_Watched')
|
||||
self.saveDuration = SETTINGS.getSettingBool('Store_Duration')
|
||||
self.minDuration = SETTINGS.getSettingInt('Seek_Tolerance')
|
||||
self.maxProgress = SETTINGS.getSettingInt('Seek_Threshold')
|
||||
self.sleepTime = SETTINGS.getSettingInt('Idle_Timer')
|
||||
self.runWhilePlaying = SETTINGS.getSettingBool('Run_While_Playing')
|
||||
self.restartPercentage = SETTINGS.getSettingInt('Restart_Percentage')
|
||||
self.OnNextMode = SETTINGS.getSettingInt('OnNext_Mode')
|
||||
self.onNextPosition = SETTINGS.getSetting("OnNext_Position_XY")
|
||||
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
return log('%s: %s'%(self.__class__.__name__,msg),level)
|
||||
|
||||
|
||||
def onPlayBackStarted(self):
|
||||
self.pendingPlay = time.time()
|
||||
self.lastSubState = BUILTIN.isSubtitle()
|
||||
self.log('onPlayBackStarted, pendingPlay = %s'%(self.pendingPlay))
|
||||
|
||||
|
||||
def onAVChange(self):
|
||||
self.log('onAVChange')
|
||||
|
||||
|
||||
def onAVStarted(self):
|
||||
self.pendingPlay = -1
|
||||
self.pendingStop = True
|
||||
self.toggleOverlay(False)
|
||||
self.pendingItem = self.getPlayerSysInfo()
|
||||
self.isPseudoTV = self.pendingItem.get('isPseudoTV',False)
|
||||
self.log('onAVStarted, pendingStop = %s, isPseudoTV = %s, pendingItem = %s'%(self.pendingStop,self.isPseudoTV,self.pendingItem))
|
||||
self._onPlay(sysInfo=self.pendingItem)
|
||||
|
||||
|
||||
def onPlayBackSeek(self, seek_time=None, seek_offset=None): #Kodi bug? `OnPlayBackSeek` no longer called by player during seek, issue limited to pvr?
|
||||
self.log('onPlayBackSeek, seek_time = %s, seek_offset = %s'%(seek_time,seek_offset))
|
||||
|
||||
|
||||
def onPlayBackError(self):
|
||||
self.log('onPlayBackError')
|
||||
self._onError()
|
||||
|
||||
|
||||
def onPlayBackEnded(self):
|
||||
self.log('onPlayBackEnded')
|
||||
self.pendingStop = False
|
||||
self.pendingPlay = -1
|
||||
self._onChange()
|
||||
|
||||
|
||||
def onPlayBackStopped(self):
|
||||
self.log('onPlayBackStopped')
|
||||
self.pendingStop = False
|
||||
self.pendingPlay = -1
|
||||
self._onStop()
|
||||
|
||||
|
||||
def getPlayerSysInfo(self):
|
||||
def __update(id, citem={}): #sysInfo from listitem maybe outdated, check with channels.json
|
||||
channels = self.service.tasks.getVerifiedChannels()
|
||||
for item in channels:
|
||||
if item.get('id',random.random()) == id:
|
||||
return combineDicts(citem,item)
|
||||
return citem
|
||||
|
||||
# with self.service.lock: # Ensure thread safety
|
||||
sysInfo = loadJSON(decodeString(self.getPlayerItem().getProperty('sysInfo')))
|
||||
sysInfo['isPseudoTV'] = '@%s'%(slugify(ADDON_NAME)) in sysInfo.get('chid','')
|
||||
sysInfo['chfile'] = BUILTIN.getInfoLabel('Filename','Player')
|
||||
sysInfo['chfolder'] = BUILTIN.getInfoLabel('Folderpath','Player')
|
||||
sysInfo['chpath'] = BUILTIN.getInfoLabel('Filenameandpath','Player')
|
||||
|
||||
if sysInfo['isPseudoTV']:
|
||||
if not sysInfo.get('fitem'): sysInfo.update({'fitem':decodePlot(BUILTIN.getInfoLabel('Plot','VideoPlayer'))})
|
||||
if not sysInfo.get('nitem'): sysInfo.update({'nitem':decodePlot(BUILTIN.getInfoLabel('NextPlot','VideoPlayer'))})
|
||||
sysInfo.update({'citem':combineDicts(sysInfo.get('citem',{}),__update(sysInfo.get('citem',{}).get('id'))),'runtime':int(self.getPlayerTime())}) #still needed for adv. rules?
|
||||
if not sysInfo.get('callback'): sysInfo['callback'] = self.jsonRPC.getCallback(sysInfo)
|
||||
PROPERTIES.setEXTProperty('%s.lastPlayed.sysInfo'%(ADDON_ID),encodeString(dumpJSON(sysInfo)))
|
||||
return sysInfo
|
||||
|
||||
|
||||
def getPlayerItem(self):
|
||||
try: return self.getPlayingItem()
|
||||
except:
|
||||
self.service.monitor.waitForAbort(0.1)
|
||||
if self.isPlaying(): return self.getPlayerItem()
|
||||
else: return xbmcgui.ListItem()
|
||||
|
||||
|
||||
def getPlayerFile(self):
|
||||
try: return self.getPlayingFile()
|
||||
except: return self.sysInfo.get('fitem',{}).get('file')
|
||||
|
||||
|
||||
def getPlayerTime(self):
|
||||
try: return (self.getTimeLabel('Duration') or self.getTotalTime())
|
||||
except: return (self.sysInfo.get('fitem',{}).get('runtime') or -1)
|
||||
|
||||
|
||||
def getPlayedTime(self):
|
||||
try: return (self.getTimeLabel('Time') or self.getTime()) #getTime retrieves Guide times not actual media time.
|
||||
except: return -1
|
||||
|
||||
|
||||
def getRemainingTime(self):
|
||||
try: return self.getPlayerTime() - self.getPlayedTime()
|
||||
except: return (self.getTimeLabel('TimeRemaining') or -1)
|
||||
|
||||
|
||||
def getPlayerProgress(self):
|
||||
try: return abs(int((self.getRemainingTime() / self.getPlayerTime()) * 100) - 100)
|
||||
except: return int((BUILTIN.getInfoLabel('Progress','Player') or '-1'))
|
||||
|
||||
|
||||
def getTimeLabel(self, prop: str='TimeRemaining') -> int and float: #prop='EpgEventElapsedTime'
|
||||
if self.isPlaying(): return timeString2Seconds(BUILTIN.getInfoLabel('%s(hh:mm:ss)'%(prop),'Player'))
|
||||
|
||||
|
||||
def setSubtitles(self, state: bool=True):
|
||||
hasSubtitle = BUILTIN.hasSubtitle()
|
||||
self.log('setSubtitles, show subtitles = %s, hasSubtitle = %s'%(state,hasSubtitle))
|
||||
if not hasSubtitle: state = False
|
||||
self.showSubtitles(state)
|
||||
|
||||
|
||||
def _onPlay(self, sysInfo={}):
|
||||
self.toggleBackground(False)
|
||||
self.toggleOverlay(False)
|
||||
self.toggleRestart(False)
|
||||
self.toggleOnNext(False)
|
||||
if self.isPseudoTV:
|
||||
oldInfo = self.sysInfo
|
||||
newChan = oldInfo.get('chid',random.random()) != sysInfo.get('chid')
|
||||
self.log('_onPlay, [%s], mode = %s, isPlaylist = %s, new channel = %s'%(sysInfo.get('citem',{}).get('id'), sysInfo.get('mode'), sysInfo.get('isPlaylist',False), newChan))
|
||||
if newChan: #New channel
|
||||
self.runActions = RulesList([sysInfo.get('citem',{})]).runActions
|
||||
self.sysInfo = self._runActions(RULES_ACTION_PLAYER_START, sysInfo.get('citem',{}), sysInfo, inherited=self)
|
||||
self.toggleRestart(bool(self.restartPercentage))
|
||||
PROPERTIES.setTrakt(self.disableTrakt)
|
||||
self.setSubtitles(self.lastSubState) #todo allow rules to set sub preference per channel.
|
||||
else: #New Program/Same Channel
|
||||
self.sysInfo = sysInfo
|
||||
if self.sysInfo.get('radio',False): timerit(BUILTIN.executebuiltin)(0.5,['ReplaceWindow(visualisation)'])
|
||||
elif self.sysInfo.get('isPlaylist',False): timerit(BUILTIN.executebuiltin)(0.5,['ReplaceWindow(fullscreenvideo)'])
|
||||
self.toggleInfo(self.infoOnChange)
|
||||
|
||||
self.jsonRPC.quePlaycount(oldInfo.get('fitem',{}),self.rollbackPlaycount)
|
||||
self.jsonRPC._setRuntime(self.sysInfo.get('fitem',{}),self.sysInfo.get('runtime'),self.saveDuration)
|
||||
|
||||
|
||||
def _onChange(self):
|
||||
if self.sysInfo:
|
||||
if not self.sysInfo.get('isPlaylist',False):
|
||||
self.log('_onChange, [%s], isPlaylist = %s, callback = %s'%(self.sysInfo.get('citem',{}).get('id'),self.sysInfo.get('isPlaylist',False),self.sysInfo.get('callback')))
|
||||
self.toggleBackground(self.enableOverlay)
|
||||
timerit(BUILTIN.executebuiltin)(0.1,['PlayMedia(%s)'%(self.sysInfo.get('callback'))])
|
||||
self.sysInfo = self._runActions(RULES_ACTION_PLAYER_CHANGE, self.sysInfo.get('citem',{}), self.sysInfo, inherited=self)
|
||||
else:
|
||||
self.toggleBackground(False)
|
||||
self.toggleOverlay(False)
|
||||
self.toggleRestart(False)
|
||||
self.toggleOnNext(False)
|
||||
self.toggleInfo(False)
|
||||
|
||||
|
||||
def _onStop(self):
|
||||
self.log('_onStop, id = %s'%(self.sysInfo.get('citem',{}).get('id')))
|
||||
self.toggleBackground(False)
|
||||
self.toggleOverlay(False)
|
||||
self.toggleRestart(False)
|
||||
self.toggleOnNext(False)
|
||||
self.toggleInfo(False)
|
||||
if self.sysInfo:
|
||||
PROPERTIES.setTrakt(False)
|
||||
self.jsonRPC.quePlaycount(self.sysInfo.get('fitem',{}),self.rollbackPlaycount)
|
||||
if self.sysInfo.get('isPlaylist',False): xbmc.PlayList(xbmc.PLAYLIST_VIDEO).clear()
|
||||
self.sysInfo = self._runActions(RULES_ACTION_PLAYER_STOP, self.sysInfo.get('citem',{}), {}, inherited=self)
|
||||
|
||||
|
||||
def _onError(self): #todo evaluate potential for error handling.
|
||||
self.log('_onError, playing file = %s'%(self.getPlayerFile()))
|
||||
if self.isPseudoTV and SETTINGS.getSettingBool('Debug_Enable'):
|
||||
DIALOG.notificationDialog(LANGUAGE(32000))
|
||||
timerit(BUILTIN.executebuiltin)(0.5,['Number(0)'])
|
||||
self.onPlayBackStopped()
|
||||
|
||||
|
||||
def _runActions(self, action, citem={}, parameter=None, inherited=None):
|
||||
if self.runActions: return self.runActions(action, citem, parameter, inherited)
|
||||
else: return parameter
|
||||
|
||||
|
||||
def toggleBackground(self, state: bool=SETTINGS.getSettingBool('Overlay_Enable')):
|
||||
if state and self.background is None and self.service.monitor.isIdle:
|
||||
BUILTIN.executebuiltin("Dialog.Close(all)")
|
||||
self.background = Background(BACKGROUND_XML, ADDON_PATH, "default", player=self)
|
||||
self.background.show()
|
||||
elif not state and hasattr(self.background,'close'):
|
||||
self.background = self.background.close()
|
||||
else: return
|
||||
self.log("toggleBackground, state = %s, background = %s"%(state,self.background))
|
||||
|
||||
|
||||
def toggleOverlay(self, state: bool=SETTINGS.getSettingBool('Overlay_Enable')):
|
||||
if state and self.overlay is None and self.isPlaying():
|
||||
self.overlay = Overlay(player=self)
|
||||
self.overlay.open()
|
||||
elif not state and hasattr(self.overlay,'close'):
|
||||
self.overlay = self.overlay.close()
|
||||
else: return
|
||||
self.log("toggleOverlay, state = %s, overlay = %s"%(state, self.overlay))
|
||||
|
||||
|
||||
def toggleRestart(self, state: bool=bool(SETTINGS.getSettingInt('Restart_Percentage'))):
|
||||
if state and self.restart is None and self.isPlaying():
|
||||
self.restart = Restart(RESTART_XML, ADDON_PATH, "default", "1080i", player=self)
|
||||
elif not state and hasattr(self.restart,'onClose'):
|
||||
self.restart = self.restart.onClose()
|
||||
else: return
|
||||
self.log("toggleRestart, state = %s, restart = %s"%(state,self.restart))
|
||||
|
||||
|
||||
def toggleOnNext(self, state: bool=bool(SETTINGS.getSettingInt('OnNext_Mode'))):
|
||||
if state and self.onnext is None and self.isPlaying():
|
||||
self.onnext = OnNext(ONNEXT_XML, ADDON_PATH, "default", "1080i", player=self, mode=self.OnNextMode, position=self.onNextPosition)
|
||||
elif hasattr(self.onnext,'onClose'):
|
||||
self.onnext = self.onnext.onClose()
|
||||
else: return
|
||||
self.log("toggleOnNext, state = %s, onnext = %s"%(state,self.onnext))
|
||||
|
||||
|
||||
def toggleInfo(self, state: bool=SETTINGS.getSettingBool('Enable_OnInfo')):
|
||||
if state and not BUILTIN.getInfoLabel('Genre','VideoPlayer') in FILLER_TYPE:
|
||||
timerit(self.toggleInfo)(float(OSD_TIMER),[False])
|
||||
BUILTIN.executebuiltin('ActivateWindow(fullscreeninfo)')
|
||||
elif not state and BUILTIN.getInfoBool('IsVisible(fullscreeninfo)','Window'):
|
||||
BUILTIN.executebuiltin('Action(back)')
|
||||
BUILTIN.executebuiltin("Dialog.Close(fullscreeninfo)")
|
||||
self.log('toggleInfo, state = %s'%(state))
|
||||
|
||||
|
||||
class Monitor(xbmc.Monitor):
|
||||
idleTime = 0
|
||||
isIdle = False
|
||||
|
||||
def __init__(self, service=None):
|
||||
self.log('__init__')
|
||||
xbmc.Monitor.__init__(self)
|
||||
self.service = service
|
||||
self.jsonRPC = service.jsonRPC
|
||||
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
return log('%s: %s'%(self.__class__.__name__,msg),level)
|
||||
|
||||
|
||||
def chkIdle(self):
|
||||
def __chkIdle():
|
||||
self.idleTime = BUILTIN.getIdle()
|
||||
self.isIdle = bool(self.idleTime) | self.idleTime > FIFTEEN
|
||||
self.log('__chkIdle, isIdle = %s, idleTime = %s'%(self.isIdle, self.idleTime))
|
||||
|
||||
def __chkResumeTime():
|
||||
if self.service.player.sysInfo.get('isPlaylist',False):
|
||||
file = self.service.player.getPlayingFile()
|
||||
if self.service.player.sysInfo.get('fitem',{}).get('file') == file:
|
||||
resume = {"position":self.service.player.getPlayedTime(),"total":self.service.player.getPlayerTime(),"file":file}
|
||||
self.log('__chkResumeTime, resume = %s'%(resume))
|
||||
self.service.player.sysInfo.setdefault('resume',{}).update(resume)
|
||||
|
||||
def __chkPlayback():
|
||||
if self.service.player.pendingPlay > 0:
|
||||
if not BUILTIN.isBusyDialog() and (time.time() - self.service.player.pendingPlay) > 60: self.service.player.onPlayBackError()
|
||||
|
||||
def __chkSleepTimer():
|
||||
if self.service.player.sleepTime > 0 and (self.idleTime > (self.service.player.sleepTime * 10800)):
|
||||
if not PROPERTIES.isRunning('__chkSleepTimer'):
|
||||
with PROPERTIES.chkRunning('__chkSleepTimer'):
|
||||
if self.sleepTimer(): self.service.player.stop()
|
||||
|
||||
def __chkBackground():
|
||||
remaining = floor(self.service.player.getRemainingTime())
|
||||
if self.isIdle and remaining <= 45:
|
||||
self.log('__chkBackground, isIdle = %s, remaining = %s'%(self.isIdle, remaining))
|
||||
self.service.player.toggleBackground(self.service.player.enableOverlay)
|
||||
|
||||
def __chkOverlay():
|
||||
played = ceil(self.service.player.getPlayedTime())
|
||||
if self.isIdle and played > OSD_TIMER:
|
||||
self.log('__chkOverlay, isIdle = %s, played = %s'%(self.isIdle, played))
|
||||
self.service.player.toggleOverlay(self.service.player.enableOverlay)
|
||||
|
||||
def __chkOnNext():
|
||||
played = self.service.player.getPlayedTime()
|
||||
remaining = floor(self.service.player.getRemainingTime())
|
||||
totalTime = int(self.service.player.getPlayerTime() * (self.service.player.maxProgress / 100))
|
||||
threshold = abs((totalTime - (totalTime * .75)) - (ONNEXT_TIMER*3))
|
||||
intTime = roundupDIV(threshold,3)
|
||||
if self.isIdle and played > self.service.player.minDuration and (remaining <= threshold and remaining >= intTime) and self.service.player.background is None:
|
||||
self.log('__chkOnNext, isIdle = %s, played = %s, remaining = %s'%(self.isIdle, played, remaining))
|
||||
self.service.player.toggleOnNext(bool(self.service.player.OnNextMode))
|
||||
|
||||
Thread(target=__chkIdle).start()
|
||||
if self.service.player.isPlaying() and self.service.player.isPseudoTV:
|
||||
Thread(target=__chkBackground).start()
|
||||
__chkResumeTime()
|
||||
__chkSleepTimer()
|
||||
__chkPlayback()
|
||||
__chkOverlay()
|
||||
__chkOnNext()
|
||||
|
||||
|
||||
def sleepTimer(self):
|
||||
self.log('sleepTimer')
|
||||
sec = 0
|
||||
cnx = False
|
||||
inc = int(100/FIFTEEN)
|
||||
xbmc.playSFX(NOTE_WAV)
|
||||
dia = DIALOG.progressDialog(message=LANGUAGE(30078))
|
||||
while not self.abortRequested() and (sec < FIFTEEN):
|
||||
sec += 1
|
||||
msg = '%s\n%s'%(LANGUAGE(32039),LANGUAGE(32040)%(FIFTEEN-sec))
|
||||
dia = DIALOG.progressDialog((inc*sec),dia, msg)
|
||||
if self.waitForAbort(1.0) or dia is None:
|
||||
cnx = True
|
||||
break
|
||||
DIALOG.progressDialog(100,dia)
|
||||
return not bool(cnx)
|
||||
|
||||
|
||||
def onNotification(self, sender, method, data):
|
||||
self.log("onNotification, sender %s - method: %s - data: %s" % (sender, method, data))
|
||||
|
||||
|
||||
def onSettingsChanged(self):
|
||||
self.log('onSettingsChanged')
|
||||
if self.service: timerit(self.onSettingsChangedTimer)(FIFTEEN)
|
||||
|
||||
|
||||
def onSettingsChangedTimer(self):
|
||||
self.log('onSettingsChangedTimer')
|
||||
self.service.tasks._que(self._onSettingsChanged,1)
|
||||
|
||||
|
||||
def _onSettingsChanged(self):
|
||||
with PROPERTIES.interruptActivity():
|
||||
self.log('_onSettingsChanged')
|
||||
self.service.currentSettings = self.service.tasks.chkSettingsChange(self.service.currentSettings) #check for settings change, take action if needed
|
||||
|
||||
|
||||
class Service():
|
||||
lock = Lock()
|
||||
currentSettings = []
|
||||
pendingSuspend = PROPERTIES.setPendingSuspend(False)
|
||||
pendingInterrupt = PROPERTIES.setPendingInterrupt(False)
|
||||
pendingShutdown = PROPERTIES.setPendingShutdown(False)
|
||||
pendingRestart = PROPERTIES.setPendingRestart(False)
|
||||
|
||||
def __init__(self):
|
||||
self.log('__init__')
|
||||
self.jsonRPC = JSONRPC(service=self)
|
||||
self.player = Player(service=self)
|
||||
self.monitor = Monitor(service=self)
|
||||
self.tasks = Tasks(service=self)
|
||||
|
||||
self.tasks.service = self
|
||||
self.monitor.service = self
|
||||
self.player.service = self
|
||||
self.jsonRPC.service = self
|
||||
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
return log('%s: %s'%(self.__class__.__name__,msg),level)
|
||||
|
||||
|
||||
def __playing(self) -> bool:
|
||||
if self.player.isPlaying() and not self.player.runWhilePlaying: return True
|
||||
return False
|
||||
|
||||
|
||||
def __shutdown(self, wait=1.0) -> bool:
|
||||
pendingShutdown = (self.monitor.waitForAbort(wait) | PROPERTIES.isPendingShutdown())
|
||||
if self.pendingShutdown != pendingShutdown:
|
||||
self.pendingShutdown = pendingShutdown
|
||||
self.log('__shutdown, pendingShutdown = %s, wait = %s'%(self.pendingShutdown,wait))
|
||||
return self.pendingShutdown
|
||||
|
||||
|
||||
def __restart(self) -> bool:
|
||||
pendingRestart = (self.pendingRestart | PROPERTIES.isPendingRestart())
|
||||
if self.pendingRestart != pendingRestart:
|
||||
self.pendingRestart = pendingRestart
|
||||
self.log('__restart, pendingRestart = %s'%(self.pendingRestart))
|
||||
return self.pendingRestart
|
||||
|
||||
|
||||
def _interrupt(self) -> bool: #break
|
||||
pendingInterrupt = (self.pendingShutdown | self.pendingRestart | self.__playing() | PROPERTIES.isInterruptActivity() | BUILTIN.isScanning())
|
||||
if pendingInterrupt != self.pendingInterrupt:
|
||||
self.pendingInterrupt = PROPERTIES.setPendingInterrupt(pendingInterrupt)
|
||||
self.log('_interrupt, pendingInterrupt = %s'%(self.pendingInterrupt))
|
||||
return self.pendingInterrupt
|
||||
|
||||
|
||||
def _suspend(self) -> bool: #continue
|
||||
pendingSuspend = (PROPERTIES.isSuspendActivity() | BUILTIN.isSettingsOpened())
|
||||
if pendingSuspend != self.pendingSuspend:
|
||||
self.pendingSuspend = PROPERTIES.setPendingSuspend(pendingSuspend)
|
||||
self.log('_suspend, pendingSuspend = %s'%(self.pendingSuspend))
|
||||
return self.pendingSuspend
|
||||
|
||||
|
||||
def __tasks(self):
|
||||
# if SETTINGS.hasWizardRun():
|
||||
self.tasks._chkEpochTimer('chkQueTimer',self.tasks._chkQueTimer,FIFTEEN)
|
||||
|
||||
|
||||
def _start(self):
|
||||
self.log('_start')
|
||||
if DIALOG.notificationWait('%s...'%(LANGUAGE(32054)),wait=OSD_TIMER):
|
||||
self.tasks._initialize()
|
||||
if self.player.isPlaying(): self.player.onAVStarted()
|
||||
while not self.monitor.abortRequested():
|
||||
self.monitor.chkIdle()
|
||||
if self.__shutdown(): break
|
||||
elif self.__restart(): break
|
||||
else: self.__tasks()
|
||||
return self._stop(self.pendingRestart)
|
||||
|
||||
|
||||
def _stop(self, pendingRestart: bool=False):
|
||||
if self.player.isPlaying(): self.player.onPlayBackStopped()
|
||||
with PROPERTIES.interruptActivity():
|
||||
for thread in thread_enumerate():
|
||||
if thread.name != "MainThread" and thread.is_alive():
|
||||
if hasattr(thread, 'cancel'): thread.cancel()
|
||||
try: thread.join(1.0)
|
||||
except: pass
|
||||
self.log('_stop, closing %s...'%(thread.name))
|
||||
return pendingRestart
|
||||
@@ -0,0 +1,29 @@
|
||||
# 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 service import Service
|
||||
|
||||
if __name__ == '__main__':
|
||||
pendingRestart = Service()._start()
|
||||
if pendingRestart:
|
||||
DIALOG.notificationWait(LANGUAGE(32124))
|
||||
Service()._start()
|
||||
else:
|
||||
sys.exit()
|
||||
@@ -0,0 +1,40 @@
|
||||
# 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 *
|
||||
|
||||
# class Service:
|
||||
# from jsonrpc import JSONRPC
|
||||
# monitor = MONITOR()
|
||||
# jsonRPC = JSONRPC()
|
||||
# def _interrupt(self) -> bool:
|
||||
# return PROPERTIES.isPendingInterrupt()
|
||||
# def _suspend(self) -> bool:
|
||||
# return PROPERTIES.isPendingSuspend()
|
||||
|
||||
# class Skin:
|
||||
# def __init__(self, service=None):
|
||||
# if service is None: service = Service()
|
||||
# self.jsonRPC = service.jsonRPC
|
||||
|
||||
# #todo match kodi skin color scheme/profile by parsing json values and creating skin vars.
|
||||
|
||||
# lookandfeel.skincolors
|
||||
# {"jsonrpc":"2.0","method":"Settings.GetSettingValue","params":{"setting":"lookandfeel.skincolors"}.get('value')
|
||||
# {"jsonrpc":"2.0","method":"Files.GetDirectory","params":{"directory":"special://skin/colors/"}..get('files')
|
||||
@@ -0,0 +1,403 @@
|
||||
# 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 cqueue import *
|
||||
from library import Library
|
||||
from autotune import Autotune
|
||||
from builder import Builder
|
||||
from backup import Backup
|
||||
from multiroom import Multiroom
|
||||
from server import HTTP
|
||||
|
||||
class Tasks():
|
||||
def __init__(self, service):
|
||||
self.service = service
|
||||
self.jsonRPC = service.jsonRPC
|
||||
self.player = service.player
|
||||
self.monitor = service.monitor
|
||||
self.cache = SETTINGS.cache
|
||||
self.quePriority = CustomQueue(priority=True,service=self.service)
|
||||
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
return log('%s: %s'%(self.__class__.__name__,msg),level)
|
||||
|
||||
|
||||
def _que(self, func, priority=-1, *args, **kwargs):# priority -1 autostack, 1 Highest, 5 Lowest
|
||||
if priority == -1: priority = self.quePriority.qsize + 1
|
||||
self.log('_que, priority = %s, func = %s, args = %s, kwargs = %s' % (priority,func.__name__, args, kwargs))
|
||||
self.quePriority._push((func, args, kwargs), priority)
|
||||
|
||||
|
||||
def _initialize(self):
|
||||
tasks = [self.chkInstanceID,
|
||||
self.chkSettings,
|
||||
self.chkDirs,
|
||||
self.chkWelcome,
|
||||
self.chkDebugging,
|
||||
self.chkBackup,
|
||||
self.chkHTTP,
|
||||
self.chkPVRBackend,]
|
||||
for func in tasks: self._que(func,1)
|
||||
self.log('_initialize, finished...')
|
||||
|
||||
|
||||
def chkSettings(self):
|
||||
self.service.currentSettings = dict(SETTINGS.getCurrentSettings())
|
||||
self.log('chkSettings, currentSettings = %s'%(self.service.currentSettings))
|
||||
|
||||
|
||||
def chkInstanceID(self):
|
||||
self.log('chkInstanceID')
|
||||
PROPERTIES.getInstanceID()
|
||||
|
||||
|
||||
@cacheit(expiration=datetime.timedelta(days=28), checksum=1)
|
||||
def chkWelcome(self):
|
||||
hasAutotuned = SETTINGS.hasAutotuned()
|
||||
self.log('chkWelcome, hasAutotuned = %s'%(hasAutotuned))
|
||||
if not hasAutotuned:
|
||||
return BUILTIN.executescript('special://home/addons/%s/resources/lib/utilities.py, Show_Wiki_QR'%(ADDON_ID))
|
||||
|
||||
|
||||
def chkDebugging(self):
|
||||
self.log('chkDebugging')
|
||||
if SETTINGS.getSettingBool('Debug_Enable'):
|
||||
if DIALOG.yesnoDialog(LANGUAGE(32142),autoclose=4):
|
||||
self.log('_chkDebugging, disabling debugging.')
|
||||
SETTINGS.setSettingBool('Debug_Enable',False)
|
||||
DIALOG.notificationDialog(LANGUAGE(32025))
|
||||
self.jsonRPC.toggleShowLog(SETTINGS.getSettingBool('Debug_Enable'))
|
||||
|
||||
|
||||
def chkBackup(self):
|
||||
self.log('chkBackup')
|
||||
Backup().hasBackup()
|
||||
|
||||
|
||||
def chkHTTP(self):
|
||||
self.log('chkHTTP')
|
||||
HTTP(service=self.service)
|
||||
|
||||
|
||||
def chkServers(self):
|
||||
self.log('chkServers')
|
||||
Multiroom(service=self.service).chkServers()
|
||||
|
||||
|
||||
def chkPVRBackend(self):
|
||||
self.log('chkPVRBackend')
|
||||
if hasAddon(PVR_CLIENT_ID,True,True,True,True):
|
||||
if not SETTINGS.hasPVRInstance():
|
||||
SETTINGS.setPVRPath(USER_LOC, SETTINGS.getFriendlyName())
|
||||
|
||||
|
||||
def _chkQueTimer(self):
|
||||
self._chkEpochTimer('chkVersion' , self.chkVersion , 21600)
|
||||
self._chkEpochTimer('chkKodiSettings' , self.chkKodiSettings , 3600)
|
||||
self._chkEpochTimer('chkServers' , self.chkServers , 300)
|
||||
self._chkEpochTimer('chkDiscovery' , self.chkDiscovery , 300)
|
||||
self._chkEpochTimer('chkRecommended' , self.chkRecommended , 900)
|
||||
self._chkEpochTimer('chkLibrary' , self.chkLibrary , 3600)
|
||||
|
||||
self._chkEpochTimer('chkFiles' , self.chkFiles , 300)
|
||||
self._chkEpochTimer('chkURLQUE' , self.chkURLQUE , 300)
|
||||
self._chkEpochTimer('chkJSONQUE' , self.chkJSONQUE , 300)
|
||||
self._chkEpochTimer('chkLOGOQUE' , self.chkLOGOQUE , 600)
|
||||
|
||||
self._chkPropTimer('chkPVRRefresh' , self.chkPVRRefresh , 1)
|
||||
self._chkPropTimer('chkFillers' , self.chkFillers , 2)
|
||||
self._chkPropTimer('chkUpdate' , self.chkUpdate , 3)
|
||||
|
||||
|
||||
def _chkEpochTimer(self, key, func, runevery=900, priority=-1, nextrun=None, *args, **kwargs):
|
||||
if nextrun is None: nextrun = PROPERTIES.getPropertyInt(key, default=0) # nextrun == 0 => force que
|
||||
epoch = int(time.time())
|
||||
if epoch >= nextrun:
|
||||
self.log('_chkEpochTimer, key = %s, last run %s' % (key, epoch - nextrun))
|
||||
PROPERTIES.setPropertyInt(key, (epoch + runevery))
|
||||
return self._que(func, priority, *args, **kwargs)
|
||||
|
||||
|
||||
def _chkPropTimer(self, key, func, priority=-1, *args, **kwargs):
|
||||
key = '%s.%s' % (ADDON_ID, key)
|
||||
if PROPERTIES.getEXTPropertyBool(key):
|
||||
self.log('_chkPropTimer, key = %s' % (key))
|
||||
PROPERTIES.clrEXTProperty(key)
|
||||
self._que(func, priority, *args, **kwargs)
|
||||
|
||||
|
||||
@cacheit(expiration=datetime.timedelta(minutes=10))
|
||||
def getOnlineVersion(self):
|
||||
try: ONLINE_VERSION = re.compile('" version="(.+?)" name="%s"'%(ADDON_NAME)).findall(str(requestURL(ADDON_URL)))[0]
|
||||
except: ONLINE_VERSION = ADDON_VERSION
|
||||
return ONLINE_VERSION
|
||||
|
||||
|
||||
def chkVersion(self):
|
||||
update = False
|
||||
ONLINE_VERSION = self.getOnlineVersion()
|
||||
if ADDON_VERSION < ONLINE_VERSION:
|
||||
update = True
|
||||
DIALOG.notificationDialog(LANGUAGE(30073)%(ONLINE_VERSION))
|
||||
elif ADDON_VERSION > (SETTINGS.getCacheSetting('lastVersion', checksum=ADDON_VERSION) or '0.0.0'):
|
||||
SETTINGS.setCacheSetting('lastVersion',ADDON_VERSION, checksum=ADDON_VERSION)
|
||||
BUILTIN.executescript('special://home/addons/%s/resources/lib/utilities.py, Show_Changelog'%(ADDON_ID))
|
||||
self.log('chkVersion, update = %s, installed version = %s, online version = %s'%(update,ADDON_VERSION,ONLINE_VERSION))
|
||||
SETTINGS.setSetting('Update_Status',{'True':'[COLOR=yellow]%s [B]v.%s[/B][/COLOR]'%(LANGUAGE(32168),ONLINE_VERSION),'False':'None'}[str(update)])
|
||||
|
||||
|
||||
def chkKodiSettings(self):
|
||||
self.log('chkKodiSettings')
|
||||
MIN_GUIDEDAYS = SETTINGS.setSettingInt('Min_Days' ,self.jsonRPC.getSettingValue('epg.pastdaystodisplay' ,default=1))
|
||||
MAX_GUIDEDAYS = SETTINGS.setSettingInt('Max_Days' ,self.jsonRPC.getSettingValue('epg.futuredaystodisplay' ,default=3))
|
||||
OSD_TIMER = SETTINGS.setSettingInt('OSD_Timer',self.jsonRPC.getSettingValue('pvrmenu.displaychannelinfo',default=5))
|
||||
|
||||
|
||||
def chkDirs(self):
|
||||
self.log('chkDirs')
|
||||
[FileAccess.makedirs(folder) for folder in [LOGO_LOC,FILLER_LOC,TEMP_LOC] if not FileAccess.exists(os.path.join(folder,''))]
|
||||
|
||||
|
||||
def chkFiles(self):
|
||||
self.log('chkFiles')
|
||||
self.chkDirs()
|
||||
if not (FileAccess.exists(LIBRARYFLEPATH) & FileAccess.exists(CHANNELFLEPATH) & FileAccess.exists(M3UFLEPATH) & FileAccess.exists(XMLTVFLEPATH) & FileAccess.exists(GENREFLEPATH)): self._que(self.chkLibrary,2)
|
||||
|
||||
|
||||
def chkDiscovery(self):
|
||||
self.log('chkDiscovery')
|
||||
timerit(Multiroom(service=self.service)._chkDiscovery)(1.0)
|
||||
|
||||
|
||||
def chkRecommended(self):
|
||||
self.log('chkRecommended')
|
||||
try:
|
||||
library = Library(service=self.service)
|
||||
library.searchRecommended()
|
||||
del library
|
||||
except Exception as e: self.log('chkRecommended failed! %s'%(e), xbmc.LOGERROR)
|
||||
|
||||
|
||||
def chkLibrary(self, force=PROPERTIES.getPropertyBool('ForceLibrary')):
|
||||
try:
|
||||
library = Library(service=self.service)
|
||||
library.importPrompt() #refactor feature
|
||||
complete = library.updateLibrary(force)
|
||||
del library
|
||||
if complete:
|
||||
self._que(self.chkChannels,3)
|
||||
if force: PROPERTIES.setPropertyBool('ForceLibrary',False)
|
||||
else:
|
||||
self._que(self.chkLibrary,2,force)
|
||||
self.log('chkLibrary, force = %s, complete = %s'%(force,complete))
|
||||
except Exception as e: self.log('chkLibrary failed! %s'%(e), xbmc.LOGERROR)
|
||||
|
||||
|
||||
def chkUpdate(self):
|
||||
ids = PROPERTIES.getUpdateChannels()
|
||||
if ids:
|
||||
channels = self.getVerifiedChannels()
|
||||
channels = [citem for id in ids for citem in channels if citem.get('id') == id]
|
||||
self.log('chkUpdate, channels = %s\nid = %s'%(len(channels),ids))
|
||||
self._que(self.chkChannels,3,channels)
|
||||
|
||||
|
||||
def chkChannels(self, channels: list=[]):
|
||||
save = False
|
||||
complete = False
|
||||
builder = Builder(service=self.service)
|
||||
hasAutotuned = SETTINGS.hasAutotuned()
|
||||
hasFirstRun = PROPERTIES.hasFirstRun()
|
||||
hasEnabledServers = PROPERTIES.hasEnabledServers()
|
||||
|
||||
if not channels:
|
||||
save = True
|
||||
channels = builder.getVerifiedChannels()
|
||||
SETTINGS.setSetting('Select_Channels','[B]%s[/B] Channels'%(len(channels)))
|
||||
PROPERTIES.setChannels(len(channels) > 0)
|
||||
self.service.currentChannels = channels #update service channels
|
||||
|
||||
if len(channels) > 0:
|
||||
if not hasAutotuned: SETTINGS.setAutotuned(complete)
|
||||
complete, updated = builder.build(channels)
|
||||
self.log('chkChannels, channels = %s, complete = %s, updated = %s'%(len(channels),complete,updated))
|
||||
if complete:
|
||||
if save:
|
||||
builder.channels.setChannels(channels)
|
||||
if updated: PROPERTIES.setPropTimer('chkPVRRefresh')
|
||||
if SETTINGS.getSettingBool('Build_Filler_Folders'): self._que(self.chkFillers,2,channels)
|
||||
else: self._que(self.chkChannels,3,channels)
|
||||
elif not hasAutotuned: return SETTINGS.setAutotuned(Autotune()._runTune())
|
||||
elif hasEnabledServers: return PROPERTIES.setPropTimer('chkPVRRefresh')
|
||||
del builder
|
||||
if not hasFirstRun: PROPERTIES.setFirstRun(complete)
|
||||
|
||||
|
||||
def chkLOGOQUE(self):
|
||||
if not PROPERTIES.isRunning('chkLOGOQUE') and PROPERTIES.hasFirstRun():
|
||||
with PROPERTIES.chkRunning('chkLOGOQUE'):
|
||||
updated = False
|
||||
library = Library(service=self.service)
|
||||
resources = library.resources
|
||||
queuePool = (SETTINGS.getCacheSetting('queueLOGO', json_data=True) or {})
|
||||
params = randomShuffle(queuePool.get('params',[]))
|
||||
for i in list(range(QUEUE_CHUNK)):
|
||||
if self.service._interrupt():
|
||||
self.log("chkLOGOQUE, _interrupt")
|
||||
break
|
||||
elif len(params) > 0:
|
||||
param = params.pop(0)
|
||||
updated = True
|
||||
self.log("chkLOGOQUE, queuing = %s\n%s"%(len(params),param))
|
||||
if param.get('name','').startswith('getLogoResources'):
|
||||
self._que(resources.getLogoResources, 10+i, *param.get('args',()), **param.get('kwargs',{}))
|
||||
elif param.get('name','').startswith('getTVShowLogo'):
|
||||
self._que(resources.getTVShowLogo, 10+i, *param.get('args',()), **param.get('kwargs',{}))
|
||||
queuePool['params'] = setDictLST(params)
|
||||
if updated and len(queuePool['params']) == 0: PROPERTIES.setPropertyBool('ForceLibrary',True)
|
||||
self.log('chkLOGOQUE, remaining = %s'%(len(queuePool['params'])))
|
||||
SETTINGS.setCacheSetting('queueLOGO', queuePool, json_data=True)
|
||||
del library
|
||||
|
||||
|
||||
def chkJSONQUE(self):
|
||||
if not PROPERTIES.isRunning('chkJSONQUE') and PROPERTIES.hasFirstRun():
|
||||
with PROPERTIES.chkRunning('chkJSONQUE'):
|
||||
queuePool = (SETTINGS.getCacheSetting('queueJSON', json_data=True) or {})
|
||||
params = queuePool.get('params',[])
|
||||
for i in list(range(QUEUE_CHUNK)):
|
||||
if self.service._interrupt():
|
||||
self.log("chkJSONQUE, _interrupt")
|
||||
break
|
||||
elif len(params) > 0:
|
||||
param = params.pop(0)
|
||||
self.log("chkJSONQUE, queuing = %s\n%s"%(len(params),param))
|
||||
self._que(self.jsonRPC.sendJSON,5+1, param)
|
||||
queuePool['params'] = setDictLST(params)
|
||||
self.log('chkJSONQUE, remaining = %s'%(len(queuePool['params'])))
|
||||
SETTINGS.setCacheSetting('queueJSON', queuePool, json_data=True)
|
||||
|
||||
|
||||
def chkURLQUE(self):
|
||||
if not PROPERTIES.isRunning('chkURLQUE') and PROPERTIES.hasFirstRun():
|
||||
with PROPERTIES.chkRunning('chkURLQUE'):
|
||||
queuePool = (SETTINGS.getCacheSetting('queueURL', json_data=True) or {})
|
||||
params = queuePool.get('params',[])
|
||||
for i in list(range(QUEUE_CHUNK)):
|
||||
if self.service._interrupt():
|
||||
self.log("chkURLQUE, _interrupt")
|
||||
break
|
||||
elif len(params) > 0:
|
||||
param = params.pop(0)
|
||||
self.log("chkURLQUE, queuing = %s\n%s"%(len(params),param))
|
||||
self._que(requestURL,1, param)
|
||||
queuePool['params'] = setDictLST(params)
|
||||
self.log('chkURLQUE, remaining = %s'%(len(queuePool['params'])))
|
||||
SETTINGS.setCacheSetting('queueURL', queuePool, json_data=True)
|
||||
|
||||
|
||||
def chkPVRRefresh(self):
|
||||
self.log('chkPVRRefresh')
|
||||
self._que(self.chkPVRToggle,1)
|
||||
|
||||
|
||||
def chkPVRToggle(self):
|
||||
if self.service.monitor.isIdle and not (self.player.isPlaying() | BUILTIN.isScanning() | BUILTIN.isRecording()): togglePVR(False,True)
|
||||
else: timerit(PROPERTIES.setPropTimer)(FIFTEEN,['chkPVRRefresh'])
|
||||
|
||||
|
||||
def chkFillers(self, channels=[]):
|
||||
self.log('chkFillers')
|
||||
if not channels: channels = self.getVerifiedChannels()
|
||||
pDialog = DIALOG.progressBGDialog(header='%s, %s'%(ADDON_NAME,LANGUAGE(32179)))
|
||||
for idx, ftype in enumerate(FILLER_TYPES):
|
||||
if not FileAccess.exists(os.path.join(FILLER_LOC,ftype.lower(),'')):
|
||||
pDialog = DIALOG.progressBGDialog(int(idx*50//len(ftype)), pDialog, message='%s: %s'%(ftype,int(idx*100//len(ftype)))+'%', header='%s, %s'%(ADDON_NAME,LANGUAGE(32179)))
|
||||
FileAccess.makedirs(os.path.join(FILLER_LOC,ftype.lower(),''))
|
||||
|
||||
genres = self.getGenreNames()
|
||||
for idx, citem in enumerate(channels):
|
||||
for ftype in FILLER_TYPES[1:]:
|
||||
for genre in genres:
|
||||
if not FileAccess.exists(os.path.join(FILLER_LOC,ftype.lower(),genre.lower(),'')):
|
||||
pDialog = DIALOG.progressBGDialog(int(idx*50//len(channels)), pDialog, message='%s: %s'%(genre,int(idx*100//len(channels)))+'%', header='%s, %s'%(ADDON_NAME,LANGUAGE(32179)))
|
||||
FileAccess.makedirs(os.path.join(FILLER_LOC,ftype.lower(),genre.lower()))
|
||||
|
||||
if not FileAccess.exists(os.path.join(FILLER_LOC,ftype.lower(),citem.get('name','').lower())):
|
||||
if ftype.lower() == 'adverts': IGNORE = IGNORE_CHTYPE + MOVIE_CHTYPE
|
||||
else: IGNORE = IGNORE_CHTYPE
|
||||
if citem.get('name') and not citem.get('radio',False) and citem.get('type') not in IGNORE:
|
||||
pDialog = DIALOG.progressBGDialog(int(idx*50//len(channels)), pDialog, message='%s: %s'%(citem.get('name'),int(idx*100//len(channels)))+'%', header='%s, %s'%(ADDON_NAME,LANGUAGE(32179)))
|
||||
FileAccess.makedirs(os.path.join(FILLER_LOC,ftype.lower(),citem['name'].lower()))
|
||||
pDialog = DIALOG.progressBGDialog(100, pDialog, message=LANGUAGE(32025), header='%s, %s'%(ADDON_NAME,LANGUAGE(32179)))
|
||||
|
||||
|
||||
@cacheit(expiration=datetime.timedelta(minutes=15),json_data=False)
|
||||
def getGenreNames(self):
|
||||
self.log('getGenres')
|
||||
try:
|
||||
library = Library(self.service)
|
||||
tvgenres = library.getTVGenres()
|
||||
moviegenres = library.getMovieGenres()
|
||||
genres = set([tvgenre.get('name') for tvgenre in tvgenres if tvgenre.get('name')] + [movgenre.get('name') for movgenre in moviegenres if movgenre.get('name')])
|
||||
del library
|
||||
return list(genres)
|
||||
except Exception as e:
|
||||
self.log('getGenres failed! %s'%(e), xbmc.LOGERROR)
|
||||
return []
|
||||
|
||||
|
||||
def chkSettingsChange(self, settings={}):
|
||||
nSettings = dict(SETTINGS.getCurrentSettings())
|
||||
for setting, value in list(settings.items()):
|
||||
actions = {'User_Folder' :{'func':self.setUserPath ,'kwargs':{'old':value,'new':nSettings.get(setting)}},
|
||||
'Debug_Enable' :{'func':self.jsonRPC.toggleShowLog ,'kwargs':{'state':SETTINGS.getSettingBool('Debug_Enable')}},
|
||||
'Overlay_Enable' :{'func':PROPERTIES.setPendingRestart,'kwargs':{}},
|
||||
'Enable_OnInfo' :{'func':PROPERTIES.setPendingRestart,'kwargs':{}},
|
||||
'Disable_Trakt' :{'func':PROPERTIES.setPendingRestart,'kwargs':{}},
|
||||
'Rollback_Watched' :{'func':PROPERTIES.setPendingRestart,'kwargs':{}},
|
||||
'Store_Duration' :{'func':PROPERTIES.setPendingRestart,'kwargs':{}},
|
||||
'Seek_Tolerance' :{'func':PROPERTIES.setPendingRestart,'kwargs':{}},
|
||||
'Seek_Threshold' :{'func':PROPERTIES.setPendingRestart,'kwargs':{}},
|
||||
'Idle_Timer' :{'func':PROPERTIES.setPendingRestart,'kwargs':{}},
|
||||
'Run_While_Playing' :{'func':PROPERTIES.setPendingRestart,'kwargs':{}},
|
||||
'Restart_Percentage':{'func':PROPERTIES.setPendingRestart,'kwargs':{}},}
|
||||
|
||||
if nSettings.get(setting) != value and actions.get(setting):
|
||||
action = actions.get(setting)
|
||||
self.log('chkSettingsChange, detected change in %s: %s => %s\naction = %s'%(setting,value,nSettings.get(setting),action))
|
||||
self._que(action.get('func'),1,*action.get('args',()),**action.get('kwargs',{}))
|
||||
return nSettings
|
||||
|
||||
|
||||
def setUserPath(self, old, new):
|
||||
with PROPERTIES.interruptActivity():
|
||||
self.log('setUserPath, old = %s, new = %s'%(old,new))
|
||||
dia = DIALOG.progressDialog(message='%s\n%s'%(LANGUAGE(32050),old))
|
||||
FileAccess.copyFolder(old, new, dia)
|
||||
SETTINGS.setPVRPath(new,prompt=True,force=True)
|
||||
PROPERTIES.setPendingRestart()
|
||||
DIALOG.progressDialog(100, dia)
|
||||
|
||||
|
||||
def getVerifiedChannels(self):
|
||||
return Builder(service=self.service).getVerifiedChannels()
|
||||
@@ -0,0 +1,336 @@
|
||||
# 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 *
|
||||
|
||||
|
||||
class Utilities:
|
||||
def __init__(self, sysARG=sys.argv):
|
||||
self.log('__init__, sysARG = %s'%(sysARG))
|
||||
self.sysARG = sysARG
|
||||
|
||||
|
||||
def log(self, msg, level=xbmc.LOGDEBUG):
|
||||
return log('%s: %s'%(self.__class__.__name__,msg),level)
|
||||
|
||||
|
||||
def qrWiki(self):
|
||||
with PROPERTIES.suspendActivity():
|
||||
DIALOG.qrDialog(URL_WIKI,LANGUAGE(32216)%(ADDON_NAME,ADDON_AUTHOR))
|
||||
|
||||
|
||||
def qrSupport(self):
|
||||
with PROPERTIES.suspendActivity():
|
||||
DIALOG.qrDialog(URL_SUPPORT, LANGUAGE(30033)%(ADDON_NAME))
|
||||
|
||||
|
||||
def qrRemote(self):
|
||||
with PROPERTIES.suspendActivity():
|
||||
DIALOG.qrDialog('http://%s/%s'%(PROPERTIES.getRemoteHost(),'remote.html'), LANGUAGE(30165))
|
||||
|
||||
|
||||
def qrReadme(self):
|
||||
with PROPERTIES.suspendActivity():
|
||||
DIALOG.qrDialog(URL_README, LANGUAGE(32043)%(ADDON_NAME,ADDON_VERSION))
|
||||
|
||||
|
||||
def qrBonjourDL(self):
|
||||
with PROPERTIES.suspendActivity():
|
||||
DIALOG.qrDialog(URL_WIN_BONJOUR, LANGUAGE(32217))
|
||||
|
||||
|
||||
def showChangelog(self):
|
||||
try:
|
||||
def __addColor(text):
|
||||
text = text.replace('- Added' ,'[COLOR=green][B]- Added:[/B][/COLOR]')
|
||||
text = text.replace('- Introduced' ,'[COLOR=green][B]- Introduced:[/B][/COLOR]')
|
||||
text = text.replace('- Addressed' ,'[COLOR=green][B]- Addressed:[/B][/COLOR]')
|
||||
text = text.replace('- New!' ,'[COLOR=yellow][B]- New!:[/B][/COLOR]')
|
||||
text = text.replace('- Optimized' ,'[COLOR=yellow][B]- Optimized:[/B][/COLOR]')
|
||||
text = text.replace('- Improved' ,'[COLOR=yellow][B]- Improved:[/B][/COLOR]')
|
||||
text = text.replace('- Modified' ,'[COLOR=yellow][B]- Modified:[/B][/COLOR]')
|
||||
text = text.replace('- Enhanced' ,'[COLOR=yellow][B]- Enhanced:[/B][/COLOR]')
|
||||
text = text.replace('- Refactored' ,'[COLOR=yellow][B]- Refactored:[/B][/COLOR]')
|
||||
text = text.replace('- Reworked' ,'[COLOR=yellow][B]- Reworked:[/B][/COLOR]')
|
||||
text = text.replace('- Tweaked' ,'[COLOR=yellow][B]- Tweaked:[/B][/COLOR]')
|
||||
text = text.replace('- Updated' ,'[COLOR=yellow][B]- Updated:[/B][/COLOR]')
|
||||
text = text.replace('- Changed' ,'[COLOR=yellow][B]- Changed:[/B][/COLOR]')
|
||||
text = text.replace('- Corrected' ,'[COLOR=yellow][B]- Corrected:[/B][/COLOR]')
|
||||
text = text.replace('- Proper' ,'[COLOR=yellow][B]- Proper:[/B][/COLOR]')
|
||||
text = text.replace('- Included' ,'[COLOR=yellow][B]- Changed:[/B][/COLOR]')
|
||||
text = text.replace('- Notice' ,'[COLOR=orange][B]- Notice:[/B][/COLOR]')
|
||||
text = text.replace('- Fixed' ,'[COLOR=orange][B]- Fixed:[/B][/COLOR]')
|
||||
text = text.replace('- Resolved' ,'[COLOR=orange][B]- Resolved:[/B][/COLOR]')
|
||||
text = text.replace('- Removed' ,'[COLOR=red][B]- Removed:[/B][/COLOR]')
|
||||
text = text.replace('- Excluded' ,'[COLOR=red][B]- Excluded:[/B][/COLOR]')
|
||||
text = text.replace('- Deprecated' ,'[COLOR=red][B]- Deprecated:[/B][/COLOR]')
|
||||
text = text.replace('- Important' ,'[COLOR=red][B]- Important:[/B][/COLOR]')
|
||||
text = text.replace('- Warning' ,'[COLOR=red][B]- Warning:[/B][/COLOR]')
|
||||
return text
|
||||
|
||||
with BUILTIN.busy_dialog():
|
||||
fle = FileAccess.open(CHANGELOG_FLE, "r")
|
||||
txt = __addColor(fle.read())
|
||||
fle.close()
|
||||
DIALOG.textviewer(txt, heading=(LANGUAGE(32045)%(ADDON_NAME,ADDON_VERSION)),usemono=True)
|
||||
except Exception as e: self.log('showChangelog failed! %s'%(e), xbmc.LOGERROR)
|
||||
|
||||
|
||||
def qrDebug(self):
|
||||
def __cleanLog(content):
|
||||
content = re.sub('//.+?:.+?@' ,'//USER:PASSWORD@' , content)
|
||||
content = re.sub('<user>.+?</user>' ,'<user>USER</user>' , content)
|
||||
content = re.sub('<pass>.+?</pass>' ,'<pass>PASSWORD</pass>', content)
|
||||
content = re.sub(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", '0.0.0.0' , content)
|
||||
return content
|
||||
|
||||
def __cleanPayload(payload):
|
||||
def __getDebug(payload): #only post errors
|
||||
debug = payload.get('debug',{})
|
||||
# [debug.pop(key) for key in list(debug.keys()) if key in ['LOGDEBUG','LOGINFO']]
|
||||
return debug
|
||||
|
||||
payload['debug'] = loadJSON(__cleanLog(dumpJSON(__getDebug(payload),idnt=4)))
|
||||
payload['channels'] = loadJSON(__cleanLog(dumpJSON(payload.get('channels',[]),idnt=4)))
|
||||
payload['m3u'] = loadJSON(__cleanLog(dumpJSON(payload.get('m3u',[]),idnt=4)))
|
||||
[payload.pop(key) for key in ['host','remotes','bonjour','library','servers'] if key in payload]
|
||||
return payload
|
||||
|
||||
def __postLog(data):
|
||||
try:
|
||||
session = requests.Session()
|
||||
response = session.post('https://paste.kodi.tv/' + 'documents', data=data.encode('utf-8'), headers={'User-Agent':'%s: %s'%(ADDON_ID, ADDON_VERSION)})
|
||||
if 'key' in response.json():
|
||||
return True, 'https://paste.kodi.tv/' + response.json()['key']
|
||||
elif 'message' in response.json():
|
||||
self.log('qrDebug, upload failed, paste may be too large')
|
||||
return False, response.json()['message']
|
||||
else:
|
||||
self.log('qrDebug failed! %s'%response.text)
|
||||
return False, LANGUAGE(30191)
|
||||
except:
|
||||
self.log('qrDebug, unable to retrieve the paste url')
|
||||
return False, LANGUAGE(30190)
|
||||
|
||||
with BUILTIN.busy_dialog():
|
||||
payload = SETTINGS.getPayload(inclDebug=True)
|
||||
if not payload.get('debug',{}): return DIALOG.notificationDialog(LANGUAGE(32187))
|
||||
elif not DIALOG.yesnoDialog(message=LANGUAGE(32188)): return
|
||||
|
||||
with BUILTIN.busy_dialog():
|
||||
succes, data = __postLog(dumpJSON(__cleanPayload(payload),idnt=4))
|
||||
|
||||
if succes: DIALOG.qrDialog(data,LANGUAGE(32189)%(data))
|
||||
else: DIALOG.okDialog(LANGUAGE(32190)%(data))
|
||||
|
||||
|
||||
def _runCPUBench(self):
|
||||
with BUILTIN.busy_dialog():
|
||||
if hasAddon('script.pystone.benchmark',install=True, enable=True, notify=True):
|
||||
return BUILTIN.executebuiltin('RunScript(script.pystone.benchmark)')
|
||||
|
||||
|
||||
def _runIOBench(self):
|
||||
with BUILTIN.busy_dialog():
|
||||
if hasAddon('script.io.benchmark',install=True, enable=True, notify=True):
|
||||
return BUILTIN.executebuiltin('RunScript(script.io.benchmark,%s)'%(escapeString(f'path={USER_LOC}')))
|
||||
|
||||
|
||||
def _runLogger(self):
|
||||
with BUILTIN.busy_dialog():
|
||||
if hasAddon('script.kodi.loguploader',install=True, enable=True, notify=True):
|
||||
return BUILTIN.executebuiltin('RunScript(script.kodi.loguploader)')
|
||||
|
||||
|
||||
def _runCleanup(self, full=False):
|
||||
self.log('_runCleanup, full = %s'%(full))
|
||||
files = {LANGUAGE(30094):M3UFLEPATH, #"M3U"
|
||||
LANGUAGE(30095):XMLTVFLEPATH, #"XMLTV"
|
||||
LANGUAGE(30096):GENREFLEPATH, #"Genre"
|
||||
LANGUAGE(30108):CHANNELFLEPATH,#"Channels"
|
||||
LANGUAGE(32041):LIBRARYFLEPATH}#"Library"
|
||||
|
||||
keys = list(files.keys())
|
||||
if not full: keys = keys[:2]
|
||||
if DIALOG.yesnoDialog('%s ?'%(msg)):
|
||||
with BUILTIN.busy_dialog(), PROPERTIES.interruptActivity():
|
||||
for key in keys:
|
||||
if FileAccess.delete(files[key]): DIALOG.notificationDialog(LANGUAGE(32127)%(key.replace(':','')))
|
||||
else: DIALOG.notificationDialog('%s %s'%((LANGUAGE(32127)%(key.replace(':',''))),LANGUAGE(32052)))
|
||||
self._runUpdate(full)
|
||||
|
||||
|
||||
def _runReload(self):
|
||||
if DIALOG.yesnoDialog('%s?'%(LANGUAGE(32121)%(xbmcaddon.Addon(PVR_CLIENT_ID).getAddonInfo('name')))): PROPERTIES.setPropTimer('chkPVRRefresh')
|
||||
|
||||
|
||||
def _runRestart(self):
|
||||
return PROPERTIES.setPendingRestart()
|
||||
|
||||
|
||||
def _runFillers(self):
|
||||
return PROPERTIES.setPropTimer('chkFillers')
|
||||
|
||||
|
||||
def _runLibrary(self):
|
||||
PROPERTIES.setPropertyBool('ForceLibrary',True)
|
||||
PROPERTIES.setEpochTimer('chkLibrary')
|
||||
DIALOG.notificationDialog('%s %s'%(LANGUAGE(30199),LANGUAGE(30200)))
|
||||
|
||||
|
||||
def _runAutotune(self):
|
||||
self.log('_runAutotune')
|
||||
SETTINGS.setAutotuned(False)
|
||||
PROPERTIES.setPropTimer('chkChannels')
|
||||
|
||||
|
||||
def _runUpdate(self, full=False):
|
||||
self.log('_runUpdate, full = %s'%(full))
|
||||
if full: PROPERTIES.setEpochTimer('chkLibrary')
|
||||
PROPERTIES.setEpochTimer('chkChannels')
|
||||
|
||||
|
||||
def buildMenu(self, select=None):
|
||||
items = [
|
||||
{'label':LANGUAGE(32117) ,'label2':LANGUAGE(32120),'icon':COLOR_LOGO,'func':self._runCleanup , 'hide':True ,'args':(False,)}, #"Rebuild M3U/XMLTV"
|
||||
{'label':LANGUAGE(32118) ,'label2':LANGUAGE(32119),'icon':COLOR_LOGO,'func':self._runCleanup , 'hide':True ,'args':(True,)}, #"Clean Start"
|
||||
{'label':LANGUAGE(32121)%(PVR_CLIENT_NAME),'label2':LANGUAGE(32122),'icon':COLOR_LOGO,'func':self._runReload , 'hide':False},#"Force PVR reload"
|
||||
{'label':LANGUAGE(32123) ,'label2':LANGUAGE(32124),'icon':COLOR_LOGO,'func':self._runRestart , 'hide':False},#"Force PTVL reload"
|
||||
{'label':LANGUAGE(32159) ,'label2':LANGUAGE(33159),'icon':COLOR_LOGO,'func':self._runLibrary , 'hide':False},
|
||||
{'label':LANGUAGE(32180) ,'label2':LANGUAGE(33180),'icon':COLOR_LOGO,'func':self._runFillers , 'hide':False},
|
||||
{'label':LANGUAGE(32181) ,'label2':LANGUAGE(33181),'icon':COLOR_LOGO,'func':self._runAutotune , 'hide':False},
|
||||
{'label':LANGUAGE(30205) ,'label2':LANGUAGE(30205),'icon':COLOR_LOGO,'func':self._runCPUBench , 'hide':False},
|
||||
{'label':LANGUAGE(30208) ,'label2':LANGUAGE(30208),'icon':COLOR_LOGO,'func':self._runIOBench , 'hide':False},
|
||||
]
|
||||
|
||||
with BUILTIN.busy_dialog():
|
||||
if not SETTINGS.getSettingBool('Debug_Enable'): items = [item for item in items if not item.get('hide',False)]
|
||||
listItems = [LISTITEMS.buildMenuListItem(item.get('label'),item.get('label2'),item.get('icon')) for item in sorted(items,key=itemgetter('label'))]
|
||||
if select is None: select = DIALOG.selectDialog(listItems, '%s - %s'%(ADDON_NAME,LANGUAGE(32126)),multi=False)
|
||||
|
||||
if not select is None:
|
||||
with PROPERTIES.interruptActivity():
|
||||
try:
|
||||
selectItem = [item for item in items if item.get('label') == listItems[select].getLabel()][0]
|
||||
self.log('buildMenu, selectItem = %s'%selectItem)
|
||||
if selectItem.get('args'): selectItem['func'](*selectItem['args'])
|
||||
else: selectItem['func']()
|
||||
except Exception as e:
|
||||
self.log("buildMenu, failed! %s"%(e), xbmc.LOGERROR)
|
||||
return DIALOG.notificationDialog(LANGUAGE(32000))
|
||||
else: SETTINGS.openSettings((6,1))
|
||||
|
||||
|
||||
def openChannelManager(self, chnum: int=1):
|
||||
self.log('openChannelManager, chnum = %s'%(chnum))
|
||||
if not PROPERTIES.isRunning('OVERLAY_MANAGER'):
|
||||
with PROPERTIES.chkRunning('OVERLAY_MANAGER'), PROPERTIES.interruptActivity():
|
||||
with BUILTIN.busy_dialog():
|
||||
from manager import Manager
|
||||
chmanager = Manager(MANAGER_XML, ADDON_PATH, "default", channel=chnum)
|
||||
del chmanager
|
||||
else: DIALOG.notificationDialog(LANGUAGE(32057)%(ADDON_NAME))
|
||||
|
||||
|
||||
def openPositionUtil(self, idx):
|
||||
self.log('openPositionUtil, idx = %s'%(idx))
|
||||
if not PROPERTIES.isRunning('OVERLAY_UTILITY'):
|
||||
with PROPERTIES.chkRunning('OVERLAY_UTILITY'), PROPERTIES.interruptActivity():
|
||||
with BUILTIN.busy_dialog():
|
||||
from overlaytool import OverlayTool
|
||||
overlaytool = OverlayTool(OVERLAYTOOL_XML, ADDON_PATH, "default", Focus_IDX=idx)
|
||||
del overlaytool
|
||||
|
||||
|
||||
def defaultChannels(self):
|
||||
self.log('defaultChannels')
|
||||
with BUILTIN.busy_dialog():
|
||||
values = SETTINGS.getSettingList('Select_server')
|
||||
values = [cleanLabel(value) for value in values]
|
||||
values.insert(0,LANGUAGE(30022)) #Auto
|
||||
values.insert(1,LANGUAGE(32069))
|
||||
select = DIALOG.selectDialog(values, LANGUAGE(30173), findItemsInLST(values, [SETTINGS.getSetting('Default_Channels')])[0], False, SELECT_DELAY, False)
|
||||
if not select is None: return SETTINGS.setSetting('Default_Channels',values[select])
|
||||
else: return SETTINGS.setSetting('Default_Channels',LANGUAGE(30022))
|
||||
|
||||
|
||||
def run(self):
|
||||
with BUILTIN.busy_dialog():
|
||||
ctl = (0,1)
|
||||
try: param = self.sysARG[1]
|
||||
except: param = None
|
||||
|
||||
#Channels
|
||||
if param.startswith('Channel_Manager'):
|
||||
ctl = (0,1)
|
||||
self.openChannelManager()
|
||||
elif param.startswith('Default_Channels'):
|
||||
ctl = (0,2)
|
||||
self.defaultChannels()
|
||||
|
||||
#Globals
|
||||
elif param.startswith('Move_Channelbug'):
|
||||
ctl = (3,15)
|
||||
self.openPositionUtil(1)
|
||||
elif param.startswith('Move_OnNext'):
|
||||
ctl = (3,15)
|
||||
self.openPositionUtil(2)
|
||||
|
||||
#Multi-Room
|
||||
elif param == 'Show_ZeroConf_QR':
|
||||
ctl = (5,5)
|
||||
self.qrBonjourDL()
|
||||
|
||||
#Misc. Scripts
|
||||
elif param == 'CPU_Bench':
|
||||
self._runCPUBench()
|
||||
elif param == 'IO_Bench':
|
||||
self._runIOBench()
|
||||
elif param == 'Logger':
|
||||
self._runLogger()
|
||||
|
||||
#Misc.Docs
|
||||
elif param == 'Utilities':
|
||||
ctl = (6,1)
|
||||
return self.buildMenu()
|
||||
elif param == 'Show_Wiki_QR':
|
||||
ctl = (6,4)
|
||||
return self.qrWiki()
|
||||
elif param == 'Show_Support_QR':
|
||||
ctl = (6,5)
|
||||
return self.qrSupport()
|
||||
elif param == 'Show_Remote_UI':
|
||||
ctl = (6,6)
|
||||
return self.qrRemote()
|
||||
elif param == 'Show_Changelog':
|
||||
ctl = (6,8)
|
||||
return self.showChangelog()
|
||||
|
||||
#Misc. Debug
|
||||
elif param == 'Debug_QR':
|
||||
ctl = (6,1)
|
||||
return self.qrDebug()
|
||||
|
||||
elif param == 'Run_Autotune':
|
||||
return self._runAutotune()
|
||||
|
||||
return SETTINGS.openSettings(ctl)
|
||||
|
||||
if __name__ == '__main__': timerit(Utilities(sys.argv).run)(0.1)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# 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 constants import *
|
||||
|
||||
#variables
|
||||
PAGE_LIMIT = int((REAL_SETTINGS.getSetting('Page_Limit') or "25"))
|
||||
MIN_GUIDEDAYS = int((REAL_SETTINGS.getSetting('Min_Days') or "1"))
|
||||
MAX_GUIDEDAYS = int((REAL_SETTINGS.getSetting('Max_Days') or "3"))
|
||||
OSD_TIMER = int((REAL_SETTINGS.getSetting('OSD_Timer') or "5"))
|
||||
EPG_ARTWORK = int((REAL_SETTINGS.getSetting('EPG_Artwork') or "0"))
|
||||
|
||||
#file paths
|
||||
USER_LOC = REAL_SETTINGS.getSetting('User_Folder')
|
||||
LOGO_LOC = os.path.join(USER_LOC,'logos')
|
||||
FILLER_LOC = os.path.join(USER_LOC,'fillers')
|
||||
M3UFLEPATH = os.path.join(USER_LOC,M3UFLE)
|
||||
XMLTVFLEPATH = os.path.join(USER_LOC,XMLTVFLE)
|
||||
GENREFLEPATH = os.path.join(USER_LOC,GENREFLE)
|
||||
PROVIDERFLEPATH = os.path.join(USER_LOC,PROVIDERFLE)
|
||||
CHANNELFLEPATH = os.path.join(USER_LOC,CHANNELFLE)
|
||||
LIBRARYFLEPATH = os.path.join(USER_LOC,LIBRARYFLE)
|
||||
@@ -0,0 +1,109 @@
|
||||
# Copyright (C) 2011 Jason Anderson
|
||||
# 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/>.
|
||||
|
||||
from globals import *
|
||||
from parsers import MP4Parser
|
||||
from parsers import AVIParser
|
||||
from parsers import MKVParser
|
||||
from parsers import FLVParser
|
||||
from parsers import TSParser
|
||||
from parsers import VFSParser
|
||||
from parsers import NFOParser
|
||||
from parsers import YTParser
|
||||
|
||||
EXTERNAL_PARSER = [NFOParser.NFOParser]
|
||||
try:
|
||||
import pymediainfo
|
||||
from parsers import MediaInfo
|
||||
EXTERNAL_PARSER.append(MediaInfo.MediaInfo)
|
||||
except: pass
|
||||
|
||||
try:
|
||||
import ffmpeg
|
||||
from parsers import FFProbe
|
||||
EXTERNAL_PARSER.append(FFProbe.FFProbe)
|
||||
except: pass
|
||||
|
||||
try:
|
||||
import hachoir
|
||||
from parsers import Hachoir
|
||||
EXTERNAL_PARSER.append(Hachoir.Hachoir)
|
||||
except: pass
|
||||
|
||||
try:
|
||||
import moviepy
|
||||
from parsers import MoviePY
|
||||
from numpy.core._multiarray_umath import *
|
||||
EXTERNAL_PARSER.append(MoviePY.MoviePY)
|
||||
except: pass
|
||||
|
||||
try:
|
||||
import cv2
|
||||
from parsers import OpenCV
|
||||
EXTERNAL_PARSER.append(OpenCV.OpenCV)
|
||||
except: pass
|
||||
|
||||
class VideoParser:
|
||||
def __init__(self):
|
||||
self.AVIExts = ['.avi']
|
||||
self.MP4Exts = ['.mp4', '.m4v', '.3gp', '.3g2', '.f4v', '.mov']
|
||||
self.MKVExts = ['.mkv']
|
||||
self.FLVExts = ['.flv']
|
||||
self.TSExts = ['.ts', '.m2ts']
|
||||
self.STRMExts = ['.strm']
|
||||
self.VFSPaths = ['resource://','plugin://','upnp://','pvr://']
|
||||
self.YTPaths = ['plugin://plugin.video.youtube','plugin://plugin.video.tubed','plugin://plugin.video.invidious']
|
||||
|
||||
|
||||
def getVideoLength(self, filename: str, fileitem: dict={}, jsonRPC=None) -> int and float:
|
||||
duration = jsonRPC._getDuration(filename)
|
||||
if duration == 0:
|
||||
if not filename: log("VideoParser: getVideoLength, no filename.")
|
||||
elif filename.lower().startswith(tuple(self.VFSPaths)):
|
||||
if filename.lower().startswith(tuple(self.YTPaths)):
|
||||
duration = YTParser.YTParser().determineLength(filename)
|
||||
if duration == 0:
|
||||
duration = VFSParser.VFSParser().determineLength(filename, fileitem, jsonRPC)
|
||||
else:
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
if not FileAccess.exists(filename):
|
||||
log("VideoParser: getVideoLength, Unable to find the file")
|
||||
duration = 0
|
||||
elif ext in self.AVIExts:
|
||||
duration = AVIParser.AVIParser().determineLength(filename)
|
||||
elif ext in self.MP4Exts:
|
||||
duration = MP4Parser.MP4Parser().determineLength(filename)
|
||||
elif ext in self.MKVExts:
|
||||
duration = MKVParser.MKVParser().determineLength(filename)
|
||||
elif ext in self.FLVExts:
|
||||
duration = FLVParser.FLVParser().determineLength(filename)
|
||||
elif ext in self.TSExts:
|
||||
duration = TSParser.TSParser().determineLength(filename)
|
||||
elif ext in self.STRMExts:
|
||||
duration = NFOParser.NFOParser().determineLength(filename)
|
||||
else:
|
||||
duration = 0
|
||||
|
||||
if duration == 0:
|
||||
for parser in EXTERNAL_PARSER:
|
||||
if MONITOR().waitForAbort(0.0001) or duration > 0: break
|
||||
duration = parser().determineLength(filename)
|
||||
if duration > 0: duration = jsonRPC._setDuration(filename, fileitem, int(duration))
|
||||
log("VideoParser: getVideoLength duration = %s, filename = %s"%(duration,filename))
|
||||
return duration
|
||||
@@ -0,0 +1,31 @@
|
||||
# Copyright (C) 2025 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/>.
|
||||
# https://github.com/xbmc/xbmc/blob/master/xbmc/input/actions/ActionIDs.h
|
||||
# https://github.com/xbmc/xbmc/blob/master/xbmc/input/Key.h
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
from globals import *
|
||||
|
||||
# https://github.com/PseudoTV/PseudoTV_Live/issues/68
|
||||
|
||||
#todo move autotuning/startup to wizard.
|
||||
|
||||
#display welcome
|
||||
#search discovery
|
||||
#parse library
|
||||
#prompt autotune
|
||||
@@ -0,0 +1,644 @@
|
||||
# Copyright (C) 2024 Lunatixz
|
||||
#
|
||||
#
|
||||
# This file is part of PseudoTV.
|
||||
#
|
||||
# PseudoTV 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 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. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
MODIFIED FROM
|
||||
xmltv.py - Python interface to XMLTV format, based on XMLTV.py
|
||||
|
||||
Copyright (C) 2001 James Oakley <jfunk@funktronics.ca>
|
||||
|
||||
This library is free software: you can redistribute it and/or modify it under
|
||||
the terms of the GNU Lesser General Public License as published by the Free
|
||||
Software Foundation; either version 3 of the License, or (at your option) any
|
||||
later version.
|
||||
|
||||
This program 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 Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License along
|
||||
with this software; if not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from globals import *
|
||||
|
||||
# The Python-XMLTV version
|
||||
VERSION = "1.4.4_PSEUDOTV"
|
||||
|
||||
# The date format used in XMLTV (the %Z will go away in 0.6)
|
||||
locale = DEFAULT_ENCODING #'utf-8'
|
||||
date_format = DTZFORMAT #'%Y%m%d%H%M%S %Z'
|
||||
date_format_notz = DTFORMAT #'%Y%m%d%H%M%S'
|
||||
|
||||
def set_attrs(d, elem, attrs):
|
||||
"""
|
||||
set_attrs(d, elem, attrs) -> None
|
||||
|
||||
Add any attributes in 'attrs' found in 'elem' to 'd'
|
||||
"""
|
||||
for attr in attrs:
|
||||
if attr in elem:
|
||||
d[attr] = elem.get(attr)
|
||||
|
||||
def set_boolean(d, name, elem):
|
||||
"""
|
||||
set_boolean(d, name, elem) -> None
|
||||
|
||||
If element, 'name' is found in 'elem', set 'd'['name'] to a boolean
|
||||
from the 'yes' or 'no' content of the node
|
||||
"""
|
||||
node = elem.find(name)
|
||||
if node is not None:
|
||||
if node.text.lower() == 'yes':
|
||||
d[name] = True
|
||||
elif node.text.lower() == 'no':
|
||||
d[name] = False
|
||||
|
||||
def append_text(d, name, elem, with_lang=True):
|
||||
"""
|
||||
append_text(d, name, elem, with_lang=True) -> None
|
||||
|
||||
Append any text nodes with 'name' found in 'elem' to 'd'['name']. If
|
||||
'with_lang' is 'True', a tuple of ('text', 'lang') is appended
|
||||
"""
|
||||
for node in elem.findall(name):
|
||||
if node is not None:
|
||||
if name not in d:
|
||||
d[name] = []
|
||||
if with_lang:
|
||||
d[name].append((node.text, node.get('lang', '')))
|
||||
else:
|
||||
d[name].append(node.text)
|
||||
|
||||
def set_text(d, name, elem, with_lang=True):
|
||||
"""
|
||||
set_text(d, name, elem, with_lang=True) -> None
|
||||
|
||||
Set 'd'['name'] to the text found in 'name', if found under 'elem'. If
|
||||
'with_lang' is 'True', a tuple of ('text', 'lang') is set
|
||||
"""
|
||||
node = elem.find(name)
|
||||
if node is not None:
|
||||
if with_lang:
|
||||
d[name] = (node.text, node.get('lang', ''))
|
||||
else:
|
||||
d[name] = node.text
|
||||
|
||||
def append_icons(d, elem):
|
||||
"""
|
||||
append_icons(d, elem) -> None
|
||||
|
||||
Append any icons found under 'elem' to 'd'
|
||||
"""
|
||||
for iconnode in elem.findall('icon'):
|
||||
if 'icon' not in d:
|
||||
d['icon'] = []
|
||||
if iconnode.get('src'):
|
||||
d['icon'].append({'src':iconnode.get('src')})
|
||||
# icond = {}
|
||||
# set_attrs(icond, iconnode, ('src', 'width', 'height'))
|
||||
|
||||
def elem_to_channel(elem):
|
||||
"""
|
||||
elem_to_channel(Element) -> dict
|
||||
|
||||
Convert channel element to dictionary
|
||||
"""
|
||||
d = {'id': elem.get('id'),
|
||||
'display-name': []}
|
||||
|
||||
append_text(d, 'display-name', elem)
|
||||
append_icons(d, elem)
|
||||
append_text(d, 'url', elem, with_lang=False)
|
||||
return d
|
||||
|
||||
def elem_to_programme(elem):
|
||||
"""
|
||||
elem_to_programme(Element) -> dict
|
||||
|
||||
Convert programme element to dictionary
|
||||
"""
|
||||
d = {'start': elem.get('start'),
|
||||
'stop': elem.get('stop'),
|
||||
'channel': elem.get('channel'),
|
||||
'catchup-id': elem.get('catchup-id','')}
|
||||
|
||||
set_attrs(d, elem, ('catchup-id', 'stop', 'pdc-start', 'vps-start', 'showview',
|
||||
'videoplus', 'clumpidx'))
|
||||
|
||||
append_text(d, 'title', elem)
|
||||
append_text(d, 'sub-title', elem)
|
||||
append_text(d, 'desc', elem)
|
||||
|
||||
crednode = elem.find('credits')
|
||||
if crednode is not None:
|
||||
creddict = {}
|
||||
# TODO: actor can have a 'role' attribute
|
||||
for credtype in ('director', 'actor', 'writer', 'adapter', 'producer',
|
||||
'presenter', 'commentator', 'guest', 'composer',
|
||||
'editor'):
|
||||
append_text(creddict, credtype, crednode, with_lang=False)
|
||||
d['credits'] = creddict
|
||||
|
||||
set_text(d, 'date', elem, with_lang=False)
|
||||
append_text(d, 'category', elem)
|
||||
set_text(d, 'language', elem)
|
||||
set_text(d, 'orig-language', elem)
|
||||
|
||||
lennode = elem.find('length')
|
||||
if lennode is not None:
|
||||
lend = {'units': lennode.get('units'),
|
||||
'length': lennode.text}
|
||||
d['length'] = lend
|
||||
|
||||
append_icons(d, elem)
|
||||
append_text(d, 'url', elem, with_lang=False)
|
||||
append_text(d, 'country', elem)
|
||||
|
||||
for epnumnode in elem.findall('episode-num'):
|
||||
if 'episode-num' not in d:
|
||||
d['episode-num'] = []
|
||||
d['episode-num'].append((epnumnode.text,
|
||||
epnumnode.get('system', 'xmltv_ns')))
|
||||
|
||||
vidnode = elem.find('video')
|
||||
if vidnode is not None:
|
||||
vidd = {}
|
||||
for name in ('present', 'colour'):
|
||||
set_boolean(vidd, name, vidnode)
|
||||
for videlem in ('aspect', 'quality'):
|
||||
venode = vidnode.find(videlem)
|
||||
if venode is not None:
|
||||
vidd[videlem] = venode.text
|
||||
d['video'] = vidd
|
||||
|
||||
audnode = elem.find('audio')
|
||||
if audnode is not None:
|
||||
audd = {}
|
||||
set_boolean(audd, 'present', audnode)
|
||||
stereonode = audnode.find('stereo')
|
||||
if stereonode is not None:
|
||||
audd['stereo'] = stereonode.text
|
||||
d['audio'] = audd
|
||||
|
||||
psnode = elem.find('previously-shown')
|
||||
if psnode is not None:
|
||||
psd = {}
|
||||
set_attrs(psd, psnode, ('start', 'channel','catchup-id'))
|
||||
d['previously-shown'] = psd
|
||||
|
||||
set_text(d, 'premiere', elem)
|
||||
set_text(d, 'last-chance', elem)
|
||||
|
||||
if elem.find('new') is not None:
|
||||
d['new'] = True
|
||||
|
||||
for stnode in elem.findall('subtitles'):
|
||||
if 'subtitles' not in d:
|
||||
d['subtitles'] = []
|
||||
std = {}
|
||||
set_attrs(std, stnode, ('type',))
|
||||
set_text(std, 'language', stnode)
|
||||
d['subtitles'].append(std)
|
||||
|
||||
for ratnode in elem.findall('rating'):
|
||||
if 'rating' not in d:
|
||||
d['rating'] = []
|
||||
ratd = {}
|
||||
set_attrs(ratd, ratnode, ('system',))
|
||||
set_text(ratd, 'value', ratnode, with_lang=False)
|
||||
append_icons(ratd, ratnode)
|
||||
d['rating'].append(ratd)
|
||||
|
||||
for srnode in elem.findall('star-rating'):
|
||||
if 'star-rating' not in d:
|
||||
d['star-rating'] = []
|
||||
srd = {}
|
||||
set_attrs(srd, srnode, ('system',))
|
||||
set_text(srd, 'value', srnode, with_lang=False)
|
||||
append_icons(srd, srnode)
|
||||
d['star-rating'].append(srd)
|
||||
|
||||
for revnode in elem.findall('review'):
|
||||
if 'review' not in d:
|
||||
d['review'] = []
|
||||
rd = {}
|
||||
set_attrs(rd, revnode, ('type', 'source', 'reviewer',))
|
||||
set_text(rd, 'value', revnode, with_lang=False)
|
||||
d['review'].append(rd)
|
||||
|
||||
return d
|
||||
|
||||
def escape_xml_string(text):
|
||||
"""Escapes special characters in a string for use in XML."""
|
||||
return xml.sax.saxutils.escape(text)
|
||||
|
||||
def read_error(msg, fp, e):
|
||||
try:
|
||||
line = int(e.args[0].split(':')[0].split('line ')[1])
|
||||
column = int(e.args[0].split(':')[1].split('column ')[1])
|
||||
log(f"{msg}, Error at line: {line}, column: {column}")
|
||||
if hasattr(fp, 'readlines'): lines = fp.readlines()
|
||||
elif hasattr(fp, 'readlines'): lines = fp.read().split('\n')
|
||||
else: lines = []
|
||||
if len(lines) >= line: log(f"{msg}, Line {line}: {lines[line-1].strip()}")
|
||||
except: log(f"{msg}, {e}")
|
||||
|
||||
def read_data(fp=None, tree=None):
|
||||
"""
|
||||
read_data(fp=None, tree=None) -> dict
|
||||
|
||||
Get the source and other info from file object fp or the ElementTree
|
||||
'tree'
|
||||
"""
|
||||
if fp:
|
||||
try:
|
||||
d = {}
|
||||
if hasattr(fp, 'read'): tree = fromstring(fp.read(), parser=XMLParser(encoding=locale))
|
||||
else: tree = ETparse(fp, parser=XMLParser(encoding=locale)).getroot()
|
||||
set_attrs(d, tree, ('date', 'source-info-url', 'source-info-name', 'source-data-url', 'generator-info-name', 'generator-info-url'))
|
||||
return d
|
||||
except Exception as e:
|
||||
read_error('read_data', fp, e)
|
||||
return {}
|
||||
|
||||
def read_channels(fp=None, tree=None):
|
||||
"""
|
||||
read_channels(fp=None, tree=None) -> list
|
||||
|
||||
Return a list of channel dictionaries from file object 'fp' or the
|
||||
ElementTree 'tree'
|
||||
"""
|
||||
if fp:
|
||||
try:
|
||||
channels = []
|
||||
if hasattr(fp, 'read'): tree = fromstring(fp.read(), parser=XMLParser(encoding=locale))
|
||||
else: tree = ETparse(fp, parser=XMLParser(encoding=locale)).getroot()
|
||||
for elem in tree.findall('channel'):
|
||||
channel = elem_to_channel(elem)
|
||||
try: channel['icon'] = [{'src': elem.findall('icon')[0].get('src')}]
|
||||
except IndexError:
|
||||
log("Icon element missing or malformed", xbmc.LOGERROR)
|
||||
channel['icon'] = []
|
||||
channels.append(channel)
|
||||
return channels
|
||||
except Exception as e:
|
||||
read_error('read_channels', fp, e)
|
||||
return []
|
||||
|
||||
def read_programmes(fp=None, tree=None):
|
||||
"""
|
||||
read_programmes(fp=None, tree=None) -> list
|
||||
|
||||
Return a list of programme dictionaries from file object 'fp' or the
|
||||
ElementTree 'tree'
|
||||
"""
|
||||
if fp:
|
||||
try:
|
||||
if hasattr(fp, 'read'): tree = fromstring(fp.read(), parser=XMLParser(encoding=locale))
|
||||
else: tree = ETparse(fp, parser=XMLParser(encoding=locale)).getroot()
|
||||
return [elem_to_programme(elem) for elem in tree.findall('programme')]
|
||||
except Exception as e:
|
||||
read_error('read_programmes', fp, e)
|
||||
return []
|
||||
|
||||
def indent(elem, level=0):
|
||||
"""
|
||||
Indent XML for pretty printing
|
||||
"""
|
||||
i = "\n" + level*" "
|
||||
if len(elem):
|
||||
if not elem.text or not elem.text.strip():
|
||||
elem.text = i + " "
|
||||
if not elem.tail or not elem.tail.strip():
|
||||
elem.tail = i
|
||||
for elem in elem:
|
||||
indent(elem, level+1)
|
||||
if not elem.tail or not elem.tail.strip():
|
||||
elem.tail = i
|
||||
else:
|
||||
if level and (not elem.tail or not elem.tail.strip()):
|
||||
elem.tail = i
|
||||
|
||||
class Writer:
|
||||
"""
|
||||
A class for generating XMLTV data
|
||||
|
||||
**All strings passed to this class must be Unicode, except for dictionary
|
||||
keys**
|
||||
"""
|
||||
def __init__(self, encoding=locale, date=None,
|
||||
source_info_url=None, source_info_name=None,
|
||||
generator_info_url=None, generator_info_name=None):
|
||||
"""
|
||||
Arguments:
|
||||
|
||||
'encoding' -- The text encoding that will be used.
|
||||
*Defaults to 'UTF-8'*
|
||||
|
||||
'date' -- The date this data was generated. *Optional*
|
||||
|
||||
'source-info-url' -- A URL for information about the source of the
|
||||
data. *Optional*
|
||||
|
||||
'source-info-name' -- A human readable description of
|
||||
'source-info-url'. *Optional*
|
||||
|
||||
'generator-info-url' -- A URL for information about the program
|
||||
that is generating the XMLTV document.
|
||||
*Optional*
|
||||
|
||||
'generator-info-name' -- A human readable description of
|
||||
'generator-info-url'. *Optional*
|
||||
|
||||
"""
|
||||
self.data = {'date': date,
|
||||
'source-info-url': source_info_url,
|
||||
'source-info-name': source_info_name,
|
||||
'generator-info-url': generator_info_url,
|
||||
'generator-info-name': generator_info_name}
|
||||
|
||||
self.root = Element('tv')
|
||||
for attr in self.data:
|
||||
if self.data[attr]:
|
||||
self.root.set(attr, self.data[attr])
|
||||
|
||||
def setattr(self, node, attr, value):
|
||||
"""
|
||||
setattr(node, attr, value) -> None
|
||||
|
||||
Set 'attr' in 'node' to 'value'
|
||||
"""
|
||||
node.set(attr, value)
|
||||
|
||||
def settext(self, node, text, with_lang=True):
|
||||
"""
|
||||
settext(node, text) -> None
|
||||
|
||||
Set 'node's text content to 'text'
|
||||
"""
|
||||
if with_lang:
|
||||
if text[0] == None:
|
||||
node.text = ''
|
||||
else:
|
||||
node.text = text[0]
|
||||
if text[1]:
|
||||
node.set('lang', text[1])
|
||||
else:
|
||||
if text == None:
|
||||
node.text = ''
|
||||
else:
|
||||
node.text = text
|
||||
|
||||
def seticons(self, node, icons):
|
||||
"""
|
||||
seticon(node, icons) -> None
|
||||
|
||||
Create 'icons' under 'node'
|
||||
"""
|
||||
for icon in icons:
|
||||
if 'src' not in icon:
|
||||
raise ValueError("'icon' element requires 'src' attribute")
|
||||
i = SubElement(node, 'icon')
|
||||
for attr in ('src', 'width', 'height'):
|
||||
if attr in icon:
|
||||
self.setattr(i, attr, icon[attr])
|
||||
|
||||
|
||||
def set_zero_ormore(self, programme, element, p):
|
||||
"""
|
||||
set_zero_ormore(programme, element, p) -> None
|
||||
|
||||
Add nodes under p for the element 'element', which occurs zero
|
||||
or more times with PCDATA and a 'lang' attribute
|
||||
"""
|
||||
if element in programme:
|
||||
for item in programme[element]:
|
||||
e = SubElement(p, element)
|
||||
self.settext(e, item)
|
||||
|
||||
def set_zero_orone(self, programme, element, p):
|
||||
"""
|
||||
set_zero_ormore(programme, element, p) -> None
|
||||
|
||||
Add nodes under p for the element 'element', which occurs zero
|
||||
times or once with PCDATA and a 'lang' attribute
|
||||
"""
|
||||
if element in programme:
|
||||
e = SubElement(p, element)
|
||||
self.settext(e, programme[element])
|
||||
|
||||
|
||||
def addProgramme(self, programme):
|
||||
"""
|
||||
Add a single XMLTV 'programme'
|
||||
|
||||
Arguments:
|
||||
|
||||
'programme' -- A dict representing XMLTV data
|
||||
"""
|
||||
p = SubElement(self.root, 'programme')
|
||||
|
||||
# programme attributes
|
||||
for attr in ('start', 'channel'):
|
||||
if attr in programme:
|
||||
self.setattr(p, attr, programme[attr])
|
||||
else:
|
||||
raise ValueError("'programme' must contain '%s' attribute" % attr)
|
||||
|
||||
for attr in ('catchup-id', 'stop', 'pdc-start', 'vps-start', 'showview', 'videoplus', 'clumpidx'):
|
||||
if attr in programme:
|
||||
self.setattr(p, attr, programme[attr])
|
||||
|
||||
for title in programme['title']:
|
||||
t = SubElement(p, 'title')
|
||||
self.settext(t, title)
|
||||
|
||||
# Sub-title and description
|
||||
for element in ('sub-title', 'desc'):
|
||||
self.set_zero_ormore(programme, element, p)
|
||||
|
||||
# Credits
|
||||
if 'credits' in programme:
|
||||
c = SubElement(p, 'credits')
|
||||
for credtype in ('director', 'actor', 'writer', 'adapter',
|
||||
'producer', 'presenter', 'commentator', 'guest'):
|
||||
if credtype in programme['credits']:
|
||||
for name in programme['credits'][credtype]:
|
||||
cred = SubElement(c, credtype)
|
||||
self.settext(cred, name, with_lang=False)
|
||||
|
||||
# Date
|
||||
if 'date' in programme:
|
||||
d = SubElement(p, 'date')
|
||||
self.settext(d, programme['date'], with_lang=False)
|
||||
|
||||
# Category
|
||||
self.set_zero_ormore(programme, 'category', p)
|
||||
|
||||
# Language and original language
|
||||
for element in ('language', 'orig-language'):
|
||||
self.set_zero_orone(programme, element, p)
|
||||
|
||||
# Length
|
||||
if 'length' in programme:
|
||||
l = SubElement(p, 'length')
|
||||
self.setattr(l, 'units', programme['length']['units'])
|
||||
self.settext(l, programme['length']['length'], with_lang=False)
|
||||
|
||||
# Icon
|
||||
if 'icon' in programme:
|
||||
self.seticons(p, programme['icon'])
|
||||
|
||||
# URL
|
||||
if 'url' in programme:
|
||||
for url in programme['url']:
|
||||
u = SubElement(p, 'url')
|
||||
self.settext(u, url, with_lang=False)
|
||||
|
||||
# Country
|
||||
self.set_zero_ormore(programme, 'country', p)
|
||||
|
||||
# Episode-num
|
||||
if 'episode-num' in programme:
|
||||
for epnum in programme['episode-num']:
|
||||
e = SubElement(p, 'episode-num')
|
||||
self.setattr(e, 'system', epnum[1])
|
||||
self.settext(e, epnum[0], with_lang=False)
|
||||
|
||||
# Video details
|
||||
if 'video' in programme:
|
||||
e = SubElement(p, 'video')
|
||||
for videlem in ('aspect', 'quality'):
|
||||
if videlem in programme['video']:
|
||||
v = SubElement(e, videlem)
|
||||
self.settext(v, programme['video'][videlem], with_lang=False)
|
||||
for attr in ('present', 'colour'):
|
||||
if attr in programme['video']:
|
||||
a = SubElement(e, attr)
|
||||
if programme['video'][attr]:
|
||||
self.settext(a, 'yes', with_lang=False)
|
||||
else:
|
||||
self.settext(a, 'no', with_lang=False)
|
||||
|
||||
# Audio details
|
||||
if 'audio' in programme:
|
||||
a = SubElement(p, 'audio')
|
||||
if 'stereo' in programme['audio']:
|
||||
s = SubElement(a, 'stereo')
|
||||
self.settext(s, programme['audio']['stereo'], with_lang=False)
|
||||
if 'present' in programme['audio']:
|
||||
p = SubElement(a, 'present')
|
||||
if programme['audio']['present']:
|
||||
self.settext(p, 'yes', with_lang=False)
|
||||
else:
|
||||
self.settext(p, 'no', with_lang=False)
|
||||
|
||||
# Previously shown
|
||||
if 'previously-shown' in programme:
|
||||
ps = SubElement(p, 'previously-shown')
|
||||
for attr in ('start', 'channel'):
|
||||
if attr in programme['previously-shown']:
|
||||
self.setattr(ps, attr, programme['previously-shown'][attr])
|
||||
|
||||
# Premiere / last chance
|
||||
for element in ('premiere', 'last-chance'):
|
||||
self.set_zero_orone(programme, element, p)
|
||||
|
||||
# New
|
||||
if 'new' in programme:
|
||||
n = SubElement(p, 'new')
|
||||
|
||||
# Subtitles
|
||||
if 'subtitles' in programme:
|
||||
for subtitles in programme['subtitles']:
|
||||
s = SubElement(p, 'subtitles')
|
||||
if 'type' in subtitles:
|
||||
self.setattr(s, 'type', subtitles['type'])
|
||||
if 'language' in subtitles:
|
||||
l = SubElement(s, 'language')
|
||||
self.settext(l, subtitles['language'])
|
||||
|
||||
# Rating
|
||||
if 'rating' in programme:
|
||||
for rating in programme['rating']:
|
||||
r = SubElement(p, 'rating')
|
||||
if 'system' in rating:
|
||||
self.setattr(r, 'system', rating['system'])
|
||||
v = SubElement(r, 'value')
|
||||
self.settext(v, rating['value'], with_lang=False)
|
||||
if 'icon' in rating:
|
||||
self.seticons(r, rating['icon'])
|
||||
|
||||
# Star rating
|
||||
if 'star-rating' in programme:
|
||||
for star_rating in programme['star-rating']:
|
||||
sr = SubElement(p, 'star-rating')
|
||||
if 'system' in star_rating:
|
||||
self.setattr(sr, 'system', star_rating['system'])
|
||||
v = SubElement(sr, 'value')
|
||||
self.settext(v, star_rating['value'], with_lang=False)
|
||||
if 'icon' in star_rating:
|
||||
self.seticons(sr, star_rating['icon'])
|
||||
|
||||
# Review
|
||||
if 'review' in programme:
|
||||
for review in programme['review']:
|
||||
r = SubElement(p, 'review')
|
||||
for attr in ('type', 'source', 'reviewer'):
|
||||
if attr in review:
|
||||
self.setattr(r, attr, review[attr])
|
||||
v = SubElement(r, 'value')
|
||||
self.settext(v, review['value'], with_lang=False)
|
||||
|
||||
def addChannel(self, channel):
|
||||
"""
|
||||
add a single XMLTV 'channel'
|
||||
|
||||
Arguments:
|
||||
|
||||
'channel' -- A dict representing XMLTV data
|
||||
"""
|
||||
c = SubElement(self.root, 'channel')
|
||||
self.setattr(c, 'id', channel['id'])
|
||||
|
||||
# Display Name
|
||||
for display_name in channel['display-name']:
|
||||
dn = SubElement(c, 'display-name')
|
||||
self.settext(dn, display_name)
|
||||
|
||||
# Icon
|
||||
if 'icon' in channel:
|
||||
self.seticons(c, channel['icon'])
|
||||
|
||||
# URL
|
||||
if 'url' in channel:
|
||||
for url in channel['url']:
|
||||
u = SubElement(c, 'url')
|
||||
self.settext(u, url, with_lang=False)
|
||||
|
||||
def write(self, file, pretty_print=False):
|
||||
"""
|
||||
write(file, pretty_print=False) -> None
|
||||
|
||||
Write XML to filename of file object in 'file'. If pretty_print is
|
||||
True, the XML will contain whitespace to make it human-readable.
|
||||
"""
|
||||
if pretty_print:
|
||||
indent(self.root)
|
||||
et = ElementTree(self.root)
|
||||
et.write(file, encoding=locale, xml_declaration=True)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user