Files
sm-emby-share/lib/providers/di/di_tmdb.dart
T
2026-07-14 11:11:36 +08:00

363 lines
10 KiB
Dart

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(':');
}