diff --git a/lib/common/rust/api/downloader_api.dart b/lib/common/rust/api/downloader_api.dart index 07e95ff..6443be9 100644 --- a/lib/common/rust/api/downloader_api.dart +++ b/lib/common/rust/api/downloader_api.dart @@ -32,6 +32,18 @@ Future downloaderInit({ bool downloaderIsInitialized() => RustLib.instance.api.crateApiDownloaderApiDownloaderIsInitialized(); +/// Check if there are pending tasks to restore from session file (without starting the downloader) +/// This reads the session.json file directly to check if there are any torrents saved. +/// +/// Parameters: +/// - working_dir: The directory where session data is stored (same as passed to downloader_init) +/// +/// Returns: true if there are tasks to restore, false otherwise +bool downloaderHasPendingSessionTasks({required String workingDir}) => + RustLib.instance.api.crateApiDownloaderApiDownloaderHasPendingSessionTasks( + workingDir: workingDir, + ); + /// Add a torrent from bytes (e.g., .torrent file content) Future downloaderAddTorrent({ required List torrentBytes, @@ -118,6 +130,17 @@ Future downloaderStop() => Future downloaderShutdown() => RustLib.instance.api.crateApiDownloaderApiDownloaderShutdown(); +/// Get all completed tasks from cache (tasks removed by downloader_remove_completed_tasks) +/// This cache is cleared when the downloader is shutdown/restarted +List downloaderGetCompletedTasksCache() => RustLib + .instance + .api + .crateApiDownloaderApiDownloaderGetCompletedTasksCache(); + +/// Clear the completed tasks cache manually +void downloaderClearCompletedTasksCache() => RustLib.instance.api + .crateApiDownloaderApiDownloaderClearCompletedTasksCache(); + /// Update global speed limits /// Note: rqbit Session doesn't support runtime limit changes, /// this function is a placeholder that returns an error. @@ -131,6 +154,7 @@ Future downloaderUpdateSpeedLimits({ ); /// Remove all completed tasks (equivalent to aria2's --seed-time=0 behavior) +/// Removed tasks are cached in memory and can be queried via downloader_get_completed_tasks_cache Future downloaderRemoveCompletedTasks() => RustLib.instance.api.crateApiDownloaderApiDownloaderRemoveCompletedTasks(); diff --git a/lib/common/rust/api/win32_api.dart b/lib/common/rust/api/win32_api.dart index d0c7f96..cda3105 100644 --- a/lib/common/rust/api/win32_api.dart +++ b/lib/common/rust/api/win32_api.dart @@ -6,6 +6,7 @@ import '../frb_generated.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; +// These functions are ignored because they are not marked as `pub`: `get_process_path` // These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `fmt`, `fmt` Future sendNotify({ @@ -20,21 +21,27 @@ Future sendNotify({ appId: appId, ); +/// Get system memory size in GB Future getSystemMemorySizeGb() => RustLib.instance.api.crateApiWin32ApiGetSystemMemorySizeGb(); +/// Get number of logical processors Future getNumberOfLogicalProcessors() => RustLib.instance.api.crateApiWin32ApiGetNumberOfLogicalProcessors(); +/// Get all system information at once Future getSystemInfo() => RustLib.instance.api.crateApiWin32ApiGetSystemInfo(); +/// Get GPU info from registry (more accurate VRAM) Future getGpuInfoFromRegistry() => RustLib.instance.api.crateApiWin32ApiGetGpuInfoFromRegistry(); +/// Resolve shortcut (.lnk) file to get target path Future resolveShortcut({required String lnkPath}) => RustLib.instance.api.crateApiWin32ApiResolveShortcut(lnkPath: lnkPath); +/// Open file explorer and select file/folder Future openDirWithExplorer({ required String path, required bool isFile, @@ -58,16 +65,19 @@ Future> getProcessListByName({required String processName}) => processName: processName, ); +/// Kill processes by name Future killProcessByName({required String processName}) => RustLib .instance .api .crateApiWin32ApiKillProcessByName(processName: processName); +/// Get disk physical sector size for performance Future getDiskPhysicalSectorSize({required String driveLetter}) => RustLib .instance .api .crateApiWin32ApiGetDiskPhysicalSectorSize(driveLetter: driveLetter); +/// Create a desktop shortcut Future createDesktopShortcut({ required String targetPath, required String shortcutName, @@ -76,12 +86,14 @@ Future createDesktopShortcut({ shortcutName: shortcutName, ); +/// Run a program with admin privileges (UAC) Future runAsAdmin({required String program, required String args}) => RustLib.instance.api.crateApiWin32ApiRunAsAdmin( program: program, args: args, ); +/// Start a program (without waiting) Future startProcess({ required String program, required List args, @@ -90,12 +102,15 @@ Future startProcess({ args: args, ); +/// Check if NVME patch is applied Future checkNvmePatchStatus() => RustLib.instance.api.crateApiWin32ApiCheckNvmePatchStatus(); +/// Add NVME patch to registry Future addNvmePatch() => RustLib.instance.api.crateApiWin32ApiAddNvmePatch(); +/// Remove NVME patch from registry Future removeNvmePatch() => RustLib.instance.api.crateApiWin32ApiRemoveNvmePatch(); diff --git a/lib/common/rust/frb_generated.dart b/lib/common/rust/frb_generated.dart index 6531c50..d6a8ec0 100644 --- a/lib/common/rust/frb_generated.dart +++ b/lib/common/rust/frb_generated.dart @@ -72,7 +72,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.11.1'; @override - int get rustContentHash => -641930410; + int get rustContentHash => -1482626931; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -118,8 +118,13 @@ abstract class RustLibApi extends BaseApi { List? trackers, }); + void crateApiDownloaderApiDownloaderClearCompletedTasksCache(); + Future> crateApiDownloaderApiDownloaderGetAllTasks(); + List + crateApiDownloaderApiDownloaderGetCompletedTasksCache(); + Future crateApiDownloaderApiDownloaderGetGlobalStats(); Future crateApiDownloaderApiDownloaderGetTaskInfo({ @@ -128,6 +133,10 @@ abstract class RustLibApi extends BaseApi { Future crateApiDownloaderApiDownloaderHasActiveTasks(); + bool crateApiDownloaderApiDownloaderHasPendingSessionTasks({ + required String workingDir, + }); + Future crateApiDownloaderApiDownloaderInit({ required String workingDir, required String defaultDownloadDir, @@ -633,6 +642,33 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["url", "outputFolder", "trackers"], ); + @override + void crateApiDownloaderApiDownloaderClearCompletedTasksCache() { + return handler.executeSync( + SyncTask( + callFfi: () { + return wire + .wire__crate__api__downloader_api__downloader_clear_completed_tasks_cache(); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_unit, + decodeErrorData: null, + ), + constMeta: + kCrateApiDownloaderApiDownloaderClearCompletedTasksCacheConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiDownloaderApiDownloaderClearCompletedTasksCacheConstMeta => + const TaskConstMeta( + debugName: "downloader_clear_completed_tasks_cache", + argNames: [], + ); + @override Future> crateApiDownloaderApiDownloaderGetAllTasks() { return handler.executeNormal( @@ -657,6 +693,34 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { TaskConstMeta get kCrateApiDownloaderApiDownloaderGetAllTasksConstMeta => const TaskConstMeta(debugName: "downloader_get_all_tasks", argNames: []); + @override + List + crateApiDownloaderApiDownloaderGetCompletedTasksCache() { + return handler.executeSync( + SyncTask( + callFfi: () { + return wire + .wire__crate__api__downloader_api__downloader_get_completed_tasks_cache(); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_list_download_task_info, + decodeErrorData: null, + ), + constMeta: + kCrateApiDownloaderApiDownloaderGetCompletedTasksCacheConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiDownloaderApiDownloaderGetCompletedTasksCacheConstMeta => + const TaskConstMeta( + debugName: "downloader_get_completed_tasks_cache", + argNames: [], + ); + @override Future crateApiDownloaderApiDownloaderGetGlobalStats() { return handler.executeNormal( @@ -742,6 +806,38 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: [], ); + @override + bool crateApiDownloaderApiDownloaderHasPendingSessionTasks({ + required String workingDir, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + var arg0 = cst_encode_String(workingDir); + return wire + .wire__crate__api__downloader_api__downloader_has_pending_session_tasks( + arg0, + ); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_bool, + decodeErrorData: null, + ), + constMeta: + kCrateApiDownloaderApiDownloaderHasPendingSessionTasksConstMeta, + argValues: [workingDir], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiDownloaderApiDownloaderHasPendingSessionTasksConstMeta => + const TaskConstMeta( + debugName: "downloader_has_pending_session_tasks", + argNames: ["workingDir"], + ); + @override Future crateApiDownloaderApiDownloaderInit({ required String workingDir, diff --git a/lib/common/rust/frb_generated.io.dart b/lib/common/rust/frb_generated.io.dart index 5d4e1ae..9780a83 100644 --- a/lib/common/rust/frb_generated.io.dart +++ b/lib/common/rust/frb_generated.io.dart @@ -1321,6 +1321,19 @@ class RustLibWire implements BaseWire { ) >(); + WireSyncRust2DartDco + wire__crate__api__downloader_api__downloader_clear_completed_tasks_cache() { + return _wire__crate__api__downloader_api__downloader_clear_completed_tasks_cache(); + } + + late final _wire__crate__api__downloader_api__downloader_clear_completed_tasks_cachePtr = + _lookup>( + 'frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_clear_completed_tasks_cache', + ); + late final _wire__crate__api__downloader_api__downloader_clear_completed_tasks_cache = + _wire__crate__api__downloader_api__downloader_clear_completed_tasks_cachePtr + .asFunction(); + void wire__crate__api__downloader_api__downloader_get_all_tasks(int port_) { return _wire__crate__api__downloader_api__downloader_get_all_tasks(port_); } @@ -1333,6 +1346,19 @@ class RustLibWire implements BaseWire { _wire__crate__api__downloader_api__downloader_get_all_tasksPtr .asFunction(); + WireSyncRust2DartDco + wire__crate__api__downloader_api__downloader_get_completed_tasks_cache() { + return _wire__crate__api__downloader_api__downloader_get_completed_tasks_cache(); + } + + late final _wire__crate__api__downloader_api__downloader_get_completed_tasks_cachePtr = + _lookup>( + 'frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_get_completed_tasks_cache', + ); + late final _wire__crate__api__downloader_api__downloader_get_completed_tasks_cache = + _wire__crate__api__downloader_api__downloader_get_completed_tasks_cachePtr + .asFunction(); + void wire__crate__api__downloader_api__downloader_get_global_stats( int port_, ) { @@ -1383,6 +1409,33 @@ class RustLibWire implements BaseWire { _wire__crate__api__downloader_api__downloader_has_active_tasksPtr .asFunction(); + WireSyncRust2DartDco + wire__crate__api__downloader_api__downloader_has_pending_session_tasks( + ffi.Pointer working_dir, + ) { + return _wire__crate__api__downloader_api__downloader_has_pending_session_tasks( + working_dir, + ); + } + + late final _wire__crate__api__downloader_api__downloader_has_pending_session_tasksPtr = + _lookup< + ffi.NativeFunction< + WireSyncRust2DartDco Function( + ffi.Pointer, + ) + > + >( + 'frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_has_pending_session_tasks', + ); + late final _wire__crate__api__downloader_api__downloader_has_pending_session_tasks = + _wire__crate__api__downloader_api__downloader_has_pending_session_tasksPtr + .asFunction< + WireSyncRust2DartDco Function( + ffi.Pointer, + ) + >(); + void wire__crate__api__downloader_api__downloader_init( int port_, ffi.Pointer working_dir, @@ -2083,9 +2136,9 @@ class RustLibWire implements BaseWire { void wire__crate__api__win32_api__resolve_shortcut( int port_, - ffi.Pointer _lnk_path, + ffi.Pointer lnk_path, ) { - return _wire__crate__api__win32_api__resolve_shortcut(port_, _lnk_path); + return _wire__crate__api__win32_api__resolve_shortcut(port_, lnk_path); } late final _wire__crate__api__win32_api__resolve_shortcutPtr = diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 09793d3..b653564 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -28,9 +28,10 @@ class S { static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); static Future load(Locale locale) { - final name = (locale.countryCode?.isEmpty ?? false) - ? locale.languageCode - : locale.toString(); + final name = + (locale.countryCode?.isEmpty ?? false) + ? locale.languageCode + : locale.toString(); final localeName = Intl.canonicalizedLocale(name); return initializeMessages(localeName).then((_) { Intl.defaultLocale = localeName; diff --git a/lib/provider/download_manager.dart b/lib/provider/download_manager.dart index df060ba..6d2f2c4 100644 --- a/lib/provider/download_manager.dart +++ b/lib/provider/download_manager.dart @@ -52,14 +52,14 @@ class DownloadManager extends _$DownloadManager { // Lazy load init () async { + await Future.delayed(const Duration(milliseconds: 16)); try { - // Check if there are existing tasks (check working dir for session data) - final dir = Directory(workingDir); - if (await dir.exists()) { - dPrint("Launch download manager"); + // Check if there are pending tasks to restore (without starting the downloader) + if (downloader_api.downloaderHasPendingSessionTasks(workingDir: workingDir)) { + dPrint("Launch download manager - found pending session tasks"); await initDownloader(); } else { - dPrint("LazyLoad download manager"); + dPrint("LazyLoad download manager - no pending tasks"); } } catch (e) { dPrint("DownloadManager.checkLazyLoad Error:$e"); @@ -246,4 +246,15 @@ class DownloadManager extends _$DownloadManager { } return await downloader_api.downloaderHasActiveTasks(); } + + /// Get all completed tasks from cache (tasks that were removed by removeCompletedTasks) + /// This cache is cleared when the downloader is shutdown/restarted + List getCompletedTasksCache() { + return downloader_api.downloaderGetCompletedTasksCache(); + } + + /// Clear the completed tasks cache manually + void clearCompletedTasksCache() { + downloader_api.downloaderClearCompletedTasksCache(); + } } diff --git a/lib/provider/download_manager.g.dart b/lib/provider/download_manager.g.dart index 8a98e08..46bde05 100644 --- a/lib/provider/download_manager.g.dart +++ b/lib/provider/download_manager.g.dart @@ -41,7 +41,7 @@ final class DownloadManagerProvider } } -String _$downloadManagerHash() => r'f12d3fb1d7c03fdfccff7d07903218f38a860437'; +String _$downloadManagerHash() => r'55c92224a5eb6bb0f84f0a97fd0585b94f61f711'; abstract class _$DownloadManager extends $Notifier { DownloadManagerState build(); diff --git a/lib/ui/home/downloader/home_downloader_ui.dart b/lib/ui/home/downloader/home_downloader_ui.dart index 4b4cf96..eb74f33 100644 --- a/lib/ui/home/downloader/home_downloader_ui.dart +++ b/lib/ui/home/downloader/home_downloader_ui.dart @@ -153,7 +153,7 @@ class HomeDownloaderUI extends HookConsumerWidget { const SizedBox(width: 32), if (type != "stopped") DropDownButton( - closeAfterClick: false, + closeAfterClick: true, title: Padding( padding: const EdgeInsets.all(3), child: Text(S.current.downloader_action_options), diff --git a/lib/ui/home/downloader/home_downloader_ui_model.g.dart b/lib/ui/home/downloader/home_downloader_ui_model.g.dart index 65a31d9..a2205b7 100644 --- a/lib/ui/home/downloader/home_downloader_ui_model.g.dart +++ b/lib/ui/home/downloader/home_downloader_ui_model.g.dart @@ -42,7 +42,7 @@ final class HomeDownloaderUIModelProvider } String _$homeDownloaderUIModelHash() => - r'567cf106d69ed24a5adb8d7f4ad9c422cf33dc1e'; + r'bf7d095d761fff078de707562cf311c20db664d9'; abstract class _$HomeDownloaderUIModel extends $Notifier { diff --git a/rust/Cargo.lock b/rust/Cargo.lock index ddc0de3..77c7d80 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2994,7 +2994,7 @@ dependencies = [ [[package]] name = "librqbit" version = "9.0.0-beta.1" -source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.2#7a9b4d7db84b7b9cccc424e294610cc800a9baa4" +source = "git+https://github.com/StarCitizenToolBox/rqbit?rev=f8c0b0927904e1d8b0e28e708bd69fd8069d413a#f8c0b0927904e1d8b0e28e708bd69fd8069d413a" dependencies = [ "anyhow", "arc-swap", @@ -3058,7 +3058,7 @@ dependencies = [ [[package]] name = "librqbit-bencode" version = "3.1.0" -source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.2#7a9b4d7db84b7b9cccc424e294610cc800a9baa4" +source = "git+https://github.com/StarCitizenToolBox/rqbit?rev=f8c0b0927904e1d8b0e28e708bd69fd8069d413a#f8c0b0927904e1d8b0e28e708bd69fd8069d413a" dependencies = [ "anyhow", "arrayvec", @@ -3074,7 +3074,7 @@ dependencies = [ [[package]] name = "librqbit-buffers" version = "4.2.0" -source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.2#7a9b4d7db84b7b9cccc424e294610cc800a9baa4" +source = "git+https://github.com/StarCitizenToolBox/rqbit?rev=f8c0b0927904e1d8b0e28e708bd69fd8069d413a#f8c0b0927904e1d8b0e28e708bd69fd8069d413a" dependencies = [ "bytes", "librqbit-clone-to-owned", @@ -3085,7 +3085,7 @@ dependencies = [ [[package]] name = "librqbit-clone-to-owned" version = "3.0.1" -source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.2#7a9b4d7db84b7b9cccc424e294610cc800a9baa4" +source = "git+https://github.com/StarCitizenToolBox/rqbit?rev=f8c0b0927904e1d8b0e28e708bd69fd8069d413a#f8c0b0927904e1d8b0e28e708bd69fd8069d413a" dependencies = [ "bytes", ] @@ -3093,7 +3093,7 @@ dependencies = [ [[package]] name = "librqbit-core" version = "5.0.0" -source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.2#7a9b4d7db84b7b9cccc424e294610cc800a9baa4" +source = "git+https://github.com/StarCitizenToolBox/rqbit?rev=f8c0b0927904e1d8b0e28e708bd69fd8069d413a#f8c0b0927904e1d8b0e28e708bd69fd8069d413a" dependencies = [ "anyhow", "bytes", @@ -3122,7 +3122,7 @@ dependencies = [ [[package]] name = "librqbit-dht" version = "5.3.0" -source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.2#7a9b4d7db84b7b9cccc424e294610cc800a9baa4" +source = "git+https://github.com/StarCitizenToolBox/rqbit?rev=f8c0b0927904e1d8b0e28e708bd69fd8069d413a#f8c0b0927904e1d8b0e28e708bd69fd8069d413a" dependencies = [ "anyhow", "backon", @@ -3169,7 +3169,7 @@ dependencies = [ [[package]] name = "librqbit-lsd" version = "0.1.0" -source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.2#7a9b4d7db84b7b9cccc424e294610cc800a9baa4" +source = "git+https://github.com/StarCitizenToolBox/rqbit?rev=f8c0b0927904e1d8b0e28e708bd69fd8069d413a#f8c0b0927904e1d8b0e28e708bd69fd8069d413a" dependencies = [ "anyhow", "atoi", @@ -3189,7 +3189,7 @@ dependencies = [ [[package]] name = "librqbit-peer-protocol" version = "4.3.0" -source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.2#7a9b4d7db84b7b9cccc424e294610cc800a9baa4" +source = "git+https://github.com/StarCitizenToolBox/rqbit?rev=f8c0b0927904e1d8b0e28e708bd69fd8069d413a#f8c0b0927904e1d8b0e28e708bd69fd8069d413a" dependencies = [ "anyhow", "bitvec", @@ -3209,7 +3209,7 @@ dependencies = [ [[package]] name = "librqbit-sha1-wrapper" version = "4.1.0" -source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.2#7a9b4d7db84b7b9cccc424e294610cc800a9baa4" +source = "git+https://github.com/StarCitizenToolBox/rqbit?rev=f8c0b0927904e1d8b0e28e708bd69fd8069d413a#f8c0b0927904e1d8b0e28e708bd69fd8069d413a" dependencies = [ "assert_cfg", "aws-lc-rs", @@ -3218,7 +3218,7 @@ dependencies = [ [[package]] name = "librqbit-tracker-comms" version = "3.0.0" -source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.2#7a9b4d7db84b7b9cccc424e294610cc800a9baa4" +source = "git+https://github.com/StarCitizenToolBox/rqbit?rev=f8c0b0927904e1d8b0e28e708bd69fd8069d413a#f8c0b0927904e1d8b0e28e708bd69fd8069d413a" dependencies = [ "anyhow", "async-stream", @@ -3246,7 +3246,7 @@ dependencies = [ [[package]] name = "librqbit-upnp" version = "1.0.0" -source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.2#7a9b4d7db84b7b9cccc424e294610cc800a9baa4" +source = "git+https://github.com/StarCitizenToolBox/rqbit?rev=f8c0b0927904e1d8b0e28e708bd69fd8069d413a#f8c0b0927904e1d8b0e28e708bd69fd8069d413a" dependencies = [ "anyhow", "bstr", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 500bca5..c3ab63e 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -35,7 +35,7 @@ unp4k_rs = { git = "https://github.com/StarCitizenToolBox/unp4k_rs", tag = "V0.0 uuid = { version = "1.19.0", features = ["v4"] } parking_lot = "0.12.5" crossbeam-channel = "0.5.15" -librqbit = { git = "https://github.com/StarCitizenToolBox/rqbit", tag = "webseed-v0.0.2" } +librqbit = { git = "https://github.com/StarCitizenToolBox/rqbit", rev = "f8c0b0927904e1d8b0e28e708bd69fd8069d413a" } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } bytes = "1.10" diff --git a/rust/src/api/downloader_api.rs b/rust/src/api/downloader_api.rs index 4b562eb..454771e 100644 --- a/rust/src/api/downloader_api.rs +++ b/rust/src/api/downloader_api.rs @@ -26,9 +26,9 @@ static SESSION_INIT_LOCK: once_cell::sync::Lazy> = static TORRENT_HANDLES: once_cell::sync::Lazy>> = once_cell::sync::Lazy::new(|| RwLock::new(HashMap::new())); -// Store output folders for each task -static TASK_OUTPUT_FOLDERS: once_cell::sync::Lazy>> = - once_cell::sync::Lazy::new(|| RwLock::new(HashMap::new())); +// Store completed tasks info (in-memory cache, cleared on restart) +static COMPLETED_TASKS_CACHE: once_cell::sync::Lazy>> = + once_cell::sync::Lazy::new(|| RwLock::new(Vec::new())); /// Download task status #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -132,12 +132,15 @@ pub async fn downloader_init( }, webseed_config: Some(WebSeedConfig{ max_concurrent_per_source: 32, - max_total_concurrent: 128, + max_total_concurrent: 64, request_timeout_secs: 30, prefer_for_large_gaps: true, min_gap_for_webseed: 10, max_errors_before_disable: 10, disable_cooldown_secs: 600, + adaptive_increase_threshold: 5, + adaptive_decrease_threshold: 10, + ..Default::default() }), ..Default::default() }, @@ -156,6 +159,44 @@ pub fn downloader_is_initialized() -> bool { SESSION.read().is_some() } +/// Check if there are pending tasks to restore from session file (without starting the downloader) +/// This reads the session.json file directly to check if there are any torrents saved. +/// +/// Parameters: +/// - working_dir: The directory where session data is stored (same as passed to downloader_init) +/// +/// Returns: true if there are tasks to restore, false otherwise +#[frb(sync)] +pub fn downloader_has_pending_session_tasks(working_dir: String) -> bool { + let session_file = PathBuf::from(&working_dir) + .join("rqbit-session") + .join("session.json"); + + if !session_file.exists() { + return false; + } + + // Try to read and parse the session file + match std::fs::read_to_string(&session_file) { + Ok(content) => { + // Parse as JSON to check if there are any torrents + // The structure is: { "torrents": { "0": {...}, "1": {...} } } + match serde_json::from_str::(&content) { + Ok(json) => { + if let Some(torrents) = json.get("torrents") { + if let Some(obj) = torrents.as_object() { + return !obj.is_empty(); + } + } + false + } + Err(_) => false, + } + } + Err(_) => false, + } +} + /// Helper function to get session fn get_session() -> Result> { SESSION.read() @@ -195,17 +236,10 @@ pub async fn downloader_add_torrent( match response { AddTorrentResponse::Added(id, handle) => { - // Store output folder - if let Some(folder) = output_folder.clone() { - TASK_OUTPUT_FOLDERS.write().insert(id, folder); - } TORRENT_HANDLES.write().insert(id, handle); Ok(id) } AddTorrentResponse::AlreadyManaged(id, handle) => { - if let Some(folder) = output_folder.clone() { - TASK_OUTPUT_FOLDERS.write().insert(id, folder); - } TORRENT_HANDLES.write().insert(id, handle); Ok(id) } @@ -251,16 +285,10 @@ pub async fn downloader_add_magnet( match response { AddTorrentResponse::Added(id, handle) => { - if let Some(folder) = output_folder.clone() { - TASK_OUTPUT_FOLDERS.write().insert(id, folder); - } TORRENT_HANDLES.write().insert(id, handle); Ok(id) } AddTorrentResponse::AlreadyManaged(id, handle) => { - if let Some(folder) = output_folder.clone() { - TASK_OUTPUT_FOLDERS.write().insert(id, folder); - } TORRENT_HANDLES.write().insert(id, handle); Ok(id) } @@ -364,11 +392,13 @@ pub async fn downloader_get_task_info(task_id: usize) -> Result 0 { @@ -440,11 +470,13 @@ pub async fn downloader_get_all_tasks() -> Result> { for (id, handle) in torrents { let stats = handle.stats(); let name = handle.name().unwrap_or_else(|| format!("Task {}", id)); - let output_folder = TASK_OUTPUT_FOLDERS - .read() - .get(&id) - .cloned() - .unwrap_or_default(); + // Get output_folder from handle's shared options + let output_folder = handle + .shared() + .options + .output_folder + .to_string_lossy() + .into_owned(); let status = get_task_status(&stats); let progress = if stats.total_bytes > 0 { @@ -552,7 +584,6 @@ pub async fn downloader_stop() -> Result<()> { session.stop().await; } TORRENT_HANDLES.write().clear(); - TASK_OUTPUT_FOLDERS.write().clear(); Ok(()) } @@ -568,10 +599,24 @@ pub async fn downloader_shutdown() -> Result<()> { } TORRENT_HANDLES.write().clear(); - TASK_OUTPUT_FOLDERS.write().clear(); + // Clear completed tasks cache on shutdown + COMPLETED_TASKS_CACHE.write().clear(); Ok(()) } +/// Get all completed tasks from cache (tasks removed by downloader_remove_completed_tasks) +/// This cache is cleared when the downloader is shutdown/restarted +#[frb(sync)] +pub fn downloader_get_completed_tasks_cache() -> Vec { + COMPLETED_TASKS_CACHE.read().clone() +} + +/// Clear the completed tasks cache manually +#[frb(sync)] +pub fn downloader_clear_completed_tasks_cache() { + COMPLETED_TASKS_CACHE.write().clear(); +} + /// Update global speed limits /// Note: rqbit Session doesn't support runtime limit changes, /// this function is a placeholder that returns an error. @@ -586,6 +631,7 @@ pub async fn downloader_update_speed_limits( } /// Remove all completed tasks (equivalent to aria2's --seed-time=0 behavior) +/// Removed tasks are cached in memory and can be queried via downloader_get_completed_tasks_cache pub async fn downloader_remove_completed_tasks() -> Result { let session = get_session()?; @@ -599,8 +645,10 @@ pub async fn downloader_remove_completed_tasks() -> Result { if has_handle { // Use TorrentIdOrHash::Id for deletion (TorrentId is just usize) if session.delete(TorrentIdOrHash::Id(task.id), false).await.is_ok() { + // Save task info to cache before removing + COMPLETED_TASKS_CACHE.write().push(task.clone()); + TORRENT_HANDLES.write().remove(&task.id); - TASK_OUTPUT_FOLDERS.write().remove(&task.id); removed_count += 1; } } diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index 4cd038b..a8cbb18 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -37,7 +37,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueNom, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -641930410; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1482626931; // Section: executor @@ -304,6 +304,24 @@ fn wire__crate__api__downloader_api__downloader_add_url_impl( }, ) } +fn wire__crate__api__downloader_api__downloader_clear_completed_tasks_cache_impl( +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartDco { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "downloader_clear_completed_tasks_cache", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + transform_result_dco::<_, _, ()>((move || { + let output_ok = Result::<_, ()>::Ok({ + crate::api::downloader_api::downloader_clear_completed_tasks_cache(); + })?; + Ok(output_ok) + })()) + }, + ) +} fn wire__crate__api__downloader_api__downloader_get_all_tasks_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ) { @@ -327,6 +345,24 @@ fn wire__crate__api__downloader_api__downloader_get_all_tasks_impl( }, ) } +fn wire__crate__api__downloader_api__downloader_get_completed_tasks_cache_impl( +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartDco { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "downloader_get_completed_tasks_cache", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + transform_result_dco::<_, _, ()>((move || { + let output_ok = Result::<_, ()>::Ok( + crate::api::downloader_api::downloader_get_completed_tasks_cache(), + )?; + Ok(output_ok) + })()) + }, + ) +} fn wire__crate__api__downloader_api__downloader_get_global_stats_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ) { @@ -400,6 +436,28 @@ fn wire__crate__api__downloader_api__downloader_has_active_tasks_impl( }, ) } +fn wire__crate__api__downloader_api__downloader_has_pending_session_tasks_impl( + working_dir: impl CstDecode, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartDco { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "downloader_has_pending_session_tasks", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let api_working_dir = working_dir.cst_decode(); + transform_result_dco::<_, _, ()>((move || { + let output_ok = Result::<_, ()>::Ok( + crate::api::downloader_api::downloader_has_pending_session_tasks( + api_working_dir, + ), + )?; + Ok(output_ok) + })()) + }, + ) +} fn wire__crate__api__downloader_api__downloader_init_impl( port_: flutter_rust_bridge::for_generated::MessagePort, working_dir: impl CstDecode, @@ -1204,7 +1262,7 @@ fn wire__crate__api__win32_api__remove_nvme_patch_impl( } fn wire__crate__api__win32_api__resolve_shortcut_impl( port_: flutter_rust_bridge::for_generated::MessagePort, - _lnk_path: impl CstDecode, + lnk_path: impl CstDecode, ) { FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( flutter_rust_bridge::for_generated::TaskInfo { @@ -1213,11 +1271,11 @@ fn wire__crate__api__win32_api__resolve_shortcut_impl( mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, }, move || { - let api__lnk_path = _lnk_path.cst_decode(); + let api_lnk_path = lnk_path.cst_decode(); move |context| { transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>( (move || { - let output_ok = crate::api::win32_api::resolve_shortcut(api__lnk_path)?; + let output_ok = crate::api::win32_api::resolve_shortcut(api_lnk_path)?; Ok(output_ok) })(), ) @@ -4067,6 +4125,12 @@ mod io { ) } + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_clear_completed_tasks_cache( + ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartDco { + wire__crate__api__downloader_api__downloader_clear_completed_tasks_cache_impl() + } + #[unsafe(no_mangle)] pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_get_all_tasks( port_: i64, @@ -4074,6 +4138,12 @@ mod io { wire__crate__api__downloader_api__downloader_get_all_tasks_impl(port_) } + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_get_completed_tasks_cache( + ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartDco { + wire__crate__api__downloader_api__downloader_get_completed_tasks_cache_impl() + } + #[unsafe(no_mangle)] pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_get_global_stats( port_: i64, @@ -4096,6 +4166,13 @@ mod io { wire__crate__api__downloader_api__downloader_has_active_tasks_impl(port_) } + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_has_pending_session_tasks( + working_dir: *mut wire_cst_list_prim_u_8_strict, + ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartDco { + wire__crate__api__downloader_api__downloader_has_pending_session_tasks_impl(working_dir) + } + #[unsafe(no_mangle)] pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_init( port_: i64, @@ -4378,9 +4455,9 @@ mod io { #[unsafe(no_mangle)] pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__win32_api__resolve_shortcut( port_: i64, - _lnk_path: *mut wire_cst_list_prim_u_8_strict, + lnk_path: *mut wire_cst_list_prim_u_8_strict, ) { - wire__crate__api__win32_api__resolve_shortcut_impl(port_, _lnk_path) + wire__crate__api__win32_api__resolve_shortcut_impl(port_, lnk_path) } #[unsafe(no_mangle)]