1.0.0+kodimov.13: updates don't stop live TV; IPTV Simple restarts only when idle

- After an add-on update the new service found port 57031 still held by the
  old proxy for a moment and moved to a random port; the stream playing kept
  asking 57031 for its manifest and died a minute later (.49, 2026-11-13).
  The proxy now waits up to 10 s for the port, and SO_REUSEADDR /
  allow_reuse_address are set before binding (upstream set them after, so
  they did nothing).
- IPTV Simple is restarted only when nothing plays and no key was pressed
  for 30 s: on .49 a channel opened during a restart failed with an error.
- After start or update, changed instance files (duplicate removed, refresh
  timer) are picked up by the guide reload instead of a restart of their
  own; the service restarts IPTV Simple itself only if that reload didn't
  happen within 10 minutes. 3 restarts after kodimov.12 -> 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
XoséandClaude Opus 5.5 committed 2026-11-13 15:16:30 +01:00
1 parent 69a37ff064
commit 7873b50203
6 files changed
+63 -15

No files matched your search

+2 -1
View File
@@ -18,7 +18,8 @@ What it adds:
- **Gets along with Radios de Vigo** (`service.radios.vigo`), which keeps its own IPTV Simple instance for the Radio section. Kodi only lets *Use channel numbers from backend* be changed while it applies (one PVR instance loaded, or several and *always* on), so with a second instance it had refused the setting. Both add-ons now check it every so often: if it is hidden they turn *always* on, and they turn the setting itself on once; neither ever turns anything off, and turning it off by hand afterwards is respected. They also take turns to restart IPTV Simple, through the `pvr.iptvsimple.restarting` property of Kodi's home window. - **Gets along with Radios de Vigo** (`service.radios.vigo`), which keeps its own IPTV Simple instance for the Radio section. Kodi only lets *Use channel numbers from backend* be changed while it applies (one PVR instance loaded, or several and *always* on), so with a second instance it had refused the setting. Both add-ons now check it every so often: if it is hidden they turn *always* on, and they turn the setting itself on once; neither ever turns anything off, and turning it off by hand afterwards is respected. They also take turns to restart IPTV Simple, through the `pvr.iptvsimple.restarting` property of Kodi's home window.
- **One IPTV Simple instance per channel list**: the TV guide setup rewrites the instance that already reads KodiMov+'s list (one set up with the original add-on, or by hand) instead of adding `instance-settings-91.xml` next to it, and the service deletes any extra one at Kodi start (keeping the one that isn't 91, which holds what was watched): two of them showed every channel twice. - **One IPTV Simple instance per channel list**: the TV guide setup rewrites the instance that already reads KodiMov+'s list (one set up with the original add-on, or by hand) instead of adding `instance-settings-91.xml` next to it, and the service deletes any extra one at Kodi start (keeping the one that isn't 91, which holds what was watched): two of them showed every channel twice.
- Fixes the programme guide export of upstream 1.0.0 (the amazon.tv guide is a flat list). - Fixes the programme guide export of upstream 1.0.0 (the amazon.tv guide is a flat list).
- Its own proxy port (57031), so it can run next to the original. - Its own proxy port (57031), so it can run next to the original. After an update the new proxy waits for the old one to free the port instead of moving to a random one, so the channel being watched keeps playing (upstream sets `allow_reuse_address` and `SO_REUSEADDR` too late to take effect).
- **Restarts IPTV Simple only while idle**: nothing playing and no key pressed for 30 seconds (a channel opened during a restart fails). After start or update a single guide reload also picks up changed instance files.
--- ---
+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.12" version="1.0.0+kodimov.13"
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, plays the LALIGA, DAZN LALIGA and Liga de Campeones channels through Movistar's new servers (from the Movistar home network), hides the channels it can't play, 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, plays the LALIGA, DAZN LALIGA and Liga de Campeones channels through Movistar's new servers (from the Movistar home network), hides the channels it can't play, 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, reproduce los canales de LALIGA, DAZN LALIGA y Liga de Campeones a través de los servidores nuevos de Movistar (desde la red de Movistar de casa), oculta los canales que no puede reproducir, 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, reproduce los canales de LALIGA, DAZN LALIGA y Liga de Campeones a través de los servidores nuevos de Movistar (desde la red de Movistar de casa), oculta los canales que no puede reproducir, 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.12: no more channels twice in the TV guide on boxes where the TV guide setup was run over an existing setup (the extra IPTV Simple instance is removed, and the setup now rewrites the existing one). LALIGA, DAZN LALIGA and Liga de Campeones no longer go missing for 6 hours when Movistar's new servers refuse the box's registration as a web player once: it is tried again, and the TV guide list is rewritten half an hour later. <news>1.0.0+kodimov.13: updating the add-on no longer stops the channel being watched (the proxy got a new port and the stream lost it). IPTV Simple is restarted only while nothing plays and nobody has used the remote for 30 seconds, and fewer times after an update.
1.0.0+kodimov.12: no more channels twice in the TV guide on boxes where the TV guide setup was run over an existing setup (the extra IPTV Simple instance is removed, and the setup now rewrites the existing one). LALIGA, DAZN LALIGA and Liga de Campeones no longer go missing for 6 hours when Movistar's new servers refuse the box's registration as a web player once: it is tried again, and the TV guide list is rewritten half an hour later.
1.0.0+kodimov.11: Movistar's channel numbers also show on boxes with more than one IPTV Simple instance (Radios de Vigo, a leftover empty one), where Kodi had refused the setting and numbered the channels 1, 2, 3...; existing TV guide setups are put right by themselves. KodiMov+ and Radios de Vigo no longer restart IPTV Simple at the same time. 1.0.0+kodimov.11: Movistar's channel numbers also show on boxes with more than one IPTV Simple instance (Radios de Vigo, a leftover empty one), where Kodi had refused the setting and numbered the channels 1, 2, 3...; existing TV guide setups are put right by themselves. KodiMov+ and Radios de Vigo no longer restart IPTV Simple at the same time.
1.0.0+kodimov.10: the LALIGA, DAZN LALIGA and Liga de Campeones channels are back, live and catch-up, on boxes connected to the Movistar home network (their streams are now served only through Movistar's new servers). New setting to always play at the best quality, up to 1080p, instead of the 576p Kodi stays at on automatic (off by default). The "Error 4027" message no longer pops up when the add-on recovers from it by itself. 1.0.0+kodimov.10: the LALIGA, DAZN LALIGA and Liga de Campeones channels are back, live and catch-up, on boxes connected to the Movistar home network (their streams are now served only through Movistar's new servers). New setting to always play at the best quality, up to 1080p, instead of the 576p Kodi stays at on automatic (off by default). The "Error 4027" message no longer pops up when the add-on recovers from it by itself.
1.0.0+kodimov.9: after Kodi starts or the add-on updates, a new programme guide shows up in Kodi within a couple of minutes instead of at the next hourly reload (only while nothing is playing). 1.0.0+kodimov.9: after Kodi starts or the add-on updates, a new programme guide shows up in Kodi within a couple of minutes instead of at the next hourly reload (only while nothing is playing).
+32 -5
View File
@@ -492,25 +492,51 @@ def reload_iptv_guide(pvr_dir, addon_id):
restart it with the hourly timer. Restarting stops live TV, so this does restart it with the hourly timer. Restarting stops live TV, so this does
nothing if something is playing.""" nothing if something is playing."""
import xbmc import xbmc
monitor, player = xbmc.Monitor(), xbmc.Player() monitor = xbmc.Monitor()
if player.isPlaying(): return if not wait_until_idle(timeout=600): return
if not set_iptv_refresh(pvr_dir, addon_id, 1): return if not set_iptv_refresh(pvr_dir, addon_id, 1): return
try: try:
restart_iptv_simple() restart_iptv_simple()
monitor.waitForAbort(90) monitor.waitForAbort(90)
finally: finally:
set_iptv_refresh(pvr_dir, addon_id, 60) set_iptv_refresh(pvr_dir, addon_id, 60)
while player.isPlaying(): # until then it keeps reloading every minute if wait_until_idle(): # until then it keeps reloading every minute
if monitor.waitForAbort(10): return
if not monitor.abortRequested():
restart_iptv_simple() restart_iptv_simple()
def is_idle(seconds=30):
"""Nothing playing and nobody pressed a key for a while. Restarting IPTV
Simple stops live TV, and a channel opened while it restarts fails."""
import xbmc
return not xbmc.Player().isPlaying() and xbmc.getGlobalIdleTime() >= seconds
def wait_until_idle(timeout=None):
"""Waits until is_idle(). False if Kodi is closing or the timeout ran out."""
import xbmc
monitor = xbmc.Monitor()
end = time.time() + timeout if timeout else None
while not is_idle():
if monitor.waitForAbort(5) or (end and time.time() > end): return False
return not monitor.abortRequested()
# IPTV Simple is shared with other add-ons (Radios de Vigo keeps its own # IPTV Simple is shared with other add-ons (Radios de Vigo keeps its own
# instance for the Radio section). Restarting it restarts every instance, so # instance for the Radio section). Restarting it restarts every instance, so
# the add-ons take turns: whoever restarts it says so in this property of # the add-ons take turns: whoever restarts it says so in this property of
# Kodi's home window (the time it started), and the others wait. # Kodi's home window (the time it started), and the others wait.
IPTV_RESTART_PROPERTY = 'pvr.iptvsimple.restarting' IPTV_RESTART_PROPERTY = 'pvr.iptvsimple.restarting'
# When this add-on last restarted it, so the service can skip a restart that
# one from the plugin (the guide reload) already did
LAST_RESTART_PROPERTY = 'kodimovplus.iptvsimple.restarted'
def last_iptv_restart():
import xbmcgui
try:
return float(xbmcgui.Window(10000).getProperty(LAST_RESTART_PROPERTY) or 0)
except ValueError:
return 0
def restart_iptv_simple(): def restart_iptv_simple():
@@ -526,6 +552,7 @@ def restart_iptv_simple():
if not busy: break if not busy: break
xbmc.sleep(1000) xbmc.sleep(1000)
window.setProperty(IPTV_RESTART_PROPERTY, str(time.time())) window.setProperty(IPTV_RESTART_PROPERTY, str(time.time()))
window.setProperty(LAST_RESTART_PROPERTY, str(time.time()))
try: try:
for enabled in (False, True): for enabled in (False, True):
xbmc.executeJSONRPC(json.dumps({'jsonrpc': '2.0', 'id': 1, 'method': 'Addons.SetAddonEnabled', xbmc.executeJSONRPC(json.dumps({'jsonrpc': '2.0', 'id': 1, 'method': 'Addons.SetAddonEnabled',
+2 -2
View File
@@ -835,7 +835,7 @@ def export_epg_now(quiet=False, reload_guide=False):
if True: #not addon.getSettingBool('use_external_epg'): if True: #not addon.getSettingBool('use_external_epg'):
notify(addon.getLocalizedString(30311)) notify(addon.getLocalizedString(30311))
changed = m.export_epg_to_xml(epg_filename, addon.getSettingInt('export_days'), report_progress, only_subscribed) changed = m.export_epg_to_xml(epg_filename, addon.getSettingInt('export_days'), report_progress, only_subscribed)
if changed and reload_guide: if reload_guide == 'force' or (changed and reload_guide):
from . import kodimov from . import kodimov
try: try:
pvr_dir = translatePath(xbmcaddon.Addon('pvr.iptvsimple').getAddonInfo('profile')) pvr_dir = translatePath(xbmcaddon.Addon('pvr.iptvsimple').getAddonInfo('profile'))
@@ -941,7 +941,7 @@ def router(paramstring):
elif params['action'] == 'create_iptv_settings': elif params['action'] == 'create_iptv_settings':
create_iptv_settings() create_iptv_settings()
elif params['action'] == 'export_epg_now': elif params['action'] == 'export_epg_now':
export_epg_now(quiet=params.get('quiet') == '1', reload_guide=params.get('reload_guide') == '1') export_epg_now(quiet=params.get('quiet') == '1', reload_guide=params.get('reload_guide'))
elif params['action'] == 'to_wishlist': elif params['action'] == 'to_wishlist':
to_wishlist(params) to_wishlist(params)
elif 'iptv' in params['action']: elif 'iptv' in params['action']:
+13 -1
View File
@@ -274,6 +274,10 @@ PORT = 57031
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
daemon_threads = True daemon_threads = True
# KodiMov+: must be set before the server binds (setting it on the
# instance afterwards did nothing): connections of the previous proxy
# left in TIME_WAIT would otherwise keep the port taken
allow_reuse_address = True
class Proxy(object): class Proxy(object):
started = False started = False
@@ -281,8 +285,8 @@ class Proxy(object):
def check_port(self, port=0, default=False): def check_port(self, port=0, default=False):
try: try:
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
s.bind((HOST, port))
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((HOST, port))
return s.getsockname()[1] return s.getsockname()[1]
except: except:
return default return default
@@ -292,6 +296,14 @@ class Proxy(object):
return return
port = self.check_port(PORT) port = self.check_port(PORT)
# KodiMov+: when the add-on updates, the previous service may still
# hold the port for a few seconds. Wait for it: a stream that is
# playing keeps asking this port for its manifest, and dies if the
# proxy moves to another one.
for _ in range(20):
if port: break
time.sleep(0.5)
port = self.check_port(PORT)
if not port: if not port:
port = self.check_port(0) port = self.check_port(0)
LOG('port: {}'.format(port)) LOG('port: {}'.format(port))
+11 -4
View File
@@ -29,7 +29,8 @@ if __name__ == '__main__':
# programme guide a minute after Kodi starts and then every 6 hours; a # programme guide a minute after Kodi starts and then every 6 hours; a
# refresh that failed is tried again half an hour later. Opening the add-on # refresh that failed is tried again half an hour later. Opening the add-on
# also renews the 10-day Movistar login. # also renews the 10-day Movistar login.
next_check = time.time() + 60 started = time.time()
next_check = started + 60
max_age = 0 # always refresh once after Kodi starts max_age = 0 # always refresh once after Kodi starts
new_api_retries = 6 new_api_retries = 6
try: try:
@@ -39,12 +40,17 @@ if __name__ == '__main__':
except RuntimeError: # IPTV Simple not installed or disabled except RuntimeError: # IPTV Simple not installed or disabled
pvr_dir = None pvr_dir = None
restart_iptv = False restart_iptv = False
player = xbmc.Player() # A changed instance file needs IPTV Simple restarted. The guide reload
# after start (below) restarts it anyway, so that one is left to do it;
# this is only the fallback, 10 minutes later, if it didn't happen.
restart_after = started + 10 * 60
monitor = xbmc.Monitor() monitor = xbmc.Monitor()
while not monitor.abortRequested(): while not monitor.abortRequested():
if monitor.waitForAbort(1): if monitor.waitForAbort(1):
break break
if restart_iptv and not player.isPlaying(): # restarting stops live TV if restart_iptv and kodimov.last_iptv_restart() > started:
restart_iptv = False
if restart_iptv and time.time() >= restart_after and kodimov.is_idle():
restart_iptv = False restart_iptv = False
kodimov.restart_iptv_simple() kodimov.restart_iptv_simple()
if time.time() >= next_check: if time.time() >= next_check:
@@ -57,13 +63,14 @@ if __name__ == '__main__':
age_limit, max_age = max_age, 6 * 3600 - 120 age_limit, max_age = max_age, 6 * 3600 - 120
# After Kodi starts (or this add-on updates) also get the new guide into Kodi now # After Kodi starts (or this add-on updates) also get the new guide into Kodi now
reload_guide = '&reload_guide=1' if age_limit == 0 else '' reload_guide = '&reload_guide=1' if age_limit == 0 else ''
if restart_iptv: reload_guide = '&reload_guide=force' # also for the changed instance files
# The new servers failed (they at times refuse everything for minutes): # The new servers failed (they at times refuse everything for minutes):
# the list lacks LALIGA, DAZN LALIGA, Liga de Campeones. Try again in # the list lacks LALIGA, DAZN LALIGA, Liga de Campeones. Try again in
# half an hour, a few times, instead of in 6 hours. # half an hour, a few times, instead of in 6 hours.
if new_api_retries and kodimov.new_api_failed(profile_dir): if new_api_retries and kodimov.new_api_failed(profile_dir):
new_api_retries -= 1 new_api_retries -= 1
age_limit = 0 age_limit = 0
reload_guide = '&reload_guide=1' reload_guide = reload_guide or '&reload_guide=1'
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: if file_age(os.path.join(profile_dir, 'epg.xml')) > age_limit:
exporting = True exporting = True