MediaShelf/tests/test_ingest.py
Jess Hallsworth edbb6f088f
Use Plex's lastViewedAt for the date correction, not just Tautulli
The correction shipped in the previous commit fired zero times on the live
library. It compared Plex's addedAt against MediaShelf's first completed
Tautulli play, but Tautulli history only starts 2025-03-08 and the wrong dates
are overwhelmingly older than that. The evidence that proved the bug in the
first place was Plex's own lastViewedAt - which the provider parsed and then
dropped on the floor.

Now stored as provider_last_viewed_at and folded into the bound:
added_at = MIN(provider_added_at, first_watched_at, provider_last_viewed_at).
Seasons also get their own provider_added_at (MIN over episodes), without which
their added_at_source could never be computed.

Fixture gains the case that actually failed: added last week, last viewed 1500
days ago, no Tautulli history at all - so only lastViewedAt carries it. Also
tightened test_correction_leaves_unwatched_items_alone, which defined
"unwatched" as watch_count=0 and so wrongly expected an item with Plex view
evidence to be left alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GVbG48GAXfCZatcmX123Ra
2026-09-10 17:49:20 +00:00

214 lines
9.3 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()
def test_episodes_without_a_parent_rating_key_still_build_a_season(scanned):
"""Plex omits parentRatingKey on some episodes (Firefly, live). Dropping
them silently lost a whole 15-episode season from the report."""
season = scanned.one(
"SELECT i.*, p.title AS show FROM media_item i "
"JOIN media_item p ON p.id = i.parent_id "
"WHERE i.kind='season' AND p.title='Orphan Show'")
assert season is not None, "season was dropped for want of a parentRatingKey"
assert season["episode_count"] == 5
assert season["size_bytes"] == 5 * 2 * 10**9
assert season["season_number"] == 1
assert ":s1" in season["provider_item_id"], "expected a synthesized season key"
def test_no_orphan_episode_warnings(scanned, rescan):
r = rescan("full")
assert not [w for w in (r.warnings or []) if "no season" in w]
def test_added_at_is_corrected_when_plex_claims_it_postdates_a_play(scanned):
"""Plex's addedAt follows the file, so replacing one resets Date Added while
the watch history survives. A play proves the item already existed."""
row = scanned.one("SELECT * FROM media_item WHERE title = 'Replaced File'")
assert row is not None
assert row["provider_added_at"] > row["first_watched_at"], \
"fixture should present an addedAt later than the first play"
assert row["added_at"] == row["first_watched_at"], \
"added_at was not pulled back to the first play"
assert row["added_at_source"] == "first_watch"
def test_correction_leaves_unwatched_items_alone(scanned):
"""Nothing contradicts Plex for an item with no evidence of ever being
viewed — by Tautulli OR by Plex's own lastViewedAt."""
rows = scanned.query(
"SELECT * FROM media_item WHERE kind='movie' AND watch_count = 0 "
"AND COALESCE(first_watched_at,0) = 0 "
"AND COALESCE(provider_last_viewed_at,0) = 0 "
"AND provider_added_at IS NOT NULL LIMIT 20")
assert rows
for r in rows:
assert r["added_at"] == r["provider_added_at"]
assert r["added_at_source"] == "provider"
def test_corrected_date_moves_the_item_into_the_uncertain_pool(scanned):
"""The reason this matters: pre_history is derived from added_at, so a
wrongly-recent date promotes an item into the CONFIDENT reclaim pool."""
row = scanned.one("SELECT * FROM media_item WHERE title = 'Replaced File'")
cov = scanned.one("SELECT earliest_event_at FROM history_coverage LIMIT 1")
if row["added_at"] < cov["earliest_event_at"]:
assert row["pre_history"] == 1
def test_first_seen_at_is_recorded(scanned):
n = scanned.scalar("SELECT COUNT(*) FROM media_item WHERE first_seen_at IS NULL "
"AND kind='movie'")
assert n == 0, "first_seen_at should be stamped on every row MediaShelf creates"
def test_added_at_correction_is_idempotent(scanned, rescan):
before = scanned.query(
"SELECT id, added_at, provider_added_at, added_at_source FROM media_item "
"ORDER BY id")
rescan("full")
after = scanned.query(
"SELECT id, added_at, provider_added_at, added_at_source FROM media_item "
"ORDER BY id")
assert [dict(r) for r in before] == [dict(r) for r in after]
def test_correction_works_from_plex_last_viewed_alone(scanned):
"""The live failure: the wrong dates are mostly older than Tautulli's history
window, so only Plex's lastViewedAt carries the evidence. The first version
of this correction used Tautulli alone and fired zero times on 65 TB."""
row = scanned.one("SELECT * FROM media_item WHERE title = 'Plex Evidence Only'")
assert row is not None
assert not row["first_watched_at"], "fixture must have no Tautulli history"
assert row["provider_last_viewed_at"] < row["provider_added_at"]
assert row["added_at"] == row["provider_last_viewed_at"], \
"correction ignored Plex's lastViewedAt"
assert row["added_at_source"] == "first_watch"
def test_seasons_get_a_provider_added_at(scanned):
"""Without one, a season's added_at_source can never be computed."""
n = scanned.scalar(
"SELECT COUNT(*) FROM media_item WHERE kind='season' AND episode_count > 0 "
"AND provider_added_at IS NULL")
assert n == 0