feat: sync rules for downloads

close #867
This commit is contained in:
edde746
2026-04-16 12:52:48 +02:00
parent c369f2c3d7
commit 360741674d
47 changed files with 2546 additions and 154 deletions
+50 -13
View File
@@ -44,8 +44,10 @@ import 'services/download_storage_service.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'services/plex_api_cache.dart';
import 'database/app_database.dart';
import 'screens/video_player_screen.dart';
import 'utils/app_logger.dart';
import 'utils/orientation_helper.dart';
import 'utils/watch_state_notifier.dart';
import 'i18n/strings.g.dart';
import 'focus/input_mode_tracker.dart';
import 'focus/key_event_utils.dart';
@@ -362,6 +364,9 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
late final DownloadManagerService _downloadManager;
late final OfflineWatchSyncService _offlineWatchSyncService;
late final AppLifecycleListener _appLifecycleListener;
StreamSubscription<WatchStateEvent>? _watchStateSubscription;
Timer? _syncDebounce;
bool _isAutoDeleteRunning = false;
/// Last time server health probes ran from a resume event (cooldown for desktop)
DateTime _lastResumeProbe = DateTime(0);
@@ -411,6 +416,8 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
@override
void dispose() {
_syncDebounce?.cancel();
_watchStateSubscription?.cancel();
_memoryCheckTimer?.cancel();
_appLifecycleListener.dispose();
WidgetsBinding.instance.removeObserver(this);
@@ -429,6 +436,34 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
PaintingBinding.instance.imageCache.clearLiveImages();
}
/// Auto-delete watched downloads and execute sync rules.
/// Shared by onWatchStatesRefreshed and the WatchStateNotifier listener.
Future<void> _autoDeleteAndSync(DownloadProvider downloadProvider) async {
if (_isAutoDeleteRunning) return;
_isAutoDeleteRunning = true;
try {
await downloadProvider.refreshMetadataFromCache();
final activeKey = VideoPlayerScreenState.activeRatingKey;
final settings = SettingsService.instanceOrNull;
if (settings != null && settings.getAutoRemoveWatchedDownloads()) {
final deleted = await downloadProvider.autoDeleteWatchedDownloads(activeRatingKey: activeKey);
if (deleted.isNotEmpty) {
final msg = deleted.length == 1
? t.messages.autoRemovedWatchedDownload(title: deleted.first)
: t.messages.autoRemovedWatchedDownload(title: '${deleted.length} items');
showGlobalSnackBar(msg);
}
}
final synced = await downloadProvider.executeSyncRules(_serverManager);
if (synced.isNotEmpty) {
showGlobalSnackBar(t.downloads.syncedNewEpisodes(count: synced.length.toString(), title: synced.first));
}
} finally {
_isAutoDeleteRunning = false;
}
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
@@ -486,28 +521,30 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
},
),
// Download provider
ChangeNotifierProvider(create: (context) => DownloadProvider(downloadManager: _downloadManager)),
ChangeNotifierProvider(
create: (context) => DownloadProvider(downloadManager: _downloadManager, database: _appDatabase)),
// Offline watch sync service
ChangeNotifierProvider<OfflineWatchSyncService>(
create: (context) {
final offlineModeProvider = context.read<OfflineModeProvider>();
final downloadProvider = context.read<DownloadProvider>();
// Wire up callback to refresh download provider after watch state sync
_offlineWatchSyncService.onWatchStatesRefreshed = () async {
await downloadProvider.refreshMetadataFromCache();
final settings = SettingsService.instanceOrNull;
if (settings != null && settings.getAutoRemoveWatchedDownloads()) {
final deleted = await downloadProvider.autoDeleteWatchedDownloads();
if (deleted.isNotEmpty) {
final msg = deleted.length == 1
? t.messages.autoRemovedWatchedDownload(title: deleted.first)
: t.messages.autoRemovedWatchedDownload(title: '${deleted.length} items');
showGlobalSnackBar(msg);
}
}
await _autoDeleteAndSync(downloadProvider);
};
// Also trigger sync rules when watch state changes during a session.
// Debounced to batch rapid changes (binge watching, bulk mark-watched).
_watchStateSubscription = WatchStateNotifier().stream.listen((event) {
if (event.changeType != WatchStateChangeType.watched) return;
if (VideoPlayerScreenState.activeRatingKey == event.ratingKey) return;
_syncDebounce?.cancel();
_syncDebounce = Timer(const Duration(seconds: 5), () {
_autoDeleteAndSync(downloadProvider);
});
});
_offlineWatchSyncService.startConnectivityMonitoring(offlineModeProvider);
return _offlineWatchSyncService;
},