fix(plex): gate the part-key re-probe to single-version items; drop dead i18n key; cover cross-facet narrowing

Deep-review round 2 on the round-1 fixes (all low):

- part_key churn (state): round 1's part-key reset re-probed EVERY sync for a
  multi-version item (an original + a Plex 'Optimized' copy), because
  _media_facts reads media[0]/part[0] whose order Plex doesn't guarantee stable,
  oscillating transcode_hw between the two versions. Gate the part-key signal on
  a single-version item (len(Media)<=1); codec-change reset still applies to all.
  A multi-version same-codec re-encode not self-healing is an acceptable rare
  edge; a per-sync re-probe loop is not.

- dead i18n key: this diff's playableClass() collapses to transcode_hw/sw, so the
  bare plex.playable.transcode key it orphaned is now unused (knip can't see
  dynamic keys). Dropped from both locales, parity kept.

- test: cross-facet narrowing by an active playable class (a co-facet's year
  range collapses to the selected subset) — the flip side of self-exclusion,
  previously unpinned.

The aria-label+title double-announcement on the card dot is a deliberate accepted
trade-off (reliable SR name + mouse tooltip). Backend + FE gates green.
This commit is contained in:
2026-08-30 17:58:38 +02:00
parent bf98b48517
commit c1a3b7b935
4 changed files with 56 additions and 11 deletions
+14 -9
View File
@@ -472,15 +472,20 @@ def _apply_item(row: PlexItem, lib_id: int, kind: str, meta: dict) -> None:
mf = _media_facts(meta)
new_vcodec = mf.get("codec_video")
new_part_key = mf.get("part_key")
# The physical file was replaced when its Plex Part key changes (a re-encode / quality upgrade gets a
# new part id) OR — a coarser signal — when the video codec changes. Either way any probed bit-depth is
# stale, so reset the probe watermark and re-derive transcode_hw from codec alone until the backfill
# re-probes. Codec alone is NOT enough: a same-codec bit-depth re-encode (8-bit→10-bit HEVC) keeps the
# codec `hevc` but flips the HW class, and only the part-key move catches it. A spurious reset (a rare
# part-id reassignment on an unchanged file) costs at most one extra bounded re-probe — safe direction.
if (row.codec_video or "").lower() != (
new_vcodec or ""
).lower() or row.part_key != new_part_key:
# The physical file was replaced when its video codec changes OR — for a SINGLE-version item — when its
# Plex Part key changes (a re-encode / quality upgrade gets a new part id). Either way any probed
# bit-depth is stale, so reset the probe watermark and re-derive transcode_hw from codec alone until the
# backfill re-probes. Codec alone is NOT enough: a same-codec bit-depth re-encode (8-bit→10-bit HEVC)
# keeps the codec `hevc` but flips the HW class, and only the part-key move catches it.
#
# The part-key check is gated on single-version because `_media_facts` reads media[0]/part[0], and for a
# MULTI-version item (an original + a Plex "Optimized" copy) that order isn't guaranteed stable across
# listings — an unguarded part-key compare would then reset+re-probe every sync and oscillate the
# transcode_hw bucket between the two versions. A multi-version same-codec re-encode not self-healing is
# an acceptable rare edge; a per-sync re-probe loop is not.
single_version = len(meta.get("Media") or []) <= 1
codec_changed = (row.codec_video or "").lower() != (new_vcodec or "").lower()
if codec_changed or (single_version and row.part_key != new_part_key):
row.video_bit_depth = None
row.media_probed_at = None
row.file_path = mf.get("file_path")
+42
View File
@@ -451,3 +451,45 @@ def test_facet_playable_self_excludes_the_active_selection(db, monkeypatch):
assert resp.status_code == 200, resp.text
counts = {p["value"]: p["count"] for p in resp.json()["playable"]}
assert counts == {"direct": 1, "remux": 1, "transcode_hw": 1, "transcode_sw": 1}
def test_active_playable_narrows_the_other_facets(db, monkeypatch):
# The flip side of self-exclusion: an active playable class DOES narrow the OTHER facet groups (so
# e.g. the year range reflects only the selected class). Seed a Direct movie (2000) and a
# software-transcode movie (2020); selecting playable=direct must collapse the year facet to 2000.
lib = PlexLibrary(plex_key="1", title="Movies", kind="movie", enabled=True)
db.add(lib)
db.flush()
db.add_all(
[
PlexItem(
rating_key="d2000",
library_id=lib.id,
kind="movie",
title="Direct2000",
playable="direct",
year=2000,
),
PlexItem(
rating_key="s2020",
library_id=lib.id,
kind="movie",
title="SW2020",
playable="transcode",
codec_video="mpeg4",
transcode_hw=False,
year=2020,
),
]
)
user = User(email="pnarrow@example.com")
db.add(user)
db.commit()
client = _client(db, user, monkeypatch)
try:
wide = client.get("/api/plex/facets?scope=movie").json()
narrow = client.get("/api/plex/facets?scope=movie&playable=direct").json()
finally:
app.dependency_overrides.clear()
assert (wide["year_min"], wide["year_max"]) == (2000, 2020) # both classes span the range
assert (narrow["year_min"], narrow["year_max"]) == (2000, 2000) # narrowed to the Direct subset
-1
View File
@@ -293,7 +293,6 @@
"playable": {
"direct": "Plays directly in the browser",
"remux": "Direct Stream — a light remux (no video re-encode)",
"transcode": "Needs transcoding (heavier)",
"transcode_hw": "Transcoded on the hardware encoder",
"transcode_sw": "Transcoded in software (heaviest)"
},
-1
View File
@@ -293,7 +293,6 @@
"playable": {
"direct": "Közvetlenül játszható a böngészőben",
"remux": "Közvetlen streamelés — könnyű remux (nincs videó-újrakódolás)",
"transcode": "Átkódolás szükséges (nehezebb)",
"transcode_hw": "Hardveres kódolóval átkódolva",
"transcode_sw": "Szoftveresen átkódolva (legnehezebb)"
},