272 lines
8.9 KiB
Dart
272 lines
8.9 KiB
Dart
|
|
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<MediaDetailRes, MediaDetailKey>((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<List<EmbyRawItem>, 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<List<EmbyRawItem>, 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<List<EmbyRawItem>, String>((ref, itemId) async {
|
||
|
|
final uc = await ref.read(specialFeaturesUseCaseProvider.future);
|
||
|
|
return uc.execute(itemId);
|
||
|
|
});
|
||
|
|
|
||
|
|
final crossServerSearchDataProvider = StreamProvider.autoDispose
|
||
|
|
.family<List<CrossServerSourceCard>, 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 <CrossServerSourceCard>[];
|
||
|
|
yield* uc.executeStream(req, cancelToken: cancelToken);
|
||
|
|
});
|
||
|
|
|
||
|
|
|
||
|
|
final tmdbLibraryAvailabilityProvider = StreamProvider.autoDispose
|
||
|
|
.family<
|
||
|
|
List<TmdbLibraryHit>,
|
||
|
|
({String mediaType, String tmdbId, String imdbId})
|
||
|
|
>((ref, key) async* {
|
||
|
|
if (key.tmdbId.isEmpty) {
|
||
|
|
yield const <TmdbLibraryHit>[];
|
||
|
|
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<MediaDetailRes, ServerScopedItemKey>((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<String, String> imageHeaders,
|
||
|
|
});
|
||
|
|
|
||
|
|
final tmdbServerScopedAuthProvider = FutureProvider.autoDispose
|
||
|
|
.family<ServerScopedAuth?, String>((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<List<EmbyRawItem>, 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<void, SeriesPrewarmKey>((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<String, String> _decodeProviderIdQuery(String q) {
|
||
|
|
final out = <String, String>{};
|
||
|
|
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<List<EmbyRawSeason>, 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<List<EmbyRawItem>, 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<SeriesResumeTarget?, SeriesResumeTargetKey>((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;
|
||
|
|
});
|