feat(library): genre sliders, artist links, and navigation utils
- music landing: diverse per-genre album sliders (online counts / offline wide-probe fallback) and home-screen library shortcuts - add ArtistLinks component and shared navigation/genreDiversity utils - player/playback-mode refinements across Rust and frontend
This commit is contained in:
@@ -311,7 +311,27 @@ impl PlayerController {
|
||||
|
||||
/// Set the queue and start playing from the specified index
|
||||
pub fn play_queue(&self, items: Vec<MediaItem>, start_index: usize) -> Result<(), PlayerError> {
|
||||
debug!("[PlayerController] play_queue: {} items, starting at index {}", items.len(), start_index);
|
||||
self.play_queue_from(items, start_index, None)
|
||||
}
|
||||
|
||||
/// Set the queue and start playing from the specified index, optionally
|
||||
/// resuming the starting track at `start_position` (seconds).
|
||||
///
|
||||
/// The seek happens immediately after load so the backend never audibly
|
||||
/// starts at 0 and there's no race against a fixed delay. Used when taking
|
||||
/// over playback from a remote session.
|
||||
pub fn play_queue_from(
|
||||
&self,
|
||||
items: Vec<MediaItem>,
|
||||
start_index: usize,
|
||||
start_position: Option<f64>,
|
||||
) -> Result<(), PlayerError> {
|
||||
debug!(
|
||||
"[PlayerController] play_queue: {} items, starting at index {} (resume: {:?})",
|
||||
items.len(),
|
||||
start_index,
|
||||
start_position
|
||||
);
|
||||
|
||||
// Reset autoplay counter on manual queue start
|
||||
self.reset_autoplay_count();
|
||||
@@ -324,6 +344,15 @@ impl PlayerController {
|
||||
// Play the current item (without modifying the queue we just set)
|
||||
if let Some(item) = self.queue.lock_safe().current().cloned() {
|
||||
self.load_and_play(&item)?;
|
||||
|
||||
// Resume from the requested position. Seeking right after load (while
|
||||
// the backend lock is no longer held) avoids the start-at-0-then-jump
|
||||
// race that a delayed frontend seek suffers from.
|
||||
if let Some(position) = start_position {
|
||||
if position > 0.5 {
|
||||
self.seek(position)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1333,6 +1362,44 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Resuming a queue at a position seeks the starting track immediately.
|
||||
/// Regression guard for taking over a remote session: the local player must
|
||||
/// pick up where the remote left off, not restart from 0.
|
||||
#[test]
|
||||
fn test_play_queue_from_resumes_at_position() {
|
||||
let controller = PlayerController::default();
|
||||
let items = create_test_items(3);
|
||||
|
||||
controller.play_queue_from(items, 1, Some(42.5)).unwrap();
|
||||
|
||||
{
|
||||
let queue = controller.queue();
|
||||
let queue_lock = queue.lock_safe();
|
||||
assert_eq!(queue_lock.current_index(), Some(1), "Should start at index 1");
|
||||
}
|
||||
assert_eq!(controller.position(), 42.5, "Should resume at the requested position");
|
||||
}
|
||||
|
||||
/// A None / near-zero start position starts the track from the beginning.
|
||||
#[test]
|
||||
fn test_play_queue_from_without_position_starts_at_zero() {
|
||||
let controller = PlayerController::default();
|
||||
|
||||
controller
|
||||
.play_queue_from(create_test_items(2), 0, None)
|
||||
.unwrap();
|
||||
assert_eq!(controller.position(), 0.0, "No resume position starts at 0");
|
||||
|
||||
controller
|
||||
.play_queue_from(create_test_items(2), 0, Some(0.2))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
controller.position(),
|
||||
0.0,
|
||||
"Sub-threshold resume position is ignored (starts at 0)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_seek_to_zero() {
|
||||
let controller = PlayerController::default();
|
||||
|
||||
Reference in New Issue
Block a user