Workstream E (backend): wire tauri-specta — annotate commands, derive Type, generate-ready Builder
- Add #[specta::specta] to all 201 #[tauri::command] functions. - Derive specta::Type on all IPC DTOs (repository/types, settings, player/storage/ download command DTOs, player enums, jellyfin SessionInfo/NowPlayingItem/PlayState, ThumbnailCacheStats, DownloadInfo, CacheConfig, etc.). - Replace tauri::generate_handler! with a tauri_specta::Builder + collect_commands! in lib.rs (exports bindings.ts in debug builds). Two contract changes required by specta constraints (frontend migration follows): - specta caps command arity at 10 args: download_item_and_start / download_item / download_video now take a single request struct (params bundled, body unchanged via destructuring). - specta can't parse split serde rename_all: SessionInfo/NowPlayingItem/PlayState switched to rename_all = "PascalCase" (Jellyfin deserialization preserved; these now serialize PascalCase to the frontend). cargo check --lib is clean (0 errors). Frontend migration to bindings.ts is the next step.
This commit is contained in:
@@ -22,7 +22,7 @@ pub struct DownloadManagerWrapper(pub Mutex<DownloadManager>);
|
||||
|
||||
/// Download statistics computed server-side
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(specta::Type, Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DownloadStats {
|
||||
pub total: usize,
|
||||
@@ -35,7 +35,7 @@ pub struct DownloadStats {
|
||||
|
||||
/// Enhanced response with pre-computed stats
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(specta::Type, Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DownloadsResponse {
|
||||
pub downloads: Vec<DownloadInfo>,
|
||||
@@ -52,22 +52,66 @@ fn sanitize_filename(name: &str) -> String {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Request payload for download_item_and_start (bundled to stay within specta's
|
||||
/// 10-argument command limit).
|
||||
#[derive(Debug, specta::Type, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DownloadItemAndStartRequest {
|
||||
pub item_id: String,
|
||||
pub user_id: String,
|
||||
pub stream_url: String,
|
||||
pub target_dir: String,
|
||||
pub item_name: Option<String>,
|
||||
pub artist_name: Option<String>,
|
||||
pub album_name: Option<String>,
|
||||
}
|
||||
|
||||
/// Request payload for download_item.
|
||||
#[derive(Debug, specta::Type, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DownloadItemRequest {
|
||||
pub item_id: String,
|
||||
pub user_id: String,
|
||||
pub file_path: String,
|
||||
pub mime_type: Option<String>,
|
||||
pub priority: Option<i32>,
|
||||
pub item_name: Option<String>,
|
||||
pub artist_name: Option<String>,
|
||||
pub album_name: Option<String>,
|
||||
pub expected_size: Option<i64>,
|
||||
}
|
||||
|
||||
/// Request payload for download_video.
|
||||
#[derive(Debug, specta::Type, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DownloadVideoRequest {
|
||||
pub item_id: String,
|
||||
pub user_id: String,
|
||||
pub file_path: String,
|
||||
pub mime_type: Option<String>,
|
||||
pub priority: Option<i32>,
|
||||
pub item_name: Option<String>,
|
||||
pub quality_preset: Option<String>,
|
||||
pub series_name: Option<String>,
|
||||
pub season_name: Option<String>,
|
||||
pub episode_number: Option<i32>,
|
||||
pub season_number: Option<i32>,
|
||||
}
|
||||
|
||||
/// Queue and start a download in a single atomic operation
|
||||
/// This simplifies the frontend flow by combining multiple steps
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn download_item_and_start(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
smart_cache: State<'_, SmartCacheWrapper>,
|
||||
download_manager: State<'_, DownloadManagerWrapper>,
|
||||
app: tauri::AppHandle,
|
||||
item_id: String,
|
||||
user_id: String,
|
||||
stream_url: String,
|
||||
target_dir: String,
|
||||
item_name: Option<String>,
|
||||
artist_name: Option<String>,
|
||||
album_name: Option<String>,
|
||||
request: DownloadItemAndStartRequest,
|
||||
) -> Result<i64, String> {
|
||||
let DownloadItemAndStartRequest {
|
||||
item_id, user_id, stream_url, target_dir, item_name, artist_name, album_name,
|
||||
} = request;
|
||||
// Sanitize filename
|
||||
let safe_name = sanitize_filename(item_name.as_deref().unwrap_or(&item_id));
|
||||
let file_path = format!("downloads/{}.mp3", safe_name);
|
||||
@@ -76,15 +120,17 @@ pub async fn download_item_and_start(
|
||||
let download_id = download_item(
|
||||
db.clone(),
|
||||
smart_cache.clone(),
|
||||
item_id,
|
||||
user_id,
|
||||
file_path,
|
||||
None, // mime_type
|
||||
None, // priority
|
||||
item_name,
|
||||
artist_name,
|
||||
album_name,
|
||||
None, // expected_size
|
||||
DownloadItemRequest {
|
||||
item_id,
|
||||
user_id,
|
||||
file_path,
|
||||
mime_type: None,
|
||||
priority: None,
|
||||
item_name,
|
||||
artist_name,
|
||||
album_name,
|
||||
expected_size: None,
|
||||
},
|
||||
).await?;
|
||||
|
||||
// Start the download immediately
|
||||
@@ -102,19 +148,15 @@ pub async fn download_item_and_start(
|
||||
|
||||
/// Queue a media item for download
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn download_item(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
smart_cache: State<'_, SmartCacheWrapper>,
|
||||
item_id: String,
|
||||
user_id: String,
|
||||
file_path: String,
|
||||
mime_type: Option<String>,
|
||||
priority: Option<i32>,
|
||||
item_name: Option<String>,
|
||||
artist_name: Option<String>,
|
||||
album_name: Option<String>,
|
||||
expected_size: Option<i64>,
|
||||
request: DownloadItemRequest,
|
||||
) -> Result<i64, String> {
|
||||
let DownloadItemRequest {
|
||||
item_id, user_id, file_path, mime_type, priority, item_name, artist_name, album_name, expected_size,
|
||||
} = request;
|
||||
let db_service = {
|
||||
let database = db.0.lock().map_err(|e| e.to_string())?;
|
||||
Arc::new(database.service())
|
||||
@@ -196,6 +238,7 @@ pub async fn download_item(
|
||||
|
||||
/// Queue an entire album for download
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn download_album(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
album_id: String,
|
||||
@@ -267,21 +310,15 @@ pub async fn download_album(
|
||||
|
||||
/// Queue a video item (movie or episode) for download with quality preset
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn download_video(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
item_id: String,
|
||||
user_id: String,
|
||||
file_path: String,
|
||||
mime_type: Option<String>,
|
||||
priority: Option<i32>,
|
||||
item_name: Option<String>,
|
||||
quality_preset: Option<String>,
|
||||
// Video-specific metadata
|
||||
series_name: Option<String>,
|
||||
season_name: Option<String>,
|
||||
episode_number: Option<i32>,
|
||||
season_number: Option<i32>,
|
||||
request: DownloadVideoRequest,
|
||||
) -> Result<i64, String> {
|
||||
let DownloadVideoRequest {
|
||||
item_id, user_id, file_path, mime_type, priority, item_name, quality_preset,
|
||||
series_name, season_name, episode_number, season_number,
|
||||
} = request;
|
||||
let db_service = {
|
||||
let database = db.0.lock().map_err(|e| e.to_string())?;
|
||||
Arc::new(database.service())
|
||||
@@ -338,6 +375,7 @@ pub async fn download_video(
|
||||
|
||||
/// Queue all episodes of a series for download
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn download_series(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
series_id: String,
|
||||
@@ -438,6 +476,7 @@ pub async fn download_series(
|
||||
|
||||
/// Queue all episodes of a specific season for download
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn download_season(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
season_id: String,
|
||||
@@ -558,6 +597,7 @@ fn compute_download_stats(downloads: &[DownloadInfo]) -> DownloadStats {
|
||||
|
||||
/// Get all downloads for a user, optionally filtered by status
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_downloads(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
user_id: String,
|
||||
@@ -632,6 +672,7 @@ pub async fn get_downloads(
|
||||
|
||||
/// Pause a download
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn pause_download(db: State<'_, DatabaseWrapper>, download_id: i64) -> Result<(), String> {
|
||||
let db_service = {
|
||||
let database = db.0.lock().map_err(|e| e.to_string())?;
|
||||
@@ -649,6 +690,7 @@ pub async fn pause_download(db: State<'_, DatabaseWrapper>, download_id: i64) ->
|
||||
|
||||
/// Resume a paused download
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn resume_download(db: State<'_, DatabaseWrapper>, download_id: i64) -> Result<(), String> {
|
||||
let db_service = {
|
||||
let database = db.0.lock().map_err(|e| e.to_string())?;
|
||||
@@ -666,6 +708,7 @@ pub async fn resume_download(db: State<'_, DatabaseWrapper>, download_id: i64) -
|
||||
|
||||
/// Cancel a download
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn cancel_download(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
download_manager: State<'_, DownloadManagerWrapper>,
|
||||
@@ -714,6 +757,7 @@ pub async fn cancel_download(
|
||||
|
||||
/// Mark a download as completed
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn mark_download_completed(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
download_id: i64,
|
||||
@@ -742,6 +786,7 @@ pub async fn mark_download_completed(
|
||||
|
||||
/// Mark a download as failed
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn mark_download_failed(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
download_id: i64,
|
||||
@@ -764,6 +809,7 @@ pub async fn mark_download_failed(
|
||||
/// Start downloading a file immediately
|
||||
/// This command actually downloads the file using the worker
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn start_download(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
download_manager: State<'_, DownloadManagerWrapper>,
|
||||
@@ -981,6 +1027,7 @@ pub async fn start_download(
|
||||
|
||||
/// Delete a completed download
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn delete_download(db: State<'_, DatabaseWrapper>, download_id: i64) -> Result<(), String> {
|
||||
let db_service = {
|
||||
let database = db.0.lock().map_err(|e| e.to_string())?;
|
||||
@@ -1048,7 +1095,7 @@ fn map_download_row(row: &rusqlite::Row) -> rusqlite::Result<DownloadInfo> {
|
||||
}
|
||||
|
||||
/// Storage statistics for downloads
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
#[derive(specta::Type, Debug, Clone, serde::Serialize)]
|
||||
pub struct StorageStats {
|
||||
pub total_bytes: i64,
|
||||
pub total_items: i64,
|
||||
@@ -1056,7 +1103,7 @@ pub struct StorageStats {
|
||||
}
|
||||
|
||||
/// Storage info for a single album
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
#[derive(specta::Type, Debug, Clone, serde::Serialize)]
|
||||
pub struct AlbumStorageInfo {
|
||||
pub album_id: String,
|
||||
pub album_name: String,
|
||||
@@ -1067,6 +1114,7 @@ pub struct AlbumStorageInfo {
|
||||
|
||||
/// Get storage statistics for downloads
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_download_storage_stats(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
user_id: String,
|
||||
@@ -1127,6 +1175,7 @@ pub async fn get_download_storage_stats(
|
||||
|
||||
/// Delete all downloads for a user
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn delete_all_downloads(db: State<'_, DatabaseWrapper>, user_id: String) -> Result<i64, String> {
|
||||
let db_service = {
|
||||
let database = db.0.lock().map_err(|e| e.to_string())?;
|
||||
@@ -1166,6 +1215,7 @@ pub async fn delete_all_downloads(db: State<'_, DatabaseWrapper>, user_id: Strin
|
||||
|
||||
/// Clear all stale pending/failed/paused downloads
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn clear_stale_downloads(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
user_id: String,
|
||||
@@ -1208,6 +1258,7 @@ pub async fn clear_stale_downloads(
|
||||
|
||||
/// Delete all downloads for a specific album
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn delete_album_downloads(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
album_id: String,
|
||||
@@ -1252,7 +1303,7 @@ pub async fn delete_album_downloads(
|
||||
}
|
||||
|
||||
/// Download manager statistics
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
#[derive(specta::Type, Debug, Clone, serde::Serialize)]
|
||||
pub struct DownloadManagerStats {
|
||||
pub max_concurrent: usize,
|
||||
pub active_count: usize,
|
||||
@@ -1261,6 +1312,7 @@ pub struct DownloadManagerStats {
|
||||
|
||||
/// Get download manager statistics
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_download_manager_stats(
|
||||
download_manager: State<'_, DownloadManagerWrapper>,
|
||||
) -> Result<DownloadManagerStats, String> {
|
||||
@@ -1278,6 +1330,7 @@ pub async fn get_download_manager_stats(
|
||||
|
||||
/// Set the maximum concurrent downloads
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn set_max_concurrent_downloads(
|
||||
download_manager: State<'_, DownloadManagerWrapper>,
|
||||
max: usize,
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::storage::db_service::{DatabaseService, Query, QueryParam};
|
||||
|
||||
/// Pin an item's metadata (protects from cache clear)
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn pin_item(db: State<'_, DatabaseWrapper>, item_id: String) -> Result<(), String> {
|
||||
let db_service = {
|
||||
let database = db.0.lock().map_err(|e| e.to_string())?;
|
||||
@@ -25,6 +26,7 @@ pub async fn pin_item(db: State<'_, DatabaseWrapper>, item_id: String) -> Result
|
||||
|
||||
/// Unpin an item's metadata
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn unpin_item(db: State<'_, DatabaseWrapper>, item_id: String) -> Result<(), String> {
|
||||
let db_service = {
|
||||
let database = db.0.lock().map_err(|e| e.to_string())?;
|
||||
@@ -42,6 +44,7 @@ pub async fn unpin_item(db: State<'_, DatabaseWrapper>, item_id: String) -> Resu
|
||||
|
||||
/// Check if an item is pinned
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn is_item_pinned(db: State<'_, DatabaseWrapper>, item_id: String) -> Result<bool, String> {
|
||||
let db_service = {
|
||||
let database = db.0.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::commands::{DatabaseWrapper, SmartCacheWrapper};
|
||||
use crate::storage::db_service::{DatabaseService, Query, QueryParam};
|
||||
|
||||
/// SmartCache statistics
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
#[derive(specta::Type, Debug, Clone, serde::Serialize)]
|
||||
pub struct SmartCacheStats {
|
||||
pub total_size: u64,
|
||||
pub storage_limit: u64,
|
||||
@@ -19,6 +19,7 @@ pub struct SmartCacheStats {
|
||||
|
||||
/// Get SmartCache statistics
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_smart_cache_stats(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
smart_cache: State<'_, SmartCacheWrapper>,
|
||||
@@ -67,6 +68,7 @@ pub async fn get_smart_cache_stats(
|
||||
|
||||
/// Update SmartCache configuration
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn update_smart_cache_config(
|
||||
smart_cache: State<'_, SmartCacheWrapper>,
|
||||
config: crate::download::cache::CacheConfig,
|
||||
@@ -79,6 +81,7 @@ pub async fn update_smart_cache_config(
|
||||
|
||||
/// Get SmartCache configuration
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_smart_cache_config(
|
||||
smart_cache: State<'_, SmartCacheWrapper>,
|
||||
) -> Result<crate::download::cache::CacheConfig, String> {
|
||||
@@ -89,7 +92,7 @@ pub async fn get_smart_cache_config(
|
||||
}
|
||||
|
||||
/// Album recommendation info
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
#[derive(specta::Type, Debug, Clone, serde::Serialize)]
|
||||
pub struct AlbumRecommendation {
|
||||
pub album_id: String,
|
||||
pub album_name: String,
|
||||
@@ -99,7 +102,7 @@ pub struct AlbumRecommendation {
|
||||
}
|
||||
|
||||
/// Album affinity status info
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
#[derive(specta::Type, Debug, Clone, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AlbumAffinityStatus {
|
||||
pub album_id: String,
|
||||
@@ -110,6 +113,7 @@ pub struct AlbumAffinityStatus {
|
||||
|
||||
/// Get album recommendations based on play history
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_album_recommendations(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
smart_cache: State<'_, SmartCacheWrapper>,
|
||||
@@ -189,6 +193,7 @@ pub async fn get_album_recommendations(
|
||||
/// Get album affinity status for all tracked albums
|
||||
/// This shows the SmartCache's internal play history and threshold status
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_album_affinity_status(
|
||||
smart_cache: State<'_, SmartCacheWrapper>,
|
||||
) -> Result<Vec<AlbumAffinityStatus>, String> {
|
||||
|
||||
Reference in New Issue
Block a user