Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
fix: guard playlist background refresh races, unify playlist resoluti…
…on, and clean warnings
  • Loading branch information
LargeModGames committed Feb 10, 2026
commit 078381e7e4cf60a2b15cd52adc50e4a90bb12bba
78 changes: 66 additions & 12 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,11 +334,19 @@ pub struct NativeTrackInfo {
pub duration_ms: u32,
}

/// A node in the playlist folder hierarchy from Spotify's rootlist
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[allow(dead_code)]
pub enum PlaylistFolderNodeType {
Folder,
Playlist,
}

/// A node in the playlist folder hierarchy from Spotify's rootlist
#[derive(Clone, Debug)]
pub struct PlaylistFolderNode {
pub name: Option<String>,
pub node_type: String, // "folder" or "playlist"
pub node_type: PlaylistFolderNodeType,
pub uri: String,
pub children: Vec<PlaylistFolderNode>,
Comment on lines +347 to +351
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PlaylistFolderNode.node_type is a free-form String but the code relies on specific values ("folder" vs "playlist") for control flow. Using a string here makes invalid states representable and adds allocations; consider making this an enum (and parsing into that) to improve type-safety and avoid unexpected fallthroughs.

Copilot uses AI. Check for mistakes.
}
Expand Down Expand Up @@ -539,13 +547,16 @@ pub struct App {
/// Whether native streaming is active (disables API-based progress calculation)
pub is_streaming_active: bool,
/// Device id for the native streaming device when known
#[allow(dead_code)]
pub native_device_id: Option<String>,
/// Native playback state - updated by player events, used when streaming is active
/// This is more reliable than current_playback_context.is_playing during native streaming
pub native_is_playing: Option<bool>,
/// Timestamp of the last native device activation
#[allow(dead_code)]
pub last_device_activation: Option<Instant>,
/// Whether a native device activation is still in progress
#[allow(dead_code)]
pub native_activation_pending: bool,
/// Selected index in the Discover view
pub discover_selected_index: usize,
Expand Down Expand Up @@ -584,6 +595,8 @@ pub struct App {
pub playlist_folder_items: Vec<PlaylistFolderItem>,
/// Current folder ID being viewed (0 = root)
pub current_playlist_folder_id: usize,
/// Incremented every time playlists are refreshed to guard stale background tasks
pub playlist_refresh_generation: u64,
/// Reference to the native streaming player for direct control (bypasses event channel)
#[cfg(feature = "streaming")]
pub streaming_player: Option<Arc<crate::player::StreamingPlayer>>,
Expand Down Expand Up @@ -725,6 +738,7 @@ impl Default for App {
playlist_folder_nodes: None,
playlist_folder_items: Vec::new(),
current_playlist_folder_id: 0,
playlist_refresh_generation: 0,
#[cfg(feature = "streaming")]
streaming_player: None,
#[cfg(all(feature = "mpris", target_os = "linux"))]
Expand Down Expand Up @@ -765,19 +779,39 @@ impl App {
self.io_tx = None;
}

/// Get the items visible in the current folder level.
/// Returns a filtered view of playlist_folder_items where current_id matches
/// the current_playlist_folder_id.
pub fn is_playlist_item_visible_in_current_folder(&self, item: &PlaylistFolderItem) -> bool {
match item {
PlaylistFolderItem::Folder(f) => f.current_id == self.current_playlist_folder_id,
PlaylistFolderItem::Playlist { current_id, .. } => {
*current_id == self.current_playlist_folder_id
}
}
}

/// Get the number of items visible in the current folder level.
pub fn get_playlist_display_count(&self) -> usize {
self
.playlist_folder_items
.iter()
.filter(|item| self.is_playlist_item_visible_in_current_folder(item))
.count()
}

/// Get a visible item by display index in the current folder.
pub fn get_playlist_display_item_at(&self, display_index: usize) -> Option<&PlaylistFolderItem> {
self
.playlist_folder_items
.iter()
.filter(|item| self.is_playlist_item_visible_in_current_folder(item))
.nth(display_index)
}

/// Get visible playlist items in the current folder (used by UI rendering).
pub fn get_playlist_display_items(&self) -> Vec<&PlaylistFolderItem> {
self
.playlist_folder_items
.iter()
.filter(|item| match item {
PlaylistFolderItem::Folder(f) => f.current_id == self.current_playlist_folder_id,
PlaylistFolderItem::Playlist { current_id, .. } => {
*current_id == self.current_playlist_folder_id
}
})
.filter(|item| self.is_playlist_item_visible_in_current_folder(item))
.collect()
}

Expand All @@ -790,6 +824,25 @@ impl App {
}
}

/// Get the currently selected playlist id in the visible playlist list.
pub fn get_selected_playlist_id(&self) -> Option<String> {
let selected_index = self.selected_playlist_index?;
if let Some(PlaylistFolderItem::Playlist { index, .. }) =
self.get_playlist_display_item_at(selected_index)
{
return self
.all_playlists
.get(*index)
.map(|p| p.id.id().to_string());
}

self
.playlists
.as_ref()
.and_then(|playlists| playlists.items.get(selected_index))
.map(|playlist| playlist.id.id().to_string())
}

fn apply_seek(&mut self, seek_ms: u32) {
if let Some(CurrentPlaybackContext {
item: Some(item), ..
Expand Down Expand Up @@ -1793,8 +1846,9 @@ impl App {

pub fn user_unfollow_playlist(&mut self) {
if let (Some(selected_index), Some(user)) = (self.selected_playlist_index, &self.user) {
let display_items = self.get_playlist_display_items();
if let Some(PlaylistFolderItem::Playlist { index, .. }) = display_items.get(selected_index) {
if let Some(PlaylistFolderItem::Playlist { index, .. }) =
self.get_playlist_display_item_at(selected_index)
{
if let Some(playlist) = self.all_playlists.get(*index) {
let selected_id = playlist.id.clone();
let user_id = user.id.clone();
Expand Down
3 changes: 3 additions & 0 deletions src/audio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub use analyzer::SpectrumData;
feature = "audio-viz-cpal"
)))]
#[derive(Clone, Default)]
#[allow(dead_code)]
pub struct SpectrumData {
pub bands: [f32; 12],
pub peak: f32,
Expand All @@ -46,12 +47,14 @@ pub struct SpectrumData {
all(feature = "audio-viz", target_os = "linux"),
feature = "audio-viz-cpal"
)))]
#[allow(dead_code)]
pub struct AudioCaptureManager;

#[cfg(not(any(
all(feature = "audio-viz", target_os = "linux"),
feature = "audio-viz-cpal"
)))]
#[allow(dead_code)]
impl AudioCaptureManager {
pub fn new() -> Option<Self> {
None
Expand Down
51 changes: 24 additions & 27 deletions src/handlers/playlist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,48 +10,44 @@ pub fn handler(key: Key, app: &mut App) {
match key {
k if common_key_events::right_event(k) => common_key_events::handle_right_event(app),
k if common_key_events::down_event(k) => {
let display_items = app.get_playlist_display_items();
if !display_items.is_empty() {
if let Some(selected_playlist_index) = app.selected_playlist_index {
let next_index =
common_key_events::on_down_press_handler(&display_items, Some(selected_playlist_index));
app.selected_playlist_index = Some(next_index);
}
let count = app.get_playlist_display_count();
if count > 0 {
let current = app.selected_playlist_index.unwrap_or(0);
app.selected_playlist_index = Some((current + 1) % count);
}
}
k if common_key_events::up_event(k) => {
let display_items = app.get_playlist_display_items();
if !display_items.is_empty() {
let next_index =
common_key_events::on_up_press_handler(&display_items, app.selected_playlist_index);
app.selected_playlist_index = Some(next_index);
let count = app.get_playlist_display_count();
if count > 0 {
let current = app.selected_playlist_index.unwrap_or(0);
app.selected_playlist_index = Some(if current == 0 { count - 1 } else { current - 1 });
}
}
k if common_key_events::high_event(k) => {
let display_items = app.get_playlist_display_items();
if !display_items.is_empty() {
let next_index = common_key_events::on_high_press_handler();
app.selected_playlist_index = Some(next_index);
if app.get_playlist_display_count() > 0 {
app.selected_playlist_index = Some(0);
}
}
k if common_key_events::middle_event(k) => {
let display_items = app.get_playlist_display_items();
if !display_items.is_empty() {
let next_index = common_key_events::on_middle_press_handler(&display_items);
let count = app.get_playlist_display_count();
if count > 0 {
let next_index = if count.is_multiple_of(2) {
count.saturating_sub(1) / 2
} else {
count / 2
};
app.selected_playlist_index = Some(next_index);
}
}
k if common_key_events::low_event(k) => {
let display_items = app.get_playlist_display_items();
if !display_items.is_empty() {
let next_index = common_key_events::on_low_press_handler(&display_items);
app.selected_playlist_index = Some(next_index);
let count = app.get_playlist_display_count();
if count > 0 {
app.selected_playlist_index = Some(count - 1);
}
}
Key::Enter => {
let display_items = app.get_playlist_display_items();
if let Some(selected_idx) = app.selected_playlist_index {
if let Some(item) = display_items.get(selected_idx) {
if let Some(item) = app.get_playlist_display_item_at(selected_idx) {
match item {
PlaylistFolderItem::Folder(folder) => {
// Navigate into/out of folder
Expand All @@ -78,9 +74,10 @@ pub fn handler(key: Key, app: &mut App) {
}
}
Key::Char('D') => {
let display_items = app.get_playlist_display_items();
if let Some(selected_idx) = app.selected_playlist_index {
if let Some(PlaylistFolderItem::Playlist { index, .. }) = display_items.get(selected_idx) {
if let Some(PlaylistFolderItem::Playlist { index, .. }) =
app.get_playlist_display_item_at(selected_idx)
{
if let Some(playlist) = app.all_playlists.get(*index) {
let selected_playlist = &playlist.name;
app.dialog = Some(selected_playlist.clone());
Expand Down
6 changes: 2 additions & 4 deletions src/handlers/sort_menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,8 @@ fn apply_sort(app: &mut App, field: SortField) {
SortContext::PlaylistTracks => {
// For playlists, dispatch network event to fetch all tracks and sort
// Get the current playlist ID
if let (Some(playlists), Some(selected_index)) =
(&app.playlists, app.selected_playlist_index)
{
if let Some(playlist) = playlists.items.get(selected_index) {
if let Some(active_playlist_index) = app.active_playlist_index {
if let Some(playlist) = app.all_playlists.get(active_playlist_index) {
let playlist_id = playlist.id.clone().into_static();
app.dispatch(crate::network::IoEvent::FetchAllPlaylistTracksAndSort(
playlist_id,
Expand Down
Loading
Loading