1.0.0+kodimov.8: guide refresh doesn't fail at random; retry after 30 min

datetime.strptime sometimes fails in Kodi's embedded Python ('NoneType'
object is not callable), which aborted the whole guide export. Build the
XMLTV times without it.

The service now checks every 30 minutes and refreshes when the files are
over 6 hours old, so a failed refresh is retried soon. write_if_changed
touches an unchanged file so its time means "last refreshed".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
XoséandClaude Opus 5.5 committed 2026-11-10 08:36:21 +01:00
1 parent 9faaa482fc
commit 5df60a1fb8
4 files changed
+25 -29

No files matched your search

+3 -2
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.kodimovplus" <addon id="plugin.video.kodimovplus"
name="KodiMov+" name="KodiMov+"
version="1.0.0+kodimov.7" version="1.0.0+kodimov.8"
provider-name="Paco8, xocko"> provider-name="Paco8, xocko">
<requires> <requires>
<!--- <import addon="xbmc.python" version="2.25.0"/> --> <!--- <import addon="xbmc.python" version="2.25.0"/> -->
@@ -21,7 +21,8 @@
<summary lang="es_ES">Movistar+ para Kodi, con inicio de sesión automático desde la red de Movistar</summary> <summary lang="es_ES">Movistar+ para Kodi, con inicio de sesión automático desde la red de Movistar</summary>
<description lang="en_GB">Fork of Paco8's Movistar+ add-on (plugin.video.movistarplus). Logs in by itself from the Movistar home network, keeps the channel list and numbers up to date, hides the channels that need stronger DRM than Kodi has, adds free Galician channels (TVG2, TeleVigo) and writes a programme guide for every channel. Requires a Movistar+ subscription.</description> <description lang="en_GB">Fork of Paco8's Movistar+ add-on (plugin.video.movistarplus). Logs in by itself from the Movistar home network, keeps the channel list and numbers up to date, hides the channels that need stronger DRM than Kodi has, adds free Galician channels (TVG2, TeleVigo) and writes a programme guide for every channel. Requires a Movistar+ subscription.</description>
<description lang="es_ES">Fork del addon Movistar+ de Paco8 (plugin.video.movistarplus). Inicia sesión solo desde la red de Movistar, mantiene la lista y los números de los canales al día, oculta los canales que necesitan una protección que Kodi no tiene añade canales gallegos gratuitos (TVG2, TeleVigo) y genera la guía de programación de todos los canales. Se necesita estar abonado a Movistar+.</description> <description lang="es_ES">Fork del addon Movistar+ de Paco8 (plugin.video.movistarplus). Inicia sesión solo desde la red de Movistar, mantiene la lista y los números de los canales al día, oculta los canales que necesitan una protección que Kodi no tiene añade canales gallegos gratuitos (TVG2, TeleVigo) y genera la guía de programación de todos los canales. Se necesita estar abonado a Movistar+.</description>
<news>1.0.0+kodimov.7: the programme guide has the synopsis, picture, genre, year and age rating of each programme. IPTV Simple now reloads the guide every hour instead of once a day, so a new guide shows up in Kodi by itself (existing TV guide setups are switched over once). <news>1.0.0+kodimov.8: the programme guide refresh no longer fails at random inside Kodi, and a failed refresh is tried again half an hour later instead of 6 hours later.
1.0.0+kodimov.7: the programme guide has the synopsis, picture, genre, year and age rating of each programme. IPTV Simple now reloads the guide every hour instead of once a day, so a new guide shows up in Kodi by itself (existing TV guide setups are switched over once).
1.0.0+kodimov.6: the programme guide refreshes on its own again on boxes first set up with kodimov.1, where the automatic guide export had stayed off (the guide ran out after 2 days). 1.0.0+kodimov.6: the programme guide refreshes on its own again on boxes first set up with kodimov.1, where the automatic guide export had stayed off (the guide ran out after 2 days).
1.0.0+kodimov.5: TV guide setup writes the programme guide before creating the IPTV Simple settings, so the guide isn't empty on a new box. 1.0.0+kodimov.5: TV guide setup writes the programme guide before creating the IPTV Simple settings, so the guide isn't empty on a new box.
1.0.0+kodimov.4: first login on a new box needs no password from the Movistar home network: the account is found and the box registered automatically. 1.0.0+kodimov.4: first login on a new box needs no password from the Movistar home network: the account is found and the box registered automatically.
+3 -1
View File
@@ -200,7 +200,9 @@ def extra_channels_m3u8():
def write_if_changed(filename, content): def write_if_changed(filename, content):
try: try:
with io.open(filename, 'r', encoding='utf-8', newline='') as f: with io.open(filename, 'r', encoding='utf-8', newline='') as f:
if f.read() == content: return False if f.read() == content:
os.utime(filename, None) # the service reads the time as "last refreshed"
return False
except (IOError, OSError): except (IOError, OSError):
pass pass
tmp = filename + '.tmp' tmp = filename + '.tmp'
+3 -2
View File
@@ -1533,8 +1533,9 @@ class Movistar(object):
if only_subscribed and not ch['subscribed']: continue if only_subscribed and not ch['subscribed']: continue
for e in epg[ch['id']]: for e in epg[ch['id']]:
#LOG('* e:{}'.format(e)) #LOG('* e:{}'.format(e))
start = datetime.strptime(e['start'], "%Y-%m-%dT%H:%M:%SZ").strftime("%Y%m%d%H%M%S +0000") # 2026-11-10T08:00:00Z -> 20261110080000 +0000, without strptime: inside
stop = datetime.strptime(e['stop'], "%Y-%m-%dT%H:%M:%SZ").strftime("%Y%m%d%H%M%S +0000") # Kodi it fails at random ("'NoneType' object is not callable")
start, stop = [re.sub(r'\D', '', e[k]) + ' +0000' for k in ('start', 'stop')]
url = e.get('stream', None) url = e.get('stream', None)
if url: if url:
url = url.replace('&', '&amp;') url = url.replace('&', '&amp;')
+16 -24
View File
@@ -12,26 +12,12 @@ from resources.lib.addon import *
from resources.lib.log import LOG from resources.lib.log import LOG
from resources.lib import kodimov from resources.lib import kodimov
def export_epg(): def file_age(filename):
from datetime import datetime, timedelta """Seconds since the file was last written (or found unchanged)."""
try:
def needs_update(filename): return time.time() - os.path.getmtime(filename)
if os.path.exists(filename): except OSError:
file_modification_time = datetime.fromtimestamp(os.path.getmtime(filename)) return float('inf')
return datetime.now() - file_modification_time > timedelta(hours=6)
return True
channels_filename = os.path.join(profile_dir, 'channels.m3u8')
epg_filename = os.path.join(profile_dir, 'epg.xml')
LOG('channels filename: {}'.format(channels_filename))
LOG('epg filename: {}'.format(epg_filename))
channels_needs_update = needs_update(channels_filename)
epg_needs_update = needs_update(epg_filename)
LOG('channels_needs_update: {} epg_needs_update: {}'.format(channels_needs_update, epg_needs_update))
if channels_needs_update or epg_needs_update:
xbmc.executebuiltin('RunPlugin(plugin://plugin.video.kodimovplus/?action=export_epg_now)')
if __name__ == '__main__': if __name__ == '__main__':
LOG('Service started') LOG('Service started')
@@ -40,8 +26,11 @@ if __name__ == '__main__':
LOG('proxy_address: {}'.format(proxy.proxy_address)) LOG('proxy_address: {}'.format(proxy.proxy_address))
# Refreshes the TV guide channel list (new channels, new numbers) and the # Refreshes the TV guide channel list (new channels, new numbers) and the
# programme guide a minute after Kodi starts and then every 6 hours. Opening the add-on also renews the 10-day Movistar login. # programme guide a minute after Kodi starts and then every 6 hours; a
next_export = time.time() + 60 # refresh that failed is tried again half an hour later. Opening the add-on
# also renews the 10-day Movistar login.
next_check = time.time() + 60
max_age = 0 # always refresh once after Kodi starts
try: try:
pvr_dir = translatePath(xbmcaddon.Addon('pvr.iptvsimple').getAddonInfo('profile')) pvr_dir = translatePath(xbmcaddon.Addon('pvr.iptvsimple').getAddonInfo('profile'))
restart_iptv = kodimov.use_hourly_iptv_refresh(pvr_dir, 'kodimovplus') restart_iptv = kodimov.use_hourly_iptv_refresh(pvr_dir, 'kodimovplus')
@@ -55,11 +44,14 @@ if __name__ == '__main__':
if restart_iptv and not player.isPlaying(): # restarting stops live TV if restart_iptv and not player.isPlaying(): # restarting stops live TV
restart_iptv = False restart_iptv = False
kodimov.restart_iptv_simple() kodimov.restart_iptv_simple()
if time.time() >= next_export: if time.time() >= next_check:
next_export = time.time() + 6 * 3600 next_check = time.time() + 30 * 60
age_limit, max_age = max_age, 6 * 3600 - 120
if addon.getSettingBool('auto_export_epg'): # channels + programme guide if addon.getSettingBool('auto_export_epg'): # channels + programme guide
if file_age(os.path.join(profile_dir, 'epg.xml')) > age_limit:
xbmc.executebuiltin('RunPlugin(plugin://plugin.video.kodimovplus/?action=export_epg_now&quiet=1)') xbmc.executebuiltin('RunPlugin(plugin://plugin.video.kodimovplus/?action=export_epg_now&quiet=1)')
elif addon.getSettingBool('export_channels'): elif addon.getSettingBool('export_channels'):
if file_age(os.path.join(profile_dir, 'channels.m3u8')) > age_limit:
xbmc.executebuiltin('RunPlugin(plugin://plugin.video.kodimovplus/?action=export_channels_now)') xbmc.executebuiltin('RunPlugin(plugin://plugin.video.kodimovplus/?action=export_channels_now)')
proxy.stop() proxy.stop()