fix: preserve chapters/markers in cache during watch state sync
This commit is contained in:
@@ -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],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user