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

+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
nothing if something is playing."""
import xbmc
monitor, player = xbmc.Monitor(), xbmc.Player()
if player.isPlaying(): return
monitor = xbmc.Monitor()
if not wait_until_idle(timeout=600): return
if not set_iptv_refresh(pvr_dir, addon_id, 1): return
try:
restart_iptv_simple()
monitor.waitForAbort(90)
finally:
set_iptv_refresh(pvr_dir, addon_id, 60)
while player.isPlaying(): # until then it keeps reloading every minute
if monitor.waitForAbort(10): return
if not monitor.abortRequested():
if wait_until_idle(): # until then it keeps reloading every minute
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
# 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
# Kodi's home window (the time it started), and the others wait.
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():
@@ -526,6 +552,7 @@ def restart_iptv_simple():
if not busy: break
xbmc.sleep(1000)
window.setProperty(IPTV_RESTART_PROPERTY, str(time.time()))
window.setProperty(LAST_RESTART_PROPERTY, str(time.time()))
try:
for enabled in (False, True):
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'):
notify(addon.getLocalizedString(30311))
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
try:
pvr_dir = translatePath(xbmcaddon.Addon('pvr.iptvsimple').getAddonInfo('profile'))
@@ -941,7 +941,7 @@ def router(paramstring):
elif params['action'] == 'create_iptv_settings':
create_iptv_settings()
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':
to_wishlist(params)
elif 'iptv' in params['action']:
+13 -1
View File
@@ -274,6 +274,10 @@ PORT = 57031
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
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):
started = False
@@ -281,8 +285,8 @@ class Proxy(object):
def check_port(self, port=0, default=False):
try:
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.bind((HOST, port))
return s.getsockname()[1]
except:
return default
@@ -292,6 +296,14 @@ class Proxy(object):
return
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:
port = self.check_port(0)
LOG('port: {}'.format(port))