Duncan Tourolle 144abb2f8b
Some checks failed
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 14m27s
Traceability Validation / Check Requirement Traces (push) Failing after 4s
🏗️ Build and Test JellyTau / Build Android APK (push) Failing after 1m42s
Workstream C: convert commands/storage to a folder module, extract thumbnail/people/series-prefs commands
- Move commands/storage.rs to commands/storage/mod.rs.
- Extract three cohesive, self-contained clusters:
  - storage/thumbnails.rs: thumbnail cache + image-URL commands.
  - storage/people.rs: person/cast metadata cache commands.
  - storage/series_prefs.rs: per-series preferred audio track commands.
- Re-exported via pub use so command names stay at commands::storage::*;
  invoke_handler unchanged. mod.rs shrinks from 1979 to 1500 lines, all tests pass.
2026-06-20 17:08:34 +02:00

176 lines
5.4 KiB
Rust

//! Person/cast metadata cache commands.
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use tauri::State;
use super::DatabaseWrapper;
use crate::storage::db_service::{DatabaseService, Query, QueryParam};
/// Cached person info returned to frontend
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CachedPerson {
pub id: String,
pub server_id: String,
pub name: String,
pub overview: Option<String>,
pub primary_image_tag: Option<String>,
pub premiere_date: Option<String>,
pub end_date: Option<String>,
}
/// Item-person association for caching
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CachedItemPerson {
pub item_id: String,
pub person_id: String,
pub server_id: String,
pub person_type: String,
pub role: Option<String>,
pub sort_order: i32,
}
/// Save a person to the cache
#[tauri::command]
pub async fn storage_save_person(
db: State<'_, DatabaseWrapper>,
person: CachedPerson,
) -> Result<(), String> {
let db_service = {
let database = db.0.lock().map_err(|e| e.to_string())?;
Arc::new(database.service())
};
let query = Query::with_params(
"INSERT OR REPLACE INTO people (
id, server_id, name, overview, primary_image_tag,
premiere_date, end_date, synced_at
) VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)",
vec![
QueryParam::String(person.id),
QueryParam::String(person.server_id),
QueryParam::String(person.name),
person.overview.map(QueryParam::String).unwrap_or(QueryParam::Null),
person.primary_image_tag.map(QueryParam::String).unwrap_or(QueryParam::Null),
person.premiere_date.map(QueryParam::String).unwrap_or(QueryParam::Null),
person.end_date.map(QueryParam::String).unwrap_or(QueryParam::Null),
],
);
db_service.execute(query).await.map_err(|e| e.to_string())?;
Ok(())
}
/// Get a cached person by ID
#[tauri::command]
pub async fn storage_get_person(
db: State<'_, DatabaseWrapper>,
person_id: String,
) -> Result<Option<CachedPerson>, String> {
let db_service = {
let database = db.0.lock().map_err(|e| e.to_string())?;
Arc::new(database.service())
};
let query = Query::with_params(
"SELECT id, server_id, name, overview, primary_image_tag, premiere_date, end_date
FROM people WHERE id = ?",
vec![QueryParam::String(person_id)],
);
let result = db_service
.query_optional(query, |row| {
Ok(CachedPerson {
id: row.get(0)?,
server_id: row.get(1)?,
name: row.get(2)?,
overview: row.get(3)?,
primary_image_tag: row.get(4)?,
premiere_date: row.get(5)?,
end_date: row.get(6)?,
})
})
.await
.map_err(|e| e.to_string())?;
Ok(result)
}
/// Save item-person associations (batch)
#[tauri::command]
pub async fn storage_save_item_people(
db: State<'_, DatabaseWrapper>,
associations: Vec<CachedItemPerson>,
) -> Result<(), String> {
let db_service = {
let database = db.0.lock().map_err(|e| e.to_string())?;
Arc::new(database.service())
};
// Clone associations for the closure
let associations_clone = associations.clone();
// Use transaction for batch insert
db_service.transaction(move |tx| {
for assoc in &associations_clone {
let query = Query::with_params(
"INSERT OR REPLACE INTO item_people (
item_id, person_id, server_id, person_type, role, sort_order, synced_at
) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)",
vec![
QueryParam::String(assoc.item_id.clone()),
QueryParam::String(assoc.person_id.clone()),
QueryParam::String(assoc.server_id.clone()),
QueryParam::String(assoc.person_type.clone()),
assoc.role.clone().map(QueryParam::String).unwrap_or(QueryParam::Null),
QueryParam::Int(assoc.sort_order),
],
);
tx.execute(query)?;
}
Ok(())
}).await.map_err(|e| e.to_string())?;
Ok(())
}
/// Get people for an item (with person details joined)
#[tauri::command]
pub async fn storage_get_item_people(
db: State<'_, DatabaseWrapper>,
item_id: String,
) -> Result<Vec<CachedItemPerson>, String> {
let db_service = {
let database = db.0.lock().map_err(|e| e.to_string())?;
Arc::new(database.service())
};
let query = Query::with_params(
"SELECT ip.item_id, ip.person_id, ip.server_id, ip.person_type, ip.role, ip.sort_order
FROM item_people ip
WHERE ip.item_id = ?
ORDER BY ip.sort_order ASC",
vec![QueryParam::String(item_id)],
);
let people = db_service
.query_many(query, |row| {
Ok(CachedItemPerson {
item_id: row.get(0)?,
person_id: row.get(1)?,
server_id: row.get(2)?,
person_type: row.get(3)?,
role: row.get(4)?,
sort_order: row.get(5)?,
})
})
.await
.map_err(|e| e.to_string())?;
Ok(people)
}