Skip to content

fix(jellyfin): item-aware local artwork mirroring with undo reconciliation - #2

Open
SaxxyToo wants to merge 1 commit into
jellyfinfrom
fix/jellyfin-artwork-persistence
Open

SaxxyToo wants to merge 1 commit into
jellyfinfrom
fix/jellyfin-artwork-persistence

Conversation

@SaxxyToo

@SaxxyToo SaxxyToo commented Oct 5, 2026

Copy link
Copy Markdown
Owner

What this fixes

With LOCAL_ARTWORK enabled against Jellyfin, mirrored local files landed in the wrong place or could escape the configured media mapping, and undo left them behind — so Jellyfin's local image provider would re-import stale art on its next refresh, silently reverting an undo.

Item-aware destinations

The Jellyfin/Emby adapter now reports an item's path and item type (movie/show/season/episode) via getItemMediaLocation, matched on the exact item id; virtual/remote/unknown types return null. The mirror uses the type to place files where Jellyfin actually looks:

  • Show / season — folder.* inside the item's own directory (previously landed in the library root or the show folder).
  • Episode — basename-thumb.* (previously clobbered the season folder's art).
  • Movie — folder.* in dedicated single-video folders; basename-poster.* / basename-fanart.* in shared multi-video folders, so applying one movie no longer overwrites a sibling's poster.
  • Season overrides — season mirrors skip when the parent show directory carries season-prefixed override images, matching Jellyfin's own provider precedence.

Containment and safe writes

  • Realpath-checked containment: .. traversal and symlink escapes are refused. Nothing is read, backed up, or moved outside the mapped MEDIA_PATH_MAP root. (Reproduced the escape before the fix.)
  • Conflicting local images (poster.jpg, cover.jpg, …) are reconciled — backed up and removed — even when the canonical file already matches, closing the stale-ghost path.
  • Writes stage to a temp file, re-check for races, then atomically rename; per-folder serialization; cleanup on failure; backups use exclusive creates.

Undo reconciliation

Undo now mirrors the restored server artwork back to the local file, compare-and-set against the pre-undo bytes:

  • Refuses when the local file changed independently — never overwrites art you edited outside PosterPilot.
  • Backs up before removal; removals only touch matching conflicts; unrelated images are untouched.
  • Best-effort: a mirror failure is logged and never fails the server-side undo. Wired into the real executor through createLocalArtworkWriterFromEnv.

Verification

  • bun run check — 0 errors; bun run lint — clean; bun run build — success.
  • bun run test — 2,533 passed, 2 skipped.
  • Local-artwork suite 10 → 43 tests; 11 new undo-mirror tests; typed-location forwarding tests for apply and custom upload; new adapter location test file.
  • Component tests fail identically before and after (vitest browser connection drops mid-run) — pre-existing, environment-level.

Follow-ups (not in this change)

  • Undo still unconditionally unlocks the metadata field on Jellyfin, where the lock is item-wide rather than per-image, so undoing one slot drops protection from the others.
  • vitest.config.ts still uses the removed-in-v4 poolOptions (warning only).

…ation

- Mirror artwork at the provider's item location: shows/seasons write
  inside their directory, episodes use basename-thumb, shared movie
  folders use basename-poster/-fanart instead of clobbering folder art
- Reject traversal and symlink escapes; all writes stay inside the
  mapped MEDIA_PATH_MAP root (realpath-checked)
- Reconcile conflicting local images even when the canonical file
  already matches, so stale poster/cover ghosts get backed up
- Restore (undo) support: compare-and-set against pre-undo server bytes,
  refuse when local artwork changed independently, back up before
  removal, never touch unrelated images
- Jellyfin/Emby adapter reports the item's path + type; apply, custom
  upload, and undo all forward it to the local artwork writer
- Season mirrors skip when parent-level season art overrides exist
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant