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,4 +1,6 @@
|
||||
//! Tauri commands for database/storage operations
|
||||
//!
|
||||
//! TRACES: UR-002, UR-011, UR-012, UR-017, UR-019, UR-025, UR-047 | IR-013 | DR-012, DR-013, DR-022, DR-060
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
@@ -7,8 +9,8 @@ use serde::{Deserialize, Serialize};
|
||||
use tauri::State;
|
||||
|
||||
use crate::credentials::CredentialStore;
|
||||
use crate::storage::Database;
|
||||
use crate::storage::db_service::{DatabaseService, Query, QueryParam};
|
||||
use crate::storage::Database;
|
||||
use crate::thumbnail::ThumbnailCache;
|
||||
|
||||
use super::SmartCacheWrapper;
|
||||
@@ -86,7 +88,8 @@ pub fn storage_get_path(db: State<DatabaseWrapper>) -> Result<String, String> {
|
||||
let db_path = database.path();
|
||||
|
||||
// Return the parent directory instead of the database file path
|
||||
let storage_dir = db_path.parent()
|
||||
let storage_dir = db_path
|
||||
.parent()
|
||||
.ok_or_else(|| "Database path has no parent directory".to_string())?;
|
||||
|
||||
Ok(storage_dir.to_string_lossy().to_string())
|
||||
@@ -160,13 +163,16 @@ pub async fn storage_save_server(
|
||||
/// Get all saved servers
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn storage_get_servers(db: State<'_, DatabaseWrapper>) -> Result<Vec<ServerInfo>, String> {
|
||||
pub async fn storage_get_servers(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
) -> Result<Vec<ServerInfo>, String> {
|
||||
let db_service = {
|
||||
let database = db.0.lock().map_err(|e| e.to_string())?;
|
||||
Arc::new(database.service())
|
||||
};
|
||||
|
||||
let query = Query::new("SELECT id, name, url, version FROM servers ORDER BY last_connected_at DESC");
|
||||
let query =
|
||||
Query::new("SELECT id, name, url, version FROM servers ORDER BY last_connected_at DESC");
|
||||
|
||||
let servers = db_service
|
||||
.query_many(query, |row| {
|
||||
@@ -221,7 +227,10 @@ pub async fn storage_delete_server(
|
||||
vec![QueryParam::String(server_id)],
|
||||
);
|
||||
|
||||
db_service.execute(delete_query).await.map_err(|e| e.to_string())?;
|
||||
db_service
|
||||
.execute(delete_query)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -237,7 +246,10 @@ pub async fn storage_save_user(
|
||||
username: String,
|
||||
access_token: Option<String>,
|
||||
) -> Result<bool, String> {
|
||||
info!("storage_save_user called: id={}, server_id={}, username={}", id, server_id, username);
|
||||
info!(
|
||||
"storage_save_user called: id={}, server_id={}, username={}",
|
||||
id, server_id, username
|
||||
);
|
||||
|
||||
let (db_service, db_path) = {
|
||||
let database = db.0.lock().map_err(|e| {
|
||||
@@ -277,7 +289,10 @@ pub async fn storage_save_user(
|
||||
"SELECT COUNT(*) FROM users WHERE id = ?",
|
||||
vec![QueryParam::String(id.clone())],
|
||||
);
|
||||
let verify_count: i32 = db_service.query_one(verify_query, |row| row.get(0)).await.unwrap_or(-1);
|
||||
let verify_count: i32 = db_service
|
||||
.query_one(verify_query, |row| row.get(0))
|
||||
.await
|
||||
.unwrap_or(-1);
|
||||
debug!("VERIFY: {} users with id={} after insert", verify_count, id);
|
||||
debug!("Database path: {:?}", db_path);
|
||||
|
||||
@@ -355,14 +370,20 @@ pub async fn storage_set_active_user(
|
||||
|
||||
// Deactivate ALL users globally (since we only connect to one server at a time)
|
||||
let deactivate_query = Query::new("UPDATE users SET is_active = 0");
|
||||
db_service.execute(deactivate_query).await.map_err(|e| e.to_string())?;
|
||||
db_service
|
||||
.execute(deactivate_query)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// Activate the specified user and update last_login_at
|
||||
let activate_query = Query::with_params(
|
||||
"UPDATE users SET is_active = 1, last_login_at = CURRENT_TIMESTAMP WHERE id = ?",
|
||||
vec![QueryParam::String(user_id.clone())],
|
||||
);
|
||||
let rows_affected = db_service.execute(activate_query).await.map_err(|e| e.to_string())?;
|
||||
let rows_affected = db_service
|
||||
.execute(activate_query)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
debug!("storage_set_active_user: {} rows affected", rows_affected);
|
||||
|
||||
@@ -372,7 +393,10 @@ pub async fn storage_set_active_user(
|
||||
|
||||
// Verify the user is now active
|
||||
let verify_query = Query::new("SELECT COUNT(*) FROM users WHERE is_active = 1");
|
||||
let verify_count: i32 = db_service.query_one(verify_query, |row| row.get(0)).await.unwrap_or(-1);
|
||||
let verify_count: i32 = db_service
|
||||
.query_one(verify_query, |row| row.get(0))
|
||||
.await
|
||||
.unwrap_or(-1);
|
||||
debug!("VERIFY: {} active users after set_active", verify_count);
|
||||
debug!("Database path: {:?}", db_path);
|
||||
|
||||
@@ -434,12 +458,21 @@ pub async fn storage_get_active_session(
|
||||
|
||||
// Debug: count total users and active users
|
||||
let total_query = Query::new("SELECT COUNT(*) FROM users");
|
||||
let total_users: i32 = db_service.query_one(total_query, |row| row.get(0)).await.unwrap_or(-1);
|
||||
let total_users: i32 = db_service
|
||||
.query_one(total_query, |row| row.get(0))
|
||||
.await
|
||||
.unwrap_or(-1);
|
||||
|
||||
let active_query = Query::new("SELECT COUNT(*) FROM users WHERE is_active = 1");
|
||||
let active_users: i32 = db_service.query_one(active_query, |row| row.get(0)).await.unwrap_or(-1);
|
||||
let active_users: i32 = db_service
|
||||
.query_one(active_query, |row| row.get(0))
|
||||
.await
|
||||
.unwrap_or(-1);
|
||||
|
||||
debug!("Database state: {} total users, {} active users", total_users, active_users);
|
||||
debug!(
|
||||
"Database state: {} total users, {} active users",
|
||||
total_users, active_users
|
||||
);
|
||||
debug!("Database path: {:?}", db_path);
|
||||
|
||||
// Find active user with their server info, ordered by most recently logged in
|
||||
@@ -449,18 +482,21 @@ pub async fn storage_get_active_session(
|
||||
JOIN servers s ON u.server_id = s.id
|
||||
WHERE u.is_active = 1
|
||||
ORDER BY u.last_login_at DESC
|
||||
LIMIT 1"
|
||||
LIMIT 1",
|
||||
);
|
||||
|
||||
let result = db_service.query_optional(session_query, |row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, String>(2)?,
|
||||
row.get::<_, String>(3)?,
|
||||
row.get::<_, String>(4)?,
|
||||
))
|
||||
}).await.map_err(|e| e.to_string())?;
|
||||
let result = db_service
|
||||
.query_optional(session_query, |row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, String>(2)?,
|
||||
row.get::<_, String>(3)?,
|
||||
row.get::<_, String>(4)?,
|
||||
))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
match result {
|
||||
Some((user_id, username, server_id, server_url, server_name)) => {
|
||||
@@ -478,7 +514,7 @@ pub async fn storage_get_active_session(
|
||||
server_name,
|
||||
access_token,
|
||||
}))
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
// Token not found or error - session is invalid
|
||||
warn!("Failed to get token from secure storage: {:?}", e);
|
||||
@@ -638,8 +674,12 @@ pub async fn storage_update_playback_context(
|
||||
QueryParam::String(user_id.clone()),
|
||||
QueryParam::String(item_id.clone()),
|
||||
QueryParam::Int64(position_ticks),
|
||||
context_type.map(QueryParam::String).unwrap_or(QueryParam::Null),
|
||||
context_id.map(QueryParam::String).unwrap_or(QueryParam::Null),
|
||||
context_type
|
||||
.map(QueryParam::String)
|
||||
.unwrap_or(QueryParam::Null),
|
||||
context_id
|
||||
.map(QueryParam::String)
|
||||
.unwrap_or(QueryParam::Null),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -721,14 +761,23 @@ pub async fn storage_mark_played(
|
||||
});
|
||||
|
||||
if !tracks.is_empty() {
|
||||
info!("Auto-queueing {} tracks from album for download", tracks.len());
|
||||
info!(
|
||||
"Auto-queueing {} tracks from album for download",
|
||||
tracks.len()
|
||||
);
|
||||
|
||||
// Queue each track with high priority (50) and mark as auto-downloaded
|
||||
for (track_id, track_name, artist_name, album_name) in tracks {
|
||||
// Generate a sanitized file path (simplified version)
|
||||
let sanitized_name = track_name
|
||||
.chars()
|
||||
.map(|c| if c.is_alphanumeric() || c == ' ' || c == '-' || c == '_' { c } else { '_' })
|
||||
.map(|c| {
|
||||
if c.is_alphanumeric() || c == ' ' || c == '-' || c == '_' {
|
||||
c
|
||||
} else {
|
||||
'_'
|
||||
}
|
||||
})
|
||||
.collect::<String>();
|
||||
let file_path = format!("downloads/{}/{}.mp3", album_id, sanitized_name);
|
||||
|
||||
@@ -1123,7 +1172,10 @@ pub async fn storage_search_items(
|
||||
limit_clause
|
||||
);
|
||||
|
||||
let query_obj = Query::with_params(sql, vec![QueryParam::String(server_id), QueryParam::String(fts_query)]);
|
||||
let query_obj = Query::with_params(
|
||||
sql,
|
||||
vec![QueryParam::String(server_id), QueryParam::String(fts_query)],
|
||||
);
|
||||
|
||||
let items = db_service
|
||||
.query_many(query_obj, row_to_cached_item)
|
||||
@@ -1181,7 +1233,8 @@ pub async fn storage_save_item(
|
||||
};
|
||||
|
||||
// Generate sort_name from name (remove leading "The ", "A ", etc.)
|
||||
let sort_name = item.name
|
||||
let sort_name = item
|
||||
.name
|
||||
.strip_prefix("The ")
|
||||
.or_else(|| item.name.strip_prefix("A "))
|
||||
.or_else(|| item.name.strip_prefix("An "))
|
||||
@@ -1202,28 +1255,66 @@ pub async fn storage_save_item(
|
||||
vec![
|
||||
QueryParam::String(item.id),
|
||||
QueryParam::String(server_id),
|
||||
item.library_id.map(QueryParam::String).unwrap_or(QueryParam::Null),
|
||||
item.parent_id.map(QueryParam::String).unwrap_or(QueryParam::Null),
|
||||
item.library_id
|
||||
.map(QueryParam::String)
|
||||
.unwrap_or(QueryParam::Null),
|
||||
item.parent_id
|
||||
.map(QueryParam::String)
|
||||
.unwrap_or(QueryParam::Null),
|
||||
QueryParam::String(item.name),
|
||||
QueryParam::String(sort_name),
|
||||
QueryParam::String(item.item_type),
|
||||
item.overview.map(QueryParam::String).unwrap_or(QueryParam::Null),
|
||||
item.genres.map(QueryParam::String).unwrap_or(QueryParam::Null),
|
||||
item.runtime_ticks.map(QueryParam::Int64).unwrap_or(QueryParam::Null),
|
||||
item.production_year.map(QueryParam::Int).unwrap_or(QueryParam::Null),
|
||||
item.community_rating.map(QueryParam::Float).unwrap_or(QueryParam::Null),
|
||||
item.official_rating.map(QueryParam::String).unwrap_or(QueryParam::Null),
|
||||
item.primary_image_tag.map(QueryParam::String).unwrap_or(QueryParam::Null),
|
||||
item.album_id.map(QueryParam::String).unwrap_or(QueryParam::Null),
|
||||
item.album_name.map(QueryParam::String).unwrap_or(QueryParam::Null),
|
||||
item.album_artist.map(QueryParam::String).unwrap_or(QueryParam::Null),
|
||||
item.artists.map(QueryParam::String).unwrap_or(QueryParam::Null),
|
||||
item.index_number.map(QueryParam::Int).unwrap_or(QueryParam::Null),
|
||||
item.series_id.map(QueryParam::String).unwrap_or(QueryParam::Null),
|
||||
item.series_name.map(QueryParam::String).unwrap_or(QueryParam::Null),
|
||||
item.season_id.map(QueryParam::String).unwrap_or(QueryParam::Null),
|
||||
item.season_name.map(QueryParam::String).unwrap_or(QueryParam::Null),
|
||||
item.parent_index_number.map(QueryParam::Int).unwrap_or(QueryParam::Null),
|
||||
item.overview
|
||||
.map(QueryParam::String)
|
||||
.unwrap_or(QueryParam::Null),
|
||||
item.genres
|
||||
.map(QueryParam::String)
|
||||
.unwrap_or(QueryParam::Null),
|
||||
item.runtime_ticks
|
||||
.map(QueryParam::Int64)
|
||||
.unwrap_or(QueryParam::Null),
|
||||
item.production_year
|
||||
.map(QueryParam::Int)
|
||||
.unwrap_or(QueryParam::Null),
|
||||
item.community_rating
|
||||
.map(QueryParam::Float)
|
||||
.unwrap_or(QueryParam::Null),
|
||||
item.official_rating
|
||||
.map(QueryParam::String)
|
||||
.unwrap_or(QueryParam::Null),
|
||||
item.primary_image_tag
|
||||
.map(QueryParam::String)
|
||||
.unwrap_or(QueryParam::Null),
|
||||
item.album_id
|
||||
.map(QueryParam::String)
|
||||
.unwrap_or(QueryParam::Null),
|
||||
item.album_name
|
||||
.map(QueryParam::String)
|
||||
.unwrap_or(QueryParam::Null),
|
||||
item.album_artist
|
||||
.map(QueryParam::String)
|
||||
.unwrap_or(QueryParam::Null),
|
||||
item.artists
|
||||
.map(QueryParam::String)
|
||||
.unwrap_or(QueryParam::Null),
|
||||
item.index_number
|
||||
.map(QueryParam::Int)
|
||||
.unwrap_or(QueryParam::Null),
|
||||
item.series_id
|
||||
.map(QueryParam::String)
|
||||
.unwrap_or(QueryParam::Null),
|
||||
item.series_name
|
||||
.map(QueryParam::String)
|
||||
.unwrap_or(QueryParam::Null),
|
||||
item.season_id
|
||||
.map(QueryParam::String)
|
||||
.unwrap_or(QueryParam::Null),
|
||||
item.season_name
|
||||
.map(QueryParam::String)
|
||||
.unwrap_or(QueryParam::Null),
|
||||
item.parent_index_number
|
||||
.map(QueryParam::Int)
|
||||
.unwrap_or(QueryParam::Null),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1256,7 +1347,6 @@ pub async fn storage_get_pending_sync_count(
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user