perf: reduce redundant metadata API requests

This commit is contained in:
edde746
2026-02-23 11:43:42 +01:00
parent 1eb88e23f5
commit 4095cc27e1
7 changed files with 88 additions and 39 deletions
+10 -7
View File
@@ -657,15 +657,18 @@ class DownloadProvider extends ChangeNotifier {
}
// Fetch full metadata to get year, summary, clearLogo
// The metadata from getChildren() is summarized and missing these fields
// The metadata from getChildren() is summarized and missing these fields.
// If metadata already has summary, it's already full (e.g., from detail screen).
PlexMetadata metadataToStore = metadata;
try {
final fullMetadata = await client.getMetadataWithImages(metadata.ratingKey);
if (fullMetadata != null) {
metadataToStore = fullMetadata.copyWith(serverId: metadata.serverId, serverName: metadata.serverName);
if (metadata.summary == null) {
try {
final fullMetadata = await client.getMetadataWithImages(metadata.ratingKey);
if (fullMetadata != null) {
metadataToStore = fullMetadata.copyWith(serverId: metadata.serverId, serverName: metadata.serverName);
}
} catch (e) {
appLogger.w('Failed to fetch full metadata for ${metadata.ratingKey}, using partial', error: e);
}
} catch (e) {
appLogger.w('Failed to fetch full metadata for ${metadata.ratingKey}, using partial', error: e);
}
// For episodes, also fetch and store show and season metadata for offline display
+5
View File
@@ -24,6 +24,7 @@ import '../utils/plex_image_helper.dart';
import '../../services/plex_client.dart';
import '../services/plex_api_cache.dart';
import '../models/plex_metadata.dart';
import '../models/plex_video_playback_data.dart';
import '../utils/content_utils.dart';
import '../utils/rating_utils.dart';
import '../models/download_models.dart';
@@ -65,6 +66,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> with WatchStateAw
bool _isLoadingSeasons = false;
PlexMetadata? _fullMetadata;
PlexMetadata? _onDeckEpisode;
PlexVideoPlaybackData? _playbackData;
bool _isLoadingMetadata = true;
List<PlexMetadata>? _extras;
late final ScrollController _scrollController;
@@ -360,6 +362,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> with WatchStateAw
metadata: metadata,
isOffline: widget.isOffline,
onRefresh: _loadFullMetadata,
playbackData: _playbackData,
);
}
}
@@ -966,6 +969,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> with WatchStateAw
final result = await client.getMetadataWithImagesAndOnDeck(widget.metadata.ratingKey);
final metadata = result['metadata'] as PlexMetadata?;
final onDeckEpisode = result['onDeckEpisode'] as PlexMetadata?;
final playbackData = result['playbackData'] as PlexVideoPlaybackData?;
if (!mounted) return;
@@ -983,6 +987,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> with WatchStateAw
setState(() {
_fullMetadata = metadataWithServerId;
_onDeckEpisode = onDeckWithServerId;
_playbackData = playbackData;
_isLoadingMetadata = false;
});
+10
View File
@@ -73,6 +73,16 @@ class _MetadataEditScreenState extends State<MetadataEditScreen> {
Future<void> _loadMetadata() async {
try {
// If the passed metadata already has full fields (e.g., from detail screen),
// use it directly instead of re-fetching. We check both summary and
// librarySectionID since the edit screen needs both for display and save.
if (widget.metadata.summary != null && widget.metadata.librarySectionID != null) {
_fullMetadata = widget.metadata;
_initFieldsFromMetadata(widget.metadata);
setState(() => _isLoading = false);
return;
}
final meta = await _client.getMetadataWithImages(widget.metadata.ratingKey);
if (!mounted) return;
if (meta != null) {
+4
View File
@@ -18,6 +18,7 @@ import '../models/livetv_channel.dart';
import '../services/plex_api_cache.dart';
import '../models/plex_media_version.dart';
import '../models/plex_metadata.dart';
import '../models/plex_video_playback_data.dart';
import '../utils/content_utils.dart';
import '../models/plex_media_info.dart';
import '../providers/download_provider.dart';
@@ -71,6 +72,7 @@ class VideoPlayerScreen extends StatefulWidget {
final SubtitleTrack? preferredSubtitleTrack;
final int selectedMediaIndex;
final bool isOffline;
final PlexVideoPlaybackData? playbackData;
// Live TV fields
final bool isLive;
@@ -90,6 +92,7 @@ class VideoPlayerScreen extends StatefulWidget {
this.preferredSubtitleTrack,
this.selectedMediaIndex = 0,
this.isOffline = false,
this.playbackData,
this.isLive = false,
this.liveChannelName,
this.liveStreamUrl,
@@ -930,6 +933,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
metadata: widget.metadata,
selectedMediaIndex: widget.selectedMediaIndex,
preferOffline: true, // Use downloaded file if available
playbackData: widget.playbackData,
);
}
@@ -1,6 +1,7 @@
import 'plex_client.dart';
import '../models/plex_media_info.dart';
import '../models/plex_metadata.dart';
import '../models/plex_video_playback_data.dart';
import '../models/download_models.dart';
import '../mpv/mpv.dart';
import '../utils/app_logger.dart';
@@ -75,10 +76,12 @@ class PlaybackInitializationService {
///
/// Returns a PlaybackInitializationResult with video URL and available versions
/// If [preferOffline] is true and offline content is available, uses local file
/// If [playbackData] is provided, skips the network call to fetch it again.
Future<PlaybackInitializationResult> getPlaybackData({
required PlexMetadata metadata,
required int selectedMediaIndex,
bool preferOffline = false,
PlexVideoPlaybackData? playbackData,
}) async {
try {
// Check for offline content first if preferOffline is enabled
@@ -94,16 +97,17 @@ class PlaybackInitializationService {
// For offline playback, we still need to fetch media info for subtitles
// but use the local file path for video
try {
final playbackData = await client.getVideoPlaybackData(metadata.ratingKey, mediaIndex: selectedMediaIndex);
final data =
playbackData ?? await client.getVideoPlaybackData(metadata.ratingKey, mediaIndex: selectedMediaIndex);
// Build list of external subtitle tracks
final externalSubtitles = _buildExternalSubtitles(playbackData.mediaInfo);
final externalSubtitles = _buildExternalSubtitles(data.mediaInfo);
// Return result with local file path
return PlaybackInitializationResult(
availableVersions: playbackData.availableVersions,
availableVersions: data.availableVersions,
videoUrl: _formatVideoUrl(offlineVideoPath),
mediaInfo: playbackData.mediaInfo,
mediaInfo: data.mediaInfo,
externalSubtitles: externalSubtitles,
isOffline: true,
);
@@ -120,21 +124,22 @@ class PlaybackInitializationService {
}
}
// Fall back to network streaming
final playbackData = await client.getVideoPlaybackData(metadata.ratingKey, mediaIndex: selectedMediaIndex);
// Use pre-parsed data or fall back to network streaming
final data =
playbackData ?? await client.getVideoPlaybackData(metadata.ratingKey, mediaIndex: selectedMediaIndex);
if (!playbackData.hasValidVideoUrl) {
if (!data.hasValidVideoUrl) {
throw PlaybackException(t.messages.fileInfoNotAvailable);
}
// Build list of external subtitle tracks
final externalSubtitles = _buildExternalSubtitles(playbackData.mediaInfo);
final externalSubtitles = _buildExternalSubtitles(data.mediaInfo);
// Return result with available versions and video URL
return PlaybackInitializationResult(
availableVersions: playbackData.availableVersions,
videoUrl: playbackData.videoUrl,
mediaInfo: playbackData.mediaInfo,
availableVersions: data.availableVersions,
videoUrl: data.videoUrl,
mediaInfo: data.mediaInfo,
externalSubtitles: externalSubtitles,
isOffline: false,
);
+32 -21
View File
@@ -460,7 +460,9 @@ class PlexClient {
),
parseCache: (cachedData) {
final metadata = _parseMetadataWithImagesFromCachedResponse(cachedData);
return {'metadata': metadata, 'onDeckEpisode': null};
final firstMetadata = PlexCacheParser.extractFirstMetadata(cachedData);
final playbackData = parseVideoPlaybackDataFromJson(firstMetadata);
return {'metadata': metadata, 'onDeckEpisode': null, 'playbackData': playbackData};
},
parseResponse: (response) {
PlexMetadata? metadata;
@@ -485,10 +487,13 @@ class PlexClient {
}
}
return {'metadata': metadata, 'onDeckEpisode': onDeckEpisode};
// Parse playback data from the same response — zero extra network cost
final playbackData = parseVideoPlaybackDataFromJson(metadataJson);
return {'metadata': metadata, 'onDeckEpisode': onDeckEpisode, 'playbackData': playbackData};
},
) ??
{'metadata': null, 'onDeckEpisode': null};
{'metadata': null, 'onDeckEpisode': null, 'playbackData': null};
}
/// Get metadata by rating key with images (includes clearLogo)
@@ -934,24 +939,10 @@ class PlexClient {
);
}
/// Get consolidated video playback data (URL, media info, versions, and markers) in a single API call.
/// This is the primary method for playback initialization.
/// Uses cache for offline mode support and network fallback.
Future<PlexVideoPlaybackData> getVideoPlaybackData(String ratingKey, {int mediaIndex = 0}) async {
Map<String, dynamic>? data;
try {
data = await _fetchWithCacheFirst<Map<String, dynamic>>(
cacheKey: '/library/metadata/$ratingKey',
networkCall: () =>
_dio.get('/library/metadata/$ratingKey', queryParameters: {'includeMarkers': 1, 'includeChapters': 1}),
parseCache: (cached) => cached as Map<String, dynamic>?,
parseResponse: (response) => response.data as Map<String, dynamic>?,
);
} catch (_) {
// Gracefully degrade: return empty playback data on total failure
}
final metadataJson = _getFirstMetadataJsonFromData(data);
/// Parse video playback data from raw metadata JSON (no network call).
/// Used by [getVideoPlaybackData] and [getMetadataWithImagesAndOnDeck] to
/// avoid redundant fetches when the response is already available.
PlexVideoPlaybackData parseVideoPlaybackDataFromJson(Map<String, dynamic>? metadataJson, {int mediaIndex = 0}) {
String? videoUrl;
PlexMediaInfo? mediaInfo;
List<PlexMediaVersion> availableVersions = [];
@@ -1004,6 +995,26 @@ class PlexClient {
);
}
/// Get consolidated video playback data (URL, media info, versions, and markers) in a single API call.
/// This is the primary method for playback initialization.
/// Uses cache for offline mode support and network fallback.
Future<PlexVideoPlaybackData> getVideoPlaybackData(String ratingKey, {int mediaIndex = 0}) async {
Map<String, dynamic>? data;
try {
data = await _fetchWithCacheFirst<Map<String, dynamic>>(
cacheKey: '/library/metadata/$ratingKey',
networkCall: () =>
_dio.get('/library/metadata/$ratingKey', queryParameters: {'includeMarkers': 1, 'includeChapters': 1}),
parseCache: (cached) => cached as Map<String, dynamic>?,
parseResponse: (response) => response.data as Map<String, dynamic>?,
);
} catch (_) {
// Gracefully degrade: return empty playback data on total failure
}
final metadataJson = _getFirstMetadataJsonFromData(data);
return parseVideoPlaybackDataFromJson(metadataJson, mediaIndex: mediaIndex);
}
/// Get file information for a media item
/// Uses cache for offline mode support and network fallback.
Future<PlexFileInfo?> getFileInfo(String ratingKey) async {
+11
View File
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
import '../mpv/mpv.dart';
import '../models/plex_metadata.dart';
import '../models/plex_video_playback_data.dart';
import '../providers/download_provider.dart';
import '../screens/video_player_screen.dart';
import '../services/external_player_service.dart';
@@ -40,12 +41,14 @@ Future<bool?> navigateToVideoPlayer(
int? selectedMediaIndex,
bool usePushReplacement = false,
bool isOffline = false,
PlexVideoPlaybackData? playbackData,
}) async {
// Extract navigator before any async operations
final navigator = Navigator.of(context);
// Load saved media version preference if not explicitly provided
int mediaIndex = selectedMediaIndex ?? 0;
var effectivePlaybackData = playbackData;
if (selectedMediaIndex == null) {
try {
final settingsService = await SettingsService.getInstance();
@@ -53,6 +56,11 @@ Future<bool?> navigateToVideoPlayer(
final savedPreference = settingsService.getMediaVersionPreference(seriesKey);
if (savedPreference != null) {
mediaIndex = savedPreference;
// Pre-parsed playbackData was built with mediaIndex=0; invalidate if
// the resolved index differs so the player re-fetches with the correct one
if (savedPreference != 0) {
effectivePlaybackData = null;
}
}
} catch (e) {
// Ignore errors loading preference, use default
@@ -109,6 +117,7 @@ Future<bool?> navigateToVideoPlayer(
preferredSubtitleTrack: preferredSubtitleTrack,
selectedMediaIndex: mediaIndex,
isOffline: isOffline,
playbackData: effectivePlaybackData,
),
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
@@ -140,6 +149,7 @@ Future<bool?> navigateToVideoPlayerWithRefresh(
SubtitleTrack? preferredSubtitleTrack,
int? selectedMediaIndex,
bool usePushReplacement = false,
PlexVideoPlaybackData? playbackData,
}) async {
final result = await navigateToVideoPlayer(
context,
@@ -149,6 +159,7 @@ Future<bool?> navigateToVideoPlayerWithRefresh(
preferredSubtitleTrack: preferredSubtitleTrack,
selectedMediaIndex: selectedMediaIndex,
usePushReplacement: usePushReplacement,
playbackData: playbackData,
);
appLogger.d('Returned from playback, refreshing metadata');