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