Skip to main content

#
jellytau_lib/commands/storage/
people.rs

    1//! Person/cast metadata cache commands.
    2//!
    3//! TRACES: UR-035, UR-036 | IR-023 | DR-040, DR-041
    4
    5use serde::{Deserialize, Serialize};
    6use std::sync::Arc;
    7use tauri::State;
    8
    9use super::DatabaseWrapper;
    10use crate::storage::db_service::{DatabaseService, Query, QueryParam};
    11
    12/// Cached person info returned to frontend
    13#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
    14#[serde(rename_all = "camelCase")]
    15pub struct CachedPerson {
    16    pub id: String,
    17    pub server_id: String,
    18    pub name: String,
    19    pub overview: Option<String>,
    20    pub primary_image_tag: Option<String>,
    21    pub premiere_date: Option<String>,
    22    pub end_date: Option<String>,
    23}
    24
    25/// Item-person association for caching
    26#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
    27#[serde(rename_all = "camelCase")]
    28pub struct CachedItemPerson {
    29    pub item_id: String,
    30    pub person_id: String,
    31    pub server_id: String,
    32    pub person_type: String,
    33    pub role: Option<String>,
    34    pub sort_order: i32,
    35}
    36
    37/// Save a person to the cache
    38#[tauri::command]
    39#[specta::specta]
    40pub async fn storage_save_person(
    41    db: State<'_, DatabaseWrapper>,
    42    person: CachedPerson,
    43) -> Result<(), String> {
    44    let db_service = {
    45        let database = db.0.lock().map_err(|e| e.to_string())?;
    46        Arc::new(database.service())
    47    };
    48
    49    let query = Query::with_params(
    50        // A real UPSERT, not INSERT OR REPLACE — `people` is now backed by the
    51        // `people_fts` index (migration 022), and REPLACE would orphan an index
    52        // entry on every re-cache: it fires no AFTER DELETE trigger without
    53        // `recursive_triggers`, and reassigns the rowid that `content_rowid`
    54        // refers to. Same defect as DR-110 fixed for `items`.
    55        //
    56        // TRACES: UR-065 | DR-110, DR-111
    57        "INSERT INTO people (
    58            id, server_id, name, overview, primary_image_tag,
    59            premiere_date, end_date, synced_at
    60        ) VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
    61        ON CONFLICT(id) DO UPDATE SET
    62            server_id = excluded.server_id,
    63            name = excluded.name,
    64            overview = excluded.overview,
    65            primary_image_tag = excluded.primary_image_tag,
    66            premiere_date = excluded.premiere_date,
    67            end_date = excluded.end_date,
    68            synced_at = CURRENT_TIMESTAMP",
    69        vec![
    70            QueryParam::String(person.id),
    71            QueryParam::String(person.server_id),
    72            QueryParam::String(person.name),
    73            person
    74                .overview
    75                .map(QueryParam::String)
    76                .unwrap_or(QueryParam::Null),
    77            person
    78                .primary_image_tag
    79                .map(QueryParam::String)
    80                .unwrap_or(QueryParam::Null),
    81            person
    82                .premiere_date
    83                .map(QueryParam::String)
    84                .unwrap_or(QueryParam::Null),
    85            person
    86                .end_date
    87                .map(QueryParam::String)
    88                .unwrap_or(QueryParam::Null),
    89        ],
    90    );
    91
    92    db_service.execute(query).await.map_err(|e| e.to_string())?;
    93    Ok(())
    94}
    95
    96/// Get a cached person by ID
    97#[tauri::command]
    98#[specta::specta]
    99pub async fn storage_get_person(
    100    db: State<'_, DatabaseWrapper>,
    101    person_id: String,
    102) -> Result<Option<CachedPerson>, String> {
    103    let db_service = {
    104        let database = db.0.lock().map_err(|e| e.to_string())?;
    105        Arc::new(database.service())
    106    };
    107
    108    let query = Query::with_params(
    109        "SELECT id, server_id, name, overview, primary_image_tag, premiere_date, end_date
    110         FROM people WHERE id = ?",
    111        vec![QueryParam::String(person_id)],
    112    );
    113
    114    let result = db_service
    115        .query_optional(query, |row| {
    116            Ok(CachedPerson {
    117                id: row.get(0)?,
    118                server_id: row.get(1)?,
    119                name: row.get(2)?,
    120                overview: row.get(3)?,
    121                primary_image_tag: row.get(4)?,
    122                premiere_date: row.get(5)?,
    123                end_date: row.get(6)?,
    124            })
    125        })
    126        .await
    127        .map_err(|e| e.to_string())?;
    128
    129    Ok(result)
    130}
    131
    132/// Save item-person associations (batch)
    133#[tauri::command]
    134#[specta::specta]
    135pub async fn storage_save_item_people(
    136    db: State<'_, DatabaseWrapper>,
    137    associations: Vec<CachedItemPerson>,
    138) -> Result<(), String> {
    139    let db_service = {
    140        let database = db.0.lock().map_err(|e| e.to_string())?;
    141        Arc::new(database.service())
    142    };
    143
    144    // Clone associations for the closure
    145    let associations_clone = associations.clone();
    146
    147    // Use transaction for batch insert
    148    db_service
    149        .transaction(move |tx| {
    150            for assoc in &associations_clone {
    151                let query = Query::with_params(
    152                    "INSERT OR REPLACE INTO item_people (
    153                    item_id, person_id, server_id, person_type, role, sort_order, synced_at
    154                ) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)",
    155                    vec![
    156                        QueryParam::String(assoc.item_id.clone()),
    157                        QueryParam::String(assoc.person_id.clone()),
    158                        QueryParam::String(assoc.server_id.clone()),
    159                        QueryParam::String(assoc.person_type.clone()),
    160                        assoc
    161                            .role
    162                            .clone()
    163                            .map(QueryParam::String)
    164                            .unwrap_or(QueryParam::Null),
    165                        QueryParam::Int(assoc.sort_order),
    166                    ],
    167                );
    168                tx.execute(query)?;
    169            }
    170            Ok(())
    171        })
    172        .await
    173        .map_err(|e| e.to_string())?;
    174
    175    Ok(())
    176}
    177
    178/// Get people for an item (with person details joined)
    179#[tauri::command]
    180#[specta::specta]
    181pub async fn storage_get_item_people(
    182    db: State<'_, DatabaseWrapper>,
    183    item_id: String,
    184) -> Result<Vec<CachedItemPerson>, String> {
    185    let db_service = {
    186        let database = db.0.lock().map_err(|e| e.to_string())?;
    187        Arc::new(database.service())
    188    };
    189
    190    let query = Query::with_params(
    191        "SELECT ip.item_id, ip.person_id, ip.server_id, ip.person_type, ip.role, ip.sort_order
    192         FROM item_people ip
    193         WHERE ip.item_id = ?
    194         ORDER BY ip.sort_order ASC",
    195        vec![QueryParam::String(item_id)],
    196    );
    197
    198    let people = db_service
    199        .query_many(query, |row| {
    200            Ok(CachedItemPerson {
    201                item_id: row.get(0)?,
    202                person_id: row.get(1)?,
    203                server_id: row.get(2)?,
    204                person_type: row.get(3)?,
    205                role: row.get(4)?,
    206                sort_order: row.get(5)?,
    207            })
    208        })
    209        .await
    210        .map_err(|e| e.to_string())?;
    211
    212    Ok(people)
    213}