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
This commit is contained in:
parent
d12cbc62ee
commit
e819548ff2
9 changed files with 234 additions and 15 deletions
|
|
@ -138,3 +138,53 @@ def test_episodes_without_a_parent_rating_key_still_build_a_season(scanned):
|
|||
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]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue