fix(plex): the watch reconcile clears only what it walked, and doubts a mirrored 0

A library left out of the allowlist is never scanned, so its watch states were cleared on
every reconcile; clearing is now limited to the sections walked. A section we mirror items
from that says it holds 0 is not believed complete. The page size is no longer read as the
section total when totalSize is missing (measured: the server always sends totalSize on a
paged request, 0 for an empty one).
This commit is contained in:
2026-10-04 04:16:52 +02:00
parent 79704b0001
commit 12a39f5291
4 changed files with 85 additions and 14 deletions
+6 -1
View File
@@ -79,11 +79,16 @@ def test_a_section_page_asks_for_guids_and_reports_the_server_total(plex):
assert params["includeGuids"] == "1" and params["type"] == "1"
assert params["X-Plex-Container-Start"] == "200" and params["X-Plex-Container-Size"] == "50"
plex.routes["/library/sections/7/all"] = {"Metadata": [], "size": 0}
# Measured on the real server (E32 review): a paged request carries `totalSize` even when the
# page is empty, past the end, or the section holds nothing — `size: 0, totalSize: 0`.
plex.routes["/library/sections/7/all"] = {"Metadata": [], "size": 0, "totalSize": 0}
assert plex.section_items("7", 2) == ([], 0)
# No total at all is not "0": the caller must not read it as a complete, empty section.
plex.routes["/library/sections/7/all"] = {"Metadata": []}
assert plex.section_items("7", 2) == ([], None)
# …and `size` is THIS page's count, never the section's: one full page is not the whole section.
plex.routes["/library/sections/7/all"] = {"Metadata": [{"ratingKey": "1"}], "size": 1}
assert plex.section_items("7", 2) == ([{"ratingKey": "1"}], None)
def test_related_flattens_the_hubs_and_degrades_to_empty(plex):
@@ -157,6 +157,51 @@ def test_a_genuinely_empty_second_section_still_lets_the_reconcile_clear(db, mon
assert out["cleared"] == 1 and "incomplete" not in out
def test_a_section_we_mirror_that_says_0_is_not_believed(db, monkeypatch, world):
# A total of 0 from the very section our items come from is a Plex that lost the library for a
# moment, not one that un-watched all of it (E32 review round 2): the shows section below says
# 0 and lists nothing — while we hold an episode from it.
state, rows = world
_local_states(state)
shows = PlexLibrary(plex_key="2", title="Shows", kind="show", enabled=True)
db.add(shows)
db.flush()
db.add(PlexItem(rating_key="e1", library_id=shows.id, kind="episode", title="e1"))
db.commit()
out = _run(db, monkeypatch, _FakePlex(_LISTED, more=[("show", [], 0)]))
assert rows()["m1"] == ("watched", 0, True)
assert out["cleared"] == 0 and out["incomplete"] == 1
def test_a_library_the_allowlist_leaves_out_keeps_its_watch_states(db, monkeypatch, world):
# Its items are absent from every scan because the scan never walks it — not because Plex
# un-watched them. It cleared them on every run (E32 review round 2).
state, _rows = world
other = PlexLibrary(plex_key="2", title="Shows", kind="show", enabled=True)
db.add(other)
db.flush()
ep = PlexItem(rating_key="e1", library_id=other.id, kind="episode", title="e1")
db.add(ep)
db.flush()
user_id = db.execute(select(PlexLink.user_id)).scalar_one()
db.add(PlexState(user_id=user_id, item_id=ep.id, status="watched", synced_to_plex=True))
_local_states(state)
sysconfig.set_value(db, "plex_libraries", "1")
db.commit()
out = _run(db, monkeypatch, _FakePlex(_LISTED, more=[("show", [], 0)]))
db.expire_all()
left = {
rk
for rk in db.execute(
select(PlexItem.rating_key).join(PlexState, PlexState.item_id == PlexItem.id)
).scalars()
}
assert "m1" not in left # the walked section still clears
kept = db.execute(select(PlexState).where(PlexState.item_id == ep.id)).scalar_one_or_none()
assert kept is not None and kept.status == "watched"
assert out["cleared"] == 1
def test_a_cut_short_section_is_not_rescued_by_a_complete_one_after_it(db, monkeypatch, world):
# Completeness is every section's, not the last one's: movies cut short (5 said, 3 handed),
# then a complete shows listing — m1 may simply not have been reached.