# -*- coding: utf-8 -*- """Keeps an IPTV Simple instance that plays this add-on's radio.m3u, and its programme guide up to date. Runs when Kodi starts and again after every add-on update. Writes the instance settings, then restarts IPTV Simple (once nothing is playing) if the settings or the station list changed since the last run. Rebuilds epg.xml every 6 hours (30 minutes after a failure); IPTV Simple reloads the station list and the guide every hour, which is when Kodi takes the new guide. """ import glob import hashlib import io import json import os import threading import time import xbmc import xbmcaddon import xbmcvfs from resources.lib import epg ADDON = xbmcaddon.Addon() ADDON_ID = ADDON.getAddonInfo('id') HOME = 'special://home/addons/{}/resources/'.format(ADDON_ID) PROFILE = 'special://profile/addon_data/{}/'.format(ADDON_ID) FIRST_INSTANCE = 95 # KodiMov+ uses 91 (VAIO) and 93 (.49) GUIDE_EVERY = 6 * 3600 GUIDE_RETRY = 30 * 60 SETTINGS = ''' Radios de Vigo true 0 {home}radio.m3u true 1 false 1 60 0 {profile}epg.xml true 0 {home}logos/ false 0 true false '''.format(home=HOME, profile=PROFILE) def log(msg): xbmc.log('[{}] {}'.format(ADDON_ID, msg), xbmc.LOGINFO) def rpc(method, **params): return json.loads(xbmc.executeJSONRPC(json.dumps( {'jsonrpc': '2.0', 'id': 1, 'method': method, 'params': params}))) def read(path): try: with io.open(path, 'r', encoding='utf-8') as f: return f.read() except (IOError, OSError): return None def write(path, content): tmp = path + '.tmp' with io.open(tmp, 'w', encoding='utf-8', newline='') as f: f.write(content) os.replace(tmp, path) def instance_file(pvr_dir): """Our instance file: the one that already reads radio.m3u, else the first free number from FIRST_INSTANCE on.""" for path in glob.glob(os.path.join(pvr_dir, 'instance-settings-*.xml')): if HOME + 'radio.m3u' in (read(path) or ''): return path n = FIRST_INSTANCE while os.path.exists(os.path.join(pvr_dir, 'instance-settings-{}.xml'.format(n))): n += 1 return os.path.join(pvr_dir, 'instance-settings-{}.xml'.format(n)) def restart_iptv_simple(): for enabled in (False, True): rpc('Addons.SetAddonEnabled', addonid='pvr.iptvsimple', enabled=enabled) xbmc.sleep(3000) def update_guide(profile, m3u): """Writes epg.xml from every source that answers; channels whose source failed keep their previous programmes. Returns True if no source failed.""" failures = [] now = time.time() new = epg.fetch_all(log=lambda msg: (failures.append(msg), log(msg)), now=now) cache_path = os.path.join(profile, 'guide.json') try: old = json.loads(read(cache_path) or '{}') except ValueError: old = {} guide = epg.merge(old, new, now) write(cache_path, json.dumps(guide)) content = epg.xmltv(guide, epg.channel_names(m3u)) epg_path = os.path.join(profile, 'epg.xml') if read(epg_path) != content: write(epg_path, content) log('guide: {} stations, {} programmes'.format(len(guide), sum(len(p) for p in guide.values()))) return not failures def main(): monitor, player = xbmc.Monitor(), xbmc.Player() profile = xbmcvfs.translatePath(PROFILE) pvr_dir = xbmcvfs.translatePath('special://profile/addon_data/pvr.iptvsimple/') for d in (profile, pvr_dir): if not os.path.isdir(d): os.makedirs(d) m3u = read(xbmcvfs.translatePath(HOME + 'radio.m3u')) or '' path = instance_file(pvr_dir) if read(path) != SETTINGS: write(path, SETTINGS) log('wrote ' + path) numbers_set = os.path.join(profile, 'numbers-set') if not os.path.exists(numbers_set): # Show the stations' own numbers (Vigo ones are their FM frequency). # With more than one IPTV Simple instance Kodi ignores them unless # "always" is on too; that also keeps KodiMov+'s TV numbers. for setting in ('pvrmanager.usebackendchannelnumbers', 'pvrmanager.usebackendchannelnumbersalways'): rpc('Settings.SetSettingValue', setting=setting, value=True) write(numbers_set, '') log('turned on backend channel numbers') # The guide is fetched in the background so Kodi can always stop this service quickly guide_ok = [True] def guide_job(): try: guide_ok[0] = update_guide(profile, m3u) except Exception as e: guide_ok[0] = False log('guide update failed: {}'.format(e)) epg_path = os.path.join(profile, 'epg.xml') job = threading.Thread(target=guide_job) job.daemon = True job.start() state = hashlib.sha1((SETTINGS + m3u).encode('utf-8')).hexdigest() restart_pending = read(os.path.join(profile, 'state')) != state if restart_pending and not os.path.exists(epg_path): job.join(180) # first install: have a guide before IPTV Simple starts reading it next_guide = time.time() + GUIDE_EVERY while not monitor.abortRequested(): if restart_pending and not player.isPlaying(): # restarting IPTV Simple stops what's playing if monitor.waitForAbort(5): break log('station list or settings changed, restarting IPTV Simple') restart_iptv_simple() write(os.path.join(profile, 'state'), state) restart_pending = False if not job.is_alive(): if not guide_ok[0]: next_guide = min(next_guide, time.time() + GUIDE_RETRY) guide_ok[0] = True if time.time() >= next_guide: next_guide = time.time() + GUIDE_EVERY job = threading.Thread(target=guide_job) job.daemon = True job.start() if monitor.waitForAbort(10): break if __name__ == '__main__': main()