fix(plex): re-probe on file replace, HU parity, public class const, playback-class test gaps

Deep-review round 1 on feature/plex-playback-class-filter:

- Sticky watermark (medium): _apply_item reset the bit-depth probe watermark
  only on a codec-string change, so a same-codec re-encode (8-bit->10-bit HEVC)
  kept a stale video_bit_depth/transcode_hw forever (the backfill only re-probes
  media_probed_at IS NULL rows). Also reset when the Plex Part key changes (the
  file was replaced), not just the codec.

- i18n parity (medium): the HU playback-class labels were mixed-language (only
  'direct' translated). Translated remux/transcode_hw/transcode_sw and the
  tooltips consistently to Hungarian (Kozvetlen streameles / Atkodolas).

- Layering (low): _PLAYBACK_CLASSES was module-private yet imported across into
  routes.plex.browse; renamed to public PLAYBACK_CLASSES (it is the shared
  source of truth for facet order + zero-fill).

- a11y (low): the browse-card playback-class dot conveyed meaning only via a
  mouse-hover title; added role=img + aria-label so SR/touch users get the class.

- tests: the NULL/unprobed transcode_hw -> SW fold (filter + facet), facet
  self-exclusion under an active playable filter, the migration codec-set ==
  HW_DECODABLE_CODECS drift guard, the probe-batch bound + oldest-first order,
  the transient probe-failure retry path, and the same-codec re-encode re-probe.

Backend gate green (ruff + full pytest); FE gate green (tsc/lint/knip/prettier/
vitest). NOTE: the facet-vs-runtime disagreement for 10-bit HEVC (C1) is the
known merge-order constraint — this branch's runtime is codec-only; the facet
becomes truthful once BUG1/tonemap's bit-depth-aware routing lands with it. Left
as a sequencing constraint, not code-changed here.
This commit is contained in:
2026-08-30 17:51:33 +02:00
parent 840526b1b8
commit bf98b48517
6 changed files with 205 additions and 18 deletions
+3 -2
View File
@@ -212,7 +212,8 @@ def apply_meta_filters(q, model, p: dict, lang: str = "en"):
# The 4 classes the feed filter + facet expose, derived from the stored `playable` + `transcode_hw`.
# ONE definition (this SQL CASE) so the filter WHERE and the facet counts can't drift: `playable_conds`
# filters on it, `facets` groups by it. transcode_hw IS NOT True groups NULL (unprobed) with software.
_PLAYBACK_CLASSES = ("direct", "remux", "transcode_hw", "transcode_sw")
# Public — it is the shared source of truth imported by routes.plex.browse (facet order + zero-fill).
PLAYBACK_CLASSES = ("direct", "remux", "transcode_hw", "transcode_sw")
def playback_class_expr():
@@ -230,7 +231,7 @@ def playback_class_expr():
def playable_conds(sel: list[str]) -> list:
"""A one-element condition list (or empty) selecting the given playback classes — an OR via
`class IN (...)` over the shared CASE. Unknown keys are dropped."""
keep = [c for c in sel if c in _PLAYBACK_CLASSES]
keep = [c for c in sel if c in PLAYBACK_CLASSES]
return [playback_class_expr().in_(keep)] if keep else []
+11 -4
View File
@@ -471,13 +471,20 @@ def _apply_item(row: PlexItem, lib_id: int, kind: str, meta: dict) -> None:
_apply_facet_fields(row, meta) # filterable/orderable extras (shared with shows)
mf = _media_facts(meta)
new_vcodec = mf.get("codec_video")
# A changed video codec means the physical file was replaced — any probed bit-depth is stale, so
# reset the probe watermark and re-derive transcode_hw from codec alone until the backfill re-probes.
if (row.codec_video or "").lower() != (new_vcodec or "").lower():
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:
row.video_bit_depth = None
row.media_probed_at = None
row.file_path = mf.get("file_path")
row.part_key = mf.get("part_key")
row.part_key = new_part_key
row.container = mf.get("container")
row.codec_video = new_vcodec
row.codec_audio = mf.get("codec_audio")