Jellyfin end to end: per-viewer playback, progress, episodes, swap browser

- Host/swap send refs only; pasted Jellyfin links become refs; the host
  hands the relay its own app-obtained credential.
- Each viewer resolves with their own account (token in a header) or
  via the relay, else a clear blocked card with Link account.
- Per-account progress reporting (Playing/Progress/Stopped), 401 marks
  the account expired.
- Previous/next episode in the controls (< > / Shift+P/N), floating
  Next episode pill near the end, auto-advance countdown (one
  designated swapper), track carry-over per series.
- Swap panel uses the shared MediaPicker; browser location remembered
  per server and opens on the current series.
- Fix: states are applied one at a time (a stale pause could land
  after a newer play).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-07 02:38:24 +03:00
1 parent 773a04369e
commit ae4edc29e8
33 files changed
+3541 -175

No files matched your search

@@ -426,6 +426,16 @@ class JellyfinClient {
await episodes(seriesId, withMediaStreams: withMediaStreams), episode.id);
}
/// The episodes before and after [episode] in its series (one
/// `/Shows/{id}/Episodes` call; see [episodeBefore] / [episodeAfter]).
Future<({JellyfinItem? previous, JellyfinItem? next})> adjacentEpisodes(
JellyfinItem episode) async {
final seriesId = episode.seriesId;
if (seriesId == null) return (previous: null, next: null);
final list = await episodes(seriesId);
return (previous: episodeBefore(list, episode.id), next: episodeAfter(list, episode.id));
}
// -------------------------------------------------------------- images
/// Image URL for caching and display. Images need no token on Jellyfin,
@@ -594,3 +604,20 @@ JellyfinItem? episodeAfter(List<JellyfinItem> episodes, String episodeId) {
}
return null;
}
/// The predecessor of [episodeId] in [episodes], the mirror of
/// [episodeAfter]: crossing seasons, skipping other copies of the same
/// episode. Null when it is first or not in the list.
JellyfinItem? episodeBefore(List<JellyfinItem> episodes, String episodeId) {
final at = episodes.indexWhere((e) => e.id == episodeId);
if (at < 0) return null;
final cur = episodes[at];
for (var i = at - 1; i >= 0; i--) {
final e = episodes[i];
final sameEpisode = cur.indexNumber != null &&
e.indexNumber == cur.indexNumber &&
e.parentIndexNumber == cur.parentIndexNumber;
if (!sameEpisode) return e;
}
return null;
}