245 lines
7.1 KiB
Dart
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;
|
|
}
|
|
}
|