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:
@@ -1,9 +1,11 @@
|
||||
//! Thumbnail cache and image-URL commands.
|
||||
//!
|
||||
//! TRACES: UR-007 | JA-028 | DR-016
|
||||
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use tokio::sync::Semaphore;
|
||||
use serde::Deserialize;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use tauri::State;
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
use super::{DatabaseWrapper, ThumbnailCacheWrapper};
|
||||
use crate::commands::repository::RepositoryManagerWrapper;
|
||||
@@ -11,7 +13,6 @@ use crate::repository::types::{ImageOptions, ImageType};
|
||||
use crate::repository::MediaRepository;
|
||||
use crate::thumbnail::{ThumbnailCacheStats, ThumbnailWorker};
|
||||
|
||||
|
||||
/// Get cached thumbnail path, returns None if not cached
|
||||
/// Also updates last_accessed timestamp for LRU tracking
|
||||
#[tauri::command]
|
||||
@@ -28,7 +29,8 @@ pub async fn thumbnail_get_cached(
|
||||
Arc::new(database.service())
|
||||
};
|
||||
|
||||
let result = thumbnail_cache.0
|
||||
let result = thumbnail_cache
|
||||
.0
|
||||
.get_cached_path(db_service, &item_id, &image_type, &tag)
|
||||
.await
|
||||
.map(|p| p.to_string_lossy().to_string());
|
||||
@@ -61,7 +63,10 @@ pub async fn thumbnail_save(
|
||||
Arc::new(database.service())
|
||||
};
|
||||
|
||||
let path = thumbnail_cache.0.save_thumbnail(db_service, &item_id, &image_type, &tag, &data, None, None).await?;
|
||||
let path = thumbnail_cache
|
||||
.0
|
||||
.save_thumbnail(db_service, &item_id, &image_type, &tag, &data, None, None)
|
||||
.await?;
|
||||
Ok(path.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
@@ -179,7 +184,7 @@ pub async fn image_get_url(
|
||||
repository_handle: String,
|
||||
request: GetImageRequest,
|
||||
) -> Result<String, String> {
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
|
||||
use std::fs;
|
||||
|
||||
let tag = request.tag.as_deref().unwrap_or("default");
|
||||
@@ -191,14 +196,18 @@ pub async fn image_get_url(
|
||||
};
|
||||
|
||||
// Check cache first
|
||||
if let Some(cached_path) = thumbnail_cache.0.get_cached_path(
|
||||
db_service.clone(),
|
||||
&request.item_id,
|
||||
&request.image_type,
|
||||
tag,
|
||||
).await {
|
||||
let image_data = fs::read(&cached_path)
|
||||
.map_err(|e| format!("Failed to read cached image: {}", e))?;
|
||||
if let Some(cached_path) = thumbnail_cache
|
||||
.0
|
||||
.get_cached_path(
|
||||
db_service.clone(),
|
||||
&request.item_id,
|
||||
&request.image_type,
|
||||
tag,
|
||||
)
|
||||
.await
|
||||
{
|
||||
let image_data =
|
||||
fs::read(&cached_path).map_err(|e| format!("Failed to read cached image: {}", e))?;
|
||||
let base64_data = BASE64.encode(&image_data);
|
||||
let mime_type = mime_from_ext(cached_path.extension().and_then(|s| s.to_str()));
|
||||
return Ok(format!("data:{};base64,{}", mime_type, base64_data));
|
||||
@@ -206,10 +215,14 @@ pub async fn image_get_url(
|
||||
|
||||
// Not cached — fetch from server and cache.
|
||||
// Acquire semaphore to limit concurrent downloads (prevents connection pool starvation).
|
||||
let _permit = image_semaphore().acquire().await
|
||||
let _permit = image_semaphore()
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|_| "Image download semaphore closed".to_string())?;
|
||||
|
||||
let repository = repository_manager.0.get(&repository_handle)
|
||||
let repository = repository_manager
|
||||
.0
|
||||
.get(&repository_handle)
|
||||
.ok_or_else(|| "Repository not found - user may need to log in".to_string())?;
|
||||
|
||||
let image_type_enum = match request.image_type.as_str() {
|
||||
@@ -229,18 +242,23 @@ pub async fn image_get_url(
|
||||
};
|
||||
|
||||
let server_url = repository.get_image_url(&request.item_id, image_type_enum, Some(options));
|
||||
let image_data = repository.download_bytes(&server_url).await
|
||||
let image_data = repository
|
||||
.download_bytes(&server_url)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to download image: {}", e))?;
|
||||
|
||||
let cached_path = thumbnail_cache.0.save_thumbnail(
|
||||
db_service,
|
||||
&request.item_id,
|
||||
&request.image_type,
|
||||
tag,
|
||||
&image_data,
|
||||
request.max_width.map(|w| w as i32),
|
||||
request.max_height.map(|h| h as i32),
|
||||
).await?;
|
||||
let cached_path = thumbnail_cache
|
||||
.0
|
||||
.save_thumbnail(
|
||||
db_service,
|
||||
&request.item_id,
|
||||
&request.image_type,
|
||||
tag,
|
||||
&image_data,
|
||||
request.max_width.map(|w| w as i32),
|
||||
request.max_height.map(|h| h as i32),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let base64_data = BASE64.encode(&image_data);
|
||||
let mime_type = mime_from_ext(cached_path.extension().and_then(|s| s.to_str()));
|
||||
|
||||
Reference in New Issue
Block a user