The application the design describes: Flask + SQLite, Plex for library data, Tautulli for watch history, report-only. Structure follows the design's seams. providers/ splits MediaProvider from HistoryProvider, because on this network library data and watch data live on different machines and Jellyfin later will have no Tautulli equivalent. scoring.py implements the reclaim score twice - as a SQL expression for the live grid (weights change on every slider drag, so storing it would mean rewriting thousands of rows per drag) and in Python for CSV export and tests, with a property test over 500 generated rows asserting the two agree. rules.py compiles saved views to parameterized SQL through a field/operator whitelist; nothing user-supplied is ever interpolated. Three properties are enforced by test rather than asserted in prose: - Ingest is idempotent. Three consecutive full scans leave every count and every byte total unchanged. A scanner that double-counts produces a report that looks plausible and is wrong. - Keep marks survive Plex reassigning every rating key in the library. They are keyed on content GUID, scoped per library so the Movies and 4K Movies copies of the same film mark independently. - Every config variable the app reads is declared in docker-compose.yml, so a variable set in Portainer can never silently do nothing. Also found and fixed while verifying against a fake Plex+Tautulli pair: executescript() commits the pending transaction, so migrations needed their BEGIN/COMMIT inside the script; replaceChildren() renders null as the literal text "null"; a hash-only URL change does not reload the document, so deep links needed a hashchange listener; and SQLite ROUND rounds half away from zero where Python rounds half to even. 73 tests, no live server required. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GVbG48GAXfCZatcmX123Ra
121 lines
5.2 KiB
Python
121 lines
5.2 KiB
Python
"""Ingest correctness. The headline property is idempotency: a scanner that
|
|
double-counts produces a report that looks plausible and is wrong, which is worse
|
|
than one that crashes."""
|
|
|
|
import mockserver
|
|
|
|
|
|
def snapshot(db):
|
|
return {
|
|
"items": db.scalar("SELECT COUNT(*) FROM media_item"),
|
|
"unit_bytes": db.scalar(
|
|
"SELECT COALESCE(SUM(size_bytes),0) FROM media_item "
|
|
"WHERE kind IN ('movie','season')"),
|
|
"parts": db.scalar("SELECT COUNT(*) FROM media_part"),
|
|
"part_bytes": db.scalar("SELECT COALESCE(SUM(size_bytes),0) FROM media_part"),
|
|
"episodes": db.scalar("SELECT COUNT(*) FROM episode"),
|
|
"events": db.scalar("SELECT COUNT(*) FROM watch_event"),
|
|
"watch_sum": db.scalar(
|
|
"SELECT COALESCE(SUM(watch_count),0) FROM media_item WHERE kind='movie'"),
|
|
"missing": db.scalar("SELECT COUNT(*) FROM media_item WHERE status='missing'"),
|
|
}
|
|
|
|
|
|
def test_scan_succeeds_and_totals_match_source(scanned):
|
|
expected = mockserver.stats()
|
|
assert scanned.scalar(
|
|
"SELECT COALESCE(SUM(size_bytes),0) FROM media_item WHERE kind IN ('movie','season')"
|
|
) == expected["total_bytes"]
|
|
assert scanned.scalar("SELECT COUNT(*) FROM episode") == expected["episodes"]
|
|
assert scanned.scalar("SELECT COUNT(*) FROM watch_event") == expected["history"]
|
|
|
|
|
|
def test_scan_is_idempotent(scanned, rescan):
|
|
first = snapshot(scanned)
|
|
rescan("full")
|
|
second = snapshot(scanned)
|
|
rescan("full")
|
|
third = snapshot(scanned)
|
|
assert first == second == third, "a repeated scan changed the data"
|
|
|
|
|
|
def test_incremental_adds_no_duplicate_events(scanned, rescan):
|
|
before = scanned.scalar("SELECT COUNT(*) FROM watch_event")
|
|
r = rescan("incremental")
|
|
assert r.status == "succeeded"
|
|
assert scanned.scalar("SELECT COUNT(*) FROM watch_event") == before
|
|
|
|
|
|
def test_seasons_are_the_unit_for_tv(scanned):
|
|
seasons = scanned.query(
|
|
"SELECT * FROM media_item WHERE kind='season' AND episode_count > 0")
|
|
assert seasons, "no seasons were built"
|
|
for s in seasons:
|
|
rolled = scanned.scalar(
|
|
"SELECT COALESCE(SUM(size_bytes),0) FROM episode "
|
|
"WHERE season_item_id=? AND status='present'", (s["id"],))
|
|
assert s["size_bytes"] == rolled
|
|
assert s["parent_id"] is not None, "season is not linked to its show"
|
|
|
|
|
|
def test_multi_part_items_sum_all_parts(scanned):
|
|
"""A movie held as two files must report the total, not the first part."""
|
|
rows = scanned.query(
|
|
"SELECT id, size_bytes, part_count FROM media_item "
|
|
"WHERE kind='movie' AND part_count > 1")
|
|
assert rows, "fixture has no multi-part movies"
|
|
for r in rows:
|
|
total = scanned.scalar(
|
|
"SELECT SUM(size_bytes) FROM media_part WHERE media_item_id=?", (r["id"],))
|
|
assert r["size_bytes"] == total
|
|
|
|
|
|
def test_dispositions_are_classified(scanned, cfg):
|
|
got = {r[0]: r[1] for r in scanned.query(
|
|
"SELECT disposition, COUNT(*) FROM watch_event GROUP BY 1")}
|
|
assert set(got) <= {"completed", "partial", "abandoned"}
|
|
assert got.get("abandoned", 0) > 0, "fixture should produce abandoned plays"
|
|
bad = scanned.scalar(
|
|
"SELECT COUNT(*) FROM watch_event WHERE disposition='completed' "
|
|
"AND percent_complete IS NOT NULL AND percent_complete < ?",
|
|
(cfg.completion_threshold,))
|
|
assert bad == 0
|
|
|
|
|
|
def test_pre_history_flag_tracks_coverage(scanned):
|
|
cov = scanned.one("SELECT * FROM history_coverage ORDER BY event_count DESC LIMIT 1")
|
|
assert cov and cov["earliest_event_at"]
|
|
wrong = scanned.scalar(
|
|
"SELECT COUNT(*) FROM media_item WHERE pre_history=1 AND added_at >= ?",
|
|
(cov["earliest_event_at"],))
|
|
assert wrong == 0
|
|
assert scanned.scalar("SELECT COUNT(*) FROM media_item WHERE pre_history=1") > 0
|
|
|
|
|
|
def test_missing_items_are_flagged_not_deleted(scanned, rescan, monkeypatch):
|
|
victim = scanned.one("SELECT * FROM media_item WHERE kind='movie' LIMIT 1")
|
|
removed = [m for m in mockserver.MOVIES
|
|
if m["ratingKey"] == victim["provider_item_id"]]
|
|
assert removed
|
|
monkeypatch.setattr(mockserver, "MOVIES",
|
|
[m for m in mockserver.MOVIES
|
|
if m["ratingKey"] != victim["provider_item_id"]])
|
|
rescan("full")
|
|
row = scanned.one("SELECT * FROM media_item WHERE id=?", (victim["id"],))
|
|
assert row is not None, "a vanished item was deleted rather than flagged"
|
|
assert row["status"] == "missing"
|
|
|
|
|
|
def test_refuses_history_from_a_different_plex_server(db, cfg, monkeypatch):
|
|
"""Joining another server's history would produce confident nonsense (§4.11)."""
|
|
from mediashelf import ingest, providers
|
|
from mediashelf.providers.base import ProviderError, ServerInfo
|
|
|
|
media = providers.build_media_provider(cfg)
|
|
history, _ = providers.build_history_provider(cfg, media)
|
|
monkeypatch.setattr(history, "server_info",
|
|
lambda: ServerInfo(kind="tautulli", name="Other",
|
|
server_id="TOTALLY-DIFFERENT"))
|
|
result = ingest.Ingest(db, cfg, media, history).run("full", "manual")
|
|
assert result.status == "failed"
|
|
assert "different plex server" in (result.error or "").lower()
|