Improve caching and UI sync
- Add skip operation guard to prevent concurrent track changes - Implement aggressive prefetch (3 tracks ahead instead of 1) - Add peek_upcoming method to queue for multi-track prefetch
This commit is contained in:
1 parent
5f7c0bb4fa
commit
53e0fd0e10
3 files changed
+114
-53
No files matched your search
@@ -103,67 +103,74 @@ async fn download_audio(url: &str) -> Result<Vec<u8>, String> {
|
||||
Ok(bytes.to_vec())
|
||||
}
|
||||
|
||||
/// Spawn background task to prefetch next track
|
||||
/// Number of tracks to prefetch ahead
|
||||
const PREFETCH_COUNT: usize = 3;
|
||||
|
||||
/// Spawn background tasks to prefetch upcoming tracks
|
||||
fn spawn_prefetch(
|
||||
client: Arc<Mutex<QobuzClient>>,
|
||||
cache: Arc<AudioCache>,
|
||||
queue: &QueueManager,
|
||||
) {
|
||||
// Get next track from queue
|
||||
let next_track = match queue.peek_next() {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
log::debug!("No next track to prefetch");
|
||||
return;
|
||||
}
|
||||
};
|
||||
// Get upcoming tracks from queue
|
||||
let upcoming_tracks = queue.peek_upcoming(PREFETCH_COUNT);
|
||||
|
||||
let track_id = next_track.id;
|
||||
let track_title = next_track.title.clone();
|
||||
|
||||
// Check if already cached or being fetched
|
||||
if cache.contains(track_id) {
|
||||
log::debug!("Next track {} already cached", track_id);
|
||||
if upcoming_tracks.is_empty() {
|
||||
log::debug!("No upcoming tracks to prefetch");
|
||||
return;
|
||||
}
|
||||
|
||||
if cache.is_fetching(track_id) {
|
||||
log::debug!("Next track {} already being fetched", track_id);
|
||||
return;
|
||||
for track in upcoming_tracks {
|
||||
let track_id = track.id;
|
||||
let track_title = track.title.clone();
|
||||
|
||||
// Check if already cached or being fetched
|
||||
if cache.contains(track_id) {
|
||||
log::debug!("Track {} already cached", track_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
if cache.is_fetching(track_id) {
|
||||
log::debug!("Track {} already being fetched", track_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Mark as fetching
|
||||
cache.mark_fetching(track_id);
|
||||
|
||||
let client_clone = client.clone();
|
||||
let cache_clone = cache.clone();
|
||||
|
||||
log::info!("Prefetching track: {} - {}", track_id, track_title);
|
||||
|
||||
// Spawn background task for each track
|
||||
tokio::spawn(async move {
|
||||
let result = async {
|
||||
let client_guard = client_clone.lock().await;
|
||||
let stream_url = client_guard
|
||||
.get_stream_url_with_fallback(track_id, Quality::HiRes)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get stream URL: {}", e))?;
|
||||
drop(client_guard);
|
||||
|
||||
let data = download_audio(&stream_url.url).await?;
|
||||
Ok::<Vec<u8>, String>(data)
|
||||
}
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(data) => {
|
||||
cache_clone.insert(track_id, data);
|
||||
log::info!("Prefetch complete for track {}", track_id);
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Prefetch failed for track {}: {}", track_id, e);
|
||||
}
|
||||
}
|
||||
|
||||
cache_clone.unmark_fetching(track_id);
|
||||
});
|
||||
}
|
||||
|
||||
// Mark as fetching
|
||||
cache.mark_fetching(track_id);
|
||||
|
||||
log::info!("Prefetching next track: {} - {}", track_id, track_title);
|
||||
|
||||
// Spawn background task
|
||||
tokio::spawn(async move {
|
||||
let result = async {
|
||||
let client_guard = client.lock().await;
|
||||
let stream_url = client_guard
|
||||
.get_stream_url_with_fallback(track_id, Quality::HiRes)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get stream URL: {}", e))?;
|
||||
drop(client_guard);
|
||||
|
||||
let data = download_audio(&stream_url.url).await?;
|
||||
Ok::<Vec<u8>, String>(data)
|
||||
}
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(data) => {
|
||||
cache.insert(track_id, data);
|
||||
log::info!("Prefetch complete for track {}", track_id);
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Prefetch failed for track {}: {}", track_id, e);
|
||||
}
|
||||
}
|
||||
|
||||
cache.unmark_fetching(track_id);
|
||||
});
|
||||
}
|
||||
|
||||
/// Pause playback
|
||||
|
||||
@@ -201,6 +201,53 @@ impl QueueManager {
|
||||
next_idx.and_then(|idx| state.tracks.get(idx).cloned())
|
||||
}
|
||||
|
||||
/// Get multiple upcoming tracks without advancing (for prefetching)
|
||||
pub fn peek_upcoming(&self, count: usize) -> Vec<QueueTrack> {
|
||||
let state = self.state.lock().unwrap();
|
||||
if state.tracks.is_empty() || count == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Don't return upcoming if repeat one (same track always)
|
||||
if state.repeat == RepeatMode::One {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut result = Vec::with_capacity(count);
|
||||
|
||||
if state.shuffle {
|
||||
let start_pos = state.shuffle_position + 1;
|
||||
for i in 0..count {
|
||||
let pos = start_pos + i;
|
||||
if pos < state.shuffle_order.len() {
|
||||
if let Some(track) = state.tracks.get(state.shuffle_order[pos]) {
|
||||
result.push(track.clone());
|
||||
}
|
||||
} else if state.repeat == RepeatMode::All {
|
||||
// Wrap around
|
||||
let wrapped_pos = pos % state.shuffle_order.len();
|
||||
if let Some(track) = state.tracks.get(state.shuffle_order[wrapped_pos]) {
|
||||
result.push(track.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let start_idx = state.current_index.map(|i| i + 1).unwrap_or(0);
|
||||
for i in 0..count {
|
||||
let idx = start_idx + i;
|
||||
if idx < state.tracks.len() {
|
||||
result.push(state.tracks[idx].clone());
|
||||
} else if state.repeat == RepeatMode::All {
|
||||
// Wrap around
|
||||
let wrapped_idx = idx % state.tracks.len();
|
||||
result.push(state.tracks[wrapped_idx].clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Advance to next track and return it
|
||||
pub fn next(&self) -> Option<QueueTrack> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
|
||||
@@ -398,13 +398,14 @@
|
||||
|
||||
// Skip track handlers - wired to backend queue
|
||||
async function handleSkipBack() {
|
||||
if (!currentTrack) return;
|
||||
if (!currentTrack || isSkipping) return;
|
||||
// If more than 3 seconds in, restart track; otherwise go to previous
|
||||
if (currentTime > 3) {
|
||||
handleSeek(0);
|
||||
return;
|
||||
}
|
||||
|
||||
isSkipping = true;
|
||||
try {
|
||||
const prevTrack = await invoke<BackendQueueTrack | null>('previous_track');
|
||||
if (prevTrack) {
|
||||
@@ -416,12 +417,15 @@
|
||||
} catch (err) {
|
||||
console.error('Failed to go to previous track:', err);
|
||||
showToast('Failed to go to previous track', 'error');
|
||||
} finally {
|
||||
isSkipping = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSkipForward() {
|
||||
if (!currentTrack) return;
|
||||
if (!currentTrack || isSkipping) return;
|
||||
|
||||
isSkipping = true;
|
||||
try {
|
||||
const nextTrackResult = await invoke<BackendQueueTrack | null>('next_track');
|
||||
if (nextTrackResult) {
|
||||
@@ -435,6 +439,8 @@
|
||||
} catch (err) {
|
||||
console.error('Failed to go to next track:', err);
|
||||
showToast('Failed to go to next track', 'error');
|
||||
} finally {
|
||||
isSkipping = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -671,6 +677,7 @@
|
||||
let pollInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
let isAdvancingTrack = false; // Prevent multiple advances
|
||||
let isSkipping = false; // Prevent concurrent skip operations
|
||||
|
||||
async function pollPlaybackState() {
|
||||
if (!currentTrack) return;
|
||||
|
||||
Reference in new issue
Block a user