refactor: reduce requests made for playback

This commit is contained in:
edde746
2025-11-10 10:13:01 +01:00
parent 995f0d006d
commit 0c102ff3a3
3 changed files with 162 additions and 29 deletions
+119
View File
@@ -10,6 +10,7 @@ import '../models/plex_library.dart';
import '../models/plex_media_info.dart';
import '../models/plex_media_version.dart';
import '../models/plex_metadata.dart';
import '../models/plex_video_playback_data.dart';
import '../models/plex_sort.dart';
import '../utils/app_logger.dart';
@@ -639,6 +640,124 @@ class PlexClient {
return [];
}
/// Get consolidated video playback data (URL, media info, and versions) in a single API call
/// This method combines the functionality of getVideoUrl(), getMediaInfo(), and getMediaVersions()
/// to reduce redundant API calls during video playback initialization.
Future<PlexVideoPlaybackData> getVideoPlaybackData(
String ratingKey, {
int mediaIndex = 0,
}) async {
final response = await _dio.get('/library/metadata/$ratingKey');
final metadataJson = _getFirstMetadataJson(response);
String? videoUrl;
PlexMediaInfo? mediaInfo;
List<PlexMediaVersion> availableVersions = [];
if (metadataJson != null &&
metadataJson['Media'] != null &&
(metadataJson['Media'] as List).isNotEmpty) {
final mediaList = metadataJson['Media'] as List;
// Parse available media versions first
availableVersions = mediaList
.map(
(media) => PlexMediaVersion.fromJson(media as Map<String, dynamic>),
)
.toList();
// Ensure the requested index is valid
if (mediaIndex < 0 || mediaIndex >= mediaList.length) {
mediaIndex = 0;
}
final media = mediaList[mediaIndex];
if (media['Part'] != null && (media['Part'] as List).isNotEmpty) {
final part = media['Part'][0];
final partKey = part['key'] as String?;
if (partKey != null) {
// Get video URL
videoUrl = '${config.baseUrl}$partKey?X-Plex-Token=${config.token}';
// Parse streams (audio and subtitle tracks) for media info
final streams = part['Stream'] as List<dynamic>? ?? [];
final audioTracks = <PlexAudioTrack>[];
final subtitleTracks = <PlexSubtitleTrack>[];
for (var stream in streams) {
final streamType = stream['streamType'] as int?;
if (streamType == 2) {
// Audio track
audioTracks.add(
PlexAudioTrack(
id: stream['id'] as int,
index: stream['index'] as int?,
codec: stream['codec'] as String?,
language: stream['language'] as String?,
languageCode: stream['languageCode'] as String?,
title: stream['title'] as String?,
displayTitle: stream['displayTitle'] as String?,
channels: stream['channels'] as int?,
selected: stream['selected'] == 1,
),
);
} else if (streamType == 3) {
// Subtitle track
subtitleTracks.add(
PlexSubtitleTrack(
id: stream['id'] as int,
index: stream['index'] as int?,
codec: stream['codec'] as String?,
language: stream['language'] as String?,
languageCode: stream['languageCode'] as String?,
title: stream['title'] as String?,
displayTitle: stream['displayTitle'] as String?,
selected: stream['selected'] == 1,
forced: stream['forced'] == 1,
key: stream['key'] as String?,
),
);
}
}
// Parse chapters
final chapters = <PlexChapter>[];
if (metadataJson['Chapter'] != null) {
final chapterList = metadataJson['Chapter'] as List<dynamic>;
for (var chapter in chapterList) {
chapters.add(
PlexChapter(
id: chapter['id'] as int,
index: chapter['index'] as int?,
startTimeOffset: chapter['startTimeOffset'] as int?,
endTimeOffset: chapter['endTimeOffset'] as int?,
title: chapter['title'] as String?,
thumb: chapter['thumb'] as String?,
),
);
}
}
// Create media info
mediaInfo = PlexMediaInfo(
videoUrl: videoUrl,
audioTracks: audioTracks,
subtitleTracks: subtitleTracks,
chapters: chapters,
);
}
}
}
return PlexVideoPlaybackData(
videoUrl: videoUrl,
mediaInfo: mediaInfo,
availableVersions: availableVersions,
);
}
/// Get file information for a media item
Future<PlexFileInfo?> getFileInfo(String ratingKey) async {
try {
+30
View File
@@ -0,0 +1,30 @@
import 'plex_media_info.dart';
import 'plex_media_version.dart';
/// Consolidated data model containing all information needed for video playback.
/// This model combines data from multiple Plex API endpoints to reduce redundant requests.
class PlexVideoPlaybackData {
/// Direct video URL for playback
final String? videoUrl;
/// Media information including audio/subtitle tracks and chapters
final PlexMediaInfo? mediaInfo;
/// Available media versions/qualities for this content
final List<PlexMediaVersion> availableVersions;
PlexVideoPlaybackData({
required this.videoUrl,
required this.mediaInfo,
required this.availableVersions,
});
/// Returns true if this playback data has a valid video URL
bool get hasValidVideoUrl => videoUrl != null && videoUrl!.isNotEmpty;
/// Returns true if media info is available
bool get hasMediaInfo => mediaInfo != null;
/// Returns true if there are multiple media versions available
bool get hasMultipleVersions => availableVersions.length > 1;
}
+13 -29
View File
@@ -10,6 +10,7 @@ import 'package:provider/provider.dart';
import '../models/plex_media_version.dart';
import '../models/plex_metadata.dart';
import '../models/plex_user_profile.dart';
import '../models/plex_video_playback_data.dart';
import '../providers/playback_state_provider.dart';
import '../providers/plex_client_provider.dart';
import '../providers/settings_provider.dart';
@@ -181,9 +182,6 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> {
// Get the video URL and start playback
_startPlayback();
// Load available media versions
_loadMediaVersions();
// Set fullscreen mode and orientation based on rotation lock setting
if (mounted) {
try {
@@ -304,18 +302,22 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> {
throw Exception('No client available');
}
// Get the direct file URL from the server using the selected media index
final videoUrl = await client.getVideoUrl(
// Get consolidated playback data (URL, media info, and versions) in a single API call
final playbackData = await client.getVideoPlaybackData(
widget.metadata.ratingKey,
mediaIndex: widget.selectedMediaIndex,
);
if (videoUrl != null) {
// Fetch media info to check for external subtitle tracks
final mediaInfo = await client.getMediaInfo(
widget.metadata.ratingKey,
mediaIndex: widget.selectedMediaIndex,
);
if (playbackData.hasValidVideoUrl) {
final videoUrl = playbackData.videoUrl!;
final mediaInfo = playbackData.mediaInfo;
// Update available versions from the playback data
if (mounted) {
setState(() {
_availableVersions = playbackData.availableVersions;
});
}
// Build list of external subtitle tracks for media_kit
final externalSubtitles = <SubtitleTrack>[];
@@ -434,24 +436,6 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> {
}
}
/// Load available media versions for this item
Future<void> _loadMediaVersions() async {
try {
final clientProvider = context.plexClient;
final client = clientProvider.client;
if (client == null) return;
final versions = await client.getMediaVersions(widget.metadata.ratingKey);
if (mounted) {
setState(() {
_availableVersions = versions;
});
}
} catch (e) {
appLogger.e('Error loading media versions: $e');
}
}
/// Cycle through BoxFit modes: contain → cover → fill → contain (for button)
void _cycleBoxFitMode() {
setState(() {