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
+39 -13
View File
@@ -56,7 +56,7 @@ pub struct MediaSessionManagerWrapper(pub Mutex<MediaSessionManager>);
pub struct VideoSettingsWrapper(pub Mutex<VideoSettings>);
/// Response for player state queries
#[derive(Debug, Serialize)]
#[derive(specta::Type, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PlayerStatus {
pub state: PlayerState,
@@ -82,7 +82,7 @@ pub struct PlayerStatus {
/// Lightweight media item for merged playback state
/// Converts from both local MediaItem and remote NowPlayingItem
#[derive(Debug, Serialize, Clone)]
#[derive(specta::Type, Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct MergedMediaItem {
pub id: String,
@@ -134,7 +134,7 @@ impl From<&crate::jellyfin::client::NowPlayingItem> for MergedMediaItem {
}
/// Response for queue queries
#[derive(Debug, Serialize)]
#[derive(specta::Type, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct QueueStatus {
pub items: Vec<MediaItem>,
@@ -146,7 +146,7 @@ pub struct QueueStatus {
}
/// Backend type for video playback
#[derive(Debug, Serialize)]
#[derive(specta::Type, Debug, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum VideoBackend {
/// Native backend (ExoPlayer on Android, libmpv on Linux)
@@ -159,7 +159,7 @@ pub enum VideoBackend {
///
/// Simplified to video playback only. Audio playback uses player_play_tracks
/// to avoid Tauri Android serialization issues with complex objects.
#[derive(Debug, Deserialize)]
#[derive(specta::Type, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlayItemRequest {
pub id: String,
@@ -172,7 +172,7 @@ pub struct PlayItemRequest {
}
/// Queue context for remote transfer - what type of queue is this?
#[derive(Debug, Deserialize)]
#[derive(specta::Type, Debug, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum PlayQueueContext {
/// Playing from a specific album
@@ -194,7 +194,7 @@ pub enum PlayQueueContext {
}
/// Request to play a queue of items
#[derive(Debug, Deserialize)]
#[derive(specta::Type, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlayQueueRequest {
pub items: Vec<PlayItemRequest>,
@@ -207,7 +207,7 @@ pub struct PlayQueueRequest {
}
/// Request to play a track from an album (backend fetches all tracks)
#[derive(Debug, Deserialize)]
#[derive(specta::Type, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlayAlbumTrackRequest {
pub album_id: String,
@@ -217,7 +217,7 @@ pub struct PlayAlbumTrackRequest {
}
/// Request to play tracks by ID (backend fetches metadata)
#[derive(Debug, Deserialize)]
#[derive(specta::Type, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlayTracksRequest {
pub track_ids: Vec<String>,
@@ -227,7 +227,7 @@ pub struct PlayTracksRequest {
}
/// Context information for track playback
#[derive(Debug, Deserialize)]
#[derive(specta::Type, Debug, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum PlayTracksContext {
Playlist {
@@ -249,7 +249,7 @@ pub enum PlayTracksContext {
}
/// Response for video seek operations
#[derive(Debug, Serialize)]
#[derive(specta::Type, Debug, Serialize)]
#[serde(tag = "strategy", rename_all = "camelCase")]
pub enum VideoSeekResponse {
/// Use native seeking (HLS or direct stream)
@@ -267,7 +267,7 @@ pub enum VideoSeekResponse {
}
/// Response for audio track switching operations
#[derive(Debug, Serialize)]
#[derive(specta::Type, Debug, Serialize)]
#[serde(tag = "strategy", rename_all = "camelCase")]
pub enum AudioTrackSwitchResponse {
/// Native backend handled it (Android ExoPlayer)
@@ -384,6 +384,7 @@ pub(super) async fn check_for_local_download(
/// @req: UR-005 - Control media playback (play operation)
/// @req: DR-009 - Audio player UI
#[tauri::command]
#[specta::specta]
pub async fn player_play_item(
player: State<'_, PlayerStateWrapper>,
session: State<'_, MediaSessionManagerWrapper>,
@@ -435,6 +436,7 @@ pub async fn player_play_item(
/// @req: UR-015 - View and manage current audio queue
/// @req: DR-005 - Queue manager with shuffle, repeat, history
#[tauri::command]
#[specta::specta]
pub async fn player_play_queue(
player: State<'_, PlayerStateWrapper>,
session: State<'_, MediaSessionManagerWrapper>,
@@ -511,6 +513,7 @@ pub async fn player_play_queue(
}
#[tauri::command]
#[specta::specta]
pub async fn player_play(
player: State<'_, PlayerStateWrapper>,
playback_mode: State<'_, super::playback_mode::PlaybackModeManagerWrapper>,
@@ -538,6 +541,7 @@ pub async fn player_play(
}
#[tauri::command]
#[specta::specta]
pub async fn player_pause(
player: State<'_, PlayerStateWrapper>,
playback_mode: State<'_, super::playback_mode::PlaybackModeManagerWrapper>,
@@ -565,6 +569,7 @@ pub async fn player_pause(
}
#[tauri::command]
#[specta::specta]
pub async fn player_toggle(
player: State<'_, PlayerStateWrapper>,
playback_mode: State<'_, super::playback_mode::PlaybackModeManagerWrapper>,
@@ -592,6 +597,7 @@ pub async fn player_toggle(
}
#[tauri::command]
#[specta::specta]
pub async fn player_stop(
player: State<'_, PlayerStateWrapper>,
session: State<'_, MediaSessionManagerWrapper>,
@@ -651,6 +657,7 @@ pub async fn player_stop(
}
#[tauri::command]
#[specta::specta]
pub async fn player_next(
player: State<'_, PlayerStateWrapper>,
session: State<'_, MediaSessionManagerWrapper>,
@@ -703,6 +710,7 @@ pub async fn player_next(
}
#[tauri::command]
#[specta::specta]
pub async fn player_previous(
player: State<'_, PlayerStateWrapper>,
session: State<'_, MediaSessionManagerWrapper>,
@@ -753,6 +761,7 @@ pub async fn player_previous(
}
#[tauri::command]
#[specta::specta]
pub async fn player_seek(
player: State<'_, PlayerStateWrapper>,
playback_mode: State<'_, super::playback_mode::PlaybackModeManagerWrapper>,
@@ -792,6 +801,7 @@ pub async fn player_seek(
/// For native (non-HTML5) backends, this command handles the entire stream reload
/// internally. For HTML5 backends, it returns the new URL for the frontend to handle.
#[tauri::command]
#[specta::specta]
pub async fn player_seek_video(
player: State<'_, PlayerStateWrapper>,
repository_manager: State<'_, super::repository::RepositoryManagerWrapper>,
@@ -929,6 +939,7 @@ pub async fn player_seek_video(
/// Switch audio track - handles both HTML5 (stream reload) and native (direct switch)
/// Note: Frontend should handle saving series preferences after this command succeeds
#[tauri::command]
#[specta::specta]
pub async fn player_switch_audio_track(
player: State<'_, PlayerStateWrapper>,
repository_manager: State<'_, super::repository::RepositoryManagerWrapper>,
@@ -984,6 +995,7 @@ pub async fn player_switch_audio_track(
}
#[tauri::command]
#[specta::specta]
pub async fn player_set_audio_track(
player: State<'_, PlayerStateWrapper>,
stream_index: i32,
@@ -994,6 +1006,7 @@ pub async fn player_set_audio_track(
}
#[tauri::command]
#[specta::specta]
pub async fn player_set_subtitle_track(
player: State<'_, PlayerStateWrapper>,
stream_index: Option<i32>,
@@ -1004,6 +1017,7 @@ pub async fn player_set_subtitle_track(
}
#[tauri::command]
#[specta::specta]
pub async fn player_set_volume(
player: State<'_, PlayerStateWrapper>,
playback_mode: State<'_, super::playback_mode::PlaybackModeManagerWrapper>,
@@ -1034,6 +1048,7 @@ pub async fn player_set_volume(
}
#[tauri::command]
#[specta::specta]
pub async fn player_toggle_mute(
player: State<'_, PlayerStateWrapper>,
playback_mode: State<'_, super::playback_mode::PlaybackModeManagerWrapper>,
@@ -1062,6 +1077,7 @@ pub async fn player_toggle_mute(
}
#[tauri::command]
#[specta::specta]
pub async fn player_toggle_shuffle(
player: State<'_, PlayerStateWrapper>,
) -> Result<QueueStatus, String> {
@@ -1072,6 +1088,7 @@ pub async fn player_toggle_shuffle(
}
#[tauri::command]
#[specta::specta]
pub async fn player_cycle_repeat(player: State<'_, PlayerStateWrapper>) -> Result<QueueStatus, String> {
let controller = player.0.lock().await;
controller.cycle_repeat();
@@ -1080,6 +1097,7 @@ pub async fn player_cycle_repeat(player: State<'_, PlayerStateWrapper>) -> Resul
}
#[tauri::command]
#[specta::specta]
pub async fn player_get_status(
player: State<'_, PlayerStateWrapper>,
playback_mode: State<'_, super::playback_mode::PlaybackModeManagerWrapper>,
@@ -1165,6 +1183,7 @@ pub async fn player_get_status(
}
#[tauri::command]
#[specta::specta]
pub async fn player_get_queue(player: State<'_, PlayerStateWrapper>) -> Result<QueueStatus, String> {
let controller = player.0.lock().await;
Ok(get_queue_status(&controller))
@@ -1216,6 +1235,7 @@ pub(super) fn get_queue_status(controller: &PlayerController) -> QueueStatus {
/// Play a track from an album - backend fetches all album tracks and builds queue
#[tauri::command]
#[specta::specta]
pub async fn player_play_album_track(
player: State<'_, PlayerStateWrapper>,
session: State<'_, MediaSessionManagerWrapper>,
@@ -1383,6 +1403,7 @@ pub async fn player_play_album_track(
/// Play tracks by ID - backend fetches all metadata
#[tauri::command]
#[specta::specta]
pub async fn player_play_tracks(
player: State<'_, PlayerStateWrapper>,
session: State<'_, MediaSessionManagerWrapper>,
@@ -1521,7 +1542,7 @@ pub async fn player_play_tracks(
}
/// Response for preload operation
#[derive(Debug, Serialize)]
#[derive(specta::Type, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PreloadResult {
/// Number of tracks queued for preload
@@ -1535,6 +1556,7 @@ pub struct PreloadResult {
/// Preload upcoming tracks from the queue
/// This queues background downloads for the next N tracks that aren't already downloaded
#[tauri::command]
#[specta::specta]
pub async fn player_preload_upcoming(
player: State<'_, PlayerStateWrapper>,
db: State<'_, DatabaseWrapper>,
@@ -1671,6 +1693,7 @@ fn sanitize_filename(name: &str) -> String {
/// Update SmartCache configuration
#[tauri::command]
#[specta::specta]
pub async fn player_set_cache_config(
smart_cache: State<'_, SmartCacheWrapper>,
config: CacheConfig,
@@ -1682,6 +1705,7 @@ pub async fn player_set_cache_config(
/// Get current SmartCache configuration
#[tauri::command]
#[specta::specta]
pub async fn player_get_cache_config(
smart_cache: State<'_, SmartCacheWrapper>,
) -> Result<CacheConfig, String> {
@@ -1691,6 +1715,7 @@ pub async fn player_get_cache_config(
/// Configure Jellyfin API client for automatic playback reporting
#[tauri::command]
#[specta::specta]
pub async fn player_configure_jellyfin(
player: State<'_, PlayerStateWrapper>,
server_url: String,
@@ -1717,6 +1742,7 @@ pub async fn player_configure_jellyfin(
/// Disable Jellyfin automatic playback reporting
#[tauri::command]
#[specta::specta]
pub async fn player_disable_jellyfin(
player: State<'_, PlayerStateWrapper>,
) -> Result<(), String> {