Skip to main content

#
jellytau_lib/commands/
offline.rs

    1//! Tauri commands for offline data access
    2
    3use serde::{Deserialize, Serialize};
    4use std::sync::Arc;
    5use tauri::State;
    6
    7use super::DatabaseWrapper;
    8use crate::storage::db_service::{DatabaseService, Query, QueryParam};
    9
    10#[derive(specta::Type, Debug, Serialize, Deserialize)]
    11#[serde(rename_all = "camelCase")]
    12pub struct OfflineItem {
    13    pub id: String,
    14    pub name: String,
    15    pub item_type: String,
    16    pub album_id: Option<String>,
    17    pub album_name: Option<String>,
    18    pub artists: Option<String>,
    19    pub runtime_ticks: Option<i64>,
    20    pub primary_image_tag: Option<String>,
    21}
    22
    23/// Check if an item is available offline
    24#[tauri::command]
    25#[specta::specta]
    26pub async fn offline_is_available(
    27    db: State<'_, DatabaseWrapper>,
    28    item_id: String,
    29) -> Result<bool, String> {
    30    let db_service = {
    31        let database = db.0.lock().map_err(|e| e.to_string())?;
    32        Arc::new(database.service())
    33    };
    34
    35    let query = Query::with_params(
    36        "SELECT COUNT(*) FROM downloads WHERE item_id = ? AND status = 'completed'",
    37        vec![QueryParam::String(item_id)],
    38    );
    39
    40    let count: i64 = db_service
    41        .query_one(query, |row| row.get(0))
    42        .await
    43        .map_err(|e| e.to_string())?;
    44
    45    Ok(count > 0)
    46}
    47
    48/// Get all offline items for a user
    49#[tauri::command]
    50#[specta::specta]
    51pub async fn offline_get_items(
    52    db: State<'_, DatabaseWrapper>,
    53    user_id: String,
    54) -> Result<Vec<OfflineItem>, String> {
    55    let db_service = {
    56        let database = db.0.lock().map_err(|e| e.to_string())?;
    57        Arc::new(database.service())
    58    };
    59
    60    let query = Query::with_params(
    61        "SELECT i.id, i.name, i.item_type, i.album_id, i.album_name, i.artists,
    62                i.runtime_ticks, i.primary_image_tag
    63         FROM items i
    64         INNER JOIN downloads d ON i.id = d.item_id
    65         WHERE d.user_id = ? AND d.status = 'completed'
    66         ORDER BY d.completed_at DESC",
    67        vec![QueryParam::String(user_id)],
    68    );
    69
    70    db_service
    71        .query_many(query, |row| {
    72            Ok(OfflineItem {
    73                id: row.get(0)?,
    74                name: row.get(1)?,
    75                item_type: row.get(2)?,
    76                album_id: row.get(3)?,
    77                album_name: row.get(4)?,
    78                artists: row.get(5)?,
    79                runtime_ticks: row.get(6)?,
    80                primary_image_tag: row.get(7)?,
    81            })
    82        })
    83        .await
    84        .map_err(|e| e.to_string())
    85}
    86
    87/// Search offline items
    88#[tauri::command]
    89#[specta::specta]
    90pub async fn offline_search(
    91    db: State<'_, DatabaseWrapper>,
    92    user_id: String,
    93    query: String,
    94) -> Result<Vec<OfflineItem>, String> {
    95    let db_service = {
    96        let database = db.0.lock().map_err(|e| e.to_string())?;
    97        Arc::new(database.service())
    98    };
    99
    100    let search_query = format!("%{}%", query.to_lowercase());
    101
    102    let db_query = Query::with_params(
    103        "SELECT i.id, i.name, i.item_type, i.album_id, i.album_name, i.artists,
    104                i.runtime_ticks, i.primary_image_tag
    105         FROM items i
    106         INNER JOIN downloads d ON i.id = d.item_id
    107         WHERE d.user_id = ? AND d.status = 'completed'
    108           AND (LOWER(i.name) LIKE ? OR LOWER(i.artists) LIKE ? OR LOWER(i.album_name) LIKE ?)
    109         ORDER BY i.name
    110         LIMIT 50",
    111        vec![
    112            QueryParam::String(user_id),
    113            QueryParam::String(search_query.clone()),
    114            QueryParam::String(search_query.clone()),
    115            QueryParam::String(search_query),
    116        ],
    117    );
    118
    119    db_service
    120        .query_many(db_query, |row| {
    121            Ok(OfflineItem {
    122                id: row.get(0)?,
    123                name: row.get(1)?,
    124                item_type: row.get(2)?,
    125                album_id: row.get(3)?,
    126                album_name: row.get(4)?,
    127                artists: row.get(5)?,
    128                runtime_ticks: row.get(6)?,
    129                primary_image_tag: row.get(7)?,
    130            })
    131        })
    132        .await
    133        .map_err(|e| e.to_string())
    134}
    135
    136// TRACES: UR-002, UR-011 | DR-017 | UT-044
    137#[cfg(test)]
    138mod tests {
    139    use super::*;
    140
    141    #[test]
    142    fn test_offline_item_serialization() {
    143        let item = OfflineItem {
    144            id: "123".to_string(),
    145            name: "Test Song".to_string(),
    146            item_type: "Audio".to_string(),
    147            album_id: Some("album1".to_string()),
    148            album_name: Some("Test Album".to_string()),
    149            artists: Some("Artist 1".to_string()),
    150            runtime_ticks: Some(180000000),
    151            primary_image_tag: Some("tag123".to_string()),
    152        };
    153
    154        let json = serde_json::to_string(&item).unwrap();
    155        assert!(json.contains("\"itemType\":\"Audio\""));
    156        assert!(json.contains("\"albumName\":\"Test Album\""));
    157    }
    158}