217 lines
6.0 KiB
Dart
217 lines
6.0 KiB
Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_riverpod/legacy.dart';
|
|
|
|
import '../core/contracts/trakt/trakt_calendar_entry.dart';
|
|
import '../core/domain/ports/tmdb_gateway.dart';
|
|
import '../shared/mappers/tmdb_image_url.dart';
|
|
import '../shared/utils/app_logger.dart';
|
|
import 'di_providers.dart';
|
|
import 'sm_account_provider.dart';
|
|
import 'tmdb_settings_provider.dart';
|
|
|
|
const _tag = 'TraktCalendar';
|
|
|
|
enum TraktCalendarFilter { all, shows, movies }
|
|
|
|
final traktCalendarFilterProvider = StateProvider<TraktCalendarFilter>(
|
|
(ref) => TraktCalendarFilter.all,
|
|
);
|
|
|
|
|
|
final traktCalendarProvider = FutureProvider<List<TraktCalendarEntry>>((
|
|
ref,
|
|
) async {
|
|
final authed = ref.read(authedTraktGatewayProvider);
|
|
if (!await authed.isConnected()) return const [];
|
|
|
|
final filter = ref.watch(traktCalendarFilterProvider);
|
|
|
|
final entries = <TraktCalendarEntry>[];
|
|
|
|
if (filter != TraktCalendarFilter.movies) {
|
|
try {
|
|
final raw = await authed.fetchCalendarShows();
|
|
entries.addAll(
|
|
raw
|
|
.map(TraktCalendarEntry.fromShowJson)
|
|
.whereType<TraktCalendarEntry>(),
|
|
);
|
|
} catch (e) {
|
|
AppLogger.warn(_tag, 'fetchCalendarShows failed', e);
|
|
}
|
|
}
|
|
|
|
if (filter != TraktCalendarFilter.shows) {
|
|
try {
|
|
final raw = await authed.fetchCalendarMovies();
|
|
entries.addAll(
|
|
raw
|
|
.map(TraktCalendarEntry.fromMovieJson)
|
|
.whereType<TraktCalendarEntry>(),
|
|
);
|
|
} catch (e) {
|
|
AppLogger.warn(_tag, 'fetchCalendarMovies failed', e);
|
|
}
|
|
}
|
|
|
|
entries.sort((a, b) => a.firstAired.compareTo(b.firstAired));
|
|
if (entries.isEmpty) return const [];
|
|
|
|
final tmdbConfig = await _resolveTmdbConfig(ref);
|
|
if (tmdbConfig == null) return entries;
|
|
|
|
final gateway = ref.read(tmdbGatewayProvider);
|
|
final cache = <String, _TmdbPosterResult>{};
|
|
const enrichConcurrency = 5;
|
|
final enriched = <TraktCalendarEntry>[];
|
|
|
|
for (var i = 0; i < entries.length; i += enrichConcurrency) {
|
|
final chunk = entries.skip(i).take(enrichConcurrency);
|
|
final results = await Future.wait(
|
|
chunk.map((entry) async {
|
|
final id = entry.tmdbId;
|
|
if (id == null || id <= 0) return entry;
|
|
|
|
final cacheKey = '${entry.mediaType}:$id';
|
|
try {
|
|
final cached = cache[cacheKey] ??= await _fetchPoster(
|
|
gateway,
|
|
tmdbConfig.config,
|
|
tmdbConfig.language,
|
|
id.toString(),
|
|
entry.mediaType,
|
|
);
|
|
var result = entry;
|
|
if (cached.posterUrl != null) {
|
|
result = result.withPoster(cached.posterUrl);
|
|
}
|
|
if (cached.title != null && cached.title!.isNotEmpty) {
|
|
result = result.withTitle(cached.title!);
|
|
}
|
|
return result;
|
|
} catch (_) {
|
|
return entry;
|
|
}
|
|
}),
|
|
);
|
|
enriched.addAll(results);
|
|
}
|
|
|
|
return enriched;
|
|
});
|
|
|
|
|
|
Future<_TmdbReadyConfig?> _resolveTmdbConfig(Ref 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 _TmdbReadyConfig(
|
|
config: TmdbRequestConfig(
|
|
apiBaseUrl: account.tmdbApiBaseUrl,
|
|
imageBaseUrl: TmdbSettingsState.officialImageBaseUrl,
|
|
accessToken: token,
|
|
apiKey: '',
|
|
onUnauthorized: () => notifier.getAccessToken(force: true),
|
|
),
|
|
language: settings.language,
|
|
);
|
|
}
|
|
|
|
return _TmdbReadyConfig(
|
|
config: TmdbRequestConfig(
|
|
apiBaseUrl: settings.effectiveApiBaseUrl,
|
|
imageBaseUrl: settings.effectiveImageBaseUrl,
|
|
accessToken: settings.effectiveAccessToken,
|
|
apiKey: settings.effectiveApiKey,
|
|
),
|
|
language: settings.language,
|
|
);
|
|
}
|
|
|
|
Future<_TmdbPosterResult> _fetchPoster(
|
|
TmdbGateway gateway,
|
|
TmdbRequestConfig config,
|
|
String language,
|
|
String tmdbId,
|
|
String mediaType,
|
|
) async {
|
|
final detail = await gateway.getMediaDetail(
|
|
config,
|
|
tmdbId,
|
|
mediaType,
|
|
language: language,
|
|
);
|
|
final posterUrl = TmdbImageUrl.poster(
|
|
config.imageBaseUrl,
|
|
detail?.posterPath,
|
|
size: 'w342',
|
|
);
|
|
return _TmdbPosterResult(posterUrl: posterUrl, title: detail?.title);
|
|
}
|
|
|
|
class _TmdbReadyConfig {
|
|
final TmdbRequestConfig config;
|
|
final String language;
|
|
const _TmdbReadyConfig({required this.config, required this.language});
|
|
}
|
|
|
|
class _TmdbPosterResult {
|
|
final String? posterUrl;
|
|
final String? title;
|
|
const _TmdbPosterResult({this.posterUrl, this.title});
|
|
}
|
|
|
|
|
|
List<CalendarDateGroup> groupByDate(List<TraktCalendarEntry> entries) {
|
|
final now = DateTime.now();
|
|
final today = DateTime(now.year, now.month, now.day);
|
|
final groups = <String, List<TraktCalendarEntry>>{};
|
|
final groupOrder = <String, DateTime>{};
|
|
|
|
for (final e in entries) {
|
|
final local = e.firstAired.toLocal();
|
|
final date = DateTime(local.year, local.month, local.day);
|
|
final label = _dateLabel(date, today);
|
|
(groups[label] ??= []).add(e);
|
|
groupOrder.putIfAbsent(label, () => date);
|
|
}
|
|
|
|
final sorted = groups.entries.toList()
|
|
..sort((a, b) => groupOrder[a.key]!.compareTo(groupOrder[b.key]!));
|
|
|
|
return [
|
|
for (final e in sorted) CalendarDateGroup(label: e.key, entries: e.value),
|
|
];
|
|
}
|
|
|
|
String _dateLabel(DateTime date, DateTime today) {
|
|
final diff = date.difference(today).inDays;
|
|
if (diff == 0) return '今天';
|
|
if (diff == 1) return '明天';
|
|
final weekday = _weekdayName(date.weekday);
|
|
return '$weekday ${date.month}/${date.day}';
|
|
}
|
|
|
|
String _weekdayName(int weekday) => switch (weekday) {
|
|
1 => '周一',
|
|
2 => '周二',
|
|
3 => '周三',
|
|
4 => '周四',
|
|
5 => '周五',
|
|
6 => '周六',
|
|
_ => '周日',
|
|
};
|
|
|
|
class CalendarDateGroup {
|
|
final String label;
|
|
final List<TraktCalendarEntry> entries;
|
|
|
|
const CalendarDateGroup({required this.label, required this.entries});
|
|
}
|