diff --git a/cmd/root.go b/cmd/root.go
index e7b694a24..da088e767 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -84,9 +84,6 @@ func startServer(ctx context.Context) func() error {
a.MountRouter("Native API", consts.URLPathNativeAPI, CreateNativeAPIRouter())
a.MountRouter("Subsonic API", consts.URLPathSubsonicAPI, CreateSubsonicAPIRouter())
a.MountRouter("Public Endpoints", consts.URLPathPublic, CreatePublicRouter())
- if conf.Server.DevEnableShare {
- a.MountRouter("Share Endpoint", consts.URLPathShares, CreateSharesRouter())
- }
if conf.Server.LastFM.Enabled {
a.MountRouter("LastFM Auth", consts.URLPathNativeAPI+"/lastfm", CreateLastFMRouter())
}
diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go
index f1df1ad17..abe023cfb 100644
--- a/cmd/wire_gen.go
+++ b/cmd/wire_gen.go
@@ -22,7 +22,6 @@ import (
"github.com/navidrome/navidrome/server/events"
"github.com/navidrome/navidrome/server/nativeapi"
"github.com/navidrome/navidrome/server/public"
- "github.com/navidrome/navidrome/server/shares"
"github.com/navidrome/navidrome/server/subsonic"
"sync"
)
@@ -75,15 +74,8 @@ func CreatePublicRouter() *public.Router {
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, externalMetadata)
transcodingCache := core.GetTranscodingCache()
mediaStreamer := core.NewMediaStreamer(dataStore, fFmpeg, transcodingCache)
- router := public.New(artworkArtwork, mediaStreamer)
- return router
-}
-
-func CreateSharesRouter() *shares.Router {
- sqlDB := db.Db()
- dataStore := persistence.New(sqlDB)
share := core.NewShare(dataStore)
- router := shares.New(dataStore, share)
+ router := public.New(dataStore, artworkArtwork, mediaStreamer, share)
return router
}
@@ -118,7 +110,7 @@ func createScanner() scanner.Scanner {
// wire_injectors.go:
-var allProviders = wire.NewSet(core.Set, artwork.Set, subsonic.New, nativeapi.New, public.New, shares.New, persistence.New, lastfm.NewRouter, listenbrainz.NewRouter, events.GetBroker, db.Db)
+var allProviders = wire.NewSet(core.Set, artwork.Set, subsonic.New, nativeapi.New, public.New, persistence.New, lastfm.NewRouter, listenbrainz.NewRouter, events.GetBroker, db.Db)
// Scanner must be a Singleton
var (
diff --git a/cmd/wire_injectors.go b/cmd/wire_injectors.go
index 1f41e1a36..cc896421f 100644
--- a/cmd/wire_injectors.go
+++ b/cmd/wire_injectors.go
@@ -17,7 +17,6 @@ import (
"github.com/navidrome/navidrome/server/events"
"github.com/navidrome/navidrome/server/nativeapi"
"github.com/navidrome/navidrome/server/public"
- "github.com/navidrome/navidrome/server/shares"
"github.com/navidrome/navidrome/server/subsonic"
)
@@ -27,7 +26,6 @@ var allProviders = wire.NewSet(
subsonic.New,
nativeapi.New,
public.New,
- shares.New,
persistence.New,
lastfm.NewRouter,
listenbrainz.NewRouter,
@@ -61,12 +59,6 @@ func CreatePublicRouter() *public.Router {
))
}
-func CreateSharesRouter() *shares.Router {
- panic(wire.Build(
- allProviders,
- ))
-}
-
func CreateLastFMRouter() *lastfm.Router {
panic(wire.Build(
allProviders,
diff --git a/consts/consts.go b/consts/consts.go
index 2a3f5956b..9bad1ee46 100644
--- a/consts/consts.go
+++ b/consts/consts.go
@@ -34,7 +34,6 @@ const (
URLPathSubsonicAPI = "/rest"
URLPathPublic = "/p"
URLPathPublicImages = URLPathPublic + "/img"
- URLPathShares = "/s"
// DefaultUILoginBackgroundURL uses Navidrome curated background images collection,
// available at https://unsplash.com/collections/20072696/navidrome
diff --git a/core/share.go b/core/share.go
index 6a696a799..736a15772 100644
--- a/core/share.go
+++ b/core/share.go
@@ -35,8 +35,7 @@ func (s *shareService) Load(ctx context.Context, id string) (*model.Share, error
return nil, err
}
share := entity.(*model.Share)
- now := time.Now()
- share.LastVisitedAt = &now
+ share.LastVisitedAt = time.Now()
share.VisitCount++
err = repo.(rest.Persistable).Update(id, share, "last_visited_at", "visit_count")
@@ -112,8 +111,7 @@ func (r *shareRepositoryWrapper) Save(entity interface{}) (string, error) {
}
s.ID = id
if s.ExpiresAt.IsZero() {
- exp := time.Now().Add(365 * 24 * time.Hour)
- s.ExpiresAt = &exp
+ s.ExpiresAt = time.Now().Add(365 * 24 * time.Hour)
}
id, err = r.Persistable.Save(s)
return id, err
diff --git a/model/share.go b/model/share.go
index 40b25e526..b689f1556 100644
--- a/model/share.go
+++ b/model/share.go
@@ -9,16 +9,16 @@ type Share struct {
UserID string `structs:"user_id" json:"userId,omitempty" orm:"column(user_id)"`
Username string `structs:"-" json:"username,omitempty" orm:"-"`
Description string `structs:"description" json:"description,omitempty"`
- ExpiresAt *time.Time `structs:"expires_at" json:"expiresAt,omitempty"`
- LastVisitedAt *time.Time `structs:"last_visited_at" json:"lastVisitedAt,omitempty"`
+ ExpiresAt time.Time `structs:"expires_at" json:"expiresAt,omitempty"`
+ LastVisitedAt time.Time `structs:"last_visited_at" json:"lastVisitedAt,omitempty"`
ResourceIDs string `structs:"resource_ids" json:"resourceIds,omitempty" orm:"column(resource_ids)"`
ResourceType string `structs:"resource_type" json:"resourceType,omitempty"`
Contents string `structs:"contents" json:"contents,omitempty"`
Format string `structs:"format" json:"format,omitempty"`
MaxBitRate int `structs:"max_bit_rate" json:"maxBitRate,omitempty"`
VisitCount int `structs:"visit_count" json:"visitCount,omitempty"`
- CreatedAt *time.Time `structs:"created_at" json:"createdAt,omitempty"`
- UpdatedAt *time.Time `structs:"updated_at" json:"updatedAt,omitempty"`
+ CreatedAt time.Time `structs:"created_at" json:"createdAt,omitempty"`
+ UpdatedAt time.Time `structs:"updated_at" json:"updatedAt,omitempty"`
Tracks []ShareTrack `structs:"-" json:"tracks,omitempty"`
}
diff --git a/persistence/share_repository.go b/persistence/share_repository.go
index 5023c7365..aa0720d12 100644
--- a/persistence/share_repository.go
+++ b/persistence/share_repository.go
@@ -51,8 +51,7 @@ func (r *shareRepository) Update(id string, entity interface{}, cols ...string)
s := entity.(*model.Share)
// TODO Validate record
s.ID = id
- now := time.Now()
- s.UpdatedAt = &now
+ s.UpdatedAt = time.Now()
cols = append(cols, "updated_at")
_, err := r.put(id, s, cols...)
if errors.Is(err, model.ErrNotFound) {
@@ -68,9 +67,8 @@ func (r *shareRepository) Save(entity interface{}) (string, error) {
if s.UserID == "" {
s.UserID = u.ID
}
- now := time.Now()
- s.CreatedAt = &now
- s.UpdatedAt = &now
+ s.CreatedAt = time.Now()
+ s.UpdatedAt = time.Now()
id, err := r.put(s.ID, s)
if errors.Is(err, model.ErrNotFound) {
return "", rest.ErrNotFound
diff --git a/server/public/handle_images.go b/server/public/handle_images.go
new file mode 100644
index 000000000..0b0455331
--- /dev/null
+++ b/server/public/handle_images.go
@@ -0,0 +1,53 @@
+package public
+
+import (
+ "context"
+ "errors"
+ "io"
+ "net/http"
+ "time"
+
+ "github.com/navidrome/navidrome/log"
+ "github.com/navidrome/navidrome/model"
+ "github.com/navidrome/navidrome/utils"
+)
+
+func (p *Router) handleImages(w http.ResponseWriter, r *http.Request) {
+ ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
+ defer cancel()
+ id := r.URL.Query().Get(":id")
+ if id == "" {
+ http.Error(w, "invalid id", http.StatusBadRequest)
+ return
+ }
+
+ artId, err := DecodeArtworkID(id)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ size := utils.ParamInt(r, "size", 0)
+ imgReader, lastUpdate, err := p.artwork.Get(ctx, artId.String(), size)
+
+ switch {
+ case errors.Is(err, context.Canceled):
+ return
+ case errors.Is(err, model.ErrNotFound):
+ log.Error(r, "Couldn't find coverArt", "id", id, err)
+ http.Error(w, "Artwork not found", http.StatusNotFound)
+ return
+ case err != nil:
+ log.Error(r, "Error retrieving coverArt", "id", id, err)
+ http.Error(w, "Error retrieving coverArt", http.StatusInternalServerError)
+ return
+ }
+
+ defer imgReader.Close()
+ w.Header().Set("Cache-Control", "public, max-age=315360000")
+ w.Header().Set("Last-Modified", lastUpdate.Format(time.RFC1123))
+ cnt, err := io.Copy(w, imgReader)
+ if err != nil {
+ log.Warn(ctx, "Error sending image", "count", cnt, err)
+ }
+}
diff --git a/server/shares/share_endpoint.go b/server/public/handle_shares.go
similarity index 63%
rename from server/shares/share_endpoint.go
rename to server/public/handle_shares.go
index 822b06e33..e0d7dde2d 100644
--- a/server/shares/share_endpoint.go
+++ b/server/public/handle_shares.go
@@ -1,14 +1,9 @@
-package shares
+package public
import (
"errors"
"net/http"
- "path"
- "github.com/go-chi/chi/v5"
- "github.com/navidrome/navidrome/conf"
- "github.com/navidrome/navidrome/consts"
- "github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/core/auth"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
@@ -16,34 +11,6 @@ import (
"github.com/navidrome/navidrome/ui"
)
-type Router struct {
- http.Handler
- ds model.DataStore
- share core.Share
- assetsHandler http.Handler
- streamer core.MediaStreamer
-}
-
-func New(ds model.DataStore, share core.Share) *Router {
- p := &Router{ds: ds, share: share}
- shareRoot := path.Join(conf.Server.BaseURL, consts.URLPathShares)
- p.assetsHandler = http.StripPrefix(shareRoot, http.FileServer(http.FS(ui.BuildAssets())))
- p.Handler = p.routes()
-
- return p
-}
-
-func (p *Router) routes() http.Handler {
- r := chi.NewRouter()
-
- r.Group(func(r chi.Router) {
- r.Use(server.URLParamsMiddleware)
- r.HandleFunc("/{id}", p.handleShares)
- r.Handle("/*", p.assetsHandler)
- })
- return r
-}
-
func (p *Router) handleShares(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get(":id")
if id == "" {
@@ -82,6 +49,7 @@ func (p *Router) mapShareInfo(s *model.Share) *model.Share {
Tracks: s.Tracks,
}
for i := range s.Tracks {
+ // TODO Use Encode(Artwork)ID?
claims := map[string]any{"id": s.Tracks[i].ID}
if s.Format != "" {
claims["f"] = s.Format
@@ -89,7 +57,7 @@ func (p *Router) mapShareInfo(s *model.Share) *model.Share {
if s.MaxBitRate != 0 {
claims["b"] = s.MaxBitRate
}
- id, _ := auth.CreateExpiringPublicToken(*s.ExpiresAt, claims)
+ id, _ := auth.CreateExpiringPublicToken(s.ExpiresAt, claims)
mapped.Tracks[i].ID = id
}
return mapped
diff --git a/server/public/share_stream.go b/server/public/handle_streams.go
similarity index 100%
rename from server/public/share_stream.go
rename to server/public/handle_streams.go
diff --git a/server/public/public_endpoints.go b/server/public/public_endpoints.go
index 0afab8ecc..e6e2551f5 100644
--- a/server/public/public_endpoints.go
+++ b/server/public/public_endpoints.go
@@ -1,29 +1,32 @@
package public
import (
- "context"
- "errors"
- "io"
"net/http"
- "time"
+ "path"
"github.com/go-chi/chi/v5"
+ "github.com/navidrome/navidrome/conf"
+ "github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/core/artwork"
- "github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/server"
- "github.com/navidrome/navidrome/utils"
+ "github.com/navidrome/navidrome/ui"
)
type Router struct {
http.Handler
- artwork artwork.Artwork
- streamer core.MediaStreamer
+ artwork artwork.Artwork
+ streamer core.MediaStreamer
+ share core.Share
+ assetsHandler http.Handler
+ ds model.DataStore
}
-func New(artwork artwork.Artwork, streamer core.MediaStreamer) *Router {
- p := &Router{artwork: artwork, streamer: streamer}
+func New(ds model.DataStore, artwork artwork.Artwork, streamer core.MediaStreamer, share core.Share) *Router {
+ p := &Router{ds: ds, artwork: artwork, streamer: streamer, share: share}
+ shareRoot := path.Join(conf.Server.BaseURL, consts.URLPathPublic)
+ p.assetsHandler = http.StripPrefix(shareRoot, http.FileServer(http.FS(ui.BuildAssets())))
p.Handler = p.routes()
return p
@@ -34,48 +37,12 @@ func (p *Router) routes() http.Handler {
r.Group(func(r chi.Router) {
r.Use(server.URLParamsMiddleware)
- r.HandleFunc("/s/{id}", p.handleStream)
r.HandleFunc("/img/{id}", p.handleImages)
+ if conf.Server.DevEnableShare {
+ r.HandleFunc("/s/{id}", p.handleStream)
+ r.HandleFunc("/{id}", p.handleShares)
+ r.Handle("/*", p.assetsHandler)
+ }
})
return r
}
-
-func (p *Router) handleImages(w http.ResponseWriter, r *http.Request) {
- ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
- defer cancel()
- id := r.URL.Query().Get(":id")
- if id == "" {
- http.Error(w, "invalid id", http.StatusBadRequest)
- return
- }
-
- artId, err := DecodeArtworkID(id)
- if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
-
- size := utils.ParamInt(r, "size", 0)
- imgReader, lastUpdate, err := p.artwork.Get(ctx, artId.String(), size)
-
- switch {
- case errors.Is(err, context.Canceled):
- return
- case errors.Is(err, model.ErrNotFound):
- log.Error(r, "Couldn't find coverArt", "id", id, err)
- http.Error(w, "Artwork not found", http.StatusNotFound)
- return
- case err != nil:
- log.Error(r, "Error retrieving coverArt", "id", id, err)
- http.Error(w, "Error retrieving coverArt", http.StatusInternalServerError)
- return
- }
-
- defer imgReader.Close()
- w.Header().Set("Cache-Control", "public, max-age=315360000")
- w.Header().Set("Last-Modified", lastUpdate.Format(time.RFC1123))
- cnt, err := io.Copy(w, imgReader)
- if err != nil {
- log.Warn(ctx, "Error sending image", "count", cnt, err)
- }
-}
diff --git a/ui/public/index.html b/ui/public/index.html
index 2ffa2037b..c78d7e308 100644
--- a/ui/public/index.html
+++ b/ui/public/index.html
@@ -34,7 +34,6 @@
window.__APP_CONFIG__ = {{ .AppConfig }}
diff --git a/ui/src/App.js b/ui/src/App.js
index 77ef41751..db2fe06d2 100644
--- a/ui/src/App.js
+++ b/ui/src/App.js
@@ -36,7 +36,7 @@ import config, { shareInfo } from './config'
import { setDispatch, startEventStream, stopEventStream } from './eventStream'
import { keyMap } from './hotkeys'
import useChangeThemeColor from './useChangeThemeColor'
-import ShareApp from './ShareApp'
+import SharePlayer from './SharePlayer'
const history = createHashHistory()
@@ -141,7 +141,7 @@ const Admin = (props) => {
const AppWithHotkeys = () => {
if (config.devEnableShare && shareInfo) {
- return
+ return
}
return (
diff --git a/ui/src/ShareApp.js b/ui/src/SharePlayer.js
similarity index 75%
rename from ui/src/ShareApp.js
rename to ui/src/SharePlayer.js
index fa6980558..74df7b95b 100644
--- a/ui/src/ShareApp.js
+++ b/ui/src/SharePlayer.js
@@ -2,12 +2,12 @@ import ReactJkMusicPlayer from 'navidrome-music-player'
import config, { shareInfo } from './config'
import { baseUrl } from './utils'
-const ShareApp = (props) => {
+const SharePlayer = () => {
const list = shareInfo?.tracks.map((s) => {
return {
name: s.title,
musicSrc: baseUrl(config.publicBaseUrl + '/s/' + s.id),
- cover: baseUrl(config.publicBaseUrl + '/img/' + s.id),
+ cover: baseUrl(config.publicBaseUrl + '/img/' + s.id + '?size=300'),
singer: s.artist,
duration: s.duration,
}
@@ -19,8 +19,10 @@ const ShareApp = (props) => {
showDownload: false,
showReload: false,
showMediaSession: true,
+ theme: 'auto',
+ showThemeSwitch: false,
}
return
}
-export default ShareApp
+export default SharePlayer
diff --git a/ui/src/album/AlbumActions.js b/ui/src/album/AlbumActions.js
index 62875c16b..c6593f578 100644
--- a/ui/src/album/AlbumActions.js
+++ b/ui/src/album/AlbumActions.js
@@ -133,6 +133,7 @@ const AlbumActions = ({
close={shareDialog.close}
ids={[record.id]}
resource={'album'}
+ title={`Share album '${record.name}'`}
/>
)
@@ -146,7 +147,6 @@ AlbumActions.propTypes = {
AlbumActions.defaultProps = {
record: {},
selectedIds: [],
- onUnselectItems: () => null,
}
export default AlbumActions
diff --git a/ui/src/config.js b/ui/src/config.js
index cb01f98d3..1692e5792 100644
--- a/ui/src/config.js
+++ b/ui/src/config.js
@@ -28,7 +28,6 @@ const defaultConfig = {
enableCoverAnimation: true,
devShowArtistPage: true,
enableReplayGain: true,
- shareBaseUrl: '/s',
publicBaseUrl: '/p',
}
diff --git a/ui/src/dialogs/ShareDialog.js b/ui/src/dialogs/ShareDialog.js
index bd6b63e98..ef751db76 100644
--- a/ui/src/dialogs/ShareDialog.js
+++ b/ui/src/dialogs/ShareDialog.js
@@ -4,6 +4,8 @@ import {
DialogActions,
DialogContent,
DialogTitle,
+ FormControlLabel,
+ Switch,
} from '@material-ui/core'
import {
SelectInput,
@@ -14,12 +16,12 @@ import {
} from 'react-admin'
import { useMemo, useState } from 'react'
import { shareUrl } from '../utils'
-import Typography from '@material-ui/core/Typography'
-export const ShareDialog = ({ open, close, onClose, ids, resource }) => {
+export const ShareDialog = ({ open, close, onClose, ids, resource, title }) => {
const notify = useNotify()
const [format, setFormat] = useState('')
const [maxBitRate, setMaxBitRate] = useState(0)
+ const [originalFormat, setUseOriginalFormat] = useState(true)
const { data: formats, loading } = useGetList(
'transcoding',
{
@@ -39,6 +41,17 @@ export const ShareDialog = ({ open, close, onClose, ids, resource }) => {
[formats, loading]
)
+ const handleOriginal = (e) => {
+ const original = e.target.checked
+
+ setUseOriginalFormat(original)
+
+ if (original) {
+ setFormat('')
+ setMaxBitRate(0)
+ }
+ }
+
const [createShare] = useCreate(
'share',
{
@@ -78,47 +91,50 @@ export const ShareDialog = ({ open, close, onClose, ids, resource }) => {
open={open}
onClose={onClose}
onBackdropClick={onClose}
- aria-labelledby="info-dialog-album"
+ aria-labelledby="share-dialog"
fullWidth={true}
maxWidth={'sm'}
>
-
- Create a link to share your music with friends
-
+ {title}
- Select transcoding options:
-
- (Leave options empty for original quality)
-
- {
- setFormat(event.target.value)
- }}
- />
- {
- setMaxBitRate(event.target.value)
- }}
+ }
+ label={'Share in original format'}
+ onChange={handleOriginal}
/>
+ {!originalFormat && (
+ {
+ setFormat(event.target.value)
+ }}
+ />
+ )}
+ {!originalFormat && (
+ {
+ setMaxBitRate(event.target.value)
+ }}
+ />
+ )}
diff --git a/ui/src/share/ShareList.js b/ui/src/share/ShareList.js
index 54244a350..0c3f5e470 100644
--- a/ui/src/share/ShareList.js
+++ b/ui/src/share/ShareList.js
@@ -33,6 +33,9 @@ const ShareList = (props) => {
label="URL"
target="_blank"
rel="noopener noreferrer"
+ onClick={(e) => {
+ e.stopPropagation()
+ }}
>
{r.id}
diff --git a/ui/src/utils/shareUrl.js b/ui/src/utils/shareUrl.js
index 0b481ce88..d93add9ce 100644
--- a/ui/src/utils/shareUrl.js
+++ b/ui/src/utils/shareUrl.js
@@ -1,6 +1,6 @@
import config from '../config'
export const shareUrl = (path) => {
- const url = new URL(config.shareBaseUrl + '/' + path, window.location.href)
+ const url = new URL(config.publicBaseUrl + '/' + path, window.location.href)
return url.href
}