MediaShelf/tests/test_ingest.py
Jess Hallsworth e819548ff2
Stop trusting Plex's Date Added on its own
Jess spotted that dates looked like file dates rather than library-add dates.
He is right, and MediaShelf was not the culprit: it reproduces Plex's addedAt
exactly (verified 500/500 identical to the second). Plex's own field is what
follows the file — replace or re-encode one and Date Added resets while the
item, its ratingKey and its watch history all survive.

Measured on the live library, comparing addedAt against lastViewedAt where both
exist: 55 of 509 movies (10.8%) and 306 of 1,393 TV Show Archive items (22.0%)
were watched BEFORE they were "added" — 19% overall. 2001: A Space Odyssey
reports added 2026-07-31, last watched 2017-08-26.

That is not cosmetic. pre_history is derived from added_at, so an old item whose
file was replaced looks post-coverage and gets promoted into the CONFIDENT
reclaim pool, which is the one pool meant to be trustworthy.

A completed play proves the item already existed, so added_at is now
MIN(provider_added_at, first_watched_at). Plex's raw value is kept in
provider_added_at, added_at_source records which applied, and the item drawer
explains the substitution instead of quietly disagreeing with Plex. Unwatched
items keep Plex's value since nothing contradicts it. first_seen_at is also
recorded now and is authoritative for anything added from here on.

Plex's API has no better field; the true insert time is only in Plex's own
metadata_items.created_at on Loki.

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

190 lines
8.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()
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 nobody ever played."""
rows = scanned.query(
"SELECT * FROM media_item WHERE kind='movie' AND watch_count = 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]