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
+61 -16
View File
@@ -4,8 +4,8 @@ use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;
use crate::jellyfin::http_client::HttpClient;
use crate::connectivity::ConnectivityMonitor;
use crate::jellyfin::http_client::HttpClient;
pub use session_verifier::SessionVerifier;
@@ -99,7 +99,10 @@ impl AuthManager {
}
/// Set the connectivity monitor (for marking server reachability)
pub fn set_connectivity_monitor(&mut self, monitor: Arc<tokio::sync::Mutex<ConnectivityMonitor>>) {
pub fn set_connectivity_monitor(
&mut self,
monitor: Arc<tokio::sync::Mutex<ConnectivityMonitor>>,
) {
self.connectivity_monitor = Some(monitor);
}
@@ -133,9 +136,17 @@ impl AuthManager {
log::info!("[AuthManager] Connecting to server: {}", normalized_url);
match self.http_client.get_json_fast::<PublicSystemInfo>(&endpoint).await {
match self
.http_client
.get_json_fast::<PublicSystemInfo>(&endpoint)
.await
{
Ok(info) => {
log::info!("[AuthManager] Connected to server: {} ({})", info.server_name, info.version);
log::info!(
"[AuthManager] Connected to server: {} ({})",
info.server_name,
info.version
);
// Mark server as reachable
if let Some(monitor) = &self.connectivity_monitor {
@@ -181,7 +192,10 @@ impl AuthManager {
let auth_header = HttpClient::build_auth_header(None, device_id);
// Build request manually for custom headers
let request = self.http_client.client.post(&endpoint)
let request = self
.http_client
.client
.post(&endpoint)
.header("Content-Type", "application/json")
.header("X-Emby-Authorization", auth_header)
.json(&serde_json::json!({
@@ -192,19 +206,31 @@ impl AuthManager {
.map_err(|e| format!("Failed to build request: {}", e))?;
// Use retry logic
let response = self.http_client.request_with_retry(request).await
let response = self
.http_client
.request_with_retry(request)
.await
.map_err(|e| format!("Login 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());
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
return Err(format!("Login failed: HTTP {}: {}", status, error_text));
}
let auth_response: AuthenticateByNameResponse = response.json().await
let auth_response: AuthenticateByNameResponse = response
.json()
.await
.map_err(|e| format!("Failed to parse login response: {}", e))?;
log::info!("[AuthManager] Login successful for user: {} ({})", auth_response.user.name, auth_response.user.id);
log::info!(
"[AuthManager] Login successful for user: {} ({})",
auth_response.user.name,
auth_response.user.id
);
// Mark server as reachable
if let Some(monitor) = &self.connectivity_monitor {
@@ -243,13 +269,19 @@ impl AuthManager {
let auth_header = HttpClient::build_auth_header(Some(access_token), device_id);
// Build request manually for custom headers
let request = self.http_client.client.get(&endpoint)
let request = self
.http_client
.client
.get(&endpoint)
.header("X-Emby-Authorization", auth_header)
.build()
.map_err(|e| format!("Failed to build request: {}", e))?;
// Use retry logic
let response = self.http_client.request_with_retry(request).await
let response = self
.http_client
.request_with_retry(request)
.await
.map_err(|e| {
log::warn!("[AuthManager] Session verification failed: {}", e);
format!("Session verification failed: {}", e)
@@ -257,24 +289,34 @@ impl AuthManager {
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
// Mark server as unreachable for auth errors
if status.as_u16() == 401 || status.as_u16() == 403 {
log::warn!("[AuthManager] Session invalid: HTTP {}", status);
if let Some(monitor) = &self.connectivity_monitor {
let monitor = monitor.lock().await;
monitor.mark_unreachable(Some(format!("Authentication failed: {}", status))).await;
monitor
.mark_unreachable(Some(format!("Authentication failed: {}", status)))
.await;
}
}
return Err(format!("HTTP {}: {}", status, error_text));
}
let user_response: JellyfinUser = response.json().await
let user_response: JellyfinUser = response
.json()
.await
.map_err(|e| format!("Failed to parse user response: {}", e))?;
log::info!("[AuthManager] Session verified successfully for: {}", user_response.name);
log::info!(
"[AuthManager] Session verified successfully for: {}",
user_response.name
);
// Mark server as reachable
if let Some(monitor) = &self.connectivity_monitor {
@@ -306,7 +348,10 @@ impl AuthManager {
let auth_header = HttpClient::build_auth_header(Some(access_token), device_id);
// Build request
let request = self.http_client.client.post(&endpoint)
let request = self
.http_client
.client
.post(&endpoint)
.header("X-Emby-Authorization", auth_header)
.build()
.map_err(|e| format!("Failed to build request: {}", e))?;