Skip to content

Shareable saved playlist restore and browser - #5

Open
NickKhunapoj wants to merge 15 commits into
ATOMIC09:yuuka-v3from
NickKhunapoj:save-queue
Open

Shareable saved playlist restore and browser#5
NickKhunapoj wants to merge 15 commits into
ATOMIC09:yuuka-v3from
NickKhunapoj:save-queue

Conversation

@NickKhunapoj

@NickKhunapoj NickKhunapoj commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a lightweight, durable saved-playlist system for music queues.

Users can save YouTube tracks from the active queue, browse saved playlists in their server, inspect details, load or delete playlists with confirmation popups, and restore a playlist through the existing music playback pipeline.

New features

Save active queue

  • Added an icon-only 💾 Save button beside Refresh on the player’s second control row.

  • Saves YouTube tracks in their intended playback order:

    1. Current track, when it is a YouTube track
    2. Pending crossfade track, when present
    3. Remaining queued YouTube tracks
  • Excludes /music local attachment tracks, preventing unusable saved playlists.

  • Rejects empty or local-only queues.

  • Stores each track’s title, YouTube URL/query, and duration.

  • Stores the current position only when the active track is saved.

  • Sends the generated restore code privately to the user.

image

Saving the active music queue and receiving a restore code.

Guild-private, shareable restore codes

  • Uses collision-safe xxxx-xxxx playlist codes.
  • Codes expire 30 days after creation.
  • Each playlist stores its originating Discord guild ID and creator ID.
  • Codes are shareable with anyone in the same server.
  • Codes cannot be listed, restored, loaded, or deleted from another server.
  • A restore code is single-use: after confirmation, voice validation, and atomic consumption, it is removed from storage.

Legacy records created before guild scoping are hidden from lists. Using an old code directly claims that record for the guild where it is first restored, preserving recovery while enforcing guild isolation afterward.

Restore command

/music restore <code>
  • Validates the code, guild scope, expiry, and stored playlist data.
  • Shows a private confirmation popup before replacing the active queue.
  • Confirmation buttons disable immediately after Load is pressed to prevent duplicate restore requests.
  • Reuses the existing voice connection logic or joins the requesting user’s voice channel.
  • Safely replaces the active queue and invalidates stale playback callbacks.
  • Starts the first restored track automatically.
  • Applies the saved first-track playback position through FFmpeg.
  • Uses just-in-time yt-dlp hydration, so only the track about to play needs a fresh stream URL.

image image

Restoring a playlist and automatically starting playback.

Public playlist browser

/music restore list

Displays a public, paginated browser for playlists saved in the current server.

Controls:

  • Row 1: ◀️ Previous, ▶️ Next, 🔢 Jump to page, 🔄 Refresh
  • Row 2: 📄 Details, ♻️ Load, 🗑️ Delete

Load and Delete accept a playlist number through a modal, then show a private confirmation popup. The confirmation can only be completed by the user who opened it.

image

Public browser for saved playlists in the current server.

Playlist details

Details are publicly visible within the current server and include:

  • Playlist code
  • Creator
  • Creation and expiry timestamps
  • Track count
  • Ordered track list
  • Direct ♻️ Load and 🗑️ Delete buttons

image

Paginated playlist details in restored playback order.

Modified behavior

  • /music queue remains the active-queue viewer.
  • Restore safely clears/replaces the current queue, history, pending crossfade state, and stale playback transitions.
  • Playback-generation protection prevents callbacks from stopped audio from modifying the restored queue.
  • Saved durations allow restored queues to display track lengths immediately.
  • Restored playlists do not persist volume, loop mode, crossfade state, controller state, or resolved stream URLs.
  • Large playlists remain lightweight: all track records are restored into the queue, but yt-dlp resolves tracks only as they are about to play.
  • Save and restore operations include DEBUG diagnostics for counts, queue state, confirmation flow, and resume offsets. Codes, track titles, URLs, and user IDs are not logged. Enable with LOG_LEVEL=DEBUG.

Storage

Saved playlists are persisted at:

data/playlists/playlists.json

The directory is created automatically and is ignored by Git.

Each record stores:

  • Restore code
  • Guild ID
  • Creator Discord ID
  • Creation and expiry timestamps
  • First-track resume position
  • Ordered track title, YouTube query/URL, and duration

Save, browse, and restore flow

flowchart LR
    User["Discord user"] -->|💾 Save| Player["Music player state"]
    Player --> Snapshot["Current + crossfade-next + queue"]
    Snapshot --> Filter["Keep YouTube tracks only"]
    Filter --> Store["Guild-scoped JSON datastore"]
    Store --> Code["Private xxxx-xxxx code"]

    User -->|/music restore list| Browser["Public server playlist browser"]
    Browser -->|📄| Details["Playlist details"]
    Browser -->|♻️ or 🗑️| Number["Playlist number modal"]
    Details -->|♻️ or 🗑️| Confirm["Private confirmation popup"]
    Number --> Confirm

    User -->|/music restore code| Validate["Validate code and guild"]
    Validate --> Confirm
    Confirm -->|Load| Consume["Consume playlist atomically"]
    Consume --> Queue["Replace active queue"]
    Queue --> Playback["Existing playback pipeline"]
    Playback --> Voice["Voice channel audio"]
Loading

Restore lifecycle

flowchart TD
    A["Save YouTube queue"] --> B["Validate non-empty saved track list"]
    B --> C["Persist guild-scoped playlist JSON"]
    C --> D["Send restore code privately"]

    E["Open /music restore list"] --> F["Public paginated server list"]
    F --> G["Choose details, load, or delete by number"]
    G --> H["Private confirmation popup"]

    I["Run /music restore code"] --> J["Validate code, expiry, and guild"]
    J --> H
    H -->|Load| K["Validate voice channel and consume playlist"]
    K --> L["Replace queue and invalidate stale callbacks"]
    L --> M["Hydrate and start first track"]
    M --> N["Resume saved position when available"]

    H -->|Delete| O["Atomically delete playlist"]
Loading

NickKhunapoj and others added 13 commits August 5, 2026 13:34
Add a 💾 Save control that stores the active queue as an owner-bound, expiring JSON playlist. Add /music queue restore <code> to consume a saved playlist, replace the current queue, reconnect when needed, and begin normal playback automatically.
Make saved playlist codes shareable while retaining creator metadata, consume codes after restoration, and move restoration to `/music restore <code>`. Restore `/music queue` as the queue-view command. Prevent stale playback callbacks and retries from draining a newly restored queue, so it can be saved again reliably.
Add /music restore list to browse saved playlists with Queue-style pagination, page navigation, refresh, and numbered detail lookup. Show each playlist’s code, creator, creation and expiry times, track count, and ordered tracks in a private paginated details view.
Publish /music restore list and playlist detail pages to the channel so everyone can view saved playlist metadata and use the pagination, refresh, and detail controls.
- Add JSON-backed, expiring saved playlists with shareable single-use codes.
- Save the active track, queued order, duration, and current playback position.
- Add `/music restore <code>` and public `/music restore list` with pagination, details, load controls, and confirmation dialogs.
- Restore playlists by replacing the active queue, reconnecting to voice when needed, and starting playback automatically.
- Add safe restore race handling, concise DEBUG logging, persistence validation, and focused tests.
- Update player controls with an icon-only save button and document the restore workflow.
- Add `🗑️` deletion controls to `/music restore list` and playlist details.
- Let users choose a playlist number to delete from the public restore list.
- Add private confirm/cancel popups for deletion, matching the restore confirmation flow.
- Disable confirmation controls immediately after deletion begins to prevent duplicate actions.
- Delete playlists atomically from JSON storage and handle unavailable, expired, malformed, and storage-error cases.
- Add deletion logs and focused tests for persistence and confirmation behavior.
- Scope saved playlists, list results, restores, loads, and deletions to their originating Discord guild.
- Keep playlist codes shareable only within the server where they were created.
- Persist and validate `guild_id` in the JSON playlist datastore.
- Exclude `/music local` attachment tracks from saved playlists.
- Preserve only YouTube tracks and their playback position when saving mixed queues.
- Reject local-only queues to prevent unusable restore codes.
- Add guild-isolation and local-track save coverage.
Co-Authored-By: Codex <267193182+codex@users.noreply.github.com>
@NickKhunapoj
NickKhunapoj marked this pull request as ready for review August 12, 2026 06:00
NickKhunapoj and others added 2 commits August 12, 2026 13:10
- Split music background work into dedicated worker pools:
  - yt-dlp metadata extraction
  - FFmpeg decoder prefill and cleanup
- Prevent metadata extraction from delaying decoder readiness and audio startup.
- Limit crossfade hydration to one active yt-dlp job at a time.
- Skip new crossfade preloads while a cancelled/stale extraction is still running, preventing queued worker buildup after skips or track changes.
- Replace the audio buffer’s 500 ms blocking read with immediate 20 ms silence frames during temporary underruns.
- Add rate-limited DEBUG diagnostics for sustained audio-buffer underruns.
- Add cleanup of both music worker pools when the cog unloads.
- Restore legacy saved-playlist claiming for records created before guild-scoped playlist metadata.
- Add focused coverage for stale crossfade hydration and non-blocking audio-buffer underruns.
- Verify with compile checks, diff checks, and 24 passing tests.

Co-Authored-By: Codex <267193182+codex@users.noreply.github.com>
…ossfade successors

- refresh the player controller after a crossfade fully completes so seek
  buttons are re-enabled once playback returns to a single active deck
- keep seeking disabled during the active overlap to avoid mutating two
  simultaneously mixed streams
- tighten seek readiness checks to require a connected, active player with a
  fully started current track
- recalculate pending crossfade preloads when loop mode changes from either
  player controls or `/music loop`
- support self-crossfade for single-track loop mode
- support queue-loop wraparound by preloading the first queued track when the
  current track is the last one
- preserve active crossfades during loop-mode changes; only obsolete pending
  preloads are discarded and rebuilt
- add coverage for post-crossfade seek readiness, seek blocking during an
  active mix, track-loop self-crossfade selection, queue-loop successor
  selection, and loop-change preload refresh

Co-Authored-By: Codex <267193182+codex@users.noreply.github.com>
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.

1 participant