fix: preserve chapters/markers in cache during watch state sync

This commit is contained in:
edde746
2026-03-15 01:04:13 +01:00
parent 9a296b3da8
commit 6d7cdc4ef4
4 changed files with 65 additions and 17 deletions
+21 -2
View File
@@ -1673,13 +1673,32 @@ class DownloadManagerService {
/// Cache metadata in the API response format for offline access
/// This simulates what PlexClient would receive from the server
/// Merges with existing cache to preserve Chapter/Marker/Media arrays
Future<void> _cacheMetadataForOffline(String serverId, String ratingKey, PlexMetadata metadata) async {
final endpoint = '/library/metadata/$ratingKey';
// Build a response structure that matches the Plex API format
// Check for existing cache entry to preserve fields not in PlexMetadata
final existing = await _apiCache.get(serverId, endpoint);
final existingMeta = PlexCacheParser.extractFirstMetadata(existing);
Map<String, dynamic> merged;
if (existingMeta != null) {
// Start with existing (has Chapter/Marker/Media), overlay new metadata
merged = existingMeta;
final newJson = metadata.toJson();
// Only update fields that toJson() sets to non-null values
for (final entry in newJson.entries) {
if (entry.value != null) {
merged[entry.key] = entry.value;
}
}
} else {
merged = metadata.toJson();
}
final cachedResponse = {
'MediaContainer': {
'Metadata': [metadata.toJson()],
'Metadata': [merged],
},
};
+29 -10
View File
@@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart';
import '../database/app_database.dart';
import '../providers/offline_mode_provider.dart';
import '../utils/app_logger.dart';
import '../utils/plex_cache_parser.dart';
import 'multi_server_manager.dart';
import 'plex_api_cache.dart';
import 'plex_client.dart';
@@ -421,11 +422,32 @@ class OfflineWatchSyncService extends ChangeNotifier {
for (final episode in seasonEpisodes) {
if (!downloadedEpisodeKeys.contains(episode.ratingKey)) continue;
await PlexApiCache.instance.put(serverId, '/library/metadata/${episode.ratingKey}', {
'MediaContainer': {
'Metadata': [episode.toJson()],
},
});
final cacheKey = '/library/metadata/${episode.ratingKey}';
final existing = await PlexApiCache.instance.get(serverId, cacheKey);
final existingMeta = PlexCacheParser.extractFirstMetadata(existing);
if (existingMeta != null) {
// Only update watch-state fields — preserves Chapter/Marker/Media/etc.
existingMeta['viewCount'] = episode.viewCount;
existingMeta['viewOffset'] = episode.viewOffset;
existingMeta['lastViewedAt'] = episode.lastViewedAt;
existingMeta['viewedLeafCount'] = episode.viewedLeafCount;
await PlexApiCache.instance.put(serverId, cacheKey, {
'MediaContainer': {'Metadata': [existingMeta]},
});
// Repair corrupted entries (missing Media/Chapter from previous overwrites)
if (existingMeta['Media'] == null) {
try {
await client.getMetadataWithImages(episode.ratingKey);
} catch (_) {}
}
} else {
// No existing entry — write what we have
await PlexApiCache.instance.put(serverId, cacheKey, {
'MediaContainer': {'Metadata': [episode.toJson()]},
});
}
synced++;
}
@@ -501,13 +523,10 @@ class OfflineWatchSyncService extends ChangeNotifier {
await _withOnlineClient(serverId, (client) async {
for (final ratingKey in ratingKeys) {
try {
// getMetadataWithImages already caches the full API response
// (with chapters/markers) via _fetchWithCacheFallback internally
final metadata = await client.getMetadataWithImages(ratingKey);
if (metadata != null) {
await PlexApiCache.instance.put(serverId, '/library/metadata/$ratingKey', {
'MediaContainer': {
'Metadata': [metadata.toJson()],
},
});
syncedCount++;
}
} catch (e) {
+3 -2
View File
@@ -954,9 +954,10 @@ class PlexClient {
/// Get chapters and markers from cached metadata or fetch if needed
/// Uses same cache key as other metadata methods for consistency
Future<PlaybackExtras> getPlaybackExtras(String ratingKey, {String? introPattern, String? creditsPattern}) async {
Future<PlaybackExtras> getPlaybackExtras(String ratingKey, {String? introPattern, String? creditsPattern, bool forceRefresh = false}) async {
try {
final data = await _fetchWithCacheFirst<Map<String, dynamic>>(
final fetch = forceRefresh ? _fetchWithCacheFallback : _fetchWithCacheFirst;
final data = await fetch<Map<String, dynamic>>(
cacheKey: '/library/metadata/$ratingKey',
networkCall: () =>
_dio.get('/library/metadata/$ratingKey', queryParameters: {'includeChapters': 1, 'includeMarkers': 1}),
+12 -3
View File
@@ -212,6 +212,7 @@ class PlexVideoControls extends StatefulWidget {
class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListener, WidgetsBindingObserver {
bool _showControls = true;
bool _forceShowControls = false;
bool _isLoadingExtras = false;
List<PlexChapter> _chapters = [];
bool _chaptersLoaded = false;
Timer? _hideTimer;
@@ -334,6 +335,10 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
void _onFirstFrameReady() {
if (widget.hasFirstFrame?.value == true) {
_startHideTimer();
// Retry with network-first if initial cache-first returned empty
if (_chapters.isEmpty && _markers.isEmpty) {
_loadPlaybackExtras(forceRefresh: true);
}
}
}
@@ -930,19 +935,21 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
}
}
Future<void> _loadPlaybackExtras() async {
Future<void> _loadPlaybackExtras({bool forceRefresh = false}) async {
// Live TV metadata uses EPG rating keys, not library items
if (widget.isLive) return;
if (_isLoadingExtras) return;
_isLoadingExtras = true;
try {
appLogger.d('_loadPlaybackExtras: starting for ${widget.metadata.ratingKey}');
appLogger.d('_loadPlaybackExtras: starting for ${widget.metadata.ratingKey} (forceRefresh=$forceRefresh)');
final client = _getClientForMetadata();
appLogger.d('_loadPlaybackExtras: got client with serverId=${client.serverId}');
final settings = await SettingsService.getInstance();
final introPattern = settings.getIntroPattern();
final creditsPattern = settings.getCreditsPattern();
final extras = await client.getPlaybackExtras(widget.metadata.ratingKey, introPattern: introPattern, creditsPattern: creditsPattern);
final extras = await client.getPlaybackExtras(widget.metadata.ratingKey, introPattern: introPattern, creditsPattern: creditsPattern, forceRefresh: forceRefresh);
appLogger.d('_loadPlaybackExtras: got ${extras.chapters.length} chapters');
if (mounted) {
@@ -975,6 +982,8 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
}
}
appLogger.e('_loadPlaybackExtras failed', error: e, stackTrace: stack);
} finally {
_isLoadingExtras = false;
}
}