use tauri_plugin_sql::{Migration, MigrationKind}; mod deezer_crypto; mod device_sync; mod metadata; mod tagger; // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ #[tauri::command] fn greet(name: &str) -> String { format!("Hello, {}! You've been greeted from Rust!", name) } /// Tag an audio file with metadata, cover art, and lyrics #[tauri::command] async fn tag_audio_file( path: String, metadata: tagger::TaggingMetadata, cover_data: Option>, embed_lyrics: bool, ) -> Result<(), String> { // Run tagging on a background thread to avoid blocking the UI tauri::async_runtime::spawn_blocking(move || { tagger::tag_audio_file(&path, &metadata, cover_data.as_deref(), embed_lyrics) }) .await .map_err(|e| format!("Tagging task failed: {}", e))? // Flatten the inner Result ?; Ok(()) } /// Read metadata from an audio file (MP3 or FLAC) #[tauri::command] fn read_audio_metadata(path: String) -> Result { metadata::read_audio_metadata(&path) } /// Decrypt Deezer track data (legacy - kept for backwards compatibility) #[tauri::command] async fn decrypt_deezer_track(data: Vec, track_id: String) -> Result, String> { // Run decryption on a background thread to avoid blocking the UI let result = tauri::async_runtime::spawn_blocking(move || { deezer_crypto::decrypt_track(&data, &track_id) }) .await .map_err(|e| format!("Decryption task failed: {}", e))?; Ok(result) } /// Download and decrypt a Deezer track, streaming directly to disk #[tauri::command] async fn download_and_decrypt_track( url: String, track_id: String, output_path: String, is_encrypted: bool, window: tauri::Window, ) -> Result<(), String> { use deezer_crypto::StreamingDecryptor; use tauri::Emitter; use tokio::fs::File; use tokio::io::AsyncWriteExt; // Build HTTP client let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(60)) .build() .map_err(|e| format!("Failed to create HTTP client: {}", e))?; // Start download let response = client .get(&url) .header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36") .send() .await .map_err(|e| format!("Download failed: {}", e))?; if !response.status().is_success() { return Err(format!("HTTP error: {}", response.status())); } let total_size = response.content_length().unwrap_or(0) as f64; // Open output file let mut file = File::create(&output_path) .await .map_err(|e| format!("Failed to create output file: {}", e))?; let mut downloaded_bytes = 0u64; let mut last_reported_percentage = 0u8; // Stream download with optional decryption if is_encrypted { let mut decryptor = StreamingDecryptor::new(&track_id); let mut stream = response.bytes_stream(); use futures_util::StreamExt; while let Some(chunk_result) = stream.next().await { let chunk = chunk_result.map_err(|e| format!("Download stream error: {}", e))?; downloaded_bytes += chunk.len() as u64; // Decrypt chunk and write to file let decrypted = decryptor.process(&chunk); if !decrypted.is_empty() { file.write_all(&decrypted) .await .map_err(|e| format!("Failed to write to file: {}", e))?; } // Emit progress every 5% if total_size > 0.0 { let percentage = ((downloaded_bytes as f64 / total_size) * 100.0) as u8; let rounded_percentage = (percentage / 5) * 5; if rounded_percentage > last_reported_percentage || percentage == 100 { last_reported_percentage = rounded_percentage; let _ = window.emit( "download-progress", serde_json::json!({ "downloaded": downloaded_bytes, "total": total_size as u64, "percentage": percentage }), ); } } } // Write any remaining buffered data let final_data = decryptor.finalize(); if !final_data.is_empty() { file.write_all(&final_data) .await .map_err(|e| format!("Failed to write final data: {}", e))?; } } else { // No encryption - just stream directly let mut stream = response.bytes_stream(); use futures_util::StreamExt; while let Some(chunk_result) = stream.next().await { let chunk = chunk_result.map_err(|e| format!("Download stream error: {}", e))?; downloaded_bytes += chunk.len() as u64; file.write_all(&chunk) .await .map_err(|e| format!("Failed to write to file: {}", e))?; // Emit progress every 5% if total_size > 0.0 { let percentage = ((downloaded_bytes as f64 / total_size) * 100.0) as u8; let rounded_percentage = (percentage / 5) * 5; if rounded_percentage > last_reported_percentage || percentage == 100 { last_reported_percentage = rounded_percentage; let _ = window.emit( "download-progress", serde_json::json!({ "downloaded": downloaded_bytes, "total": total_size as u64, "percentage": percentage }), ); } } } } // Ensure all data is flushed file.flush() .await .map_err(|e| format!("Failed to flush file: {}", e))?; Ok(()) } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let library_migrations = vec![Migration { version: 1, description: "create_library_tables", sql: " CREATE TABLE IF NOT EXISTS artists ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, path TEXT NOT NULL UNIQUE, album_count INTEGER DEFAULT 0, track_count INTEGER DEFAULT 0, primary_cover_path TEXT, last_scanned INTEGER, created_at INTEGER DEFAULT (strftime('%s', 'now')) ); CREATE TABLE IF NOT EXISTS albums ( id INTEGER PRIMARY KEY AUTOINCREMENT, artist_id INTEGER NOT NULL, artist_name TEXT NOT NULL, title TEXT NOT NULL, path TEXT NOT NULL UNIQUE, cover_path TEXT, track_count INTEGER DEFAULT 0, year INTEGER, last_scanned INTEGER, created_at INTEGER DEFAULT (strftime('%s', 'now')), FOREIGN KEY (artist_id) REFERENCES artists(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS tracks ( id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT NOT NULL UNIQUE, title TEXT NOT NULL, artist TEXT NOT NULL, album TEXT NOT NULL, duration INTEGER NOT NULL, format TEXT NOT NULL, has_lyrics INTEGER DEFAULT 0, last_scanned INTEGER, created_at INTEGER DEFAULT (strftime('%s', 'now')) ); CREATE INDEX IF NOT EXISTS idx_artists_name ON artists(name); CREATE INDEX IF NOT EXISTS idx_albums_artist_id ON albums(artist_id); CREATE INDEX IF NOT EXISTS idx_albums_year ON albums(year); CREATE INDEX IF NOT EXISTS idx_albums_artist_title ON albums(artist_name, title); CREATE INDEX IF NOT EXISTS idx_tracks_has_lyrics ON tracks(has_lyrics); CREATE INDEX IF NOT EXISTS idx_tracks_path ON tracks(path); ", kind: MigrationKind::Up, }]; let deezer_migrations = vec![Migration { version: 1, description: "create_deezer_cache_tables", sql: " CREATE TABLE IF NOT EXISTS deezer_playlists ( id TEXT PRIMARY KEY, title TEXT NOT NULL, nb_tracks INTEGER DEFAULT 0, creator_name TEXT, picture_small TEXT, picture_medium TEXT, cached_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS deezer_albums ( id TEXT PRIMARY KEY, title TEXT NOT NULL, artist_name TEXT NOT NULL, nb_tracks INTEGER DEFAULT 0, release_date TEXT, picture_small TEXT, picture_medium TEXT, cached_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS deezer_artists ( id TEXT PRIMARY KEY, name TEXT NOT NULL, picture_small TEXT, picture_medium TEXT, cached_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS deezer_tracks ( id TEXT PRIMARY KEY, title TEXT NOT NULL, artist_name TEXT NOT NULL, album_title TEXT, duration INTEGER DEFAULT 0, cached_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS deezer_playlist_tracks ( id INTEGER PRIMARY KEY AUTOINCREMENT, playlist_id TEXT NOT NULL, track_id TEXT NOT NULL, title TEXT NOT NULL, artist_name TEXT NOT NULL, album_title TEXT, album_picture TEXT, duration INTEGER DEFAULT 0, track_number INTEGER, cached_at INTEGER NOT NULL, UNIQUE(playlist_id, track_id) ); CREATE INDEX IF NOT EXISTS idx_deezer_playlists_title ON deezer_playlists(title); CREATE INDEX IF NOT EXISTS idx_deezer_albums_artist ON deezer_albums(artist_name); CREATE INDEX IF NOT EXISTS idx_deezer_artists_name ON deezer_artists(name); CREATE INDEX IF NOT EXISTS idx_deezer_tracks_title ON deezer_tracks(title); CREATE INDEX IF NOT EXISTS idx_deezer_playlist_tracks_playlist ON deezer_playlist_tracks(playlist_id); CREATE INDEX IF NOT EXISTS idx_deezer_playlist_tracks_track ON deezer_playlist_tracks(track_id); ", kind: MigrationKind::Up, }]; tauri::Builder::default() .plugin(tauri_plugin_oauth::init()) .plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_process::init()) .plugin( tauri_plugin_sql::Builder::new() .add_migrations("sqlite:library.db", library_migrations) .add_migrations("sqlite:deezer.db", deezer_migrations) .build(), ) .plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_store::Builder::new().build()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_fs::init()) .invoke_handler(tauri::generate_handler![ greet, tag_audio_file, read_audio_metadata, decrypt_deezer_track, download_and_decrypt_track, device_sync::index_and_compare, device_sync::sync_to_device ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }