import 'dart:convert'; import '../media/ids.dart'; import '../media/media_backend.dart'; import '../media/media_source_info.dart'; import '../utils/app_logger.dart'; import '../utils/plex_cache_parser.dart'; import 'api_cache.dart'; import 'jellyfin_api_cache.dart'; import 'jellyfin_cache_resolver.dart'; import 'jellyfin_media_info_mapper.dart'; import 'plex_mappers.dart'; class CachedPlaybackMetadataService { const CachedPlaybackMetadataService._(); static Future fetchMediaSourceInfo({ required MediaBackend backend, required String cacheServerId, required String itemId, int mediaIndex = 0, }) async { try { return switch (backend) { MediaBackend.plex => _fetchPlexMediaSourceInfo(ServerId(cacheServerId), itemId, mediaIndex: mediaIndex), MediaBackend.jellyfin || MediaBackend.emby => _fetchJellyfinMediaSourceInfo( cacheServerId, itemId, backend: backend, mediaIndex: mediaIndex, ), }; } catch (e) { appLogger.d('Cached media source info unavailable for $cacheServerId:$itemId', error: e); return null; } } static Future fetchPlaybackExtras({ required MediaBackend backend, required String cacheServerId, required String itemId, String? introPattern, String? creditsPattern, bool forceChapterFallback = false, }) async { try { return switch (backend) { MediaBackend.plex => _fetchPlexPlaybackExtras( ServerId(cacheServerId), itemId, introPattern: introPattern, creditsPattern: creditsPattern, forceChapterFallback: forceChapterFallback, ), MediaBackend.jellyfin || MediaBackend.emby => _fetchJellyfinPlaybackExtras( cacheServerId, itemId, backend: backend, introPattern: introPattern, creditsPattern: creditsPattern, forceChapterFallback: forceChapterFallback, ), }; } catch (e) { appLogger.d('Cached playback extras unavailable for $cacheServerId:$itemId', error: e); return null; } } static Future _fetchPlexMediaSourceInfo( ServerId serverId, String itemId, { required int mediaIndex, }) async { final metadata = await _plexMetadata(ServerId(serverId), itemId); return metadata == null ? null : plexMediaSourceInfoFromCacheJson(metadata, mediaIndex: mediaIndex); } static Future _fetchPlexPlaybackExtras( ServerId serverId, String itemId, { String? introPattern, String? creditsPattern, bool forceChapterFallback = false, }) async { final metadata = await _plexMetadata(ServerId(serverId), itemId); if (metadata == null) return null; return plexPlaybackExtrasFromCacheJson( metadata, introPattern: introPattern, creditsPattern: creditsPattern, forceChapterFallback: forceChapterFallback, ); } static Future?> _plexMetadata(ServerId serverId, String itemId) async { final cached = await ApiCache.forBackend(MediaBackend.plex).get(serverId, '/library/metadata/$itemId'); return PlexCacheParser.extractFirstMetadata(cached); } static Future _fetchJellyfinMediaSourceInfo( String cacheServerId, String itemId, { required MediaBackend backend, required int mediaIndex, }) async { final resolved = await _jellyfinRawItem(cacheServerId, itemId, backend: backend); final raw = resolved.raw; final sources = raw['MediaSources']; if (sources is! List || sources.isEmpty) return null; final selected = mediaIndex >= 0 && mediaIndex < sources.length ? sources[mediaIndex] : sources.first; if (selected is! Map) return null; return jellyfinMediaSourceToMediaSourceInfo(selected, chapters: raw['Chapters'], trickplay: raw['Trickplay']); } static Future _fetchJellyfinPlaybackExtras( String cacheServerId, String itemId, { required MediaBackend backend, String? introPattern, String? creditsPattern, bool forceChapterFallback = false, }) async { final resolved = await _jellyfinRawItem(cacheServerId, itemId, backend: backend); final raw = resolved.raw; final markers = await _jellyfinMediaSegmentMarkers(resolved.scopeId, itemId, backend: backend); return jellyfinPlaybackExtrasFromRaw( raw, itemId, introPattern: introPattern, creditsPattern: creditsPattern, forceChapterFallback: forceChapterFallback, markers: markers, ); } static Future> _jellyfinMediaSegmentMarkers( String cacheServerId, String itemId, { required MediaBackend backend, }) async { try { final raw = await ApiCache.forBackend( backend, ).get(ServerId(cacheServerId), JellyfinApiCache.mediaSegmentsEndpoint(itemId)); return jellyfinMediaSegmentsToMarkers(raw); } catch (e) { appLogger.d('Cached Jellyfin media segments unavailable for $cacheServerId:$itemId', error: e); return const []; } } static Future<({Map raw, String scopeId})> _jellyfinRawItem( String cacheServerId, String itemId, { required MediaBackend backend, }) async { final cache = ApiCache.forBackend(backend); final resolved = await JellyfinCacheResolver(cache.database).findItem(cacheServerId, itemId); if (resolved == null) throw StateError('No MediaBrowser cache row for $cacheServerId:$itemId'); return (raw: jsonDecode(resolved.cacheRow.data) as Map, scopeId: resolved.key.scopeId); } }