Skip to content

Add Jellyfin support#2266

Merged
ydkmlt84 merged 35 commits into
Maintainerr:jellyfin-devfrom
enoch85:remaining-issues/abstraction-cleanup-squashed
Jan 31, 2026
Merged

Add Jellyfin support#2266
ydkmlt84 merged 35 commits into
Maintainerr:jellyfin-devfrom
enoch85:remaining-issues/abstraction-cleanup-squashed

Conversation

@enoch85

@enoch85 enoch85 commented Jan 9, 2026

Copy link
Copy Markdown
Collaborator

This pull request adds comprehensive Jellyfin support to Maintainerr, enabling the application to work with either Plex or Jellyfin as the media server backend. The implementation follows a clean abstraction pattern, treating both media servers through a unified interface while preserving their distinct capabilities.

Key Changes

  • Introduces media server abstraction layer with support for both Plex and Jellyfin
  • Implements media server switching functionality with optional rule migration
  • Updates data types from Plex-specific numeric IDs to server-agnostic string identifiers
  • Adds comprehensive test coverage for new functionality

Modern React Patterns (UI Improvements)

  • Removed useEffect in favor of better React patterns - The rule editor form now uses React 19's useSyncExternalStore for scroll detection and relies on React's declarative `key` prop pattern for form reset on navigation. This eliminates unnecessary effect-based state synchronization, making the UI more predictable and snappier.
  • Simplified form state management by removing defensive filtering code that was only needed due to stale effect timing
  • Cleaner component lifecycle without effect chains

@benscobie

Since the code is working now and the other PR is getting quite messy, let's work here instead.


Testing

git clone https://github.com/enoch85/Maintainerr.git
git checkout remaining-issues/abstraction-cleanup-squashed
cd docker-dev-jellyfin-temp
docker compose build --no-cache maintainerr
docker compose up -d

@enoch85 enoch85 changed the title Remaining issues/abstraction cleanup squashed Add Jellyfin support Jan 9, 2026
@enoch85 enoch85 changed the base branch from jellyfin-dev to main January 9, 2026 21:41
- 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
…tibility

- 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
@enoch85 enoch85 force-pushed the remaining-issues/abstraction-cleanup-squashed branch from 88d5e45 to 79f4701 Compare January 9, 2026 22:09
- 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
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.
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.
@enoch85 enoch85 force-pushed the remaining-issues/abstraction-cleanup-squashed branch from e05b507 to e7b0d0a Compare January 12, 2026 14:05
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.
@enoch85

enoch85 commented Jan 12, 2026

Copy link
Copy Markdown
Collaborator Author

Made these tests, all checkout:

  1. Rule Groups
    Create rule group with useRules = false (manual collection)
    Create rule group with useRules = true and add at least one rule
    Edit an existing rule group
    Delete a rule group (should also delete associated collection)
  2. Collections
    Go to Collections page - all collections should load
    Click on a collection to view details
    Check that "Test Media" button only appears when useRules = true
    For useRules = false collections, Test Media button should be hidden
  3. Test Media Modal (if you have a collection with rules)
    Click "Test Media" button
    Modal should open and load media items
    Close modal
  4. Settings Persistence
    Go to Settings → Jellyfin
    Verify your Jellyfin URL and API key are saved
    Restart container, verify settings persist
  5. Delete Test Data
    Delete the test rule group you created
    Verify the associated collection is also deleted

@enoch85

enoch85 commented Jan 12, 2026

Copy link
Copy Markdown
Collaborator Author

@benscobie Decided to remove useEffect since it's better React imo. Learned it from developing on Bazarr, and maybe it's placebo but I feel the UI is snappier now.

- 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
@enoch85

enoch85 commented Jan 12, 2026

Copy link
Copy Markdown
Collaborator Author

@benscobie Turns out apps/ui/src/hooks/useInteraction.ts was not triggered at all, so opted to remove it and its usage and replace with CSS. Tested and works on both mobile and desktop. 👍

@enoch85

enoch85 commented Jan 28, 2026

Copy link
Copy Markdown
Collaborator Author

@benscobie I'm sure you have stuff to do, but just a friendly reminder that this PR exists. :) Would be nice to be able to get this done before Sunday this week.

Been running in my prod stable since the last commit (2 weeks ago).

@Nomcon

Nomcon commented Jan 29, 2026

Copy link
Copy Markdown

Not sure if this is the right place but since issues are disabled on this branch, i figured it might be.

I am pretty sure that the "Jellyfin - Amount of watched episodes" and "Jellyfin - Total views" values dont work correctly for series, or at least not how i expect.
Both of the values are always 0 for me, despite getting the "Jellyfin - Last view date" not being null.

The "total views" value seems fine on movies tho

@enoch85

enoch85 commented Jan 29, 2026

Copy link
Copy Markdown
Collaborator Author

Not sure if this is the right place but since issues are disabled on this branch, i figured it might be.

I am pretty sure that the "Jellyfin - Amount of watched episodes" and "Jellyfin - Total views" values dont work correctly for series, or at least not how i expect. Both of the values are always 0 for me, despite getting the "Jellyfin - Last view date" not being null.

The "total views" value seems fine on movies tho

Can you paste your rules please?

Please also include a "Test media" so that I can compare what gets triggered.

@Nomcon

Nomcon commented Jan 29, 2026

Copy link
Copy Markdown

Of course, here are my rules:
maintainerr_rules_1769694436900.yaml
maintainerr_rules_1769694491481.yaml

and here are the corresponding tests:
- mediaServerId: bf0aa6445faaa4f57c4731fed468a307 result: false sectionResults: - id: 0 result: false ruleResults: - action: before firstValueName: Jellyfin - Last view date firstValue: 2026-01-29T13:48:49.936Z secondValueName: custom_days secondValue: 2025-02-03T13:48:49.936Z result: false - id: 1 result: true operator: AND ruleResults: - action: equals firstValueName: Jellyfin - Total views firstValue: 0 secondValueName: number secondValue: 0 result: true - id: 2 result: true operator: AND ruleResults: - action: equals firstValueName: Jellyfin - Amount of watched episodes firstValue: 0 secondValueName: number secondValue: 0 result: true

and

- mediaServerId: e7524b943574eb9b8715ca6777b4d4bf result: false sectionResults: - id: 0 result: false ruleResults: - action: before firstValueName: Jellyfin - Last view date firstValue: 2025-11-16T23:28:13.801Z secondValueName: custom_days secondValue: 2025-02-03T13:51:26.122Z result: false - id: 1 result: false operator: AND ruleResults: - action: equals firstValueName: Jellyfin - Times viewed firstValue: 2 secondValueName: number secondValue: 0 result: false

I actually just realized that the last view date doesnt seem right either, for seasons its always todays date, even tho it wasnt watched today according to my jellystat.

@ydkmlt84

Copy link
Copy Markdown
Collaborator

/release-pr

@github-actions

Copy link
Copy Markdown
Contributor

🔒 Release builds can only be triggered for branches in the Maintainerr/Maintainerr repository. This prevents running privileged jobs on forks.

@enoch85 enoch85 changed the base branch from main to jellyfin-dev January 31, 2026 04:26
@ydkmlt84 ydkmlt84 merged commit 6f9b548 into Maintainerr:jellyfin-dev Jan 31, 2026
7 checks passed
ydkmlt84 pushed a commit that referenced this pull request Jan 31, 2026
* 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 (#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
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.

3 participants