Skip to content

Jellyfin Integration - #234

Open
RogerSik wants to merge 5 commits into
StudioNirin:mainfrom
RogerSik:main
Open

RogerSik wants to merge 5 commits into
StudioNirin:mainfrom
RogerSik:main

Conversation

@RogerSik

Copy link
Copy Markdown

PexCache-D extended with Jellyfin. It works here fine. Here we have at home Plex as main player and Jellyfin as backup and because I'm curious to see how Jellyfin progress.

The entire code is written by Claude because I'm a sysadmin and not a programmer. I'm completely fine with it if you disregard this pull request because of AI or code quality or etc. At least it will be findable over the search for other Jellyfin user.

RogerSik and others added 2 commits October 29, 2026 14:46
Continue Watching + Next Up (plus next N episodes) of every Jellyfin user
are cached alongside Plex OnDeck; files playing on Jellyfin are skipped.
Jellyfin paths are translated into Plex paths via jellyfin_path_mappings.
Move-back is skipped when Jellyfin data is incomplete. Configured via
settings keys or JELLYFIN_* env vars.

Refs: https://nextcloud.sikorski.cloud/index.php/apps/deck/board/78/card/258

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat: Jellyfin as additional OnDeck source
@RogerSik
RogerSik requested a review from StudioNirin as a code owner October 30, 2026 06:32
@StudioNirin

Copy link
Copy Markdown
Owner

@Brandon-Haney
I remember this was on the wishlist a while back, but i dont have a jellyfin instance to do any testing with at the moment. Have considered setting one up but as I have lifetime plex for cheap its not been a big priority.
Any of this seem to cause any issues or conflicts? Worth considering the merge at least

@Brandon-Haney Brandon-Haney left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for putting this together. Mapping Jellyfin paths into Plex paths is a good fit, and with Jellyfin unconfigured it stays out of the way of existing users. I've read through the code but haven't run it, and CI hasn't run yet either.

There are some things I'd want fixed before we seriously consider merging this. Most of the detail is in the inline comments:

  1. Sessions: name the server the session was found on, and count sessions on unmapped paths.
  2. A failed Jellyfin request shouldn't drop a user's whole list.
  3. Long shows are at risk of hitting the 30s timeout.
  4. Validate and normalize the path mappings.
  5. Log unmapped paths at INFO.
  6. State the minimum Jellyfin version in the README.

One more that can't go inline because the file isn't in this diff: export_settings(include_sensitive=False) in web/services/settings_service.py redacts from an explicit list, so jellyfin_api_key and jellyfin_url would stay in exports people share. Please blank both there, the same way PLEX_TOKEN and PLEX_URL are handled, and add a test for it.

Also, jellyfin_skip_users is the only user filter here. With users_toggle off, Plex only looks at the main account, but Jellyfin still fetches everyone. I think those should match, or the README should say that they don't.

My open PR #228 adds a test requiring every settings key to be in KNOWN_SETTINGS_KEYS, and REDACTED_EXPORT_KEYS needs the secret ones too. Those don't exist on main yet, so whichever PR merges second will pick that up.

I'm not asking for a Settings UI right now. That's a separate conversation if this moves forward. Please add tests alongside the fixes, especially for items 1 and 2.

Comment thread core/app.py Outdated
"""Check for active Plex sessions."""
"""Check for active Plex and Jellyfin sessions."""
sessions = self.plex_manager.get_active_sessions()
jellyfin_paths = self.jellyfin_manager.get_active_session_paths() if self.jellyfin_manager else []

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few things here:

The messages should name the server that actually has the session. Right now the generic message means Plex and the other one says Jellyfin. Something like this would cover Plex, Jellyfin or both:

active_servers = [name for name, found in (("Plex", sessions), ("Jellyfin", jellyfin_sessions)) if found]
if active_servers:
    message = f"There is an active session on {' and '.join(active_servers)}. Exiting..."

get_active_session_paths() only returns mapped paths, so a session on an unmapped path looks like no session and exit_if_active_session doesn't fire. It would help to have it report the session separately from the paths.

The [FILTER] Skipped N active session(s) line sits inside if sessions, so Jellyfin-only skips never show up at INFO. It should cover both.

The log analyzer also matches this text. docs/index.html has /There is an active session\. Exiting/i, which should become /There is an active session.*Exiting/i. The timeline pattern /active session.*Exiting/i already works with the wording above, but neither one matches "active Jellyfin session" as it's written now.

Comment thread core/jellyfin_api.py
logging.debug(f"[JELLYFIN] [USER:{item.username}] OnDeck found: {item.file_path}")
return on_deck_files

def _fetch_user_on_deck_media(self, user_id: str, username: str, days_to_monitor: int,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The list only comes back at the end, so one failed request loses everything already collected for that user. Could each item get its own try/except, so the failure only costs that item and still marks the data incomplete? We made the same kind of change on the Plex side in #204.

It's also worth remembering that incomplete Jellyfin data skips array restore for Plex files as well. I think that's the right call for safety, but the warning should make it obvious that Jellyfin is what's holding things up, especially for people running it as a backup server.

Comment thread core/jellyfin_api.py Outdated
logging.warning(f"[JELLYFIN] Skipping next episode fetch for '{show}' - missing index data")
return

episodes = self._get(f"/Shows/{item['SeriesId']}/Episodes", userId=user_id,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pulls every episode of the show with MediaSources, once per Next Up item and per user, under a 30s timeout. A long-running show on a slow NAS could plausibly time out, and that then runs into the problem above. /Shows/{seriesId}/Episodes supports startItemId, so passing the current episode would skip everything before it.

Comment thread core/jellyfin_api.py Outdated
skip_users: Optional[List[str]] = None, session: Optional[Any] = None):
self.url = url.rstrip('/')
# Longest prefix first, so nested mappings win
self.path_mappings = sorted((path_mappings or {}).items(), key=lambda kv: len(kv[0]), reverse=True)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The prefixes aren't normalized. {"/media": "/mnt/user/plex-media/"} also matches /media2/... and produces // in the path, and I'd expect that to carry through into the tracker and exclude list entries. Could this normalize trailing slashes on both sides?

Comment thread core/config.py Outdated
env_mappings = os.environ.get('JELLYFIN_PATH_MAPPINGS')
if env_mappings:
try:
self.jellyfin.path_mappings = json.loads(env_mappings)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid JSON that isn't an object, like [], gets past this and then raises on .items() when the manager is created. A bad setting on an optional integration shouldn't stop a Plex run, so please check it's a dict and log an error if it isn't.

Comment thread core/jellyfin_api.py Outdated
if plex_path:
result.append(plex_path)
else:
logging.debug(f"[JELLYFIN] No jellyfin_path_mappings entry for {path}, skipping")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be INFO, as Plex's [CONFIG] Skipping unmapped path is. At DEBUG, a wrong mapping just looks like "Jellyfin OnDeck: 0 items" and nothing tells the user why. One line per library prefix would be enough.

Comment thread README.md Outdated

## Jellyfin (optional)

Jellyfin can be added as a second OnDeck source next to Plex, for setups where both servers share the same media. Every enabled Jellyfin user's *Continue Watching* and *Next Up* (plus the next `number_episodes` episodes) are cached like Plex OnDeck items, and files playing on Jellyfin are skipped. There is no Jellyfin watchlist. Plex stays required.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add that this needs Jellyfin 10.9 or newer. /UserItems/Resume isn't in 10.8.13, so on an older server every user fetch would 404, the data would count as incomplete, and nothing would ever move back to the array.

- Name the server(s) with an active session in the exit message and count
  Jellyfin sessions on unmapped paths; log [FILTER] skips for Jellyfin too
- Per-item and per-list error handling, so one failed request only costs
  that item while still marking the data incomplete
- Fetch episodes from the current one on (startItemId) to keep long shows
  under the request timeout
- Normalize trailing slashes in jellyfin_path_mappings, match whole folders
  only, and reject non-object/invalid mappings without stopping the run
- Log unmapped paths at INFO once per library folder
- Redact jellyfin_api_key and jellyfin_url in sensitive-free exports
- README: Jellyfin 10.9+, users_toggle scope, incomplete-data behavior
- Log analyzer matches the new active-session message

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Brandon-Haney

Copy link
Copy Markdown
Collaborator

Thanks for the quick turnaround. I went back through the new commit and everything from the first review is covered, with tests for each, so I'm happy with where this is.

Two small things, neither blocking:

  • jellyfin_skip_users in the settings file goes straight into a set, so if someone writes it as a string like "guest,kid" it turns into a set of single characters and nobody gets skipped. Splitting a string on commas, the same way the env var is handled, would cover it.
  • startItemId has an edge on current releases. If the item is an alternate version, Jellyfin doesn't find it in the episode list and returns nothing, so that show gets no prefetch. It's handled on Jellyfin's master branch but not in 10.11.0 or earlier. Retrying without startItemId when the list comes back empty would cover it. Nothing breaks either way, the current episode still gets cached.

Since neither Nirin nor I run Jellyfin, could you paste a --verbose log from a real run on your setup? Mainly the [JELLYFIN] lines and the fetch summary, plus one run with something playing on Jellyfin if you can. I'll approve the CI run so the tests get a proper pass too.

I'll sort out the KNOWN_SETTINGS_KEYS part on my side with #228, whichever one merges first.

Brandon-Haney added a commit to Brandon-Haney/PlexCache-R that referenced this pull request Nov 3, 2026
PR StudioNirin#234 adds four jellyfin_* settings keys. Listing them here keeps
test_every_real_settings_key_is_known passing whichever of the two PRs
merges first, and keeps an imported redacted export from replacing the
installation's Jellyfin URL and API key.
Brandon-Haney added a commit to Brandon-Haney/PlexCache-R that referenced this pull request Nov 3, 2026
PR StudioNirin#234 adds four jellyfin_* settings keys. Listing them here keeps
test_every_real_settings_key_is_known passing whichever of the two PRs
merges first, and keeps an imported redacted export from replacing the
installation's Jellyfin URL and API key.
…in#234)

- Accept jellyfin_skip_users as a comma-separated string in the settings
  file, so "guest,kid" no longer turns into a set of single characters
- Retry the episode list without startItemId when it comes back empty;
  Jellyfin <= 10.11 returns nothing when the current item is an alternate
  version

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@RogerSik

RogerSik commented Nov 3, 2026

Copy link
Copy Markdown
Author

Thanks! Both points are in 02ba6c7:

  • jellyfin_skip_users accepts a comma-separated string in the settings file, split the same way as the env var (test included).
  • When the episode list comes back empty with startItemId, it's fetched again without it (test included).

Logs below are from real --verbose runs on 02ba6c7 against Jellyfin 12.4.0, next to Plex on Unraid. Usernames and host are redacted, and I cut the 67 OnDeck found lines down to a few.

Run 1: nothing playing on Jellyfin (a Plex session was active)

21:02:56 - INFO - === PlexCache-D v3.2.0 (build: 02ba6c7e22e279e0d332d81abf30f3e470ff8775) ===
21:02:56 - INFO - [JELLYFIN] Additional OnDeck source enabled: https://[REDACTED]
21:03:02 - DEBUG - Skipping active session file: /mnt/user/plex-media/TV Shows/Relic Hunter (1999) [tmdbid-2855]/Season 03/Relic Hunter (1999) - S03E18 - Faux Fox - 1080p.mkv
21:03:02 - INFO - [FILTER] Skipped 1 active session(s)
21:03:11 - DEBUG - [JELLYFIN] [USER:<user>] OnDeck found: /mnt/user/plex-media/TV Shows/Family Guy (1999) [tmdbid-1434]/Season 02/Family Guy - S02E03 - Der letzte Knall.mkv
21:03:11 - DEBUG - [JELLYFIN] [USER:<user>] OnDeck found: /mnt/user/plex-media/TV Shows/Family Guy (1999) [tmdbid-1434]/Season 02/Family Guy - S02E04 - Hundeliebe.mkv
...
21:03:11 - DEBUG - [JELLYFIN] [USER:<user>] OnDeck found: /mnt/user/plex-media/TV Shows/Kevin Can FK Himself (2021) [tmdbid-99652]/Season 01/Kevin Can FK Himself (2021) - S01E02 - New Tricks - 1080p.mkv
21:03:11 - DEBUG - [JELLYFIN] [USER:<user>] OnDeck found: /mnt/user/plex-media/TV Shows/Kevin Can FK Himself (2021) [tmdbid-99652]/Season 01/Kevin Can FK Himself (2021) - S01E03 - We're Selling Washing Machines - 1080p.mkv
...
21:03:11 - INFO - [FETCH] Jellyfin OnDeck: 67 items
21:03:11 - INFO - [FETCH] OnDeck: 275 items from 5 users
21:04:03 - INFO - [FETCH] Watchlist: 6 items
21:04:08 - INFO - [FETCH] Media: 645 total (633 already cached, 12 to cache)

Run 2: Family Guy playing on Jellyfin, Plex session still active

21:05:05 - INFO - === PlexCache-D v3.2.0 (build: 02ba6c7e22e279e0d332d81abf30f3e470ff8775) ===
21:05:05 - INFO - [JELLYFIN] Additional OnDeck source enabled: https://[REDACTED]
21:05:08 - DEBUG - [JELLYFIN] Active session detected, skipping: Family Guy
21:05:08 - DEBUG - Skipping active session file: /mnt/user/plex-media/TV Shows/Relic Hunter (1999) [tmdbid-2855]/Season 03/Relic Hunter (1999) - S03E18 - Faux Fox - 1080p.mkv
21:05:08 - DEBUG - Skipping active Jellyfin session file: /mnt/user/plex-media/TV Shows/Family Guy (1999) [tmdbid-1434]/Season 02/Family Guy - S02E03 - Der letzte Knall.mkv
21:05:08 - INFO - [FILTER] Skipped 2 active session(s)
21:05:34 - INFO - [FETCH] Jellyfin OnDeck: 67 items
21:05:34 - INFO - [FETCH] OnDeck: 275 items from 5 users
21:06:16 - INFO - [FETCH] Watchlist: 6 items
21:06:21 - INFO - [FETCH] Media: 643 total (643 already cached, 0 to cache)

Neither run logged any unmapped paths, [JELLYFIN] errors or "OnDeck data incomplete" warnings. exit_if_active_session is off here, so both sessions were skipped and the run carried on.

Jellyfin has no watchlist. With jellyfin_favorites_toggle (and
watchlist_toggle) on, unplayed favorite movies and the first
watchlist_episodes unplayed episodes of favorite series (without
specials) go through the regular watchlist path: tracker, retention,
move-back after watching. Retention counts from the first run that sees
a favorite, since Jellyfin has no favorited-at date. A failed favorites
request skips the array restore, like incomplete OnDeck data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@RogerSik

RogerSik commented Nov 4, 2026 •

Copy link
Copy Markdown
Author

Jellyfin don't have a watchlist. But have a favorites list. With the newest commit the favorites will be used like Plex is used with watchlist. Which is useful for movie caching.

For me its working fine on my Jellyfin instance.

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