Repository navigation
Playlists, streaming, a queue, autoplay and light/dark themes for the desktop player - #17
Merged
amiralibg merged 10 commits intoNov 2, 2026
Conversation
Playlists are stored as catalog metadata in one JSON file beside lyrics.db (PLAYLISTS_PATH), never as library ids, which are hashes of a path on this machine. Which rows are on disk is matched against the library scan on every read, so a song downloaded or copied in later shows up without the playlist being told. "Download all" queues exactly the rows that matched nothing, as one job whose track ids are the rows' own. Exports land in the music folder's Playlists/ as .unstream.json (what import reads) or .m3u8 for other players; import takes the file's text, so no path ever comes in from a request. /api/stream plays a song that isn't on disk: it picks an upload the way the downloader does and relays the audio through the backend, so it follows the connection setting and YouTube's per-URL headers. Upstream ranges are capped at 4 MB, since googlevideo throttles open-ended requests; resolved sources are cached until shortly before their URL expires. Both are desktop-only unless UNSTREAM_PLAYLISTS / UNSTREAM_STREAMING say otherwise: a hosted instance would share one playlists file between every visitor and relay every stream over its own bandwidth.
- Playlists tab (Alt/Cmd+4; Settings moves to 5) with the person's playlists also listed in the sidebar: create, rename, reorder by drag, remove songs, delete, play, shuffle, download what's missing, export, and import a file from another device. - Any song can be played in full: album and playlist rows get a play button that streams on the desktop (the web keeps its previews), and "Save as playlist" turns an opened album or playlist into your own. - A song menu on every row: play next, add to queue, add to a playlist or a new one. - A queue panel: what plays next in play order, drag to reorder, remove, jump, clear. - The player warms up the next streamed song while this one plays, and restores the last queue and position on launch, paused. - Copy in English and Farsi, with the new backend errors worded in both.
Why entries are songs and not files, why they live in a file and not in the webview, why streams are relayed in chunks, and why both stay off on a hosted instance. CONTEXT.md gains the Farsi terms the new copy uses.
Autoplay, on by default and switchable from the queue panel and Settings. When one song is left, the player asks /api/recommend for songs like the one playing and appends ten it hasn't queued or played this session, marked as autoplay's. Seeding from whatever is current lets a long session drift like a radio rather than circle one artist. Suggestions come from Deezer — the artist's radio, topped up from related artists, then the artist's own top tracks — and, when that finds too little, from YouTube's Mix for the song. The fallback is what makes it work for Persian artists, whom Deezer often files under an empty Persian-script entry. Only the mix's first entries are read; paging a whole one took over a minute. Songs already in the library play from the file. Songs the person queues go ahead of autoplay's, repeat never triggers it, and switching it off removes what it queued straight away.
The desktop pins YouTube's player clients to tv,web. They no longer work: tv answers "The page needs to be reloaded" and web offers no audio, so every YouTube extraction failed, and with it streaming and downloads. Each failed song then fell through to SoundCloud, whose catalogue is mostly HLS and has no single-file stream for the relay, so the person saw "couldn't find this song" for songs YouTube has. ytdlp.run() keeps the pin as the first try and retries once with yt-dlp's own client choice when YouTube refuses it, so neither going stale breaks anything on its own. Streaming and downloads both go through it. Streams from uploads with only segmented formats are now fetched whole into a small cache and served from disk with ranges, instead of failing.
Suggestions are shuffled before they are cut to size, so the song the test looked for was sometimes cut, about one run in three. The filtering is now checked on an uncut list.
Theme is now two choices in Settings (and in the web header and mobile settings sheet): the mode — light, dark, or match the system — and the palette — the original lime, which stays the default, or blue. Nothing changes for anyone who does neither. Every colour is a token, and each palette and mode is another set of values for the same tokens, selected by data-palette and data-theme on <html>. Components were already token-based except for washes mixed from raw white and black and a dozen dark surfaces spelled as hex; those now use fg, well, shell and panel, so a white wash doesn't vanish on a white page. The accent keeps its lime-* name in every palette. The light accents are deeper than the dark ones — olive and royal blue — because the accent is also text, and lime or sky blue on white fails contrast. The theme is applied before the first render. The splash and the manifest keep the original palette: neither can read a setting.
A fresh stream paid ~18s retrying the pinned tv,web clients that YouTube now refuses before falling back to the default. Once the default has worked where the pin was refused, skip the pin for the rest of the process. The player warmed only the next song, after a 4s delay. Warm the next two immediately so a slow resolve finishes before the current song ends. The frontend Dockerfile accepts NPM_CONFIG_REGISTRY/timeout/retries build args for slow or filtered networks.
alimohammad0816
force-pushed
the
feature/playlists-and-player
branch
from
October 17, 2026 11:37
3bbb5e2 to
066de59
Compare
A streamed song has no catalog page to hand to /api/download, so the player bar had no way to save it. POST /api/download/song starts a job from the song's metadata, the same way a playlist download does, and the bar shows queued/done state from the downloads store.
Owner
|
Built this locally on my Mac and the packaged app runs fine, streaming included, so that covers the one thing you couldn't test. Full-length playback and playlists are a big upgrade for the desktop app. Thanks for this. Two small things for a follow-up, neither blocks the merge. Exporting a playlist silently overwrites a file with the same name in Merging now, it'll go out in the next release. |
Merged
Contributor
Author
|
Thanks for testing it and for merging the PR! I'm glad it's useful. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Playlists, full-length streaming and a proper queue for the desktop player
The desktop player could only play the on-disk library. This turns it into a full music player:
.unstream.json(lossless, imports on another device and re-matches files already there) or.m3u8for any other player. Exports go into the music folder'sPlaylists/and the toast offers "Reveal in folder".data-palette/data-themeon<html>, so no component knows which theme it's in.tv,web, which now fail (tv: "The page needs to be reloaded",web: no audio).ytdlp.run()keeps the pin as the first try and retries once with yt-dlp's own client choice. Segmented-only (HLS) uploads are fetched into a small cache for streaming instead of failing.How it's built (details in
docs/DESIGN.md→ "Playlists, streaming and the queue")PLAYLISTS_PATH, besidelyrics.db; the Tauri launcher sets it), written atomically, never overwritten if unparseable — not inlocalStorage, whose bucket moves with the sidecar port./api/streamrelays audio through the backend so it follows the connection/VPN setting and YouTube's per-URL headers. Upstream ranges are capped at 4 MB (googlevideo throttles open-ended requests); progressive m4a/mp3 is preferred for WebKit; resolved sources are cached until shortly beforeexpire.UNSTREAM_PLAYLISTS/UNSTREAM_STREAMINGto opt in): a hosted instance would otherwise share one playlists file between visitors and relay every stream over its own bandwidth.CONTEXT.mdgains صف پخش / پخش / پخش خودکار / تم / رنگ / خروجی / وارد کردن.docs/DESIGN.mdgains a section on the colour system.Testing
backend: 26 new tests (test_playlists.py,test_stream.py,test_recommend.py) — storage, dedupe, reorder, untrusted-input cleaning, library matching, JSON and M3U round trips, download of missing songs, range bounding, source cache and re-resolve on a dead URL, suggestion seeding, top-up and the YouTube fallback, the player-client retry and cached HLS ranges. Full suite: 183 passed.frontend:npm run lint,format:check,buildclean.?desktop-preview): streamed a Deezer album track end to end, saved the album as a playlist, added a library file to it, exported both formats, re-imported the JSON (local file re-matched), downloaded a playlist's missing song and watched the row flip to on disk, played from the playlist, queue panel, session restore after reload, autoplay carrying on from a finished queue into a streamed suggestion and clearing when switched off, all four palette × mode combinations, both RTL (fa) and LTR (en). The client-pin fix was reproduced and verified against the packaged sidecar with the launcher's exact environment.PLAYLISTS_PATHenv var inspawn_backend.Shortcut note: Playlists takes ⌘/Alt+4, so Settings moves to ⌘/Alt+5.