fix: stop Jellyfin live TV sessions

close #1533
This commit is contained in:
edde746
2026-07-11 22:00:05 +02:00
parent 4443b76145
commit 2679745941
21 changed files with 277 additions and 20 deletions
@@ -311,6 +311,53 @@ void main() {
expect(body['IsPaused'], isTrue);
});
test('live playback reports preserve the same server session identity', () async {
final requests = <({String path, Map<String, dynamic> body})>[];
final scoped = JellyfinClient.forTesting(
connection: _conn(),
httpClient: MockClient((request) async {
requests.add((path: request.url.path, body: jsonDecode(request.body) as Map<String, dynamic>));
return http.Response('', 204);
}),
);
addTearDown(scoped.close);
await scoped.reportPlaybackStarted(
itemId: 'channel-1',
position: Duration.zero,
playSessionId: 'play-1',
liveStreamId: 'live-1',
mediaSourceId: 'source-1',
);
await scoped.reportPlaybackProgress(
itemId: 'channel-1',
position: const Duration(seconds: 10),
duration: Duration.zero,
playSessionId: 'play-1',
liveStreamId: 'live-1',
mediaSourceId: 'source-1',
);
await scoped.reportPlaybackStopped(
itemId: 'channel-1',
position: const Duration(seconds: 20),
playSessionId: 'play-1',
liveStreamId: 'live-1',
mediaSourceId: 'source-1',
);
expect(requests.map((request) => request.path), [
'/Sessions/Playing',
'/Sessions/Playing/Progress',
'/Sessions/Playing/Stopped',
]);
for (final request in requests) {
expect(request.body['ItemId'], 'channel-1');
expect(request.body['PlaySessionId'], 'play-1');
expect(request.body['LiveStreamId'], 'live-1');
expect(request.body['MediaSourceId'], 'source-1');
}
});
test('resolveDownload pins direct stream URL and subtitles to selected media source', () async {
final requests = <Uri>[];
String? playbackInfoBody;
@@ -1396,6 +1443,8 @@ void main() {
expect(body['EnableTranscoding'], isFalse);
expect(resolution, isNotNull);
expect(resolution!.playSessionId, 'live-session-1');
expect(resolution.mediaSourceId, 'source-1');
expect(resolution.liveStreamId, 'open-stream-1');
final uri = Uri.parse(resolution.url);
expect(uri.path, '/Videos/channel-1/stream');
expect(uri.queryParameters['Static'], 'true');
@@ -1407,6 +1456,38 @@ void main() {
expect(uri.queryParameters['api_key'], 'tok-abc');
});
test('live TV stream resolution recovers identity from a negotiated direct URL', () async {
final scoped = JellyfinClient.forTesting(
connection: _conn(),
httpClient: MockClient((request) async {
if (request.url.path == '/Items/channel-1/PlaybackInfo') {
return http.Response(
jsonEncode({
'MediaSources': [
{
'Container': 'ts',
'DirectStreamUrl':
'/Videos/channel-1/stream?MediaSourceId=source-url&LiveStreamId=live-url&PlaySessionId=play-url',
},
],
}),
200,
headers: {'content-type': 'application/json'},
);
}
return http.Response('{}', 404);
}),
);
addTearDown(scoped.close);
final resolution = await scoped.liveTv.resolveStreamUrl('channel-1');
expect(resolution, isNotNull);
expect(resolution!.playSessionId, 'play-url');
expect(resolution.mediaSourceId, 'source-url');
expect(resolution.liveStreamId, 'live-url');
});
test('buildTrickplayTileUrl wires width, sheet index, api_key, and DeviceId', () {
final url = client.buildTrickplayTileUrl('item-99', 320, 4);
final uri = Uri.parse(url);