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:
2026-06-20 18:20:25 +02:00
parent 55f1b85f12
commit ada3ed64ab
40 changed files with 598 additions and 354 deletions
+34 -7
View File
@@ -32,7 +32,7 @@ pub struct CredentialStoreWrapper(pub Mutex<CredentialStore>);
pub struct ThumbnailCacheWrapper(pub Arc<ThumbnailCache>);
/// Server info returned to frontend
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
pub struct ServerInfo {
pub id: String,
pub name: String,
@@ -41,7 +41,7 @@ pub struct ServerInfo {
}
/// User info returned to frontend
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserInfo {
pub id: String,
@@ -51,7 +51,7 @@ pub struct UserInfo {
}
/// Active session info (for session restoration)
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ActiveSession {
pub user_id: String,
@@ -63,7 +63,7 @@ pub struct ActiveSession {
}
/// Security status info
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SecurityStatus {
pub using_keyring: bool,
@@ -72,6 +72,7 @@ pub struct SecurityStatus {
/// Initialize the database and run migrations
#[tauri::command]
#[specta::specta]
pub fn storage_init(db: State<DatabaseWrapper>) -> Result<String, String> {
let database = db.0.lock().map_err(|e| e.to_string())?;
Ok(database.path().to_string_lossy().to_string())
@@ -79,6 +80,7 @@ pub fn storage_init(db: State<DatabaseWrapper>) -> Result<String, String> {
/// Get storage directory path (parent directory of the database file)
#[tauri::command]
#[specta::specta]
pub fn storage_get_path(db: State<DatabaseWrapper>) -> Result<String, String> {
let database = db.0.lock().map_err(|e| e.to_string())?;
let db_path = database.path();
@@ -92,6 +94,7 @@ pub fn storage_get_path(db: State<DatabaseWrapper>) -> Result<String, String> {
/// Get database file size in bytes
#[tauri::command]
#[specta::specta]
pub fn storage_get_size(db: State<DatabaseWrapper>) -> Result<Option<u64>, String> {
let database = db.0.lock().map_err(|e| e.to_string())?;
Ok(database.file_size())
@@ -99,6 +102,7 @@ pub fn storage_get_size(db: State<DatabaseWrapper>) -> Result<Option<u64>, Strin
/// Get security status (keyring vs encrypted file fallback)
#[tauri::command]
#[specta::specta]
pub fn storage_get_security_status(
creds: State<CredentialStoreWrapper>,
) -> Result<SecurityStatus, String> {
@@ -117,6 +121,7 @@ pub fn storage_get_security_status(
/// Save a server connection
/// Uses INSERT ... ON CONFLICT to avoid triggering CASCADE DELETE on users
#[tauri::command]
#[specta::specta]
pub async fn storage_save_server(
db: State<'_, DatabaseWrapper>,
id: String,
@@ -154,6 +159,7 @@ pub async fn storage_save_server(
/// Get all saved servers
#[tauri::command]
#[specta::specta]
pub async fn storage_get_servers(db: State<'_, DatabaseWrapper>) -> Result<Vec<ServerInfo>, String> {
let db_service = {
let database = db.0.lock().map_err(|e| e.to_string())?;
@@ -179,6 +185,7 @@ pub async fn storage_get_servers(db: State<'_, DatabaseWrapper>) -> Result<Vec<S
/// Delete a server and all associated data
#[tauri::command]
#[specta::specta]
pub async fn storage_delete_server(
db: State<'_, DatabaseWrapper>,
creds: State<'_, CredentialStoreWrapper>,
@@ -221,6 +228,7 @@ pub async fn storage_delete_server(
/// Save a user account (token stored in secure storage, not database)
#[tauri::command]
#[specta::specta]
pub async fn storage_save_user(
db: State<'_, DatabaseWrapper>,
creds: State<'_, CredentialStoreWrapper>,
@@ -298,6 +306,7 @@ pub async fn storage_save_user(
/// Get users for a server
#[tauri::command]
#[specta::specta]
pub async fn storage_get_users(
db: State<'_, DatabaseWrapper>,
server_id: String,
@@ -330,6 +339,7 @@ pub async fn storage_get_users(
/// Set a user as active (and deactivate all other users globally)
#[tauri::command]
#[specta::specta]
pub async fn storage_set_active_user(
db: State<'_, DatabaseWrapper>,
user_id: String,
@@ -378,6 +388,7 @@ pub async fn storage_set_active_user(
/// Get the active user for a server
#[tauri::command]
#[specta::specta]
pub async fn storage_get_active_user(
db: State<'_, DatabaseWrapper>,
server_id: String,
@@ -408,6 +419,7 @@ pub async fn storage_get_active_user(
/// Get the active session (user + server + token) for session restoration
#[tauri::command]
#[specta::specta]
pub async fn storage_get_active_session(
db: State<'_, DatabaseWrapper>,
creds: State<'_, CredentialStoreWrapper>,
@@ -483,6 +495,7 @@ pub async fn storage_get_active_session(
/// Get user's access token from secure storage
#[tauri::command]
#[specta::specta]
pub fn storage_get_access_token(
creds: State<CredentialStoreWrapper>,
user_id: String,
@@ -497,6 +510,7 @@ pub fn storage_get_access_token(
/// Delete a user account and their token from secure storage
#[tauri::command]
#[specta::specta]
pub async fn storage_delete_user(
db: State<'_, DatabaseWrapper>,
creds: State<'_, CredentialStoreWrapper>,
@@ -529,7 +543,7 @@ pub async fn storage_delete_user(
}
/// Playback progress info
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlaybackProgress {
pub item_id: String,
@@ -542,6 +556,7 @@ pub struct PlaybackProgress {
/// Update playback progress in local database
/// This stores the progress locally for offline access and "continue watching"
#[tauri::command]
#[specta::specta]
pub async fn storage_update_playback_progress(
db: State<'_, DatabaseWrapper>,
user_id: String,
@@ -593,6 +608,7 @@ pub async fn storage_update_playback_progress(
/// Update playback progress with context in local database
/// This stores the progress along with playback context (container vs single)
#[tauri::command]
#[specta::specta]
pub async fn storage_update_playback_context(
db: State<'_, DatabaseWrapper>,
user_id: String,
@@ -642,6 +658,7 @@ pub async fn storage_update_playback_context(
/// Mark item as played in local database
#[tauri::command]
#[specta::specta]
pub async fn storage_mark_played(
db: State<'_, DatabaseWrapper>,
smart_cache: State<'_, SmartCacheWrapper>,
@@ -772,6 +789,7 @@ pub async fn storage_mark_played(
/// Get playback progress for an item
#[tauri::command]
#[specta::specta]
pub async fn storage_get_playback_progress(
db: State<'_, DatabaseWrapper>,
user_id: String,
@@ -804,6 +822,7 @@ pub async fn storage_get_playback_progress(
/// Mark pending sync as completed for an item
#[tauri::command]
#[specta::specta]
pub async fn storage_mark_synced(
db: State<'_, DatabaseWrapper>,
user_id: String,
@@ -828,6 +847,7 @@ pub async fn storage_mark_synced(
/// Toggle favorite status for an item in local database
/// This updates the is_favorite field and marks it for sync to Jellyfin
#[tauri::command]
#[specta::specta]
pub async fn storage_toggle_favorite(
db: State<'_, DatabaseWrapper>,
user_id: String,
@@ -873,7 +893,7 @@ pub async fn storage_toggle_favorite(
// =============================================================================
/// Cached library info returned to frontend
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CachedLibrary {
pub id: String,
@@ -884,7 +904,7 @@ pub struct CachedLibrary {
}
/// Cached media item returned to frontend
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CachedItem {
pub id: String,
@@ -915,6 +935,7 @@ pub struct CachedItem {
/// Get cached libraries for a server
#[tauri::command]
#[specta::specta]
pub async fn storage_get_libraries(
db: State<'_, DatabaseWrapper>,
server_id: String,
@@ -977,6 +998,7 @@ fn row_to_cached_item(row: &rusqlite::Row) -> rusqlite::Result<CachedItem> {
/// Get cached items with optional filtering
#[tauri::command]
#[specta::specta]
pub async fn storage_get_items(
db: State<'_, DatabaseWrapper>,
server_id: String,
@@ -1043,6 +1065,7 @@ pub async fn storage_get_items(
/// Get a single cached item by ID
#[tauri::command]
#[specta::specta]
pub async fn storage_get_item(
db: State<'_, DatabaseWrapper>,
item_id: String,
@@ -1070,6 +1093,7 @@ pub async fn storage_get_item(
/// Search cached items using FTS
#[tauri::command]
#[specta::specta]
pub async fn storage_search_items(
db: State<'_, DatabaseWrapper>,
server_id: String,
@@ -1111,6 +1135,7 @@ pub async fn storage_search_items(
/// Save a library to the cache
#[tauri::command]
#[specta::specta]
pub async fn storage_save_library(
db: State<'_, DatabaseWrapper>,
id: String,
@@ -1144,6 +1169,7 @@ pub async fn storage_save_library(
/// Save an item to the cache
#[tauri::command]
#[specta::specta]
pub async fn storage_save_item(
db: State<'_, DatabaseWrapper>,
item: CachedItem,
@@ -1207,6 +1233,7 @@ pub async fn storage_save_item(
/// Get count of pending sync operations for a user
#[tauri::command]
#[specta::specta]
pub async fn storage_get_pending_sync_count(
db: State<'_, DatabaseWrapper>,
user_id: String,
+6 -2
View File
@@ -9,7 +9,7 @@ use crate::storage::db_service::{DatabaseService, Query, QueryParam};
/// Cached person info returned to frontend
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CachedPerson {
pub id: String,
@@ -22,7 +22,7 @@ pub struct CachedPerson {
}
/// Item-person association for caching
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CachedItemPerson {
pub item_id: String,
@@ -35,6 +35,7 @@ pub struct CachedItemPerson {
/// Save a person to the cache
#[tauri::command]
#[specta::specta]
pub async fn storage_save_person(
db: State<'_, DatabaseWrapper>,
person: CachedPerson,
@@ -66,6 +67,7 @@ pub async fn storage_save_person(
/// Get a cached person by ID
#[tauri::command]
#[specta::specta]
pub async fn storage_get_person(
db: State<'_, DatabaseWrapper>,
person_id: String,
@@ -101,6 +103,7 @@ pub async fn storage_get_person(
/// Save item-person associations (batch)
#[tauri::command]
#[specta::specta]
pub async fn storage_save_item_people(
db: State<'_, DatabaseWrapper>,
associations: Vec<CachedItemPerson>,
@@ -139,6 +142,7 @@ pub async fn storage_save_item_people(
/// Get people for an item (with person details joined)
#[tauri::command]
#[specta::specta]
pub async fn storage_get_item_people(
db: State<'_, DatabaseWrapper>,
item_id: String,
@@ -9,7 +9,7 @@ use crate::storage::db_service::{DatabaseService, Query, QueryParam};
/// Audio track preference for a series
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SeriesAudioPreference {
pub series_id: String,
@@ -20,6 +20,7 @@ pub struct SeriesAudioPreference {
/// Save user's preferred audio track for a series
#[tauri::command]
#[specta::specta]
pub async fn storage_save_series_audio_preference(
db: State<'_, DatabaseWrapper>,
user_id: String,
@@ -59,6 +60,7 @@ pub async fn storage_save_series_audio_preference(
/// Get user's preferred audio track for a series
#[tauri::command]
#[specta::specta]
pub async fn storage_get_series_audio_preference(
db: State<'_, DatabaseWrapper>,
user_id: String,
+8 -1
View File
@@ -15,6 +15,7 @@ use crate::thumbnail::{ThumbnailCacheStats, ThumbnailWorker};
/// Get cached thumbnail path, returns None if not cached
/// Also updates last_accessed timestamp for LRU tracking
#[tauri::command]
#[specta::specta]
pub async fn thumbnail_get_cached(
db: State<'_, DatabaseWrapper>,
thumbnail_cache: State<'_, ThumbnailCacheWrapper>,
@@ -38,6 +39,7 @@ pub async fn thumbnail_get_cached(
/// Download and save a thumbnail to cache
/// Returns the local file path on success
#[tauri::command]
#[specta::specta]
pub async fn thumbnail_save(
db: State<'_, DatabaseWrapper>,
thumbnail_cache: State<'_, ThumbnailCacheWrapper>,
@@ -65,6 +67,7 @@ pub async fn thumbnail_save(
/// Get thumbnail cache statistics
#[tauri::command]
#[specta::specta]
pub async fn thumbnail_get_stats(
db: State<'_, DatabaseWrapper>,
thumbnail_cache: State<'_, ThumbnailCacheWrapper>,
@@ -87,6 +90,7 @@ pub async fn thumbnail_get_stats(
/// Set thumbnail cache storage limit in bytes
#[tauri::command]
#[specta::specta]
pub async fn thumbnail_set_limit(
db: State<'_, DatabaseWrapper>,
thumbnail_cache: State<'_, ThumbnailCacheWrapper>,
@@ -102,6 +106,7 @@ pub async fn thumbnail_set_limit(
/// Clear all cached thumbnails
#[tauri::command]
#[specta::specta]
pub async fn thumbnail_clear_cache(
db: State<'_, DatabaseWrapper>,
thumbnail_cache: State<'_, ThumbnailCacheWrapper>,
@@ -116,6 +121,7 @@ pub async fn thumbnail_clear_cache(
/// Delete cached thumbnails for a specific item
#[tauri::command]
#[specta::specta]
pub async fn thumbnail_delete_item(
db: State<'_, DatabaseWrapper>,
thumbnail_cache: State<'_, ThumbnailCacheWrapper>,
@@ -149,7 +155,7 @@ fn image_semaphore() -> &'static Semaphore {
}
/// Request to get an image URL (with caching)
#[derive(Debug, Deserialize)]
#[derive(specta::Type, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetImageRequest {
pub item_id: String,
@@ -165,6 +171,7 @@ pub struct GetImageRequest {
/// Get image as base64 data URL, caching if not already cached
/// This extends the thumbnail system to serve all images through Rust with automatic caching
#[tauri::command]
#[specta::specta]
pub async fn image_get_url(
repository_manager: State<'_, RepositoryManagerWrapper>,
thumbnail_cache: State<'_, ThumbnailCacheWrapper>,