mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-05 13:37:38 +03:00
More work on Shares
This commit is contained in:
parent
ab04e33da6
commit
84aa094e56
19 changed files with 150 additions and 167 deletions
|
@ -84,9 +84,6 @@ func startServer(ctx context.Context) func() error {
|
||||||
a.MountRouter("Native API", consts.URLPathNativeAPI, CreateNativeAPIRouter())
|
a.MountRouter("Native API", consts.URLPathNativeAPI, CreateNativeAPIRouter())
|
||||||
a.MountRouter("Subsonic API", consts.URLPathSubsonicAPI, CreateSubsonicAPIRouter())
|
a.MountRouter("Subsonic API", consts.URLPathSubsonicAPI, CreateSubsonicAPIRouter())
|
||||||
a.MountRouter("Public Endpoints", consts.URLPathPublic, CreatePublicRouter())
|
a.MountRouter("Public Endpoints", consts.URLPathPublic, CreatePublicRouter())
|
||||||
if conf.Server.DevEnableShare {
|
|
||||||
a.MountRouter("Share Endpoint", consts.URLPathShares, CreateSharesRouter())
|
|
||||||
}
|
|
||||||
if conf.Server.LastFM.Enabled {
|
if conf.Server.LastFM.Enabled {
|
||||||
a.MountRouter("LastFM Auth", consts.URLPathNativeAPI+"/lastfm", CreateLastFMRouter())
|
a.MountRouter("LastFM Auth", consts.URLPathNativeAPI+"/lastfm", CreateLastFMRouter())
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,6 @@ import (
|
||||||
"github.com/navidrome/navidrome/server/events"
|
"github.com/navidrome/navidrome/server/events"
|
||||||
"github.com/navidrome/navidrome/server/nativeapi"
|
"github.com/navidrome/navidrome/server/nativeapi"
|
||||||
"github.com/navidrome/navidrome/server/public"
|
"github.com/navidrome/navidrome/server/public"
|
||||||
"github.com/navidrome/navidrome/server/shares"
|
|
||||||
"github.com/navidrome/navidrome/server/subsonic"
|
"github.com/navidrome/navidrome/server/subsonic"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
@ -75,15 +74,8 @@ func CreatePublicRouter() *public.Router {
|
||||||
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, externalMetadata)
|
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, externalMetadata)
|
||||||
transcodingCache := core.GetTranscodingCache()
|
transcodingCache := core.GetTranscodingCache()
|
||||||
mediaStreamer := core.NewMediaStreamer(dataStore, fFmpeg, transcodingCache)
|
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)
|
share := core.NewShare(dataStore)
|
||||||
router := shares.New(dataStore, share)
|
router := public.New(dataStore, artworkArtwork, mediaStreamer, share)
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,7 +110,7 @@ func createScanner() scanner.Scanner {
|
||||||
|
|
||||||
// wire_injectors.go:
|
// 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
|
// Scanner must be a Singleton
|
||||||
var (
|
var (
|
||||||
|
|
|
@ -17,7 +17,6 @@ import (
|
||||||
"github.com/navidrome/navidrome/server/events"
|
"github.com/navidrome/navidrome/server/events"
|
||||||
"github.com/navidrome/navidrome/server/nativeapi"
|
"github.com/navidrome/navidrome/server/nativeapi"
|
||||||
"github.com/navidrome/navidrome/server/public"
|
"github.com/navidrome/navidrome/server/public"
|
||||||
"github.com/navidrome/navidrome/server/shares"
|
|
||||||
"github.com/navidrome/navidrome/server/subsonic"
|
"github.com/navidrome/navidrome/server/subsonic"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -27,7 +26,6 @@ var allProviders = wire.NewSet(
|
||||||
subsonic.New,
|
subsonic.New,
|
||||||
nativeapi.New,
|
nativeapi.New,
|
||||||
public.New,
|
public.New,
|
||||||
shares.New,
|
|
||||||
persistence.New,
|
persistence.New,
|
||||||
lastfm.NewRouter,
|
lastfm.NewRouter,
|
||||||
listenbrainz.NewRouter,
|
listenbrainz.NewRouter,
|
||||||
|
@ -61,12 +59,6 @@ func CreatePublicRouter() *public.Router {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateSharesRouter() *shares.Router {
|
|
||||||
panic(wire.Build(
|
|
||||||
allProviders,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateLastFMRouter() *lastfm.Router {
|
func CreateLastFMRouter() *lastfm.Router {
|
||||||
panic(wire.Build(
|
panic(wire.Build(
|
||||||
allProviders,
|
allProviders,
|
||||||
|
|
|
@ -34,7 +34,6 @@ const (
|
||||||
URLPathSubsonicAPI = "/rest"
|
URLPathSubsonicAPI = "/rest"
|
||||||
URLPathPublic = "/p"
|
URLPathPublic = "/p"
|
||||||
URLPathPublicImages = URLPathPublic + "/img"
|
URLPathPublicImages = URLPathPublic + "/img"
|
||||||
URLPathShares = "/s"
|
|
||||||
|
|
||||||
// DefaultUILoginBackgroundURL uses Navidrome curated background images collection,
|
// DefaultUILoginBackgroundURL uses Navidrome curated background images collection,
|
||||||
// available at https://unsplash.com/collections/20072696/navidrome
|
// available at https://unsplash.com/collections/20072696/navidrome
|
||||||
|
|
|
@ -35,8 +35,7 @@ func (s *shareService) Load(ctx context.Context, id string) (*model.Share, error
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
share := entity.(*model.Share)
|
share := entity.(*model.Share)
|
||||||
now := time.Now()
|
share.LastVisitedAt = time.Now()
|
||||||
share.LastVisitedAt = &now
|
|
||||||
share.VisitCount++
|
share.VisitCount++
|
||||||
|
|
||||||
err = repo.(rest.Persistable).Update(id, share, "last_visited_at", "visit_count")
|
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
|
s.ID = id
|
||||||
if s.ExpiresAt.IsZero() {
|
if s.ExpiresAt.IsZero() {
|
||||||
exp := time.Now().Add(365 * 24 * time.Hour)
|
s.ExpiresAt = time.Now().Add(365 * 24 * time.Hour)
|
||||||
s.ExpiresAt = &exp
|
|
||||||
}
|
}
|
||||||
id, err = r.Persistable.Save(s)
|
id, err = r.Persistable.Save(s)
|
||||||
return id, err
|
return id, err
|
||||||
|
|
|
@ -9,16 +9,16 @@ type Share struct {
|
||||||
UserID string `structs:"user_id" json:"userId,omitempty" orm:"column(user_id)"`
|
UserID string `structs:"user_id" json:"userId,omitempty" orm:"column(user_id)"`
|
||||||
Username string `structs:"-" json:"username,omitempty" orm:"-"`
|
Username string `structs:"-" json:"username,omitempty" orm:"-"`
|
||||||
Description string `structs:"description" json:"description,omitempty"`
|
Description string `structs:"description" json:"description,omitempty"`
|
||||||
ExpiresAt *time.Time `structs:"expires_at" json:"expiresAt,omitempty"`
|
ExpiresAt time.Time `structs:"expires_at" json:"expiresAt,omitempty"`
|
||||||
LastVisitedAt *time.Time `structs:"last_visited_at" json:"lastVisitedAt,omitempty"`
|
LastVisitedAt time.Time `structs:"last_visited_at" json:"lastVisitedAt,omitempty"`
|
||||||
ResourceIDs string `structs:"resource_ids" json:"resourceIds,omitempty" orm:"column(resource_ids)"`
|
ResourceIDs string `structs:"resource_ids" json:"resourceIds,omitempty" orm:"column(resource_ids)"`
|
||||||
ResourceType string `structs:"resource_type" json:"resourceType,omitempty"`
|
ResourceType string `structs:"resource_type" json:"resourceType,omitempty"`
|
||||||
Contents string `structs:"contents" json:"contents,omitempty"`
|
Contents string `structs:"contents" json:"contents,omitempty"`
|
||||||
Format string `structs:"format" json:"format,omitempty"`
|
Format string `structs:"format" json:"format,omitempty"`
|
||||||
MaxBitRate int `structs:"max_bit_rate" json:"maxBitRate,omitempty"`
|
MaxBitRate int `structs:"max_bit_rate" json:"maxBitRate,omitempty"`
|
||||||
VisitCount int `structs:"visit_count" json:"visitCount,omitempty"`
|
VisitCount int `structs:"visit_count" json:"visitCount,omitempty"`
|
||||||
CreatedAt *time.Time `structs:"created_at" json:"createdAt,omitempty"`
|
CreatedAt time.Time `structs:"created_at" json:"createdAt,omitempty"`
|
||||||
UpdatedAt *time.Time `structs:"updated_at" json:"updatedAt,omitempty"`
|
UpdatedAt time.Time `structs:"updated_at" json:"updatedAt,omitempty"`
|
||||||
Tracks []ShareTrack `structs:"-" json:"tracks,omitempty"`
|
Tracks []ShareTrack `structs:"-" json:"tracks,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,8 +51,7 @@ func (r *shareRepository) Update(id string, entity interface{}, cols ...string)
|
||||||
s := entity.(*model.Share)
|
s := entity.(*model.Share)
|
||||||
// TODO Validate record
|
// TODO Validate record
|
||||||
s.ID = id
|
s.ID = id
|
||||||
now := time.Now()
|
s.UpdatedAt = time.Now()
|
||||||
s.UpdatedAt = &now
|
|
||||||
cols = append(cols, "updated_at")
|
cols = append(cols, "updated_at")
|
||||||
_, err := r.put(id, s, cols...)
|
_, err := r.put(id, s, cols...)
|
||||||
if errors.Is(err, model.ErrNotFound) {
|
if errors.Is(err, model.ErrNotFound) {
|
||||||
|
@ -68,9 +67,8 @@ func (r *shareRepository) Save(entity interface{}) (string, error) {
|
||||||
if s.UserID == "" {
|
if s.UserID == "" {
|
||||||
s.UserID = u.ID
|
s.UserID = u.ID
|
||||||
}
|
}
|
||||||
now := time.Now()
|
s.CreatedAt = time.Now()
|
||||||
s.CreatedAt = &now
|
s.UpdatedAt = time.Now()
|
||||||
s.UpdatedAt = &now
|
|
||||||
id, err := r.put(s.ID, s)
|
id, err := r.put(s.ID, s)
|
||||||
if errors.Is(err, model.ErrNotFound) {
|
if errors.Is(err, model.ErrNotFound) {
|
||||||
return "", rest.ErrNotFound
|
return "", rest.ErrNotFound
|
||||||
|
|
53
server/public/handle_images.go
Normal file
53
server/public/handle_images.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,9 @@
|
||||||
package shares
|
package public
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"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/core/auth"
|
||||||
"github.com/navidrome/navidrome/log"
|
"github.com/navidrome/navidrome/log"
|
||||||
"github.com/navidrome/navidrome/model"
|
"github.com/navidrome/navidrome/model"
|
||||||
|
@ -16,34 +11,6 @@ import (
|
||||||
"github.com/navidrome/navidrome/ui"
|
"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) {
|
func (p *Router) handleShares(w http.ResponseWriter, r *http.Request) {
|
||||||
id := r.URL.Query().Get(":id")
|
id := r.URL.Query().Get(":id")
|
||||||
if id == "" {
|
if id == "" {
|
||||||
|
@ -82,6 +49,7 @@ func (p *Router) mapShareInfo(s *model.Share) *model.Share {
|
||||||
Tracks: s.Tracks,
|
Tracks: s.Tracks,
|
||||||
}
|
}
|
||||||
for i := range s.Tracks {
|
for i := range s.Tracks {
|
||||||
|
// TODO Use Encode(Artwork)ID?
|
||||||
claims := map[string]any{"id": s.Tracks[i].ID}
|
claims := map[string]any{"id": s.Tracks[i].ID}
|
||||||
if s.Format != "" {
|
if s.Format != "" {
|
||||||
claims["f"] = s.Format
|
claims["f"] = s.Format
|
||||||
|
@ -89,7 +57,7 @@ func (p *Router) mapShareInfo(s *model.Share) *model.Share {
|
||||||
if s.MaxBitRate != 0 {
|
if s.MaxBitRate != 0 {
|
||||||
claims["b"] = s.MaxBitRate
|
claims["b"] = s.MaxBitRate
|
||||||
}
|
}
|
||||||
id, _ := auth.CreateExpiringPublicToken(*s.ExpiresAt, claims)
|
id, _ := auth.CreateExpiringPublicToken(s.ExpiresAt, claims)
|
||||||
mapped.Tracks[i].ID = id
|
mapped.Tracks[i].ID = id
|
||||||
}
|
}
|
||||||
return mapped
|
return mapped
|
|
@ -1,29 +1,32 @@
|
||||||
package public
|
package public
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"path"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"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"
|
||||||
"github.com/navidrome/navidrome/core/artwork"
|
"github.com/navidrome/navidrome/core/artwork"
|
||||||
"github.com/navidrome/navidrome/log"
|
|
||||||
"github.com/navidrome/navidrome/model"
|
"github.com/navidrome/navidrome/model"
|
||||||
"github.com/navidrome/navidrome/server"
|
"github.com/navidrome/navidrome/server"
|
||||||
"github.com/navidrome/navidrome/utils"
|
"github.com/navidrome/navidrome/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Router struct {
|
type Router struct {
|
||||||
http.Handler
|
http.Handler
|
||||||
artwork artwork.Artwork
|
artwork artwork.Artwork
|
||||||
streamer core.MediaStreamer
|
streamer core.MediaStreamer
|
||||||
|
share core.Share
|
||||||
|
assetsHandler http.Handler
|
||||||
|
ds model.DataStore
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(artwork artwork.Artwork, streamer core.MediaStreamer) *Router {
|
func New(ds model.DataStore, artwork artwork.Artwork, streamer core.MediaStreamer, share core.Share) *Router {
|
||||||
p := &Router{artwork: artwork, streamer: streamer}
|
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()
|
p.Handler = p.routes()
|
||||||
|
|
||||||
return p
|
return p
|
||||||
|
@ -34,48 +37,12 @@ func (p *Router) routes() http.Handler {
|
||||||
|
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(server.URLParamsMiddleware)
|
r.Use(server.URLParamsMiddleware)
|
||||||
r.HandleFunc("/s/{id}", p.handleStream)
|
|
||||||
r.HandleFunc("/img/{id}", p.handleImages)
|
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
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -36,7 +36,7 @@ import config, { shareInfo } from './config'
|
||||||
import { setDispatch, startEventStream, stopEventStream } from './eventStream'
|
import { setDispatch, startEventStream, stopEventStream } from './eventStream'
|
||||||
import { keyMap } from './hotkeys'
|
import { keyMap } from './hotkeys'
|
||||||
import useChangeThemeColor from './useChangeThemeColor'
|
import useChangeThemeColor from './useChangeThemeColor'
|
||||||
import ShareApp from './ShareApp'
|
import SharePlayer from './SharePlayer'
|
||||||
|
|
||||||
const history = createHashHistory()
|
const history = createHashHistory()
|
||||||
|
|
||||||
|
@ -141,7 +141,7 @@ const Admin = (props) => {
|
||||||
|
|
||||||
const AppWithHotkeys = () => {
|
const AppWithHotkeys = () => {
|
||||||
if (config.devEnableShare && shareInfo) {
|
if (config.devEnableShare && shareInfo) {
|
||||||
return <ShareApp />
|
return <SharePlayer />
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<HotKeys keyMap={keyMap}>
|
<HotKeys keyMap={keyMap}>
|
||||||
|
|
|
@ -2,12 +2,12 @@ import ReactJkMusicPlayer from 'navidrome-music-player'
|
||||||
import config, { shareInfo } from './config'
|
import config, { shareInfo } from './config'
|
||||||
import { baseUrl } from './utils'
|
import { baseUrl } from './utils'
|
||||||
|
|
||||||
const ShareApp = (props) => {
|
const SharePlayer = () => {
|
||||||
const list = shareInfo?.tracks.map((s) => {
|
const list = shareInfo?.tracks.map((s) => {
|
||||||
return {
|
return {
|
||||||
name: s.title,
|
name: s.title,
|
||||||
musicSrc: baseUrl(config.publicBaseUrl + '/s/' + s.id),
|
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,
|
singer: s.artist,
|
||||||
duration: s.duration,
|
duration: s.duration,
|
||||||
}
|
}
|
||||||
|
@ -19,8 +19,10 @@ const ShareApp = (props) => {
|
||||||
showDownload: false,
|
showDownload: false,
|
||||||
showReload: false,
|
showReload: false,
|
||||||
showMediaSession: true,
|
showMediaSession: true,
|
||||||
|
theme: 'auto',
|
||||||
|
showThemeSwitch: false,
|
||||||
}
|
}
|
||||||
return <ReactJkMusicPlayer {...options} />
|
return <ReactJkMusicPlayer {...options} />
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ShareApp
|
export default SharePlayer
|
|
@ -133,6 +133,7 @@ const AlbumActions = ({
|
||||||
close={shareDialog.close}
|
close={shareDialog.close}
|
||||||
ids={[record.id]}
|
ids={[record.id]}
|
||||||
resource={'album'}
|
resource={'album'}
|
||||||
|
title={`Share album '${record.name}'`}
|
||||||
/>
|
/>
|
||||||
</TopToolbar>
|
</TopToolbar>
|
||||||
)
|
)
|
||||||
|
@ -146,7 +147,6 @@ AlbumActions.propTypes = {
|
||||||
AlbumActions.defaultProps = {
|
AlbumActions.defaultProps = {
|
||||||
record: {},
|
record: {},
|
||||||
selectedIds: [],
|
selectedIds: [],
|
||||||
onUnselectItems: () => null,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AlbumActions
|
export default AlbumActions
|
||||||
|
|
|
@ -28,7 +28,6 @@ const defaultConfig = {
|
||||||
enableCoverAnimation: true,
|
enableCoverAnimation: true,
|
||||||
devShowArtistPage: true,
|
devShowArtistPage: true,
|
||||||
enableReplayGain: true,
|
enableReplayGain: true,
|
||||||
shareBaseUrl: '/s',
|
|
||||||
publicBaseUrl: '/p',
|
publicBaseUrl: '/p',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@ import {
|
||||||
DialogActions,
|
DialogActions,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
FormControlLabel,
|
||||||
|
Switch,
|
||||||
} from '@material-ui/core'
|
} from '@material-ui/core'
|
||||||
import {
|
import {
|
||||||
SelectInput,
|
SelectInput,
|
||||||
|
@ -14,12 +16,12 @@ import {
|
||||||
} from 'react-admin'
|
} from 'react-admin'
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { shareUrl } from '../utils'
|
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 notify = useNotify()
|
||||||
const [format, setFormat] = useState('')
|
const [format, setFormat] = useState('')
|
||||||
const [maxBitRate, setMaxBitRate] = useState(0)
|
const [maxBitRate, setMaxBitRate] = useState(0)
|
||||||
|
const [originalFormat, setUseOriginalFormat] = useState(true)
|
||||||
const { data: formats, loading } = useGetList(
|
const { data: formats, loading } = useGetList(
|
||||||
'transcoding',
|
'transcoding',
|
||||||
{
|
{
|
||||||
|
@ -39,6 +41,17 @@ export const ShareDialog = ({ open, close, onClose, ids, resource }) => {
|
||||||
[formats, loading]
|
[formats, loading]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handleOriginal = (e) => {
|
||||||
|
const original = e.target.checked
|
||||||
|
|
||||||
|
setUseOriginalFormat(original)
|
||||||
|
|
||||||
|
if (original) {
|
||||||
|
setFormat('')
|
||||||
|
setMaxBitRate(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [createShare] = useCreate(
|
const [createShare] = useCreate(
|
||||||
'share',
|
'share',
|
||||||
{
|
{
|
||||||
|
@ -78,19 +91,19 @@ export const ShareDialog = ({ open, close, onClose, ids, resource }) => {
|
||||||
open={open}
|
open={open}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onBackdropClick={onClose}
|
onBackdropClick={onClose}
|
||||||
aria-labelledby="info-dialog-album"
|
aria-labelledby="share-dialog"
|
||||||
fullWidth={true}
|
fullWidth={true}
|
||||||
maxWidth={'sm'}
|
maxWidth={'sm'}
|
||||||
>
|
>
|
||||||
<DialogTitle id="info-dialog-album">
|
<DialogTitle id="share-dialog">{title}</DialogTitle>
|
||||||
Create a link to share your music with friends
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<SimpleForm toolbar={null} variant={'outlined'}>
|
<SimpleForm toolbar={null} variant={'outlined'}>
|
||||||
<Typography variant="body1">Select transcoding options:</Typography>
|
<FormControlLabel
|
||||||
<Typography variant="caption">
|
control={<Switch checked={originalFormat} />}
|
||||||
(Leave options empty for original quality)
|
label={'Share in original format'}
|
||||||
</Typography>
|
onChange={handleOriginal}
|
||||||
|
/>
|
||||||
|
{!originalFormat && (
|
||||||
<SelectInput
|
<SelectInput
|
||||||
source="format"
|
source="format"
|
||||||
choices={formatOptions}
|
choices={formatOptions}
|
||||||
|
@ -99,6 +112,8 @@ export const ShareDialog = ({ open, close, onClose, ids, resource }) => {
|
||||||
setFormat(event.target.value)
|
setFormat(event.target.value)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
{!originalFormat && (
|
||||||
<SelectInput
|
<SelectInput
|
||||||
source="bitrate"
|
source="bitrate"
|
||||||
choices={[
|
choices={[
|
||||||
|
@ -119,6 +134,7 @@ export const ShareDialog = ({ open, close, onClose, ids, resource }) => {
|
||||||
setMaxBitRate(event.target.value)
|
setMaxBitRate(event.target.value)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</SimpleForm>
|
</SimpleForm>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
|
|
|
@ -33,6 +33,9 @@ const ShareList = (props) => {
|
||||||
label="URL"
|
label="URL"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{r.id}
|
{r.id}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import config from '../config'
|
import config from '../config'
|
||||||
|
|
||||||
export const shareUrl = (path) => {
|
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
|
return url.href
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue