diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go
index def3d830d..3e2122803 100644
--- a/cmd/wire_gen.go
+++ b/cmd/wire_gen.go
@@ -50,7 +50,7 @@ func CreateSubsonicAPIRouter() *subsonic.Router {
 	transcoderTranscoder := transcoder.New()
 	transcodingCache := core.GetTranscodingCache()
 	mediaStreamer := core.NewMediaStreamer(dataStore, transcoderTranscoder, transcodingCache)
-	archiver := core.NewArchiver(dataStore)
+	archiver := core.NewArchiver(mediaStreamer, dataStore)
 	players := core.NewPlayers(dataStore)
 	agentsAgents := agents.New(dataStore)
 	externalMetadata := core.NewExternalMetadata(dataStore, agentsAgents)
diff --git a/conf/configuration.go b/conf/configuration.go
index 0690bc469..a25a4fffb 100644
--- a/conf/configuration.go
+++ b/conf/configuration.go
@@ -34,6 +34,7 @@ type configOptions struct {
 	ImageCacheSize          string
 	AutoImportPlaylists     bool
 	PlaylistsPath           string
+	AutoTranscodeDownload   bool
 
 	SearchFullString       bool
 	RecentlyAddedByModTime bool
@@ -228,6 +229,7 @@ func init() {
 	viper.SetDefault("playlistspath", consts.DefaultPlaylistsPath)
 	viper.SetDefault("enabledownloads", true)
 	viper.SetDefault("enableexternalservices", true)
+	viper.SetDefault("autotranscodedownload", false)
 
 	// Config options only valid for file/env configuration
 	viper.SetDefault("searchfullstring", false)
diff --git a/core/archiver.go b/core/archiver.go
index 520db2733..a66478099 100644
--- a/core/archiver.go
+++ b/core/archiver.go
@@ -7,6 +7,7 @@ import (
 	"io"
 	"os"
 	"path/filepath"
+	"strings"
 
 	"github.com/Masterminds/squirrel"
 	"github.com/navidrome/navidrome/log"
@@ -14,22 +15,23 @@ import (
 )
 
 type Archiver interface {
-	ZipAlbum(ctx context.Context, id string, w io.Writer) error
-	ZipArtist(ctx context.Context, id string, w io.Writer) error
-	ZipPlaylist(ctx context.Context, id string, w io.Writer) error
+	ZipAlbum(ctx context.Context, id string, format string, bitrate int, w io.Writer) error
+	ZipArtist(ctx context.Context, id string, format string, bitrate int, w io.Writer) error
+	ZipPlaylist(ctx context.Context, id string, format string, bitrate int, w io.Writer) error
 }
 
-func NewArchiver(ds model.DataStore) Archiver {
-	return &archiver{ds: ds}
+func NewArchiver(ms MediaStreamer, ds model.DataStore) Archiver {
+	return &archiver{ds: ds, ms: ms}
 }
 
 type archiver struct {
 	ds model.DataStore
+	ms MediaStreamer
 }
 
-type createHeader func(idx int, mf model.MediaFile) *zip.FileHeader
+type createHeader func(idx int, mf model.MediaFile, format string) *zip.FileHeader
 
-func (a *archiver) ZipAlbum(ctx context.Context, id string, out io.Writer) error {
+func (a *archiver) ZipAlbum(ctx context.Context, id string, format string, bitrate int, out io.Writer) error {
 	mfs, err := a.ds.MediaFile(ctx).GetAll(model.QueryOptions{
 		Filters: squirrel.Eq{"album_id": id},
 		Sort:    "album",
@@ -38,10 +40,10 @@ func (a *archiver) ZipAlbum(ctx context.Context, id string, out io.Writer) error
 		log.Error(ctx, "Error loading mediafiles from album", "id", id, err)
 		return err
 	}
-	return a.zipTracks(ctx, id, out, mfs, a.createHeader)
+	return a.zipTracks(ctx, id, format, bitrate, out, mfs, a.createHeader)
 }
 
-func (a *archiver) ZipArtist(ctx context.Context, id string, out io.Writer) error {
+func (a *archiver) ZipArtist(ctx context.Context, id string, format string, bitrate int, out io.Writer) error {
 	mfs, err := a.ds.MediaFile(ctx).GetAll(model.QueryOptions{
 		Sort:    "album",
 		Filters: squirrel.Eq{"album_artist_id": id},
@@ -50,23 +52,25 @@ func (a *archiver) ZipArtist(ctx context.Context, id string, out io.Writer) erro
 		log.Error(ctx, "Error loading mediafiles from artist", "id", id, err)
 		return err
 	}
-	return a.zipTracks(ctx, id, out, mfs, a.createHeader)
+	return a.zipTracks(ctx, id, format, bitrate, out, mfs, a.createHeader)
 }
 
-func (a *archiver) ZipPlaylist(ctx context.Context, id string, out io.Writer) error {
+func (a *archiver) ZipPlaylist(ctx context.Context, id string, format string, bitrate int, out io.Writer) error {
 	pls, err := a.ds.Playlist(ctx).GetWithTracks(id)
 	if err != nil {
 		log.Error(ctx, "Error loading mediafiles from playlist", "id", id, err)
 		return err
 	}
-	return a.zipTracks(ctx, id, out, pls.MediaFiles(), a.createPlaylistHeader)
+	return a.zipTracks(ctx, id, format, bitrate, out, pls.MediaFiles(), a.createPlaylistHeader)
 }
 
-func (a *archiver) zipTracks(ctx context.Context, id string, out io.Writer, mfs model.MediaFiles, ch createHeader) error {
+func (a *archiver) zipTracks(ctx context.Context, id string, format string, bitrate int, out io.Writer, mfs model.MediaFiles, ch createHeader) error {
 	z := zip.NewWriter(out)
+
 	for idx, mf := range mfs {
-		_ = a.addFileToZip(ctx, z, mf, ch(idx, mf))
+		_ = a.addFileToZip(ctx, z, mf, format, bitrate, ch(idx, mf, format))
 	}
+
 	err := z.Close()
 	if err != nil {
 		log.Error(ctx, "Error closing zip file", "id", id, err)
@@ -74,8 +78,13 @@ func (a *archiver) zipTracks(ctx context.Context, id string, out io.Writer, mfs
 	return err
 }
 
-func (a *archiver) createHeader(idx int, mf model.MediaFile) *zip.FileHeader {
+func (a *archiver) createHeader(idx int, mf model.MediaFile, format string) *zip.FileHeader {
 	_, file := filepath.Split(mf.Path)
+
+	if format != "raw" {
+		file = strings.Replace(file, "."+mf.Suffix, "."+format, 1)
+	}
+
 	return &zip.FileHeader{
 		Name:     fmt.Sprintf("%s/%s", mf.Album, file),
 		Modified: mf.UpdatedAt,
@@ -83,8 +92,13 @@ func (a *archiver) createHeader(idx int, mf model.MediaFile) *zip.FileHeader {
 	}
 }
 
-func (a *archiver) createPlaylistHeader(idx int, mf model.MediaFile) *zip.FileHeader {
+func (a *archiver) createPlaylistHeader(idx int, mf model.MediaFile, format string) *zip.FileHeader {
 	_, file := filepath.Split(mf.Path)
+
+	if format != "raw" {
+		file = strings.Replace(file, "."+mf.Suffix, "."+format, 1)
+	}
+
 	return &zip.FileHeader{
 		Name:     fmt.Sprintf("%d - %s - %s", idx+1, mf.AlbumArtist, file),
 		Modified: mf.UpdatedAt,
@@ -92,22 +106,46 @@ func (a *archiver) createPlaylistHeader(idx int, mf model.MediaFile) *zip.FileHe
 	}
 }
 
-func (a *archiver) addFileToZip(ctx context.Context, z *zip.Writer, mf model.MediaFile, zh *zip.FileHeader) error {
+func (a *archiver) addFileToZip(ctx context.Context, z *zip.Writer, mf model.MediaFile, format string, bitrate int, zh *zip.FileHeader) error {
 	w, err := z.CreateHeader(zh)
 	if err != nil {
 		log.Error(ctx, "Error creating zip entry", "file", mf.Path, err)
 		return err
 	}
-	f, err := os.Open(mf.Path)
-	defer func() { _ = f.Close() }()
-	if err != nil {
-		log.Error(ctx, "Error opening file for zipping", "file", mf.Path, err)
-		return err
+
+	if format != "raw" {
+		stream, err := a.ms.DoStream(ctx, &mf, format, bitrate)
+
+		if err != nil {
+			return err
+		}
+
+		defer func() {
+			if err := stream.Close(); err != nil && log.CurrentLevel() >= log.LevelDebug {
+				log.Error("Error closing stream", "id", mf.ID, "file", stream.Name(), err)
+			}
+		}()
+
+		_, err = io.Copy(w, stream)
+
+		if err != nil {
+			log.Error(ctx, "Error zipping file", "file", mf.Path, err)
+			return err
+		}
+
+		return nil
+	} else {
+		f, err := os.Open(mf.Path)
+		defer func() { _ = f.Close() }()
+		if err != nil {
+			log.Error(ctx, "Error opening file for zipping", "file", mf.Path, err)
+			return err
+		}
+		_, err = io.Copy(w, f)
+		if err != nil {
+			log.Error(ctx, "Error zipping file", "file", mf.Path, err)
+			return err
+		}
+		return nil
 	}
-	_, err = io.Copy(w, f)
-	if err != nil {
-		log.Error(ctx, "Error zipping file", "file", mf.Path, err)
-		return err
-	}
-	return nil
 }
diff --git a/core/media_streamer.go b/core/media_streamer.go
index c35afb734..85038dc97 100644
--- a/core/media_streamer.go
+++ b/core/media_streamer.go
@@ -20,6 +20,7 @@ import (
 
 type MediaStreamer interface {
 	NewStream(ctx context.Context, id string, reqFormat string, reqBitRate int) (*Stream, error)
+	DoStream(ctx context.Context, mf *model.MediaFile, reqFormat string, reqBitRate int) (*Stream, error)
 }
 
 type TranscodingCache cache.FileCache
@@ -51,6 +52,10 @@ func (ms *mediaStreamer) NewStream(ctx context.Context, id string, reqFormat str
 		return nil, err
 	}
 
+	return ms.DoStream(ctx, mf, reqFormat, reqBitRate)
+}
+
+func (ms *mediaStreamer) DoStream(ctx context.Context, mf *model.MediaFile, reqFormat string, reqBitRate int) (*Stream, error) {
 	var format string
 	var bitRate int
 	var cached bool
diff --git a/server/subsonic/stream.go b/server/subsonic/stream.go
index 6f3601cfd..f83b7a2bb 100644
--- a/server/subsonic/stream.go
+++ b/server/subsonic/stream.go
@@ -1,6 +1,7 @@
 package subsonic
 
 import (
+	"context"
 	"fmt"
 	"io"
 	"net/http"
@@ -16,31 +17,7 @@ import (
 	"github.com/navidrome/navidrome/utils"
 )
 
-func (api *Router) Stream(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
-	ctx := r.Context()
-	id, err := requiredParamString(r, "id")
-	if err != nil {
-		return nil, err
-	}
-	maxBitRate := utils.ParamInt(r, "maxBitRate", 0)
-	format := utils.ParamString(r, "format")
-	estimateContentLength := utils.ParamBool(r, "estimateContentLength", false)
-
-	stream, err := api.streamer.NewStream(ctx, id, format, maxBitRate)
-	if err != nil {
-		return nil, err
-	}
-
-	// Make sure the stream will be closed at the end, to avoid leakage
-	defer func() {
-		if err := stream.Close(); err != nil && log.CurrentLevel() >= log.LevelDebug {
-			log.Error(r.Context(), "Error closing stream", "id", id, "file", stream.Name(), err)
-		}
-	}()
-
-	w.Header().Set("X-Content-Type-Options", "nosniff")
-	w.Header().Set("X-Content-Duration", strconv.FormatFloat(float64(stream.Duration()), 'G', -1, 32))
-
+func (api *Router) ServeStream(ctx context.Context, w http.ResponseWriter, r *http.Request, stream *core.Stream, id string) {
 	if stream.Seekable() {
 		http.ServeContent(w, r, stream.Name(), stream.ModTime(), stream)
 	} else {
@@ -48,6 +25,8 @@ func (api *Router) Stream(w http.ResponseWriter, r *http.Request) (*responses.Su
 		w.Header().Set("Accept-Ranges", "none")
 		w.Header().Set("Content-Type", stream.ContentType())
 
+		estimateContentLength := utils.ParamBool(r, "estimateContentLength", false)
+
 		// if Client requests the estimated content-length, send it
 		if estimateContentLength {
 			length := strconv.Itoa(stream.EstimatedContentLength())
@@ -68,6 +47,33 @@ func (api *Router) Stream(w http.ResponseWriter, r *http.Request) (*responses.Su
 			}
 		}
 	}
+}
+
+func (api *Router) Stream(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
+	ctx := r.Context()
+	id, err := requiredParamString(r, "id")
+	if err != nil {
+		return nil, err
+	}
+	maxBitRate := utils.ParamInt(r, "maxBitRate", 0)
+	format := utils.ParamString(r, "format")
+
+	stream, err := api.streamer.NewStream(ctx, id, format, maxBitRate)
+	if err != nil {
+		return nil, err
+	}
+
+	// Make sure the stream will be closed at the end, to avoid leakage
+	defer func() {
+		if err := stream.Close(); err != nil && log.CurrentLevel() >= log.LevelDebug {
+			log.Error("Error closing stream", "id", id, "file", stream.Name(), err)
+		}
+	}()
+
+	w.Header().Set("X-Content-Type-Options", "nosniff")
+	w.Header().Set("X-Content-Duration", strconv.FormatFloat(float64(stream.Duration()), 'G', -1, 32))
+
+	api.ServeStream(ctx, w, r, stream, id)
 
 	return nil, nil
 }
@@ -90,6 +96,27 @@ func (api *Router) Download(w http.ResponseWriter, r *http.Request) (*responses.
 		return nil, err
 	}
 
+	maxBitRate := utils.ParamInt(r, "bitrate", 0)
+	format := utils.ParamString(r, "format")
+
+	if format == "" {
+		if conf.Server.AutoTranscodeDownload {
+			// if we are not provided a format, see if we have requested transcoding for this client
+			// This must be enabled via a config option. For the UI, we are always given an option.
+			// This will impact other clients which do not use the UI
+			transcoding, ok := request.TranscodingFrom(ctx)
+
+			if !ok {
+				format = "raw"
+			} else {
+				format = transcoding.TargetFormat
+				maxBitRate = transcoding.DefaultBitRate
+			}
+		} else {
+			format = "raw"
+		}
+	}
+
 	setHeaders := func(name string) {
 		name = strings.ReplaceAll(name, ",", "_")
 		disposition := fmt.Sprintf("attachment; filename=\"%s.zip\"", name)
@@ -99,24 +126,26 @@ func (api *Router) Download(w http.ResponseWriter, r *http.Request) (*responses.
 
 	switch v := entity.(type) {
 	case *model.MediaFile:
-		stream, err := api.streamer.NewStream(ctx, id, "raw", 0)
+		stream, err := api.streamer.NewStream(ctx, id, format, maxBitRate)
+
 		if err != nil {
 			return nil, err
 		}
 
 		disposition := fmt.Sprintf("attachment; filename=\"%s\"", stream.Name())
 		w.Header().Set("Content-Disposition", disposition)
-		http.ServeContent(w, r, stream.Name(), stream.ModTime(), stream)
+
+		api.ServeStream(ctx, w, r, stream, id)
 		return nil, nil
 	case *model.Album:
 		setHeaders(v.Name)
-		err = api.archiver.ZipAlbum(ctx, id, w)
+		err = api.archiver.ZipAlbum(ctx, id, format, maxBitRate, w)
 	case *model.Artist:
 		setHeaders(v.Name)
-		err = api.archiver.ZipArtist(ctx, id, w)
+		err = api.archiver.ZipArtist(ctx, id, format, maxBitRate, w)
 	case *model.Playlist:
 		setHeaders(v.Name)
-		err = api.archiver.ZipPlaylist(ctx, id, w)
+		err = api.archiver.ZipPlaylist(ctx, id, format, maxBitRate, w)
 	default:
 		err = model.ErrNotFound
 	}
diff --git a/ui/src/App.js b/ui/src/App.js
index 5f2791010..9281964f9 100644
--- a/ui/src/App.js
+++ b/ui/src/App.js
@@ -25,6 +25,7 @@ import {
   albumViewReducer,
   activityReducer,
   settingsReducer,
+  downloadMenuDialogReducer,
 } from './reducers'
 import createAdminStore from './store/createAdminStore'
 import { i18nProvider } from './i18n'
@@ -52,6 +53,7 @@ const adminStore = createAdminStore({
     albumView: albumViewReducer,
     theme: themeReducer,
     addToPlaylistDialog: addToPlaylistDialogReducer,
+    downloadMenuDialog: downloadMenuDialogReducer,
     expandInfoDialog: expandInfoDialogReducer,
     listenBrainzTokenDialog: listenBrainzTokenDialogReducer,
     activity: activityReducer,
diff --git a/ui/src/actions/dialogs.js b/ui/src/actions/dialogs.js
index 8feb44755..d3764b852 100644
--- a/ui/src/actions/dialogs.js
+++ b/ui/src/actions/dialogs.js
@@ -1,11 +1,17 @@
 export const ADD_TO_PLAYLIST_OPEN = 'ADD_TO_PLAYLIST_OPEN'
 export const ADD_TO_PLAYLIST_CLOSE = 'ADD_TO_PLAYLIST_CLOSE'
+export const DOWNLOAD_MENU_OPEN = 'DOWNLOAD_MENU_OPEN'
+export const DOWNLOAD_MENU_CLOSE = 'DOWNLOAD_MENU_CLOSE'
 export const DUPLICATE_SONG_WARNING_OPEN = 'DUPLICATE_SONG_WARNING_OPEN'
 export const DUPLICATE_SONG_WARNING_CLOSE = 'DUPLICATE_SONG_WARNING_CLOSE'
 export const EXTENDED_INFO_OPEN = 'EXTENDED_INFO_OPEN'
 export const EXTENDED_INFO_CLOSE = 'EXTENDED_INFO_CLOSE'
 export const LISTENBRAINZ_TOKEN_OPEN = 'LISTENBRAINZ_TOKEN_OPEN'
 export const LISTENBRAINZ_TOKEN_CLOSE = 'LISTENBRAINZ_TOKEN_CLOSE'
+export const DOWNLOAD_MENU_ALBUM = 'album'
+export const DOWNLOAD_MENU_ARTIST = 'artist'
+export const DOWNLOAD_MENU_PLAY = 'playlist'
+export const DOWNLOAD_MENU_SONG = 'song'
 
 export const openAddToPlaylist = ({ selectedIds, onSuccess }) => ({
   type: ADD_TO_PLAYLIST_OPEN,
@@ -17,6 +23,18 @@ export const closeAddToPlaylist = () => ({
   type: ADD_TO_PLAYLIST_CLOSE,
 })
 
+export const openDownloadMenu = (record, recordType) => {
+  return {
+    type: DOWNLOAD_MENU_OPEN,
+    recordType,
+    record,
+  }
+}
+
+export const closeDownloadMenu = () => ({
+  type: DOWNLOAD_MENU_CLOSE,
+})
+
 export const openDuplicateSongWarning = (duplicateIds) => ({
   type: DUPLICATE_SONG_WARNING_OPEN,
   duplicateIds,
diff --git a/ui/src/album/AlbumActions.js b/ui/src/album/AlbumActions.js
index 58dc25b7b..060672ea7 100644
--- a/ui/src/album/AlbumActions.js
+++ b/ui/src/album/AlbumActions.js
@@ -18,8 +18,9 @@ import {
   playTracks,
   shuffleTracks,
   openAddToPlaylist,
+  openDownloadMenu,
+  DOWNLOAD_MENU_ALBUM,
 } from '../actions'
-import subsonic from '../subsonic'
 import { formatBytes } from '../utils'
 import { useMediaQuery, makeStyles } from '@material-ui/core'
 import config from '../config'
@@ -64,8 +65,8 @@ const AlbumActions = ({
   }, [dispatch, ids])
 
   const handleDownload = React.useCallback(() => {
-    subsonic.download(record.id)
-  }, [record])
+    dispatch(openDownloadMenu(record, DOWNLOAD_MENU_ALBUM))
+  }, [dispatch, record])
 
   return (
     <TopToolbar className={className} {...sanitizeListRestProps(rest)}>
diff --git a/ui/src/album/AlbumList.js b/ui/src/album/AlbumList.js
index be9bea98a..fe3175190 100644
--- a/ui/src/album/AlbumList.js
+++ b/ui/src/album/AlbumList.js
@@ -28,6 +28,7 @@ import { AddToPlaylistDialog } from '../dialogs'
 import albumLists, { defaultAlbumList } from './albumLists'
 import config from '../config'
 import AlbumInfo from './AlbumInfo'
+import DownloadMenuDialog from '../dialogs/DownloadMenuDialog'
 import ExpandInfoDialog from '../dialogs/ExpandInfoDialog'
 
 const AlbumFilter = (props) => {
@@ -132,6 +133,7 @@ const AlbumList = (props) => {
         )}
       </List>
       <AddToPlaylistDialog />
+      <DownloadMenuDialog />
       <ExpandInfoDialog content={<AlbumInfo />} />
     </>
   )
diff --git a/ui/src/album/AlbumSongs.js b/ui/src/album/AlbumSongs.js
index 9a2453279..0e2a57fd4 100644
--- a/ui/src/album/AlbumSongs.js
+++ b/ui/src/album/AlbumSongs.js
@@ -29,6 +29,7 @@ import {
 } from '../common'
 import { AddToPlaylistDialog } from '../dialogs'
 import config from '../config'
+import DownloadMenuDialog from '../dialogs/DownloadMenuDialog'
 import ExpandInfoDialog from '../dialogs/ExpandInfoDialog'
 
 const useStyles = makeStyles(
@@ -187,6 +188,7 @@ const AlbumSongs = (props) => {
         </Card>
       </div>
       <AddToPlaylistDialog />
+      <DownloadMenuDialog />
       <ExpandInfoDialog content={<SongInfo />} />
     </>
   )
diff --git a/ui/src/artist/ArtistList.js b/ui/src/artist/ArtistList.js
index c64feac23..427d4cbe1 100644
--- a/ui/src/artist/ArtistList.js
+++ b/ui/src/artist/ArtistList.js
@@ -31,6 +31,7 @@ import {
 import config from '../config'
 import ArtistListActions from './ArtistListActions'
 import { DraggableTypes } from '../consts'
+import DownloadMenuDialog from '../dialogs/DownloadMenuDialog'
 
 const useStyles = makeStyles({
   contextHeader: {
@@ -173,6 +174,7 @@ const ArtistList = (props) => {
         <ArtistListView {...props} />
       </List>
       <AddToPlaylistDialog />
+      <DownloadMenuDialog />
     </>
   )
 }
diff --git a/ui/src/common/ContextMenus.js b/ui/src/common/ContextMenus.js
index ae113c098..4514e7683 100644
--- a/ui/src/common/ContextMenus.js
+++ b/ui/src/common/ContextMenus.js
@@ -14,9 +14,11 @@ import {
   playTracks,
   shuffleTracks,
   openAddToPlaylist,
+  openDownloadMenu,
   openExtendedInfoDialog,
+  DOWNLOAD_MENU_ALBUM,
+  DOWNLOAD_MENU_ARTIST,
 } from '../actions'
-import subsonic from '../subsonic'
 import { LoveButton } from './LoveButton'
 import config from '../config'
 import { formatBytes } from '../utils'
@@ -83,7 +85,16 @@ const ContextMenu = ({
       label: `${translate('resources.album.actions.download')} (${formatBytes(
         record.size
       )})`,
-      action: () => subsonic.download(record.id),
+      action: () => {
+        dispatch(
+          openDownloadMenu(
+            record,
+            record.duration !== undefined
+              ? DOWNLOAD_MENU_ALBUM
+              : DOWNLOAD_MENU_ARTIST
+          )
+        )
+      },
     },
     ...(!hideInfo && {
       info: {
diff --git a/ui/src/common/SongContextMenu.js b/ui/src/common/SongContextMenu.js
index 6df06bf08..776718086 100644
--- a/ui/src/common/SongContextMenu.js
+++ b/ui/src/common/SongContextMenu.js
@@ -12,8 +12,9 @@ import {
   setTrack,
   openAddToPlaylist,
   openExtendedInfoDialog,
+  openDownloadMenu,
+  DOWNLOAD_MENU_SONG,
 } from '../actions'
-import subsonic from '../subsonic'
 import { LoveButton } from './LoveButton'
 import config from '../config'
 import { formatBytes } from '../utils'
@@ -67,7 +68,9 @@ export const SongContextMenu = ({
       label: `${translate('resources.song.actions.download')} (${formatBytes(
         record.size
       )})`,
-      action: (record) => subsonic.download(record.mediaFileId || record.id),
+      action: (record) => {
+        dispatch(openDownloadMenu(record, DOWNLOAD_MENU_SONG))
+      },
     },
     info: {
       enabled: true,
diff --git a/ui/src/dialogs/DownloadMenuDialog.js b/ui/src/dialogs/DownloadMenuDialog.js
new file mode 100644
index 000000000..1b21f75b9
--- /dev/null
+++ b/ui/src/dialogs/DownloadMenuDialog.js
@@ -0,0 +1,173 @@
+import React, { useState } from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import { ReferenceManyField, useTranslate } from 'react-admin'
+import {
+  Box,
+  Button,
+  Dialog,
+  DialogActions,
+  DialogContent,
+  DialogTitle,
+  FormControlLabel,
+  FormGroup,
+  MenuItem,
+  Switch,
+  TextField,
+} from '@material-ui/core'
+import subsonic from '../subsonic'
+import { closeDownloadMenu } from '../actions'
+import { formatBytes } from '../utils'
+
+const DownloadTranscodings = (props) => {
+  const translate = useTranslate()
+
+  return (
+    <>
+      <TextField
+        fullWidth
+        id="downloadFormat"
+        select
+        label={translate('resources.transcoding.fields.targetFormat')}
+        onChange={(e) => props.onChange(e.target.value)}
+        value={props.value}
+      >
+        {Object.values(props.data).map((transcoding) => (
+          <MenuItem key={transcoding.id} value={transcoding.targetFormat}>
+            {transcoding.name}
+          </MenuItem>
+        ))}
+      </TextField>
+    </>
+  )
+}
+
+const DownloadMenuDialog = () => {
+  const { open, record, recordType } = useSelector(
+    (state) => state.downloadMenuDialog
+  )
+  const dispatch = useDispatch()
+  const translate = useTranslate()
+
+  const [originalFormat, setUseOriginalFormat] = useState(true)
+  const [targetFormat, setTargetFormat] = useState('')
+  const [targetRate, setTargetRate] = useState(0)
+
+  const handleClose = (e) => {
+    dispatch(closeDownloadMenu())
+    e.stopPropagation()
+  }
+
+  const handleDownload = (e) => {
+    if (record) {
+      subsonic.download(
+        record.id,
+        originalFormat ? 'raw' : targetFormat,
+        targetRate
+      )
+      dispatch(closeDownloadMenu())
+    }
+    e.stopPropagation()
+  }
+
+  const handleOriginal = (e) => {
+    const original = e.target.checked
+
+    setUseOriginalFormat(original)
+
+    if (original) {
+      setTargetFormat('')
+      setTargetRate(0)
+    }
+  }
+
+  const type = recordType
+    ? translate(`resources.${recordType}.name`, {
+        smart_count: 1,
+      }).toLocaleLowerCase()
+    : ''
+
+  return (
+    <>
+      <Dialog
+        open={open}
+        onClose={handleClose}
+        onBackdropClick={handleClose}
+        aria-labelledby="download-dialog"
+        fullWidth={true}
+        maxWidth={'sm'}
+      >
+        <DialogTitle id="download-dialog">
+          {record &&
+            `${translate('resources.album.actions.download')} ${type} ${
+              record.name || record.title
+            } (${formatBytes(record.size)})`}
+        </DialogTitle>
+        <DialogContent>
+          <Box
+            component="form"
+            sx={{
+              '& .MuiTextField-root': { m: 1, width: '25ch' },
+            }}
+          >
+            <div>
+              <FormGroup>
+                <FormControlLabel
+                  control={<Switch checked={originalFormat} />}
+                  label={translate('message.originalFormat')}
+                  onChange={handleOriginal}
+                />
+              </FormGroup>
+              {!originalFormat && (
+                <>
+                  <ReferenceManyField
+                    fullWidth
+                    source=""
+                    target="name"
+                    reference="transcoding"
+                    sort={{ field: 'name', order: 'ASC' }}
+                  >
+                    <DownloadTranscodings
+                      onChange={setTargetFormat}
+                      value={targetFormat}
+                    />
+                  </ReferenceManyField>
+                  <TextField
+                    fullWidth
+                    id="downloadRate"
+                    select
+                    label={translate('resources.player.fields.maxBitRate')}
+                    value={targetRate}
+                    onChange={(e) => setTargetRate(e.target.value)}
+                  >
+                    <MenuItem value={0}>-</MenuItem>
+                    {[32, 48, 64, 80, 96, 112, 128, 160, 192, 256, 320].map(
+                      (bits) => (
+                        <MenuItem key={bits} value={bits}>
+                          {bits}
+                        </MenuItem>
+                      )
+                    )}
+                  </TextField>
+                </>
+              )}
+            </div>
+          </Box>
+        </DialogContent>
+        <DialogActions>
+          <Button
+            onClick={handleDownload}
+            color="primary"
+            disabled={!originalFormat && !targetFormat}
+          >
+            {translate('resources.album.actions.download')}
+          </Button>
+          <Button onClick={handleClose} color="secondary">
+            {translate('ra.action.close')}
+          </Button>
+        </DialogActions>
+      </Dialog>
+    </>
+  )
+}
+
+export default DownloadMenuDialog
diff --git a/ui/src/i18n/en.json b/ui/src/i18n/en.json
index e49f7cb51..955152900 100644
--- a/ui/src/i18n/en.json
+++ b/ui/src/i18n/en.json
@@ -322,7 +322,8 @@
             "lastfm": "Open in Last.fm",
             "musicbrainz": "Open in MusicBrainz"
         },
-        "lastfmLink": "Read More..."
+        "lastfmLink": "Read More...",
+        "originalFormat": "Download in original format"
     },
     "menu": {
         "library": "Library",
diff --git a/ui/src/playlist/PlaylistActions.js b/ui/src/playlist/PlaylistActions.js
index e52d00f3f..42ed503f3 100644
--- a/ui/src/playlist/PlaylistActions.js
+++ b/ui/src/playlist/PlaylistActions.js
@@ -14,9 +14,15 @@ import CloudDownloadOutlinedIcon from '@material-ui/icons/CloudDownloadOutlined'
 import { RiPlayListAddFill, RiPlayList2Fill } from 'react-icons/ri'
 import QueueMusicIcon from '@material-ui/icons/QueueMusic'
 import { httpClient } from '../dataProvider'
-import { playNext, addTracks, playTracks, shuffleTracks } from '../actions'
+import {
+  playNext,
+  addTracks,
+  playTracks,
+  shuffleTracks,
+  openDownloadMenu,
+  DOWNLOAD_MENU_PLAY,
+} from '../actions'
 import { M3U_MIME_TYPE, REST_URL } from '../consts'
-import subsonic from '../subsonic'
 import PropTypes from 'prop-types'
 import { formatBytes } from '../utils'
 import { useMediaQuery, makeStyles } from '@material-ui/core'
@@ -79,8 +85,8 @@ const PlaylistActions = ({ className, ids, data, record, ...rest }) => {
   }, [getAllSongsAndDispatch])
 
   const handleDownload = React.useCallback(() => {
-    subsonic.download(record.id)
-  }, [record])
+    dispatch(openDownloadMenu(record, DOWNLOAD_MENU_PLAY))
+  }, [dispatch, record])
 
   const handleExport = React.useCallback(
     () =>
diff --git a/ui/src/playlist/PlaylistSongs.js b/ui/src/playlist/PlaylistSongs.js
index 086fb73f1..fffc1fcba 100644
--- a/ui/src/playlist/PlaylistSongs.js
+++ b/ui/src/playlist/PlaylistSongs.js
@@ -31,6 +31,7 @@ import { AddToPlaylistDialog } from '../dialogs'
 import { AlbumLinkField } from '../song/AlbumLinkField'
 import { playTracks } from '../actions'
 import PlaylistSongBulkActions from './PlaylistSongBulkActions'
+import DownloadMenuDialog from '../dialogs/DownloadMenuDialog'
 import ExpandInfoDialog from '../dialogs/ExpandInfoDialog'
 
 const useStyles = makeStyles(
@@ -214,6 +215,7 @@ const PlaylistSongs = ({ playlistId, readOnly, actions, ...props }) => {
         </Card>
       </div>
       <AddToPlaylistDialog />
+      <DownloadMenuDialog />
       <ExpandInfoDialog content={<SongInfo />} />
       {React.cloneElement(props.pagination, listContext)}
     </>
diff --git a/ui/src/reducers/dialogReducer.js b/ui/src/reducers/dialogReducer.js
index ea95248f4..0468e32c4 100644
--- a/ui/src/reducers/dialogReducer.js
+++ b/ui/src/reducers/dialogReducer.js
@@ -1,6 +1,12 @@
 import {
   ADD_TO_PLAYLIST_CLOSE,
   ADD_TO_PLAYLIST_OPEN,
+  DOWNLOAD_MENU_ALBUM,
+  DOWNLOAD_MENU_ARTIST,
+  DOWNLOAD_MENU_CLOSE,
+  DOWNLOAD_MENU_OPEN,
+  DOWNLOAD_MENU_PLAY,
+  DOWNLOAD_MENU_SONG,
   DUPLICATE_SONG_WARNING_OPEN,
   DUPLICATE_SONG_WARNING_CLOSE,
   EXTENDED_INFO_OPEN,
@@ -40,6 +46,49 @@ export const addToPlaylistDialogReducer = (
   }
 }
 
+export const downloadMenuDialogReducer = (
+  previousState = {
+    open: false,
+  },
+  payload
+) => {
+  const { type } = payload
+  switch (type) {
+    case DOWNLOAD_MENU_OPEN: {
+      switch (payload.recordType) {
+        case DOWNLOAD_MENU_ALBUM:
+        case DOWNLOAD_MENU_ARTIST:
+        case DOWNLOAD_MENU_PLAY:
+        case DOWNLOAD_MENU_SONG: {
+          return {
+            ...previousState,
+            open: true,
+            record: payload.record,
+            recordType: payload.recordType,
+          }
+        }
+        default: {
+          return {
+            ...previousState,
+            open: true,
+            record: payload.record,
+            recordType: undefined,
+          }
+        }
+      }
+    }
+    case DOWNLOAD_MENU_CLOSE: {
+      return {
+        ...previousState,
+        open: false,
+        recordType: undefined,
+      }
+    }
+    default:
+      return previousState
+  }
+}
+
 export const expandInfoDialogReducer = (
   previousState = {
     open: false,
diff --git a/ui/src/song/SongList.js b/ui/src/song/SongList.js
index 5307dae84..8fbe7ded6 100644
--- a/ui/src/song/SongList.js
+++ b/ui/src/song/SongList.js
@@ -34,6 +34,7 @@ import { AlbumLinkField } from './AlbumLinkField'
 import { AddToPlaylistDialog } from '../dialogs'
 import { SongBulkActions, QualityInfo, useSelectedFields } from '../common'
 import config from '../config'
+import DownloadMenuDialog from '../dialogs/DownloadMenuDialog'
 import ExpandInfoDialog from '../dialogs/ExpandInfoDialog'
 
 const useStyles = makeStyles({
@@ -194,6 +195,7 @@ const SongList = (props) => {
         )}
       </List>
       <AddToPlaylistDialog />
+      <DownloadMenuDialog />
       <ExpandInfoDialog content={<SongInfo />} />
     </>
   )
diff --git a/ui/src/subsonic/index.js b/ui/src/subsonic/index.js
index 4861c5ec1..54b71a018 100644
--- a/ui/src/subsonic/index.js
+++ b/ui/src/subsonic/index.js
@@ -38,7 +38,8 @@ const unstar = (id) => httpClient(url('unstar', id))
 
 const setRating = (id, rating) => httpClient(url('setRating', id, { rating }))
 
-const download = (id) => (window.location.href = baseUrl(url('download', id)))
+const download = (id, format = 'raw', bitrate = '0') =>
+  (window.location.href = baseUrl(url('download', id, { format, bitrate })))
 
 const startScan = (options) => httpClient(url('startScan', null, options))