Skip to content

fix(media-sources): mark missing libraries unavailable instead of deleting them - #2086

Merged
chrisbenincasa merged 4 commits into
mainfrom
fix/library-reconciliation-1973
Sep 22, 2026
Merged

chrisbenincasa merged 4 commits into
mainfrom
fix/library-reconciliation-1973

Conversation

@chrisbenincasa

Copy link
Copy Markdown
Owner

Fixes #1973.

Problem

MediaSourceLibraryRefresher runs at startup and every hour. It deleted any stored library that was absent from the media server's library list. program.library_id and program_grouping.library_id cascade on delete, and channel_programs cascades from program. One incomplete response, such as a restricted token or a server that is still starting, silently removed the library's programs and channel schedule entries. The three backend handlers had also drifted apart. Only Emby guarded against an empty response, and only before filtering out unsupported types. Jellyfin and Emby never synced library renames.

Changes

  • Shared reconcile. Plex, Jellyfin and Emby each map their response to { externalKey, name, mediaType } and call one pure function, reconcileLibraries.
  • Never delete missing libraries. A new nullable media_source_library.unavailable_since column marks them, and the mark clears when the library returns. The user's enabled flag is never touched.
  • Empty-response guard. Reconciliation is skipped when the server reports no supported libraries but some are stored.
  • Duplicate rows. Programs and groupings move to the kept row before duplicates are deleted. The kept row is the enabled one when there is one. This now applies to all backends.
  • Scan gate. MediaSourceScanCoordinator.add() refuses unavailable libraries, which covers the scheduled task and the manual scan endpoint.
  • Cron offset. ScanLibrariesTask now runs at :30 so it never fires in the same second as the hourly refresh.
  • Logging. A warn with preserved program and schedule counts is logged when a library first goes missing, and an info when it returns. The count query only runs on that transition.
  • API and web. unavailableSince is exposed on library responses, and an "Unavailable" badge appears in the library table and the Manage Libraries dialog.
  • Docs. Plex and Jellyfin sync docs are updated, and a "Library shows as Unavailable" entry is added to common issues.

Not included

  • Program deletion via DELETE /trash. It still skips lineup invalidation and a guide rebuild. It overlaps with the incremental empty-trash work and is left for that change.
  • Generated client. The client on main is stale against the server. This PR adds only the 15 unavailableSince lines to web/src/generated/types.gen.ts, because a full regen pulls in unrelated changes that break ShowSearchSlotProgrammingForm.tsx. docs/generated is unchanged.
  • Translations. Messages for the new UI strings are not extracted.

Test plan

  • reconcileLibraries.test.ts: missing → unavailable, returning → available with enabled kept, empty-response guard, new libraries added disabled, no re-marking, renames, duplicates keep the enabled row
  • MediaSourceLibraryRefresher.test.ts: per-backend wiring, including unsupported-only responses and request failure
  • MediaSourceDB.test.ts: duplicate merge moves programs, unavailable/available round trip keeps programs and enabled
  • pnpm turbo test (server), server and web typecheck, pnpm lint-changed
  • Manual: restrict a Plex token so a library disappears, confirm programs and schedules survive and the badge shows, then restore access and confirm it clears
  • Manual: check the badge renders in the library table (desktop and mobile) and in the Manage Libraries dialog

🤖 Generated with Claude Code

chrisbenincasa and others added 2 commits September 21, 2026 19:59
…eting them

The hourly library refresh deleted any stored library absent from a media
server response. Library foreign keys cascade, so one incomplete response
(restricted token, server still starting) silently removed the library's
programs and channel schedule entries.

- Share one reconcile function across Plex, Jellyfin and Emby
- Mark missing libraries with unavailable_since; clear it when they return
- Skip reconciliation when the server reports no supported libraries
- Move programs and groupings to the kept row before deleting duplicate
  library rows
- Refuse scans of unavailable libraries in MediaSourceScanCoordinator
- Offset the library scan cron from the hourly refresh
- Show an Unavailable badge in the library table and libraries dialog

Fixes #1973

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@chrisbenincasa
chrisbenincasa force-pushed the fix/library-reconciliation-1973 branch from 4892916 to 830e636 Compare September 22, 2026 00:03
@chrisbenincasa
chrisbenincasa changed the base branch from main to fix/external-subtitles-jellyfin-emby September 22, 2026 00:03
Base automatically changed from fix/external-subtitles-jellyfin-emby to main September 22, 2026 15:54
chrisbenincasa and others added 2 commits September 22, 2026 15:24
The pseudo-LOCALE, es and en catalogs each had `<<<<<<< HEAD` baked into 36
translation strings, so the marker rendered in the UI wherever those messages
were used. In en it was appended to real strings; in es and pseudo-LOCALE it
replaced the empty msgstr.

`tunarr-v1.1.0-dev.1-openapi.json` was committed with an unresolved conflict
holding both copies of the spec. Restored the HEAD side, which is byte
identical to the file as added in 580091a and matches the current
/api/programs/{id}/scan route.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ejected

A revoked or mistyped access token left stale libraries looking healthy
forever. Reaching that case first meant repairing the client error handling it
depended on.

`doRequest` throws on every axios error and `doTypeCheckedGet` never caught it,
so its error classification block was unreachable and every isFailure() branch
in the Plex, Jellyfin and Emby clients was dead for HTTP errors. The 404 ->
not_found mapping had never worked either. It now returns failure results as
its signature always promised, with a new auth_error code for 401 and 403.

- Mark a source's libraries unavailable after 3 consecutive rejected refreshes,
  tracked in media_source.consecutive_auth_failures and cleared by any
  successful fetch. A server rejecting tokens while it restarts recovers well
  inside that; a revoked token does not.
- Leave libraries alone for every other failure, which carries no information
  about them
- Never delete duplicates on the auth path, since a rejected credential says
  nothing about which rows are redundant
- Guard the refreshAll loop so one unreachable source no longer strands the
  sources behind it
- Fix deleteCachedClient, which built its key as type|uri|token while the cache
  is written as type|id, so it never deleted and credential edits were ignored
  for up to the 1 hour TTL
- Show a warning icon rather than a chip in the library table, where the chip
  wrapped onto its own line

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@chrisbenincasa
chrisbenincasa merged commit 9856711 into main Sep 22, 2026
2 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

This is included in v2026.9.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: v1.0.0 and v1.3.10: startup silently deletes program rows below a specific timestamp boundary, undetected by API

1 participant