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}