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

+1 -17
View File
@@ -7,7 +7,7 @@ import 'package:sync_relay_core/core.dart';
import 'package:watch_together/src/jellyfin/accounts.dart';
import 'package:watch_together/src/jellyfin/format.dart';
import 'package:watch_together/src/jellyfin/sign_in.dart';
import 'package:watch_together/src/screens/home/host_tab.dart' show interimJellyfinLink, mediaSummary;
import 'package:watch_together/src/screens/home/host_tab.dart' show mediaSummary;
import 'package:watch_together/src/screens/home/media_picker.dart';
/// Captured Jellyfin 10.11 answers shared with packages/core's client tests.
@@ -342,22 +342,6 @@ void main() {
expect(m.subtitle, 'Black Summoner · S1E6');
expect(m.runtime, isNotNull);
});
test('INTERIM: the host plays a Jellyfin pick as its authorised stream link', () async {
final (accounts, _) = await linkedAccounts();
const m = JellyfinMedia(
serverId: fixtureServerId, baseUrl: 'https://example.invalid', itemId: 'abc', title: 't');
final url = Uri.parse(interimJellyfinLink(accounts, m));
expect(url.path, '/Videos/abc/stream');
expect(url.queryParameters['static'], 'true');
expect(url.queryParameters['api_key'], 'TEST_ACCESS_TOKEN');
expect(
() => interimJellyfinLink(
accounts, const JellyfinMedia(serverId: 'gone', baseUrl: 'x', itemId: 'i', title: 't')),
throwsFormatException);
accounts.markExpired(fixtureServerId);
expect(() => interimJellyfinLink(accounts, m), throwsFormatException);
});
});
group('format', () {