Initial commit
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
part of '../di_providers.dart';
|
||||
|
||||
|
||||
final appDataDirProvider = FutureProvider<Directory>((ref) async {
|
||||
final base = await getApplicationSupportDirectory();
|
||||
final dir = Directory('${base.path}/smplayer');
|
||||
if (!await dir.exists()) {
|
||||
await dir.create(recursive: true);
|
||||
}
|
||||
return dir;
|
||||
});
|
||||
|
||||
|
||||
final deviceIdProvider = FutureProvider<String>((ref) async {
|
||||
final store = DeviceIdStore();
|
||||
return store.getOrCreate();
|
||||
});
|
||||
|
||||
|
||||
final serverRepositoryProvider = FutureProvider<ServerRepository>((ref) async {
|
||||
final dir = await ref.watch(appDataDirProvider.future);
|
||||
return JsonServerRepository(file: File('${dir.path}/servers.json'));
|
||||
});
|
||||
|
||||
|
||||
final sessionRepositoryProvider = FutureProvider<SessionRepository>((
|
||||
ref,
|
||||
) async {
|
||||
final dir = await ref.watch(appDataDirProvider.future);
|
||||
return JsonSessionRepository(file: File('${dir.path}/sessions.json'));
|
||||
});
|
||||
|
||||
|
||||
final searchHistoryStoreProvider = Provider<SearchHistoryStore>(
|
||||
(ref) => SearchHistoryStore(),
|
||||
);
|
||||
|
||||
|
||||
final danmakuSourcesStoreProvider = Provider<DanmakuSourcesStore>(
|
||||
(ref) => PrefsDanmakuSourcesStore(),
|
||||
);
|
||||
|
||||
|
||||
final homePageCacheStoreProvider = FutureProvider<HomePageCacheStore>((
|
||||
ref,
|
||||
) async {
|
||||
final dir = await ref.watch(appDataDirProvider.future);
|
||||
return HomePageCacheStore(directory: dir);
|
||||
});
|
||||
|
||||
|
||||
final discoverPageCacheStoreProvider = FutureProvider<DiscoverPageCacheStore>((
|
||||
ref,
|
||||
) async {
|
||||
final dir = await ref.watch(appDataDirProvider.future);
|
||||
return DiscoverPageCacheStore(directory: dir);
|
||||
});
|
||||
|
||||
|
||||
final embyGatewayProvider = FutureProvider<EmbyGateway>((ref) async {
|
||||
final deviceId = await ref.read(deviceIdProvider.future);
|
||||
return EmbyApiClient(deviceId: deviceId, deviceName: kEmbyDeviceName);
|
||||
});
|
||||
|
||||
|
||||
final tmdbGatewayProvider = Provider<TmdbGateway>((ref) => TmdbApiClient());
|
||||
|
||||
|
||||
final danmakuGatewayProvider = Provider<DanmakuGateway>(
|
||||
(ref) => DispatchingDanmakuGateway(
|
||||
httpGateway: DanmakuApiClient(),
|
||||
scriptWidgetGateway: ScriptWidgetDanmakuGateway(
|
||||
loadService: () => ref.read(scriptWidgetServiceProvider.future),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -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;
|
||||
});
|
||||
@@ -0,0 +1,261 @@
|
||||
part of '../di_providers.dart';
|
||||
|
||||
|
||||
final playbackResolverProvider = FutureProvider<PlaybackResolver>((ref) async {
|
||||
final gateway = await ref.read(embyGatewayProvider.future);
|
||||
final deviceId = await ref.read(deviceIdProvider.future);
|
||||
return PlaybackResolver(
|
||||
gateway: gateway,
|
||||
deviceId: deviceId,
|
||||
deviceName: kEmbyDeviceName,
|
||||
);
|
||||
});
|
||||
|
||||
typedef _EmbyDeps = ({
|
||||
SessionRepository session,
|
||||
ServerRepository server,
|
||||
EmbyGateway gw,
|
||||
});
|
||||
|
||||
Future<_EmbyDeps> _embyDeps(Ref ref) async => (
|
||||
session: await ref.watch(sessionRepositoryProvider.future),
|
||||
server: await ref.watch(serverRepositoryProvider.future),
|
||||
gw: await ref.watch(embyGatewayProvider.future),
|
||||
);
|
||||
|
||||
|
||||
FutureProvider<T> _authedUseCaseProvider<T>(
|
||||
T Function({
|
||||
required SessionRepository sessionRepository,
|
||||
required ServerRepository serverRepository,
|
||||
required EmbyGateway embyGateway,
|
||||
})
|
||||
ctor,
|
||||
) {
|
||||
return FutureProvider<T>((ref) async {
|
||||
final d = await _embyDeps(ref);
|
||||
return ctor(
|
||||
sessionRepository: d.session,
|
||||
serverRepository: d.server,
|
||||
embyGateway: d.gw,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
final listServersUseCaseProvider = FutureProvider<ListServersUseCase>((
|
||||
ref,
|
||||
) async {
|
||||
final repo = await ref.watch(serverRepositoryProvider.future);
|
||||
return ListServersUseCase(serverRepository: repo);
|
||||
});
|
||||
|
||||
final saveServerUseCaseProvider = FutureProvider<SaveServerUseCase>((
|
||||
ref,
|
||||
) async {
|
||||
final repo = await ref.watch(serverRepositoryProvider.future);
|
||||
return SaveServerUseCase(serverRepository: repo);
|
||||
});
|
||||
|
||||
final reorderServersUseCaseProvider = FutureProvider<ReorderServersUseCase>((
|
||||
ref,
|
||||
) async {
|
||||
final repo = await ref.watch(serverRepositoryProvider.future);
|
||||
return ReorderServersUseCase(serverRepository: repo);
|
||||
});
|
||||
|
||||
final probeServerUseCaseProvider = FutureProvider<ProbeServerUseCase>((
|
||||
ref,
|
||||
) async {
|
||||
final gw = await ref.watch(embyGatewayProvider.future);
|
||||
return ProbeServerUseCase(embyGateway: gw);
|
||||
});
|
||||
|
||||
final exportBackupUseCaseProvider = FutureProvider<ExportBackupUseCase>((
|
||||
ref,
|
||||
) async {
|
||||
return ExportBackupUseCase(
|
||||
serverRepository: await ref.watch(serverRepositoryProvider.future),
|
||||
sessionRepository: await ref.watch(sessionRepositoryProvider.future),
|
||||
danmakuSourcesStore: ref.read(danmakuSourcesStoreProvider),
|
||||
scriptWidgetRepository: await ref.watch(
|
||||
scriptWidgetRepositoryProvider.future,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
final importBackupUseCaseProvider = FutureProvider<ImportBackupUseCase>((
|
||||
ref,
|
||||
) async {
|
||||
return ImportBackupUseCase(
|
||||
serverRepository: await ref.watch(serverRepositoryProvider.future),
|
||||
sessionRepository: await ref.watch(sessionRepositoryProvider.future),
|
||||
danmakuSourcesStore: ref.read(danmakuSourcesStoreProvider),
|
||||
scriptWidgetRepository: await ref.watch(
|
||||
scriptWidgetRepositoryProvider.future,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
final serverSyncGatewayProvider = Provider<ServerSyncGateway>(
|
||||
(ref) => ServerSyncClient(),
|
||||
);
|
||||
|
||||
|
||||
final uploadServerSyncUseCaseProvider = FutureProvider<UploadServerSyncUseCase>(
|
||||
(ref) async {
|
||||
return UploadServerSyncUseCase(
|
||||
export: await ref.watch(exportBackupUseCaseProvider.future),
|
||||
gateway: ref.read(serverSyncGatewayProvider),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
final pullServerSyncUseCaseProvider = FutureProvider<PullServerSyncUseCase>((
|
||||
ref,
|
||||
) async {
|
||||
return PullServerSyncUseCase(
|
||||
serverRepository: await ref.watch(serverRepositoryProvider.future),
|
||||
sessionRepository: await ref.watch(sessionRepositoryProvider.future),
|
||||
danmakuSourcesStore: ref.read(danmakuSourcesStoreProvider),
|
||||
scriptWidgetRepository: await ref.watch(
|
||||
scriptWidgetRepositoryProvider.future,
|
||||
),
|
||||
gateway: ref.read(serverSyncGatewayProvider),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
final repairSessionsUseCaseProvider = FutureProvider<RepairSessionsUseCase>((
|
||||
ref,
|
||||
) async {
|
||||
final d = await _embyDeps(ref);
|
||||
return RepairSessionsUseCase(
|
||||
serverRepository: d.server,
|
||||
sessionRepository: d.session,
|
||||
embyGateway: d.gw,
|
||||
);
|
||||
});
|
||||
|
||||
final deleteServerUseCaseProvider = FutureProvider<DeleteServerUseCase>((
|
||||
ref,
|
||||
) async {
|
||||
final sRepo = await ref.watch(serverRepositoryProvider.future);
|
||||
final ssRepo = await ref.watch(sessionRepositoryProvider.future);
|
||||
return DeleteServerUseCase(
|
||||
serverRepository: sRepo,
|
||||
sessionRepository: ssRepo,
|
||||
);
|
||||
});
|
||||
|
||||
final togglePauseServerUseCaseProvider =
|
||||
FutureProvider<TogglePauseServerUseCase>((ref) async {
|
||||
final sRepo = await ref.watch(serverRepositoryProvider.future);
|
||||
return TogglePauseServerUseCase(serverRepository: sRepo);
|
||||
});
|
||||
|
||||
final authByNameUseCaseProvider = FutureProvider<AuthByNameUseCase>((
|
||||
ref,
|
||||
) async {
|
||||
final d = await _embyDeps(ref);
|
||||
return AuthByNameUseCase(
|
||||
serverRepository: d.server,
|
||||
sessionRepository: d.session,
|
||||
embyGateway: d.gw,
|
||||
);
|
||||
});
|
||||
|
||||
final logoutUseCaseProvider = FutureProvider<LogoutUseCase>((ref) async {
|
||||
return LogoutUseCase(
|
||||
sessionRepository: await ref.watch(sessionRepositoryProvider.future),
|
||||
);
|
||||
});
|
||||
|
||||
final listSessionsUseCaseProvider = FutureProvider<ListSessionsUseCase>((
|
||||
ref,
|
||||
) async {
|
||||
return ListSessionsUseCase(
|
||||
sessionRepository: await ref.watch(sessionRepositoryProvider.future),
|
||||
serverRepository: await ref.watch(serverRepositoryProvider.future),
|
||||
);
|
||||
});
|
||||
|
||||
final switchSessionUseCaseProvider = FutureProvider<SwitchSessionUseCase>((
|
||||
ref,
|
||||
) async {
|
||||
return SwitchSessionUseCase(
|
||||
sessionRepository: await ref.watch(sessionRepositoryProvider.future),
|
||||
);
|
||||
});
|
||||
|
||||
final changePasswordUseCaseProvider = FutureProvider<ChangePasswordUseCase>((
|
||||
ref,
|
||||
) async {
|
||||
final d = await _embyDeps(ref);
|
||||
return ChangePasswordUseCase(
|
||||
sessionRepository: d.session,
|
||||
serverRepository: d.server,
|
||||
embyGateway: d.gw,
|
||||
);
|
||||
});
|
||||
|
||||
final libraryViewsUseCaseProvider = _authedUseCaseProvider(
|
||||
GetLibraryViewsUseCase.new,
|
||||
);
|
||||
final libraryResumeUseCaseProvider = _authedUseCaseProvider(
|
||||
GetLibraryResumeUseCase.new,
|
||||
);
|
||||
final libraryLatestUseCaseProvider = _authedUseCaseProvider(
|
||||
GetLibraryLatestUseCase.new,
|
||||
);
|
||||
final libraryItemsUseCaseProvider = _authedUseCaseProvider(
|
||||
GetLibraryItemsUseCase.new,
|
||||
);
|
||||
final homeBannerUseCaseProvider = _authedUseCaseProvider(
|
||||
GetHomeBannerUseCase.new,
|
||||
);
|
||||
final mediaDetailUseCaseProvider = _authedUseCaseProvider(
|
||||
GetMediaDetailUseCase.new,
|
||||
);
|
||||
final setFavoriteUseCaseProvider = _authedUseCaseProvider(
|
||||
SetFavoriteUseCase.new,
|
||||
);
|
||||
final setPlayedUseCaseProvider = _authedUseCaseProvider(SetPlayedUseCase.new);
|
||||
final hideFromResumeUseCaseProvider = _authedUseCaseProvider(
|
||||
HideFromResumeUseCase.new,
|
||||
);
|
||||
final seriesEpisodesUseCaseProvider = _authedUseCaseProvider(
|
||||
GetSeriesEpisodesUseCase.new,
|
||||
);
|
||||
final seriesSeasonsUseCaseProvider = _authedUseCaseProvider(
|
||||
GetSeriesSeasonsUseCase.new,
|
||||
);
|
||||
final similarItemsUseCaseProvider = _authedUseCaseProvider(
|
||||
GetSimilarItemsUseCase.new,
|
||||
);
|
||||
final additionalPartsUseCaseProvider = _authedUseCaseProvider(
|
||||
GetAdditionalPartsUseCase.new,
|
||||
);
|
||||
final specialFeaturesUseCaseProvider = _authedUseCaseProvider(
|
||||
GetSpecialFeaturesUseCase.new,
|
||||
);
|
||||
final crossServerSearchUseCaseProvider = _authedUseCaseProvider(
|
||||
CrossServerSearchUseCase.new,
|
||||
);
|
||||
final tmdbAvailabilityUseCaseProvider = _authedUseCaseProvider(
|
||||
TmdbAvailabilityUseCase.new,
|
||||
);
|
||||
|
||||
final globalKeywordSearchUseCaseProvider = _authedUseCaseProvider(
|
||||
GlobalKeywordSearchUseCase.new,
|
||||
);
|
||||
final aggregatedResumeUseCaseProvider = _authedUseCaseProvider(
|
||||
GetAggregatedResumeUseCase.new,
|
||||
);
|
||||
final aggregatedFavoritesUseCaseProvider = _authedUseCaseProvider(
|
||||
GetAggregatedFavoritesUseCase.new,
|
||||
);
|
||||
final aggregatedRecentUseCaseProvider = _authedUseCaseProvider(
|
||||
GetAggregatedRecentUseCase.new,
|
||||
);
|
||||
@@ -0,0 +1,84 @@
|
||||
part of '../di_providers.dart';
|
||||
|
||||
final scriptWidgetRuntimeProvider = Provider<ScriptWidgetRuntime>((ref) {
|
||||
final runtime = QuickJsWidgetRuntime();
|
||||
ref.onDispose(() {
|
||||
unawaited(runtime.dispose());
|
||||
});
|
||||
return runtime;
|
||||
});
|
||||
|
||||
final scriptWidgetRemoteGatewayProvider = Provider<ScriptWidgetRemoteGateway>(
|
||||
(ref) => ScriptWidgetRemoteClient(),
|
||||
);
|
||||
|
||||
final scriptWidgetRepositoryProvider = FutureProvider<ScriptWidgetRepository>((
|
||||
ref,
|
||||
) async {
|
||||
final appDataDirectory = await ref.watch(appDataDirProvider.future);
|
||||
return JsonScriptWidgetRepository(
|
||||
file: File('${appDataDirectory.path}/script_widgets.json'),
|
||||
);
|
||||
});
|
||||
|
||||
final scriptWidgetServiceProvider = FutureProvider<ScriptWidgetService>((
|
||||
ref,
|
||||
) async {
|
||||
final repository = await ref.watch(scriptWidgetRepositoryProvider.future);
|
||||
return ScriptWidgetService(
|
||||
repository: repository,
|
||||
remoteGateway: ref.watch(scriptWidgetRemoteGatewayProvider),
|
||||
runtime: ref.watch(scriptWidgetRuntimeProvider),
|
||||
danmakuSourcesStore: ref.watch(danmakuSourcesStoreProvider),
|
||||
onDanmakuSourcesChanged: () async {
|
||||
ref.invalidate(danmakuSettingsProvider);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
final installedScriptWidgetsProvider =
|
||||
FutureProvider<List<InstalledScriptWidget>>((ref) async {
|
||||
final service = await ref.watch(scriptWidgetServiceProvider.future);
|
||||
return service.listWidgets();
|
||||
});
|
||||
|
||||
final scriptWidgetSubscriptionsProvider =
|
||||
FutureProvider<List<ScriptWidgetSubscription>>((ref) async {
|
||||
final service = await ref.watch(scriptWidgetServiceProvider.future);
|
||||
return service.listSubscriptions();
|
||||
});
|
||||
|
||||
final installedScriptWidgetProvider =
|
||||
FutureProvider.family<InstalledScriptWidget, String>((ref, widgetId) async {
|
||||
final service = await ref.watch(scriptWidgetServiceProvider.future);
|
||||
return service.requireWidget(widgetId);
|
||||
});
|
||||
|
||||
typedef ScriptWidgetModulePreviewKey = ({String widgetId, String moduleKey});
|
||||
|
||||
final scriptWidgetModulePreviewProvider =
|
||||
FutureProvider.family<
|
||||
List<ScriptWidgetContentSection>,
|
||||
ScriptWidgetModulePreviewKey
|
||||
>((ref, previewKey) async {
|
||||
final service = await ref.watch(scriptWidgetServiceProvider.future);
|
||||
final installedWidget = await service.requireWidget(previewKey.widgetId);
|
||||
final module = installedWidget.manifest.modules
|
||||
.where(
|
||||
(candidate) =>
|
||||
(candidate.id.isEmpty
|
||||
? candidate.functionName
|
||||
: candidate.id) ==
|
||||
previewKey.moduleKey,
|
||||
)
|
||||
.firstOrNull;
|
||||
if (module == null) {
|
||||
throw StateError('未找到模块功能:${previewKey.moduleKey}');
|
||||
}
|
||||
|
||||
return service.loadModuleSections(
|
||||
previewKey.widgetId,
|
||||
module,
|
||||
const <String, Object?>{},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,362 @@
|
||||
part of '../di_providers.dart';
|
||||
|
||||
typedef TmdbMediaKey = ({String tmdbId, String mediaType});
|
||||
typedef TmdbSeasonKey = ({String tmdbId, int seasonNumber});
|
||||
|
||||
|
||||
typedef _TmdbReady = ({TmdbRequestConfig config, String language});
|
||||
|
||||
|
||||
final _tmdbReadyProvider = FutureProvider<_TmdbReady?>((ref) async {
|
||||
final settings = await ref.watch(tmdbSettingsProvider.future);
|
||||
if (!settings.canRequest) return null;
|
||||
|
||||
if (settings.accessMode == TmdbAccessMode.smAccount) {
|
||||
final account = await ref.watch(smAccountProvider.future);
|
||||
if (!account.canUseDataPlane) return null;
|
||||
final notifier = ref.read(smAccountProvider.notifier);
|
||||
final token = await notifier.getAccessToken();
|
||||
if (token == null) return null;
|
||||
return (
|
||||
config: TmdbRequestConfig(
|
||||
apiBaseUrl: account.tmdbApiBaseUrl,
|
||||
imageBaseUrl: TmdbSettingsState.officialImageBaseUrl,
|
||||
accessToken: token,
|
||||
apiKey: '',
|
||||
onUnauthorized: () => notifier.getAccessToken(force: true),
|
||||
),
|
||||
language: settings.language,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
config: TmdbRequestConfig(
|
||||
apiBaseUrl: settings.effectiveApiBaseUrl,
|
||||
imageBaseUrl: settings.effectiveImageBaseUrl,
|
||||
accessToken: settings.effectiveAccessToken,
|
||||
apiKey: settings.effectiveApiKey,
|
||||
),
|
||||
language: settings.language,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
final _tmdbCacheScopeProvider = FutureProvider<DiscoverPageCacheScope?>((
|
||||
ref,
|
||||
) async {
|
||||
final settings = await ref.watch(tmdbSettingsProvider.future);
|
||||
if (!settings.canRequest) return null;
|
||||
|
||||
if (settings.accessMode == TmdbAccessMode.smAccount) {
|
||||
final account = await ref.watch(smAccountProvider.future);
|
||||
if (!account.hasActiveSession) return null;
|
||||
return DiscoverPageCacheScope(
|
||||
apiBaseUrl: account.tmdbApiBaseUrl,
|
||||
imageBaseUrl: TmdbSettingsState.officialImageBaseUrl,
|
||||
language: settings.language,
|
||||
);
|
||||
}
|
||||
|
||||
return DiscoverPageCacheScope(
|
||||
apiBaseUrl: settings.effectiveApiBaseUrl,
|
||||
imageBaseUrl: settings.effectiveImageBaseUrl,
|
||||
language: settings.language,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
final tmdbEnrichedDetailProvider =
|
||||
FutureProvider.family<TmdbEnrichedDetail?, TmdbMediaKey>((ref, key) async {
|
||||
final ready = await ref.watch(_tmdbReadyProvider.future);
|
||||
if (ready == null || key.tmdbId.isEmpty) return null;
|
||||
return ref
|
||||
.read(tmdbGatewayProvider)
|
||||
.getEnrichedDetail(
|
||||
ready.config,
|
||||
key.tmdbId,
|
||||
key.mediaType,
|
||||
language: ready.language,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
final tmdbImagesProvider = Provider.family<TmdbImageSet?, TmdbMediaKey>((
|
||||
ref,
|
||||
key,
|
||||
) {
|
||||
return ref.watch(tmdbEnrichedDetailProvider(key)).value?.images;
|
||||
});
|
||||
|
||||
|
||||
final tmdbMediaDetailProvider = Provider.family<TmdbMediaDetail?, TmdbMediaKey>(
|
||||
(ref, key) {
|
||||
return ref.watch(tmdbEnrichedDetailProvider(key)).value?.detail;
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
final tmdbSeasonDetailProvider =
|
||||
FutureProvider.family<TmdbSeasonDetail?, TmdbSeasonKey>((ref, key) async {
|
||||
final ready = await ref.watch(_tmdbReadyProvider.future);
|
||||
if (ready == null || key.tmdbId.isEmpty) return null;
|
||||
return ref
|
||||
.read(tmdbGatewayProvider)
|
||||
.getSeasonDetail(
|
||||
ready.config,
|
||||
key.tmdbId,
|
||||
key.seasonNumber,
|
||||
language: ready.language,
|
||||
);
|
||||
});
|
||||
|
||||
enum _TmdbRecommendationRowKind {
|
||||
trendingDay,
|
||||
trendingWeek,
|
||||
popularMovies,
|
||||
popularTv,
|
||||
airingTodayTv,
|
||||
topRatedMovies,
|
||||
}
|
||||
|
||||
final _tmdbRecommendationRowProvider =
|
||||
AsyncNotifierProvider.family<
|
||||
_TmdbRecommendationRowNotifier,
|
||||
TmdbRecommendationsRes?,
|
||||
_TmdbRecommendationRowKind
|
||||
>(_TmdbRecommendationRowNotifier.new);
|
||||
|
||||
final tmdbTrendingDayProvider = _tmdbRecommendationRowProvider(
|
||||
_TmdbRecommendationRowKind.trendingDay,
|
||||
);
|
||||
|
||||
final tmdbTrendingWeekProvider = _tmdbRecommendationRowProvider(
|
||||
_TmdbRecommendationRowKind.trendingWeek,
|
||||
);
|
||||
|
||||
final tmdbPopularMoviesProvider = _tmdbRecommendationRowProvider(
|
||||
_TmdbRecommendationRowKind.popularMovies,
|
||||
);
|
||||
|
||||
final tmdbPopularTvProvider = _tmdbRecommendationRowProvider(
|
||||
_TmdbRecommendationRowKind.popularTv,
|
||||
);
|
||||
|
||||
final tmdbAiringTodayTvProvider = _tmdbRecommendationRowProvider(
|
||||
_TmdbRecommendationRowKind.airingTodayTv,
|
||||
);
|
||||
|
||||
final tmdbTopRatedMoviesProvider = _tmdbRecommendationRowProvider(
|
||||
_TmdbRecommendationRowKind.topRatedMovies,
|
||||
);
|
||||
|
||||
|
||||
typedef TmdbDiscoverKey = ({
|
||||
String mediaType,
|
||||
String sortBy,
|
||||
String? withGenres,
|
||||
String? voteCountGte,
|
||||
});
|
||||
|
||||
final tmdbDiscoverProvider =
|
||||
AsyncNotifierProvider.family<
|
||||
_TmdbDiscoverRowNotifier,
|
||||
TmdbRecommendationsRes?,
|
||||
TmdbDiscoverKey
|
||||
>(_TmdbDiscoverRowNotifier.new);
|
||||
|
||||
class _TmdbRecommendationRowNotifier
|
||||
extends AsyncNotifier<TmdbRecommendationsRes?> {
|
||||
final _TmdbRecommendationRowKind arg;
|
||||
|
||||
_TmdbRecommendationRowNotifier(this.arg);
|
||||
|
||||
@override
|
||||
Future<TmdbRecommendationsRes?> build() async {
|
||||
final rowKey = _rowKeyForKind(arg);
|
||||
|
||||
final scope = await ref.watch(_tmdbCacheScopeProvider.future);
|
||||
if (scope == null) return null;
|
||||
|
||||
final cached = await _readDiscoverRowCacheByScope(ref, scope, rowKey);
|
||||
if (cached != null) {
|
||||
unawaited(_refreshFromNetwork(arg, rowKey));
|
||||
return cached;
|
||||
}
|
||||
|
||||
final ready = await ref.watch(_tmdbReadyProvider.future);
|
||||
if (ready == null) return null;
|
||||
return _fetchAndCache(arg, ready, rowKey);
|
||||
}
|
||||
|
||||
Future<void> _refreshFromNetwork(
|
||||
_TmdbRecommendationRowKind kind,
|
||||
String rowKey,
|
||||
) async {
|
||||
try {
|
||||
final ready = await ref.read(_tmdbReadyProvider.future);
|
||||
if (ready == null) return;
|
||||
final fresh = await _fetchAndCache(kind, ready, rowKey);
|
||||
if (fresh != null) {
|
||||
state = AsyncValue.data(fresh);
|
||||
}
|
||||
} catch (e) {
|
||||
AppLogger.warn('TMDB', 'refresh discover row $rowKey failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<TmdbRecommendationsRes?> _fetchAndCache(
|
||||
_TmdbRecommendationRowKind kind,
|
||||
_TmdbReady ready,
|
||||
String rowKey,
|
||||
) async {
|
||||
final fresh = await _fetchRow(kind, ready);
|
||||
if (fresh != null) {
|
||||
await _writeDiscoverRowCache(ref, ready, rowKey, fresh);
|
||||
}
|
||||
return fresh;
|
||||
}
|
||||
|
||||
Future<TmdbRecommendationsRes?> _fetchRow(
|
||||
_TmdbRecommendationRowKind kind,
|
||||
_TmdbReady ready,
|
||||
) {
|
||||
final gateway = ref.read(tmdbGatewayProvider);
|
||||
return switch (kind) {
|
||||
_TmdbRecommendationRowKind.trendingDay => gateway.getTrending(
|
||||
ready.config,
|
||||
language: ready.language,
|
||||
mediaType: 'all',
|
||||
timeWindow: 'day',
|
||||
),
|
||||
_TmdbRecommendationRowKind.trendingWeek => gateway.getTrending(
|
||||
ready.config,
|
||||
language: ready.language,
|
||||
mediaType: 'all',
|
||||
timeWindow: 'week',
|
||||
),
|
||||
_TmdbRecommendationRowKind.popularMovies => gateway.getPopularMovies(
|
||||
ready.config,
|
||||
language: ready.language,
|
||||
),
|
||||
_TmdbRecommendationRowKind.popularTv => gateway.getPopularTv(
|
||||
ready.config,
|
||||
language: ready.language,
|
||||
),
|
||||
_TmdbRecommendationRowKind.airingTodayTv => gateway.getAiringTodayTv(
|
||||
ready.config,
|
||||
language: ready.language,
|
||||
),
|
||||
_TmdbRecommendationRowKind.topRatedMovies => gateway.getTopRatedMovies(
|
||||
ready.config,
|
||||
language: ready.language,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class _TmdbDiscoverRowNotifier extends AsyncNotifier<TmdbRecommendationsRes?> {
|
||||
final TmdbDiscoverKey arg;
|
||||
|
||||
_TmdbDiscoverRowNotifier(this.arg);
|
||||
|
||||
@override
|
||||
Future<TmdbRecommendationsRes?> build() async {
|
||||
final rowKey = _rowKeyForDiscover(arg);
|
||||
|
||||
final scope = await ref.watch(_tmdbCacheScopeProvider.future);
|
||||
if (scope == null) return null;
|
||||
|
||||
final cached = await _readDiscoverRowCacheByScope(ref, scope, rowKey);
|
||||
if (cached != null) {
|
||||
unawaited(_refreshFromNetwork(arg, rowKey));
|
||||
return cached;
|
||||
}
|
||||
|
||||
final ready = await ref.watch(_tmdbReadyProvider.future);
|
||||
if (ready == null) return null;
|
||||
return _fetchAndCache(arg, ready, rowKey);
|
||||
}
|
||||
|
||||
Future<void> _refreshFromNetwork(TmdbDiscoverKey key, String rowKey) async {
|
||||
try {
|
||||
final ready = await ref.read(_tmdbReadyProvider.future);
|
||||
if (ready == null) return;
|
||||
final fresh = await _fetchAndCache(key, ready, rowKey);
|
||||
if (fresh != null) {
|
||||
state = AsyncValue.data(fresh);
|
||||
}
|
||||
} catch (e) {
|
||||
AppLogger.warn('TMDB', 'refresh discover row $rowKey failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<TmdbRecommendationsRes?> _fetchAndCache(
|
||||
TmdbDiscoverKey key,
|
||||
_TmdbReady ready,
|
||||
String rowKey,
|
||||
) async {
|
||||
final fresh = await ref
|
||||
.read(tmdbGatewayProvider)
|
||||
.getDiscover(
|
||||
ready.config,
|
||||
key.mediaType,
|
||||
language: ready.language,
|
||||
sortBy: key.sortBy,
|
||||
withGenres: key.withGenres,
|
||||
voteCountGte: key.voteCountGte,
|
||||
);
|
||||
if (fresh != null) {
|
||||
await _writeDiscoverRowCache(ref, ready, rowKey, fresh);
|
||||
}
|
||||
return fresh;
|
||||
}
|
||||
}
|
||||
|
||||
DiscoverPageCacheScope _cacheScopeForReady(_TmdbReady ready) {
|
||||
return DiscoverPageCacheScope(
|
||||
apiBaseUrl: ready.config.apiBaseUrl,
|
||||
imageBaseUrl: ready.config.imageBaseUrl,
|
||||
language: ready.language,
|
||||
);
|
||||
}
|
||||
|
||||
Future<TmdbRecommendationsRes?> _readDiscoverRowCacheByScope(
|
||||
Ref ref,
|
||||
DiscoverPageCacheScope scope,
|
||||
String rowKey,
|
||||
) async {
|
||||
final store = await ref.read(discoverPageCacheStoreProvider.future);
|
||||
final entry = await store.readRow(scope, rowKey);
|
||||
return entry?.data;
|
||||
}
|
||||
|
||||
Future<void> _writeDiscoverRowCache(
|
||||
Ref ref,
|
||||
_TmdbReady ready,
|
||||
String rowKey,
|
||||
TmdbRecommendationsRes data,
|
||||
) async {
|
||||
final store = await ref.read(discoverPageCacheStoreProvider.future);
|
||||
await store.writeRow(_cacheScopeForReady(ready), rowKey, data);
|
||||
}
|
||||
|
||||
String _rowKeyForKind(_TmdbRecommendationRowKind kind) {
|
||||
return switch (kind) {
|
||||
_TmdbRecommendationRowKind.trendingDay => 'trending:day',
|
||||
_TmdbRecommendationRowKind.trendingWeek => 'trending:week',
|
||||
_TmdbRecommendationRowKind.popularMovies => 'popular:movie',
|
||||
_TmdbRecommendationRowKind.popularTv => 'popular:tv',
|
||||
_TmdbRecommendationRowKind.airingTodayTv => 'airing_today:tv',
|
||||
_TmdbRecommendationRowKind.topRatedMovies => 'top_rated:movie',
|
||||
};
|
||||
}
|
||||
|
||||
String _rowKeyForDiscover(TmdbDiscoverKey key) {
|
||||
return [
|
||||
'discover',
|
||||
key.mediaType,
|
||||
key.sortBy,
|
||||
key.withGenres ?? '',
|
||||
key.voteCountGte ?? '',
|
||||
].join(':');
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
part of '../di_providers.dart';
|
||||
|
||||
|
||||
final traktGatewayProvider = Provider<TraktGateway>((ref) => TraktApiClient());
|
||||
|
||||
|
||||
final traktTokenStoreProvider = Provider<TraktTokenStore>(
|
||||
(ref) => TraktTokenStore(),
|
||||
);
|
||||
|
||||
|
||||
final traktAuthProvider = Provider<TraktAuth>((ref) {
|
||||
return TraktAuth(
|
||||
store: ref.read(traktTokenStoreProvider),
|
||||
gateway: ref.read(traktGatewayProvider),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
final authedTraktGatewayProvider = Provider<AuthedTraktGateway>((ref) {
|
||||
return AuthedTraktClient(auth: ref.read(traktAuthProvider));
|
||||
});
|
||||
|
||||
|
||||
final traktSyncQueueProvider = FutureProvider<TraktPendingSyncQueue>((
|
||||
ref,
|
||||
) async {
|
||||
final dir = await ref.watch(appDataDirProvider.future);
|
||||
return TraktPendingSyncQueue(
|
||||
store: JsonTraktSyncJobStore(
|
||||
file: File('${dir.path}/trakt_pending_sync_queue.json'),
|
||||
),
|
||||
sender: ref.read(authedTraktGatewayProvider),
|
||||
policy: TraktRetryPolicy(),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
final traktSyncServiceProvider = FutureProvider<TraktSyncService>((ref) async {
|
||||
final queue = await ref.read(traktSyncQueueProvider.future);
|
||||
return TraktSyncService(
|
||||
traktGateway: ref.read(authedTraktGatewayProvider),
|
||||
queue: queue,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
final traktContinueWatchingProvider =
|
||||
FutureProvider<List<TraktContinueWatchingItem>>((ref) async {
|
||||
final authed = ref.read(authedTraktGatewayProvider);
|
||||
if (!await authed.isConnected()) return const [];
|
||||
|
||||
final ready = await ref.watch(_tmdbReadyProvider.future);
|
||||
if (ready == null) return const [];
|
||||
|
||||
final List<Map<String, dynamic>> raw;
|
||||
try {
|
||||
raw = await authed.fetchPlayback();
|
||||
} catch (e) {
|
||||
AppLogger.warn('Trakt', 'fetchPlayback failed', e);
|
||||
return const [];
|
||||
}
|
||||
|
||||
final entries = raw
|
||||
.map(TraktPlaybackEntry.fromJson)
|
||||
.whereType<TraktPlaybackEntry>()
|
||||
.toList();
|
||||
entries.sort((a, b) {
|
||||
final pa = a.pausedAt;
|
||||
final pb = b.pausedAt;
|
||||
if (pa == null && pb == null) return 0;
|
||||
if (pa == null) return 1;
|
||||
if (pb == null) return -1;
|
||||
return pb.compareTo(pa);
|
||||
});
|
||||
|
||||
final base = <TraktContinueWatchingItem>[];
|
||||
final seenKeys = <String>{};
|
||||
for (final e in entries) {
|
||||
final item = TraktContinueWatchingItem.fromEntry(e);
|
||||
if (item == null) continue;
|
||||
if (!seenKeys.add('${item.mediaType}:${item.tmdbId}')) continue;
|
||||
base.add(item);
|
||||
if (base.length >= 20) break;
|
||||
}
|
||||
if (base.isEmpty) return const [];
|
||||
|
||||
const enrichConcurrency = 5;
|
||||
final gateway = ref.read(tmdbGatewayProvider);
|
||||
final enriched = <TraktContinueWatchingItem>[];
|
||||
for (var i = 0; i < base.length; i += enrichConcurrency) {
|
||||
final chunk = base.skip(i).take(enrichConcurrency);
|
||||
final results = await Future.wait(
|
||||
chunk.map((it) async {
|
||||
try {
|
||||
final detail = await gateway.getMediaDetail(
|
||||
ready.config,
|
||||
it.tmdbId.toString(),
|
||||
it.mediaType,
|
||||
language: ready.language,
|
||||
);
|
||||
final localized = detail?.title;
|
||||
final titled = (localized != null && localized.isNotEmpty)
|
||||
? it.withTitle(localized)
|
||||
: it;
|
||||
final url = TmdbImageUrl.backdrop(
|
||||
ready.config.imageBaseUrl,
|
||||
detail?.backdropPath,
|
||||
size: 'w780',
|
||||
);
|
||||
return titled.withBackdrop(url);
|
||||
} catch (_) {
|
||||
return it;
|
||||
}
|
||||
}),
|
||||
);
|
||||
enriched.addAll(results);
|
||||
}
|
||||
|
||||
return enriched;
|
||||
});
|
||||
Reference in New Issue
Block a user