Metadata provider abstraction layer with TVDB support#2406
Closed
enoch85 wants to merge 203 commits into
Closed
Conversation
* feat: Add Jellyfin support with abstraction layer - Add MediaServerService abstraction for Plex/Jellyfin - Implement JellyfinService with full API integration - Update rules engine to support multiple media servers - Add Jellyfin settings UI and configuration - Refactor collections to use MediaServerService - Add comprehensive type safety improvements - Update database schema for media server support * snc against this branch instead * feat(api): add deprecated /api/plex legacy wrapper for backward compatibility - Add PlexApiLegacyController with all 19 original endpoints - All endpoints delegate to MediaServerFactory abstraction - Add deprecation headers (X-Deprecated, Deprecation, Link) - Uses MediaServerSetupGuard (works with both Plex and Jellyfin) - Maps old route patterns to new abstraction interface - Single file design for easy removal when legacy support ends To remove legacy support: delete plex-api-legacy.controller.ts and remove PlexApiLegacyController from plex-api.module.ts * chore: merge main branch changes - Use leftJoinAndSelect for rules (allows groups without rules) - Delete unused CollectionDetail/index.tsx - Use useRuleGroupForCollection hook instead of manual fetch - Conditionally show Test Media button only when useRules=true - Simplify addCollectionToDB and RemoveCollectionFromDB methods * fix: update SchemaSync migration to use mediaServerId instead of plexId The SchemaSync migration from main referenced plexId (integer) but our JellyfinSupport migration already renamed this to mediaServerId (varchar). Updated both UP and DOWN methods to use the correct column name and type. * regenerated with typeorm * fix: standardize quotation marks and formatting in SchemaSync migration * generate clean typeorm * fix(ui): filter out empty/invalid rules to prevent crash When useRules=false, the backend creates a rule with empty ruleJson. These rules have no firstVal, causing TypeError when shouldFilterApp tries to access rule.firstVal[0]. Refactored to remove all useEffect usage: - Use useSyncExternalStore for scroll detection (React 19 pattern) - Move rule filtering to event handlers (updateLibraryId, handleUpdateArrAction) - Extract helper functions outside component (parseValidRules, shouldFilterApp, filterRulesForArrSettings) This is cleaner and more aligned with modern React patterns. * fix: complete merge of main branch rule editor fixes This commit completes the merge from main that was started in 0923d01. The original merge included the leftJoinAndSelect changes but missed removing the empty rule creation code from rules.service.ts. Backend (rules.service.ts): - Remove empty rule creation in createRuleGroup and updateRuleGroup - When useRules=false, the backend was creating rules with empty ruleJson - This aligns with main's fix from commit 730adb5 (Maintainerr#2270) - The RemoveEmptyRules migration (already present) cleans up existing data Frontend (RuleFormPage.tsx): - Add key={id} prop to AddModal component - This ensures the form fully remounts when navigating between rule groups - Replaces the need for useEffect-based form reset logic Frontend (AddModal/index.tsx): - Remove parseValidRules helper (no longer needed since empty rules won't exist) - Remove defensive firstVal check in filterRulesForArrSettings - Simplify rules state initialization to direct parsing The root cause was that commit 0923d01 merged the query changes (leftJoinAndSelect for rules) but not the corresponding code removal that prevents empty rules from being created in the first place. * replace more useEffect usage * prettier * refactor(ui): replace JS touch detection with CSS media queries - Remove unused useInteraction, useIsTouch hooks and InteractionContext - Add CSS @media (hover: hover/none) for touch-friendly MediaCard - Simplify MediaCard: click always opens modal, CSS handles hover states - Reduces bundle size by ~150 lines of JS with 15 lines of CSS * fix click behaviour on mobile * fix bug on about page showing 0 items in collections * add support for retrieving seasons and episode view counts * retrieve admin user ID for UserData fields in getSeasons and getItems * fix: make watchedAt optional in WatchRecord interface * add temporary debug for viewdate * more temp debug * possible fix for viewdate season/episodes * prettier * feat: add getPlaylistItems method to JellyfinAdapterService and update JellyfinGetterService to utilize it * fix cd/ci tests
Added a warning about the development branch and Jellyfin support.
When upgrading from pre-Jellyfin versions, media_server_type is null. Instead of defaulting to Plex everywhere (which caused noisy errors if Plex was down), auto-detect the server type from existing credentials during settings init and persist it. - settings.service.ts: auto-detect Jellyfin/Plex from credentials on init - media-server.factory.ts: throw instead of defaulting to Plex when no server type is configured; distinguish "not configured" from "unreachable" in startup logging - collections.service.ts: update return type to match factory change
…ching Handle partial failures gracefully in Jellyfin adapter. If any single user query fails when fetching watch history or play counts, the other successful queries are still processed instead of losing all data.
Query parameters are received as strings. Without ParseIntPipe, numeric comparisons and math operations work on strings, producing incorrect results. Added ParseIntPipe with optional: true to: - media-server.controller.ts: page, limit parameters - plex-api-legacy.controller.ts: amount parameter - rules.controller.ts: rulegroupId, typeId parameters Also refactored rules.controller.ts to use individual @query parameters instead of inline object types for better type safety.
The Plex adapter's resetMetadataCache() was a no-op despite PlexApiService having this capability. Now properly delegates to plexApi.resetMetadataCache() when an itemId is provided.
Instead of hardcoding progress to 100, use the actual PlayedPercentage from Jellyfin's UserData when available. Falls back to 100 if not set. Note: Plex API doesn't expose progress percentage in watch history, so it remains hardcoded to 100 (which is correct for completed watches).
The testResult state was never cleared when URL or API key inputs changed. Users could modify credentials and save with a stale success indicator. Now clears test result and testedSettings when credentials are modified.
The factory already handled errors properly, but the warning message didn't include the actual error details for debugging. Now includes the error message in the log output.
Remove leftover debug console.log statement from addImportExclusion.
Add comprehensive unit tests for PlexAdapterService covering: - Lifecycle management (isSetup, initialize, uninitialize) - Server type identification (returns PLEX) - Feature detection (LABELS, PLAYLISTS, COLLECTION_VISIBILITY, etc.) - Cache management with resetMetadataCache delegation - Server status mapping to MediaServerStatus - User fetching and mapping to MediaUser - Library fetching and mapping to MediaLibrary - Library content queries with Jellyfin ID detection - Watch history retrieval and mapping to WatchRecord - Collection operations (create, delete) - Search content delegation This brings the test count from 392 to 419 tests.
Jellyfin provides ratings through CommunityRating (typically TMDB, 0-10 scale) and CriticRating (typically Rotten Tomatoes, 0-100 scale normalized to 0-10). Map these to the existing rating rule properties: - rating_imdb/rating_tmdb: Use CommunityRating (TMDB-sourced) - rating_rottenTomatoesCritic: Use CriticRating - rating_rottenTomatoesAudience: Use CommunityRating as approximation - All *Show variants: Traverse to parent/grandparent for show metadata This enables Jellyfin users to create rules based on ratings, which previously returned null for all external rating properties.
Jellyfin genres don't have IDs, so we were using array index which was fragile and could change between requests or items. Now using djb2 hash algorithm to generate stable IDs from genre names. Fixes review item #11.
During initial setup, the Plex and Jellyfin tabs were both visible before the user selected a media server type. Now only the General tab is shown until a selection is made. Fixes review item #14.
…switch Added a switchInProgress flag that: - Prevents concurrent switch operations from starting - Blocks factory.getService() calls during the switch - Is always released in a finally block This prevents API requests from routing to the wrong adapter during the window between settings update and server initialization. Fixes review item #8.
…apters - Document error handling contract in IMediaServerService interface: - Read operations return empty/undefined on failure - Write operations throw with descriptive message - Add try/catch with logging to Plex adapter write operations - Improve error messages to include context (collection ID, item ID, etc.) Fixes review item #10.
The lock added to MediaServerFactory caused a circular dependency between MediaServerModule and SettingsModule. Removed the lock entirely as it was over-engineering - the switch operation is rare and short-lived.
- Hide Plex and Tautulli rules when on Jellyfin media server - Hide Jellyfin rules when on Plex media server - Use MediaServerType enum from contracts instead of string literals
- Replace string literals 'plex' and 'jellyfin' with MediaServerType enum - Update Settings/index.tsx to use enum for tab visibility - Update MediaServerSelector to use enum for all comparisons
… in jellyfin adapter
…mproved performance
…ction for admin operations
… into feat/jellyfin-connection-error-handling
…stream/main) Resolve conflicts: - seerr-api.service.ts: use unified logConnectionTestError with 'Seerr' label - Remove deleted jellyseerr files (unified into seerr)
Resolve conflict in CollectionItem: keep both resolveImageUrl and formatSize.
Resolve conflicts between the metadata provider abstraction layer and the Seerr unification + collection size features from main.
enoch85
added a commit
that referenced
this pull request
Mar 9, 2026
…, #2442, #2406, #2386, #2370 PR #2466 - fix: honor Jellyfin played threshold - Respect configured played percentage threshold for Jellyfin watch status PR #2461 - feat(rules): add ARR disk target path selection for disk space rules - Allow selecting specific disk target paths for Radarr/Sonarr disk space rules PR #2458 - feat: clean up empty ended shows in Sonarr after season actions - Automatically remove ended shows from Sonarr when all seasons are processed PR #2453 - fix: improve Plex viewCount reliability and add isWatched boolean - Use native Plex viewCount field with watch history fallback - Add new isWatched boolean rule property PR #2452 - build(deps): bump actions/download-artifact from 7 to 8 PR #2451 - build(deps): bump actions/upload-artifact from 6 to 7 PR #2442 - fix(server): reject null/undefined in numeric rule comparisons - Add getComparisonResult wrapper that fails closed on null/undefined operands - Strict type checking for BIGGER/SMALLER comparisons PR #2406 - Metadata provider abstraction layer with TVDB support - Add MetadataService as central metadata resolution layer - TVDB support as alternative metadata provider - Dynamic provider preference with fallback - Replace TmdbIdService with unified MetadataService PR #2386 - feat: missing_episode rules - Add missing episode count as a rule property for Sonarr PR #2370 - build(deps-dev): bump the eslint group with 2 updates
Collaborator
Author
|
This is now included in the You can check the latest commits here: https://github.com/Maintainerr/Maintainerr/commits/jellyfin-dev
Thank you very much! 🚀 |
711d7a0 to
77ac814
Compare
enoch85
added a commit
that referenced
this pull request
Mar 24, 2026
PR #2406 - Metadata provider abstraction layer with TVDB support - Tighten provider settings validation - Tighten resolver and settings boundaries - Prefer validated servarr provider lookups - Fix review findings - Formatting and lint fixes
enoch85
added a commit
that referenced
this pull request
Mar 24, 2026
PR #2406 - Metadata provider abstraction layer with TVDB support - Tighten provider settings validation and extract MetadataSettingsService - Tighten resolver and settings boundaries - Prefer validated servarr provider lookups via findServarrLookupMatch - Refactor deleteShowIfEmpty/unmonitorShowIfEmptyAndEnded to use metadata layer - Resolve TMDB ID through metadata layer in Seerr cleanup - Fix review findings, formatting and lint fixes
Collaborator
Author
|
@ydkmlt84 Tested and is solid afaics. |
1200852 to
ac7e7eb
Compare
Collaborator
Author
|
This got too diverged. Will close and make a new PR. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a fully provider-agnostic metadata layer that centralizes all external ID resolution, image fetching, and content details behind a single
IMetadataProviderinterface. Adding a new metadata provider requires zero changes toMetadataService— implement the interface, register it, and it slots into the preference/fallback chain automatically.Provider-agnostic architecture
Each provider (TMDB, TVDB, or any future addition) implements
IMetadataProvider:isAvailable()— is this provider configured and ready?extractId()/assignId()— read/write the provider's own ID from a shared ID baggetDetails()/getPosterUrl()/getBackdropUrl()/getPersonDetails()— normalised data accessfindByExternalId()— cross-provider ID search (e.g. look up a TVDB entry by IMDB ID)MetadataServicenever references TMDB or TVDB by name. It receives all registered providers via injection, orders them by the user's preference setting, and iterates with automatic fallback.Adding a new provider
modules/api/xxx-api/IMetadataProviderMetadataModule: import the API module, add it to theMetadataProvidersfactory arrayMetadataProviderPreferencein@maintainerr/contractsThe preference selector, fallback chain, and ID resolution all pick it up automatically.
What changed
MetadataService — single entry point for all metadata operations
resolveIds()/resolveAllSeriesIds()/resolveAllMovieIds()— resolve media server items to external IDs with cross-provider gap fillinggetDetails()/getPersonDetails()/getPosterUrl()/getBackdropUrl()— fetch content details and images with ordered provider fallbackTMDB_PRIMARY|TVDB_PRIMARY) controls resolution order and image sourceSettings_Updatedevents — no restart neededTVDB v4 API integration (
TvdbApiService)Custom TMDB API key support
TmdbApiServicenow accepts user-provided API keys; falls back to built-in defaultMetadataController — new controller at
api/metadataTmdbApiController(route changed fromapi/moviedb→api/metadata)/person/:idendpoint removed — person details now served throughMetadataService.getPersonDetails()with provider fallback/movie/imdb/:idendpoint removed — IMDB lookups now handled internally by the ID resolution pipelineAll consumers updated
MetadataService.resolveIds()resolveAllMovieIds()/resolveAllSeriesIds()MetadataServicefor details + imagesMediaIdFindereliminatedFrontend — new Metadata settings page
Migration:
AddMetadataSupport— addstmdb_api_key,tvdb_api_key,metadata_provider_preferencecolumns (+tvdbIdoncollection_media)Module encapsulation
TmdbApiModule/TvdbApiModuleremoved fromAppModuleroot importsMetadataModuleandSettingsModuleimport them explicitlyCloses #2400