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
214 lines
9.3 KiB
Python
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
|