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

@@ -235,6 +235,22 @@ void main() {
expect(episodeAfter(eps, eps.last.id), isNull);
expect(episodeAfter(eps, 'nope'), isNull);
});
test('adjacentEpisodes: previous mirrors next across seasons and copies', () async {
http.on('GET', '/Shows/550eff23b37ec61082c988678ccc214c/Episodes', fixture: 'episodes');
final eps = await client.episodes('550eff23b37ec61082c988678ccc214c');
final s3e2 = eps.firstWhere((e) => e.episodeLabel == 'S3E2');
final around = await client.adjacentEpisodes(s3e2);
// S3E1 exists twice: the predecessor is the copy right before S3E2.
expect(around.previous!.episodeLabel, 'S3E1');
expect(around.next!.episodeLabel, 'S3E3');
final s3e1 = eps.lastWhere((e) => e.episodeLabel == 'S3E1');
expect(episodeBefore(eps, s3e1.id)!.episodeLabel, 'S2E12');
expect(episodeBefore(eps, eps.first.id), isNull);
expect(episodeBefore(eps, 'nope'), isNull);
expect((await client.adjacentEpisodes(const JellyfinItem(id: 'x', name: 'm', type: 'Movie'))).next,
isNull);
});
});
group('images', () {