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

245 lines
7.1 KiB
Dart

import 'dart:async';
import 'dart:collection';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/contracts/library.dart';
import '../shared/utils/app_logger.dart';
import '../shared/utils/tmdb_key_utils.dart';
import 'di_providers.dart';
import 'library_overview_provider.dart';
import 'playback_active_provider.dart'
show playbackActiveProvider, detailPageActiveProvider;
import 'session_provider.dart';
import 'tmdb_settings_provider.dart';
final tmdbPrefetchProvider = NotifierProvider<TmdbPrefetchNotifier, void>(
TmdbPrefetchNotifier.new,
);
class TmdbPrefetchNotifier extends Notifier<void> {
static const _requestDelay = Duration(milliseconds: 200);
static const _retryDelay = Duration(seconds: 2);
static const _visibleItemCount = 6;
static const _maxRetries = 2;
static const _initialDelay = Duration(milliseconds: 1500);
final Queue<TmdbMediaKey> _queue = Queue<TmdbMediaKey>();
final Set<TmdbMediaKey> _enqueuedKeys = <TmdbMediaKey>{};
final Map<TmdbMediaKey, int> _failureCount = <TmdbMediaKey, int>{};
bool _disposeHookRegistered = false;
bool _disposed = false;
bool _isProcessing = false;
bool _initialDelayElapsed = false;
bool _initialDelayTimerStarted = false;
bool _lastPlaybackActive = false;
bool _lastDetailActive = false;
String? _lastSessionServerId;
List<EmbyRawItem>? _lastResumeRef;
Map<String, List<EmbyRawItem>>? _lastLatestRef;
@override
void build() {
_registerDisposeHook();
final session = ref.watch(activeSessionProvider);
final serverId = session?.serverId;
if (_lastSessionServerId != serverId) {
_lastSessionServerId = serverId;
_resetQueue();
}
final playbackActive = ref.watch(playbackActiveProvider);
final detailActive = ref.watch(detailPageActiveProvider);
final resumedFromPlayback = _lastPlaybackActive && !playbackActive;
_lastPlaybackActive = playbackActive;
final resumedFromDetail = _lastDetailActive && !detailActive;
_lastDetailActive = detailActive;
final settings = ref.watch(tmdbSettingsProvider).value;
if (!(settings?.canRequest ?? false)) return;
final overview = ref.watch(libraryOverviewProvider).value;
if (overview == null) return;
final resumeSame = identical(_lastResumeRef, overview.resumeItems);
final latestSame = identical(_lastLatestRef, overview.latestByLibrary);
if (resumeSame && latestSame) {
if (resumedFromPlayback || resumedFromDetail) {
AppLogger.debug('prefetch', 'tmdb.build.resume_after_pause');
_scheduleProcessing();
} else {
AppLogger.debug('prefetch', 'tmdb.build.skip');
}
return;
}
_lastResumeRef = overview.resumeItems;
_lastLatestRef = overview.latestByLibrary;
final visibleItems = <EmbyRawItem>[
...overview.resumeItems.take(_visibleItemCount),
];
for (final items in overview.latestByLibrary.values) {
visibleItems.addAll(items.take(_visibleItemCount));
}
_enqueueItems(visibleItems, priority: true);
AppLogger.debug('prefetch', 'tmdb.build.enqueued', {
'queue': _queue.length,
'tracked': _enqueuedKeys.length,
});
_scheduleProcessing();
}
void prefetchItem(EmbyRawItem item, {bool priority = false}) {
final key = tmdbMediaKeyFrom(item);
if (key == null) return;
_enqueueKey(key, priority: priority);
_scheduleProcessing();
}
void _registerDisposeHook() {
if (_disposeHookRegistered) return;
_disposeHookRegistered = true;
ref.onDispose(() {
_disposed = true;
_resetQueue();
});
}
void _resetQueue() {
_queue.clear();
_enqueuedKeys.clear();
_failureCount.clear();
_lastResumeRef = null;
_lastLatestRef = null;
}
void _enqueueItems(Iterable<EmbyRawItem> items, {bool priority = false}) {
final keys = <TmdbMediaKey>[];
for (final item in items) {
final key = tmdbMediaKeyFrom(item);
if (key != null) keys.add(key);
}
if (priority) {
for (final key in keys.reversed) {
_enqueueKey(key, priority: true);
}
} else {
for (final key in keys) {
_enqueueKey(key);
}
}
}
void _enqueueKey(TmdbMediaKey key, {bool priority = false}) {
if (_enqueuedKeys.contains(key)) {
if (priority && _queue.remove(key)) {
_queue.addFirst(key);
}
return;
}
_enqueuedKeys.add(key);
if (priority) {
_queue.addFirst(key);
} else {
_queue.add(key);
}
}
bool get _shouldPause =>
ref.read(playbackActiveProvider) || ref.read(detailPageActiveProvider);
void _scheduleProcessing() {
if (_disposed || _isProcessing || _queue.isEmpty) return;
if (_shouldPause) return;
if (!_initialDelayElapsed) {
_startInitialDelay();
return;
}
unawaited(_processQueue());
}
void _startInitialDelay() {
if (_initialDelayTimerStarted) return;
_initialDelayTimerStarted = true;
Future<void>.delayed(_initialDelay, () {
_initialDelayElapsed = true;
if (_disposed) return;
_scheduleProcessing();
});
}
Future<void> _processQueue() async {
if (_isProcessing) return;
_isProcessing = true;
try {
while (!_disposed && _queue.isNotEmpty) {
if (!_canRequestTmdb()) break;
if (_shouldPause) {
AppLogger.debug('prefetch', 'tmdb.queue.suspended', {
'queue': _queue.length,
});
break;
}
final key = _queue.removeFirst();
_enqueuedKeys.remove(key);
bool requeued = false;
try {
final detail = await ref.read(tmdbEnrichedDetailProvider(key).future);
if (_disposed) break;
if (detail == null) {
ref.invalidate(tmdbEnrichedDetailProvider(key));
_failureCount.remove(key);
AppLogger.debug('prefetch', 'tmdb.fetch.empty', {'key': key});
} else {
_failureCount.remove(key);
AppLogger.debug('prefetch', 'tmdb.fetch.ok', {'key': key});
}
} catch (e) {
if (_disposed) break;
ref.invalidate(tmdbEnrichedDetailProvider(key));
final attempts = (_failureCount[key] ?? 0) + 1;
if (attempts <= _maxRetries) {
_failureCount[key] = attempts;
_enqueuedKeys.add(key);
_queue.addLast(key);
requeued = true;
AppLogger.debug('prefetch', 'tmdb.fetch.retry', {
'key': key,
'attempts': attempts,
'error': e.toString(),
});
} else {
_failureCount.remove(key);
AppLogger.debug('prefetch', 'tmdb.fetch.giveup', {
'key': key,
'error': e.toString(),
});
}
}
if (!_disposed && _queue.isNotEmpty) {
await Future<void>.delayed(requeued ? _retryDelay : _requestDelay);
}
}
} finally {
_isProcessing = false;
if (!_disposed &&
_queue.isNotEmpty &&
_canRequestTmdb() &&
!_shouldPause) {
_scheduleProcessing();
}
}
}
bool _canRequestTmdb() {
return ref.read(tmdbSettingsProvider).value?.canRequest ?? false;
}
}