Introduces shared seams for paginated views, D-pad reorder, media control routing, async singletons and the device method channel, then points the open-coded copies at them. Also removes unused models and duplicated provider/server plumbing, folds the twice-implemented artifact store in the server, and factors the repeated Flutter toolchain prologue in CI into a composite action.
77 lines
3.5 KiB
Dart
77 lines
3.5 KiB
Dart
part of '../../jellyfin_client.dart';
|
|
|
|
mixin _JellyfinFileInfoMethods on _JellyfinClientInternals {
|
|
@override
|
|
Future<MediaFileInfo?> getFileInfo(MediaItem item) async {
|
|
// Lightweight browse responses omit `MediaSources`; detail and some cached
|
|
// entries still include them, otherwise fetch the full item on demand.
|
|
final raw = item.raw is Map<String, dynamic> ? item.raw as Map<String, dynamic> : null;
|
|
Map<String, dynamic>? itemJson = raw;
|
|
if (itemJson == null || itemJson['MediaSources'] is! List) {
|
|
final fresh = await fetchItem(item.id);
|
|
itemJson = fresh?.raw is Map<String, dynamic> ? fresh!.raw as Map<String, dynamic> : null;
|
|
}
|
|
if (itemJson == null) return null;
|
|
return _buildFileInfoFromJellyfinItem(itemJson);
|
|
}
|
|
|
|
MediaFileInfo? _buildFileInfoFromJellyfinItem(Map<String, dynamic> json) {
|
|
final sources = json['MediaSources'];
|
|
if (sources is! List || sources.isEmpty) return null;
|
|
final source = sources.first;
|
|
if (source is! Map<String, dynamic>) return null;
|
|
|
|
final parsed = walkStreams(source['MediaStreams'] as List?, const JellyfinFileInfoStreamReader());
|
|
final videoStream = parsed.videoStream;
|
|
final audioStream = parsed.audioStream;
|
|
final audioTracks = parsed.audioTracks;
|
|
final subtitleTracks = parsed.subtitleTracks;
|
|
|
|
final width = flexibleInt(videoStream?['Width']);
|
|
final height = flexibleInt(videoStream?['Height']);
|
|
final aspectRatioString = videoStream?['AspectRatio'] as String?;
|
|
double? aspectRatio;
|
|
if (aspectRatioString != null && aspectRatioString.contains(':')) {
|
|
final parts = aspectRatioString.split(':');
|
|
final num = flexibleDouble(parts.first);
|
|
final den = flexibleDouble(parts[1]);
|
|
if (num != null && den != null && den != 0) aspectRatio = num / den;
|
|
}
|
|
aspectRatio ??= (width != null && height != null && height != 0) ? width / height : null;
|
|
|
|
final durationMs = jellyfinTicksToMs(flexibleInt(source['RunTimeTicks']));
|
|
|
|
final bitrateBps = flexibleInt(source['Bitrate']);
|
|
final videoBitrateBps = flexibleInt(videoStream?['BitRate']);
|
|
|
|
return MediaFileInfo(
|
|
container: source['Container'] as String?,
|
|
videoCodec: videoStream?['Codec'] as String?,
|
|
videoResolution: resolutionLabelFromDimensions(width, height),
|
|
videoFrameRate: videoStream?['RealFrameRate']?.toString() ?? videoStream?['AverageFrameRate']?.toString(),
|
|
videoProfile: videoStream?['Profile'] as String?,
|
|
width: width,
|
|
height: height,
|
|
aspectRatio: aspectRatio,
|
|
// Plex stores bitrate as kbps; Jellyfin returns bps. Normalise to kbps.
|
|
bitrate: bitrateKbpsFromBps(bitrateBps),
|
|
duration: durationMs,
|
|
audioCodec: audioStream?['Codec'] as String?,
|
|
audioProfile: audioStream?['Profile'] as String?,
|
|
audioChannels: flexibleInt(audioStream?['Channels']),
|
|
filePath: source['Path'] as String?,
|
|
fileSize: flexibleInt(source['Size']),
|
|
colorSpace: videoStream?['ColorSpace'] as String?,
|
|
colorRange: videoStream?['ColorRange'] as String?,
|
|
colorPrimaries: videoStream?['ColorPrimaries'] as String?,
|
|
chromaSubsampling: null,
|
|
frameRate: flexibleDouble(videoStream?['RealFrameRate']) ?? flexibleDouble(videoStream?['AverageFrameRate']),
|
|
bitDepth: flexibleInt(videoStream?['BitDepth']),
|
|
videoBitrate: bitrateKbpsFromBps(videoBitrateBps),
|
|
audioChannelLayout: audioStream?['ChannelLayout'] as String?,
|
|
audioTracks: audioTracks,
|
|
subtitleTracks: subtitleTracks,
|
|
);
|
|
}
|
|
}
|