Background-audio handoff for video + repository/player refactor

Hand video playback off to a native audio-only stream when the app is
backgrounded or locked, with no on-device video decode (UR-040). Adds
player_enter/exit_background_audio commands, an audio-only stream URL
for video items across the repository layer, and the frontend handoff
state machine wired into VideoPlayer. Includes accompanying
repository/offline/player refactoring and regenerates the traceability
matrix.
This commit is contained in:
2026-07-22 21:52:07 +02:00
parent 4e6ab017d4
commit 3fbf6afdbc
72 changed files with 6728 additions and 2338 deletions
+12 -40
View File
@@ -40,7 +40,7 @@ pub enum ErrorKind {
/// Enhanced HTTP client with retry logic and error classification
#[derive(Clone)]
pub struct HttpClient {
pub(crate) client: Client, // Make accessible within crate for custom requests
pub(crate) client: Client, // Make accessible within crate for custom requests
config: HttpConfig,
}
@@ -131,18 +131,15 @@ impl HttpClient {
/// Check if a request should be retried based on the error
pub fn should_retry(error: &reqwest::Error) -> bool {
match Self::classify_error(error) {
ErrorKind::Network => true, // Retry network errors
ErrorKind::Server => true, // Retry 5xx server errors
ErrorKind::Network => true, // Retry network errors
ErrorKind::Server => true, // Retry 5xx server errors
ErrorKind::Authentication => false, // Don't retry 401/403
ErrorKind::Client => false, // Don't retry other 4xx errors
ErrorKind::Client => false, // Don't retry other 4xx errors
}
}
/// Make a request with automatic retry on network errors
pub async fn request_with_retry(
&self,
request: Request,
) -> Result<Response, reqwest::Error> {
pub async fn request_with_retry(&self, request: Request) -> Result<Response, reqwest::Error> {
let max_retries = self.config.max_retries;
let mut last_error: Option<reqwest::Error> = None;
@@ -192,31 +189,6 @@ impl HttpClient {
Err(last_error.unwrap())
}
/// Make a GET request with retry
pub async fn get_with_retry(&self, url: &str) -> Result<Response, reqwest::Error> {
let request = self.client.get(url).build()?;
self.request_with_retry(request).await
}
/// Make a GET request and deserialize JSON response with retry
pub async fn get_json_with_retry<T: DeserializeOwned>(
&self,
url: &str,
) -> Result<T, String> {
let response = self.get_with_retry(url).await
.map_err(|e| format!("Request failed: {}", e))?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await
.unwrap_or_else(|_| "Unknown error".to_string());
return Err(format!("HTTP {}: {}", status, error_text));
}
response.json::<T>().await
.map_err(|e| format!("Failed to parse JSON: {}", e))
}
/// Make a GET request and deserialize JSON with a short timeout and no retries.
///
/// Intended for the initial "connect to server" probe on the login screen:
@@ -258,17 +230,17 @@ impl HttpClient {
/// Quick ping to check if a server is reachable (no retry)
pub async fn ping(&self, url: &str) -> bool {
let request = self.client.get(url)
let request = self
.client
.get(url)
.timeout(Duration::from_secs(5)) // Shorter timeout for ping
.build();
match request {
Ok(req) => {
match self.client.execute(req).await {
Ok(response) => response.status().is_success(),
Err(_) => false,
}
}
Ok(req) => match self.client.execute(req).await {
Ok(response) => response.status().is_success(),
Err(_) => false,
},
Err(_) => false,
}
}