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

@@ -36,6 +36,9 @@ class SyncClient {
String token = '';
String memberId = '';
String? mediaUrl;
/// This member's `/j/...` URL from `welcome` (Jellyfin media the server
/// relays); private like [token].
String? relayUrl;
StateBroadcast? lastState;
Completer<WelcomeMessage>? _pendingWelcome;
@@ -89,6 +92,7 @@ class SyncClient {
token = welcome.token;
memberId = welcome.memberId;
mediaUrl = welcome.mediaUrl;
relayUrl = welcome.relayUrl;
if (welcome.state != null) {
try {
lastState = StateBroadcast.fromJson(welcome.state!);
@@ -214,6 +218,7 @@ class SyncClient {
required int eventId,
double duration = 0,
int mediaId = 0,
Map<String, bool>? jf,
}) {
final safeBuffered = buffered.isNaN || buffered < 0 ? 0.0 : buffered;
_send(
@@ -223,6 +228,7 @@ class SyncClient {
eventId: eventId,
duration: duration.isFinite && duration > 0 ? duration : 0,
mediaId: mediaId,
jf: jf,
).toJson(),
);
}
@@ -254,6 +260,7 @@ class SyncClient {
token = '';
memberId = '';
mediaUrl = null;
relayUrl = null;
lastState = null;
}