fix: always cache markers for auto-skip

close #210
This commit is contained in:
edde746
2026-01-04 04:26:05 +01:00
parent 6bfda5e6d4
commit da6ba9bd67
3 changed files with 69 additions and 123 deletions
+9 -1
View File
@@ -13,7 +13,15 @@ class PlexVideoPlaybackData {
/// Available media versions/qualities for this content
final List<PlexMediaVersion> availableVersions;
PlexVideoPlaybackData({required this.videoUrl, required this.mediaInfo, required this.availableVersions});
/// Markers for intro/credits skip functionality
final List<PlexMarker> markers;
PlexVideoPlaybackData({
required this.videoUrl,
required this.mediaInfo,
required this.availableVersions,
this.markers = const [],
});
/// Returns true if this playback data has a valid video URL
bool get hasValidVideoUrl => videoUrl != null && videoUrl!.isNotEmpty;
+2 -5
View File
@@ -344,16 +344,13 @@ class DownloadManagerService {
}
final metadata = PlexMetadata.fromJson(firstMetadata).copyWith(serverId: serverId);
// Get video playback data (includes URL, streams, etc.)
// Get video playback data (includes URL, streams, markers, etc.)
// This also caches the metadata with chapters/markers for offline use
final playbackData = await client.getVideoPlaybackData(metadata.ratingKey);
if (playbackData.videoUrl == null) {
throw Exception('Could not get video URL');
}
// Cache playback extras (chapters + markers) for offline use
// This fetches and caches in one call
await client.getPlaybackExtras(metadata.ratingKey);
// Determine file extension from URL or default to mp4
final extension = _getExtensionFromUrl(playbackData.videoUrl!) ?? 'mp4';
+58 -117
View File
@@ -444,7 +444,10 @@ class PlexClient {
// Online: try network first
try {
final response = await _dio.get('/library/metadata/$ratingKey', queryParameters: queryParams);
// Always include markers and chapters to ensure cached data is complete
// This prevents cache corruption when different methods fetch the same endpoint
final mergedParams = {'includeMarkers': 1, 'includeChapters': 1, ...?queryParams};
final response = await _dio.get('/library/metadata/$ratingKey', queryParameters: mergedParams);
// Cache at base endpoint
if (response.data != null) {
@@ -781,36 +784,6 @@ class PlexClient {
return '${config.baseUrl}/$path'.withPlexToken(config.token);
}
/// Get video URL for direct playback
/// [mediaIndex] specifies which Media item to use (defaults to 0 - first version)
/// Uses cache for offline mode support and network fallback.
Future<String?> getVideoUrl(String ratingKey, {int mediaIndex = 0}) async {
final data = await _fetchMetadataWithCache(ratingKey);
final metadataJson = _getFirstMetadataJsonFromData(data);
if (metadataJson != null && metadataJson['Media'] != null && (metadataJson['Media'] as List).isNotEmpty) {
final mediaList = metadataJson['Media'] as List;
// 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) {
// Return direct play URL
return '${config.baseUrl}$partKey'.withPlexToken(config.token);
}
}
}
return null;
}
/// Get chapters for a media item
Future<List<PlexChapter>> getChapters(String ratingKey) async {
final response = await _dio.get('/library/metadata/$ratingKey', queryParameters: {'includeChapters': 1});
@@ -952,63 +925,8 @@ class PlexClient {
return PlaybackExtras(chapters: chapters, markers: markers);
}
/// Get detailed media info including chapters and tracks
/// [mediaIndex] specifies which Media item to use (defaults to 0 - first version)
/// Uses cache for offline mode support and network fallback.
Future<PlexMediaInfo?> getMediaInfo(String ratingKey, {int mediaIndex = 0}) async {
final data = await _fetchMetadataWithCache(ratingKey);
final metadataJson = _getFirstMetadataJsonFromData(data);
if (metadataJson != null && metadataJson['Media'] != null && (metadataJson['Media'] as List).isNotEmpty) {
final mediaList = metadataJson['Media'] as List;
// 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) {
// Parse streams using helper
final streams = _parseStreams(part['Stream'] as List<dynamic>?);
// Parse chapters using helper
final chapters = _parseChapters(metadataJson);
return PlexMediaInfo(
videoUrl: '${config.baseUrl}$partKey'.withPlexToken(config.token),
audioTracks: streams.audio,
subtitleTracks: streams.subtitles,
chapters: chapters,
);
}
}
}
return null;
}
/// Get all available media versions for a media item
/// Returns a list of PlexMediaVersion objects representing different quality/format options
/// Uses cache for offline mode support and network fallback.
Future<List<PlexMediaVersion>> getMediaVersions(String ratingKey) async {
final data = await _fetchMetadataWithCache(ratingKey);
final metadataJson = _getFirstMetadataJsonFromData(data);
if (metadataJson != null && metadataJson['Media'] != null && (metadataJson['Media'] as List).isNotEmpty) {
final mediaList = metadataJson['Media'] as List;
return mediaList.map((media) => PlexMediaVersion.fromJson(media as Map<String, dynamic>)).toList();
}
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.
/// 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 {
final data = await _fetchMetadataWithCache(ratingKey);
@@ -1017,45 +935,68 @@ class PlexClient {
String? videoUrl;
PlexMediaInfo? mediaInfo;
List<PlexMediaVersion> availableVersions = [];
List<PlexMarker> markers = [];
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;
if (metadataJson != null) {
// Parse markers (for auto-skip functionality)
if (metadataJson['Marker'] != null) {
final markerList = metadataJson['Marker'] as List;
for (var marker in markerList) {
markers.add(
PlexMarker(
id: marker['id'] as int,
type: marker['type'] as String,
startTimeOffset: marker['startTimeOffset'] as int,
endTimeOffset: marker['endTimeOffset'] as int,
),
);
}
}
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 (metadataJson['Media'] != null && (metadataJson['Media'] as List).isNotEmpty) {
final mediaList = metadataJson['Media'] as List;
if (partKey != null) {
// Get video URL
videoUrl = '${config.baseUrl}$partKey'.withPlexToken(config.token);
// Parse available media versions first
availableVersions = mediaList.map((media) => PlexMediaVersion.fromJson(media as Map<String, dynamic>)).toList();
// Parse streams using helper
final streams = _parseStreams(part['Stream'] as List<dynamic>?);
// Parse chapters using helper
final chapters = _parseChapters(metadataJson);
// Ensure the requested index is valid
if (mediaIndex < 0 || mediaIndex >= mediaList.length) {
mediaIndex = 0;
}
// Create media info
mediaInfo = PlexMediaInfo(
videoUrl: videoUrl,
audioTracks: streams.audio,
subtitleTracks: streams.subtitles,
chapters: chapters,
partId: part['id'] as int?,
);
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'.withPlexToken(config.token);
// Parse streams using helper
final streams = _parseStreams(part['Stream'] as List<dynamic>?);
// Parse chapters using helper
final chapters = _parseChapters(metadataJson);
// Create media info
mediaInfo = PlexMediaInfo(
videoUrl: videoUrl,
audioTracks: streams.audio,
subtitleTracks: streams.subtitles,
chapters: chapters,
partId: part['id'] as int?,
);
}
}
}
}
return PlexVideoPlaybackData(videoUrl: videoUrl, mediaInfo: mediaInfo, availableVersions: availableVersions);
return PlexVideoPlaybackData(
videoUrl: videoUrl,
mediaInfo: mediaInfo,
availableVersions: availableVersions,
markers: markers,
);
}
/// Get file information for a media item