fix(plex): the rails' Restart comes from the one rollup; plain text decodes whole entity names only

Both Continue-watching rails built a show's card state inline; they now take it from rollup.show_state, so a show part watched with nothing to resume offers Play alone there too. A named reference counts only when its whole name is an HTML5 entity: html.unescape still decoded the legacy "&not" inside "&notice;". Tests cover the grid's in-progress-but-not-resumable case through its SQL and both rails.
This commit is contained in:
2026-10-09 23:46:39 +02:00
parent 6df257d7df
commit fc53bc03bc
7 changed files with 51 additions and 14 deletions
+24
View File
@@ -165,6 +165,30 @@ def test_continue_watching_movies_and_shows(db, monkeypatch, override_dep):
assert "m1" not in ids
def test_a_continue_watching_show_says_whether_it_has_an_episode_to_resume(
db, monkeypatch, override_dep
):
# Both in-progress rails (Discover's and Home's) take a show's card state from the one rollup:
# a show part watched with nothing to resume offers Play alone, not Continue + Restart.
from app.discover.queries import plex_in_progress
user = _seed(db)
def rails():
d = _get(db, user, monkeypatch, override_dep, "/api/plex/discover?scope=both").json()
home = {c["id"]: c for _, c in plex_in_progress(db, user.id, lang="en")}
return {c["id"]: c for c in d["continue_watching"]}["sh1"], home["sh1"]
assert [c["resumable"] for c in rails()] == [True, True]
ep1 = db.execute(select(PlexItem).where(PlexItem.rating_key == "sh1e1")).scalar_one()
st = db.execute(
select(PlexState).where(PlexState.user_id == user.id, PlexState.item_id == ep1.id)
).scalar_one()
st.status, st.position_seconds = "watched", 0
db.commit()
assert [(c["status"], c["resumable"]) for c in rails()] == [("in_progress", False)] * 2
def test_continue_watching_null_status_in_hidden_out(db, monkeypatch, override_dep):
# A started movie whose PlexState.status is NULL is still "in progress": `NULL NOT IN (...)` is NULL
# (not TRUE), so a bare notin_ would wrongly drop it — the or_(is_(None), notin_) branch keeps it.
+2
View File
@@ -33,6 +33,8 @@ HU_SUMMARY = "<p>A műsorban minden p&aacute;rosnak stoppal kell eljutnia.</p>"
("a < b and 1 <3 you", "a < b and 1 <3 you"),
# Only a reference with its semicolon is one: an ampersand in prose stays as written.
("AT&notice, R&copy and H&M", "AT&notice, R&copy and H&M"),
# ...and only a whole entity name: a legacy one at its start ("&not", "&amp") is not.
("AT&notice; and &ampx;", "AT&notice; and &ampx;"),
("&#233;t&#xE9; &copy;", "été ©"),
("Plain summary.", "Plain summary."),
(" padded ", "padded"),
+5 -2
View File
@@ -220,15 +220,18 @@ class TestRollup:
for i in _get(db, user, monkeypatch, override_dep, "/api/plex/library").json()["items"]
}
assert cards["s1"]["resumable"] is True
# e1 finished, e2 untouched: in progress, but nothing to resume
e1 = db.execute(select(PlexItem).where(PlexItem.rating_key == "e1")).scalar_one()
db.execute(
delete(PlexState).where(PlexState.user_id == user.id, PlexState.position_seconds > 0)
delete(PlexState).where(PlexState.user_id == user.id, PlexState.item_id == e1.id)
)
db.add(PlexState(user_id=user.id, item_id=e1.id, status="watched"))
db.commit()
cards = {
i["id"]: i
for i in _get(db, user, monkeypatch, override_dep, "/api/plex/library").json()["items"]
}
assert cards["s1"]["resumable"] is False
assert (cards["s1"]["status"], cards["s1"]["resumable"]) == ("in_progress", False)
def _fake_paginate(by_type):