part of '../di_providers.dart'; typedef SeriesSeasonsKey = ({String serverId, String seriesId}); typedef EpisodesKey = ({String serverId, String seriesId, String seasonId}); typedef CrossServerSearchKey = ({ String activeServerId, String anchorType, String providerIdQuery, String seriesProviderIdQuery, int parentIndexNumber, int indexNumber, String itemName, String excludedServerId, }); typedef MediaDetailKey = ({String serverId, String itemId}); final mediaDetailDataProvider = FutureProvider.family((ref, key) async { final uc = await ref.read(mediaDetailUseCaseProvider.future); final sw = Stopwatch()..start(); final res = await uc .execute(MediaDetailReq(itemId: key.itemId)) .timeout(const Duration(seconds: 30)); final rawSources = res.base.extra['MediaSources']; final srcCount = rawSources is List ? rawSources.length : 0; AppLogger.debug( 'DetailTiming', 'base.fetch type=${res.base.Type} sources=$srcCount ms=${sw.elapsedMilliseconds}', ); return res; }); final similarItemsDataProvider = FutureProvider.family, String>((ref, itemId) async { final uc = await ref.read(similarItemsUseCaseProvider.future); final res = await uc.execute(SimilarItemsReq(itemId: itemId)); return res.items; }); final additionalPartsDataProvider = FutureProvider.family, String>((ref, itemId) async { final uc = await ref.read(additionalPartsUseCaseProvider.future); final res = await uc.execute(AdditionalPartsReq(itemId: itemId)); return res.items; }); final specialFeaturesDataProvider = FutureProvider.autoDispose .family, String>((ref, itemId) async { final uc = await ref.read(specialFeaturesUseCaseProvider.future); return uc.execute(itemId); }); final crossServerSearchDataProvider = StreamProvider.autoDispose .family, CrossServerSearchKey>(( ref, key, ) async* { final uc = await ref.read(crossServerSearchUseCaseProvider.future); final cancelToken = CancellationToken(); ref.onDispose(() => cancelToken.cancel('provider disposed')); final req = CrossServerSearchReq( anchorType: key.anchorType, itemName: key.itemName, providerIds: _decodeProviderIdQuery(key.providerIdQuery), seriesProviderIds: key.seriesProviderIdQuery.isEmpty ? null : _decodeProviderIdQuery(key.seriesProviderIdQuery), parentIndexNumber: key.parentIndexNumber < 0 ? null : key.parentIndexNumber, indexNumber: key.indexNumber < 0 ? null : key.indexNumber, excludedServerId: key.excludedServerId.isEmpty ? null : key.excludedServerId, ); yield const []; yield* uc.executeStream(req, cancelToken: cancelToken); }); final tmdbLibraryAvailabilityProvider = StreamProvider.autoDispose .family< List, ({String mediaType, String tmdbId, String imdbId}) >((ref, key) async* { if (key.tmdbId.isEmpty) { yield const []; return; } final uc = await ref.read(tmdbAvailabilityUseCaseProvider.future); final cancelToken = CancellationToken(); ref.onDispose(() => cancelToken.cancel('provider disposed')); yield* uc.executeStream( TmdbAvailabilityReq( tmdbId: key.tmdbId, mediaType: key.mediaType, imdbId: key.imdbId.isEmpty ? null : key.imdbId, ), cancelToken: cancelToken, ); }); typedef ServerScopedItemKey = ({String serverId, String itemId}); final tmdbServerScopedDetailProvider = FutureProvider.autoDispose .family((ref, key) async { final uc = await ref.read(mediaDetailUseCaseProvider.future); return uc.executeForServer(serverId: key.serverId, itemId: key.itemId); }); typedef ServerScopedAuth = ({ String baseUrl, String token, String userId, Map imageHeaders, }); final tmdbServerScopedAuthProvider = FutureProvider.autoDispose .family((ref, serverId) async { final sessionRepo = await ref.read(sessionRepositoryProvider.future); final serverRepo = await ref.read(serverRepositoryProvider.future); final session = await sessionRepo.getByServerId(serverId); if (session == null) return null; final server = await serverRepo.findById(serverId); if (server == null) return null; final deviceId = await ref.read(deviceIdProvider.future); return ( baseUrl: server.baseUrl, token: session.token, userId: session.userId, imageHeaders: EmbyRequestHeaders.image( deviceId: deviceId, deviceName: kEmbyDeviceName, token: session.token, userId: session.userId, ), ); }); final tmdbServerScopedAdditionalPartsProvider = FutureProvider.autoDispose .family, ServerScopedItemKey>((ref, key) async { final uc = await ref.read(additionalPartsUseCaseProvider.future); final res = await uc.executeForServer( serverId: key.serverId, itemId: key.itemId, ); return res.items; }); typedef SeriesPrewarmKey = ({ String activeServerId, String seriesProviderIdQuery, }); final seriesPrewarmProvider = FutureProvider.autoDispose .family((ref, key) async { if (key.seriesProviderIdQuery.isEmpty) return; final keepAlive = ref.keepAlive(); final uc = await ref.read(crossServerSearchUseCaseProvider.future); final cancelToken = CancellationToken(); var disposed = false; ref.onDispose(() { disposed = true; cancelToken.cancel('prewarm disposed'); }); try { await uc.prewarmSeriesIdentity( _decodeProviderIdQuery(key.seriesProviderIdQuery), cancelToken: cancelToken, ); } finally { if (!disposed) keepAlive.close(); } }); Map _decodeProviderIdQuery(String q) { final out = {}; if (q.isEmpty) return out; for (final part in q.split(',')) { final dotIdx = part.indexOf('.'); if (dotIdx > 0) { out[part.substring(0, dotIdx)] = part.substring(dotIdx + 1); } } return out; } final seriesSeasonsDataProvider = FutureProvider.family, SeriesSeasonsKey>(( ref, key, ) async { final uc = await ref.read(seriesSeasonsUseCaseProvider.future); final res = await uc.executeForServer( serverId: key.serverId, input: SeriesSeasonsReq(seriesId: key.seriesId), ); return res.items; }); final seriesEpisodesDataProvider = FutureProvider.family, EpisodesKey>((ref, key) async { final uc = await ref.read(seriesEpisodesUseCaseProvider.future); final res = await uc.executeForServer( serverId: key.serverId, input: MediaEpisodesReq(seriesId: key.seriesId, seasonId: key.seasonId), ); return res.items; }); typedef SeriesResumeTargetKey = ({String serverId, String seriesId}); final seriesResumeTargetProvider = FutureProvider.autoDispose .family((ref, key) async { try { final sessionRepo = await ref.read(sessionRepositoryProvider.future); final serverRepo = await ref.read(serverRepositoryProvider.future); final ctx = await resolveAuthedContextForServer( sessionRepository: sessionRepo, serverRepository: serverRepo, serverId: key.serverId, ); if (ctx == null) return null; final gw = await ref.read(embyGatewayProvider.future); return resolveSeriesResumeTarget(gw, ctx, key.seriesId); } catch (e) { AppLogger.warn('SeriesResumeTarget', 'resolve failed', e); return null; } }); final tmdbTvRecentHitProvider = FutureProvider.autoDispose .family< TmdbLibraryHit?, ({String mediaType, String tmdbId, String imdbId}) >((ref, key) async { final hits = await ref.watch(tmdbLibraryAvailabilityProvider(key).future); final seriesHits = hits .where((h) => h.itemType == 'Series') .toList(growable: false); if (seriesHits.length < 2) return null; final uc = await ref.read(mediaDetailUseCaseProvider.future); DateTime? bestDate; TmdbLibraryHit? best; for (final h in seriesHits) { try { final res = await uc.executeForServer( serverId: h.serverId, itemId: h.itemId, ); final lp = parseEmbyDate(res.base.UserData?.extra['LastPlayedDate']); if (lp != null && (bestDate == null || lp.isAfter(bestDate))) { bestDate = lp; best = h; } } catch (_) {} } return best; });