Split software arch desc for easier manintenance. Many fixes related to next video playing and remote playback
This commit is contained in:
@@ -10,6 +10,7 @@ pub mod offline;
|
||||
pub mod playback_mode;
|
||||
pub mod playback_reporting;
|
||||
pub mod player;
|
||||
pub mod playlist;
|
||||
pub mod repository;
|
||||
pub mod sessions;
|
||||
pub mod storage;
|
||||
@@ -25,6 +26,7 @@ pub use playback_mode::*;
|
||||
#[allow(unused_imports)] // Used when playback_reporting is fully integrated
|
||||
pub use playback_reporting::*;
|
||||
pub use player::*;
|
||||
pub use playlist::*;
|
||||
pub use repository::{*, RepositoryManager, RepositoryManagerWrapper};
|
||||
pub use sessions::*;
|
||||
pub use storage::*;
|
||||
|
||||
@@ -2396,11 +2396,15 @@ pub async fn player_play_next_episode(
|
||||
|
||||
/// Handle playback ended event - triggers autoplay decision logic
|
||||
/// This is called from:
|
||||
/// - Frontend when HTML5 video ends (Linux/desktop)
|
||||
/// - Frontend when HTML5 video ends (Linux/desktop) - passes itemId + repositoryHandle for the video
|
||||
/// - Frontend when audio track ends via backend event - no itemId/repositoryHandle needed
|
||||
/// - Android JNI callback also triggers this logic directly
|
||||
#[tauri::command]
|
||||
pub async fn player_on_playback_ended(
|
||||
player: State<'_, PlayerStateWrapper>,
|
||||
repository_manager: State<'_, super::repository::RepositoryManagerWrapper>,
|
||||
item_id: Option<String>,
|
||||
repository_handle: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
use crate::player::autoplay::AutoplayDecision;
|
||||
use crate::player::PlayerStatusEvent;
|
||||
@@ -2408,9 +2412,24 @@ pub async fn player_on_playback_ended(
|
||||
let controller_arc = player.0.clone();
|
||||
|
||||
// Run autoplay decision logic
|
||||
// If item_id is provided (HTML5 video case), use the video-specific path
|
||||
// that bypasses the backend queue and stale end_reason
|
||||
let decision = {
|
||||
let controller = controller_arc.lock().await;
|
||||
controller.on_playback_ended().await?
|
||||
if let Some(ref id) = item_id {
|
||||
// Video path: need repository to look up episode info
|
||||
let repo = repository_handle
|
||||
.as_ref()
|
||||
.and_then(|handle| repository_manager.0.get(handle));
|
||||
if let Some(repo) = repo {
|
||||
controller.on_video_playback_ended(id, repo).await?
|
||||
} else {
|
||||
log::warn!("[Autoplay] No repository available for video autoplay (itemId: {})", id);
|
||||
AutoplayDecision::Stop
|
||||
}
|
||||
} else {
|
||||
controller.on_playback_ended().await?
|
||||
}
|
||||
};
|
||||
|
||||
// Handle the decision
|
||||
@@ -2420,12 +2439,12 @@ pub async fn player_on_playback_ended(
|
||||
let controller = controller_arc.lock().await;
|
||||
if let Some(emitter) = controller.event_emitter() {
|
||||
// Emit StateChanged to idle to clear the current media from mini player
|
||||
// Note: Do NOT emit PlaybackEnded here - it would cause an infinite loop
|
||||
// (frontend receives PlaybackEnded → calls player_on_playback_ended → Stop → PlaybackEnded → ...)
|
||||
emitter.emit(PlayerStatusEvent::StateChanged {
|
||||
state: "idle".to_string(),
|
||||
media_id: None,
|
||||
});
|
||||
// Also emit PlaybackEnded event
|
||||
emitter.emit(PlayerStatusEvent::PlaybackEnded);
|
||||
}
|
||||
}
|
||||
AutoplayDecision::AdvanceToNext => {
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
//! Tauri commands for playlist management
|
||||
//! Uses handle-based system: UUID -> Arc<HybridRepository>
|
||||
//!
|
||||
//! TRACES: UR-014 | JA-019, JA-020
|
||||
|
||||
use log::debug;
|
||||
use tauri::State;
|
||||
|
||||
use crate::repository::{MediaRepository, types::*};
|
||||
use super::repository::RepositoryManagerWrapper;
|
||||
|
||||
/// Create a new playlist
|
||||
#[tauri::command]
|
||||
pub async fn playlist_create(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
name: String,
|
||||
item_ids: Option<Vec<String>>,
|
||||
) -> Result<PlaylistCreatedResult, String> {
|
||||
debug!("[PLAYLIST] create called: name={}", name);
|
||||
let repo = manager.0.get(&handle).ok_or("Repository not found")?;
|
||||
let ids = item_ids.unwrap_or_default();
|
||||
repo.as_ref().create_playlist(&name, &ids)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
/// Delete a playlist
|
||||
#[tauri::command]
|
||||
pub async fn playlist_delete(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
playlist_id: String,
|
||||
) -> Result<(), String> {
|
||||
debug!("[PLAYLIST] delete called: id={}", playlist_id);
|
||||
let repo = manager.0.get(&handle).ok_or("Repository not found")?;
|
||||
repo.as_ref().delete_playlist(&playlist_id)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
/// Rename a playlist
|
||||
#[tauri::command]
|
||||
pub async fn playlist_rename(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
playlist_id: String,
|
||||
name: String,
|
||||
) -> Result<(), String> {
|
||||
debug!("[PLAYLIST] rename called: id={}, name={}", playlist_id, name);
|
||||
let repo = manager.0.get(&handle).ok_or("Repository not found")?;
|
||||
repo.as_ref().rename_playlist(&playlist_id, &name)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
/// Get playlist items with PlaylistItemId
|
||||
#[tauri::command]
|
||||
pub async fn playlist_get_items(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
playlist_id: String,
|
||||
) -> Result<Vec<PlaylistEntry>, String> {
|
||||
debug!("[PLAYLIST] get_items called: id={}", playlist_id);
|
||||
let repo = manager.0.get(&handle).ok_or("Repository not found")?;
|
||||
repo.as_ref().get_playlist_items(&playlist_id)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
/// Add items to a playlist
|
||||
#[tauri::command]
|
||||
pub async fn playlist_add_items(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
playlist_id: String,
|
||||
item_ids: Vec<String>,
|
||||
) -> Result<(), String> {
|
||||
debug!("[PLAYLIST] add_items called: id={}, count={}", playlist_id, item_ids.len());
|
||||
let repo = manager.0.get(&handle).ok_or("Repository not found")?;
|
||||
repo.as_ref().add_to_playlist(&playlist_id, &item_ids)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
/// Remove items from a playlist (uses PlaylistItemId entry IDs, NOT media item IDs)
|
||||
#[tauri::command]
|
||||
pub async fn playlist_remove_items(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
playlist_id: String,
|
||||
entry_ids: Vec<String>,
|
||||
) -> Result<(), String> {
|
||||
debug!("[PLAYLIST] remove_items called: id={}, count={}", playlist_id, entry_ids.len());
|
||||
let repo = manager.0.get(&handle).ok_or("Repository not found")?;
|
||||
repo.as_ref().remove_from_playlist(&playlist_id, &entry_ids)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
/// Move a playlist item to a new position
|
||||
#[tauri::command]
|
||||
pub async fn playlist_move_item(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
playlist_id: String,
|
||||
item_id: String,
|
||||
new_index: u32,
|
||||
) -> Result<(), String> {
|
||||
debug!("[PLAYLIST] move_item called: playlist={}, item={}, index={}", playlist_id, item_id, new_index);
|
||||
let repo = manager.0.get(&handle).ok_or("Repository not found")?;
|
||||
repo.as_ref().move_playlist_item(&playlist_id, &item_id, new_index)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
Reference in New Issue
Block a user