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:
XoséandClaude Opus 5.5 committed 2026-11-12 22:28:54 +01:00
1 parent 927195e28d
commit 7ed2e2fb06
6 files changed
+391 -15

No files matched your search

+21
View File
@@ -23,6 +23,27 @@ station list changed. On the first run it turns on *Use channel numbers from
backend* + *always*, so the frequencies show as channel numbers and KodiMov+ backend* + *always*, so the frequencies show as channel numbers and KodiMov+
keeps Movistar's TV numbers. keeps Movistar's TV numbers.
## Programme guide
`resources/lib/epg.py` builds `epg.xml` in the add-on's profile folder when
Kodi starts and then every 6 hours (30 minutes after a failed source). A
source that fails keeps its previous programmes (`guide.json`). IPTV Simple
reloads the list and the guide every hour, which is when Kodi takes it.
| Stations | Source |
|---|---|
| BBC Radio 4, World Service, 5 Live | BBC RMS schedule API (`rms.api.bbc.co.uk`) |
| LBC News | epgshare01 UK guide (`epg_ripper_UK1.xml.gz`) |
| Antena 1, 2, 3 | RTP EPG JSON (`rtp.pt/EPG/json/rtp-channels-page/list-grid/radio/<1-3>/...`) |
| RNE, Radio 3, Radio 5, Radio Clásica | RTVE guide pages (`rtve.es/play/guia-rne/<slug>/`) |
| WBUR | WBUR weekly schedule (`api.wbur.org/schedule`) |
No usable source yet: CRTVG (built in JavaScript), SER (blocks scripts),
Onda Cero, esRadio, Radio Marca, WNYC, KQED, NPR, Times Radio, TSF, Observador
and the music stations. To add one, write a function in `epg.py` and map the
station's tvg-id in `CHANNELS`. Test with
`python3 -I resources/lib/epg.py /tmp/epg.xml`.
Uninstalling the add-on leaves that instance behind: remove it in Uninstalling the add-on leaves that instance behind: remove it in
IPTV Simple's settings (or delete the instance file). IPTV Simple's settings (or delete the instance file).
+1 -1
View File
@@ -15,7 +15,7 @@
<description lang="gl_ES">Engade á sección Radio de Kodi as emisoras do dial FM de Vigo (co número de canal na súa frecuencia, incluídas as portuguesas de Valença) e emisoras de noticias de Reino Unido, Estados Unidos e Portugal. Crea a súa propia instancia de IPTV Simple e mantena ao día cando se actualiza o complemento.</description> <description lang="gl_ES">Engade á sección Radio de Kodi as emisoras do dial FM de Vigo (co número de canal na súa frecuencia, incluídas as portuguesas de Valença) e emisoras de noticias de Reino Unido, Estados Unidos e Portugal. Crea a súa propia instancia de IPTV Simple e mantena ao día cando se actualiza o complemento.</description>
<description lang="es_ES">Añade a la sección Radio de Kodi las emisoras del dial FM de Vigo (con el número de canal en su frecuencia, incluidas las portuguesas de Valença) y emisoras de noticias de Reino Unido, Estados Unidos y Portugal. Crea su propia instancia de IPTV Simple y la mantiene al día cuando se actualiza el complemento.</description> <description lang="es_ES">Añade a la sección Radio de Kodi las emisoras del dial FM de Vigo (con el número de canal en su frecuencia, incluidas las portuguesas de Valença) y emisoras de noticias de Reino Unido, Estados Unidos y Portugal. Crea su propia instancia de IPTV Simple y la mantiene al día cuando se actualiza el complemento.</description>
<description lang="en_GB">Adds the stations on Vigo's FM dial (numbered by frequency, including the Portuguese ones from Valença) and news stations from the UK, US and Portugal to Kodi's Radio section. It creates its own IPTV Simple instance and keeps it up to date when the add-on updates.</description> <description lang="en_GB">Adds the stations on Vigo's FM dial (numbered by frequency, including the Portuguese ones from Valença) and news stations from the UK, US and Portugal to Kodi's Radio section. It creates its own IPTV Simple instance and keeps it up to date when the add-on updates.</description>
<news>1.0.0: first version, 38 Vigo stations and 12 news stations.</news> <news>1.0.0: first version, 38 Vigo stations and 12 news stations, with a programme guide (updated every 6 hours) for BBC Radio 4, World Service, 5 Live, LBC News, Antena 1-3, RNE, Radio 3, Radio 5, Radio Clásica and WBUR.</news>
<platform>all</platform> <platform>all</platform>
<license>MIT</license> <license>MIT</license>
<source>https://git.anosanube.ovh/xocko/radios-vigo</source> <source>https://git.anosanube.ovh/xocko/radios-vigo</source>
View File
Whitespace-only changes.
View File
Whitespace-only changes.
+294
View File
@@ -0,0 +1,294 @@
# -*- coding: utf-8 -*-
"""Builds an XMLTV programme guide for the stations that publish a schedule.
Plain Python (no Kodi modules), so it can be tested outside Kodi:
python3 -I resources/lib/epg.py /tmp/epg.xml
Each source returns programmes as dicts with UTC epoch 'start'/'stop',
'title' and optional 'subtitle', 'desc', 'category' and 'icon'. Times are
parsed by hand: datetime.strptime fails at random inside Kodi's Python.
"""
import calendar
import gzip
import html
import io
import json
import re
import time
import urllib.request
import xml.etree.ElementTree as ET
UA = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36'}
DAY = 86400
# tvg-id in radio.m3u -> (source, source's own id)
CHANNELS = {
'bbc-radio-4': ('bbc', 'bbc_radio_fourfm'),
'bbc-world-service': ('bbc', 'bbc_world_service'),
'bbc-radio-5-live': ('bbc', 'bbc_radio_five_live'),
'lbc-news': ('epgshare_uk', 'LBC.News.uk'),
'antena-1': ('rtp', '1'),
'antena-2': ('rtp', '2'),
'antena-3': ('rtp', '3'),
'rne-radio-nacional-galicia': ('rtve', 'rne'),
'rne-radio-3': ('rtve', 'radio3'),
'rne-radio-5-galicia': ('rtve', 'radio5'),
'rne-radio-clasica': ('rtve', 'radioclasica'),
'wbur-boston': ('wbur', None),
}
def get(url, timeout=30):
data = urllib.request.urlopen(urllib.request.Request(url, headers=UA), timeout=timeout).read()
return data
# --- time zones -------------------------------------------------------------
def _last_sunday(year, month):
last = calendar.monthrange(year, month)[1]
return last - (calendar.weekday(year, month, last) + 1) % 7
def _nth_sunday(year, month, n):
first = (6 - calendar.weekday(year, month, 1)) % 7 + 1
return first + 7 * (n - 1)
def utc_offset(zone, ts):
"""Hours east of UTC at UTC epoch ts for 'madrid', 'lisbon' or 'boston'."""
year = time.gmtime(ts).tm_year
if zone in ('madrid', 'lisbon'): # EU summer time: last Sunday of March to last Sunday of October, 01:00 UTC
start = calendar.timegm((year, 3, _last_sunday(year, 3), 1, 0, 0))
end = calendar.timegm((year, 10, _last_sunday(year, 10), 1, 0, 0))
base = 1 if zone == 'madrid' else 0
return base + (1 if start <= ts < end else 0)
# US Eastern: second Sunday of March 02:00 EST to first Sunday of November 02:00 EDT
start = calendar.timegm((year, 3, _nth_sunday(year, 3, 2), 7, 0, 0))
end = calendar.timegm((year, 11, _nth_sunday(year, 11, 1), 6, 0, 0))
return -4 if start <= ts < end else -5
def local_to_utc(zone, y, mo, d, h, mi, s=0):
naive = calendar.timegm((y, mo, d, h, mi, s))
guess = naive - utc_offset(zone, naive) * 3600
return naive - utc_offset(zone, guess) * 3600
def local_date(zone, ts):
"""(year, month, day, weekday) of UTC epoch ts in the zone."""
t = time.gmtime(ts + utc_offset(zone, ts) * 3600)
return t.tm_year, t.tm_mon, t.tm_mday, t.tm_wday
def iso_utc(text):
"""'2026-11-11T23:32:30Z' -> epoch."""
n = [int(x) for x in re.findall(r'\d+', text)[:6]]
return calendar.timegm(tuple(n))
def chain_stops(programmes, last_length=3600):
"""Sources that give only start times: each programme ends when the next starts."""
programmes.sort(key=lambda p: p['start'])
for p, nxt in zip(programmes, programmes[1:] + [None]):
p.setdefault('stop', nxt['start'] if nxt else p['start'] + last_length)
return [p for p in programmes if p['stop'] > p['start']]
def text(fragment):
return html.unescape(re.sub(r'<[^>]+>', '', fragment or '')).strip()
# --- sources ----------------------------------------------------------------
def bbc(service, now):
out = []
for day in (now, now + DAY):
url = 'https://rms.api.bbc.co.uk/v2/experience/inline/schedules/{}/{}'.format(
service, time.strftime('%Y-%m-%d', time.gmtime(day)))
for module in json.loads(get(url))['data']:
for b in module.get('data') or []:
if b.get('type') != 'broadcast_summary':
continue
titles, synopses = b.get('titles') or {}, b.get('synopses') or {}
out.append({'start': iso_utc(b['start']), 'stop': iso_utc(b['end']),
'title': titles.get('primary') or '',
'subtitle': titles.get('secondary') or '',
'desc': synopses.get('medium') or synopses.get('short') or '',
'icon': (b.get('image_url') or '').replace('{recipe}', '480x270') or None})
return out
def rtp(channel, now):
out = []
for day in (now, now + DAY):
y, m, d, _ = local_date('lisbon', day)
url = 'https://www.rtp.pt/EPG/json/rtp-channels-page/list-grid/radio/{}/{}-{}-{}/lis'.format(channel, d, m, y)
result = json.loads(get(url))['result']
for part in ('morning', 'afternoon', 'evening'):
for item in result.get(part) or []:
n = [int(x) for x in re.findall(r'\d+', item['date'])[:6]]
images = item.get('image') or []
episode = item.get('episode') or {}
out.append({'start': local_to_utc('lisbon', *n),
'title': text(item.get('name')),
'subtitle': text(episode.get('title')),
'desc': text(episode.get('sinopse')) or text(item.get('description')),
'icon': images[-1]['src'] if images else None})
return chain_stops(out)
RTVE_ITEM = re.compile(
r'data-inicio="(\d\d):(\d\d)".*?<span class="datfin">\s*(?:<!-- -->)?(\d\d):(\d\d)</span>'
r'.*?<span class="pretitle">([^<]*)</span>\s*<h2 class="maintitle">([^<]*)</h2>(.*?)</div>'
r'.*?<img [^>]*src="([^"]+)"', re.S)
def rtve(slug, now):
out = []
for day, suffix in ((now, ''), (now + DAY, 'manana/')):
y, m, d, _ = local_date('madrid', day)
page = get('https://www.rtve.es/play/guia-rne/{}/{}'.format(slug, suffix)).decode('utf-8', 'replace')
midnight = local_to_utc('madrid', y, m, d, 0, 0)
previous = -1
for h1, m1, h2, m2, category, title, rest, image in RTVE_ITEM.findall(page):
start = local_to_utc('madrid', y, m, d, int(h1), int(m1))
if start < previous: # the page went past midnight
start += DAY
stop = local_to_utc('madrid', y, m, d, int(h2), int(m2))
while stop <= start:
stop += DAY
previous = start
desc = re.search(r'<p>(.*?)</p>', rest, re.S)
out.append({'start': start, 'stop': stop, 'title': text(title), 'category': text(category),
'desc': text(desc.group(1)) if desc else '', 'icon': html.unescape(image)})
if not out or out[-1]['stop'] < midnight:
raise ValueError('no programmes on ' + slug)
return out
def wbur(_, now):
body = json.loads(get('https://api.wbur.org/schedule'))['body']
out = []
for day in (now - DAY, now, now + DAY, now + 2 * DAY):
y, m, d, wday = local_date('boston', day)
rows = body['saturday' if wday == 5 else 'sunday' if wday == 6 else 'weekday']
for row in rows:
hh, mm, ampm = re.match(r'(\d+):(\d+)\s*([AP]M)', row['time']).groups()
hour = int(hh) % 12 + (12 if ampm == 'PM' else 0)
labels = [text(i.get('label')) for i in row.get('items') or [] if i.get('label')]
if labels:
out.append({'start': local_to_utc('boston', y, m, d, hour, int(mm)), 'title': ' / '.join(labels)})
return chain_stops(out)
def epgshare_uk(channel_ids, now):
"""All wanted channels from one download of epgshare01's UK guide."""
wanted = set(channel_ids)
data = gzip.GzipFile(fileobj=io.BytesIO(get('https://epgshare01.online/epgshare01/epg_ripper_UK1.xml.gz', 120)))
out = dict((c, []) for c in wanted)
for _, el in ET.iterparse(data):
if el.tag == 'programme':
channel = el.get('channel')
if channel in wanted:
out[channel].append({'start': xmltv_time(el.get('start')), 'stop': xmltv_time(el.get('stop')),
'title': el.findtext('title') or '', 'subtitle': el.findtext('sub-title') or '',
'desc': el.findtext('desc') or ''})
el.clear()
elif el.tag == 'channel':
el.clear()
return out
def xmltv_time(value):
n = re.match(r'(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)\s*([+-])(\d\d)(\d\d)', value).groups()
ts = calendar.timegm(tuple(int(x) for x in n[:6]))
offset = (int(n[7]) * 3600 + int(n[8]) * 60) * (1 if n[6] == '+' else -1)
return ts - offset
SOURCES = {'bbc': bbc, 'rtp': rtp, 'rtve': rtve, 'wbur': wbur}
# --- guide ------------------------------------------------------------------
def fetch_all(log=print, now=None):
"""{tvg-id: [programmes]} for every channel whose source answered."""
now = now or time.time()
guide = {}
for cid, (source, sid) in CHANNELS.items():
if source not in SOURCES:
continue
try:
guide[cid] = SOURCES[source](sid, now)
except Exception as e: # one broken source mustn't stop the others
log('EPG {} ({}) failed: {}'.format(cid, source, e))
shared = dict((sid, cid) for cid, (source, sid) in CHANNELS.items() if source == 'epgshare_uk')
if shared:
try:
for sid, programmes in epgshare_uk(list(shared), now).items():
if programmes:
guide[shared[sid]] = programmes
except Exception as e:
log('EPG epgshare01 UK failed: {}'.format(e))
return guide
def xmltv(guide, names):
def stamp(ts):
return time.strftime('%Y%m%d%H%M%S +0000', time.gmtime(ts))
def esc(s):
return html.escape(s or '', quote=True)
lines = ['<?xml version="1.0" encoding="UTF-8"?>', '<tv generator-info-name="service.radios.vigo">']
for cid in sorted(guide):
lines.append(' <channel id="{}"><display-name>{}</display-name></channel>'.format(esc(cid), esc(names.get(cid, cid))))
for cid in sorted(guide):
for p in sorted(guide[cid], key=lambda p: p['start']):
lines.append(' <programme start="{}" stop="{}" channel="{}">'.format(stamp(p['start']), stamp(p['stop']), esc(cid)))
lines.append(' <title>{}</title>'.format(esc(p['title'])))
if p.get('subtitle'):
lines.append(' <sub-title>{}</sub-title>'.format(esc(p['subtitle'])))
if p.get('desc'):
lines.append(' <desc>{}</desc>'.format(esc(p['desc'])))
if p.get('category'):
lines.append(' <category>{}</category>'.format(esc(p['category'])))
if p.get('icon'):
lines.append(' <icon src="{}"/>'.format(esc(p['icon'])))
lines.append(' </programme>')
lines.append('</tv>')
return '\n'.join(lines) + '\n'
def merge(old, new, now):
"""New programmes, plus the old ones of channels whose source failed this
time, as long as they still reach into the future."""
guide = dict(new)
for cid, programmes in (old or {}).items():
if cid not in guide and any(p['stop'] > now for p in programmes):
guide[cid] = programmes
for cid in guide: # keep a few hours of the past, drop the rest
guide[cid] = [p for p in guide[cid] if p['stop'] > now - 6 * 3600]
return guide
def channel_names(m3u):
return dict(re.findall(r'tvg-id="([^"]+)".*,(.*)', m3u))
if __name__ == '__main__':
import os
import sys
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
with io.open(os.path.join(root, 'radio.m3u'), encoding='utf-8') as f:
names = channel_names(f.read())
g = fetch_all()
for cid in sorted(CHANNELS):
ps = g.get(cid, [])
first = time.strftime('%d %H:%M', time.gmtime(ps[0]['start'])) if ps else '-'
last = time.strftime('%d %H:%M', time.gmtime(max(p['stop'] for p in ps))) if ps else '-'
print('{:<28} {:>4} programmes {} -> {} UTC'.format(cid, len(ps), first, last))
if len(sys.argv) > 1:
with io.open(sys.argv[1], 'w', encoding='utf-8') as f:
f.write(xmltv(g, names))
+75 -14
View File
@@ -1,24 +1,34 @@
# -*- coding: utf-8 -*- # -*- 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 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 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 glob
import hashlib import hashlib
import io import io
import json import json
import os import os
import threading
import time
import xbmc import xbmc
import xbmcaddon import xbmcaddon
import xbmcvfs import xbmcvfs
from resources.lib import epg
ADDON = xbmcaddon.Addon() ADDON = xbmcaddon.Addon()
ADDON_ID = ADDON.getAddonInfo('id') ADDON_ID = ADDON.getAddonInfo('id')
HOME = 'special://home/addons/{}/resources/'.format(ADDON_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) FIRST_INSTANCE = 95 # KodiMov+ uses 91 (VAIO) and 93 (.49)
GUIDE_EVERY = 6 * 3600
GUIDE_RETRY = 30 * 60
SETTINGS = '''<settings version="2"> SETTINGS = '''<settings version="2">
<setting id="kodi_addon_instance_name">Radios de Vigo</setting> <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="m3uCache" default="true">true</setting>
<setting id="startNum" default="true">1</setting> <setting id="startNum" default="true">1</setting>
<setting id="numberByOrder" default="true">false</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="epgPathType">0</setting>
<setting id="epgPath" default="true" /> <setting id="epgPath">{profile}epg.xml</setting>
<setting id="epgUrl" default="true" /> <setting id="epgUrl" default="true" />
<setting id="epgCache" default="true">true</setting>
<setting id="logoPathType">0</setting> <setting id="logoPathType">0</setting>
<setting id="logoPath">{home}logos/</setting> <setting id="logoPath">{home}logos/</setting>
<setting id="useLogosLocalPathOnly" default="true">false</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="useFFmpegReconnect" default="true">true</setting>
<setting id="useInputstreamAdaptiveforHls" default="true">false</setting> <setting id="useInputstreamAdaptiveforHls" default="true">false</setting>
</settings> </settings>
'''.format(home=HOME) '''.format(home=HOME, profile=PROFILE)
def log(msg): def log(msg):
@@ -85,13 +97,35 @@ def restart_iptv_simple():
xbmc.sleep(3000) 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(): def main():
monitor, player = xbmc.Monitor(), xbmc.Player() 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/') pvr_dir = xbmcvfs.translatePath('special://profile/addon_data/pvr.iptvsimple/')
for d in (profile, pvr_dir): for d in (profile, pvr_dir):
if not os.path.isdir(d): if not os.path.isdir(d):
os.makedirs(d) os.makedirs(d)
m3u = read(xbmcvfs.translatePath(HOME + 'radio.m3u')) or ''
path = instance_file(pvr_dir) path = instance_file(pvr_dir)
if read(path) != SETTINGS: if read(path) != SETTINGS:
@@ -108,19 +142,46 @@ def main():
write(numbers_set, '') write(numbers_set, '')
log('turned on backend channel numbers') log('turned on backend channel numbers')
m3u = read(xbmcvfs.translatePath(HOME + 'radio.m3u')) or '' # The guide is fetched in the background so Kodi can always stop this service quickly
state = hashlib.sha1((SETTINGS + m3u).encode('utf-8')).hexdigest() guide_ok = [True]
if read(os.path.join(profile, 'state')) == state:
return
while player.isPlaying(): # restarting IPTV Simple stops the radio or TV playing def guide_job():
if monitor.waitForAbort(10): try:
return 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): if monitor.waitForAbort(5):
return break
log('station list or settings changed, restarting IPTV Simple') log('station list or settings changed, restarting IPTV Simple')
restart_iptv_simple() restart_iptv_simple()
write(os.path.join(profile, 'state'), state) 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__': if __name__ == '__main__':