Programme guide for 12 stations, rebuilt every 6 hours
BBC (RMS API), LBC News (epgshare01), Antena 1-3 (RTP JSON), RNE, Radio 3, Radio 5, Radio Clásica (RTVE guide pages) and WBUR (weekly schedule). IPTV Simple now reloads the list and guide hourly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
927195e28d
commit
7ed2e2fb06
6 files changed
+394
-18
No files matched your search
+78
-17
@@ -1,24 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Keeps an IPTV Simple instance that plays this add-on's radio.m3u.
|
||||
"""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.
|
||||
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 = '''<settings version="2">
|
||||
<setting id="kodi_addon_instance_name">Radios de Vigo</setting>
|
||||
@@ -29,10 +39,12 @@ SETTINGS = '''<settings version="2">
|
||||
<setting id="m3uCache" default="true">true</setting>
|
||||
<setting id="startNum" default="true">1</setting>
|
||||
<setting id="numberByOrder" default="true">false</setting>
|
||||
<setting id="m3uRefreshMode" default="true">0</setting>
|
||||
<setting id="m3uRefreshMode">1</setting>
|
||||
<setting id="m3uRefreshIntervalMins">60</setting>
|
||||
<setting id="epgPathType">0</setting>
|
||||
<setting id="epgPath" default="true" />
|
||||
<setting id="epgPath">{profile}epg.xml</setting>
|
||||
<setting id="epgUrl" default="true" />
|
||||
<setting id="epgCache" default="true">true</setting>
|
||||
<setting id="logoPathType">0</setting>
|
||||
<setting id="logoPath">{home}logos/</setting>
|
||||
<setting id="useLogosLocalPathOnly" default="true">false</setting>
|
||||
@@ -40,7 +52,7 @@ SETTINGS = '''<settings version="2">
|
||||
<setting id="useFFmpegReconnect" default="true">true</setting>
|
||||
<setting id="useInputstreamAdaptiveforHls" default="true">false</setting>
|
||||
</settings>
|
||||
'''.format(home=HOME)
|
||||
'''.format(home=HOME, profile=PROFILE)
|
||||
|
||||
|
||||
def log(msg):
|
||||
@@ -85,13 +97,35 @@ def restart_iptv_simple():
|
||||
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(ADDON.getAddonInfo('profile'))
|
||||
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:
|
||||
@@ -108,19 +142,46 @@ def main():
|
||||
write(numbers_set, '')
|
||||
log('turned on backend channel numbers')
|
||||
|
||||
m3u = read(xbmcvfs.translatePath(HOME + 'radio.m3u')) or ''
|
||||
state = hashlib.sha1((SETTINGS + m3u).encode('utf-8')).hexdigest()
|
||||
if read(os.path.join(profile, 'state')) == state:
|
||||
return
|
||||
# The guide is fetched in the background so Kodi can always stop this service quickly
|
||||
guide_ok = [True]
|
||||
|
||||
while player.isPlaying(): # restarting IPTV Simple stops the radio or TV playing
|
||||
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):
|
||||
return
|
||||
if monitor.waitForAbort(5):
|
||||
return
|
||||
log('station list or settings changed, restarting IPTV Simple')
|
||||
restart_iptv_simple()
|
||||
write(os.path.join(profile, 'state'), state)
|
||||
break
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Reference in new issue
Block a user