Architecture remediation A/B/F: poison-tolerant locks, graceful backend init, doc fixes
Workstream A — poison-tolerant locking: - Add utils/lock.rs with MutexSafe/RwLockSafe extension traits that recover a poisoned std::sync lock instead of panicking, plus unit tests. - Replace all 153 .lock().unwrap() and 4 .read()/.write().unwrap() production sites with _safe variants across 14 files, eliminating the player crash-cascade class. Tokio async mutexes are unchanged. Workstream B — graceful backend init: - create_player_backend no longer panics when MPV/ExoPlayer fail to initialize; it falls back to NullBackend and emits a backend-init-failed event so the UI can show "playback unavailable" instead of the app crashing. Fatal DB-setup panics are kept. Workstream F — doc reconciliation: - Rewrite software-architecture.md's inaccurate "thin UI / ~800 lines" claims to reflect reality (~20.5k non-test frontend) and document the events+polling hybrid plus the new locking/backend-init behavior.
This commit is contained in:
+77
-76
@@ -44,6 +44,7 @@ pub use android::{
|
||||
set_media_command_handler, set_remote_volume_handler, get_detected_codecs,
|
||||
};
|
||||
|
||||
use crate::utils::lock::MutexSafe;
|
||||
use log::{debug, error, warn};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
@@ -121,7 +122,7 @@ impl PlayerController {
|
||||
|
||||
/// Configure the Jellyfin API client for automatic playback reporting
|
||||
pub fn set_jellyfin_client(&self, client: Option<JellyfinClient>) {
|
||||
let mut jellyfin = self.jellyfin_client.lock().unwrap();
|
||||
let mut jellyfin = self.jellyfin_client.lock_safe();
|
||||
*jellyfin = client;
|
||||
log::info!("[PlayerController] Jellyfin client configured: {}", jellyfin.is_some());
|
||||
}
|
||||
@@ -157,24 +158,24 @@ impl PlayerController {
|
||||
/// Set the end reason for the next playback end event
|
||||
fn set_end_reason(&self, reason: EndReason) {
|
||||
log::debug!("[PlayerController] Setting end reason: {:?}", reason);
|
||||
*self.end_reason.lock().unwrap() = Some(reason);
|
||||
*self.end_reason.lock_safe() = Some(reason);
|
||||
}
|
||||
|
||||
/// Get and clear the current end reason
|
||||
fn take_end_reason(&self) -> Option<EndReason> {
|
||||
self.end_reason.lock().unwrap().take()
|
||||
self.end_reason.lock_safe().take()
|
||||
}
|
||||
|
||||
/// Increment autoplay episode counter. Returns true if limit is reached.
|
||||
fn increment_autoplay_count(&self) -> bool {
|
||||
let max = self.autoplay_settings.lock().unwrap().max_episodes;
|
||||
let max = self.autoplay_settings.lock_safe().max_episodes;
|
||||
|
||||
if max == 0 {
|
||||
// Unlimited
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut count = self.autoplay_episode_count.lock().unwrap();
|
||||
let mut count = self.autoplay_episode_count.lock_safe();
|
||||
*count += 1;
|
||||
debug!("[PlayerController] Autoplay episode count: {}/{}", *count, max);
|
||||
|
||||
@@ -183,7 +184,7 @@ impl PlayerController {
|
||||
|
||||
/// Reset autoplay episode counter (called on manual play actions)
|
||||
fn reset_autoplay_count(&self) {
|
||||
let mut count = self.autoplay_episode_count.lock().unwrap();
|
||||
let mut count = self.autoplay_episode_count.lock_safe();
|
||||
if *count > 0 {
|
||||
debug!("[PlayerController] Resetting autoplay episode counter (was {})", *count);
|
||||
}
|
||||
@@ -199,7 +200,7 @@ impl PlayerController {
|
||||
|
||||
// Update queue with this single item
|
||||
{
|
||||
let mut queue = self.queue.lock().unwrap();
|
||||
let mut queue = self.queue.lock_safe();
|
||||
queue.set_queue(vec![item.clone()], 0);
|
||||
}
|
||||
|
||||
@@ -217,7 +218,7 @@ impl PlayerController {
|
||||
// Set end reason to NewTrackLoaded to prevent autoplay when MPV ends current track
|
||||
self.set_end_reason(EndReason::NewTrackLoaded);
|
||||
|
||||
let mut backend = self.backend.lock().unwrap();
|
||||
let mut backend = self.backend.lock_safe();
|
||||
backend.load(item)?;
|
||||
backend.play()?;
|
||||
drop(backend);
|
||||
@@ -297,12 +298,12 @@ impl PlayerController {
|
||||
self.reset_autoplay_count();
|
||||
|
||||
{
|
||||
let mut queue = self.queue.lock().unwrap();
|
||||
let mut queue = self.queue.lock_safe();
|
||||
queue.set_queue(items, start_index);
|
||||
}
|
||||
|
||||
// Play the current item (without modifying the queue we just set)
|
||||
if let Some(item) = self.queue.lock().unwrap().current().cloned() {
|
||||
if let Some(item) = self.queue.lock_safe().current().cloned() {
|
||||
self.load_and_play(&item)?;
|
||||
}
|
||||
|
||||
@@ -312,19 +313,19 @@ impl PlayerController {
|
||||
/// Play/resume playback
|
||||
pub fn play(&self) -> Result<(), PlayerError> {
|
||||
debug!("[PlayerController] play");
|
||||
let mut backend = self.backend.lock().unwrap();
|
||||
let mut backend = self.backend.lock_safe();
|
||||
backend.play()
|
||||
}
|
||||
|
||||
/// Pause playback
|
||||
pub fn pause(&self) -> Result<(), PlayerError> {
|
||||
let mut backend = self.backend.lock().unwrap();
|
||||
let mut backend = self.backend.lock_safe();
|
||||
backend.pause()
|
||||
}
|
||||
|
||||
/// Toggle play/pause
|
||||
pub fn toggle_playback(&self) -> Result<(), PlayerError> {
|
||||
let mut backend = self.backend.lock().unwrap();
|
||||
let mut backend = self.backend.lock_safe();
|
||||
if backend.state().is_playing() {
|
||||
backend.pause()
|
||||
} else {
|
||||
@@ -339,16 +340,16 @@ impl PlayerController {
|
||||
|
||||
// Get current playback info before stopping
|
||||
let jellyfin_id = {
|
||||
let queue = self.queue.lock().unwrap();
|
||||
let queue = self.queue.lock_safe();
|
||||
queue.current().and_then(|item| item.jellyfin_id().map(|s| s.to_string()))
|
||||
};
|
||||
|
||||
let position_ticks = {
|
||||
let backend = self.backend.lock().unwrap();
|
||||
let backend = self.backend.lock_safe();
|
||||
(backend.position() * 10_000_000.0) as i64
|
||||
};
|
||||
|
||||
let mut backend = self.backend.lock().unwrap();
|
||||
let mut backend = self.backend.lock_safe();
|
||||
backend.stop()?;
|
||||
drop(backend);
|
||||
|
||||
@@ -412,7 +413,7 @@ impl PlayerController {
|
||||
self.reset_autoplay_count();
|
||||
|
||||
let next_item = {
|
||||
let mut queue = self.queue.lock().unwrap();
|
||||
let mut queue = self.queue.lock_safe();
|
||||
queue.next().cloned()
|
||||
};
|
||||
|
||||
@@ -435,7 +436,7 @@ impl PlayerController {
|
||||
self.reset_autoplay_count();
|
||||
// If we're more than 3 seconds in, restart current track
|
||||
{
|
||||
let backend = self.backend.lock().unwrap();
|
||||
let backend = self.backend.lock_safe();
|
||||
if backend.position() > 3.0 {
|
||||
debug!("[PlayerController] previous: restarting current track (position > 3s)");
|
||||
drop(backend);
|
||||
@@ -444,7 +445,7 @@ impl PlayerController {
|
||||
}
|
||||
|
||||
let prev_item = {
|
||||
let mut queue = self.queue.lock().unwrap();
|
||||
let mut queue = self.queue.lock_safe();
|
||||
queue.previous().cloned()
|
||||
};
|
||||
|
||||
@@ -459,40 +460,40 @@ impl PlayerController {
|
||||
|
||||
/// Seek to a position in seconds
|
||||
pub fn seek(&self, position: f64) -> Result<(), PlayerError> {
|
||||
let mut backend = self.backend.lock().unwrap();
|
||||
let mut backend = self.backend.lock_safe();
|
||||
backend.seek(position)
|
||||
}
|
||||
|
||||
/// Set volume (0.0 - 1.0)
|
||||
pub fn set_volume(&self, volume: f32) -> Result<(), PlayerError> {
|
||||
self.backend.lock().unwrap().set_volume(volume)
|
||||
self.backend.lock_safe().set_volume(volume)
|
||||
}
|
||||
|
||||
/// Set the active audio track by stream index
|
||||
pub fn set_audio_track(&self, stream_index: i32) -> Result<(), PlayerError> {
|
||||
let mut backend = self.backend.lock().unwrap();
|
||||
let mut backend = self.backend.lock_safe();
|
||||
backend.set_audio_track(stream_index)
|
||||
}
|
||||
|
||||
/// Set the active subtitle track by stream index (None to disable subtitles)
|
||||
pub fn set_subtitle_track(&self, stream_index: Option<i32>) -> Result<(), PlayerError> {
|
||||
let mut backend = self.backend.lock().unwrap();
|
||||
let mut backend = self.backend.lock_safe();
|
||||
backend.set_subtitle_track(stream_index)
|
||||
}
|
||||
|
||||
/// Get current state
|
||||
pub fn state(&self) -> PlayerState {
|
||||
self.backend.lock().unwrap().state()
|
||||
self.backend.lock_safe().state()
|
||||
}
|
||||
|
||||
/// Get current position
|
||||
pub fn position(&self) -> f64 {
|
||||
self.backend.lock().unwrap().position()
|
||||
self.backend.lock_safe().position()
|
||||
}
|
||||
|
||||
/// Get duration
|
||||
pub fn duration(&self) -> Option<f64> {
|
||||
self.backend.lock().unwrap().duration()
|
||||
self.backend.lock_safe().duration()
|
||||
}
|
||||
|
||||
/// Get queue reference
|
||||
@@ -502,27 +503,27 @@ impl PlayerController {
|
||||
|
||||
/// Toggle shuffle
|
||||
pub fn toggle_shuffle(&self) {
|
||||
self.queue.lock().unwrap().toggle_shuffle();
|
||||
self.queue.lock_safe().toggle_shuffle();
|
||||
}
|
||||
|
||||
/// Cycle repeat mode
|
||||
pub fn cycle_repeat(&self) {
|
||||
self.queue.lock().unwrap().cycle_repeat();
|
||||
self.queue.lock_safe().cycle_repeat();
|
||||
}
|
||||
|
||||
/// Check if shuffle is enabled
|
||||
pub fn is_shuffle(&self) -> bool {
|
||||
self.queue.lock().unwrap().is_shuffle()
|
||||
self.queue.lock_safe().is_shuffle()
|
||||
}
|
||||
|
||||
/// Get repeat mode
|
||||
pub fn repeat_mode(&self) -> RepeatMode {
|
||||
self.queue.lock().unwrap().repeat_mode()
|
||||
self.queue.lock_safe().repeat_mode()
|
||||
}
|
||||
|
||||
/// Get current volume (0.0 - 1.0)
|
||||
pub fn volume(&self) -> f32 {
|
||||
self.backend.lock().unwrap().volume()
|
||||
self.backend.lock_safe().volume()
|
||||
}
|
||||
|
||||
/// Check if muted
|
||||
@@ -532,35 +533,35 @@ impl PlayerController {
|
||||
|
||||
/// Set audio settings (crossfade, gapless, normalization)
|
||||
pub fn set_audio_settings(&mut self, settings: &AudioSettings) -> Result<(), PlayerError> {
|
||||
self.backend.lock().unwrap().set_audio_settings(settings)
|
||||
self.backend.lock_safe().set_audio_settings(settings)
|
||||
}
|
||||
|
||||
/// Get current audio settings
|
||||
pub fn audio_settings(&self) -> AudioSettings {
|
||||
self.backend.lock().unwrap().audio_settings()
|
||||
self.backend.lock_safe().audio_settings()
|
||||
}
|
||||
|
||||
// ===== Sleep Timer Methods =====
|
||||
|
||||
/// Set the event emitter for notifications
|
||||
pub fn set_event_emitter(&self, emitter: Arc<dyn PlayerEventEmitter>) {
|
||||
let mut event_emitter = self.event_emitter.lock().unwrap();
|
||||
let mut event_emitter = self.event_emitter.lock_safe();
|
||||
*event_emitter = Some(emitter);
|
||||
}
|
||||
|
||||
/// Get the event emitter
|
||||
pub fn event_emitter(&self) -> Option<Arc<dyn PlayerEventEmitter>> {
|
||||
self.event_emitter.lock().unwrap().clone()
|
||||
self.event_emitter.lock_safe().clone()
|
||||
}
|
||||
|
||||
/// Get sleep timer state
|
||||
pub fn sleep_timer_state(&self) -> SleepTimerState {
|
||||
self.sleep_timer.lock().unwrap().clone()
|
||||
self.sleep_timer.lock_safe().clone()
|
||||
}
|
||||
|
||||
/// Set sleep timer mode (in-memory only, not persisted)
|
||||
pub fn set_sleep_timer(&self, mode: SleepTimerMode) {
|
||||
let mut timer = self.sleep_timer.lock().unwrap();
|
||||
let mut timer = self.sleep_timer.lock_safe();
|
||||
timer.mode = mode.clone();
|
||||
if let SleepTimerMode::Time { end_time } = mode {
|
||||
let now = chrono::Utc::now().timestamp_millis();
|
||||
@@ -589,7 +590,7 @@ impl PlayerController {
|
||||
loop {
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
|
||||
let mut timer = sleep_timer.lock().unwrap();
|
||||
let mut timer = sleep_timer.lock_safe();
|
||||
if timer.is_active() {
|
||||
timer.update_remaining_seconds();
|
||||
|
||||
@@ -599,7 +600,7 @@ impl PlayerController {
|
||||
timer.cancel();
|
||||
|
||||
// Emit cancelled state
|
||||
if let Some(emitter) = event_emitter.lock().unwrap().as_ref() {
|
||||
if let Some(emitter) = event_emitter.lock_safe().as_ref() {
|
||||
emitter.emit(PlayerStatusEvent::SleepTimerChanged {
|
||||
mode: SleepTimerMode::Off,
|
||||
remaining_seconds: 0,
|
||||
@@ -608,14 +609,14 @@ impl PlayerController {
|
||||
drop(timer);
|
||||
|
||||
// Stop the backend
|
||||
if let Err(e) = backend.lock().unwrap().stop() {
|
||||
if let Err(e) = backend.lock_safe().stop() {
|
||||
error!("[SleepTimer] Failed to stop playback: {}", e);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Emit update event
|
||||
if let Some(emitter) = event_emitter.lock().unwrap().as_ref() {
|
||||
if let Some(emitter) = event_emitter.lock_safe().as_ref() {
|
||||
emitter.emit(PlayerStatusEvent::SleepTimerChanged {
|
||||
mode: timer.mode.clone(),
|
||||
remaining_seconds: timer.remaining_seconds,
|
||||
@@ -629,9 +630,9 @@ impl PlayerController {
|
||||
|
||||
/// Emit sleep timer changed event to frontend
|
||||
fn emit_sleep_timer_changed(&self) {
|
||||
let timer = self.sleep_timer.lock().unwrap().clone();
|
||||
let timer = self.sleep_timer.lock_safe().clone();
|
||||
|
||||
if let Some(emitter) = self.event_emitter.lock().unwrap().as_ref() {
|
||||
if let Some(emitter) = self.event_emitter.lock_safe().as_ref() {
|
||||
emitter.emit(PlayerStatusEvent::SleepTimerChanged {
|
||||
mode: timer.mode,
|
||||
remaining_seconds: timer.remaining_seconds,
|
||||
@@ -641,12 +642,12 @@ impl PlayerController {
|
||||
|
||||
/// Emit queue changed event to frontend
|
||||
pub fn emit_queue_changed(&self) {
|
||||
let queue = self.queue.lock().unwrap();
|
||||
let queue = self.queue.lock_safe();
|
||||
|
||||
debug!("PlayerController::emit_queue_changed() - Emitting queue with {} items, current_index: {:?}",
|
||||
queue.items().len(), queue.current_index());
|
||||
|
||||
if let Some(emitter) = self.event_emitter.lock().unwrap().as_ref() {
|
||||
if let Some(emitter) = self.event_emitter.lock_safe().as_ref() {
|
||||
emitter.emit(PlayerStatusEvent::QueueChanged {
|
||||
items: queue.items().to_vec(),
|
||||
current_index: queue.current_index(),
|
||||
@@ -664,19 +665,19 @@ impl PlayerController {
|
||||
|
||||
/// Get autoplay settings
|
||||
pub fn autoplay_settings(&self) -> AutoplaySettings {
|
||||
self.autoplay_settings.lock().unwrap().clone()
|
||||
self.autoplay_settings.lock_safe().clone()
|
||||
}
|
||||
|
||||
/// Set autoplay settings (in-memory only, persistence handled by command layer)
|
||||
pub fn set_autoplay_settings(&self, settings: AutoplaySettings) {
|
||||
let validated = settings.with_validated_countdown();
|
||||
*self.autoplay_settings.lock().unwrap() = validated;
|
||||
*self.autoplay_settings.lock_safe() = validated;
|
||||
}
|
||||
|
||||
/// Cancel active autoplay countdown
|
||||
pub fn cancel_autoplay_countdown(&self) {
|
||||
if let Some(cancel_flag) = self.countdown_cancel.lock().unwrap().as_ref() {
|
||||
*cancel_flag.lock().unwrap() = true;
|
||||
if let Some(cancel_flag) = self.countdown_cancel.lock_safe().as_ref() {
|
||||
*cancel_flag.lock_safe() = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -719,7 +720,7 @@ impl PlayerController {
|
||||
}
|
||||
|
||||
let current_item = {
|
||||
let queue = self.queue.lock().unwrap();
|
||||
let queue = self.queue.lock_safe();
|
||||
queue.current().cloned()
|
||||
};
|
||||
|
||||
@@ -729,7 +730,7 @@ impl PlayerController {
|
||||
|
||||
// Check sleep timer state
|
||||
let timer_mode = {
|
||||
let timer = self.sleep_timer.lock().unwrap();
|
||||
let timer = self.sleep_timer.lock_safe();
|
||||
timer.mode.clone()
|
||||
};
|
||||
|
||||
@@ -739,14 +740,14 @@ impl PlayerController {
|
||||
let now = chrono::Utc::now().timestamp_millis();
|
||||
if now >= *end_time {
|
||||
debug!("[PlayerController] Time-based sleep timer expired at track boundary");
|
||||
self.sleep_timer.lock().unwrap().cancel();
|
||||
self.sleep_timer.lock_safe().cancel();
|
||||
self.emit_sleep_timer_changed();
|
||||
return Ok(AutoplayDecision::Stop);
|
||||
}
|
||||
}
|
||||
SleepTimerMode::EndOfTrack => {
|
||||
// Stop at end of track
|
||||
self.sleep_timer.lock().unwrap().cancel();
|
||||
self.sleep_timer.lock_safe().cancel();
|
||||
self.emit_sleep_timer_changed();
|
||||
return Ok(AutoplayDecision::Stop);
|
||||
}
|
||||
@@ -756,7 +757,7 @@ impl PlayerController {
|
||||
&& self.is_episode_item(¤t).await;
|
||||
|
||||
if is_episode {
|
||||
let should_stop = self.sleep_timer.lock().unwrap().decrement_episode();
|
||||
let should_stop = self.sleep_timer.lock_safe().decrement_episode();
|
||||
self.emit_sleep_timer_changed();
|
||||
|
||||
if should_stop {
|
||||
@@ -773,7 +774,7 @@ impl PlayerController {
|
||||
// Note: This path is typically not hit for HTML5 video (which uses on_video_playback_ended).
|
||||
// It's here for the Android ExoPlayer path where video items may be in the backend queue.
|
||||
if current.media_type == MediaType::Video && self.is_episode_item(¤t).await {
|
||||
let repo = self.repository.lock().unwrap().clone();
|
||||
let repo = self.repository.lock_safe().clone();
|
||||
let jellyfin_id = current.jellyfin_id().unwrap_or(¤t.id);
|
||||
let next_ep_result = if let Some(repo) = &repo {
|
||||
self.fetch_next_episode_for_item(jellyfin_id, repo).await?
|
||||
@@ -782,7 +783,7 @@ impl PlayerController {
|
||||
None
|
||||
};
|
||||
if let Some(next_ep) = next_ep_result {
|
||||
let settings = self.autoplay_settings.lock().unwrap().clone();
|
||||
let settings = self.autoplay_settings.lock_safe().clone();
|
||||
|
||||
// Check if auto-play episode limit is reached
|
||||
let limit_reached = self.increment_autoplay_count();
|
||||
@@ -803,7 +804,7 @@ impl PlayerController {
|
||||
|
||||
// For audio/movies, check if there's a next track in the queue
|
||||
let has_next = {
|
||||
let queue = self.queue.lock().unwrap();
|
||||
let queue = self.queue.lock_safe();
|
||||
queue.has_next()
|
||||
};
|
||||
|
||||
@@ -837,7 +838,7 @@ impl PlayerController {
|
||||
|
||||
// Check sleep timer state
|
||||
let timer_mode = {
|
||||
let timer = self.sleep_timer.lock().unwrap();
|
||||
let timer = self.sleep_timer.lock_safe();
|
||||
timer.mode.clone()
|
||||
};
|
||||
|
||||
@@ -846,18 +847,18 @@ impl PlayerController {
|
||||
let now = chrono::Utc::now().timestamp_millis();
|
||||
if now >= *end_time {
|
||||
debug!("[PlayerController] Time-based sleep timer expired at video end");
|
||||
self.sleep_timer.lock().unwrap().cancel();
|
||||
self.sleep_timer.lock_safe().cancel();
|
||||
self.emit_sleep_timer_changed();
|
||||
return Ok(AutoplayDecision::Stop);
|
||||
}
|
||||
}
|
||||
SleepTimerMode::EndOfTrack => {
|
||||
self.sleep_timer.lock().unwrap().cancel();
|
||||
self.sleep_timer.lock_safe().cancel();
|
||||
self.emit_sleep_timer_changed();
|
||||
return Ok(AutoplayDecision::Stop);
|
||||
}
|
||||
SleepTimerMode::Episodes { .. } => {
|
||||
let should_stop = self.sleep_timer.lock().unwrap().decrement_episode();
|
||||
let should_stop = self.sleep_timer.lock_safe().decrement_episode();
|
||||
self.emit_sleep_timer_changed();
|
||||
if should_stop {
|
||||
return Ok(AutoplayDecision::Stop);
|
||||
@@ -868,7 +869,7 @@ impl PlayerController {
|
||||
|
||||
// Fetch next episode for the video that just ended
|
||||
if let Some(next_ep) = self.fetch_next_episode_for_item(item_id, &repo).await? {
|
||||
let settings = self.autoplay_settings.lock().unwrap().clone();
|
||||
let settings = self.autoplay_settings.lock_safe().clone();
|
||||
|
||||
let limit_reached = self.increment_autoplay_count();
|
||||
if limit_reached {
|
||||
@@ -961,7 +962,7 @@ impl PlayerController {
|
||||
pub fn start_autoplay_countdown(&self, _next_item: crate::repository::types::MediaItem, countdown_seconds: u32) {
|
||||
// Create cancellation flag
|
||||
let cancel_flag = Arc::new(Mutex::new(false));
|
||||
*self.countdown_cancel.lock().unwrap() = Some(cancel_flag.clone());
|
||||
*self.countdown_cancel.lock_safe() = Some(cancel_flag.clone());
|
||||
|
||||
let event_emitter = self.event_emitter.clone();
|
||||
|
||||
@@ -972,7 +973,7 @@ impl PlayerController {
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
|
||||
// Check cancellation
|
||||
if *cancel_flag.lock().unwrap() {
|
||||
if *cancel_flag.lock_safe() {
|
||||
log::info!("[PlayerController] Autoplay countdown cancelled");
|
||||
return;
|
||||
}
|
||||
@@ -980,7 +981,7 @@ impl PlayerController {
|
||||
remaining -= 1;
|
||||
|
||||
// Emit countdown tick event
|
||||
if let Some(emitter) = event_emitter.lock().unwrap().as_ref() {
|
||||
if let Some(emitter) = event_emitter.lock_safe().as_ref() {
|
||||
emitter.emit(PlayerStatusEvent::CountdownTick {
|
||||
remaining_seconds: remaining,
|
||||
});
|
||||
@@ -1081,7 +1082,7 @@ mod tests {
|
||||
// Verify initial state
|
||||
{
|
||||
let queue = controller.queue();
|
||||
let queue_lock = queue.lock().unwrap();
|
||||
let queue_lock = queue.lock_safe();
|
||||
assert_eq!(queue_lock.items().len(), 5, "Queue should have 5 items");
|
||||
assert_eq!(queue_lock.current_index(), Some(0), "Should start at index 0");
|
||||
assert_eq!(queue_lock.current().unwrap().id, "item_0", "Current item should be item_0");
|
||||
@@ -1093,7 +1094,7 @@ mod tests {
|
||||
// Verify queue is intact and index advanced
|
||||
{
|
||||
let queue = controller.queue();
|
||||
let queue_lock = queue.lock().unwrap();
|
||||
let queue_lock = queue.lock_safe();
|
||||
assert_eq!(queue_lock.items().len(), 5, "Queue should still have 5 items after skip");
|
||||
assert_eq!(queue_lock.current_index(), Some(1), "Index should advance to 1");
|
||||
assert_eq!(queue_lock.current().unwrap().id, "item_1", "Current item should be item_1");
|
||||
@@ -1112,7 +1113,7 @@ mod tests {
|
||||
// Verify queue still intact and index advanced again
|
||||
{
|
||||
let queue = controller.queue();
|
||||
let queue_lock = queue.lock().unwrap();
|
||||
let queue_lock = queue.lock_safe();
|
||||
assert_eq!(queue_lock.items().len(), 5, "Queue should still have 5 items after second skip");
|
||||
assert_eq!(queue_lock.current_index(), Some(2), "Index should advance to 2");
|
||||
assert_eq!(queue_lock.current().unwrap().id, "item_2", "Current item should be item_2");
|
||||
@@ -1125,7 +1126,7 @@ mod tests {
|
||||
// Verify we're at the last item
|
||||
{
|
||||
let queue = controller.queue();
|
||||
let queue_lock = queue.lock().unwrap();
|
||||
let queue_lock = queue.lock_safe();
|
||||
assert_eq!(queue_lock.items().len(), 5, "Queue should still have 5 items at end");
|
||||
assert_eq!(queue_lock.current_index(), Some(4), "Index should be at last item (4)");
|
||||
assert_eq!(queue_lock.current().unwrap().id, "item_4", "Current item should be item_4");
|
||||
@@ -1147,7 +1148,7 @@ mod tests {
|
||||
// Verify we're at the last item
|
||||
{
|
||||
let queue = controller.queue();
|
||||
let queue_lock = queue.lock().unwrap();
|
||||
let queue_lock = queue.lock_safe();
|
||||
assert_eq!(queue_lock.current_index(), Some(2), "Should be at last item");
|
||||
}
|
||||
|
||||
@@ -1158,7 +1159,7 @@ mod tests {
|
||||
// Verify queue is still intact
|
||||
{
|
||||
let queue = controller.queue();
|
||||
let queue_lock = queue.lock().unwrap();
|
||||
let queue_lock = queue.lock_safe();
|
||||
assert_eq!(queue_lock.items().len(), 3, "Queue should still have 3 items after skip at end");
|
||||
// When we skip past the end, the queue index should stay at the last item
|
||||
// or become None (depending on implementation)
|
||||
@@ -1187,7 +1188,7 @@ mod tests {
|
||||
// Verify we wrapped to the first item
|
||||
{
|
||||
let queue = controller.queue();
|
||||
let queue_lock = queue.lock().unwrap();
|
||||
let queue_lock = queue.lock_safe();
|
||||
assert_eq!(queue_lock.items().len(), 3, "Queue should still have 3 items");
|
||||
assert_eq!(queue_lock.current_index(), Some(0), "Should wrap to index 0");
|
||||
assert_eq!(queue_lock.current().unwrap().id, "item_0", "Should be back at item_0");
|
||||
@@ -1206,7 +1207,7 @@ mod tests {
|
||||
// Verify starting position
|
||||
{
|
||||
let queue = controller.queue();
|
||||
let queue_lock = queue.lock().unwrap();
|
||||
let queue_lock = queue.lock_safe();
|
||||
assert_eq!(queue_lock.current_index(), Some(3), "Should start at index 3");
|
||||
}
|
||||
|
||||
@@ -1216,7 +1217,7 @@ mod tests {
|
||||
// Verify queue is intact and index moved back
|
||||
{
|
||||
let queue = controller.queue();
|
||||
let queue_lock = queue.lock().unwrap();
|
||||
let queue_lock = queue.lock_safe();
|
||||
assert_eq!(queue_lock.items().len(), 5, "Queue should still have 5 items after previous");
|
||||
assert_eq!(queue_lock.current_index(), Some(2), "Index should move to 2");
|
||||
assert_eq!(queue_lock.current().unwrap().id, "item_2", "Current item should be item_2");
|
||||
@@ -1385,7 +1386,7 @@ mod tests {
|
||||
|
||||
// Set sleep timer to end of track
|
||||
{
|
||||
let mut timer = controller.sleep_timer.lock().unwrap();
|
||||
let mut timer = controller.sleep_timer.lock_safe();
|
||||
timer.mode = SleepTimerMode::EndOfTrack;
|
||||
}
|
||||
|
||||
@@ -1400,7 +1401,7 @@ mod tests {
|
||||
|
||||
// Verify timer was cancelled
|
||||
{
|
||||
let timer = controller.sleep_timer.lock().unwrap();
|
||||
let timer = controller.sleep_timer.lock_safe();
|
||||
assert!(
|
||||
matches!(timer.mode, SleepTimerMode::Off),
|
||||
"Sleep timer should be cancelled after EndOfTrack"
|
||||
|
||||
Reference in New Issue
Block a user