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:
@@ -75,7 +75,10 @@ impl ThumbnailCache {
|
||||
],
|
||||
);
|
||||
|
||||
if let Ok(Some(path_str)) = db.query_optional(exact, |row| row.get::<_, String>(0)).await {
|
||||
if let Ok(Some(path_str)) = db
|
||||
.query_optional(exact, |row| row.get::<_, String>(0))
|
||||
.await
|
||||
{
|
||||
let path = PathBuf::from(&path_str);
|
||||
if path.exists() {
|
||||
self.touch(&db, item_id, image_type, Some(tag)).await;
|
||||
@@ -83,14 +86,16 @@ impl ThumbnailCache {
|
||||
}
|
||||
// File gone — drop the stale row and fall through to the tag-agnostic
|
||||
// lookup below (another cached image for this item may still exist).
|
||||
let _ = db.execute(Query::with_params(
|
||||
"DELETE FROM thumbnails WHERE item_id = ? AND image_type = ? AND image_tag = ?",
|
||||
vec![
|
||||
QueryParam::String(item_id.to_string()),
|
||||
QueryParam::String(image_type.to_string()),
|
||||
QueryParam::String(tag.to_string()),
|
||||
],
|
||||
)).await;
|
||||
let _ = db
|
||||
.execute(Query::with_params(
|
||||
"DELETE FROM thumbnails WHERE item_id = ? AND image_type = ? AND image_tag = ?",
|
||||
vec![
|
||||
QueryParam::String(item_id.to_string()),
|
||||
QueryParam::String(image_type.to_string()),
|
||||
QueryParam::String(tag.to_string()),
|
||||
],
|
||||
))
|
||||
.await;
|
||||
}
|
||||
|
||||
// Fallback: any cached image for this item + type, newest first. The
|
||||
@@ -204,7 +209,11 @@ impl ThumbnailCache {
|
||||
}
|
||||
|
||||
/// Ensure there's enough space by evicting LRU items if needed
|
||||
async fn ensure_space(&self, db: Arc<RusqliteService>, needed_bytes: u64) -> Result<(), String> {
|
||||
async fn ensure_space(
|
||||
&self,
|
||||
db: Arc<RusqliteService>,
|
||||
needed_bytes: u64,
|
||||
) -> Result<(), String> {
|
||||
let max_size = {
|
||||
let config = self.config.lock().map_err(|e| e.to_string())?;
|
||||
config.max_size_bytes
|
||||
@@ -236,9 +245,7 @@ impl ThumbnailCache {
|
||||
);
|
||||
|
||||
let items: Vec<(i64, String, i64)> = db
|
||||
.query_many(query, |row| {
|
||||
Ok((row.get(0)?, row.get(1)?, row.get(2)?))
|
||||
})
|
||||
.query_many(query, |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
@@ -276,9 +283,7 @@ impl ThumbnailCache {
|
||||
pub async fn get_item_count(&self, db: Arc<RusqliteService>) -> i64 {
|
||||
let query = Query::new("SELECT COUNT(*) FROM thumbnails");
|
||||
|
||||
db.query_one(query, |row| row.get(0))
|
||||
.await
|
||||
.unwrap_or(0)
|
||||
db.query_one(query, |row| row.get(0)).await.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Get the current cache limit in bytes
|
||||
@@ -297,7 +302,11 @@ impl ThumbnailCache {
|
||||
}
|
||||
|
||||
/// Set the cache limit in bytes
|
||||
pub async fn set_limit(&self, db: Arc<RusqliteService>, limit_bytes: u64) -> Result<(), String> {
|
||||
pub async fn set_limit(
|
||||
&self,
|
||||
db: Arc<RusqliteService>,
|
||||
limit_bytes: u64,
|
||||
) -> Result<(), String> {
|
||||
// Update database setting
|
||||
let query = Query::with_params(
|
||||
"INSERT OR REPLACE INTO cache_settings (key, value, updated_at)
|
||||
@@ -444,14 +453,24 @@ mod tests {
|
||||
// Save a thumbnail
|
||||
let data = b"fake image data";
|
||||
let path = cache
|
||||
.save_thumbnail(conn.clone(), "item1", "Primary", "tag1", data, Some(100), Some(100))
|
||||
.save_thumbnail(
|
||||
conn.clone(),
|
||||
"item1",
|
||||
"Primary",
|
||||
"tag1",
|
||||
data,
|
||||
Some(100),
|
||||
Some(100),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(path.exists());
|
||||
|
||||
// Get cached path
|
||||
let cached = cache.get_cached_path(conn.clone(), "item1", "Primary", "tag1").await;
|
||||
let cached = cache
|
||||
.get_cached_path(conn.clone(), "item1", "Primary", "tag1")
|
||||
.await;
|
||||
assert!(cached.is_some());
|
||||
assert_eq!(cached.unwrap(), path);
|
||||
}
|
||||
@@ -461,7 +480,9 @@ mod tests {
|
||||
let (conn, temp_dir) = setup_test_db();
|
||||
let cache = ThumbnailCache::new(temp_dir.path().to_path_buf(), CacheConfig::default());
|
||||
|
||||
let cached = cache.get_cached_path(conn.clone(), "nonexistent", "Primary", "tag1").await;
|
||||
let cached = cache
|
||||
.get_cached_path(conn.clone(), "nonexistent", "Primary", "tag1")
|
||||
.await;
|
||||
assert!(cached.is_none());
|
||||
}
|
||||
|
||||
@@ -488,11 +509,15 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
// First item should be evicted
|
||||
let cached = cache.get_cached_path(conn.clone(), "item1", "Primary", "tag1").await;
|
||||
let cached = cache
|
||||
.get_cached_path(conn.clone(), "item1", "Primary", "tag1")
|
||||
.await;
|
||||
assert!(cached.is_none());
|
||||
|
||||
// Second item should exist
|
||||
let cached = cache.get_cached_path(conn.clone(), "item2", "Primary", "tag2").await;
|
||||
let cached = cache
|
||||
.get_cached_path(conn.clone(), "item2", "Primary", "tag2")
|
||||
.await;
|
||||
assert!(cached.is_some());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user