From 864ca405c2599c500a8cf04f3c7557fe23a712f1 Mon Sep 17 00:00:00 2001 From: PiratesIRC <98669745+PiratesIRC@users.noreply.github.com> Date: Mon, 14 Sep 2026 06:53:25 -0500 Subject: [PATCH] fix(scheduler): the timer run stands aside when a scan will trigger the steps Measured 2026-09-14: with Run After IPTV Checker Scan on, the 05:05 run that had been waiting for the checker and the run the checker then triggered sorted the same channels concurrently, one second apart, in two workers. The timer path now checks, before it waits, whether a scan is running and the trigger setting is on; if so it logs that it is standing aside and returns "skipped". A missing or unreadable progress file counts as not running, so the schedule is unchanged where IPTV Checker is absent. The scheduled-run timestamp is not written for a run that stood aside. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WJpo1EzMNmJiSiNpbwBi6G --- CHANGELOG.md | 17 +++++++ Stream-Mapparr/__init__.py | 2 +- Stream-Mapparr/plugin.json | 2 +- Stream-Mapparr/plugin.py | 27 +++++++++- tests/test_iptv_checker_trigger.py | 79 ++++++++++++++++++++++++++++++ 5 files changed, 124 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85217a6..c0bf56f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Stream-Mapparr CHANGELOG +## 1.26.2571153 (2026-09-14) + +### Changed + +- **A scheduled run now stands aside when IPTV Checker is mid-scan and Run After + IPTV Checker Scan is on.** On the first morning the trigger fired, the fixed-time + run had been waiting for that same scan under Wait for IPTV Checker Completion, + saw the scan end, and sorted the same channels one second after the triggered run, + in a different worker process, while that run was still writing. Nothing was + damaged, but two delete-and-recreate passes over the same rows at once is not a + state to leave open. The fixed-time run now checks, before it waits, whether a + scan is running and the trigger will cover the day; if both are true it logs that + it is standing aside and returns without running. A missing or unreadable + progress file counts as not running, so an installation without IPTV Checker + keeps its schedule exactly as before. The scheduled-run timestamp behind + Validate Settings is not written for a run that stood aside. + ## 1.26.2561754 (2026-09-13) ### Added diff --git a/Stream-Mapparr/__init__.py b/Stream-Mapparr/__init__.py index cb08636..1416582 100644 --- a/Stream-Mapparr/__init__.py +++ b/Stream-Mapparr/__init__.py @@ -5,5 +5,5 @@ from .plugin import Plugin -__version__ = "1.26.2561754" +__version__ = "1.26.2571153" __all__ = ["Plugin"] \ No newline at end of file diff --git a/Stream-Mapparr/plugin.json b/Stream-Mapparr/plugin.json index ee72c50..df97357 100644 --- a/Stream-Mapparr/plugin.json +++ b/Stream-Mapparr/plugin.json @@ -1,6 +1,6 @@ { "name": "Stream-Mapparr", - "version": "1.26.2561754", + "version": "1.26.2571153", "description": "Automatically add matching streams to channels based on name similarity and quality precedence. Supports unlimited stream matching, channel visibility management, and CSV export cleanup.", "author": "PiratesIRC", "license": "MIT", diff --git a/Stream-Mapparr/plugin.py b/Stream-Mapparr/plugin.py index 8ee8a8c..88de673 100644 --- a/Stream-Mapparr/plugin.py +++ b/Stream-Mapparr/plugin.py @@ -303,7 +303,7 @@ class PluginConfig: """ # === PLUGIN METADATA === - PLUGIN_VERSION = "1.26.2561754" + PLUGIN_VERSION = "1.26.2571153" FUZZY_MATCHER_MIN_VERSION = "25.358.0200" # Requires custom ignore tags Unicode fix # Match sensitivity presets (maps select value to threshold number) @@ -8091,6 +8091,18 @@ def _run_scheduled_sequence(self, settings, logger, trigger=None): and the timestamp means "the timer fired". """ if trigger is None: + # Measured 2026-09-14: with the trigger on, a timer run that waited + # for the checker and the run the checker then triggered sorted the + # same channels concurrently, one second apart, in two workers. + # When the trigger will cover the day, the timer stands aside. The + # slot is already claimed, so nothing re-runs; the scheduled-run + # timestamp is not written, because no run happened here. + if (self._should_run_after_iptv_checker_scan(settings) + and self._iptv_checker_is_running(logger)): + logger.info("[Stream-Mapparr] IPTV Checker is running and 'Run After IPTV Checker Scan' " + "is on, so this scheduled run stands aside; the scan will trigger the steps when it finishes") + return {"status": "skipped", + "message": "Left to the IPTV Checker trigger, because a scan is in progress"} if not self._wait_for_iptv_checker_completion(settings, logger): logger.warning("[Stream-Mapparr] IPTV Checker wait timed out, proceeding anyway") else: @@ -8143,6 +8155,19 @@ def _run_scheduled_sequence(self, settings, logger, trigger=None): logger.info("[Stream-Mapparr] Scheduled run completed successfully") return {"status": "success"} + def _iptv_checker_is_running(self, logger): + """True only when the IPTV Checker progress file says a check is running. + + Fails open to False: a missing, unreadable or unparseable file means the + timer run proceeds as it always did, so a checker that is not installed + can never silence the schedule. + """ + try: + with open(PluginConfig.IPTV_CHECKER_PROGRESS_FILE, 'r') as fh: + return json.load(fh).get('status') == 'running' + except (OSError, ValueError, AttributeError): + return False + def _should_run_after_iptv_checker_scan(self, settings): """Pure gate for the IPTV Checker trigger: the setting alone, no ORM.""" return self._get_bool_setting(settings, "run_after_iptv_checker_scan", diff --git a/tests/test_iptv_checker_trigger.py b/tests/test_iptv_checker_trigger.py index a183ee4..8fc7d9c 100644 --- a/tests/test_iptv_checker_trigger.py +++ b/tests/test_iptv_checker_trigger.py @@ -206,3 +206,82 @@ def test_a_triggered_run_names_its_trigger_in_the_header(plugin_module): def test_a_timer_run_does_not_mention_a_trigger(plugin_module): line = _mode_line(plugin_module, is_scheduled=True) assert "Scheduled" in line and "after" not in line + + +# --------------------------------------------------------------------------- # +# The timer run stands aside when the trigger will cover the day (2026-09-14) +# --------------------------------------------------------------------------- # +def _wire_running(monkeypatch, p, running): + monkeypatch.setattr(p, "_iptv_checker_is_running", lambda logger: running) + + +def test_the_timer_skips_when_the_trigger_is_on_and_the_checker_is_running(plugin_module, monkeypatch): + """Measured 2026-09-14: the waiting timer run and the triggered run sorted + the same channels concurrently, one second apart, in two workers.""" + p = _plugin(plugin_module) + calls, stamps = _wire_sequence(monkeypatch, p) + _wire_running(monkeypatch, p, True) + settings = {"run_after_iptv_checker_scan": True, "scheduled_sort_streams": True, + "scheduled_match_streams": False} + result = p._run_scheduled_sequence(settings, log) + assert result["status"] == "skipped" + assert calls == [] + assert stamps == [] + + +def test_the_timer_still_waits_when_the_trigger_is_off(plugin_module, monkeypatch): + p = _plugin(plugin_module) + calls, _ = _wire_sequence(monkeypatch, p) + _wire_running(monkeypatch, p, True) + settings = {"run_after_iptv_checker_scan": False, "scheduled_sort_streams": True, + "scheduled_match_streams": False} + p._run_scheduled_sequence(settings, log) + assert calls == ["wait", "load", ("sort", True, None)] + + +def test_the_timer_runs_normally_when_the_checker_is_idle(plugin_module, monkeypatch): + p = _plugin(plugin_module) + calls, _ = _wire_sequence(monkeypatch, p) + _wire_running(monkeypatch, p, False) + settings = {"run_after_iptv_checker_scan": True, "scheduled_sort_streams": True, + "scheduled_match_streams": False} + p._run_scheduled_sequence(settings, log) + assert calls == ["wait", "load", ("sort", True, None)] + + +def test_the_triggered_path_never_consults_the_running_check(plugin_module, monkeypatch): + p = _plugin(plugin_module) + calls, _ = _wire_sequence(monkeypatch, p) + + def boom(logger): + raise AssertionError("the triggered path must not ask whether the checker is running") + monkeypatch.setattr(p, "_iptv_checker_is_running", boom) + settings = {"run_after_iptv_checker_scan": True, "scheduled_sort_streams": True, + "scheduled_match_streams": False} + p._run_scheduled_sequence(settings, log, trigger=TRIGGER) + assert calls == ["load", ("sort", True, TRIGGER)] + + +def _progress(plugin_module, monkeypatch, tmp_path, content): + path = tmp_path / "iptv_checker_progress.json" + if content is not None: + path.write_text(content, encoding="utf-8") + monkeypatch.setattr(plugin_module.PluginConfig, "IPTV_CHECKER_PROGRESS_FILE", str(path)) + + +def test_running_check_reads_the_progress_file(plugin_module, monkeypatch, tmp_path): + p = _plugin(plugin_module) + _progress(plugin_module, monkeypatch, tmp_path, '{"status": "running", "current": 3, "total": 9}') + assert p._iptv_checker_is_running(log) is True + _progress(plugin_module, monkeypatch, tmp_path, '{"status": "idle"}') + assert p._iptv_checker_is_running(log) is False + + +def test_running_check_fails_open_to_not_running(plugin_module, monkeypatch, tmp_path): + """A missing or unreadable file must not make the timer stand aside, or a + checker that is not installed would silence the schedule forever.""" + p = _plugin(plugin_module) + _progress(plugin_module, monkeypatch, tmp_path, None) + assert p._iptv_checker_is_running(log) is False + _progress(plugin_module, monkeypatch, tmp_path, "{not json") + assert p._iptv_checker_is_running(log) is False