Initial commit
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user