soulsync/tests/test_video_scan_scope.py
BoulderBadgeDad 7cd289e7b5 video scan: harden library scope + pause ALL enrichers during any scan
1) Scan only the MAPPED libraries — never fall back to 'all'. The scan path used
   _sections/_views with the selected name, but an empty name returned ALL sections
   of that type — so a missing/unreadable selection silently scanned every library
   (how the 4K movie + 'YouTube' TV libraries leaked in as movies/shows). New
   _scan_sections / _scan_views return [] when a kind isn't mapped; available_libraries
   still lists all (for the Settings dropdown). Now an unmapped kind scans NOTHING.

2) A library scan (full/incremental/deep) now pauses EVERY enricher, including the
   YouTube date enricher — a separate singleton outside engine.workers that kept
   running through scans. pause_for_scan/resume_after_scan pause+resume it too, only
   if it wasn't already manually paused (never override the user).

kettui: source-scope tests (mapped-only / unmapped-scans-nothing / listing still
shows all) + engine pause tests (pauses+resumes YT / preserves manual pause).
123 scanner/source/enrichment tests + isolation guard green.
2026-06-20 13:24:57 -07:00

103 lines
3.8 KiB
Python

"""Video scan SCOPE + worker-pause coupling.
1. The scan must read ONLY the user-mapped Movies/TV libraries — and when a kind
isn't mapped, scan NOTHING (never silently fall back to all libraries, which is
how YouTube/4K libraries leaked in).
2. A library scan (any mode) pauses EVERY enricher — including the YouTube date
enricher, which is a separate singleton outside engine.workers.
"""
from __future__ import annotations
import pytest
from core.video.sources import PlexVideoSource
from database.video_database import VideoDatabase
from core.video.enrichment.engine import VideoEnrichmentEngine
import core.video.youtube_enrichment as yt_mod
# ── 1. scan scope ──────────────────────────────────────────────────────────
class _Sec:
def __init__(self, type_, title):
self.type = type_
self.title = title
class _Lib:
def __init__(self, secs):
self._secs = secs
def sections(self):
return self._secs
class _Server:
def __init__(self, secs):
self.library = _Lib(secs)
_SECTIONS = [_Sec("movie", "Movies"), _Sec("movie", "4K Movies"),
_Sec("show", "TV Shows"), _Sec("show", "YouTube")]
def test_scan_uses_only_the_mapped_libraries():
src = PlexVideoSource(_Server(_SECTIONS), movies_lib="Movies", tv_lib="TV Shows")
assert [s.title for s in src._scan_sections("movie", src._movies_lib)] == ["Movies"]
assert [s.title for s in src._scan_sections("show", src._tv_lib)] == ["TV Shows"]
# the 4K + YouTube libraries are NOT scanned…
titles = [s.title for s in src._scan_sections("movie", src._movies_lib)] + \
[s.title for s in src._scan_sections("show", src._tv_lib)]
assert "4K Movies" not in titles and "YouTube" not in titles
# …but available_libraries still LISTS them all (for the Settings dropdown).
avail = src.available_libraries()
assert {x["title"] for x in avail["movies"]} == {"Movies", "4K Movies"}
assert {x["title"] for x in avail["tv"]} == {"TV Shows", "YouTube"}
def test_unmapped_kind_scans_nothing_not_everything():
# The hardening: a missing selection must scan NOTHING, never fall back to all.
src = PlexVideoSource(_Server(_SECTIONS), movies_lib=None, tv_lib=None)
assert src._scan_sections("movie", src._movies_lib) == []
assert src._scan_sections("show", src._tv_lib) == []
# _sections (used for LISTING) still returns all — only the SCAN path is gated.
assert len(src._sections("movie")) == 2
# ── 2. scan pauses every enricher (incl. the YouTube date singleton) ───────
class _FakeYT:
def __init__(self, paused=False):
self._paused = paused
def pause(self):
self._paused = True
def resume(self):
self._paused = False
@pytest.fixture()
def engine(tmp_path):
db = VideoDatabase(database_path=str(tmp_path / "video_library.db"))
return VideoEnrichmentEngine(db, clients={})
def test_scan_pauses_and_resumes_youtube_date_enricher(engine, monkeypatch):
fake = _FakeYT()
monkeypatch.setattr(yt_mod, "get_youtube_date_enricher", lambda: fake)
engine.pause_for_scan()
assert fake._paused is True
assert "youtube" in engine._scan_paused
engine.resume_after_scan()
assert fake._paused is False
def test_scan_never_resumes_a_manually_paused_youtube_enricher(engine, monkeypatch):
fake = _FakeYT(paused=True) # the user paused it themselves
monkeypatch.setattr(yt_mod, "get_youtube_date_enricher", lambda: fake)
engine.pause_for_scan()
assert "youtube" not in engine._scan_paused # we didn't touch it
engine.resume_after_scan()
assert fake._paused is True # still paused after the scan