fix(jellyfin): resolve versions on demand
This commit is contained in:
@@ -6,6 +6,7 @@ import '../media/media_server_client.dart';
|
||||
import '../media/media_version.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../utils/dialogs.dart';
|
||||
import '../utils/media_version_resolver.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
|
||||
/// Configuration for download version selection, threaded through the queue pipeline.
|
||||
@@ -42,8 +43,9 @@ Future<DownloadVersionConfig?> resolveDownloadVersion(
|
||||
final kind = metadata.kind;
|
||||
|
||||
if (kind == MediaKind.movie || kind == MediaKind.episode) {
|
||||
final versions = metadata.mediaVersions ?? fallbackVersions;
|
||||
if (versions != null && versions.length > 1) {
|
||||
final versions = await resolveMediaVersions(metadata, client, fallbackVersions: fallbackVersions);
|
||||
if (!context.mounted) return null;
|
||||
if (versions.length > 1) {
|
||||
final selectedIndex = await showVersionPickerDialog(context, versions, t.downloads.selectVersion);
|
||||
if (selectedIndex == null || !context.mounted) return null;
|
||||
return DownloadVersionConfig(mediaIndex: selectedIndex);
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import '../media/media_item.dart';
|
||||
import '../media/media_server_client.dart';
|
||||
import '../media/media_version.dart';
|
||||
import 'app_logger.dart';
|
||||
|
||||
List<MediaVersion>? _nonEmptyVersions(List<MediaVersion>? versions) {
|
||||
return versions == null || versions.isEmpty ? null : versions;
|
||||
}
|
||||
|
||||
/// Resolve media versions for item-level actions without making every browse
|
||||
/// row carry heavy backend-specific media-source payloads.
|
||||
Future<List<MediaVersion>> resolveMediaVersions(
|
||||
MediaItem metadata,
|
||||
MediaServerClient client, {
|
||||
List<MediaVersion>? fallbackVersions,
|
||||
}) async {
|
||||
final inlineVersions = _nonEmptyVersions(metadata.mediaVersions);
|
||||
if (inlineVersions != null) return inlineVersions;
|
||||
|
||||
final fallback = _nonEmptyVersions(fallbackVersions);
|
||||
if (fallback != null) return fallback;
|
||||
|
||||
try {
|
||||
final fullMetadata = await client.fetchItem(metadata.id);
|
||||
return _nonEmptyVersions(fullMetadata?.mediaVersions) ?? const <MediaVersion>[];
|
||||
} catch (e, st) {
|
||||
appLogger.w(
|
||||
'Failed to resolve media versions for ${metadata.backend.id} item ${metadata.id}',
|
||||
error: e,
|
||||
stackTrace: st,
|
||||
);
|
||||
return const <MediaVersion>[];
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import '../models/transcode_quality_preset.dart';
|
||||
import '../utils/download_version_utils.dart';
|
||||
import '../utils/download_utils.dart';
|
||||
import '../utils/quality_preset_labels.dart';
|
||||
import '../utils/media_version_resolver.dart';
|
||||
import '../utils/global_key_utils.dart';
|
||||
import '../providers/download_provider.dart';
|
||||
import '../providers/multi_server_provider.dart';
|
||||
@@ -860,13 +861,12 @@ class MediaContextMenuState extends State<MediaContextMenu> {
|
||||
|
||||
Future<bool> _handlePlayVersion(BuildContext context) async {
|
||||
final item = _mediaItem!;
|
||||
final client = context.tryGetMediaClientForServer(_itemServerId);
|
||||
// Same flag the in-player Version & Quality sheet reads — keeps both
|
||||
// surfaces honest about what the active backend can actually do.
|
||||
final canTranscode = _itemServerId == null
|
||||
? false
|
||||
: (context.read<MultiServerProvider>().getClientForServer(_itemServerId!)?.capabilities.videoTranscoding ??
|
||||
false);
|
||||
final versions = item.mediaVersions ?? const [];
|
||||
final canTranscode = client?.capabilities.videoTranscoding ?? false;
|
||||
final versions = client == null ? item.mediaVersions ?? const [] : await resolveMediaVersions(item, client);
|
||||
if (!context.mounted) return false;
|
||||
|
||||
int selectedVersionIndex = 0;
|
||||
if (versions.length > 1) {
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:plezy/connection/connection.dart';
|
||||
import 'package:plezy/database/app_database.dart';
|
||||
import 'package:plezy/media/media_item.dart';
|
||||
import 'package:plezy/media/media_kind.dart';
|
||||
import 'package:plezy/media/media_version.dart';
|
||||
import 'package:plezy/services/jellyfin_api_cache.dart';
|
||||
import 'package:plezy/services/jellyfin_client.dart';
|
||||
import 'package:plezy/utils/media_version_resolver.dart';
|
||||
|
||||
JellyfinConnection _conn() => JellyfinConnection(
|
||||
id: 'srv-1/user-1',
|
||||
baseUrl: 'https://jf.example.com',
|
||||
serverName: 'Home',
|
||||
serverMachineId: 'srv-1',
|
||||
userId: 'user-1',
|
||||
userName: 'edde',
|
||||
accessToken: 'tok-abc',
|
||||
deviceId: 'dev-xyz',
|
||||
createdAt: DateTime.fromMillisecondsSinceEpoch(0),
|
||||
);
|
||||
|
||||
MediaItem _item({List<MediaVersion>? mediaVersions}) => MediaItem.jellyfin(
|
||||
id: 'movie-1',
|
||||
kind: MediaKind.movie,
|
||||
title: 'Movie',
|
||||
mediaVersions: mediaVersions,
|
||||
serverId: 'srv-1',
|
||||
serverName: 'Home',
|
||||
);
|
||||
|
||||
MediaVersion _version(String id) => MediaVersion(id: id, videoResolution: '1080', videoCodec: 'h264');
|
||||
|
||||
void main() {
|
||||
late AppDatabase db;
|
||||
|
||||
setUp(() {
|
||||
db = AppDatabase.forTesting(NativeDatabase.memory());
|
||||
JellyfinApiCache.initialize(db);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
group('resolveMediaVersions', () {
|
||||
test('fetches full metadata when the browse row has no versions', () async {
|
||||
final requests = <Uri>[];
|
||||
final client = JellyfinClient.forTesting(
|
||||
connection: _conn(),
|
||||
httpClient: MockClient((request) async {
|
||||
requests.add(request.url);
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'Id': 'movie-1',
|
||||
'Name': 'Movie',
|
||||
'Type': 'Movie',
|
||||
'MediaSources': [
|
||||
{
|
||||
'Id': 'src-1080',
|
||||
'Container': 'mkv',
|
||||
'MediaStreams': [
|
||||
{'Type': 'Video', 'Codec': 'h264', 'Height': 1080, 'Width': 1920},
|
||||
],
|
||||
},
|
||||
{
|
||||
'Id': 'src-4k',
|
||||
'Container': 'mkv',
|
||||
'MediaStreams': [
|
||||
{'Type': 'Video', 'Codec': 'hevc', 'Height': 2160, 'Width': 3840},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
200,
|
||||
headers: {'content-type': 'application/json'},
|
||||
);
|
||||
}),
|
||||
);
|
||||
addTearDown(client.close);
|
||||
|
||||
final versions = await resolveMediaVersions(_item(), client);
|
||||
|
||||
expect(versions.map((version) => version.id), ['src-1080', 'src-4k']);
|
||||
expect(requests, hasLength(1));
|
||||
expect(requests.single.path, '/Users/user-1/Items/movie-1');
|
||||
expect(requests.single.queryParameters['Fields'], contains('MediaSources'));
|
||||
});
|
||||
|
||||
test('uses inline versions without fetching full metadata', () async {
|
||||
var requested = false;
|
||||
final client = JellyfinClient.forTesting(
|
||||
connection: _conn(),
|
||||
httpClient: MockClient((request) async {
|
||||
requested = true;
|
||||
return http.Response('unexpected', 500);
|
||||
}),
|
||||
);
|
||||
addTearDown(client.close);
|
||||
final inlineVersions = [_version('inline')];
|
||||
|
||||
final versions = await resolveMediaVersions(_item(mediaVersions: inlineVersions), client);
|
||||
|
||||
expect(versions, same(inlineVersions));
|
||||
expect(requested, isFalse);
|
||||
});
|
||||
|
||||
test('uses fallback versions without fetching full metadata', () async {
|
||||
var requested = false;
|
||||
final client = JellyfinClient.forTesting(
|
||||
connection: _conn(),
|
||||
httpClient: MockClient((request) async {
|
||||
requested = true;
|
||||
return http.Response('unexpected', 500);
|
||||
}),
|
||||
);
|
||||
addTearDown(client.close);
|
||||
final fallbackVersions = [_version('fallback')];
|
||||
|
||||
final versions = await resolveMediaVersions(_item(), client, fallbackVersions: fallbackVersions);
|
||||
|
||||
expect(versions, same(fallbackVersions));
|
||||
expect(requested, isFalse);
|
||||
});
|
||||
|
||||
test('returns an empty list when detail lookup fails', () async {
|
||||
final client = JellyfinClient.forTesting(
|
||||
connection: _conn(),
|
||||
httpClient: MockClient((request) async => http.Response('server error', 500)),
|
||||
);
|
||||
addTearDown(client.close);
|
||||
|
||||
final versions = await resolveMediaVersions(_item(), client);
|
||||
|
||||
expect(versions, isEmpty);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user