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:
@@ -128,7 +128,9 @@ impl DatabaseService for RusqliteService {
|
||||
async fn execute(&self, query: Query) -> DbResult<usize> {
|
||||
let conn = Arc::clone(&self.conn);
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let conn = conn.lock().map_err(|e| format!("Failed to lock connection: {}", e))?;
|
||||
let conn = conn
|
||||
.lock()
|
||||
.map_err(|e| format!("Failed to lock connection: {}", e))?;
|
||||
execute_query(&conn, query)
|
||||
})
|
||||
.await
|
||||
@@ -139,7 +141,9 @@ impl DatabaseService for RusqliteService {
|
||||
let conn = Arc::clone(&self.conn);
|
||||
let sql = sql.to_string();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let conn = conn.lock().map_err(|e| format!("Failed to lock connection: {}", e))?;
|
||||
let conn = conn
|
||||
.lock()
|
||||
.map_err(|e| format!("Failed to lock connection: {}", e))?;
|
||||
conn.execute_batch(&sql)
|
||||
.map_err(|e| format!("Execute batch failed: {}", e))
|
||||
})
|
||||
@@ -154,7 +158,9 @@ impl DatabaseService for RusqliteService {
|
||||
{
|
||||
let conn = Arc::clone(&self.conn);
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let conn = conn.lock().map_err(|e| format!("Failed to lock connection: {}", e))?;
|
||||
let conn = conn
|
||||
.lock()
|
||||
.map_err(|e| format!("Failed to lock connection: {}", e))?;
|
||||
query_one(&conn, query, mapper)
|
||||
})
|
||||
.await
|
||||
@@ -168,7 +174,9 @@ impl DatabaseService for RusqliteService {
|
||||
{
|
||||
let conn = Arc::clone(&self.conn);
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let conn = conn.lock().map_err(|e| format!("Failed to lock connection: {}", e))?;
|
||||
let conn = conn
|
||||
.lock()
|
||||
.map_err(|e| format!("Failed to lock connection: {}", e))?;
|
||||
query_optional(&conn, query, mapper)
|
||||
})
|
||||
.await
|
||||
@@ -182,7 +190,9 @@ impl DatabaseService for RusqliteService {
|
||||
{
|
||||
let conn = Arc::clone(&self.conn);
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let conn = conn.lock().map_err(|e| format!("Failed to lock connection: {}", e))?;
|
||||
let conn = conn
|
||||
.lock()
|
||||
.map_err(|e| format!("Failed to lock connection: {}", e))?;
|
||||
query_many(&conn, query, mapper)
|
||||
})
|
||||
.await
|
||||
@@ -196,7 +206,9 @@ impl DatabaseService for RusqliteService {
|
||||
{
|
||||
let conn = Arc::clone(&self.conn);
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let conn = conn.lock().map_err(|e| format!("Failed to lock connection: {}", e))?;
|
||||
let conn = conn
|
||||
.lock()
|
||||
.map_err(|e| format!("Failed to lock connection: {}", e))?;
|
||||
|
||||
conn.execute("BEGIN TRANSACTION", [])
|
||||
.map_err(|e| format!("Failed to begin transaction: {}", e))?;
|
||||
@@ -224,7 +236,9 @@ impl DatabaseService for RusqliteService {
|
||||
async fn last_insert_rowid(&self) -> DbResult<i64> {
|
||||
let conn = Arc::clone(&self.conn);
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let conn = conn.lock().map_err(|e| format!("Failed to lock connection: {}", e))?;
|
||||
let conn = conn
|
||||
.lock()
|
||||
.map_err(|e| format!("Failed to lock connection: {}", e))?;
|
||||
Ok(conn.last_insert_rowid())
|
||||
})
|
||||
.await
|
||||
@@ -255,7 +269,9 @@ where
|
||||
{
|
||||
match query_one(conn, query, mapper) {
|
||||
Ok(value) => Ok(Some(value)),
|
||||
Err(e) if e.contains("Query returned no rows") || e.contains("QueryReturnedNoRows") => Ok(None),
|
||||
Err(e) if e.contains("Query returned no rows") || e.contains("QueryReturnedNoRows") => {
|
||||
Ok(None)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
@@ -325,10 +341,7 @@ mod tests {
|
||||
let service = RusqliteService::new(Arc::new(Mutex::new(conn)));
|
||||
|
||||
let query = Query::new("SELECT name FROM test WHERE id = 1");
|
||||
let name: String = service
|
||||
.query_one(query, |row| row.get(0))
|
||||
.await
|
||||
.unwrap();
|
||||
let name: String = service.query_one(query, |row| row.get(0)).await.unwrap();
|
||||
|
||||
assert_eq!(name, "Bob");
|
||||
}
|
||||
@@ -346,10 +359,7 @@ mod tests {
|
||||
let service = RusqliteService::new(Arc::new(Mutex::new(conn)));
|
||||
|
||||
let query = Query::new("SELECT name FROM test ORDER BY id");
|
||||
let names: Vec<String> = service
|
||||
.query_many(query, |row| row.get(0))
|
||||
.await
|
||||
.unwrap();
|
||||
let names: Vec<String> = service.query_many(query, |row| row.get(0)).await.unwrap();
|
||||
|
||||
assert_eq!(names, vec!["Alice", "Bob"]);
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ use std::sync::{Arc, Mutex};
|
||||
use log::{debug, error, info};
|
||||
use rusqlite::{Connection, Result as SqliteResult};
|
||||
|
||||
use schema::MIGRATIONS;
|
||||
pub use db_service::{DatabaseService, RusqliteService};
|
||||
use schema::MIGRATIONS;
|
||||
|
||||
/// Database connection wrapper with thread-safe access
|
||||
pub struct Database {
|
||||
@@ -113,10 +113,7 @@ impl Database {
|
||||
match conn.execute_batch(sql) {
|
||||
Ok(_) => {
|
||||
info!("Successfully applied migration: {}", name);
|
||||
match conn.execute(
|
||||
"INSERT INTO _migrations (name) VALUES (?1)",
|
||||
[name],
|
||||
) {
|
||||
match conn.execute("INSERT INTO _migrations (name) VALUES (?1)", [name]) {
|
||||
Ok(_) => debug!("Recorded migration: {}", name),
|
||||
Err(e) => {
|
||||
error!("Failed to record migration {}: {}", name, e);
|
||||
@@ -264,9 +261,11 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let name: String = conn
|
||||
.query_row("SELECT name FROM servers WHERE id = ?1", ["server1"], |row: &rusqlite::Row| {
|
||||
row.get(0)
|
||||
})
|
||||
.query_row(
|
||||
"SELECT name FROM servers WHERE id = ?1",
|
||||
["server1"],
|
||||
|row: &rusqlite::Row| row.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(name, "Updated Server");
|
||||
|
||||
@@ -275,7 +274,9 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let count: i32 = conn
|
||||
.query_row("SELECT COUNT(*) FROM servers", [], |row: &rusqlite::Row| row.get(0))
|
||||
.query_row("SELECT COUNT(*) FROM servers", [], |row: &rusqlite::Row| {
|
||||
row.get(0)
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(count, 0);
|
||||
}
|
||||
@@ -313,16 +314,15 @@ mod tests {
|
||||
assert_eq!(is_active, 1);
|
||||
|
||||
// Update is_active
|
||||
conn.execute(
|
||||
"UPDATE users SET is_active = 0 WHERE id = ?1",
|
||||
["user1"],
|
||||
)
|
||||
.unwrap();
|
||||
conn.execute("UPDATE users SET is_active = 0 WHERE id = ?1", ["user1"])
|
||||
.unwrap();
|
||||
|
||||
let is_active: i32 = conn
|
||||
.query_row("SELECT is_active FROM users WHERE id = ?1", ["user1"], |row: &rusqlite::Row| {
|
||||
row.get(0)
|
||||
})
|
||||
.query_row(
|
||||
"SELECT is_active FROM users WHERE id = ?1",
|
||||
["user1"],
|
||||
|row: &rusqlite::Row| row.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(is_active, 0);
|
||||
}
|
||||
@@ -348,9 +348,11 @@ mod tests {
|
||||
|
||||
// Verify user exists
|
||||
let count: i32 = conn
|
||||
.query_row("SELECT COUNT(*) FROM users WHERE server_id = ?1", ["server1"], |row: &rusqlite::Row| {
|
||||
row.get(0)
|
||||
})
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM users WHERE server_id = ?1",
|
||||
["server1"],
|
||||
|row: &rusqlite::Row| row.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(count, 1);
|
||||
|
||||
@@ -360,7 +362,9 @@ mod tests {
|
||||
|
||||
// User should be deleted via CASCADE
|
||||
let count: i32 = conn
|
||||
.query_row("SELECT COUNT(*) FROM users", [], |row: &rusqlite::Row| row.get(0))
|
||||
.query_row("SELECT COUNT(*) FROM users", [], |row: &rusqlite::Row| {
|
||||
row.get(0)
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(count, 0);
|
||||
}
|
||||
@@ -677,15 +681,16 @@ mod tests {
|
||||
|
||||
// Initially both users are active (simulating the old bug)
|
||||
let active_count: i32 = conn
|
||||
.query_row("SELECT COUNT(*) FROM users WHERE is_active = 1", [], |row: &rusqlite::Row| {
|
||||
row.get(0)
|
||||
})
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM users WHERE is_active = 1",
|
||||
[],
|
||||
|row: &rusqlite::Row| row.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(active_count, 2);
|
||||
|
||||
// Now simulate setting user1 as active (global deactivation)
|
||||
conn.execute("UPDATE users SET is_active = 0", [])
|
||||
.unwrap();
|
||||
conn.execute("UPDATE users SET is_active = 0", []).unwrap();
|
||||
conn.execute(
|
||||
"UPDATE users SET is_active = 1, last_login_at = CURRENT_TIMESTAMP WHERE id = ?1",
|
||||
["user1"],
|
||||
@@ -694,9 +699,11 @@ mod tests {
|
||||
|
||||
// Only one user should be active now
|
||||
let active_count: i32 = conn
|
||||
.query_row("SELECT COUNT(*) FROM users WHERE is_active = 1", [], |row: &rusqlite::Row| {
|
||||
row.get(0)
|
||||
})
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM users WHERE is_active = 1",
|
||||
[],
|
||||
|row: &rusqlite::Row| row.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(active_count, 1);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user