First working POC
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
use std::sync::Arc;
|
||||
use tauri::State;
|
||||
|
||||
use crate::auth::{AuthManager, SessionVerifier, ServerInfo, AuthResult, Session};
|
||||
|
||||
/// Wrapper for AuthManager to manage in Tauri state
|
||||
pub struct AuthManagerWrapper(pub Arc<AuthManager>);
|
||||
|
||||
/// Wrapper for SessionVerifier to manage in Tauri state
|
||||
pub struct SessionVerifierWrapper(pub Arc<tokio::sync::Mutex<Option<SessionVerifier>>>);
|
||||
|
||||
/// Initialize the auth manager (call on app startup)
|
||||
/// Restores session from storage if available
|
||||
#[tauri::command]
|
||||
pub async fn auth_initialize(
|
||||
auth_manager: State<'_, AuthManagerWrapper>,
|
||||
database: State<'_, crate::commands::DatabaseWrapper>,
|
||||
credentials: State<'_, crate::commands::CredentialStoreWrapper>,
|
||||
) -> Result<Option<Session>, String> {
|
||||
// First check if we already have a session in memory
|
||||
if let Some(session) = auth_manager.0.get_session().await {
|
||||
return Ok(Some(session));
|
||||
}
|
||||
|
||||
// Try to restore session from storage
|
||||
log::info!("[AuthManager] Restoring session from storage...");
|
||||
|
||||
// Use the existing storage_get_active_session function
|
||||
let active_session = match crate::commands::storage::storage_get_active_session(database, credentials).await {
|
||||
Ok(Some(session)) => session,
|
||||
Ok(None) => {
|
||||
log::info!("[AuthManager] No active session in storage");
|
||||
return Ok(None);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("[AuthManager] Failed to get active session: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
// Create session object from active session with normalized URL
|
||||
let normalized_url = crate::auth::AuthManager::normalize_url(&active_session.server_url);
|
||||
|
||||
let session = Session {
|
||||
user_id: active_session.user_id,
|
||||
username: active_session.username,
|
||||
server_id: active_session.server_id,
|
||||
server_url: normalized_url,
|
||||
server_name: active_session.server_name,
|
||||
access_token: active_session.access_token,
|
||||
verified: false, // Will be verified in background
|
||||
needs_reauth: false,
|
||||
};
|
||||
|
||||
// Store in AuthManager
|
||||
auth_manager.0.set_session(Some(session.clone())).await;
|
||||
|
||||
log::info!("[AuthManager] Session restored for user: {} with normalized URL: {}", session.username, session.server_url);
|
||||
Ok(Some(session))
|
||||
}
|
||||
|
||||
/// Connect to a Jellyfin server and get server info
|
||||
#[tauri::command]
|
||||
pub async fn auth_connect_to_server(
|
||||
server_url: String,
|
||||
auth_manager: State<'_, AuthManagerWrapper>,
|
||||
) -> Result<ServerInfo, String> {
|
||||
auth_manager.0.connect_to_server(&server_url).await
|
||||
}
|
||||
|
||||
/// Login with username and password
|
||||
#[tauri::command]
|
||||
pub async fn auth_login(
|
||||
server_url: String,
|
||||
username: String,
|
||||
password: String,
|
||||
device_id: String,
|
||||
auth_manager: State<'_, AuthManagerWrapper>,
|
||||
) -> Result<AuthResult, String> {
|
||||
let result = auth_manager.0.login(&server_url, &username, &password, &device_id).await?;
|
||||
|
||||
// Create session from auth result with normalized URL
|
||||
let normalized_url = crate::auth::AuthManager::normalize_url(&server_url);
|
||||
|
||||
let session = Session {
|
||||
user_id: result.user.id.clone(),
|
||||
username: result.user.name.clone(),
|
||||
server_id: result.server_id.clone(),
|
||||
server_url: normalized_url,
|
||||
server_name: String::new(), // Will be set by frontend
|
||||
access_token: result.access_token.clone(),
|
||||
verified: true,
|
||||
needs_reauth: false,
|
||||
};
|
||||
|
||||
auth_manager.0.set_session(Some(session)).await;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Verify current session
|
||||
#[tauri::command]
|
||||
pub async fn auth_verify_session(
|
||||
server_url: String,
|
||||
user_id: String,
|
||||
access_token: String,
|
||||
device_id: String,
|
||||
auth_manager: State<'_, AuthManagerWrapper>,
|
||||
) -> Result<bool, String> {
|
||||
match auth_manager.0.verify_session(&server_url, &user_id, &access_token, &device_id).await {
|
||||
Ok(_) => Ok(true),
|
||||
Err(e) => {
|
||||
log::warn!("[AuthCommands] Session verification failed: {}", e);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Logout (clear session and call Jellyfin logout endpoint)
|
||||
#[tauri::command]
|
||||
pub async fn auth_logout(
|
||||
server_url: String,
|
||||
access_token: String,
|
||||
device_id: String,
|
||||
auth_manager: State<'_, AuthManagerWrapper>,
|
||||
session_verifier: State<'_, SessionVerifierWrapper>,
|
||||
) -> Result<(), String> {
|
||||
// Stop session verification
|
||||
let mut verifier_guard = session_verifier.0.lock().await;
|
||||
if let Some(verifier) = verifier_guard.take() {
|
||||
verifier.stop();
|
||||
}
|
||||
drop(verifier_guard);
|
||||
|
||||
// Call Jellyfin logout endpoint
|
||||
auth_manager.0.logout(&server_url, &access_token, &device_id).await?;
|
||||
|
||||
// Clear session
|
||||
auth_manager.0.set_session(None).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get current session
|
||||
#[tauri::command]
|
||||
pub async fn auth_get_session(
|
||||
auth_manager: State<'_, AuthManagerWrapper>,
|
||||
) -> Result<Option<Session>, String> {
|
||||
Ok(auth_manager.0.get_session().await)
|
||||
}
|
||||
|
||||
/// Set current session (for restoration from storage)
|
||||
#[tauri::command]
|
||||
pub async fn auth_set_session(
|
||||
session: Option<Session>,
|
||||
auth_manager: State<'_, AuthManagerWrapper>,
|
||||
) -> Result<(), String> {
|
||||
// Normalize the server URL if session is provided
|
||||
let normalized_session = session.map(|mut s| {
|
||||
s.server_url = crate::auth::AuthManager::normalize_url(&s.server_url);
|
||||
s
|
||||
});
|
||||
|
||||
auth_manager.0.set_session(normalized_session).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start background session verification
|
||||
#[tauri::command]
|
||||
pub async fn auth_start_verification(
|
||||
device_id: String,
|
||||
app_handle: tauri::AppHandle,
|
||||
auth_manager: State<'_, AuthManagerWrapper>,
|
||||
session_verifier: State<'_, SessionVerifierWrapper>,
|
||||
) -> Result<(), String> {
|
||||
let mut verifier_guard = session_verifier.0.lock().await;
|
||||
|
||||
// Stop existing verifier if any
|
||||
if let Some(verifier) = verifier_guard.take() {
|
||||
verifier.stop();
|
||||
}
|
||||
|
||||
// Get AuthManager Arc
|
||||
let manager = auth_manager.0.clone();
|
||||
|
||||
// Create new verifier
|
||||
let mut verifier = SessionVerifier::new(manager, device_id);
|
||||
verifier.set_app_handle(app_handle);
|
||||
verifier.start().await;
|
||||
|
||||
*verifier_guard = Some(verifier);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop background session verification
|
||||
#[tauri::command]
|
||||
pub async fn auth_stop_verification(
|
||||
session_verifier: State<'_, SessionVerifierWrapper>,
|
||||
) -> Result<(), String> {
|
||||
let mut verifier_guard = session_verifier.0.lock().await;
|
||||
|
||||
if let Some(verifier) = verifier_guard.take() {
|
||||
verifier.stop();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Re-authenticate with password (when session expired)
|
||||
#[tauri::command]
|
||||
pub async fn auth_reauthenticate(
|
||||
password: String,
|
||||
device_id: String,
|
||||
auth_manager: State<'_, AuthManagerWrapper>,
|
||||
) -> Result<AuthResult, String> {
|
||||
// Get current session to extract server_url and username
|
||||
let session = auth_manager.0.get_session().await
|
||||
.ok_or_else(|| "No active session to re-authenticate".to_string())?;
|
||||
|
||||
// Re-login with stored credentials
|
||||
let result = auth_manager.0.login(&session.server_url, &session.username, &password, &device_id).await?;
|
||||
|
||||
// Update session with new token
|
||||
let updated_session = Session {
|
||||
user_id: result.user.id.clone(),
|
||||
username: result.user.name.clone(),
|
||||
server_id: result.server_id.clone(),
|
||||
server_url: session.server_url,
|
||||
server_name: session.server_name,
|
||||
access_token: result.access_token.clone(),
|
||||
verified: true,
|
||||
needs_reauth: false,
|
||||
};
|
||||
|
||||
auth_manager.0.set_session(Some(updated_session)).await;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
use std::sync::Arc;
|
||||
use tauri::State;
|
||||
use crate::connectivity::{ConnectivityMonitor, ConnectivityStatus};
|
||||
|
||||
/// Wrapper for ConnectivityMonitor managed state
|
||||
pub struct ConnectivityMonitorWrapper(pub Arc<tokio::sync::Mutex<ConnectivityMonitor>>);
|
||||
|
||||
/// Check if the server is currently reachable
|
||||
#[tauri::command]
|
||||
pub async fn connectivity_check_server(
|
||||
state: State<'_, ConnectivityMonitorWrapper>,
|
||||
) -> Result<bool, String> {
|
||||
let monitor = state.0.lock().await;
|
||||
Ok(monitor.check_reachability().await)
|
||||
}
|
||||
|
||||
/// Set the server URL and trigger an immediate check
|
||||
#[tauri::command]
|
||||
pub async fn connectivity_set_server_url(
|
||||
url: String,
|
||||
state: State<'_, ConnectivityMonitorWrapper>,
|
||||
) -> Result<(), String> {
|
||||
let monitor = state.0.lock().await;
|
||||
monitor.set_server_url(url).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the current connectivity status
|
||||
#[tauri::command]
|
||||
pub async fn connectivity_get_status(
|
||||
state: State<'_, ConnectivityMonitorWrapper>,
|
||||
) -> Result<ConnectivityStatus, String> {
|
||||
let monitor = state.0.lock().await;
|
||||
Ok(monitor.get_status().await)
|
||||
}
|
||||
|
||||
/// Start monitoring connectivity with adaptive polling
|
||||
#[tauri::command]
|
||||
pub async fn connectivity_start_monitoring(
|
||||
state: State<'_, ConnectivityMonitorWrapper>,
|
||||
) -> Result<(), String> {
|
||||
let monitor = state.0.lock().await;
|
||||
monitor.start_monitoring().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop monitoring connectivity
|
||||
#[tauri::command]
|
||||
pub async fn connectivity_stop_monitoring(
|
||||
state: State<'_, ConnectivityMonitorWrapper>,
|
||||
) -> Result<(), String> {
|
||||
let monitor = state.0.lock().await;
|
||||
monitor.stop_monitoring();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark the server as reachable (called after successful API calls)
|
||||
#[tauri::command]
|
||||
pub async fn connectivity_mark_reachable(
|
||||
state: State<'_, ConnectivityMonitorWrapper>,
|
||||
) -> Result<(), String> {
|
||||
let monitor = state.0.lock().await;
|
||||
monitor.mark_reachable().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark the server as unreachable (called after failed API calls)
|
||||
#[tauri::command]
|
||||
pub async fn connectivity_mark_unreachable(
|
||||
error: Option<String>,
|
||||
state: State<'_, ConnectivityMonitorWrapper>,
|
||||
) -> Result<(), String> {
|
||||
let monitor = state.0.lock().await;
|
||||
monitor.mark_unreachable(error).await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
//! Tauri commands for unit conversions and formatting
|
||||
//!
|
||||
//! These commands expose conversion utilities to the frontend,
|
||||
//! allowing centralized conversion logic in Rust.
|
||||
|
||||
use crate::utils::conversions::{
|
||||
format_time, format_time_long, calculate_progress,
|
||||
ticks_to_seconds, percent_to_volume,
|
||||
};
|
||||
|
||||
/// Format time in seconds to MM:SS display string
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `seconds` - Time in seconds
|
||||
///
|
||||
/// # Returns
|
||||
/// Formatted string like "3:45" or "12:09"
|
||||
#[tauri::command]
|
||||
pub fn format_time_seconds(seconds: f64) -> String {
|
||||
format_time(seconds)
|
||||
}
|
||||
|
||||
/// Format time in seconds to HH:MM:SS or MM:SS display string
|
||||
///
|
||||
/// Automatically chooses format based on duration:
|
||||
/// - Less than 1 hour: Returns MM:SS format
|
||||
/// - 1 hour or more: Returns HH:MM:SS format
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `seconds` - Time in seconds
|
||||
///
|
||||
/// # Returns
|
||||
/// Formatted string like "1:23:45" or "3:45"
|
||||
#[tauri::command]
|
||||
pub fn format_time_seconds_long(seconds: f64) -> String {
|
||||
format_time_long(seconds)
|
||||
}
|
||||
|
||||
/// Convert Jellyfin ticks to seconds
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `ticks` - Time in Jellyfin ticks (10,000,000 ticks = 1 second)
|
||||
///
|
||||
/// # Returns
|
||||
/// Time in seconds
|
||||
#[tauri::command]
|
||||
pub fn convert_ticks_to_seconds(ticks: i64) -> f64 {
|
||||
ticks_to_seconds(ticks)
|
||||
}
|
||||
|
||||
/// Calculate progress percentage from position and duration
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `position` - Current position in seconds
|
||||
/// * `duration` - Total duration in seconds
|
||||
///
|
||||
/// # Returns
|
||||
/// Progress as percentage (0.0 to 100.0)
|
||||
#[tauri::command]
|
||||
pub fn calc_progress(position: f64, duration: f64) -> f64 {
|
||||
calculate_progress(position, duration)
|
||||
}
|
||||
|
||||
/// Convert percentage volume (0-100) to normalized (0.0-1.0)
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `percent` - Volume as percentage (0 to 100)
|
||||
///
|
||||
/// # Returns
|
||||
/// Normalized volume (0.0 to 1.0)
|
||||
#[tauri::command]
|
||||
pub fn convert_percent_to_volume(percent: f64) -> f64 {
|
||||
percent_to_volume(percent)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
pub mod auth;
|
||||
pub mod connectivity;
|
||||
pub mod conversions;
|
||||
pub mod download;
|
||||
pub mod offline;
|
||||
pub mod playback_mode;
|
||||
pub mod playback_reporting;
|
||||
pub mod player;
|
||||
pub mod repository;
|
||||
pub mod sessions;
|
||||
pub mod storage;
|
||||
pub mod sync;
|
||||
|
||||
pub use auth::*;
|
||||
pub use connectivity::*;
|
||||
pub use conversions::*;
|
||||
pub use download::*;
|
||||
pub use offline::*;
|
||||
pub use playback_mode::*;
|
||||
#[allow(unused_imports)] // Used when playback_reporting is fully integrated
|
||||
pub use playback_reporting::*;
|
||||
pub use player::*;
|
||||
pub use repository::{*, RepositoryManager, RepositoryManagerWrapper};
|
||||
pub use sessions::*;
|
||||
pub use storage::*;
|
||||
pub use sync::*;
|
||||
@@ -0,0 +1,154 @@
|
||||
//! Tauri commands for offline data access
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tauri::State;
|
||||
|
||||
use super::DatabaseWrapper;
|
||||
use crate::storage::db_service::{DatabaseService, Query, QueryParam};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OfflineItem {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub item_type: String,
|
||||
pub album_id: Option<String>,
|
||||
pub album_name: Option<String>,
|
||||
pub artists: Option<String>,
|
||||
pub runtime_ticks: Option<i64>,
|
||||
pub primary_image_tag: Option<String>,
|
||||
}
|
||||
|
||||
/// Check if an item is available offline
|
||||
#[tauri::command]
|
||||
pub async fn offline_is_available(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
item_id: String,
|
||||
) -> Result<bool, String> {
|
||||
let db_service = {
|
||||
let database = db.0.lock().map_err(|e| e.to_string())?;
|
||||
Arc::new(database.service())
|
||||
};
|
||||
|
||||
let query = Query::with_params(
|
||||
"SELECT COUNT(*) FROM downloads WHERE item_id = ? AND status = 'completed'",
|
||||
vec![QueryParam::String(item_id)],
|
||||
);
|
||||
|
||||
let count: i64 = db_service
|
||||
.query_one(query, |row| row.get(0))
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(count > 0)
|
||||
}
|
||||
|
||||
/// Get all offline items for a user
|
||||
#[tauri::command]
|
||||
pub async fn offline_get_items(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
user_id: String,
|
||||
) -> Result<Vec<OfflineItem>, String> {
|
||||
let db_service = {
|
||||
let database = db.0.lock().map_err(|e| e.to_string())?;
|
||||
Arc::new(database.service())
|
||||
};
|
||||
|
||||
let query = Query::with_params(
|
||||
"SELECT i.id, i.name, i.item_type, i.album_id, i.album_name, i.artists,
|
||||
i.runtime_ticks, i.primary_image_tag
|
||||
FROM items i
|
||||
INNER JOIN downloads d ON i.id = d.item_id
|
||||
WHERE d.user_id = ? AND d.status = 'completed'
|
||||
ORDER BY d.completed_at DESC",
|
||||
vec![QueryParam::String(user_id)],
|
||||
);
|
||||
|
||||
db_service
|
||||
.query_many(query, |row| {
|
||||
Ok(OfflineItem {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
item_type: row.get(2)?,
|
||||
album_id: row.get(3)?,
|
||||
album_name: row.get(4)?,
|
||||
artists: row.get(5)?,
|
||||
runtime_ticks: row.get(6)?,
|
||||
primary_image_tag: row.get(7)?,
|
||||
})
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Search offline items
|
||||
#[tauri::command]
|
||||
pub async fn offline_search(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
user_id: String,
|
||||
query: String,
|
||||
) -> Result<Vec<OfflineItem>, String> {
|
||||
let db_service = {
|
||||
let database = db.0.lock().map_err(|e| e.to_string())?;
|
||||
Arc::new(database.service())
|
||||
};
|
||||
|
||||
let search_query = format!("%{}%", query.to_lowercase());
|
||||
|
||||
let db_query = Query::with_params(
|
||||
"SELECT i.id, i.name, i.item_type, i.album_id, i.album_name, i.artists,
|
||||
i.runtime_ticks, i.primary_image_tag
|
||||
FROM items i
|
||||
INNER JOIN downloads d ON i.id = d.item_id
|
||||
WHERE d.user_id = ? AND d.status = 'completed'
|
||||
AND (LOWER(i.name) LIKE ? OR LOWER(i.artists) LIKE ? OR LOWER(i.album_name) LIKE ?)
|
||||
ORDER BY i.name
|
||||
LIMIT 50",
|
||||
vec![
|
||||
QueryParam::String(user_id),
|
||||
QueryParam::String(search_query.clone()),
|
||||
QueryParam::String(search_query.clone()),
|
||||
QueryParam::String(search_query),
|
||||
],
|
||||
);
|
||||
|
||||
db_service
|
||||
.query_many(db_query, |row| {
|
||||
Ok(OfflineItem {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
item_type: row.get(2)?,
|
||||
album_id: row.get(3)?,
|
||||
album_name: row.get(4)?,
|
||||
artists: row.get(5)?,
|
||||
runtime_ticks: row.get(6)?,
|
||||
primary_image_tag: row.get(7)?,
|
||||
})
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_offline_item_serialization() {
|
||||
let item = OfflineItem {
|
||||
id: "123".to_string(),
|
||||
name: "Test Song".to_string(),
|
||||
item_type: "Audio".to_string(),
|
||||
album_id: Some("album1".to_string()),
|
||||
album_name: Some("Test Album".to_string()),
|
||||
artists: Some("Artist 1".to_string()),
|
||||
runtime_ticks: Some(180000000),
|
||||
primary_image_tag: Some("tag123".to_string()),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&item).unwrap();
|
||||
assert!(json.contains("\"itemType\":\"Audio\""));
|
||||
assert!(json.contains("\"albumName\":\"Test Album\""));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
use std::sync::Arc;
|
||||
use tauri::State;
|
||||
|
||||
use crate::playback_mode::{PlaybackMode, PlaybackModeManager};
|
||||
|
||||
/// Wrapper for PlaybackModeManager to manage in Tauri state
|
||||
pub struct PlaybackModeManagerWrapper(pub Arc<PlaybackModeManager>);
|
||||
|
||||
/// Get the current playback mode
|
||||
#[tauri::command]
|
||||
pub fn playback_mode_get_current(
|
||||
manager: State<'_, PlaybackModeManagerWrapper>,
|
||||
) -> Result<PlaybackMode, String> {
|
||||
Ok(manager.0.get_mode())
|
||||
}
|
||||
|
||||
/// Set the playback mode (internal/testing use)
|
||||
#[tauri::command]
|
||||
pub fn playback_mode_set(
|
||||
manager: State<'_, PlaybackModeManagerWrapper>,
|
||||
mode: PlaybackMode,
|
||||
) -> Result<(), String> {
|
||||
manager.0.set_mode(mode);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if currently transferring between playback modes
|
||||
#[tauri::command]
|
||||
pub fn playback_mode_is_transferring(
|
||||
manager: State<'_, PlaybackModeManagerWrapper>,
|
||||
) -> Result<bool, String> {
|
||||
Ok(manager.0.is_transferring())
|
||||
}
|
||||
|
||||
/// Transfer playback from local device to a remote Jellyfin session
|
||||
#[tauri::command]
|
||||
pub async fn playback_mode_transfer_to_remote(
|
||||
manager: State<'_, PlaybackModeManagerWrapper>,
|
||||
session_id: String,
|
||||
) -> Result<(), String> {
|
||||
log::info!(
|
||||
"[PlaybackModeCommands] Transferring to remote session: {}",
|
||||
session_id
|
||||
);
|
||||
manager.0.transfer_to_remote(session_id).await
|
||||
}
|
||||
|
||||
/// Transfer playback from remote session back to local device
|
||||
///
|
||||
/// Parameters:
|
||||
/// - current_item_id: The Jellyfin item ID currently playing on remote
|
||||
/// - position_ticks: Current playback position in ticks (10,000 ticks = 1ms)
|
||||
#[tauri::command]
|
||||
pub async fn playback_mode_transfer_to_local(
|
||||
manager: State<'_, PlaybackModeManagerWrapper>,
|
||||
current_item_id: String,
|
||||
position_ticks: i64,
|
||||
) -> Result<(), String> {
|
||||
log::info!(
|
||||
"[PlaybackModeCommands] Transferring to local: item_id={}, position={}",
|
||||
current_item_id,
|
||||
position_ticks
|
||||
);
|
||||
manager
|
||||
.0
|
||||
.transfer_to_local(current_item_id, position_ticks)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get remote session status (for polling position/duration)
|
||||
#[tauri::command]
|
||||
pub async fn playback_mode_get_remote_status(
|
||||
manager: State<'_, PlaybackModeManagerWrapper>,
|
||||
player: State<'_, crate::commands::PlayerStateWrapper>,
|
||||
) -> Result<RemoteSessionStatus, String> {
|
||||
let mode = manager.0.get_mode();
|
||||
|
||||
if let crate::playback_mode::PlaybackMode::Remote { session_id } = mode {
|
||||
// Get Jellyfin client from player controller - clone before await
|
||||
let client = {
|
||||
let controller = player.0.lock().await;
|
||||
let client_arc = controller.jellyfin_client();
|
||||
let client_opt = client_arc.lock().map_err(|e| e.to_string())?;
|
||||
client_opt.as_ref().ok_or("Jellyfin client not configured")?.clone()
|
||||
};
|
||||
|
||||
// Get session info
|
||||
match client.get_session(&session_id).await {
|
||||
Ok(Some(session)) => {
|
||||
let position_ticks = session.play_state.as_ref()
|
||||
.and_then(|ps| ps.position_ticks)
|
||||
.unwrap_or(0);
|
||||
|
||||
let duration_ticks = session.now_playing_item.as_ref()
|
||||
.and_then(|item| item.run_time_ticks)
|
||||
.unwrap_or(0);
|
||||
|
||||
let is_paused = session.play_state.as_ref()
|
||||
.and_then(|ps| ps.is_paused)
|
||||
.unwrap_or(true);
|
||||
|
||||
Ok(RemoteSessionStatus {
|
||||
position: position_ticks as f64 / 10_000_000.0,
|
||||
duration: if duration_ticks > 0 {
|
||||
Some(duration_ticks as f64 / 10_000_000.0)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
is_playing: !is_paused,
|
||||
now_playing_item: session.now_playing_item.clone(),
|
||||
})
|
||||
}
|
||||
Ok(None) => Err("Remote session not found".to_string()),
|
||||
Err(e) => Err(format!("Failed to get session status: {}", e)),
|
||||
}
|
||||
} else {
|
||||
Err("Not in remote playback mode".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Remote session status for UI updates
|
||||
#[derive(serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RemoteSessionStatus {
|
||||
pub position: f64,
|
||||
pub duration: Option<f64>,
|
||||
pub is_playing: bool,
|
||||
pub now_playing_item: Option<crate::jellyfin::NowPlayingItem>,
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
//! Tauri commands for playback reporting operations
|
||||
//!
|
||||
//! These commands provide frontend access to the Rust playback reporting system,
|
||||
//! replacing the TypeScript implementation with native Rust reporting.
|
||||
//!
|
||||
//! Commands are registered but not yet called from the frontend.
|
||||
//! Dead code warnings are suppressed until frontend migration is complete.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::sync::Arc;
|
||||
use tauri::State;
|
||||
use tokio::sync::Mutex as TokioMutex;
|
||||
|
||||
use crate::commands::connectivity::ConnectivityMonitorWrapper;
|
||||
use crate::commands::storage::DatabaseWrapper;
|
||||
use crate::jellyfin::client::JellyfinClient;
|
||||
use crate::jellyfin::JellyfinConfig;
|
||||
use crate::playback_reporting::{PlaybackReporter, PlaybackOperation, PlaybackContext};
|
||||
use crate::utils::conversions::seconds_to_ticks;
|
||||
|
||||
/// Tauri state wrapper for PlaybackReporter
|
||||
pub struct PlaybackReporterWrapper(pub Arc<TokioMutex<Option<PlaybackReporter>>>);
|
||||
|
||||
/// Initialize playback reporter (called after login)
|
||||
#[tauri::command]
|
||||
pub async fn playback_reporter_init(
|
||||
reporter_wrapper: State<'_, PlaybackReporterWrapper>,
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
server_url: String,
|
||||
user_id: String,
|
||||
access_token: String,
|
||||
device_id: String,
|
||||
) -> Result<(), String> {
|
||||
log::info!("[PlaybackReporter] Initializing for user: {}", user_id);
|
||||
|
||||
// Get database service
|
||||
let db_service = {
|
||||
let database = db.0.lock().map_err(|e| e.to_string())?;
|
||||
Arc::new(database.service())
|
||||
};
|
||||
|
||||
// Create JellyfinClient
|
||||
let jellyfin_config = JellyfinConfig {
|
||||
server_url,
|
||||
access_token,
|
||||
device_id,
|
||||
};
|
||||
|
||||
let jellyfin_client = JellyfinClient::new(jellyfin_config)
|
||||
.map_err(|e| format!("Failed to create JellyfinClient: {}", e))?;
|
||||
|
||||
// Create PlaybackReporter
|
||||
let reporter = PlaybackReporter::new(
|
||||
db_service,
|
||||
Arc::new(TokioMutex::new(Some(jellyfin_client))),
|
||||
user_id.clone(),
|
||||
);
|
||||
|
||||
// Store in wrapper
|
||||
*reporter_wrapper.0.lock().await = Some(reporter);
|
||||
|
||||
log::info!("[PlaybackReporter] Initialized successfully for user: {}", user_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Destroy playback reporter (called on logout)
|
||||
#[tauri::command]
|
||||
pub async fn playback_reporter_destroy(
|
||||
reporter_wrapper: State<'_, PlaybackReporterWrapper>,
|
||||
) -> Result<(), String> {
|
||||
log::info!("[PlaybackReporter] Destroying reporter");
|
||||
*reporter_wrapper.0.lock().await = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Report playback start
|
||||
#[tauri::command]
|
||||
pub async fn playback_report_start(
|
||||
reporter: State<'_, PlaybackReporterWrapper>,
|
||||
connectivity: State<'_, ConnectivityMonitorWrapper>,
|
||||
item_id: String,
|
||||
position_seconds: f64,
|
||||
context_type: Option<String>,
|
||||
context_id: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
let reporter_guard = reporter.0.lock().await;
|
||||
let reporter_instance = reporter_guard
|
||||
.as_ref()
|
||||
.ok_or("PlaybackReporter not initialized")?;
|
||||
|
||||
let position_ticks = seconds_to_ticks(position_seconds);
|
||||
let context = context_type.map(|ct| PlaybackContext {
|
||||
context_type: ct,
|
||||
context_id,
|
||||
});
|
||||
|
||||
let operation = PlaybackOperation::Start {
|
||||
item_id,
|
||||
position_ticks,
|
||||
context,
|
||||
};
|
||||
|
||||
let monitor = connectivity.0.lock().await;
|
||||
let is_online = monitor.get_status().await.is_server_reachable;
|
||||
drop(monitor);
|
||||
|
||||
reporter_instance.report(operation, is_online).await
|
||||
}
|
||||
|
||||
/// Report playback progress
|
||||
#[tauri::command]
|
||||
pub async fn playback_report_progress(
|
||||
reporter: State<'_, PlaybackReporterWrapper>,
|
||||
connectivity: State<'_, ConnectivityMonitorWrapper>,
|
||||
item_id: String,
|
||||
position_seconds: f64,
|
||||
is_paused: bool,
|
||||
) -> Result<(), String> {
|
||||
let reporter_guard = reporter.0.lock().await;
|
||||
let reporter_instance = reporter_guard
|
||||
.as_ref()
|
||||
.ok_or("PlaybackReporter not initialized")?;
|
||||
|
||||
let position_ticks = seconds_to_ticks(position_seconds);
|
||||
let operation = PlaybackOperation::Progress {
|
||||
item_id,
|
||||
position_ticks,
|
||||
is_paused,
|
||||
};
|
||||
|
||||
let monitor = connectivity.0.lock().await;
|
||||
let is_online = monitor.get_status().await.is_server_reachable;
|
||||
drop(monitor);
|
||||
|
||||
reporter_instance.report(operation, is_online).await
|
||||
}
|
||||
|
||||
/// Report playback stopped
|
||||
#[tauri::command]
|
||||
pub async fn playback_report_stopped(
|
||||
reporter: State<'_, PlaybackReporterWrapper>,
|
||||
connectivity: State<'_, ConnectivityMonitorWrapper>,
|
||||
item_id: String,
|
||||
position_seconds: f64,
|
||||
) -> Result<(), String> {
|
||||
let reporter_guard = reporter.0.lock().await;
|
||||
let reporter_instance = reporter_guard
|
||||
.as_ref()
|
||||
.ok_or("PlaybackReporter not initialized")?;
|
||||
|
||||
let position_ticks = seconds_to_ticks(position_seconds);
|
||||
let operation = PlaybackOperation::Stopped {
|
||||
item_id,
|
||||
position_ticks,
|
||||
};
|
||||
|
||||
let monitor = connectivity.0.lock().await;
|
||||
let is_online = monitor.get_status().await.is_server_reachable;
|
||||
drop(monitor);
|
||||
|
||||
reporter_instance.report(operation, is_online).await
|
||||
}
|
||||
|
||||
/// Mark item as played
|
||||
#[tauri::command]
|
||||
pub async fn playback_mark_played(
|
||||
reporter: State<'_, PlaybackReporterWrapper>,
|
||||
connectivity: State<'_, ConnectivityMonitorWrapper>,
|
||||
item_id: String,
|
||||
) -> Result<(), String> {
|
||||
let reporter_guard = reporter.0.lock().await;
|
||||
let reporter_instance = reporter_guard
|
||||
.as_ref()
|
||||
.ok_or("PlaybackReporter not initialized")?;
|
||||
|
||||
let operation = PlaybackOperation::MarkPlayed { item_id };
|
||||
|
||||
let monitor = connectivity.0.lock().await;
|
||||
let is_online = monitor.get_status().await.is_server_reachable;
|
||||
drop(monitor);
|
||||
|
||||
reporter_instance.report(operation, is_online).await
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,435 @@
|
||||
// Tauri commands for repository access
|
||||
// Uses handle-based system: UUID -> Arc<HybridRepository>
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use log::{debug, error, info};
|
||||
use tauri::State;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::jellyfin::HttpClient;
|
||||
use crate::repository::{HybridRepository, MediaRepository, OnlineRepository, OfflineRepository, types::*};
|
||||
|
||||
/// Repository handle manager
|
||||
pub struct RepositoryManager {
|
||||
repositories: Arc<Mutex<HashMap<String, Arc<HybridRepository>>>>,
|
||||
}
|
||||
|
||||
impl RepositoryManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
repositories: Arc::new(Mutex::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create(&self, handle: String, repository: HybridRepository) {
|
||||
let mut repos = self.repositories.lock().unwrap();
|
||||
repos.insert(handle, Arc::new(repository));
|
||||
}
|
||||
|
||||
pub fn get(&self, handle: &str) -> Option<Arc<HybridRepository>> {
|
||||
let repos = self.repositories.lock().unwrap();
|
||||
repos.get(handle).cloned()
|
||||
}
|
||||
|
||||
pub fn destroy(&self, handle: &str) {
|
||||
let mut repos = self.repositories.lock().unwrap();
|
||||
repos.remove(handle);
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper for Tauri state
|
||||
pub struct RepositoryManagerWrapper(pub RepositoryManager);
|
||||
|
||||
/// Create a new repository instance
|
||||
/// Returns a handle (UUID) for accessing the repository
|
||||
#[tauri::command]
|
||||
pub async fn repository_create(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
db: State<'_, crate::commands::storage::DatabaseWrapper>,
|
||||
server_url: String,
|
||||
user_id: String,
|
||||
access_token: String,
|
||||
server_id: String,
|
||||
) -> Result<String, String> {
|
||||
info!("[REPO] repository_create called for user: {}", user_id);
|
||||
|
||||
// Create HTTP client for online repository
|
||||
debug!("[REPO] Creating HTTP client...");
|
||||
let http_config = crate::jellyfin::HttpConfig::default();
|
||||
let http_client = HttpClient::new(http_config).map_err(|e| {
|
||||
error!("[REPO] HTTP client creation failed: {}", e);
|
||||
e.to_string()
|
||||
})?;
|
||||
debug!("[REPO] HTTP client created successfully");
|
||||
|
||||
// Create online repository
|
||||
debug!("[REPO] Creating online repository...");
|
||||
let online = OnlineRepository::new(Arc::new(http_client), server_url, user_id.clone(), access_token);
|
||||
debug!("[REPO] Online repository created");
|
||||
|
||||
// Create offline repository with async-safe database service
|
||||
debug!("[REPO] Creating database service...");
|
||||
let db_service = {
|
||||
let database = db.0.lock().map_err(|e| {
|
||||
error!("[REPO] Database lock failed: {}", e);
|
||||
e.to_string()
|
||||
})?;
|
||||
debug!("[REPO] Database lock acquired, getting service...");
|
||||
Arc::new(database.service())
|
||||
}; // Lock is released here
|
||||
debug!("[REPO] Database service created");
|
||||
|
||||
debug!("[REPO] Creating offline repository...");
|
||||
let offline = OfflineRepository::new(db_service, server_id, user_id);
|
||||
debug!("[REPO] Offline repository created");
|
||||
|
||||
// Create hybrid repository
|
||||
debug!("[REPO] Creating hybrid repository...");
|
||||
let hybrid = HybridRepository::new(online, offline);
|
||||
debug!("[REPO] Hybrid repository created");
|
||||
|
||||
// Generate handle and store repository
|
||||
let uuid = Uuid::new_v4();
|
||||
let handle = format!("{}", uuid);
|
||||
info!("[REPO] Generated handle: {}", handle);
|
||||
|
||||
// Store repository synchronously
|
||||
debug!("[REPO] Storing repository...");
|
||||
manager.0.create(handle.clone(), hybrid);
|
||||
info!("[REPO] Repository stored successfully");
|
||||
|
||||
Ok(handle)
|
||||
}
|
||||
|
||||
/// Destroy a repository instance
|
||||
#[tauri::command]
|
||||
pub async fn repository_destroy(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
) -> Result<(), String> {
|
||||
manager.0.destroy(&handle);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get libraries
|
||||
#[tauri::command]
|
||||
pub async fn repository_get_libraries(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
) -> Result<Vec<Library>, String> {
|
||||
debug!("[REPO] get_libraries called with handle: {}", handle);
|
||||
let repo = manager.0.get(&handle).ok_or_else(|| {
|
||||
error!("[REPO] Repository not found for handle: {}", handle);
|
||||
"Repository not found".to_string()
|
||||
})?;
|
||||
debug!("[REPO] Repository found, fetching libraries...");
|
||||
repo.as_ref().get_libraries()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("[REPO] Error fetching libraries: {:?}", e);
|
||||
format!("{:?}", e)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get items in a container (library, folder, album, etc.)
|
||||
#[tauri::command]
|
||||
pub async fn repository_get_items(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
parent_id: String,
|
||||
options: Option<GetItemsOptions>,
|
||||
) -> Result<SearchResult, String> {
|
||||
let repo = manager.0.get(&handle).ok_or("Repository not found")?;
|
||||
repo.as_ref().get_items(&parent_id, options)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
/// Get a single item by ID
|
||||
#[tauri::command]
|
||||
pub async fn repository_get_item(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
item_id: String,
|
||||
) -> Result<MediaItem, String> {
|
||||
let repo = manager.0.get(&handle).ok_or("Repository not found")?;
|
||||
repo.as_ref().get_item(&item_id)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
/// Get latest items in a library
|
||||
#[tauri::command]
|
||||
pub async fn repository_get_latest_items(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
parent_id: String,
|
||||
limit: Option<usize>,
|
||||
) -> Result<Vec<MediaItem>, String> {
|
||||
let repo = manager.0.get(&handle).ok_or("Repository not found")?;
|
||||
repo.as_ref().get_latest_items(&parent_id, limit)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
/// Get resume items (continue watching/listening)
|
||||
#[tauri::command]
|
||||
pub async fn repository_get_resume_items(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
parent_id: Option<String>,
|
||||
limit: Option<usize>,
|
||||
) -> Result<Vec<MediaItem>, String> {
|
||||
debug!("[REPO] get_resume_items called with handle: {}", handle);
|
||||
let repo = manager.0.get(&handle).ok_or_else(|| {
|
||||
error!("[REPO] Repository not found for handle: {}", handle);
|
||||
"Repository not found".to_string()
|
||||
})?;
|
||||
debug!("[REPO] Repository found, fetching resume items...");
|
||||
repo.as_ref().get_resume_items(parent_id.as_deref(), limit)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("[REPO] Error fetching resume items: {:?}", e);
|
||||
format!("{:?}", e)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get next up episodes
|
||||
#[tauri::command]
|
||||
pub async fn repository_get_next_up_episodes(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
series_id: Option<String>,
|
||||
limit: Option<usize>,
|
||||
) -> Result<Vec<MediaItem>, String> {
|
||||
let repo = manager.0.get(&handle).ok_or("Repository not found")?;
|
||||
repo.as_ref().get_next_up_episodes(series_id.as_deref(), limit)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
/// Get recently played audio
|
||||
#[tauri::command]
|
||||
pub async fn repository_get_recently_played_audio(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
limit: Option<usize>,
|
||||
) -> Result<Vec<MediaItem>, String> {
|
||||
let repo = manager.0.get(&handle).ok_or("Repository not found")?;
|
||||
repo.as_ref().get_recently_played_audio(limit)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
/// Get resume movies
|
||||
#[tauri::command]
|
||||
pub async fn repository_get_resume_movies(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
limit: Option<usize>,
|
||||
) -> Result<Vec<MediaItem>, String> {
|
||||
let repo = manager.0.get(&handle).ok_or("Repository not found")?;
|
||||
repo.as_ref().get_resume_movies(limit)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
/// Get genres for a library
|
||||
#[tauri::command]
|
||||
pub async fn repository_get_genres(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
parent_id: Option<String>,
|
||||
) -> Result<Vec<Genre>, String> {
|
||||
let repo = manager.0.get(&handle).ok_or("Repository not found")?;
|
||||
repo.as_ref().get_genres(parent_id.as_deref())
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
/// Search for items
|
||||
#[tauri::command]
|
||||
pub async fn repository_search(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
query: String,
|
||||
options: Option<SearchOptions>,
|
||||
) -> Result<SearchResult, String> {
|
||||
let repo = manager.0.get(&handle).ok_or("Repository not found")?;
|
||||
repo.as_ref().search(&query, options)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
/// Get playback info for an item
|
||||
#[tauri::command]
|
||||
pub async fn repository_get_playback_info(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
item_id: String,
|
||||
) -> Result<PlaybackInfo, String> {
|
||||
let repo = manager.0.get(&handle).ok_or("Repository not found")?;
|
||||
repo.as_ref().get_playback_info(&item_id)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
/// Get video stream URL with optional seeking support
|
||||
#[tauri::command]
|
||||
pub async fn repository_get_video_stream_url(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
item_id: String,
|
||||
media_source_id: Option<String>,
|
||||
start_time_seconds: Option<f64>,
|
||||
audio_stream_index: Option<i32>,
|
||||
) -> Result<String, String> {
|
||||
let repo = manager.0.get(&handle).ok_or("Repository not found")?;
|
||||
repo.as_ref()
|
||||
.get_video_stream_url(
|
||||
&item_id,
|
||||
media_source_id.as_deref(),
|
||||
start_time_seconds,
|
||||
audio_stream_index,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
/// Get audio stream URL for a track
|
||||
#[tauri::command]
|
||||
pub async fn repository_get_audio_stream_url(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
item_id: String,
|
||||
) -> Result<String, String> {
|
||||
let repo = manager.0.get(&handle).ok_or("Repository not found")?;
|
||||
repo.as_ref()
|
||||
.get_audio_stream_url(&item_id)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
/// Report playback start
|
||||
#[tauri::command]
|
||||
pub async fn repository_report_playback_start(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
item_id: String,
|
||||
position_ticks: i64,
|
||||
) -> Result<(), String> {
|
||||
let repo = manager.0.get(&handle).ok_or("Repository not found")?;
|
||||
repo.as_ref().report_playback_start(&item_id, position_ticks)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
/// Report playback progress
|
||||
#[tauri::command]
|
||||
pub async fn repository_report_playback_progress(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
item_id: String,
|
||||
position_ticks: i64,
|
||||
) -> Result<(), String> {
|
||||
let repo = manager.0.get(&handle).ok_or("Repository not found")?;
|
||||
repo.as_ref().report_playback_progress(&item_id, position_ticks)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
/// Report playback stopped
|
||||
#[tauri::command]
|
||||
pub async fn repository_report_playback_stopped(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
item_id: String,
|
||||
position_ticks: i64,
|
||||
) -> Result<(), String> {
|
||||
let repo = manager.0.get(&handle).ok_or("Repository not found")?;
|
||||
repo.as_ref().report_playback_stopped(&item_id, position_ticks)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
/// Get image URL for an item
|
||||
#[tauri::command]
|
||||
pub fn repository_get_image_url(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
item_id: String,
|
||||
image_type: ImageType,
|
||||
options: Option<ImageOptions>,
|
||||
) -> Result<String, String> {
|
||||
let repo = manager.0.get(&handle).ok_or("Repository not found")?;
|
||||
Ok(repo.as_ref().get_image_url(&item_id, image_type, options))
|
||||
}
|
||||
|
||||
/// Mark an item as favorite
|
||||
#[tauri::command]
|
||||
pub async fn repository_mark_favorite(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
item_id: String,
|
||||
) -> Result<(), String> {
|
||||
let repo = manager.0.get(&handle).ok_or("Repository not found")?;
|
||||
repo.as_ref().mark_favorite(&item_id)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
/// Unmark an item as favorite
|
||||
#[tauri::command]
|
||||
pub async fn repository_unmark_favorite(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
item_id: String,
|
||||
) -> Result<(), String> {
|
||||
let repo = manager.0.get(&handle).ok_or("Repository not found")?;
|
||||
repo.as_ref().unmark_favorite(&item_id)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
/// Get person details
|
||||
#[tauri::command]
|
||||
pub async fn repository_get_person(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
person_id: String,
|
||||
) -> Result<MediaItem, String> {
|
||||
let repo = manager.0.get(&handle).ok_or("Repository not found")?;
|
||||
repo.as_ref().get_person(&person_id)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
/// Get items by person (actor, director, etc.)
|
||||
#[tauri::command]
|
||||
pub async fn repository_get_items_by_person(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
person_id: String,
|
||||
options: Option<GetItemsOptions>,
|
||||
) -> Result<SearchResult, String> {
|
||||
let repo = manager.0.get(&handle).ok_or("Repository not found")?;
|
||||
repo.as_ref().get_items_by_person(&person_id, options)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
/// Get similar/related items for a media item
|
||||
#[tauri::command]
|
||||
pub async fn repository_get_similar_items(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
item_id: String,
|
||||
limit: Option<usize>,
|
||||
) -> Result<SearchResult, String> {
|
||||
let repo = manager.0.get(&handle).ok_or("Repository not found")?;
|
||||
repo.as_ref().get_similar_items(&item_id, limit)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
use std::sync::Arc;
|
||||
use tauri::State;
|
||||
use crate::session_poller::{PollingHint, SessionPollerManager};
|
||||
use crate::jellyfin::client::SessionInfo;
|
||||
|
||||
/// Tauri state wrapper for SessionPollerManager
|
||||
pub struct SessionPollerWrapper(pub Arc<SessionPollerManager>);
|
||||
|
||||
/// Set polling frequency hint based on UI state
|
||||
#[tauri::command]
|
||||
pub fn sessions_set_polling_hint(
|
||||
poller: State<'_, SessionPollerWrapper>,
|
||||
hint: String,
|
||||
) -> Result<(), String> {
|
||||
let parsed_hint = match hint.as_str() {
|
||||
"cast_active" => PollingHint::CastActive,
|
||||
"cast_discovery" => PollingHint::CastDiscovery,
|
||||
"normal" => PollingHint::Normal,
|
||||
_ => return Err(format!("Invalid polling hint: {}", hint)),
|
||||
};
|
||||
|
||||
poller.0.set_polling_hint(parsed_hint);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Manually trigger a session poll (for refresh button)
|
||||
#[tauri::command]
|
||||
pub async fn sessions_poll_now(
|
||||
poller: State<'_, SessionPollerWrapper>,
|
||||
) -> Result<Vec<SessionInfo>, String> {
|
||||
poller.0.poll_now().await
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,235 @@
|
||||
//! Tauri commands for sync queue operations
|
||||
//!
|
||||
//! The sync queue stores mutations (favorites, playback progress, etc.)
|
||||
//! that need to be synced to the Jellyfin server when connectivity is restored.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tauri::State;
|
||||
|
||||
use super::storage::DatabaseWrapper;
|
||||
use crate::storage::db_service::{DatabaseService, Query, QueryParam};
|
||||
|
||||
/// Sync queue item returned to frontend
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SyncQueueItem {
|
||||
pub id: i64,
|
||||
pub user_id: String,
|
||||
pub operation: String,
|
||||
pub item_id: Option<String>,
|
||||
pub payload: Option<String>,
|
||||
pub status: String,
|
||||
pub retry_count: i32,
|
||||
pub created_at: Option<String>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
/// Queue a mutation for sync to server
|
||||
#[tauri::command]
|
||||
pub async fn sync_queue_mutation(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
user_id: String,
|
||||
operation: String,
|
||||
item_id: Option<String>,
|
||||
payload: Option<String>,
|
||||
) -> Result<i64, String> {
|
||||
let db_service = {
|
||||
let database = db.0.lock().map_err(|e| e.to_string())?;
|
||||
Arc::new(database.service())
|
||||
};
|
||||
|
||||
let query = Query::with_params(
|
||||
"INSERT INTO sync_queue (user_id, operation, item_id, payload, status, created_at)
|
||||
VALUES (?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP)",
|
||||
vec![
|
||||
QueryParam::String(user_id),
|
||||
QueryParam::String(operation),
|
||||
item_id.map(QueryParam::String).unwrap_or(QueryParam::Null),
|
||||
payload.map(QueryParam::String).unwrap_or(QueryParam::Null),
|
||||
],
|
||||
);
|
||||
|
||||
db_service.execute(query).await.map_err(|e| e.to_string())?;
|
||||
let id = db_service.last_insert_rowid().await.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Get all pending sync operations for a user
|
||||
#[tauri::command]
|
||||
pub async fn sync_get_pending(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
user_id: String,
|
||||
limit: Option<i32>,
|
||||
) -> Result<Vec<SyncQueueItem>, String> {
|
||||
let db_service = {
|
||||
let database = db.0.lock().map_err(|e| e.to_string())?;
|
||||
Arc::new(database.service())
|
||||
};
|
||||
|
||||
let sql = if let Some(l) = limit {
|
||||
format!(
|
||||
"SELECT id, user_id, operation, item_id, payload, status, retry_count, created_at, error_message
|
||||
FROM sync_queue
|
||||
WHERE user_id = ? AND status IN ('pending', 'failed')
|
||||
ORDER BY created_at ASC
|
||||
LIMIT {}",
|
||||
l
|
||||
)
|
||||
} else {
|
||||
"SELECT id, user_id, operation, item_id, payload, status, retry_count, created_at, error_message
|
||||
FROM sync_queue
|
||||
WHERE user_id = ? AND status IN ('pending', 'failed')
|
||||
ORDER BY created_at ASC".to_string()
|
||||
};
|
||||
|
||||
let query = Query::with_params(sql, vec![QueryParam::String(user_id)]);
|
||||
|
||||
db_service
|
||||
.query_many(query, |row| {
|
||||
Ok(SyncQueueItem {
|
||||
id: row.get(0)?,
|
||||
user_id: row.get(1)?,
|
||||
operation: row.get(2)?,
|
||||
item_id: row.get(3)?,
|
||||
payload: row.get(4)?,
|
||||
status: row.get(5)?,
|
||||
retry_count: row.get(6)?,
|
||||
created_at: row.get(7)?,
|
||||
error_message: row.get(8)?,
|
||||
})
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Mark a sync operation as in progress
|
||||
#[tauri::command]
|
||||
pub async fn sync_mark_processing(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
id: i64,
|
||||
) -> Result<(), String> {
|
||||
let db_service = {
|
||||
let database = db.0.lock().map_err(|e| e.to_string())?;
|
||||
Arc::new(database.service())
|
||||
};
|
||||
|
||||
let query = Query::with_params(
|
||||
"UPDATE sync_queue SET status = 'processing' WHERE id = ?",
|
||||
vec![QueryParam::Int64(id)],
|
||||
);
|
||||
|
||||
db_service.execute(query).await.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark a sync operation as completed
|
||||
#[tauri::command]
|
||||
pub async fn sync_mark_completed(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
id: i64,
|
||||
) -> Result<(), String> {
|
||||
let db_service = {
|
||||
let database = db.0.lock().map_err(|e| e.to_string())?;
|
||||
Arc::new(database.service())
|
||||
};
|
||||
|
||||
let query = Query::with_params(
|
||||
"UPDATE sync_queue SET status = 'completed', processed_at = CURRENT_TIMESTAMP WHERE id = ?",
|
||||
vec![QueryParam::Int64(id)],
|
||||
);
|
||||
|
||||
db_service.execute(query).await.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark a sync operation as failed with error message
|
||||
#[tauri::command]
|
||||
pub async fn sync_mark_failed(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
id: i64,
|
||||
error: String,
|
||||
) -> Result<(), String> {
|
||||
let db_service = {
|
||||
let database = db.0.lock().map_err(|e| e.to_string())?;
|
||||
Arc::new(database.service())
|
||||
};
|
||||
|
||||
let query = Query::with_params(
|
||||
"UPDATE sync_queue
|
||||
SET status = 'failed',
|
||||
retry_count = retry_count + 1,
|
||||
error_message = ?,
|
||||
processed_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?",
|
||||
vec![QueryParam::String(error), QueryParam::Int64(id)],
|
||||
);
|
||||
|
||||
db_service.execute(query).await.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get count of pending sync operations for a user
|
||||
#[tauri::command]
|
||||
pub async fn sync_get_pending_count(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
user_id: String,
|
||||
) -> Result<i32, String> {
|
||||
let db_service = {
|
||||
let database = db.0.lock().map_err(|e| e.to_string())?;
|
||||
Arc::new(database.service())
|
||||
};
|
||||
|
||||
let query = Query::with_params(
|
||||
"SELECT COUNT(*) FROM sync_queue WHERE user_id = ? AND status IN ('pending', 'failed')",
|
||||
vec![QueryParam::String(user_id)],
|
||||
);
|
||||
|
||||
db_service
|
||||
.query_one(query, |row| row.get(0))
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Delete completed sync operations older than specified days
|
||||
#[tauri::command]
|
||||
pub async fn sync_cleanup_completed(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
days_old: i32,
|
||||
) -> Result<i32, String> {
|
||||
let db_service = {
|
||||
let database = db.0.lock().map_err(|e| e.to_string())?;
|
||||
Arc::new(database.service())
|
||||
};
|
||||
|
||||
let query = Query::with_params(
|
||||
"DELETE FROM sync_queue
|
||||
WHERE status = 'completed'
|
||||
AND processed_at < datetime('now', ?)",
|
||||
vec![QueryParam::String(format!("-{} days", days_old))],
|
||||
);
|
||||
|
||||
let deleted = db_service.execute(query).await.map_err(|e| e.to_string())?;
|
||||
Ok(deleted as i32)
|
||||
}
|
||||
|
||||
/// Delete all sync operations for a user (used during logout)
|
||||
#[tauri::command]
|
||||
pub async fn sync_clear_user(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
user_id: String,
|
||||
) -> Result<(), String> {
|
||||
let db_service = {
|
||||
let database = db.0.lock().map_err(|e| e.to_string())?;
|
||||
Arc::new(database.service())
|
||||
};
|
||||
|
||||
let query = Query::with_params(
|
||||
"DELETE FROM sync_queue WHERE user_id = ?",
|
||||
vec![QueryParam::String(user_id)],
|
||||
);
|
||||
|
||||
db_service.execute(query).await.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user