Architecture remediation A/B/F: poison-tolerant locks, graceful backend init, doc fixes

Workstream A — poison-tolerant locking:
- Add utils/lock.rs with MutexSafe/RwLockSafe extension traits that recover a
  poisoned std::sync lock instead of panicking, plus unit tests.
- Replace all 153 .lock().unwrap() and 4 .read()/.write().unwrap() production
  sites with _safe variants across 14 files, eliminating the player
  crash-cascade class. Tokio async mutexes are unchanged.

Workstream B — graceful backend init:
- create_player_backend no longer panics when MPV/ExoPlayer fail to initialize;
  it falls back to NullBackend and emits a backend-init-failed event so the UI
  can show "playback unavailable" instead of the app crashing. Fatal DB-setup
  panics are kept.

Workstream F — doc reconciliation:
- Rewrite software-architecture.md's inaccurate "thin UI / ~800 lines" claims to
  reflect reality (~20.5k non-test frontend) and document the events+polling
  hybrid plus the new locking/backend-init behavior.
This commit is contained in:
2026-06-20 16:03:54 +02:00
parent 0738ef10ec
commit 6866f03c55
18 changed files with 345 additions and 178 deletions
+5 -4
View File
@@ -10,6 +10,7 @@ pub mod cache;
pub mod events;
pub mod worker;
use crate::utils::lock::MutexSafe;
use std::path::PathBuf;
use std::collections::HashSet;
use std::sync::{Arc, Mutex};
@@ -45,18 +46,18 @@ impl DownloadManager {
/// Check if a new download can be started based on concurrent limit
pub fn can_start_download(&self) -> bool {
let active = self.active_downloads.lock().unwrap();
let active = self.active_downloads.lock_safe();
active.len() < self.max_concurrent
}
/// Get the number of currently active downloads
pub fn active_count(&self) -> usize {
self.active_downloads.lock().unwrap().len()
self.active_downloads.lock_safe().len()
}
/// Register a download as active
pub fn register_download(&self, download_id: i64) -> bool {
let mut active = self.active_downloads.lock().unwrap();
let mut active = self.active_downloads.lock_safe();
if active.len() >= self.max_concurrent {
return false;
}
@@ -65,7 +66,7 @@ impl DownloadManager {
/// Unregister a download when it completes or fails
pub fn unregister_download(&self, download_id: i64) {
let mut active = self.active_downloads.lock().unwrap();
let mut active = self.active_downloads.lock_safe();
active.remove(&download_id);
}