Skip to content

feat(downloads): offline downloads for songs, albums and playlists - #2129

Open
gossip31 wants to merge 1 commit into
jeffvli:developmentfrom
gossip31:feature/offline-downloads
Open

feat(downloads): offline downloads for songs, albums and playlists#2129
gossip31 wants to merge 1 commit into
jeffvli:developmentfrom
gossip31:feature/offline-downloads

Conversation

@gossip31

@gossip31 gossip31 commented Jun 6, 2026

Copy link
Copy Markdown

Summary

Adds an offline download system to the Electron build. Songs, albums and playlists can be downloaded via the existing context menu or via a download button in album/playlist detail headers, and play back from disk when available.

Design

Main process owns a queue (3 concurrent) backed by a manifest under {userData}/downloads. Files stream to <serverId>/<songId>.<ext> via electron net.request; failed jobs retry up to twice on a 4 s backoff before surfacing as terminal failures.

A new feishin-local:// protocol scheme serves songs back to the renderer. The handler streams via createReadStream (no whole-file reads) and rejects any manifest entry whose resolved path escapes the downloads root. The scheme is registered with corsEnabled, secure, standard, stream, supportFetchAPI and bypassCSP; CSP allows media-src and connect-src for feishin-local:.

Player integration: useSongUrl prefers the local URL when a song is in the manifest. getSongUrl returns the absolute file path so the MPV engine can play it directly.

Renderer state: a zustand store mirrors the manifest. useDownloadBridge hydrates on mount, reconciles, and listens to downloads-progress events.

UI

  • Floating downloads panel (bottom-right) with per-song progress (% / bytes / speed / ETA) plus an aggregate progress bar tracked against a session total so the bar fills monotonically across a playlist.
  • DOWNLOAD_STATUS table column rendering a green check, a ring progress, a queued clock or a failed glyph per row with hover tooltips. Wired into song / playlist-song / queue-song / album-detail / full-screen / side-queue tables.
  • Album and playlist detail headers gain a download button that shows N/M progress and disables itself mid-download.
  • /downloads route lists downloaded songs grouped by album or artist with search, sort, multi-select delete, per-group delete, per-row hover delete and a top-N storage breakdown bar.
  • Toasts coalesce per session: one debounced start notice and one summary on drain (N songs downloaded / Download failed — title (+M) / N downloaded · M failed). (will retry) failures are suppressed.

Migration

Settings version v28 → v29 (guard is loose through v30) injects the new sidebar entry and the DOWNLOAD_STATUS column into existing persisted column layouts. Default albumDetail enabled-columns also includes the new column for fresh installs.

i18n

New keys live under common.*, setting.* and page.sidebar.* in en.json. Other locales fall back to English at runtime until translated.

Known limitations / follow-ups

  • Electron build only. The web build hides the UI via isElectron() and falls back to the existing OS-download path for single songs.
  • Subsonic credentials live in the download URL and are passed at enqueue time. Long-queued jobs will fail if credentials rotate before they start; the auto-retry covers transient cases, not credential rotation.
  • Grouping in the /downloads route is case-sensitive on album/artist strings.
  • Localisation only covers en.json; the i18next extractor config in this repo writes to a non-existent path, so keys were added manually.

Test plan

  • Right-click a song, an album and a playlist → download enqueues, panel shows progress, files land in {userData}/downloads/<serverId>/.
  • Disable the network → downloaded songs still play.
  • Restart the app → manifest re-hydrates, downloaded songs still play.
  • Cancel a mid-flight download → .part is removed, no manifest entry.
  • Delete a downloaded song from the /downloads page (single, group, multi-select) → file removed from disk, manifest updated, badge clears.
  • Change the downloads folder from settings → new downloads land in the new path.
  • Verify against Subsonic, Navidrome and Jellyfin backends.
  • Verify MPV playback as well as the web audio player.
  • Verify web build still loads and the row indicator stays hidden.

Adds an offline download system to the Electron build. Songs,
albums and playlists can be downloaded via the existing context
menu or via a download button in album/playlist headers, and play
from disk when available.

Main process owns a queue (max 3 concurrent) backed by a manifest
under {userData}/downloads. Files stream to <serverId>/<songId>.<ext>
via electron net.request; failed jobs retry twice on a 4s delay
before surfacing. A new feishin-local:// protocol serves songs back
to the renderer; it streams via createReadStream and rejects any
manifest entry whose relative path escapes the downloads root.

useSongUrl prefers the local URL when a song is in the manifest;
getSongUrl returns the absolute file path so the MPV engine can
play it directly. CSP allows media-src and connect-src for
feishin-local:.

The renderer mirrors the manifest in a zustand store. A floating
panel shows active and queued downloads with per-song progress
plus an aggregate progress bar tracked against a session total so
the bar fills monotonically across a playlist. The dedicated
DOWNLOAD_STATUS table column renders a green check, a ring
progress, a queued clock or a failed glyph per row with a hover
title carrying the size and relative date. The album and playlist
detail headers gain a download button that shows N/M progress and
disables itself mid-download. A new /downloads route lists what's
on disk grouped by album or artist with multi-select delete,
search, sort, and a top-N storage breakdown bar.

Toasts coalesce to one per session: a debounced start notice when
new songs are queued and a single summary on drain ("N songs
downloaded" / "Download failed — title (+M)" / "N downloaded · M
failed"). Failed events that carry the "will retry" marker are
suppressed so retries don't spam.

Settings migration v28 → v29 (idempotent through v30) injects the
new sidebar entry and the DOWNLOAD_STATUS column into existing
persisted layouts (song, playlist-song, queue-song, album-detail,
full-screen, side-queue).

Translation keys live under common.* / setting.* / page.sidebar.*
in en.json; other locales fall back to English until translated.
@vercel

vercel Bot commented Jun 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
feishin Ready Ready Preview, Comment Jun 6, 2026 11:10am

@gossip31

gossip31 commented Jun 6, 2026

Copy link
Copy Markdown
Author

Addresses #1066

@jeffvli

jeffvli commented Jun 18, 2026

Copy link
Copy Markdown
Owner

Thanks for the PR. This might take me a bit of time to review!

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