Skip to content

Folders#92

Merged
LargeModGames merged 3 commits intomainfrom
folders
Feb 10, 2026
Merged

Folders#92
LargeModGames merged 3 commits intomainfrom
folders

Conversation

@LargeModGames
Copy link
Copy Markdown
Owner

This PR fixes playlist folder support in My Playlists and resolves the root cause of missing playlists beyond the first page.
It now fetches all user playlists (not just the first 50), builds folder-aware playlist navigation from Spotify rootlist data when streaming is enabled, and updates playlist actions (open/play/unfollow) to work correctly inside nested folders.
It also includes follow-up stability fixes:

  • Corrects active_playlist_index usage to track the real all_playlists index (fixes wrong playback context from folder views)
  • Adds a safety upper bound to playlist pagination to avoid potential infinite loops on inconsistent API responses
    Testing
  • cargo build
    • Output: Finished 'dev' profile [unoptimized + debuginfo] target(s) in ...
  • cargo build --features streaming
    • Output: Finished 'dev' profile [unoptimized + debuginfo] target(s) in ...
  • cargo build --no-default-features
    • Output: Finished 'dev' profile [unoptimized + debuginfo] target(s) in ... (with existing non-blocking warnings unrelated to this change)
      Manual verification performed:
  • Folder entries render and are navigable
  • Entering a folder shows contents plus back entry
  • Playlist selection from inside folders loads tracks
  • Unfollow action is restricted to playlist items and works with folder-aware indexing
    Additional notes
  • Folder icons use Unicode (📁). On systems without emoji glyph support this may render as tofu boxes ([]); functionality is unaffected.
  • Without streaming, folder hierarchy is unavailable (expected), but all playlists are still fetched across pages (so the >50 playlist bug is fixed in both modes).
  • Potential follow-up: add configurable ASCII fallback for folder icons ([F]) for terminals without emoji support.

Copilot AI review requested due to automatic review settings February 10, 2026 15:19
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6ab9a515ca

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/network.rs Outdated

// Update app state with complete data
let mut app = app.lock().await;
app.all_playlists.extend(remaining_playlists);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Sync legacy playlist page with full playlist state

The background completion path appends only app.all_playlists and never refreshes app.playlists, so the two playlist sources diverge after page 1. My Playlists flows that still resolve IDs from app.playlists[selected_playlist_index] (for example in src/handlers/track_table.rs and src/handlers/sort_menu.rs) can then target the wrong playlist or no playlist when the selection comes from folder views or playlists beyond the first page.

Useful? React with 👍 / 👎.

Comment thread src/network.rs
#[cfg(feature = "streaming")]
{
let streaming_player = self.streaming_player.clone();
tokio::spawn(async move {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Prevent overlapping background playlist refresh tasks

Each call to get_current_user_playlists launches a detached background fetch without any generation/cancellation guard, and refreshes are triggered repeatedly by follow/unfollow actions. If a second refresh starts before the first task finishes, both tasks can race to mutate playlist state, and because the task appends results later, stale or duplicate entries can be introduced depending on completion order.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR restores proper playlist folder support in the “My Playlists” panel and fixes missing playlists beyond the first page by introducing an all-playlists store and (when streaming is enabled) building a folder hierarchy from Spotify rootlist data.

Changes:

  • Fetch first page immediately, then paginate remaining playlists in a background task; maintain a flat all_playlists list.
  • Add folder-aware playlist navigation (PlaylistFolderNode/PlaylistFolderItem) and render folder entries in the UI.
  • Update playlist open/play/unfollow flows to use all_playlists indexing (instead of first-page indexing).

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/network.rs Adds background pagination + optional rootlist folder parsing and folder-item construction.
src/app.rs Adds new playlist state (all_playlists, folder nodes/items) and helpers for folder-aware display.
src/ui/mod.rs Renders folder-aware playlist list with folder/back entries.
src/handlers/playlist.rs Updates playlist panel navigation and actions to operate on folder-aware items.
src/handlers/track_table.rs Updates playback context lookup to use all_playlists via active_playlist_index.
src/player/streaming.rs Exposes librespot Session for rootlist calls.
Cargo.toml Adds optional librespot-protocol + protobuf under the streaming feature.
Cargo.lock Locks new streaming-related dependencies.

Comment thread src/network.rs
Comment on lines +2701 to +2703

app.playlist_folder_nodes = folder_nodes;
app.playlist_folder_items = folder_items;
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.

After rebuilding playlist_folder_items from rootlist, the ordering/contents of the visible playlist list can change (folders inserted, playlists moved). Updating playlist_folder_items without reconciling current_playlist_folder_id and selected_playlist_index can cause the UI selection to point at a different item than before (and potentially make the current folder id invalid). Consider resetting selection/folder to a safe default or preserving the selected playlist/folder by identity when replacing the list.

Copilot uses AI. Check for mistakes.
Comment thread src/network.rs
});
}
}

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.

parse_rootlist_items doesn’t unwind any still-open start-group sections at the end of the loop. If the rootlist data is inconsistent (missing end-group), this will drop all previously accumulated siblings stored in stack and return only the innermost partial group. Consider draining name_stack/stack after the loop to wrap remaining children and restore the full root vector.

Suggested change
// If there are any still-open groups (missing `end-group`),
// unwind them now so we don't lose accumulated siblings in `stack`.
while let Some((group_id, name)) = name_stack.pop() {
let children = std::mem::take(&mut root);
root = stack.pop().unwrap_or_default();
root.push(PlaylistFolderNode {
name: Some(name),
node_type: "folder".to_string(),
uri: format!("spotify:folder:{}", group_id),
children,
});
}

Copilot uses AI. Check for mistakes.
Comment thread src/ui/mod.rs Outdated
Comment on lines +332 to +338
let playlist_items: Vec<String> = if display_items.is_empty() {
// Fallback: show flat list from legacy playlists field
match &app.playlists {
Some(p) => p.items.iter().map(|item| item.name.to_owned()).collect(),
None => vec![],
}
} else {
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.

The fallback to the legacy flat playlist list is gated on display_items.is_empty(), which can also happen when playlist_folder_items exists but current_playlist_folder_id doesn’t match any entries (or if a folder view is temporarily empty). In that case the UI would unexpectedly show the legacy flat list while handlers still operate on folder-aware indices. Consider basing the fallback on whether playlist_folder_items is empty/uninitialized (or resetting current_playlist_folder_id when it becomes invalid) rather than on the filtered result.

Copilot uses AI. Check for mistakes.
Comment thread src/app.rs
Comment on lines +339 to +343
pub struct PlaylistFolderNode {
pub name: Option<String>,
pub node_type: String, // "folder" or "playlist"
pub uri: String,
pub children: Vec<PlaylistFolderNode>,
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.
Comment thread src/app.rs Outdated
Comment on lines +771 to +776
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,
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.

get_playlist_display_items allocates a new Vec of references and linearly scans playlist_folder_items on every call. This method is invoked in render paths and key handlers, so with large libraries it can become a noticeable CPU hot spot. Consider returning an iterator (or cached per-folder index lists) to avoid repeated allocations and full scans each frame.

Copilot uses AI. Check for mistakes.
Comment on lines +361 to 366
let context_id = match &app.active_playlist_index {
Some(active_playlist_index) => app
.all_playlists
.get(active_playlist_index.to_owned())
.map(|selected_playlist| playlist_context_id_from_ref(&selected_playlist.id)),
_ => None,
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.

selected_playlist_index now refers to an entry in the folder-aware display list, but much of this file still uses (&app.playlists, &app.selected_playlist_index) to resolve the active playlist for pagination and random playback. Once folders are enabled or playlists exceed the first page, those code paths can fetch/play tracks from the wrong playlist or fail to paginate. Consider resolving playlist IDs via active_playlist_index (set when opening) and all_playlists, and using that consistently for all TrackTableContext::MyPlaylists actions.

Copilot uses AI. Check for mistakes.
Comment thread src/network.rs Outdated
Comment on lines +121 to +123
/// Fetch all user playlists (all pages) and populate folder hierarchy
#[allow(dead_code)]
GetAllPlaylistsAndFolders,
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.

GetAllPlaylistsAndFolders is added to IoEvent but is not dispatched anywhere in the codebase (only referenced in this match). Keeping an unused public event (even with allow(dead_code)) increases surface area and maintenance overhead; either wire it up where playlists are requested or remove it and rely on the existing background pagination path.

Suggested change
/// Fetch all user playlists (all pages) and populate folder hierarchy
#[allow(dead_code)]
GetAllPlaylistsAndFolders,

Copilot uses AI. Check for mistakes.
Comment thread src/network.rs
Comment on lines +2663 to +2666
offset += items_count;
}
Err(_) => break,
}
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.

This background pagination loop silently stops on errors (Err(_) => break) without logging or surfacing anything to the user. That can leave the app permanently showing an incomplete playlist list with no indication why. Consider at least logging the error (or calling handle_error) and/or retrying with backoff.

Copilot uses AI. Check for mistakes.
@LargeModGames LargeModGames merged commit a7dc053 into main Feb 10, 2026
7 of 8 checks passed
@LargeModGames LargeModGames deleted the folders branch February 10, 2026 16:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants