Jellyfin sync only ingests played movies — shows never sync #30

Closed
opened 2026-10-17 00:25:48 +00:00 by agent-pi · 1 comment
Collaborator

TV is wired end-to-end except at the one gate that matters: the Jellyfin delta sync only requests played movies, so shows reach the ledger solely through a Trakt export import. A user who connects Jellyfin and skips the Trakt bootstrap gets an empty /shows page and no TV recommendations, because the engine has no show seeds to fan out from.

Everything downstream already handles shows: the media table carries a show media type, the engine ranks core_tv and discover_tv categories, the TMDB client speaks /tv, and the browse UI has /shows and /discover pages. The sync is the only movie-only piece.

This is Phase 1 scope, not new work: the spec's Phase 1 sync bullet covers the played-items ledger generally, and the movie/TV browse lists were both Phase 1 deliverables. Episode-level tracking remains deliberately deferred (see #25); shows here means series-level, the same resolution a Trakt import produces.

Spec §8.1, §14 (Phase 1).

TV is wired end-to-end except at the one gate that matters: the Jellyfin delta sync only requests played movies, so shows reach the ledger solely through a Trakt export import. A user who connects Jellyfin and skips the Trakt bootstrap gets an empty /shows page and no TV recommendations, because the engine has no show seeds to fan out from. Everything downstream already handles shows: the media table carries a show media type, the engine ranks core_tv and discover_tv categories, the TMDB client speaks /tv, and the browse UI has /shows and /discover pages. The sync is the only movie-only piece. This is Phase 1 scope, not new work: the spec's Phase 1 sync bullet covers the played-items ledger generally, and the movie/TV browse lists were both Phase 1 deliverables. Episode-level tracking remains deliberately deferred (see #25); shows here means series-level, the same resolution a Trakt import produces. Spec §8.1, §14 (Phase 1).
Author
Collaborator

Implementation recon:

  • The client's item mapper already maps Series to the canonical "show" type, but every played-items fetch filters on Movie only, so the sync never sees shows.
  • The movie sync walks the user's entire played list every cycle (paged, DatePlayed desc) and relies on the ledger's (user, media, watched_at, source) uniqueness for re-sync dedupe — no watermark machinery exists, and this change adds none.
  • Trakt keys a show when any of tmdb/imdb/trakt/tvdb is present. Jellyfin series will key on any of tmdb/imdb/tvdb (trakt ids don't exist there), so a Trakt-imported show and its Jellyfin-synced twin always land on one media row.

Plan of attack:

  1. services/jellyfin.py: JellyfinEpisode and JellyfinSeries dataclasses; fetch_played_episodes (paged, IncludeItemTypes=Episode, IsPlayed, DatePlayed desc); fetch_series (batched ids= lookup carrying ProviderIds, Genres, Overview, and UserData for the rating).
  2. services/sync.py: sync_jellyfin_shows — walk played episodes, group by parent series, fetch each distinct series once, upsert the show (keyable on tmdb/imdb/tvdb), mirror the series rating, append one watch event per played episode (source ref = episode item id, timestamp = that episode's LastPlayedDate). Series whose metadata is missing or unkeyable are skipped without failing the pass.
  3. worker.py: the jellyfin_sync handler runs the movies pass and the shows pass, recording one shared sync result.

TDD throughout: failing tests per slice (test_jellyfin.py, test_sync.py, worker tests), then the implementation, then the full uv run nox suite before pushing.

Branch: feat/issue-30-jellyfin-show-sync

Commits:

  • :sparkles: feat: fetch played episodes and series from the Jellyfin client
  • :sparkles: feat: sync played Jellyfin shows into the ledger

PR: POST /repos/marvin8/xique/pulls (head feat/issue-30-jellyfin-show-sync, base main, Closes #30).

Implementation recon: - The client's item mapper already maps `Series` to the canonical `"show"` type, but every played-items fetch filters on `Movie` only, so the sync never sees shows. - The movie sync walks the user's **entire** played list every cycle (paged, `DatePlayed` desc) and relies on the ledger's `(user, media, watched_at, source)` uniqueness for re-sync dedupe — no watermark machinery exists, and this change adds none. - Trakt keys a show when any of tmdb/imdb/trakt/tvdb is present. Jellyfin series will key on any of tmdb/imdb/tvdb (trakt ids don't exist there), so a Trakt-imported show and its Jellyfin-synced twin always land on one media row. Plan of attack: 1. `services/jellyfin.py`: `JellyfinEpisode` and `JellyfinSeries` dataclasses; `fetch_played_episodes` (paged, `IncludeItemTypes=Episode`, `IsPlayed`, `DatePlayed` desc); `fetch_series` (batched `ids=` lookup carrying ProviderIds, Genres, Overview, and UserData for the rating). 2. `services/sync.py`: `sync_jellyfin_shows` — walk played episodes, group by parent series, fetch each distinct series once, upsert the show (keyable on tmdb/imdb/tvdb), mirror the series rating, append **one watch event per played episode** (source ref = episode item id, timestamp = that episode's `LastPlayedDate`). Series whose metadata is missing or unkeyable are skipped without failing the pass. 3. `worker.py`: the `jellyfin_sync` handler runs the movies pass and the shows pass, recording one shared sync result. TDD throughout: failing tests per slice (`test_jellyfin.py`, `test_sync.py`, worker tests), then the implementation, then the full `uv run nox` suite before pushing. Branch: `feat/issue-30-jellyfin-show-sync` Commits: - `:sparkles: feat: fetch played episodes and series from the Jellyfin client` - `:sparkles: feat: sync played Jellyfin shows into the ledger` PR: `POST /repos/marvin8/xique/pulls` (head `feat/issue-30-jellyfin-show-sync`, base `main`, `Closes #30`).
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
marvin8/xique#30
No description provided.