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:
@@ -20,6 +20,28 @@ pub enum PlaybackMode {
|
||||
Idle,
|
||||
}
|
||||
|
||||
/// Number of Jellyfin ticks per second (100ns units).
|
||||
const TICKS_PER_SECOND: f64 = 10_000_000.0;
|
||||
|
||||
/// Below this many seconds we treat the position as "at the start" and don't
|
||||
/// send a resume position, so a fresh track casts from 0 rather than ~0.
|
||||
const RESUME_THRESHOLD_SECONDS: f64 = 0.5;
|
||||
|
||||
/// Convert a live playback position (seconds) into the `StartPositionTicks` to
|
||||
/// hand to a remote session, or `None` if we're effectively at the start.
|
||||
///
|
||||
/// Pure helper so the resume-position math is unit-testable without a remote
|
||||
/// session or HTTP. The *source* of `position_seconds` matters too: callers
|
||||
/// must pass the live backend position (`PlayerController::position()`), not the
|
||||
/// snapshot embedded in `PlayerState`, which is stale mid-track on Android.
|
||||
fn start_position_ticks_from_seconds(position_seconds: f64) -> Option<i64> {
|
||||
if position_seconds > RESUME_THRESHOLD_SECONDS {
|
||||
Some((position_seconds * TICKS_PER_SECOND) as i64)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Manages playback mode transfers between local and remote sessions
|
||||
pub struct PlaybackModeManager {
|
||||
jellyfin_client: Arc<Mutex<Option<JellyfinClient>>>,
|
||||
@@ -195,7 +217,6 @@ impl PlaybackModeManager {
|
||||
|
||||
let queue_arc = player.queue();
|
||||
let queue = queue_arc.lock_safe();
|
||||
let state = player.state();
|
||||
|
||||
let original_index = queue.current_index().unwrap_or(0);
|
||||
let items = queue.items();
|
||||
@@ -210,7 +231,11 @@ impl PlaybackModeManager {
|
||||
}
|
||||
|
||||
let (ids, adjusted_index) = self.extract_jellyfin_ids(items, original_index)?;
|
||||
let position = state.position().unwrap_or(0.0);
|
||||
// Read the LIVE backend position, not the snapshot embedded in PlayerState.
|
||||
// On Android the embedded position is only refreshed on play/pause
|
||||
// transitions, so PlayerState::position() is stale (often 0) mid-track;
|
||||
// PlayerController::position() always reflects the backend's current time.
|
||||
let position = player.position();
|
||||
let context = queue.context().clone();
|
||||
|
||||
log::info!(
|
||||
@@ -272,12 +297,8 @@ impl PlaybackModeManager {
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate position in ticks
|
||||
let start_position_ticks = if position_seconds > 0.5 {
|
||||
Some((position_seconds * 10_000_000.0) as i64)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// Calculate position in ticks (from the live position read above)
|
||||
let start_position_ticks = start_position_ticks_from_seconds(position_seconds);
|
||||
|
||||
// Log queue context for debugging (context is tracked but we always send track IDs)
|
||||
match &queue_context {
|
||||
@@ -577,6 +598,23 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// The resume position handed to a remote session is derived from a live
|
||||
/// playback position. Guards the seconds->ticks conversion and the
|
||||
/// at-the-start threshold (Bug: casting restarted the track from 0).
|
||||
#[test]
|
||||
fn test_start_position_ticks_from_seconds() {
|
||||
// Mid-track positions convert to ticks (10M ticks per second).
|
||||
assert_eq!(start_position_ticks_from_seconds(5.0), Some(50_000_000));
|
||||
assert_eq!(start_position_ticks_from_seconds(123.45), Some(1_234_500_000));
|
||||
|
||||
// At/near the start, send no resume position so the track casts from 0.
|
||||
assert_eq!(start_position_ticks_from_seconds(0.0), None);
|
||||
assert_eq!(start_position_ticks_from_seconds(0.5), None);
|
||||
|
||||
// Just past the threshold resumes rather than restarting.
|
||||
assert!(start_position_ticks_from_seconds(0.6).is_some());
|
||||
}
|
||||
|
||||
// Tests for extract_jellyfin_ids - verify all track IDs are sent to remote, not just album/playlist ID
|
||||
mod extract_jellyfin_ids_tests {
|
||||
use crate::player::{MediaItem, MediaSource, MediaType};
|
||||
|
||||
Reference in New Issue
Block a user