From 68cc46242c5f9f28bf21317aae7dd8ed535b81be Mon Sep 17 00:00:00 2001 From: Katelyn Dickey Date: Thu, 12 Mar 2026 01:22:26 -0400 Subject: [PATCH 1/8] feat(share): add a download dialog to share page + ux fixes Adds a dialog that allows a choice to download either the current track, or all tracks in the list. If there is only one track, it will simply download the track itself rather than a zip with a single file. Fixes an issue where long downloads would show no indication of progress. (The player's default is to use a background XHR with no visual feedback) Uses an invisible link to initiate a browser-handled download instead, which should avoid rage clicks that would otherwise lead to multiple simultaneous downloads (and possibly transcodes). Also did a little bit of housekeeping: - Use the share description as the page title - Set the json share description to the content field, if the description is empty (to match the existing logic elsewhere) - Also some mild refactoring to allow the share page to access the (default) theme, and i18n. (This does slightly change the appearance) - Side note: the withTheme HOC is definitely the wrong place to do locale loading things. But I am not too familiar with react-admin, so i just kept the related code together. --- server/public/handle_streams.go | 19 ++++++ server/serve_index.go | 3 + ui/index.html | 2 +- ui/src/App.jsx | 9 ++- ui/src/i18n/en.json | 8 ++- ui/src/layout/Login.jsx | 26 ++------ ui/src/share/SharePlayer.jsx | 107 +++++++++++++++++++++++++++----- ui/src/utils/urls.js | 6 ++ ui/src/utils/withTheme.jsx | 31 +++++++++ 9 files changed, 172 insertions(+), 39 deletions(-) create mode 100644 ui/src/utils/withTheme.jsx diff --git a/server/public/handle_streams.go b/server/public/handle_streams.go index c7b8a4d4f..01552464e 100644 --- a/server/public/handle_streams.go +++ b/server/public/handle_streams.go @@ -2,10 +2,13 @@ package public import ( "errors" + "fmt" "net/http" "strconv" + "strings" "time" + "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/core/auth" streampkg "github.com/navidrome/navidrome/core/stream" "github.com/navidrome/navidrome/log" @@ -72,6 +75,10 @@ func (pub *Router) handleStream(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Content-Duration", strconv.FormatFloat(float64(stream.Duration()), 'G', -1, 32)) + if conf.Server.EnableDownloads && p.BoolOr("download", false) { + w.Header().Set("Content-Disposition", "attachment; filename=\""+downloadFilename(mf, info.format)+"\"") + } + n, err := stream.Serve(ctx, w, r) if err != nil || n == 0 { http.Error(w, "internal error", http.StatusInternalServerError) @@ -100,3 +107,15 @@ func decodeStreamInfo(tokenString string) (shareTrackInfo, error) { shareID: c.ShareID, }, nil } + +func sanitizeName(target string) string { + return strings.ReplaceAll(target, "/", "_") +} + +func downloadFilename(mf *model.MediaFile, format string) string { + ext := mf.Suffix + if format != "" && format != "raw" { + ext = format + } + return fmt.Sprintf("%s - %s.%s", sanitizeName(mf.Artist), sanitizeName(mf.Title), ext) +} diff --git a/server/serve_index.go b/server/serve_index.go index 13fa4a9ce..e0a62a997 100644 --- a/server/serve_index.go +++ b/server/serve_index.go @@ -160,6 +160,9 @@ func addShareData(r *http.Request, data map[string]any, shareInfo *model.Share) Description: shareInfo.Description, Downloadable: shareInfo.Downloadable, } + if sd.Description == "" { + sd.Description = shareInfo.Contents + } sd.Tracks = slice.Map(shareInfo.Tracks, func(mf model.MediaFile) shareTrack { return shareTrack{ ID: mf.ID, diff --git a/ui/index.html b/ui/index.html index 827751856..55a6fc572 100644 --- a/ui/index.html +++ b/ui/index.html @@ -26,7 +26,7 @@ - Navidrome + {{ if .ShareDescription }}{{ .ShareDescription }} | Navidrome{{ else }}Navidrome{{ end }}