diff --git a/addons.xml b/addons.xml index f6c1ac55..d3213fb7 100644 --- a/addons.xml +++ b/addons.xml @@ -1,6 +1,6 @@ - + @@ -13,6 +13,9 @@ + + + diff --git a/addons.xml.md5 b/addons.xml.md5 index 35a432b3..956ee1a2 100644 --- a/addons.xml.md5 +++ b/addons.xml.md5 @@ -1 +1 @@ -71285eb34decd5a522c45cd0caa225b6 \ No newline at end of file +2a000c898af4675775527af17eba9ff3 \ No newline at end of file diff --git a/plugin.video.pseudotv.live/README.md b/plugin.video.pseudotv.live/README.md index 35fa36b3..3f0446ac 100644 --- a/plugin.video.pseudotv.live/README.md +++ b/plugin.video.pseudotv.live/README.md @@ -38,9 +38,9 @@ PseudoTV Live transforms your Kodi Library and Sources (Plugins, UPnP, etc...) i ------------ # Features: -- "Autotuned" Channels based on your Kodi library; categorized by: `"TV Networks", "TV Shows", "TV Genres", "Movie Genres", "Movie Studios", "Mixed Genres", "Playlists", "Music Genres"` +- "Autotuned" Channels based on your Kodi library; categorized by: `"TV Networks", "TV Shows", "TV Genres", "Movie Genres", "Movie Studios", "Mixed Genres", "Playlists", "Music Genres", "Mixed"` -- Automatic Channel logos; sourced from Kodi resource packs. +- Automatic Channel logos, Rating, bumpers; sourced from Kodi resource packs. - Optional video overlay to display channel bug and other informative information. @@ -62,13 +62,15 @@ PseudoTV Live transforms your Kodi Library and Sources (Plugins, UPnP, etc...) i - Option to save "accurate" (Parsed) duration meta to your Kodi database. +- Option to disable Trakt scrobbling and rollback media playcount and resume points for passive viewing. + - "on the fly" channel creation, with automated background building. - Ease of use; User Interface provided by Kodi PVR frontend. - Music Genre PVR "Radio" Channels. -- Multi-Room channel configurations w/Automatic client detection. +- Multi-Room channel configurations w/Client pairing via bonjour network announcement. - Your choice of "Playback Methods". See post below for details. @@ -80,29 +82,21 @@ PseudoTV Live transforms your Kodi Library and Sources (Plugins, UPnP, etc...) i - "Recommended Channels" & "Recommended Services" Plugins preconfigured for easy import into PseudoTV Live. +- "Add to PseudoTV Live" Content option allows quick channel creation from any Kodi source. + - Much more... ------------ -# Supported Plugins: +# Supported Plugins: (Temporarily unavailable) *All plugins are supported by PseudoTV Live through the channel manager. The list below contains configuration free channels. [PlutoTV](https://forum.kodi.tv/showthread.php?tid=315513) [ChannelsDVR](https://forum.kodi.tv/showthread.php?tid=334947) -[Locast](https://forum.kodi.tv/showthread.php?tid=357406) - [HDhomerun Simple](https://forum.kodi.tv/showthread.php?tid=327117) -[AiryTV](https://forum.kodi.tv/showthread.php?tid=361486) - -[Discovery+](https://forum.kodi.tv/showthread.php?tid=340055) - -[Crackle](https://forum.kodi.tv/showthread.php?tid=) - -[TubiTV](https://forum.kodi.tv/showthread.php?tid=) - ------------ # Settings: @@ -136,14 +130,13 @@ Adjusting seek threshold(percentage). threshold to which the current content can - In-order to reduce parsing times when using "Prefer File Metadata" PseudoTV Live can store the new accurate duration meta to the Kodi library, there are no downsides. If you notice performance penalties when enabled, disable it... There is a fallback 28 day cache to avoid unnecessary file parsing. -## Recommended Services: +## Recommended Services: (Temporarily unavailable) Recommended Services are considered "third-party" and are not treated as "PseudoTV" channels. Channel configurations, channel numbering, onscreen overlays are all disabled. Imports are 1:1 m3u/xmltv imports with the exception of channel numbers which maybe altered as described below. ## Options: - Centralized file location: Location to store PseudoTV Live M3U/XMLTV and other shared resources. ie. Playlists, Nodes and Channel Logos. - ------------ @@ -182,9 +175,9 @@ For "Multi-Room", Select an instance of Kodi/PseudoTV Live that will act as your Enable under "Multi-Room", select between two options. - 1 Remote Path - Use a remote url hosted by your server instance which is auto-detected. *http://localhost:50001/pseudotv.m3u **http://localhost:50001/pseudotv.xml ***http://localhost:50001/genres.xml + 1 Remote URL - Use a remote url hosted by your server instance which can be selected in settings. *http://localhost:50001/pseudotv.m3u **http://localhost:50001/pseudotv.xml ***http://localhost:50001/genres.xml - 1 Network Path - Select a shared network path same as configure on the server instance. + 1 Network Folder - Select a shared network path same as configure on the server instance. ## - Channel Ordering (Numbering): @@ -209,7 +202,7 @@ Each import is limited to 9999 (assuming each channel is an interger. Sub-Number #### - IPTV Simple Settings: -"only number by order" must be disbled if you would like to respect the channel numbers assigned in PseudoTV Live. +"only number by order" must be disabled if you would like to respect the channel numbers assigned in PseudoTV Live. *NOTE: PseudoTV Live automatically applies the optimal settings to IPTV Simple in-order to maximize the user experience. #### - Kodi PVR & LiveTV Settings: @@ -231,6 +224,8 @@ If you want the exact channel numbers from PseudoTV Live to reflect onscreen, yo # FYI & Known Issues: +- .mp4 files are not recommend there is a high probability PseudoTV Live will not properly parse its runtime. + - If you experience poor performance using PseudoTV Live; Try disabling "Accurate Duration" parsing, and setting "Playback Method" to playlists. It is recommend on low power devices like AndroidTV/AppleTV to outsource channel building to a "server" instance of Kodi running on a PC; then configure all other Kodi instances as a client. diff --git a/plugin.video.pseudotv.live/addon.xml b/plugin.video.pseudotv.live/addon.xml index 0e829094..807ebe58 100644 --- a/plugin.video.pseudotv.live/addon.xml +++ b/plugin.video.pseudotv.live/addon.xml @@ -1,5 +1,5 @@ - + @@ -12,6 +12,9 @@ + + + diff --git a/plugin.video.pseudotv.live/changelog.txt b/plugin.video.pseudotv.live/changelog.txt index c58ce663..089575c9 100644 --- a/plugin.video.pseudotv.live/changelog.txt +++ b/plugin.video.pseudotv.live/changelog.txt @@ -4,7 +4,12 @@ v.0.4.8 - Open Kodi settings, under PVR & Live TV; Click "clear data" and select "All". -Refactored Announcements & Discoveries for server/client multi-room (W.I.P). --Refactored HTTP server +-Refactored HTTP server. +-Added Disable Trakt scrobbling during playback to global options. +-Added Rollback watched playcount & resume points to global options. +-Fixed TV bumpers and resources (W.I.P); See readme for details. +-Added TV adverts and resources (W.I.P); See readme for details. +-Added Trailers and resources (W.I.P); See readme for details. v.0.4.7 -Notice The following steps are required!. diff --git a/plugin.video.pseudotv.live/remotes/channel.json b/plugin.video.pseudotv.live/remotes/channel.json new file mode 100644 index 00000000..10cfb5bd --- /dev/null +++ b/plugin.video.pseudotv.live/remotes/channel.json @@ -0,0 +1,13 @@ +{ + "id": "", + "type": "", + "number": 0, + "name": "", + "logo": "", + "path": [], + "group": [], + "rules": [], + "catchup": "vod", + "radio": false, + "favorite": false +} \ No newline at end of file diff --git a/plugin.video.pseudotv.live/remotes/channels.json b/plugin.video.pseudotv.live/remotes/channels.json index f1b08d55..4528b6e7 100644 --- a/plugin.video.pseudotv.live/remotes/channels.json +++ b/plugin.video.pseudotv.live/remotes/channels.json @@ -1,19 +1,5 @@ { "uuid": "", - "channels": [ - { - "id": "", - "type": "", - "number": 0, - "name": "", - "logo": "", - "path": [], - "group": [], - "rules": [], - "catchup": "vod", - "radio": false, - "favorite": false - } - ], + "channels": [], "imports": [] } \ No newline at end of file diff --git a/plugin.video.pseudotv.live/remotes/rule.json b/plugin.video.pseudotv.live/remotes/rule.json index 56f792c2..84553d28 100644 --- a/plugin.video.pseudotv.live/remotes/rule.json +++ b/plugin.video.pseudotv.live/remotes/rule.json @@ -1,9 +1,9 @@ -[{ - "id": 0, - "index": { - "0": { - "label": "", - "value": "" - } - } -}] \ No newline at end of file +{ + "id": 0, + "index": { + "0": { + "label": "", + "value": "" + } + } +} \ No newline at end of file diff --git a/plugin.video.pseudotv.live/resources/language/resource.language.en_gb/strings.po b/plugin.video.pseudotv.live/resources/language/resource.language.en_gb/strings.po index 6271a9ac..c389bc00 100644 --- a/plugin.video.pseudotv.live/resources/language/resource.language.en_gb/strings.po +++ b/plugin.video.pseudotv.live/resources/language/resource.language.en_gb/strings.po @@ -126,7 +126,7 @@ msgid "Four Inserts" msgstr "" msgctxt "#30027" -msgid "Logos" +msgid "Logo Resource" msgstr "" msgctxt "#30028" @@ -526,7 +526,7 @@ msgid "Kodi Trailers" msgstr "" msgctxt "#30127" -msgid "Both Kodi/IMDB Trailers" +msgid "Kodi/IMDB Trailers" msgstr "" msgctxt "#30128" @@ -541,8 +541,25 @@ msgctxt "#30130" msgid "Start Client Pairing" msgstr "" +msgctxt "#30131" +msgid "Disable Trakt scrobbling during playback" +msgstr "" +msgctxt "#30132" +msgid "Rollback watched playcount after playback" +msgstr "" +msgctxt "#30133" +msgid "Player" +msgstr "" + +msgctxt "#30134" +msgid "Random Filler (Percentage)" +msgstr "" + +msgctxt "#30135" +msgid "Include" +msgstr "" @@ -705,7 +722,7 @@ msgid "%s settings updated." msgstr "" msgctxt "#32038" -msgid "Something went wrong! wait while we try again... (%s)" +msgid "Something went wrong! wait while we try again...\nPlayback Attempt (%s)" msgstr "" msgctxt "#32039" @@ -1089,7 +1106,7 @@ msgid "[COLOR=yellow][B]Notice!! [/B][/COLOR] Something went wrong, Please doubl msgstr "" msgctxt "#32134" -msgid "Playback failed! This maybe because %s is building this channel.\nPlease check your channel configuration." +msgid "Playback failed! This maybe because %s is building this channel.\nPlease check your channel settings." msgstr "" msgctxt "#32135" @@ -1208,6 +1225,10 @@ msgctxt "#32163" msgid "Announcing %s server!\n[B]%s[/B] on your client instance.\nTimeout in %s seconds, cancel when finished." msgstr "" +msgctxt "#32164" +msgid "Can not locate broadcast!" +msgstr "" + # Help - strings 33000 thru 33999 reserved for common strings used in add-ons @@ -1271,7 +1292,7 @@ msgctxt "#33016" msgid "[COLOR=red][B]Currently Unavailable (Coming Soon)![/B][/COLOR]\nGlobal Filler rules apply to all channels, Advanced channel manager rules apply per channel. *see readme for details." msgstr "" -msgctxt "#33022" +msgctxt "#33023" msgid "Artwork changes will propagate slowly over time and are not instantaneous." msgstr "" @@ -1451,6 +1472,18 @@ msgctxt "#33130" msgid "Detect Bonjour Announcement over your local network" msgstr "" +msgctxt "#33131" +msgid "Prevent Trakt scrobbling during PseudoTV usage." +msgstr "" + +msgctxt "#33132" +msgid "Restore the previous playcount and resume points at the end of playback. ie. Passive PseudoTV usage." +msgstr "" + +msgctxt "#33134" +msgid "Probability a non-channel specific i.e generic video will be injected when no match is found.\n[0% Disabled, 25% Default] [Lower == Less Probable]" +msgstr "" + msgctxt "#33149" msgid "Override Low-Power background pausing and performance throttling." msgstr "" @@ -1461,4 +1494,8 @@ msgstr "" msgctxt "#33159" msgid "Force a complete rebuild of your Autotune library." -msgstr "" \ No newline at end of file +msgstr "" + +msgctxt "#33160" +msgid "Select one or more logo resource packs." +msgstr " diff --git a/plugin.video.pseudotv.live/resources/lib/builder.py b/plugin.video.pseudotv.live/resources/lib/builder.py index ff67f585..c865c276 100644 --- a/plugin.video.pseudotv.live/resources/lib/builder.py +++ b/plugin.video.pseudotv.live/resources/lib/builder.py @@ -22,7 +22,6 @@ from channels import Channels from rules import RulesList from xmltvs import XMLTVS -from jsonrpc import JSONRPC from xsp import XSP from m3u import M3U from fillers import Fillers @@ -54,16 +53,16 @@ def __init__(self, service=None): self.limits = {} #{"end":0,"start":0,"total":0} self.incRatings = SETTINGS.getSettingInt('Fillers_Ratings') - self.srcRatings = {"RESC":SETTINGS.getSetting('Resource_Ratings').split('|')} + self.srcRatings = {"resource":SETTINGS.getSetting('Resource_Ratings').split('|')} self.incBumpers = SETTINGS.getSettingInt('Fillers_Bumpers') - self.srcBumpers = {"PATH":[SETTINGS.getSetting('Resource_Bumpers')]} + self.srcBumpers = {"resource":SETTINGS.getSetting('Resource_Bumpers').split('|')} self.incAdverts = SETTINGS.getSettingInt('Fillers_Commercials') - self.srcAdverts = {"PATH":[SETTINGS.getSetting('Resource_Commericals')]} + self.srcAdverts = {"resource":SETTINGS.getSetting('Resource_Commericals').split('|')} self.incTrailer = SETTINGS.getSettingInt('Fillers_Trailers') - self.srcTrailer = {"PATH":[SETTINGS.getSettingInt('Resource_Trailers')]} + self.srcTrailer = {"resource":SETTINGS.getSetting('Resource_Trailers').split('|')} self.minDuration = SETTINGS.getSettingInt('Seek_Tolerance') self.maxDays = MAX_GUIDEDAYS @@ -75,10 +74,9 @@ def __init__(self, service=None): self.rules = RulesList(self.channels.getChannels()) self.runActions = self.rules.runActions self.xmltv = XMLTVS() - self.jsonRPC = JSONRPC() + self.jsonRPC = service.jsonRPC self.xsp = XSP() self.m3u = M3U() - self.fillers = Fillers(self) self.resources = Resources(self.jsonRPC,self.cache) @@ -162,7 +160,7 @@ def getFileList(self, citem, now, start): if cacheResponse: if self.fillBCTs and not radio: - cacheResponse = self.fillers.injectBCTs(citem, cacheResponse) + cacheResponse = Fillers(self).injectBCTs(citem, cacheResponse) cacheResponse = self.addScheduling(citem, cacheResponse, start) return sorted(cacheResponse, key=lambda k: k['start']) return cacheResponse diff --git a/plugin.video.pseudotv.live/resources/lib/cache.py b/plugin.video.pseudotv.live/resources/lib/cache.py index f3293cfc..32683752 100644 --- a/plugin.video.pseudotv.live/resources/lib/cache.py +++ b/plugin.video.pseudotv.live/resources/lib/cache.py @@ -43,8 +43,8 @@ class Cache: def cacheLocker(self): #simplecache is not thread safe, threadlock not avoiding collisions? Hack/Lazy avoidance. if xbmcgui.Window(10000).getProperty('%s.cacheLocker'%(ADDON_ID)) == 'true': while not xbmc.Monitor().abortRequested(): - if xbmc.Monitor().waitForAbort(0.001): break - elif not xbmcgui.Window(10000).getProperty('%s.cacheLocker'%(ADDON_ID)) == 'true': break + if not xbmcgui.Window(10000).getProperty('%s.cacheLocker'%(ADDON_ID)) == 'true': break + elif xbmc.Monitor().waitForAbort(0.001): break xbmcgui.Window(10000).setProperty('%s.cacheLocker'%(ADDON_ID),'true') try: yield finally: @@ -60,18 +60,21 @@ def log(self, msg, level=xbmc.LOGDEBUG): log('%s: %s'%(self.__class__.__name__,msg),level) - def set(self, name, data, checksum=ADDON_VERSION, expiration=datetime.timedelta(minutes=15), json_data=False): - if data is not None or not DEBUG_CACHE: + def getname(self, name): + if not name.startswith(ADDON_ID): name = '%s.%s'%(ADDON_ID,name) + return name.lower() + + + def set(self, name, value, checksum=ADDON_VERSION, expiration=datetime.timedelta(minutes=15), json_data=False): + if value is not None or not DEBUG_CACHE: with self.cacheLocker(): - self.log('set, name = %s, checksum = %s'%(self.getname(name),checksum)) - self.cache.set(self.getname(name),data,checksum,expiration,json_data) - return data + self.cache.set(self.getname(name),value,checksum,expiration,json_data) + return value def get(self, name, checksum=ADDON_VERSION, json_data=False, default=None): if not DEBUG_CACHE: with self.cacheLocker(): - self.log('get, name = %s, checksum = %s, default = %s'%(self.getname(name),checksum,default)) return (self.cache.get(self.getname(name),checksum,json_data) or default) return default @@ -90,8 +93,4 @@ def clear(self, name, wait=15): self.log('clear, failed! %s'%(e), xbmc.LOGERROR) finally: del connection - del sqlite3 - - def getname(self, name): - if not name.startswith(ADDON_ID): name = '%s.%s'%(ADDON_ID,name) - return name.lower() \ No newline at end of file + del sqlite3 \ No newline at end of file diff --git a/plugin.video.pseudotv.live/resources/lib/channels.py b/plugin.video.pseudotv.live/resources/lib/channels.py index b0d396db..ea017533 100644 --- a/plugin.video.pseudotv.live/resources/lib/channels.py +++ b/plugin.video.pseudotv.live/resources/lib/channels.py @@ -19,13 +19,14 @@ # -*- coding: utf-8 -*- from globals import * - +#todo create dataclasses for all jsons +# https://pypi.org/project/dataclasses-json/ class Channels: def __init__(self): self.cache = Cache() self.channelDATA = getJSON(CHANNELFLE_DEFAULT) - self.channelTEMP = self.channelDATA.get('channels',[]).pop(0) + self.channelTEMP = getJSON(CHANNEL_ITEM) self.channelDATA.update(self._load()) self.setChannels() diff --git a/plugin.video.pseudotv.live/resources/lib/constants.py b/plugin.video.pseudotv.live/resources/lib/constants.py index 50f1bced..634f6c70 100644 --- a/plugin.video.pseudotv.live/resources/lib/constants.py +++ b/plugin.video.pseudotv.live/resources/lib/constants.py @@ -106,7 +106,7 @@ MEDIA_LOC = os.path.join(ADDON_PATH,'resources','skins','default','media') SFX_LOC = os.path.join(MEDIA_LOC,'sfx') BACKUP_LOC = os.path.join(SETTINGS_LOC,'backup') -CACHE_LOC = os.path.join(SETTINGS_LOC,'cache') #default User_Folder path +CACHE_LOC = os.path.join(SETTINGS_LOC,'cache') #files XMLTVFLE = '%s.xml'%('pseudotv') @@ -135,7 +135,8 @@ #remotes IMPORT_ASSET = os.path.join(ADDON_PATH,'remotes','asset.json') -RULEFLE_DEFAULT = os.path.join(ADDON_PATH,'remotes','rule.json') +RULEFLE_ITEM = os.path.join(ADDON_PATH,'remotes','rule.json') +CHANNEL_ITEM = os.path.join(ADDON_PATH,'remotes','channel.json') M3UFLE_DEFAULT = os.path.join(ADDON_PATH,'remotes','m3u.json') GROUPFLE_DEFAULT = os.path.join(ADDON_PATH,'remotes','groups.xml') LIBRARYFLE_DEFAULT = os.path.join(ADDON_PATH,'remotes',LIBRARYFLE) diff --git a/plugin.video.pseudotv.live/resources/lib/fileaccess.py b/plugin.video.pseudotv.live/resources/lib/fileaccess.py index 0bc30f6c..6e877fdb 100644 --- a/plugin.video.pseudotv.live/resources/lib/fileaccess.py +++ b/plugin.video.pseudotv.live/resources/lib/fileaccess.py @@ -274,7 +274,7 @@ def checkpath(self): if not FileAccess.exists(lockpath): if FileAccess.makedirs(lockpath): return lockpath - else: + else:#fallback to local folder. #todo log error with lock path lockpath = os.path.join(SETTINGS_LOC,'cache') if not FileAccess.exists(lockpath): diff --git a/plugin.video.pseudotv.live/resources/lib/fillers.py b/plugin.video.pseudotv.live/resources/lib/fillers.py index b6c71b2d..9f36ae9a 100644 --- a/plugin.video.pseudotv.live/resources/lib/fillers.py +++ b/plugin.video.pseudotv.live/resources/lib/fillers.py @@ -20,6 +20,7 @@ from globals import * from resources import Resources +#todo support converting other region ratings, move desc. to language po RATING_DESC = {"G" :"General audiences – All ages admitted.\nNothing that would offend parents for viewing by children.", "PG" :"Parental guidance suggested – Some material may not be suitable for children.\nParents urged to give “parental guidance.” May contain some material parents might not like for their young children", "PG-13":"Parents strongly cautioned – Some material may be inappropriate for children under 13.\nParents are urged to be cautious. Some material may be inappropriate for pre-teenagers.", @@ -29,6 +30,7 @@ IMDB_PATHS = ['plugin://plugin.video.imdb.trailers/?action=list1&key=showing', 'plugin://plugin.video.imdb.trailers/?action=list1&key=coming'] + #Ratings - resource only, Movie Type only any channel type #Bumpers - plugin, path only, tv type, tv network, custom channel type #Adverts - plugin, path only, tv type, any tv channel type @@ -43,12 +45,11 @@ def __init__(self, builder): self.resources = Resources(self.jsonRPC,self.cache) #default global rules: - self.bctTypes = {"ratings" :{"max":1 ,"auto":builder.incRatings == 1,"enabled":bool(builder.incRatings),"sources":builder.srcRatings,"resources":{}}, - "bumpers" :{"max":1 ,"auto":builder.incBumpers == 1,"enabled":bool(builder.incBumpers),"sources":builder.srcBumpers,"resources":{}}, - "adverts" :{"max":builder.incAdverts,"auto":builder.incAdverts == 1,"enabled":bool(builder.incAdverts),"sources":builder.srcAdverts,"resources":{}}, - "trailers" :{"max":builder.incTrailer,"auto":builder.incTrailer == 1,"enabled":bool(builder.incTrailer),"sources":builder.srcTrailer,"resources":{}}} - - self.fillResources() + self.bctTypes = {"ratings" :{"max":1 ,"auto":builder.incRatings == 1,"enabled":bool(builder.incRatings),"sources":builder.srcRatings,"items":{}}, + "bumpers" :{"max":1 ,"auto":builder.incBumpers == 1,"enabled":bool(builder.incBumpers),"sources":builder.srcBumpers,"items":{}}, + "adverts" :{"max":builder.incAdverts,"auto":builder.incAdverts == 1,"enabled":bool(builder.incAdverts),"sources":builder.srcAdverts,"items":{}}, + "trailers" :{"max":builder.incTrailer,"auto":builder.incTrailer == 1,"enabled":bool(builder.incTrailer),"sources":builder.srcTrailer,"items":{}}} + self.fillSources() print('self.bctTypes',self.bctTypes) @@ -56,93 +57,213 @@ def log(self, msg, level=xbmc.LOGDEBUG): return log('%s: %s'%(self.__class__.__name__,msg),level) - def fillResources(self): - data = {} - for key, values in self.bctTypes.items(): - if values.get("sources",{}).get("RESC"): - data = self.buildResource(key,values['sources']["RESC"]) - if data: values["resources"].update(data) - - - # @cacheit(expiration=datetime.timedelta(minutes=15),json_data=True) - def buildResource(self, type, ids): - self.log('buildResource, type = %s, ids = %s'%(type, ids)) - def _parse(addonids): - for addon in addonids: - if not hasAddon(addon): continue - yield self.resources.walkResource(addon,exts=VIDEO_EXTS) - - def _rating(resource): - for path in resource: - for file in resource[path]: - tmpDCT.setdefault(file.split('.')[0],[]).append(os.path.join(path,file)) #{'PG-13':[],'R':[]} - tmpDCT = {} - for id in list(_parse(ids)): - if type == 'ratings': _rating(id) - return tmpDCT + def fillSources(self): + for ftype, values in self.bctTypes.items(): + for id in values.get("sources",{}).get("resource",[]): + values['items'].update(self.buildResource(ftype,id)) + # for id in values.get("sources",{}).get("paths",[]): #parse vfs for media + # values['items'].update(self.getfilelist(ftype,id)) + + + @cacheit(expiration=datetime.timedelta(minutes=15),json_data=True) + def buildResource(self, ftype, addonid): + self.log('buildResource, type = %s, addonid = %s'%(ftype, addonid)) + def _parse(addonid): + if hasAddon(addonid): + #{'special://home/addons/resource.videos.ratings.mpaa.classic/resources': ['G.mkv', 'NC-17.mkv', 'NR.mkv', 'PG-13.mkv', 'PG.mkv', 'R.mkv']} + return self.resources.walkResource(addonid,exts=VIDEO_EXTS) + return {} + + def _rating(addonid): + tmpDCT = {} + for path, files in _parse(addonid).items(): + for file in files: + dur = self.jsonRPC.getDuration(os.path.join(path,file), accurate=True) + if dur > 0: tmpDCT.setdefault(file.split('.')[0],[]).append((os.path.join(path,file),dur)) #{'PG-13':[('PG-13.mkv',7)]} + return tmpDCT + + def _bumper(addonid): + tmpDCT = {} + for path, files in _parse(addonid).items(): + for file in files: + dur = self.jsonRPC.getDuration(os.path.join(path,file), accurate=True) + if dur > 0: tmpDCT.setdefault(os.path.basename(path).lower(),[]).append((os.path.join(path,file),dur)) + return tmpDCT + + def _advert(addonid): + tmpDCT = {} + for path, files in _parse(addonid).items(): + for file in files: + dur = self.jsonRPC.getDuration(os.path.join(path,file), accurate=True) + if dur > 0: tmpDCT.setdefault(os.path.basename(path).lower(),[]).append((os.path.join(path,file),dur)) + return tmpDCT + + def _trailer(addonid): + tmpDCT = {} + for path, files in _parse(addonid).items(): + for file in files: + dur = self.jsonRPC.getDuration(os.path.join(path,file), accurate=True) + if dur > 0: tmpDCT.setdefault(os.path.basename(path).lower(),[]).append((os.path.join(path,file),dur)) + return tmpDCT + if ftype == 'ratings': return _rating(addonid) + elif ftype == 'bumpers': return _bumper(addonid) + elif ftype == 'adverts': return _advert(addonid) + elif ftype == 'trailers': return _trailer(addonid) + return {} - def getRating(self, mpaa): - def _convert(rating): - #https://www.spherex.com/tv-ratings-vs-movie-ratings - #https://www.spherex.com/which-is-more-regulated-film-or-tv - return rating.replace('TV-Y','G').replace('TV-Y7','G').replace('TV-G','G').replace('NA','NR').replace('TV-PG','PG').replace('TV-14','PG-13').replace('TV-MA','R') - try: - mpaa = re.compile(":(.*?)/", re.IGNORECASE).search(mpaa.upper()).group(1) - mpaa = mpaa.strip() - except: - mpaa = mpaa.upper() - files = [] - for rating in [mpaa, _convert(mpaa)]: - print('getRating',rating) - files = self.bctTypes['ratings'].get('resources',{}).get(rating,[]) - if files: return rating, random.choice(files) - return mpaa, None + def convertMPAA(self, ompaa): + tmpLST = ompaa.split(' / ') + tmpLST.append(ompaa.upper()) + try: mpaa = re.compile(":(.*?)/", re.IGNORECASE).search(ompaa.upper()).group(1).strip() + except: mpaa = ompaa.upper() + #https://www.spherex.com/tv-ratings-vs-movie-ratings, #https://www.spherex.com/which-is-more-regulated-film-or-tv + mpaa = mpaa.replace('TV-Y','G').replace('TV-Y7','G').replace('TV-G','G').replace('NA','NR').replace('TV-PG','PG').replace('TV-14','PG-13').replace('TV-MA','R') + tmpLST.append(mpaa) + return mpaa, tmpLST + + + def getRating(self, keys=[]): + def _parse(key): + tmpLST.extend(self.bctTypes['ratings'].get('items',{}).get(key,[])) + try: + tmpLST = [] + poolit(_parse)(keys) + return random.choice(tmpLST) + except: return None, 0 - # @cacheit(expiration=datetime.timedelta(minutes=15),json_data=True) + def getBumper(self, keys=['resources']): + def _parse(key): + tmpLST.extend(self.bctTypes['bumpers'].get('items',{}).get(key.lower(),[])) + try: + tmpLST = [] + poolit(_parse)(keys) + random.shuffle(tmpLST) + return random.choice(tmpLST) + except: return None, 0 + + + def getAdverts(self, keys=['resources'], count=1): + def _parse(key): + return self.bctTypes['adverts'].get('items',{}).get(key.lower(),[]) + try: + tmpLST = poolit(_parse)(keys) + random.shuffle(tmpLST) + removeDUPDICT(random.choices(tmpLST,k=count)) + except: return [(None, 0)] + + + def getTrailers(self, keys=['resources'], count=1): + def _parse(key): + return self.bctTypes['trailers'].get('items',{}).get(key.lower(),[]) + try: + tmpLST = poolit(_parse)(keys) + random.shuffle(tmpLST) + removeDUPDICT(random.choices(tmpLST,k=count)) + except: return [(None, 0)] + + def buildKodiTrailers(self, fileList): def _parse(fileItem): - if not fileItem.get('trailer','').startswith(tuple(VFS_TYPES)): - return {'title':fileItem.get('title','Trailer'),'plot':(fileItem.get('plotoutline') or fileItem.get('plot','')),'path':fileItem.get('trailer')} - return poolit(_parse)(fileList) - + if fileItem.get('trailer') and not fileItem.get('trailer','').startswith(tuple(VFS_TYPES)): + dur = self.jsonRPC.getDuration(fileItem.get('trailer'), accurate=True) + if dur > 0: tmpLST.append((fileItem.get('trailer'), dur)) + tmpLST = [] + poolit(_parse)(fileList) + if len(tmpLST) > 0: + tmpLST.reverse() + self.bctTypes['trailers']['items'].setdefault("resources",[]).extend(tmpLST) + self.bctTypes['trailers']['items']['resources'] = [t for t in (set(tuple(i) for i in self.bctTypes['trailers']['items']['resources']))] + print('buildKodiTrailers',self.bctTypes['trailers']['items']['resources']) - def getTrailers(self, fileList): - files = self.buildKodiTrailers(fileList) - print('getTrailers',files) - if files: return random.choice(files) - def injectBCTs(self, citem, fileList): nfileList = [] + if self.bctTypes['trailers']['enabled'] and SETTINGS.getSettingInt('Include_Trailers') < 2: + self.buildKodiTrailers(fileList) + for idx, fileItem in enumerate(fileList): if not fileItem: continue elif self.builder.service._interrupt() or self.builder.service._suspend(): break else: + chtype = citem.get('type','') + chname = citem.get('name','') + ftype = fileItem.get('type','') + fgenre = fileItem.get('genre',citem.get('group',[''])) + ftitle = fileItem.get('title',fileItem.get('label')) + fmpaa = fileItem.get('mpaa','NR') + runtime = fileItem.get('duration',0) + if runtime == 0: continue + + #pre roll - bumpers + if self.bctTypes['bumpers']['enabled']: + # #todo movie bumpers for audio/video codecs? imax bumpers? + if ftype.startswith(tuple(TV_TYPES)): + if chtype in ['Playlists','TV Networks','TV Genres','Mixed Genres','Custom']: + bkeys = ['resources',chname, fgenre[0]] if chanceBool(SETTINGS.getSettingInt('Random_Bumper_Chance')) else [chname, fgenre[0]] + file, dur = self.getBumper(bkeys) + if file: + runtime += dur + self.log('injectBCTs, adding bumper %s - %s'%(file,dur)) + if self.builder.pDialog: self.builder.pDialog = DIALOG.progressBGDialog(self.builder.pCount, self.builder.pDialog, message='%s - Inserting Filler: Bumpers'%(self.builder.pName),header='%s, %s'%(ADDON_NAME,self.builder.pMSG)) + nfileList.append(self.builder.buildCells(citem,dur,entries=1,info={'title':'%s (%s)'%(chname,fgenre[0]),'genre':['Bumper'],'plot':file,'path':file})[0]) + #pre roll - ratings - if fileItem.get('type').startswith(tuple(MOVIE_TYPES)): - if self.bctTypes['ratings'].get('enabled',False): - mpaa, file = self.getRating(fileItem.get('mpaa','NR')) - print('injectBCTs',mpaa,file) + if self.bctTypes['ratings']['enabled']: + if ftype.startswith(tuple(MOVIE_TYPES)): + mpaa, rkeys = self.convertMPAA(fmpaa) + file, dur = self.getRating(rkeys) if file: - dur = self.jsonRPC.getDuration(file, accurate=True) - if dur > 0: - self.log('injectBCTs, adding ratings %s\n%s - %s'%(fileItem.get('title'),file,dur)) - nfileList.append(self.builder.buildCells(citem,dur,entries=1,info={'title':fileItem.get('title'),'genre':['ratings'],'plot':'[B]%s (%s)[/B]\n%s'%(fileItem.get('title'),mpaa,RATING_DESC.get(mpaa,'')),'path':file})[0]) - # #pre roll - bumpers - # elif fileItem.get('type').startswith(tuple(TV_TYPES)): - # ... + runtime += dur + self.log('injectBCTs, adding rating %s - %s'%(file,dur)) + if self.builder.pDialog: self.builder.pDialog = DIALOG.progressBGDialog(self.builder.pCount, self.builder.pDialog, message='%s - Inserting Filler: Ratings'%(self.builder.pName),header='%s, %s'%(ADDON_NAME,self.builder.pMSG)) + nfileList.append(self.builder.buildCells(citem,dur,entries=1,info={'title':'%s (%s)'%(ftitle,mpaa),'genre':['Rating'],'plot':RATING_DESC.get(mpaa,''),'path':file})[0]) + + # original media nfileList.append(fileItem) - #post roll - commercials/trailers - # if self.bctTypes['trailers'].get('enabled',False): - # tmpItem = self.getTrailers(fileList): - # dur = self.jsonRPC.getDuration(tmpItem.get('file'),tmpItem, accurate=True) - # print('injectBCTs',tmpItem,dur) - # if dur > 0: nfileList.append(self.builder.buildCells(citem,dur,entries=1,info=tmpItem)[0]) + + # post roll - commercials + pfileList = [] + pfillRuntime = roundRuntimeUP(runtime) + self.log('injectBCTs, post roll current runtime %s, available runtime %s'%(runtime, pfillRuntime)) + if self.bctTypes['adverts']['enabled']: + acnt = 25 if self.bctTypes['adverts']['auto'] else self.bctTypes['adverts']['max'] + afillRuntime = (pfillRuntime // 2) if self.bctTypes['trailers']['enabled'] else pfillRuntime #if trailers enabled only fill half the required space, leaving room for trailers. + pfillRuntime -= afillRuntime + self.log('injectBCTs, advert fill runtime %s'%(afillRuntime)) + if chtype in ['Playlists','TV Networks','TV Genres','Mixed Genres','Custom']: + akeys = ['resources',chname, fgenre[0]] if chanceBool(SETTINGS.getSettingInt('Random_Advert_Chance')) else [chname, fgenre[0]] + for file, dur in self.getAdverts(akeys, acnt): + if file: + if afillRuntime <= 0: break + afillRuntime -= dur + self.log('injectBCTs, adding advert %s - %s'%(file,dur)) + if self.builder.pDialog: self.builder.pDialog = DIALOG.progressBGDialog(self.builder.pCount, self.builder.pDialog, message='%s - Inserting Filler: Adverts'%(self.builder.pName),header='%s, %s'%(ADDON_NAME,self.builder.pMSG)) + pfileList.append(self.builder.buildCells(citem,dur,entries=1,info={'title':'%s (%s)'%(chname,fgenre[0]),'genre':['Adverts'],'plot':file,'path':file})[0]) + + # post roll - trailers + if self.bctTypes['trailers']['enabled']: + self.log('injectBCTs, trailers fill runtime %s'%(pfillRuntime)) + tcnt = 25 if self.bctTypes['trailers']['auto'] else self.bctTypes['trailers']['max'] + self.log('injectBCTs, trailers fill runtime %s'%(pfillRuntime)) + if chtype in ['Playlists','TV Networks','TV Genres','Movie Genres','Movie Genres','Movie Studios','Mixed Genres','Custom']: + akeys = ['resources',chname, fgenre[0]] if chanceBool(SETTINGS.getSettingInt('Random_Trailers_Chance')) else [chname, fgenre[0]] + for file, dur in self.getTrailers(akeys, tcnt): + if file: + if pfillRuntime <= 0: break + pfillRuntime -= dur + self.log('injectBCTs, adding trailers %s - %s'%(file,dur)) + if self.builder.pDialog: self.builder.pDialog = DIALOG.progressBGDialog(self.builder.pCount, self.builder.pDialog, message='%s - Inserting Filler: Trailers'%(self.builder.pName),header='%s, %s'%(ADDON_NAME,self.builder.pMSG)) + pfileList.append(self.builder.buildCells(citem,dur,entries=1,info={'title':'%s (%s)'%(chname,fgenre[0]),'genre':['Trailers'],'plot':file,'path':file})[0]) + + if len(pfileList) > 0: + pfileList.shuffle() + nfileList.extend(pfileList) return nfileList + # getSettingInt('Include_Trailers') # HasAddon(plugin.video.imdb.trailers) # HasContent(Movies) # return fileList diff --git a/plugin.video.pseudotv.live/resources/lib/globals.py b/plugin.video.pseudotv.live/resources/lib/globals.py index 4086ca26..382ed233 100644 --- a/plugin.video.pseudotv.live/resources/lib/globals.py +++ b/plugin.video.pseudotv.live/resources/lib/globals.py @@ -90,7 +90,6 @@ def slugify(s, lowercase=False): s = re.sub(r'^-+|-+$', '', s) return s - def stripNumber(s): return re.sub(r'\d+','',s) @@ -100,7 +99,10 @@ def stripRegion(s): if match.group(1): return match.group(1) except: pass return s - + +def chanceBool(percent=25): + return random.randrange(100) < percent + def unquoteString(text): return urllib.parse.unquote(text) @@ -148,7 +150,7 @@ def setURL(url, file): except Exception as e: log("saveURL, failed! %s"%e, xbmc.LOGERROR) -def removeDUPDICT(d): +def removeDUPDICT(l): return [dict(t) for t in {tuple(d.items()) for d in l}] def diffLSTDICT(old, new): @@ -158,7 +160,9 @@ def diffLSTDICT(old, new): return setDictLST([loadJSON(e) for e in sDIFF]) def setInstanceID(): - PROPERTIES.setEXTProperty('%s.InstanceID'%(ADDON_ID),uuid.uuid4()) + instanceID = PROPERTIES.getEXTProperty('%s.InstanceID'%(ADDON_ID)) + if instanceID: PROPERTIES.clearTrash(instanceID) + PROPERTIES.setEXTProperty('%s.InstanceID'%(ADDON_ID),getMD5(uuid.uuid4())) def getInstanceID(): instanceID = PROPERTIES.getEXTProperty('%s.InstanceID'%(ADDON_ID)) @@ -281,6 +285,12 @@ def pagination(list, end): for start in range(0, len(list), end): yield seq[start:start+end] +def roundRuntimeUP(runtime): + runtime = datetime.datetime.fromtimestamp(runtime) + next_half_hour = runtime.replace(minute=0, second=0) + datetime.timedelta(minutes=30) + if next_half_hour < runtime: next_half_hour += datetime.timedelta(days=1) + return (next_half_hour - runtime).total_seconds() + def roundTimeDown(dt, offset=30): # round the given time down to the nearest n = datetime.datetime.fromtimestamp(dt) delta = datetime.timedelta(minutes=offset) @@ -500,6 +510,12 @@ def getDiscovery(): def setDiscovery(servers={}): return PROPERTIES.setEXTProperty('%s.SERVER_DISCOVERY'%(ADDON_ID),dumpJSON(servers)) +def disableTrakt(): + PROPERTIES.setEXTProperty('script.trakt.paused','true') + +def clearTrakt(): + PROPERTIES.clearEXTProperty('script.trakt.paused') + def chunkLst(lst, n): for i in range(0, len(lst), n): yield lst[i:i + n] diff --git a/plugin.video.pseudotv.live/resources/lib/jsonrpc.py b/plugin.video.pseudotv.live/resources/lib/jsonrpc.py index 8d4c952b..89f405f5 100644 --- a/plugin.video.pseudotv.live/resources/lib/jsonrpc.py +++ b/plugin.video.pseudotv.live/resources/lib/jsonrpc.py @@ -39,8 +39,8 @@ def log(self, msg, level=xbmc.LOGDEBUG): def sendLocker(self): #kodi jsonrpc not thread safe avoid request collision during threading. if PROPERTIES.getEXTProperty('%s.sendLocker'%(ADDON_ID)) == 'true': while not MONITOR.abortRequested(): - if MONITOR.waitForAbort(0.001): break - elif not PROPERTIES.getEXTProperty('%s.sendLocker'%(ADDON_ID)) == 'true': break + if not PROPERTIES.getEXTProperty('%s.sendLocker'%(ADDON_ID)) == 'true': break + elif MONITOR.waitForAbort(0.001): break PROPERTIES.setEXTProperty('%s.sendLocker'%(ADDON_ID),'true') try: yield finally: @@ -57,7 +57,6 @@ def _sendJSON(self, command): def sendJSON(self, param, timeout=15): #todo dynamic timeout based on parmas and timeout history. with self.sendLocker(): - self.log('sendJSON, timeout = %s'%(timeout)) command = param command["jsonrpc"] = "2.0" command["id"] = ADDON_ID @@ -75,8 +74,9 @@ def queueJSON(self, param): queuePool = SETTINGS.getCacheSetting('queuePool', json_data=True, default={}) params = queuePool.setdefault('params',[]) params.append(param) - queuePool['params'] = setDictLST(params) - self.log("queueJSON, queueing = %s"%(len(queuePool['params']))) + queuePool['params'] = sorted(setDictLST(params), key=lambda d: d.get('params',{}).get('playcount',-1)) + queuePool['params'].reverse() #prioritize playcount rollback over duration amendments. + self.log("queueJSON, queueing = %s\n%s"%(len(queuePool['params']),param)) SETTINGS.setCacheSetting('queuePool', queuePool, json_data=True) @@ -270,18 +270,19 @@ def getPVRBroadcastDetails(self, id): def getDuration(self, path, item={}, accurate=bool(SETTINGS.getSettingInt('Duration_Type'))): self.log("getDuration, accurate = %s, path = %s" % (accurate, path)) - duration = 0 - for runtime in [int(item.get('runtime', '0') or '0'),int(item.get('duration', '0') or '0'),int((item.get('streamdetails', {}).get('video',[]) or [{}])[0].get('duration','') or '0')]: - if runtime != 0: break + for runtime in [float(item.get('runtime' , '0') or '0'), + float(item.get('duration', '0') or '0'), + float((item.get('streamdetails', {}).get('video',[]) or [{}])[0].get('duration','') or '0')]: + if runtime > 0: break if (runtime == 0 or accurate): if not path.startswith(tuple(VFS_TYPES)):# no additional parsing needed item[runtime] has only meta available. + duration = 0 if isStack(path):# handle "stacked" videos paths = splitStacks(path) for file in paths: duration += self.parseDuration(file) - else: - duration = self.parseDuration(path, item) + else: duration = self.parseDuration(path, item) if duration > 0: runtime = duration self.log("getDuration, path = %s, runtime = %s" % (path, runtime)) return runtime @@ -309,20 +310,51 @@ def parseDuration(self, path, item={}, save=SETTINGS.getSettingBool('Store_Durat 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 save and runsafe and (item.get('id', -1) > 0): self.queDuration(item['type'], item.get('id', -1), duration) + if save and runsafe and item.get('type'): self.queDuration(item, duration) if runsafe: runtime = duration self.log("parseDuration, returning runtime = %s" % (runtime)) return runtime - def queDuration(self, media, dbid, dur): - self.log('queDuration, media = %s, dbid = %s, dur = %s' % (media, dbid, dur)) - param = {'movie' : {"method":"VideoLibrary.SetMovieDetails" ,"params":{"movieid":dbid, "runtime":dur}}, - 'episode' : {"method":"VideoLibrary.SetEpisodeDetails" ,"params":{"episodeid":dbid, "runtime":dur}}, - 'musicvideo': {"method":"VideoLibrary.SetMusicVideoDetails","params":{"musicvideoid":dbid, "runtime":dur}}, - 'song' : {"method":"AudioLibrary.SetSongDetails" ,"params":{"songid":dbid, "runtime":dur}}} - self.queueJSON(param[media]) + def queDuration(self, item, dur): + #overcome inconsistent keys from Kodis jsonRPC. + param = {'video' : {}, + 'movie' : {"method":"VideoLibrary.SetMovieDetails" ,"params":{"movieid" :item.get('id',-1) , "runtime": dur}}, + 'movies' : {"method":"VideoLibrary.SetMovieDetails" ,"params":{"movieid" :item.get('movieid',-1) , "runtime": dur}}, + 'episode' : {"method":"VideoLibrary.SetEpisodeDetails" ,"params":{"episodeid" :item.get('id',-1) , "runtime": dur}}, + 'episodes' : {"method":"VideoLibrary.SetEpisodeDetails" ,"params":{"episodeid" :item.get('episodeid',-1) , "runtime": dur}}, + 'musicvideo' : {"method":"VideoLibrary.SetMusicVideoDetails","params":{"musicvideoid":item.get('id',-1) , "runtime": dur}}, + 'musicvideos': {"method":"VideoLibrary.SetMusicVideoDetails","params":{"musicvideoid":item.get('musicvideoid',-1) , "runtime": dur}}, + 'song' : {"method":"AudioLibrary.SetSongDetails" ,"params":{"songid" :item.get('id',-1) , "runtime": dur}}, + 'songs' : {"method":"AudioLibrary.SetSongDetails" ,"params":{"songid" :item.get('songid',-1) , "runtime": dur}}} + try: + params = param[item['type']] + if -1 in params: raise Exception('no dbid found') + elif params: + self.log('queDuration, media = %s, dur = %s' % (item['type'], dur)) + self.queueJSON(params) + except Exception as e: self.log("queDuration, failed! %s\nitem = %s"%(e,item), xbmc.LOGERROR) + + def quePlaycount(self, item): + #overcome inconsistent keys from Kodis jsonRPC. + param = {'video' : {}, + 'movie' : {"method":"VideoLibrary.SetMovieDetails" ,"params":{"movieid" :item.get('id',-1) , "playcount": item.get('playcount',0), "resume": {"position": item.get('position',0), "total": item.get('total',0)}}}, + 'movies' : {"method":"VideoLibrary.SetMovieDetails" ,"params":{"movieid" :item.get('movieid',-1) , "playcount": item.get('playcount',0), "resume": {"position": item.get('position',0), "total": item.get('total',0)}}}, + 'episode' : {"method":"VideoLibrary.SetEpisodeDetails" ,"params":{"episodeid" :item.get('id',-1) , "playcount": item.get('playcount',0), "resume": {"position": item.get('position',0), "total": item.get('total',0)}}}, + 'episodes' : {"method":"VideoLibrary.SetEpisodeDetails" ,"params":{"episodeid" :item.get('episodeid',-1) , "playcount": item.get('playcount',0), "resume": {"position": item.get('position',0), "total": item.get('total',0)}}}, + 'musicvideo' : {"method":"VideoLibrary.SetMusicVideoDetails","params":{"musicvideoid":item.get('id',-1) , "playcount": item.get('playcount',0), "resume": {"position": item.get('position',0), "total": item.get('total',0)}}}, + 'musicvideos': {"method":"VideoLibrary.SetMusicVideoDetails","params":{"musicvideoid":item.get('musicvideoid',-1) , "playcount": item.get('playcount',0), "resume": {"position": item.get('position',0), "total": item.get('total',0)}}}, + 'song' : {"method":"AudioLibrary.SetSongDetails" ,"params":{"songid" :item.get('id',-1) , "playcount": item.get('playcount',0), "resume": {"position": item.get('position',0), "total": item.get('total',0)}}}, + 'songs' : {"method":"AudioLibrary.SetSongDetails" ,"params":{"songid" :item.get('songid',-1) , "playcount": item.get('playcount',0), "resume": {"position": item.get('position',0), "total": item.get('total',0)}}}} + try: + params = param[item['type']] + if -1 in params: raise Exception('no dbid found') + elif params: + self.log('quePlaycount, media = %s, count = %s' % (item['type'],item.get('playcount',0))) + self.queueJSON(params) + except Exception as e: self.log("quePlaycount, failed! %s\nitem = %s"%(e,item), xbmc.LOGERROR) + def requestList(self, citem, path, media='video', page=SETTINGS.getSettingInt('Page_Limit'), sort={}, limits={}, query={}): # {"method": "VideoLibrary.GetEpisodes", diff --git a/plugin.video.pseudotv.live/resources/lib/kodi.py b/plugin.video.pseudotv.live/resources/lib/kodi.py index 75860759..2efb8e85 100644 --- a/plugin.video.pseudotv.live/resources/lib/kodi.py +++ b/plugin.video.pseudotv.live/resources/lib/kodi.py @@ -108,6 +108,18 @@ def getMD5(text,hash=0,hexit=True): if hexit: return hex(hash)[2:].upper().zfill(8) else: return hash +def convertString(data): + if isinstance(data, dict): + return dumpJSON(data) + elif isinstance(data, list): + return ', '.join(data) + elif isinstance(data, bytes): + return data.decode(DEFAULT_ENCODING) + elif not isinstance(data, str): + return str(data) + else: + return data + class Settings: #Kodi often breaks settings API with changes between versions. Stick with core setsettings/getsettings to avoid specifics; that may break. def __init__(self): @@ -119,18 +131,19 @@ def log(self, msg, level=xbmc.LOGDEBUG): log('%s: %s'%(self.__class__.__name__,msg),level) + def getRealSettings(self): + try: return xbmcaddon.Addon(id=ADDON_ID) + except: return REAL_SETTINGS + + def updateSettings(self): self.log('updateSettings') #todo build json of third-party addon settings # self.pluginMeta.setdefault(addonID,{})['settings'] = [{'key':'value'}] - def getRealSettings(self): - try: return xbmcaddon.Addon(id=ADDON_ID) - except: return REAL_SETTINGS - - - def openSettings(self): + def openSettings(self): + self.log('openSettings') REAL_SETTINGS.openSettings() @@ -138,7 +151,7 @@ def openSettings(self): def _getSetting(self, func, key): try: value = func(key) - self.log('%s, key = %s, value = %s'%(func.__name__,key,value)) + self.log('%s, key = %s, value = %s'%(func.__name__,key,'%s...'%((convertString(value)[:128])))) return value except Exception as e: self.log("_getSetting, failed! %s - key = %s"%(e,key), xbmc.LOGERROR) @@ -203,7 +216,7 @@ def getPropertySetting(self, key): #SET def _setSetting(self, func, key, value): try: - self.log('%s, key = %s, value = %s'%(func.__name__,key,value)) + self.log('%s, key = %s, value = %s'%(func.__name__,key,'%s...'%((convertString(value)[:128])))) return func(key, value) except Exception as e: self.log("_setSetting, failed! %s - key = %s"%(e,key), xbmc.LOGERROR) @@ -416,42 +429,53 @@ def log(self, msg, level=xbmc.LOGDEBUG): log('%s: %s'%(self.__class__.__name__,msg),level) + def setInstanceID(self): + instanceID = self.getEXTProperty('%s.InstanceID'%(ADDON_ID)) + if instanceID: self.clearTrash(instanceID) + self.setEXTProperty('%s.InstanceID'%(ADDON_ID),getMD5(uuid.uuid4())) + + def getInstanceID(self): instanceID = self.getEXTProperty('%s.InstanceID'%(ADDON_ID)) - if not instanceID: self.setEXTProperty('%s.InstanceID'%(ADDON_ID),uuid.uuid4()) + if not instanceID: self.setInstanceID() return self.getEXTProperty('%s.InstanceID'%(ADDON_ID)) def getKey(self, key, instanceID=True): if self.winID == 10000 and not key.startswith(ADDON_ID): #create unique id - if instanceID: return '%s.%s.%s'%(ADDON_ID,key,getMD5(self.InstanceID)) + if instanceID: return self.setTrash('%s.%s.%s'%(ADDON_ID,key,self.InstanceID)) else: return '%s.%s'%(ADDON_ID,key) return key #CLEAR def clearEXTProperty(self, key): + self.log('clearEXTProperty, id = %s, key = %s'%(10000,key)) return xbmcgui.Window(10000).clearProperty(key) def clearProperties(self): + self.log('clearProperties') return self.window.clearProperties() def clearProperty(self, key): - return self.window.clearProperty(self.getKey(key)) + key = self.getKey(key) + self.log('clearProperty, id = %s, key = %s'%(self.winID,key)) + return self.window.clearProperty(key) #GET def getEXTProperty(self, key): value = xbmcgui.Window(10000).getProperty(key) - self.log('getEXTProperty, id = %s, key = %s, value = %s'%(10000,key,value)) + self.log('getEXTProperty, id = %s, key = %s, value = %s'%(10000,key,'%s...'%(convertString(value)[:128]))) return value def getProperty(self, key): - value = self.window.getProperty(self.getKey(key)) - self.log('getProperty, id = %s, key = %s, value = %s'%(self.winID,self.getKey(key),value)) + key = self.getKey(key) + value = self.window.getProperty(key) + self.log('getProperty, id = %s, key = %s, value = %s'%(self.winID,key,'%s...'%(convertString(value)[:128]))) return value @@ -477,13 +501,13 @@ def getPropertyFloat(self, key): #SET def setEXTProperty(self, key, value): - self.log('setEXTProperty, id = %s, key = %s, value = %s'%(10000,key,str(value))) + self.log('setEXTProperty, id = %s, key = %s, value = %s'%(10000,key,'%s...'%((convertString(value)[:128])))) return xbmcgui.Window(10000).setProperty(key,str(value)) def setProperty(self, key, value: str) -> bool: key = self.getKey(key) - self.log('setProperty, id = %s, key = %s, value = %s'%(self.winID,key,value)) + self.log('setProperty, id = %s, key = %s, value = %s'%(self.winID,key,'%s...'%((convertString(value)[:128])))) self.window.setProperty(key, str(value)) return True @@ -507,6 +531,19 @@ def setPropertyInt(self, key, value): def setPropertyFloat(self, key, value): return self.setProperty(key, float(value)) + + def setTrash(self, key): #catalog instance properties that may become abandoned. + tmpDCT = loadJSON(self.getEXTProperty('%s.TRASH'%(ADDON_ID))) + if key not in tmpDCT.setdefault(self.InstanceID,[]): + tmpDCT.setdefault(self.InstanceID,[]).append(key) + self.setEXTProperty('%s.TRASH'%(ADDON_ID),dumpJSON(tmpDCT)) + return key + + + def clearTrash(self, instanceID=None): #clear abandoned properties after instanceID change + tmpDCT = loadJSON(self.getEXTProperty('%s.TRASH'%(ADDON_ID))) + for prop in tmpDCT.get(instanceID,[]): self.clearEXTProperty(prop) + class ListItems: def __init__(self): @@ -910,7 +947,6 @@ def buildMenuItem(option): def buildDXSP(self, params={}): # https://github.com/xbmc/xbmc/blob/master/xbmc/playlists/SmartPlayList.cpp from jsonrpc import JSONRPC - jsonRPC = JSONRPC() def type(): enumLST = ['songs', 'albums', 'artists', 'movies', 'tvshows', 'episodes', 'musicvideos', 'mixed'] @@ -930,19 +966,19 @@ def field(rules=[]): #rules = {"and":[]} print('field',rules,params) params['type'] = type() if params['type'] == 'songs': - enumLST = jsonRPC.getEnums("List.Filter.Fields.Songs", type='items') + enumLST = JSONRPC().getEnums("List.Filter.Fields.Songs", type='items') elif params['type'] == 'albums': - enumLST = jsonRPC.getEnums("List.Filter.Fields.Albums", type='items') + enumLST = JSONRPC().getEnums("List.Filter.Fields.Albums", type='items') elif params['type'] == 'artists': - enumLST = jsonRPC.getEnums("List.Filter.Fields.Artists", type='items') + enumLST = JSONRPC().getEnums("List.Filter.Fields.Artists", type='items') elif params['type'] == 'tvshows': - enumLST = jsonRPC.getEnums("List.Filter.Fields.TVShows", type='items') + enumLST = JSONRPC().getEnums("List.Filter.Fields.TVShows", type='items') elif params['type'] == 'episodes': - enumLST = jsonRPC.getEnums("List.Filter.Fields.Episodes", type='items') + enumLST = JSONRPC().getEnums("List.Filter.Fields.Episodes", type='items') elif params['type'] == 'movies': - enumLST = jsonRPC.getEnums("List.Filter.Fields.Movies", type='items') + enumLST = JSONRPC().getEnums("List.Filter.Fields.Movies", type='items') elif params['type'] == 'musicvideos': - enumLST = jsonRPC.getEnums("List.Filter.Fields.MusicVideos") + enumLST = JSONRPC().getEnums("List.Filter.Fields.MusicVideos") elif params['type'] == 'mixed': enumLST = ['playlist', 'virtualfolder'] else: return @@ -955,7 +991,7 @@ def field(rules=[]): #rules = {"and":[]} def operator(rule): #rule = {"field":""} print('operator',rule,params) - enumLST = jsonRPC.getEnums("List.Filter.Operators") + enumLST = JSONRPC().getEnums("List.Filter.Operators") enumSEL = -1 if rule["field"] != 'date': if 'inthelast' in enumLST: enumLST.remove('inthelast') diff --git a/plugin.video.pseudotv.live/resources/lib/library.py b/plugin.video.pseudotv.live/resources/lib/library.py index 055a4c22..d960c9ee 100644 --- a/plugin.video.pseudotv.live/resources/lib/library.py +++ b/plugin.video.pseudotv.live/resources/lib/library.py @@ -19,7 +19,6 @@ # -*- coding: utf-8 -*- from globals import * -from jsonrpc import JSONRPC from predefined import Predefined from resources import Resources from channels import Channels @@ -28,6 +27,8 @@ REG_KEY = 'PseudoTV_Recommended.%s' class Service: + from jsonrpc import JSONRPC + jsonRPC = JSONRPC() def _interrupt(self, wait=.001) -> bool: #break return MONITOR.waitForAbort(wait) @@ -43,7 +44,7 @@ def __init__(self, service=None): self.parserMSG = '' self.parserDialog = None self.cache = Cache() - self.jsonRPC = JSONRPC() + self.jsonRPC = service.jsonRPC self.predefined = Predefined() self.channels = Channels() self.resources = Resources(self.jsonRPC,self.cache) diff --git a/plugin.video.pseudotv.live/resources/lib/manager.py b/plugin.video.pseudotv.live/resources/lib/manager.py index 5cc54c45..1d192bb9 100644 --- a/plugin.video.pseudotv.live/resources/lib/manager.py +++ b/plugin.video.pseudotv.live/resources/lib/manager.py @@ -104,6 +104,7 @@ def __init__(self, *args, **kwargs): self.resources = Resources(self.jsonRPC, self.cache) self.newChannel = self.channels.getTemplate() + print('newChannel',self.newChannel) self.channelList = sorted(self.createChannelList(self.buildArray(), self.channels.getChannels()), key=lambda k: k['number']) self.channelList.extend(self.channels.getAutotuned()) self.newChannels = self.channelList.copy() diff --git a/plugin.video.pseudotv.live/resources/lib/multiroom.py b/plugin.video.pseudotv.live/resources/lib/multiroom.py index b525932a..05ac07e2 100644 --- a/plugin.video.pseudotv.live/resources/lib/multiroom.py +++ b/plugin.video.pseudotv.live/resources/lib/multiroom.py @@ -83,9 +83,6 @@ def pairDiscovery(self,wait=60): # SETTINGS.chkDiscovery(servers) - - - def chkDiscovery(self, servers, forced=False): current_server = self.getSetting('Remote_URL') if (not current_server or forced) and len(list(servers.keys())) == 1: diff --git a/plugin.video.pseudotv.live/resources/lib/overlay.py b/plugin.video.pseudotv.live/resources/lib/overlay.py index 6b26b4f2..86cf8b62 100644 --- a/plugin.video.pseudotv.live/resources/lib/overlay.py +++ b/plugin.video.pseudotv.live/resources/lib/overlay.py @@ -20,7 +20,6 @@ # -*- coding: utf-8 -*- from globals import * -from jsonrpc import JSONRPC from resources import Resources # class Video(xbmcgui.WindowXML): @@ -95,7 +94,8 @@ class Overlay(): showingOverlay = False controlManager = dict() - def __init__(self, player, runActions): + def __init__(self, jsonRPC, player, runActions): + self.jsonRPC = jsonRPC self.player = player self.runActions = runActions @@ -260,11 +260,9 @@ def getWait(state): if SETTINGS.getSettingBool('Force_Diffuse'): self._channelBug.setColorDiffuse(self.channelBugColor) elif hasAddon('script.module.pil'): - jsonRPC = JSONRPC() - resources = Resources(jsonRPC,jsonRPC.cache) + resources = Resources(self.jsonRPC,self.jsonRPC.cache) if resources.isMono(logo): self._channelBug.setColorDiffuse(self.channelBugColor) del resources - del jsonRPC self.setVisible(self._channelBug,True) self.setImage(self._channelBug,logo) @@ -319,7 +317,7 @@ def getOnNextInterval(interval=3): else: chname = citem.get('name',BUILTIN.getInfoLabel('ChannelName','VideoPlayer')) nowTitle = fitem.get('label',BUILTIN.getInfoLabel('Title','VideoPlayer')) - nextTitle = fitem.get('showlabel',BUILTIN.getInfoLabel('NextTitle','VideoPlayer')) + nextTitle = nitem.get('showlabel',BUILTIN.getInfoLabel('NextTitle','VideoPlayer')) onNow = '%s on %s'%(nowTitle,chname) if chname not in nowTitle else fitem.get('showlabel',nowTitle) onNext = '%s @ %s'%(nextTitle,BUILTIN.getInfoLabel('NextStartTime','VideoPlayer')) @@ -381,6 +379,4 @@ def updateUpNext(self, nowItem={}, nextItem={}): "runtime" :(nextItem.get("runtime" ,"") or "")}} data.update(next_episode) except: pass - jsonRPC = JSONRPC() - jsonRPC.notifyAll(message='upnext_data', data=binascii.hexlify(json.dumps(data).encode('utf-8')).decode('utf-8'), sender='%s.SIGNAL'%(ADDON_ID)) - del jsonRPC \ No newline at end of file + self.jsonRPC.notifyAll(message='upnext_data', data=binascii.hexlify(json.dumps(data).encode('utf-8')).decode('utf-8'), sender='%s.SIGNAL'%(ADDON_ID)) \ No newline at end of file diff --git a/plugin.video.pseudotv.live/resources/lib/plugin.py b/plugin.video.pseudotv.live/resources/lib/plugin.py index 8f4b2e7d..468a7f4f 100644 --- a/plugin.video.pseudotv.live/resources/lib/plugin.py +++ b/plugin.video.pseudotv.live/resources/lib/plugin.py @@ -55,8 +55,12 @@ def __init__(self, sysARG=sys.argv): "fitem" : decodePlot(BUILTIN.getInfoLabel('Plot')), "isPlaylist": bool(SETTINGS.getSettingInt('Playback_Method'))}) + if not self.sysInfo.get('start') and self.sysInfo['fitem']: + self.sysInfo['start'] = self.sysInfo['fitem'].get('start') + self.sysInfo['stop'] = self.sysInfo['fitem'].get('stop') + try: self.sysInfo['seek'] = float(self.sysInfo['now']) - float(self.sysInfo['start']) - except: self.sysInfo['seek'] = None + except: self.sysInfo['seek'] = -1 try: self.sysInfo["citem"] = self.sysInfo["fitem"].pop('citem') except: self.sysInfo["citem"] = {'id':self.sysInfo['chid']} @@ -78,8 +82,9 @@ def playVOD(self, title, vid): def playLive(self, name, chid, vid): with self.preparingPlayback(): - # if self.sysInfo['seek'] <= self.seekTOL: self.sysInfo['seek'] = 0 self.log('playLive, id = %s, seek = %s'%(chid,self.sysInfo['seek'])) + # if self.sysInfo['seek'] <= self.seekTOL: self.sysInfo['seek'] = 0 + # if round(self.sysInfo['seek']) <= self.seekTOL or round(nowitem['progresspercentage']) > self.seekTHD: liz = xbmcgui.ListItem(name,path=vid) liz.setProperty("IsPlayable","true") liz.setProperty('sysInfo',dumpJSON(self.sysInfo)) @@ -128,57 +133,65 @@ def playPlaylist(self, name, chid): def buildfItem(item={}, media='video'): return LISTITEMS.buildItemListItem(decodePlot(item.get('plot','')), media) + found = False listitems = [xbmcgui.ListItem()] fitem = self.sysInfo.get('fitem') pvritem = self.matchChannel(name,chid,radio=False,isPlaylist=True) if pvritem.get('citem'): self.sysInfo['citem'].update(pvritem.pop('citem')) if pvritem: + pastItems = pvritem.get('broadcastpast',[]) nowitem = pvritem.get('broadcastnow',{}) nextitems = pvritem.get('broadcastnext',[]) # upcoming items nextitems.insert(0,nowitem) - - for pos, nextitem in enumerate(nextitems): + for pos, nextitem in enumerate(pastItems + nextitems): fitem = decodePlot(nextitem.get('plot',{})) - if (fitem.get('file') == self.sysInfo.get('fitem',{}).get('file') and fitem.get('idx') == self.sysInfo.get('fitem',{}).get('idx')) or (nextitem.get('start') == datetime.datetime.fromtimestamp((datetime.datetime.timestamp(strpTime(self.sysInfo['start',random.random()], DTJSONFORMAT)) - getTimeoffset())).strftime(DTJSONFORMAT)): + if (fitem.get('file') == self.sysInfo.get('fitem',{}).get('file') and fitem.get('idx') == self.sysInfo.get('fitem',{}).get('idx')): + found = True del nextitems[0:pos] # start array at correct position break - - nowitem = nextitems.pop(0) - liz = LISTITEMS.buildItemListItem(fitem) - if round(nowitem['progress']) <= self.seekTOL or round(nowitem['progresspercentage']) > self.seekTHD: - self.log('playPlaylist, progress start at the beginning') - nowitem['progress'] = 0 - nowitem['progresspercentage'] = 0 - - if (nowitem['progress'] > 0 and nowitem['runtime'] > 0): - self.log('playPlaylist, within seek tolerance setting seek totaltime = %s, resumetime = %s'%((nowitem['runtime'] * 60),nowitem['progress'])) - liz.setProperty('startoffset', str(nowitem['progress'])) #secs - infoTag = ListItemInfoTag(liz, 'video') - infoTag.set_resume_point({'ResumeTime':nowitem['progress'],'TotalTime':(nowitem['runtime'] * 60)}) + + if found: + nowitem = nextitems.pop(0) + liz = LISTITEMS.buildItemListItem(fitem) + if round(nowitem['progresspercentage']) > self.seekTHD: + self.log('playPlaylist, progress past threshold advance to nextitem') + nowitem = nextitems.pop(0) - del nextitems[PAGE_LIMIT:]# list of upcoming items, truncate for speed. - self.sysInfo['fitem'] = fitem - pvritem['broadcastnow'] = nowitem # current item - pvritem['broadcastnext'] = nextitems # upcoming items - self.sysInfo['pvritem'] = pvritem - liz.setProperty('sysInfo',dumpJSON(self.sysInfo)) - listitems = [liz] - listitems.extend(poolit(buildfItem)(nextitems)) - channelPlaylist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) - channelPlaylist.clear() - xbmc.sleep(100) - - for idx,lz in enumerate(listitems): - lz.setProperty('sysInfo',dumpJSON(self.sysInfo)) - channelPlaylist.add(lz.getPath(),lz,idx) + if round(nowitem['progress']) <= self.seekTOL: + self.log('playPlaylist, progress start at the beginning') + nowitem['progress'] = 0 + nowitem['progresspercentage'] = 0 + + if (nowitem['progress'] > 0 and nowitem['runtime'] > 0): + self.log('playPlaylist, within seek tolerance setting seek totaltime = %s, resumetime = %s'%((nowitem['runtime'] * 60),nowitem['progress'])) + liz.setProperty('startoffset', str(nowitem['progress'])) #secs + infoTag = ListItemInfoTag(liz, 'video') + infoTag.set_resume_point({'ResumeTime':nowitem['progress'],'TotalTime':(nowitem['runtime'] * 60)}) + + del nextitems[PAGE_LIMIT:]# list of upcoming items, truncate for speed. + self.sysInfo['fitem'] = fitem + pvritem['broadcastnow'] = nowitem # current item + pvritem['broadcastnext'] = nextitems # upcoming items + self.sysInfo['pvritem'] = pvritem + liz.setProperty('sysInfo',dumpJSON(self.sysInfo)) + listitems = [liz] + listitems.extend(poolit(buildfItem)(nextitems)) + channelPlaylist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + channelPlaylist.clear() + xbmc.sleep(100) - self.log('playPlaylist, Playlist size = %s'%(channelPlaylist.size())) - if isPlaylistRandom(): channelPlaylist.unshuffle() - PLAYER.play(channelPlaylist,windowed=True) - else: self.playError() - + for idx,lz in enumerate(listitems): + lz.setProperty('sysInfo',dumpJSON(self.sysInfo)) + channelPlaylist.add(lz.getPath(),lz,idx) + + self.log('playPlaylist, Playlist size = %s'%(channelPlaylist.size())) + if isPlaylistRandom(): channelPlaylist.unshuffle() + PLAYER.play(channelPlaylist,windowed=True) + else: DIALOG.notificationDialog(LANGUAGE(32164)) + else: DIALOG.notificationDialog(LANGUAGE(32000)) + @timeit def matchChannel(self, chname, id, radio=False, isPlaylist=False): self.log('matchChannel, id = %s, chname = %s, radio = %s, isPlaylist = %s'%(id,chname,radio,isPlaylist)) @@ -235,7 +248,9 @@ def _match(): def _extend(pvritem): channelItem = {} def _parseBroadcast(broadcast={}): - if broadcast.get('progresspercentage',0) > 0 and broadcast.get('progresspercentage',0) != 100: + if broadcast.get('progresspercentage',0) == 100: + channelItem.setdefault('broadcastpast',[]).append(broadcast) + elif broadcast.get('progresspercentage',0) > 0 and broadcast.get('progresspercentage',0) != 100: channelItem['broadcastnow'] = broadcast elif broadcast.get('progresspercentage',0) == 0 and broadcast.get('progresspercentage',0) != 100: channelItem.setdefault('broadcastnext',[]).append(broadcast) @@ -262,33 +277,32 @@ def _parseBroadcast(broadcast={}): def playCheck(self, oldInfo={}): + #check that resource or plugin installed? self.log('playCheck, id = %s\n%s'%(oldInfo.get('chid','-1'),oldInfo)) - if oldInfo.get('chid',random.random()) == self.sysInfo.get('chid') and oldInfo.get('start',random.random()) == self.sysInfo.get('start'): - self.sysInfo['playcount'] = oldInfo.get('playcount',0) + 1 - self.sysInfo['runtime'] = oldInfo.get('runtime',-1) - if self.sysInfo['duration'] > self.sysInfo['runtime']: + if self.sysInfo.get('chid') == oldInfo.get('chid',random.random()) and self.sysInfo.get('start') == oldInfo.get('start',random.random()): + self.sysInfo['playcount'] = oldInfo.get('playcount',0) + 1 #carry over playcount + self.sysInfo['runtime'] = oldInfo.get('runtime',-1) #carry over previous player runtime + + if self.sysInfo['now'] >= self.sysInfo['stop']: + self.log('playCheck, failed! Current time (%s) is past the contents stop time (%s).'%(self.sysInfo['now'],self.sysInfo['stop'])) + elif self.sysInfo['duration'] > self.sysInfo['runtime']: self.log('playCheck, failed! Duration error between player (%s) and pvr (%s).'%(self.sysInfo['duration'],self.sysInfo['runtime'])) - # return False - elif self.sysInfo['seek'] >= oldInfo['runtime']: - self.log('playCheck, failed! Seeking past duration.') - # return False - elif self.sysInfo['seek'] == oldInfo['seek']: + elif self.sysInfo['seek'] >= oldInfo.get('runtime',self.sysInfo['duration']): + self.log('playCheck, failed! Seeking to a position (%s) past contents runtime (%s).'%(self.sysInfo['seek'],oldInfo.get('runtime',self.sysInfo['duration']))) + elif self.sysInfo['seek'] == oldInfo.get('seek',self.sysInfo['seek']): self.log('playCheck, failed! Seeking to same position.') - # return False return True def playError(self): - MONITOR.waitForAbort(1) #allow a full second to pass beyond any msecs differential. self.log('playError, id = %s, attempt = %s\n%s'%(self.sysInfo.get('chid','-1'),self.sysInfo['playcount'],self.sysInfo)) PROPERTIES.setEXTProperty('%s.lastPlayed.sysInfo'%(ADDON_ID),dumpJSON(self.sysInfo)) - if self.sysInfo['playcount'] == 1 and not PLAYER.isPlaying(): setInstanceID() #reset instance and force cache flush. - elif self.sysInfo['playcount'] == 2: + if self.sysInfo['playcount'] in [1,2,3]: with busy_dialog(): DIALOG.notificationWait(LANGUAGE(32038)%(self.sysInfo.get('playcount',0))) self.resolveURL(False, xbmcgui.ListItem()) #release pending playback. + MONITOR.waitForAbort(PROMPT_DELAY/1000) #allow a full second to pass beyond any msecs differential. return BUILTIN.executebuiltin('PlayMedia(%s%s)'%(self.sysARG[0],self.sysARG[2])) #retry channel - elif self.sysInfo['playcount'] == 3: bruteForcePVR() elif self.sysInfo['playcount'] == 4: DIALOG.okDialog(LANGUAGE(32134)%(ADDON_NAME)) else: DIALOG.notificationWait(LANGUAGE(32000)) self.resolveURL(False, xbmcgui.ListItem()) #release pending playback. diff --git a/plugin.video.pseudotv.live/resources/lib/pool.py b/plugin.video.pseudotv.live/resources/lib/pool.py index 17af3db1..8be0a30a 100644 --- a/plugin.video.pseudotv.live/resources/lib/pool.py +++ b/plugin.video.pseudotv.live/resources/lib/pool.py @@ -64,6 +64,7 @@ def run(self): try: self.result = method(*args, **kwargs) except: self.error = sys.exc_info()[0] timer = waiter() + timer.daemon=True timer.start() timer.join(timeout) if timer.is_alive(): @@ -111,8 +112,7 @@ def wrapper(items=[], *args, **kwargs): results = pool.executors(method, items, *args, **kwargs) else: results = pool.generator(method, items, *args, **kwargs) - except Exception as e: - log('poolit, failed! %s'%(e), xbmc.LOGERROR) + except Exception as e: log('poolit, failed! %s'%(e), xbmc.LOGERROR) results = pool.generator(method, items, *args, **kwargs) log('%s => %s'%(pool.__class__.__name__, method.__qualname__.replace('.',': '))) return list([_f for _f in results if _f]) diff --git a/plugin.video.pseudotv.live/resources/lib/rules.py b/plugin.video.pseudotv.live/resources/lib/rules.py index 77ff9848..f0404c64 100644 --- a/plugin.video.pseudotv.live/resources/lib/rules.py +++ b/plugin.video.pseudotv.live/resources/lib/rules.py @@ -548,7 +548,7 @@ def __init__(self): self.optionLabels = ['Page Limit','Method','Order','Ignore Folders'] self.optionValues = [int((REAL_SETTINGS.getSetting('Page_Limit') or "25")), 'random','ascending',False] self.actions = [RULES_ACTION_CHANNEL_START,RULES_ACTION_CHANNEL_STOP] - self.selectBoxOptions = [[n for n in range(25, 275, 25)], JSONRPC().getEnums("List.Sort",type="method"), JSONRPC().getEnums("List.Sort",type="order"), [True, False]] + # self.selectBoxOptions = [[n for n in range(25, 275, 25)], JSONRPC().getEnums("List.Sort",type="method"), JSONRPC().getEnums("List.Sort",type="order"), [True, False]] self.storedValues = [] @@ -620,7 +620,7 @@ def runAction(self, actionid, citem, parameter, builder): if self.optionValues[0]: self.log("%s: runAction, id: %s, provisional value = %s"%(self.__class__.__name__,citem['id'],self.optionValues[0])) - if builder.pDialog: builder.pDialog = DIALOG.progressBGDialog(builder.pCount, builder.pDialog, message='%s - Rule: %s'%(builder.pName,self.getTitle()),header='%s, %s'%(ADDON_NAME,builder.pMSG)) + if builder.pDialog: builder.pDialog = DIALOG.progressBGDialog(builder.pCount, builder.pDialog, message='%s - Applying Rule: %s'%(builder.pName,self.getTitle()),header='%s, %s'%(ADDON_NAME,builder.pMSG)) if self.optionValues[0] == "Seasonal": queries = list(Seasonal().buildSeasonal()) else: queries = PROVISIONAL_TYPES.get(citem['type'],[]) for provisional in queries: @@ -714,7 +714,7 @@ def _mergeShows(shows, movies): elif actionid == RULES_ACTION_CHANNEL_BUILD_FILELIST_POST: try: if parameter: - if builder.pDialog: builder.pDialog = DIALOG.progressBGDialog(builder.pCount, builder.pDialog, message='%s - Rule: %s'%(builder.pName,self.getTitle()),header='%s, %s'%(ADDON_NAME,builder.pMSG)) + if builder.pDialog: builder.pDialog = DIALOG.progressBGDialog(builder.pCount, builder.pDialog, message='%s - Applying Rule: %s'%(builder.pName,self.getTitle()),header='%s, %s'%(ADDON_NAME,builder.pMSG)) poolit(_sortShows)(list(sorted(parameter, key=lambda k: k.get('episode',0)))) self.storedValues[0] = dict(_chunkShows()) return _mergeShows(self.storedValues[0],self.storedValues[1]) diff --git a/plugin.video.pseudotv.live/resources/lib/seasonal.py b/plugin.video.pseudotv.live/resources/lib/seasonal.py index 22f3731b..9f920042 100644 --- a/plugin.video.pseudotv.live/resources/lib/seasonal.py +++ b/plugin.video.pseudotv.live/resources/lib/seasonal.py @@ -158,9 +158,7 @@ def buildSeasonal(self, nearest=SETTINGS.getSettingBool('NEAREST_SEASON')): holiday.pop("query") item["holiday"] = holiday item_sort = SORT.copy() - if param.get('sort'): - item_sort.update(param.pop("sort")) - item["sort"] = item_sort - if param.get('filter'): - item["filter"] = param.pop("filter") + if param.get("sort"): item_sort.update(param.get("sort")) + item["sort"] = item_sort + if param.get("filter"): item["filter"] = param.get("filter") yield item \ No newline at end of file diff --git a/plugin.video.pseudotv.live/resources/lib/server.py b/plugin.video.pseudotv.live/resources/lib/server.py index 73f1e1a8..03a3bf35 100644 --- a/plugin.video.pseudotv.live/resources/lib/server.py +++ b/plugin.video.pseudotv.live/resources/lib/server.py @@ -67,9 +67,9 @@ def log(self, msg, level=xbmc.LOGDEBUG): def getSettings(self): return {'Resource_Logos' :SETTINGS.getSetting('Resource_Logos'), - 'Resource_Ratings' :SETTINGS.getSetting('Resource_Ratings')} - # 'Resource_Commericals':SETTINGS.getSetting('Resource_Commericals'), - # 'Resource_Trailers' :SETTINGS.getSetting('Resource_Trailers')} + 'Resource_Ratings' :SETTINGS.getSetting('Resource_Ratings'), + 'Resource_Commericals':SETTINGS.getSetting('Resource_Commericals'), + 'Resource_Trailers' :SETTINGS.getSetting('Resource_Trailers')} def _start(self): diff --git a/plugin.video.pseudotv.live/resources/lib/service.py b/plugin.video.pseudotv.live/resources/lib/service.py index e9acce16..b25e5497 100644 --- a/plugin.video.pseudotv.live/resources/lib/service.py +++ b/plugin.video.pseudotv.live/resources/lib/service.py @@ -21,6 +21,7 @@ from overlay import Overlay, Background from rules import RulesList from tasks import Tasks +from jsonrpc import JSONRPC class Player(xbmc.Player): sysInfo = {} @@ -35,6 +36,8 @@ class Player(xbmc.Player): def __init__(self): self.log('__init__') xbmc.Player.__init__(self) + self.disableTrakt = SETTINGS.getSettingBool('Disable_Trakt') #todo adv. rule opt + self.rollbackPlaycount = SETTINGS.getSettingBool('Rollback_Watched')#todo adv. rule opt """ Player() Trigger Order @@ -150,7 +153,7 @@ def _onPlay(self): self.log('_onPlay') self.toggleBackground(False) BUILTIN.executebuiltin('ReplaceWindow(fullscreenvideo)') - + if self.disableTrakt: disableTrakt() self.sysInfo = self.getPlayerSysInfo() if self.sysInfo.get('citem',{}).get('id') != self.sysInfo.get('citem',{}).get('id',random.random()): #playing new channel self.sysInfo = self.runActions(RULES_ACTION_PLAYER_START, self.sysInfo.get('citem'), self.sysInfo, inherited=self) @@ -160,7 +163,9 @@ def _onPlay(self): def _onChange(self): self.log('_onChange') try: + clearTrakt() self.toggleBackground(True) + if self.rollbackPlaycount and self.sysInfo.get('fitem'): self.myService.jsonRPC.quePlaycount(self.sysInfo['fitem']) if self.sysInfo.get('isPlaylist',False) and self.sysInfo.get('pvritem'): broadcastnext = self.sysInfo['pvritem']['broadcastnext'] self.sysInfo['pvritem']['broadcastnow'] = broadcastnext.pop(0) @@ -176,6 +181,7 @@ def _onChange(self): def _onStop(self): self.log('_onStop') + clearTrakt() self.toggleBackground(False) if self.sysInfo.get('isPlaylist',False): xbmc.PlayList(xbmc.PLAYLIST_VIDEO).clear() self.sysInfo = self.runActions(RULES_ACTION_PLAYER_STOP, self.sysInfo.get('citem'), self.sysInfo, inherited=self) @@ -192,8 +198,7 @@ def toggleBackground(self, state=True): self.background = Background("%s.background.xml"%(ADDON_ID), ADDON_PATH, "default", player=self, runActions=self.runActions) self.background.show() elif not state and hasattr(self.background, 'close'): - self.background.close() - self.background = None + self.background = self.background.close() if self.isPlaying(): BUILTIN.executebuiltin('ActivateWindow(fullscreenvideo)') @@ -243,7 +248,7 @@ def toggleOverlay(self, state): if state and self.overlay is None: conditions = self.enableOverlay & self.myService.player.isPlaying() & self.myService.player.isPseudoTV if conditions: - self.overlay = Overlay(player=self.myService.player, runActions=self.myService.player.runActions) + self.overlay = Overlay(jsonRPC=self.myService.jsonRPC,player=self.myService.player, runActions=self.myService.player.runActions) self.overlay.open() elif not state and hasattr(self.overlay, 'close'): self.overlay.close() @@ -296,7 +301,7 @@ def onSettingsChanged(self): def onSettingsChangedTimer(self): self.log('onSettingsChangedTimer') - self.myService._que(self.myService.monitor._onSettingsChanged,1) + self.tasks._que(self.myService.monitor._onSettingsChanged,1) def _onSettingsChanged(self): @@ -310,10 +315,10 @@ class Service(): currentChannels = [] currentSettings = [] - queue = PriorityQueue() + jsonRPC = JSONRPC() player = Player() monitor = Monitor() - tasks = Tasks() + tasks = Tasks(jsonRPC) def __init__(self): self.log('__init__') @@ -346,39 +351,20 @@ def _playing(self) -> bool: def _run(self): - self.monitor.chkIdle() + with setRunning('_run'): + self.monitor.chkIdle() + - def _tasks(self): - self.tasks.chkQueTimer() - self._queue() + with setRunning('_tasks'): + self.tasks.chkQueTimer() + self.tasks._queue() - def _queue(self): - try: - priority, randomheap, package = self.queue.get(block=False) - try: - func, args, kwargs = package - self.log("_queue, priority = %s, func = %s"%(priority,func.__name__)) - func(*args,**kwargs) - except Exception as e: - self.log("_queue, func = %s failed! %s"%(func.__name__,e), xbmc.LOGERROR) - except Empty: self.log("_queue, empty!") - - - def _que(self, func, priority=3, *args, **kwargs): - try: # priority 1 Highest, 5 Lowest - self.queue.put((self.queue.qsize()+priority, random.random(), (func, args, kwargs)), block=False) - self.log('_que, func = %s, args = %s, kwargs = %s' % (func.__name__, args, kwargs)) - except TypeError: pass - except Exception as e: - self.log("_que, failed! %s" % (e), xbmc.LOGERROR) - - def _initialize(self): self.log('_initialize') if self.player.isPlaying(): self.player.onAVStarted() #if playback already in-progress run onAVStarted tasks. - self.currentSettings = dict(SETTINGS.getCurrentSettings()) #startup settings + self.currentSettings = dict(SETTINGS.getCurrentSettings()) #startup settings self.player.myService = self self.monitor.myService = self self.tasks.myService = self @@ -389,10 +375,11 @@ def start(self): self.log('start') self._initialize() while not self.monitor.abortRequested(): - self._run() - if self._interrupt(wait=1): break + if self._interrupt(wait=2): break elif self._suspend(): continue - else: self._tasks() + else: + if not isRunning('_run'): self._run() + if not isRunning('_tasks'): self._tasks() self.stop() diff --git a/plugin.video.pseudotv.live/resources/lib/tasks.py b/plugin.video.pseudotv.live/resources/lib/tasks.py index 1abebdc4..8be522f8 100644 --- a/plugin.video.pseudotv.live/resources/lib/tasks.py +++ b/plugin.video.pseudotv.live/resources/lib/tasks.py @@ -20,7 +20,6 @@ from globals import * from library import Library -from jsonrpc import JSONRPC from autotune import Autotune from builder import Builder from backup import Backup @@ -29,12 +28,11 @@ from server import HTTP class Tasks(): - queueRunning = False - backgroundRunning = False + queue = PriorityQueue() - - def __init__(self): + def __init__(self, jsonRPC=None): self.log('__init__') + self.jsonRPC = jsonRPC def log(self, msg, level=xbmc.LOGDEBUG): @@ -55,6 +53,28 @@ def _startProcess(self): self.httpServer = HTTP(self.myService.monitor) + def _queue(self): + try: + priority, randomheap, package = self.queue.get(block=False) + try: + func, args, kwargs = package + self.log("_queue, priority = %s, func = %s"%(priority,func.__name__)) + func(*args,**kwargs) + except Exception as e: + self.log("_queue, func = %s failed! %s"%(func.__name__,e), xbmc.LOGERROR) + except Empty: self.log("_queue, empty!") + + + def _que(self, func, priority=-1, *args, **kwargs): + try: # priority -1 autostack, 1 Highest, 5 Lowest + if priority == -1: priority = self.queue.qsize() + 1 + self.queue.put((priority, random.random(), (func, args, kwargs)), block=False) + self.log('_que, func = %s, args = %s, kwargs = %s' % (func.__name__, args, kwargs)) + except TypeError: pass + except Exception as e: + self.log("_que, failed! %s" % (e), xbmc.LOGERROR) + + def chkBackup(self): self.log('chkBackup') Backup().hasBackup() @@ -71,25 +91,25 @@ def chkHTTP(self): def chkQueTimer(self): if self.chkUpdateTime('chkQueTimer',runEvery=30): - self.myService._que(self._chkQueTimer,1) + self._que(self._chkQueTimer) def _chkQueTimer(self): self.log('chkQueTimer') if self.chkUpdateTime('chkHTTP',runEvery=900): - self.myService._que(self.chkHTTP,1) + self._que(self.chkHTTP) if self.chkUpdateTime('chkFiles',runEvery=600): - self.myService._que(self.chkFiles,1) + self._que(self.chkFiles) if self.chkUpdateTime('chkPVRSettings',runEvery=(MAX_GUIDEDAYS*3600)): - self.myService._que(self.chkPVRSettings,1) + self._que(self.chkPVRSettings) if self.chkUpdateTime('chkRecommended',runEvery=900): - self.myService._que(self.chkRecommended,2) + self._que(self.chkRecommended) if self.chkUpdateTime('chkLibrary',runEvery=(MAX_GUIDEDAYS*3600)): - self.myService._que(self.chkLibrary,2) + self._que(self.chkLibrary) if self.chkUpdateTime('chkChannels',runEvery=(MAX_GUIDEDAYS*3600)): - self.myService._que(self.chkChannels,3) + self._que(self.chkChannels) if self.chkUpdateTime('chkJSONQUE',runEvery=600): - self.myService._que(self.chkJSONQUE,4) + self._que(self.chkJSONQUE) def chkWelcome(self): @@ -108,7 +128,7 @@ def chkVersion(self): def chkDebugging(self): self.log('chkDebugging') if SETTINGS.getSettingBool('Enable_Debugging'): - if DIALOG.yesnoDialog(LANGUAGE(32142),autoclose=PROMPT_DELAY): + if DIALOG.yesnoDialog(LANGUAGE(32142),autoclose=4): self.log('_chkDebugging, disabling debugging.') SETTINGS.setSettingBool('Enable_Debugging',False) DIALOG.notificationDialog(LANGUAGE(321423)) @@ -139,15 +159,13 @@ def chkPVRSettings(self): try: if isClient(): return with sudo_dialog(msg='%s %s'%(LANGUAGE(32028),LANGUAGE(30069))): - jsonRPC = JSONRPC() - if (jsonRPC.getSettingValue('epg.pastdaystodisplay') or 1) != MIN_GUIDEDAYS: + if (self.jsonRPC.getSettingValue('epg.pastdaystodisplay') or 1) != MIN_GUIDEDAYS: SETTINGS.setSettingInt('Min_Days',min) - if (jsonRPC.getSettingValue('epg.futuredaystodisplay') or 3) != MAX_GUIDEDAYS: + if (self.jsonRPC.getSettingValue('epg.futuredaystodisplay') or 3) != MAX_GUIDEDAYS: SETTINGS.setSettingInt('Max_Days',max) - PROPERTIES.setPropertyBool('hasPVRSource',jsonRPC.hasPVRSource()) - del jsonRPC + PROPERTIES.setPropertyBool('hasPVRSource',self.jsonRPC.hasPVRSource()) except Exception as e: self.log('chkPVRSettings failed! %s'%(e), xbmc.LOGERROR) @@ -157,7 +175,7 @@ def chkLibrary(self): library.importPrompt() complete = library.updateLibrary() del library - if not complete: self.myService._que(self.chkLibrary,2) + if not complete: self._que(self.chkLibrary,2) elif not hasAutotuned(): self.runAutoTune() #run autotune for the first time this Kodi/PTVL instance. except Exception as e: self.log('chkLibrary failed! %s'%(e), xbmc.LOGERROR) @@ -182,7 +200,7 @@ def chkChannels(self): if complete: setFirstrun(state=True) #set init. boot status to true. self.myService.currentChannels = list(channels) - else: self.myService._que(self.chkChannels,3) + else: self._que(self.chkChannels,3) except Exception as e: self.log('chkChannels failed! %s'%(e), xbmc.LOGERROR) @@ -190,35 +208,33 @@ def chkFiles(self): self.log('_chkFiles') # check for missing files and run appropriate action to rebuild them only after init. startup. if hasFirstrun() and not isClient(): - if not (FileAccess.exists(LIBRARYFLEPATH)): self.myService._que(self.chkLibrary,2) + if not (FileAccess.exists(LIBRARYFLEPATH)): self._que(self.chkLibrary,2) if not (FileAccess.exists(CHANNELFLEPATH) & FileAccess.exists(M3UFLEPATH) & FileAccess.exists(XMLTVFLEPATH) & FileAccess.exists(GENREFLEPATH)): - self.myService._que(self.chkChannels,3) + self._que(self.chkChannels,1) def chkJSONQUE(self): - if not self.queueRunning: - threadit(self.runJSON) + if not isRunning('runJSONQUE'): + timerit(self.runJSONQUE)(0.5) - def runJSON(self): - #Only run after idle for 2mins to reduce system impact. Check interval every 15mins, run in chunks set by PAGE_LIMIT. - self.queueRunning = True - queuePool = SETTINGS.getCacheSetting('queuePool', json_data=True, default={}) - params = queuePool.get('params',[]) - for param in (list(chunkLst(params,int((REAL_SETTINGS.getSetting('Page_Limit') or "25")))) or [[]])[0]: - if self.myService._interrupt(): - self.log('runJSON, _interrupt') - break - elif not self.myService.isIdle or self.myService.player.isPlaying(): - self.log('runJSON, waiting for idle...') - break - elif len(params) > 0: - self.myService._que(JSONRPC().sendJSON,5,params.pop(0)) - queuePool['params'] = setDictLST(params) - self.log('runJSON, remaining = %s'%(len(queuePool['params']))) - SETTINGS.setCacheSetting('queuePool', queuePool, json_data=True) - self.queueRunning = False - + def runJSONQUE(self): + with setRunning('runJSONQUE'): + queuePool = SETTINGS.getCacheSetting('queuePool', json_data=True, default={}) + params = queuePool.get('params',[]) + for param in (list(chunkLst(params,int((REAL_SETTINGS.getSetting('Page_Limit') or "25")))) or [[]])[0]: + if self.myService._interrupt() or self.myService._suspend(): + self.log('runJSONQUE, _interrupt or _suspend, cancelling.') + break + elif self.myService._playing(): + self.log('runJSONQUE, playback detected, cancelling.') + break + elif len(params) > 0: + self._que(self.jsonRPC.sendJSON,-1,params.pop(0)) + queuePool['params'] = setDictLST(params) + self.log('runJSONQUE, remaining = %s'%(len(queuePool['params']))) + SETTINGS.setCacheSetting('queuePool', queuePool, json_data=True) + def runAutoTune(self): try: @@ -247,7 +263,7 @@ def chkChannelChange(self, channels=[]): nChannels = self.getChannels() if channels != nChannels: self.log('chkChannelChange, resetting chkChannels') - self.myService._que(self.chkChannels,3) + self._que(self.chkChannels,2) return nChannels return channels @@ -268,7 +284,7 @@ def chkSettingsChange(self, settings=[]): if nSettings.get(setting) != value and actions.get(setting): with sudo_dialog(LANGUAGE(32157)): self.log('chkSettingsChange, detected change in %s - from: %s to: %s'%(setting,value,nSettings.get(setting))) - self.myService._que(actions[setting].get('func'),1,*actions[setting].get('args',()),**actions[setting].get('kwargs',{})) + self._que(actions[setting].get('func'),-1,*actions[setting].get('args',()),**actions[setting].get('kwargs',{})) return nSettings diff --git a/plugin.video.pseudotv.live/resources/settings.xml b/plugin.video.pseudotv.live/resources/settings.xml index af0e43bc..53664e1d 100644 --- a/plugin.video.pseudotv.live/resources/settings.xml +++ b/plugin.video.pseudotv.live/resources/settings.xml @@ -309,6 +309,20 @@ + + 2 + resource.images.pseudotv.logos + + kodi.resource.images + + + + + 30067 + installed + true + + 0 0 @@ -420,6 +434,11 @@ true + + + String.Contains(Window(10000).Property(plugin.video.pseudotv.live.isClient),false) + + 30012 @@ -496,7 +515,27 @@ - + + + 1 + false + + + + + System.HasAddon(script.trakt) + System.AddonIsEnabled(script.trakt) + + + + + + 1 + false + + + + 1 true @@ -627,7 +666,7 @@ - + 3 25 @@ -762,6 +801,7 @@ + true String.Contains(Window(10000).Property(plugin.video.pseudotv.live.isClient),false) @@ -779,6 +819,7 @@ + true String.Contains(Window(10000).Property(plugin.video.pseudotv.live.isClient),false) @@ -801,6 +842,7 @@ + true String.Contains(Window(10000).Property(plugin.video.pseudotv.live.isClient),false) @@ -823,6 +865,7 @@ + true String.Contains(Window(10000).Property(plugin.video.pseudotv.live.isClient),false) @@ -832,41 +875,76 @@ - + 2 - resource.images.pseudotv.logos + resource.videos.ratings.mpaa.classic kodi.resource.images + + + 1 + String.Contains(Window(10000).Property(plugin.video.pseudotv.live.isClient),false) + + - 30067 + 30069 installed true - + 2 - resource.videos.ratings.mpaa.classic + resource.videos.bumpers.pseudotv kodi.resource.images + + + + 1 + String.Contains(Window(10000).Property(plugin.video.pseudotv.live.isClient),false) + + + - 30069 + 30068 installed true - + 2 - + 25 + + 0 + 1 + 100 + + + + + 1 + String.Contains(Window(10000).Property(plugin.video.pseudotv.live.isClient),false) + + + + + false + + + + 2 + resource.videos.adverts.pseudotv kodi.resource.images + 0 String.Contains(Window(10000).Property(plugin.video.pseudotv.live.isClient),false) @@ -877,25 +955,67 @@ true - + 2 - 0 + 25 - - - - + 0 + 1 + 100 + 0 String.Contains(Window(10000).Property(plugin.video.pseudotv.live.isClient),false) - + + false + + + + 2 + resource.videos.trailers.pseudotv + + kodi.resource.images + + + + + 0 + String.Contains(Window(10000).Property(plugin.video.pseudotv.live.isClient),false) + + + + + 30068 + installed + true + + + + 2 + 25 + + 0 + 1 + 100 + + + + + 0 + String.Contains(Window(10000).Property(plugin.video.pseudotv.live.isClient),false) + + + + + false + - + 2 0 @@ -908,6 +1028,7 @@ + 0 String.Contains(Window(10000).Property(plugin.video.pseudotv.live.isClient),false) diff --git a/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.6.zip b/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.6.zip deleted file mode 100644 index e7aebf62..00000000 Binary files a/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.6.zip and /dev/null differ diff --git a/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.6a.zip b/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.6a.zip deleted file mode 100644 index 74dcea1c..00000000 Binary files a/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.6a.zip and /dev/null differ diff --git a/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.6b.zip b/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.6b.zip deleted file mode 100644 index 349f5b6f..00000000 Binary files a/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.6b.zip and /dev/null differ diff --git a/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.6c.zip b/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.6c.zip deleted file mode 100644 index b1a80595..00000000 Binary files a/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.6c.zip and /dev/null differ diff --git a/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.6d.zip b/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.6d.zip deleted file mode 100644 index 6b9ad214..00000000 Binary files a/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.6d.zip and /dev/null differ diff --git a/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.7.zip b/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.7.zip deleted file mode 100644 index f3f9b51d..00000000 Binary files a/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.7.zip and /dev/null differ diff --git a/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.7a.zip b/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.7a.zip deleted file mode 100644 index 84c29dac..00000000 Binary files a/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.7a.zip and /dev/null differ diff --git a/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.7b.zip b/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.7b.zip deleted file mode 100644 index b8954b54..00000000 Binary files a/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.7b.zip and /dev/null differ diff --git a/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.7d.zip b/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.7d.zip index 85ea992f..32007327 100644 Binary files a/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.7d.zip and b/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.7d.zip differ diff --git a/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.7c.zip b/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.8.zip similarity index 99% rename from zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.7c.zip rename to zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.8.zip index fe119ebd..2cde02fb 100644 Binary files a/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.7c.zip and b/zips/plugin.video.pseudotv.live/plugin.video.pseudotv.live-0.4.8.zip differ