# 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 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)