Ctrl K
ComunidadGuías…

backend/tests/test_foreign_track_cleanup_deferred.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""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"