Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion Stream-Mapparr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@

from .plugin import Plugin

__version__ = "1.26.2561754"
__version__ = "1.26.2571153"
__all__ = ["Plugin"]
2 changes: 1 addition & 1 deletion Stream-Mapparr/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
27 changes: 26 additions & 1 deletion Stream-Mapparr/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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",
Expand Down
79 changes: 79 additions & 0 deletions tests/test_iptv_checker_trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -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