backend/tests/test_foreign_track_cleanup_deferred.py
Rama master · réplica de lectura del repositorio original.
"""A provider download hands the foreign-track remux to the automation queue.
Prod 2026-09-24/25: ``save_subtitle`` ran the foreign-track cleanup inline, so
every subtitle a scheduled search saved could wait up to 3600 s for the media
I/O slot behind ffsubsync. Items took 2 227-5 642 s instead of 158 s and the
search was booked ``timeout`` / ``timeout_abandoned``. The remux now runs in the
automation drain, which owns long media work; the search only enqueues.
"""
from types import SimpleNamespace
from unittest.mock import patch
from providers.base import SubtitleFormat
_SRT = b"1\n00:00:01,000 --> 00:00:02,000\nHallo Welt\n\n2\n00:00:03,000 --> 00:00:04,000\nZweite Zeile\n"
def _result():
return SimpleNamespace(
content=_SRT,
format=SubtitleFormat.SRT,
language="de",
provider_name="opensubtitles",
subtitle_id="s1",
score=300,
)
def test_save_subtitle_enqueues_instead_of_remuxing(app_ctx, tmp_path, monkeypatch):
from db.models.core import SubtitleAutomationQueueEntry
from db.repositories.subtitle_automation_queue import SubtitleAutomationQueueRepository
from providers.download_manager import save_subtitle
monkeypatch.setenv("SUBLARR_MEDIA_PATH", str(tmp_path))
from config import reload_settings
reload_settings()
video = tmp_path / "Show - S01E01.mkv"
video.write_bytes(b"v")
with (
patch("services.foreign_track_cleanup.foreign_track_cleanup_decision", return_value=True),
patch("services.foreign_track_cleanup.maybe_run_foreign_track_cleanup") as inline,
):
save_subtitle(_result(), str(tmp_path / "Show - S01E01.de.srt"), series_id=42)
assert not inline.called, "a search must not wait for the remux"
rows = [
r
for r in SubtitleAutomationQueueRepository().list_for_item(42)
if r["task_type"] == SubtitleAutomationQueueEntry.TASK_FOREIGN_TRACK_CLEANUP
]
assert len(rows) == 1
assert rows[0]["file_path"] == str(video)
assert rows[0]["wanted_item_id"] == 42
assert rows[0]["target_language"] == "de"
def test_save_subtitle_movie_enqueues_radarr_movie_id(app_ctx, tmp_path, monkeypatch):
"""Fix round 1: a movie download must not lose its Radarr movie id.
Before the ``radarr_movie_id`` column existed, the queue row for a movie
download had nowhere to carry it, so the drain could never resolve a
per-movie track-policy override.
"""
from db.models.core import SubtitleAutomationQueueEntry
from db.repositories.subtitle_automation_queue import SubtitleAutomationQueueRepository
from providers.download_manager import save_subtitle
monkeypatch.setenv("SUBLARR_MEDIA_PATH", str(tmp_path))
from config import reload_settings
reload_settings()
video = tmp_path / "Movie (2020).mkv"
video.write_bytes(b"v")
with (
patch("services.foreign_track_cleanup.foreign_track_cleanup_decision") as applies,
patch("services.foreign_track_cleanup.maybe_run_foreign_track_cleanup") as inline,
):
applies.return_value = True
save_subtitle(_result(), str(tmp_path / "Movie (2020).de.srt"), movie_id=77)
applies.assert_called_once_with({"sonarr_series_id": None, "radarr_movie_id": 77})
assert not inline.called, "a search must not wait for the remux"
# Movies enqueue under wanted_item_id=0 (no Sonarr series id) — the movie
# id lives in the new radarr_movie_id column instead.
rows = [
r
for r in SubtitleAutomationQueueRepository().list_for_item(0)
if r["task_type"] == SubtitleAutomationQueueEntry.TASK_FOREIGN_TRACK_CLEANUP
]
assert len(rows) == 1
assert rows[0]["radarr_movie_id"] == 77
def test_the_drain_runs_the_cleanup_with_the_profile_keep_set(app_ctx, tmp_path):
from db.models.core import SubtitleAutomationQueueEntry
from db.repositories.subtitle_automation_queue import SubtitleAutomationQueueRepository
from services.subtitle_automation_runner import SubtitleAutomationRunner
video = tmp_path / "Ep.mkv"
video.write_bytes(b"v")
repo = SubtitleAutomationQueueRepository()
repo.enqueue(
wanted_item_id=42,
file_path=str(video),
target_language="de",
task_type=SubtitleAutomationQueueEntry.TASK_FOREIGN_TRACK_CLEANUP,
)
with (
patch("services.embedded_extractor.resolve_profile_for_item", return_value={}),
patch("services.embedded_extractor.compute_keep_langs", return_value={"ja"}),
patch("services.foreign_track_cleanup.maybe_run_foreign_track_cleanup") as run,
):
assert SubtitleAutomationRunner().process_one() is True
item, video_arg = run.call_args.args
assert item == {"sonarr_series_id": 42, "radarr_movie_id": None, "target_language": "de"}
assert video_arg == str(video)
assert run.call_args.kwargs["target_languages"] == {"ja", "de"}
def test_a_movie_row_carries_no_series(app_ctx, tmp_path):
from db.models.core import SubtitleAutomationQueueEntry
from db.repositories.subtitle_automation_queue import SubtitleAutomationQueueRepository
from services.subtitle_automation_runner import SubtitleAutomationRunner
video = tmp_path / "Movie.mkv"
video.write_bytes(b"v")
SubtitleAutomationQueueRepository().enqueue(
wanted_item_id=0,
file_path=str(video),
target_language="en",
task_type=SubtitleAutomationQueueEntry.TASK_FOREIGN_TRACK_CLEANUP,
)
with (
patch("services.embedded_extractor.resolve_profile_for_item", return_value={}),
patch("services.embedded_extractor.compute_keep_langs", return_value=set()),
patch("services.foreign_track_cleanup.maybe_run_foreign_track_cleanup") as run,
):
SubtitleAutomationRunner().process_one()
assert run.call_args.args[0]["sonarr_series_id"] is None
def _queue_cleanup(video, series_id=42):
from db.models.core import SubtitleAutomationQueueEntry
from db.repositories.subtitle_automation_queue import SubtitleAutomationQueueRepository
repo = SubtitleAutomationQueueRepository()
repo.enqueue(
wanted_item_id=series_id,
file_path=str(video),
target_language="de",
task_type=SubtitleAutomationQueueEntry.TASK_FOREIGN_TRACK_CLEANUP,
)
return repo
def _drain_once(outcome):
from services.subtitle_automation_runner import SubtitleAutomationRunner
with (
patch("services.embedded_extractor.resolve_profile_for_item", return_value={}),
patch("services.embedded_extractor.compute_keep_langs", return_value=set()),
patch(
"services.foreign_track_cleanup.maybe_run_foreign_track_cleanup", return_value=outcome
),
patch("services.media_server_notify.notify_media_servers") as notify,
):
SubtitleAutomationRunner().process_one()
return notify
def test_a_stripped_video_is_announced_to_the_media_server(app_ctx, tmp_path):
video = tmp_path / "Ep.mkv"
video.write_bytes(b"v")
_queue_cleanup(video)
notify = _drain_once("stripped")
notify.assert_called_once_with(str(video), "episode")
def test_a_failed_remux_goes_to_the_backoff_ladder(app_ctx, tmp_path):
"""A busy media gate or a remux error used to be swallowed and the row
marked done — the cleanup was simply lost."""
video = tmp_path / "Ep.mkv"
video.write_bytes(b"v")
repo = _queue_cleanup(video)
notify = _drain_once("failed")
row = repo.list_for_item(42)[0]
assert row["state"] == "failed" and row["next_retry_at"] is not None
assert not notify.called
def test_a_terminal_row_does_not_block_the_next_download(app_ctx, tmp_path):
from db.models.core import SubtitleAutomationQueueEntry
from extensions import db
video = tmp_path / "Ep.mkv"
video.write_bytes(b"v")
repo = _queue_cleanup(video)
row = db.session.query(SubtitleAutomationQueueEntry).one()
row.state, row.next_retry_at, row.last_error = "failed", None, "video gone"
db.session.commit()
_queue_cleanup(video)
assert repo.list_for_item(42)[0]["state"] == "pending"
def test_nothing_is_queued_when_cleanup_is_off(app_ctx, tmp_path, monkeypatch):
from config import get_settings, reload_settings
from providers.download_manager import save_subtitle
monkeypatch.setenv("SUBLARR_MEDIA_PATH", str(tmp_path))
reload_settings()
monkeypatch.setattr(get_settings(), "cleanup_foreign_tracks_default", False, raising=False)
(tmp_path / "Show - S01E01.mkv").write_bytes(b"v")
save_subtitle(_result(), str(tmp_path / "Show - S01E01.de.srt"), series_id=42)
from db.repositories.subtitle_automation_queue import SubtitleAutomationQueueRepository
assert SubtitleAutomationQueueRepository().list_for_item(42) == []
def test_a_failed_enqueue_leaves_the_session_usable(app_ctx, tmp_path, monkeypatch):
from config import reload_settings
from providers.download_manager import save_subtitle
monkeypatch.setenv("SUBLARR_MEDIA_PATH", str(tmp_path))
reload_settings()
(tmp_path / "Show - S01E01.mkv").write_bytes(b"v")
with (
patch("services.foreign_track_cleanup.foreign_track_cleanup_decision", return_value=True),
patch(
"db.repositories.subtitle_automation_queue.SubtitleAutomationQueueRepository.enqueue",
side_effect=RuntimeError("unique constraint"),
),
patch("extensions.db.session.rollback") as rollback,
):
save_subtitle(_result(), str(tmp_path / "Show - S01E01.de.srt"), series_id=42)
assert rollback.called
def test_a_terminal_extraction_row_stays_closed_on_re_enqueue(app_ctx):
"""The scanner re-enqueues extraction every scan; reopening would undo the cap."""
from db.models.core import SubtitleAutomationQueueEntry
from db.repositories.subtitle_automation_queue import SubtitleAutomationQueueRepository
from extensions import db
repo = SubtitleAutomationQueueRepository()
repo.enqueue(wanted_item_id=5, file_path="/m/x.mkv", target_language="de")
row = db.session.query(SubtitleAutomationQueueEntry).one()
row.state, row.next_retry_at = "failed", None
db.session.commit()
repo.enqueue(wanted_item_id=5, file_path="/m/x.mkv", target_language="de")
assert repo.list_for_item(5)[0]["state"] == "failed"