fix(plex): resolve deep-review findings on the Discover/Collections batch

Round of USER-triggered deep-review fixes (11 CONFIRMED + 1 PLAUSIBLE, all
verified) on the 22-commit branch:

- collections: split the mutate-then-resync error surface. A resync READ that
  fails after a successful Plex mutation no longer reports a false 422 "Plex
  write failed" + rollback; it returns 502 "sync pending" (the next full sync
  reconciles the mirror). New _write_or_pending_error helper; delete is exempt
  (no Plex read). Covered by new happy-path / 422-rollback / 502 tests.
- New-collection dup-name guard is now advisory only: existingNames is the
  search-scoped, all-library union, but the backend enforces uniqueness PER
  library — so it no longer blocks a valid same-name collection in another
  library, and the backend 409 is localized to nameTaken.
- member-remove now confirms before the write-through (matches delete-collection).
- add-title / new-collection library search render an error state on request
  failure instead of a misleading "No matches".
- history: seed navCounter past the current entry's persisted sfScroll on load
  so post-reload ids can't collide with back-stack entries and mis-restore scroll.
- PageScroller: LRU-cap the navId-keyed savedScroll map (was unbounded).
- share isIntrinsicReadOnly between the browse tile and the info page so the
  lock/edit affordance can't drift.
This commit is contained in:
2026-08-23 06:57:33 +02:00
parent b71b33654a
commit 6af238c2ea
10 changed files with 227 additions and 21 deletions
+29 -4
View File
@@ -208,6 +208,23 @@ def _reject_duplicate_name(
raise HTTPException(status_code=409, detail="A collection with this name already exists")
def _write_or_pending_error(wrote: bool, e: Exception) -> HTTPException:
"""Map a PlexError raised inside a mutate-then-resync block to the honest status. If the mutating
call itself failed (``wrote`` False) the write never landed → 422 "Plex write failed". If the
write succeeded but the follow-up resync READ (Plex metadata/children) failed, the change IS on
the Plex server → don't claim a write failure; report 502 "sync pending" (the next full sync
reconciles the local mirror)."""
if wrote:
return HTTPException(
status_code=502,
detail=(
"Collection updated in Plex, but refreshing the local copy failed; "
f"it will reconcile on the next sync ({e})"
),
)
return HTTPException(status_code=422, detail=f"Plex write failed: {e}")
class CreateCollectionIn(BaseModel):
library: str = ""
title: str = ""
@@ -239,15 +256,17 @@ def create_collection(
if lib is None:
raise HTTPException(status_code=404, detail="Unknown library")
_reject_duplicate_name(db, lib.id, title)
wrote = False
try:
with PlexClient(db) as plex:
new = plex.create_collection(lib.plex_key, title, seed)
wrote = True
col = plex_sync.resync_collection(db, plex, lib, str(new.get("ratingKey")))
col.editable = True
db.commit()
except (PlexError, PlexNotConfigured) as e:
db.rollback()
raise HTTPException(status_code=422, detail=f"Plex write failed: {e}")
raise _write_or_pending_error(wrote, e)
return _collection_card(col)
@@ -260,14 +279,16 @@ def collection_add_item(
) -> dict:
col = _editable_collection_or_403(db, rating_key)
lib = db.get(PlexLibrary, col.library_id)
wrote = False
try:
with PlexClient(db) as plex:
plex.add_collection_item(rating_key, str(item_rating_key))
wrote = True
col = plex_sync.resync_collection(db, plex, lib, rating_key)
db.commit()
except (PlexError, PlexNotConfigured) as e:
db.rollback()
raise HTTPException(status_code=422, detail=f"Plex write failed: {e}")
raise _write_or_pending_error(wrote, e)
return _collection_card(col)
@@ -280,14 +301,16 @@ def collection_remove_item(
) -> dict:
col = _editable_collection_or_403(db, rating_key)
lib = db.get(PlexLibrary, col.library_id)
wrote = False
try:
with PlexClient(db) as plex:
plex.remove_collection_item(rating_key, str(item_rating_key))
wrote = True
col = plex_sync.resync_collection(db, plex, lib, rating_key)
db.commit()
except (PlexError, PlexNotConfigured) as e:
db.rollback()
raise HTTPException(status_code=422, detail=f"Plex write failed: {e}")
raise _write_or_pending_error(wrote, e)
return _collection_card(col)
@@ -308,14 +331,16 @@ def collection_rename(
raise HTTPException(status_code=400, detail="title is required")
_reject_duplicate_name(db, col.library_id, title, exclude_rk=rating_key)
lib = db.get(PlexLibrary, col.library_id)
wrote = False
try:
with PlexClient(db) as plex:
plex.rename_collection(lib.plex_key, rating_key, title)
wrote = True
col = plex_sync.resync_collection(db, plex, lib, rating_key)
db.commit()
except (PlexError, PlexNotConfigured) as e:
db.rollback()
raise HTTPException(status_code=422, detail=f"Plex write failed: {e}")
raise _write_or_pending_error(wrote, e)
return _collection_card(col)