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