romm_cli/tui/app/background/
tasks.rs1use std::path::PathBuf;
4use std::time::Duration;
5
6use crate::commands::library_scan::ScanCacheInvalidate;
7use crate::core::cache::RomCacheKey;
8use crate::core::startup_library_snapshot;
9use crate::types::SaveMetadata;
10
11use super::super::event::{AppEvent, BackgroundAction};
12use super::super::AppScreen;
13use super::types::{
14 CoverLoadDone, LibraryMetadataRefreshDone, LibraryUploadComplete, SaveListDone,
15};
16
17impl super::super::App {
18 pub(in crate::tui::app) fn spawn_library_metadata_refresh(&mut self) {
19 self.library_metadata_refresh_gen = self.library_metadata_refresh_gen.saturating_add(1);
20 let gen = self.library_metadata_refresh_gen;
21 let client = self.client.clone();
22 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
23 self.library_metadata_rx = Some(rx);
24 tokio::spawn(async move {
25 let fetch = startup_library_snapshot::fetch_merged_library_metadata(&client).await;
26 let _ = tx.send(LibraryMetadataRefreshDone {
27 gen,
28 platforms: fetch.platforms,
29 collections: fetch.collections,
30 collection_digest: fetch.collection_digest,
31 warnings: fetch.warnings,
32 });
33 });
34 }
35
36 pub(crate) fn drain_background_events(&mut self) -> Vec<AppEvent> {
38 let mut events = Vec::new();
39 events.extend(self.drain_library_metadata_refresh());
40 events.extend(self.drain_rom_load_results());
41 events.extend(self.drain_collection_prefetch_results());
42 events.extend(self.drain_search_load_results());
43 events.extend(self.drain_cover_load_results());
44 events.extend(self.drain_save_results());
45 events.extend(self.drain_settings_results());
46 events.extend(self.drain_library_upload());
47 events.extend(self.drain_library_scan());
48 self.drive_collection_prefetch_scheduler();
49 events.push(AppEvent::Background(BackgroundAction::DrivePrefetch));
50 events.push(AppEvent::Background(BackgroundAction::PollFooterClear));
51 events
52 }
53
54 pub fn poll_background_tasks(&mut self) {
56 let events = self.drain_background_events();
57 for event in events {
58 if let AppEvent::Background(bg) = event {
59 let action = super::super::event::map_background(bg);
60 self.apply_background(action);
61 }
62 }
63 }
64
65 pub(in crate::tui::app) fn spawn_library_rescan_worker(
66 &mut self,
67 cache_on_success: ScanCacheInvalidate,
68 ) {
69 if self.library_scan_inflight {
70 return;
71 }
72 self.library_scan_inflight = true;
73 self.library_scan_pending_invalidate = Some(cache_on_success);
74 if let AppScreen::LibraryBrowse(ref mut lib) = self.screen {
75 lib.set_metadata_footer(Some("Server library scan running…".into()));
76 }
77 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
78 self.library_scan_rx = Some(rx);
79 let client = self.client.clone();
80 tokio::spawn(async move {
81 let result = async {
82 let start =
83 crate::commands::library_scan::start_scan_library(&client, None).await?;
84 crate::commands::library_scan::wait_for_task_terminal(
85 &client,
86 &start.task_id,
87 Duration::from_secs(3600),
88 None,
89 |_| {},
90 )
91 .await?;
92 Ok::<(), anyhow::Error>(())
93 }
94 .await
95 .map_err(|e| e.to_string());
96 let _ = tx.send(result);
97 });
98 }
99
100 fn drain_library_scan(&mut self) -> Vec<AppEvent> {
101 let Some(rx) = &mut self.library_scan_rx else {
102 return Vec::new();
103 };
104 match rx.try_recv() {
105 Ok(result) => {
106 vec![AppEvent::Background(BackgroundAction::LibraryScanDone(
107 result,
108 ))]
109 }
110 Err(tokio::sync::mpsc::error::TryRecvError::Empty) => Vec::new(),
111 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {
112 self.library_scan_rx = None;
113 self.library_scan_inflight = false;
114 self.library_scan_pending_invalidate = None;
115 Vec::new()
116 }
117 }
118 }
119
120 pub(in crate::tui::app) fn apply_library_scan_cache_invalidate(
121 &mut self,
122 inv: &ScanCacheInvalidate,
123 ) {
124 match inv {
125 ScanCacheInvalidate::None => {}
126 ScanCacheInvalidate::Platform(pid) => {
127 self.rom_cache.remove(&RomCacheKey::Platform(*pid));
128 }
129 ScanCacheInvalidate::AllPlatforms => {
130 self.rom_cache.remove_all_platform_entries();
131 if let AppScreen::LibraryBrowse(lib) = &self.screen {
132 if let Some(ref k) = lib.cache_key() {
133 if !matches!(k, RomCacheKey::Platform(_)) {
134 self.rom_cache.remove(k);
135 }
136 }
137 }
138 }
139 }
140 }
141
142 pub(in crate::tui::app) fn on_library_scan_completed_success(&mut self) {
143 let inv = self
144 .library_scan_pending_invalidate
145 .take()
146 .unwrap_or(ScanCacheInvalidate::AllPlatforms);
147 self.apply_library_scan_cache_invalidate(&inv);
148 if matches!(self.screen, AppScreen::LibraryBrowse(_)) {
149 self.force_rom_reload_after_metadata = true;
150 self.spawn_library_metadata_refresh();
151 }
152 }
153
154 pub(in crate::tui::app) fn format_upload_bytes(n: u64) -> String {
155 const KB: u64 = 1024;
156 const MB: u64 = KB * 1024;
157 const GB: u64 = MB * 1024;
158 if n >= GB {
159 format!("{:.2} GiB", n as f64 / GB as f64)
160 } else if n >= MB {
161 format!("{:.2} MiB", n as f64 / MB as f64)
162 } else if n >= KB {
163 format!("{:.1} KiB", n as f64 / KB as f64)
164 } else {
165 format!("{n} B")
166 }
167 }
168
169 pub(in crate::tui::app) fn spawn_library_upload_worker(
170 &mut self,
171 platform_id: u64,
172 path: PathBuf,
173 scan_after: bool,
174 ) {
175 if self.library_upload_inflight || self.library_scan_inflight {
176 return;
177 }
178 self.library_upload_inflight = true;
179 self.library_upload_progress_rx = None;
180 if let AppScreen::LibraryBrowse(ref mut lib) = self.screen {
181 lib.set_metadata_footer(Some("Preparing upload…".into()));
182 }
183 let (prog_tx, prog_rx) = tokio::sync::mpsc::unbounded_channel();
184 let (done_tx, done_rx) = tokio::sync::mpsc::unbounded_channel();
185 self.library_upload_progress_rx = Some(prog_rx);
186 self.library_upload_done_rx = Some(done_rx);
187 let client = self.client.clone();
188 tokio::spawn(async move {
189 let result: Result<LibraryUploadComplete, String> = async {
190 client
191 .upload_rom(platform_id, &path, move |uploaded, total| {
192 let _ = prog_tx.send((uploaded, total));
193 })
194 .await
195 .map_err(|e| e.to_string())?;
196 Ok(LibraryUploadComplete {
197 platform_id,
198 scan_after,
199 })
200 }
201 .await;
202 let _ = done_tx.send(result);
203 });
204 }
205
206 fn drain_library_upload(&mut self) -> Vec<AppEvent> {
207 let mut events = Vec::new();
208 if let Some(rx) = &mut self.library_upload_progress_rx {
209 while let Ok((up, tot)) = rx.try_recv() {
210 events.push(AppEvent::Background(
211 BackgroundAction::LibraryUploadProgress {
212 uploaded: up,
213 total: tot,
214 },
215 ));
216 }
217 }
218
219 let Some(rx) = &mut self.library_upload_done_rx else {
220 return events;
221 };
222 match rx.try_recv() {
223 Ok(result) => {
224 events.push(AppEvent::Background(BackgroundAction::LibraryUploadDone(
225 result,
226 )));
227 }
228 Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {}
229 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {
230 self.library_upload_done_rx = None;
231 self.library_upload_progress_rx = None;
232 self.library_upload_inflight = false;
233 }
234 }
235 events
236 }
237
238 fn drain_search_load_results(&mut self) -> Vec<AppEvent> {
239 let mut events = Vec::new();
240 loop {
241 match self.search_load_rx.try_recv() {
242 Ok(done) => events.push(AppEvent::Background(BackgroundAction::SearchLoad(done))),
243 Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
244 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
245 }
246 }
247 events
248 }
249
250 pub(in crate::tui::app) fn spawn_cover_load_worker(&mut self, rom_id: u64, url: String) {
251 if let Some(task) = self.cover_load_task.take() {
252 task.abort();
253 }
254 let tx = self.cover_load_tx.clone();
255 self.cover_load_task = Some(tokio::spawn(async move {
256 let result = async {
257 let response = reqwest::get(&url).await.map_err(|e| e.to_string())?;
258 let status = response.status();
259 if !status.is_success() {
260 return Err(format!("HTTP {}", status.as_u16()));
261 }
262 let bytes = response.bytes().await.map_err(|e| e.to_string())?;
263 image::load_from_memory(&bytes).map_err(|e| e.to_string())
264 }
265 .await;
266 let _ = tx.send(CoverLoadDone { rom_id, result });
267 }));
268 }
269
270 fn drain_cover_load_results(&mut self) -> Vec<AppEvent> {
271 let mut events = Vec::new();
272 loop {
273 match self.cover_load_rx.try_recv() {
274 Ok(done) => events.push(AppEvent::Background(BackgroundAction::CoverLoad(done))),
275 Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
276 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
277 }
278 }
279 events
280 }
281
282 pub(in crate::tui::app) fn maybe_start_game_detail_cover_load(&mut self) {
283 let (rom_id, url) = match &mut self.screen {
284 AppScreen::GameDetail(detail) => {
285 if !detail.should_request_cover_load() {
286 return;
287 }
288 detail.set_cover_loading();
289 let Some(url) = detail.cover_last_url.clone() else {
290 return;
291 };
292 (detail.rom.id, url)
293 }
294 _ => return,
295 };
296 self.spawn_cover_load_worker(rom_id, url);
297 }
298
299 pub(in crate::tui::app) fn spawn_save_list_worker(&mut self, rom_id: u64) {
300 if let AppScreen::GameDetail(detail) = &mut self.screen {
301 detail.set_saves_loading();
302 }
303 let client = self.client.clone();
304 let tx = self.save_list_tx.clone();
305 tokio::spawn(async move {
306 let result = async {
307 let value = client
308 .request_json(
309 "GET",
310 "/api/saves",
311 &[("rom_id".to_string(), rom_id.to_string())],
312 None,
313 )
314 .await?;
315 SaveMetadata::from_api_value(value)
316 }
317 .await
318 .map_err(|e| format!("{e:#}"));
319 let _ = tx.send(SaveListDone { rom_id, result });
320 });
321 }
322
323 pub(in crate::tui::app) fn refresh_current_game_saves(&mut self) {
324 if let AppScreen::GameDetail(detail) = &self.screen {
325 self.spawn_save_list_worker(detail.rom.id);
326 }
327 }
328
329 fn drain_save_results(&mut self) -> Vec<AppEvent> {
330 let mut events = Vec::new();
331 while let Ok(done) = self.save_list_rx.try_recv() {
332 events.push(AppEvent::Background(BackgroundAction::SaveList(done)));
333 }
334 while let Ok(done) = self.save_upload_rx.try_recv() {
335 events.push(AppEvent::Background(BackgroundAction::SaveUpload(done)));
336 }
337 while let Ok(done) = self.save_download_rx.try_recv() {
338 events.push(AppEvent::Background(BackgroundAction::SaveDownload(done)));
339 }
340 events
341 }
342
343 fn drain_settings_results(&mut self) -> Vec<AppEvent> {
344 let mut events = Vec::new();
345 while let Ok(done) = self.device_list_rx.try_recv() {
346 events.push(AppEvent::Background(BackgroundAction::DeviceList(done)));
347 }
348 while let Ok(done) = self.platform_list_rx.try_recv() {
349 events.push(AppEvent::Background(BackgroundAction::PlatformList(done)));
350 }
351 while let Ok(done) = self.sync_push_pull_rx.try_recv() {
352 events.push(AppEvent::Background(BackgroundAction::SyncPushPull(done)));
353 }
354 events
355 }
356
357 fn drain_rom_load_results(&mut self) -> Vec<AppEvent> {
358 let mut events = Vec::new();
359 loop {
360 match self.rom_load_rx.try_recv() {
361 Ok(done) => events.push(AppEvent::Background(BackgroundAction::RomLoad(done))),
362 Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
363 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
364 }
365 }
366 events
367 }
368
369 fn drain_library_metadata_refresh(&mut self) -> Vec<AppEvent> {
370 let mut events = Vec::new();
371 let mut disconnected = false;
372 if let Some(rx) = &mut self.library_metadata_rx {
373 loop {
374 match rx.try_recv() {
375 Ok(msg) => events.push(AppEvent::Background(
376 BackgroundAction::LibraryMetadataRefresh(msg),
377 )),
378 Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
379 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {
380 disconnected = true;
381 break;
382 }
383 }
384 }
385 }
386 if disconnected {
387 self.library_metadata_rx = None;
388 }
389 events
390 }
391
392 pub(in crate::tui::app) fn apply_library_metadata_refresh(
393 &mut self,
394 msg: LibraryMetadataRefreshDone,
395 ) {
396 if msg.gen != self.library_metadata_refresh_gen {
397 return;
398 }
399
400 let (selection_reload, post_scan_reload) = {
401 let AppScreen::LibraryBrowse(ref mut lib) = self.screen else {
402 return;
403 };
404
405 let had_cached_lists = !lib.platforms.is_empty() || !lib.collections.is_empty();
406 let live_empty = msg.collections.is_empty();
407 if live_empty && had_cached_lists && !msg.warnings.is_empty() {
408 lib.set_temporary_metadata_footer(
409 "Could not refresh library metadata (keeping cached list).".into(),
410 std::time::Duration::from_secs(3),
411 );
412 self.force_rom_reload_after_metadata = false;
413 return;
414 }
415
416 let old_digest = startup_library_snapshot::build_collection_digest_from_collections(
417 &lib.collections,
418 );
419 let digest_changed = old_digest != msg.collection_digest;
420 let update_platforms = !msg.platforms.is_empty();
421 let selection_changed = lib.replace_metadata_preserving_selection(
422 msg.platforms,
423 msg.collections,
424 update_platforms,
425 true,
426 );
427 startup_library_snapshot::save_snapshot(&lib.platforms, &lib.collections);
428
429 let footer = if msg.warnings.is_empty() {
430 if digest_changed {
431 Some("Collection metadata updated.".into())
432 } else {
433 None
434 }
435 } else {
436 let w = msg.warnings.join(" | ");
437 let short: String = if w.chars().count() > 160 {
438 let prefix: String = w.chars().take(157).collect();
439 format!("{prefix}…")
440 } else {
441 w
442 };
443 Some(format!("Partial refresh: {}", short))
444 };
445 lib.set_metadata_footer(footer);
446
447 let force_reload = std::mem::take(&mut self.force_rom_reload_after_metadata);
448 let selection_reload = if selection_changed && lib.list_len() > 0 {
449 lib.clear_roms();
450 let key = lib.cache_key();
451 let expected = lib.expected_rom_count();
452 let req = Self::selected_rom_request_for_library(lib);
453 lib.set_rom_loading(expected > 0);
454 Some((key, req, expected, "refresh_selection"))
455 } else {
456 None
457 };
458 let post_scan_reload = if force_reload && lib.list_len() > 0 && !selection_changed {
459 lib.clear_roms();
460 let key = lib.cache_key();
461 let expected = lib.expected_rom_count();
462 let req = Self::selected_rom_request_for_library(lib);
463 lib.set_rom_loading(expected > 0);
464 Some((key, req, expected, "post_scan_reload"))
465 } else {
466 None
467 };
468 (selection_reload, post_scan_reload)
469 };
470
471 if let Some((key, req, expected, context)) = selection_reload {
472 self.queue_primary_rom_load(key, req, expected, context);
473 } else if let Some((key, req, expected, context)) = post_scan_reload {
474 self.queue_primary_rom_load(key, req, expected, context);
475 }
476
477 self.queue_collection_prefetches_from_screen(1, "refresh_warmup");
478 }
479
480 fn drain_collection_prefetch_results(&mut self) -> Vec<AppEvent> {
481 let mut events = Vec::new();
482 loop {
483 match self.collection_prefetch_rx.try_recv() {
484 Ok(done) => events.push(AppEvent::Background(
485 BackgroundAction::CollectionPrefetch(done),
486 )),
487 Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
488 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
489 }
490 }
491 events
492 }
493}