Initial commit

This commit is contained in:
admin1
2026-07-14 11:11:36 +08:00
commit 656499cf94
604 changed files with 119518 additions and 0 deletions
+271
View File
@@ -0,0 +1,271 @@
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;
});