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

591 lines
38 KiB
Python

# Copyright (C) 2024 Lunatixz
#
#
# This file is part of PseudoTV Live.
#
# PseudoTV Live is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PseudoTV Live is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with PseudoTV Live. If not, see <http://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from globals import *
from 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)