perf: reduce redundant metadata API requests
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user