mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-29 14:37:49 +01:00
Merge branch 'master' into os-lyrics
This commit is contained in:
commit
86d2c0fdf7
151 changed files with 1418 additions and 1267 deletions
6
.github/workflows/pipeline.yml
vendored
6
.github/workflows/pipeline.yml
vendored
|
|
@ -126,7 +126,7 @@ jobs:
|
|||
path: ui/build
|
||||
|
||||
- name: Config /github/workspace folder as trusted
|
||||
uses: docker://deluan/ci-goreleaser:1.21.4-2
|
||||
uses: docker://deluan/ci-goreleaser:1.21.5-1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
|
@ -134,7 +134,7 @@ jobs:
|
|||
|
||||
- name: Run GoReleaser - SNAPSHOT
|
||||
if: startsWith(github.ref, 'refs/tags/') != true
|
||||
uses: docker://deluan/ci-goreleaser:1.21.4-2
|
||||
uses: docker://deluan/ci-goreleaser:1.21.5-1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
|
@ -142,7 +142,7 @@ jobs:
|
|||
|
||||
- name: Run GoReleaser - RELEASE
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: docker://deluan/ci-goreleaser:1.21.4-2
|
||||
uses: docker://deluan/ci-goreleaser:1.21.5-1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
|
|
|||
2
Makefile
2
Makefile
|
|
@ -9,7 +9,7 @@ GIT_SHA=source_archive
|
|||
GIT_TAG=$(patsubst navidrome-%,v%,$(notdir $(PWD)))
|
||||
endif
|
||||
|
||||
CI_RELEASER_VERSION=1.21.4-2 ## https://github.com/navidrome/ci-goreleaser
|
||||
CI_RELEASER_VERSION=1.21.5-1 ## https://github.com/navidrome/ci-goreleaser
|
||||
|
||||
setup: check_env download-deps setup-git ##@1_Run_First Install dependencies and prepare development environment
|
||||
@echo Downloading Node dependencies...
|
||||
|
|
|
|||
99
cmd/inspect.go
Normal file
99
cmd/inspect.go
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/scanner"
|
||||
"github.com/navidrome/navidrome/scanner/metadata"
|
||||
"github.com/navidrome/navidrome/tests"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var (
|
||||
extractor string
|
||||
format string
|
||||
)
|
||||
|
||||
func init() {
|
||||
inspectCmd.Flags().StringVarP(&extractor, "extractor", "x", "", "extractor to use (ffmpeg or taglib, default: auto)")
|
||||
inspectCmd.Flags().StringVarP(&format, "format", "f", "pretty", "output format (pretty, toml, yaml, json, jsonindent)")
|
||||
rootCmd.AddCommand(inspectCmd)
|
||||
}
|
||||
|
||||
var inspectCmd = &cobra.Command{
|
||||
Use: "inspect [files to inspect]",
|
||||
Short: "Inspect tags",
|
||||
Long: "Show file tags as seen by Navidrome",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
runInspector(args)
|
||||
},
|
||||
}
|
||||
|
||||
var marshalers = map[string]func(interface{}) ([]byte, error){
|
||||
"pretty": prettyMarshal,
|
||||
"toml": toml.Marshal,
|
||||
"yaml": yaml.Marshal,
|
||||
"json": json.Marshal,
|
||||
"jsonindent": func(v interface{}) ([]byte, error) {
|
||||
return json.MarshalIndent(v, "", " ")
|
||||
},
|
||||
}
|
||||
|
||||
func prettyMarshal(v interface{}) ([]byte, error) {
|
||||
out := v.([]inspectorOutput)
|
||||
var res strings.Builder
|
||||
for i := range out {
|
||||
res.WriteString(fmt.Sprintf("====================\nFile: %s\n\n", out[i].File))
|
||||
t, _ := toml.Marshal(out[i].RawTags)
|
||||
res.WriteString(fmt.Sprintf("Raw tags:\n%s\n\n", t))
|
||||
t, _ = toml.Marshal(out[i].MappedTags)
|
||||
res.WriteString(fmt.Sprintf("Mapped tags:\n%s\n\n", t))
|
||||
}
|
||||
return []byte(res.String()), nil
|
||||
}
|
||||
|
||||
type inspectorOutput struct {
|
||||
File string
|
||||
RawTags metadata.ParsedTags
|
||||
MappedTags model.MediaFile
|
||||
}
|
||||
|
||||
func runInspector(args []string) {
|
||||
if extractor != "" {
|
||||
conf.Server.Scanner.Extractor = extractor
|
||||
}
|
||||
log.Info("Using extractor", "extractor", conf.Server.Scanner.Extractor)
|
||||
md, err := metadata.Extract(args...)
|
||||
if err != nil {
|
||||
log.Fatal("Error extracting tags", err)
|
||||
}
|
||||
mapper := scanner.NewMediaFileMapper(conf.Server.MusicFolder, &tests.MockedGenreRepo{})
|
||||
marshal := marshalers[format]
|
||||
if marshal == nil {
|
||||
log.Fatal("Invalid format", "format", format)
|
||||
}
|
||||
var out []inspectorOutput
|
||||
for k, v := range md {
|
||||
if !model.IsAudioFile(k) {
|
||||
continue
|
||||
}
|
||||
if len(v.Tags) == 0 {
|
||||
continue
|
||||
}
|
||||
out = append(out, inspectorOutput{
|
||||
File: k,
|
||||
RawTags: v.Tags,
|
||||
MappedTags: mapper.ToMediaFile(v),
|
||||
})
|
||||
}
|
||||
data, _ := marshal(out)
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
|
|
@ -205,7 +205,7 @@ func Load() {
|
|||
}
|
||||
|
||||
// Print current configuration if log level is Debug
|
||||
if log.CurrentLevel() >= log.LevelDebug {
|
||||
if log.IsGreaterOrEqualTo(log.LevelDebug) {
|
||||
prettyConf := pretty.Sprintf("Loaded configuration from '%s': %# v", Server.ConfigFile, Server)
|
||||
if Server.EnableLogRedacting {
|
||||
prettyConf = log.Redact(prettyConf)
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ var audioFormats = map[string]format{
|
|||
".dsf": {typ: "audio/dsd", lossless: true},
|
||||
".wv": {typ: "audio/x-wavpack", lossless: true},
|
||||
".wvp": {typ: "audio/x-wavpack", lossless: true},
|
||||
".tak": {typ: "audio/tak", lossless: true},
|
||||
".mka": {typ: "audio/x-matroska"},
|
||||
}
|
||||
var imageFormats = map[string]string{
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ func (a *Agents) GetSimilarArtists(ctx context.Context, id, name, mbid string, l
|
|||
}
|
||||
similar, err := agent.GetSimilarArtists(ctx, id, name, mbid, limit)
|
||||
if len(similar) > 0 && err == nil {
|
||||
if log.CurrentLevel() >= log.LevelTrace {
|
||||
if log.IsGreaterOrEqualTo(log.LevelTrace) {
|
||||
log.Debug(ctx, "Got Similar Artists", "agent", ag.AgentName(), "artist", name, "similar", similar, "elapsed", time.Since(start))
|
||||
} else {
|
||||
log.Debug(ctx, "Got Similar Artists", "agent", ag.AgentName(), "artist", name, "similarReceived", len(similar), "elapsed", time.Since(start))
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import (
|
|||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/server"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
)
|
||||
|
||||
//go:embed token_received.html
|
||||
|
|
@ -89,13 +89,14 @@ func (s *Router) unlink(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
func (s *Router) callback(w http.ResponseWriter, r *http.Request) {
|
||||
token := utils.ParamString(r, "token")
|
||||
if token == "" {
|
||||
p := req.Params(r)
|
||||
token, err := p.String("token")
|
||||
if err != nil {
|
||||
_ = rest.RespondWithError(w, http.StatusBadRequest, "token not received")
|
||||
return
|
||||
}
|
||||
uid := utils.ParamString(r, "uid")
|
||||
if uid == "" {
|
||||
uid, err := p.String("uid")
|
||||
if err != nil {
|
||||
_ = rest.RespondWithError(w, http.StatusBadRequest, "uid not received")
|
||||
return
|
||||
}
|
||||
|
|
@ -103,7 +104,7 @@ func (s *Router) callback(w http.ResponseWriter, r *http.Request) {
|
|||
// Need to add user to context, as this is a non-authenticated endpoint, so it does not
|
||||
// automatically contain any user info
|
||||
ctx := request.WithUser(r.Context(), model.User{ID: uid})
|
||||
err := s.fetchSessionKey(ctx, uid, token)
|
||||
err = s.fetchSessionKey(ctx, uid, token)
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ func (a *archiver) addFileToZip(ctx context.Context, z *zip.Writer, mf model.Med
|
|||
}
|
||||
|
||||
defer func() {
|
||||
if err := r.Close(); err != nil && log.CurrentLevel() >= log.LevelDebug {
|
||||
if err := r.Close(); err != nil && log.IsGreaterOrEqualTo(log.LevelDebug) {
|
||||
log.Error(ctx, "Error closing stream", "id", mf.ID, "file", mf.Path, err)
|
||||
}
|
||||
}()
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ type ffCmd struct {
|
|||
func (j *ffCmd) start() error {
|
||||
cmd := exec.Command(j.args[0], j.args[1:]...) // #nosec
|
||||
cmd.Stdout = j.out
|
||||
if log.CurrentLevel() >= log.LevelTrace {
|
||||
if log.IsGreaterOrEqualTo(log.LevelTrace) {
|
||||
cmd.Stderr = os.Stderr
|
||||
} else {
|
||||
cmd.Stderr = io.Discard
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
//go:build beep
|
||||
|
||||
package beepaudio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/faiface/beep"
|
||||
"github.com/faiface/beep/flac"
|
||||
"github.com/faiface/beep/mp3"
|
||||
"github.com/faiface/beep/wav"
|
||||
"github.com/navidrome/navidrome/core/ffmpeg"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
)
|
||||
|
||||
func DecodeMp3(path string) (s beep.StreamSeekCloser, format beep.Format, err error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, beep.Format{}, err
|
||||
}
|
||||
return mp3.Decode(f)
|
||||
}
|
||||
|
||||
func DecodeWAV(path string) (s beep.StreamSeekCloser, format beep.Format, err error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, beep.Format{}, err
|
||||
}
|
||||
return wav.Decode(f)
|
||||
}
|
||||
|
||||
func DecodeFLAC(path string) (s beep.StreamSeekCloser, format beep.Format, fileToCleanup string, err error) {
|
||||
// TODO: Turn this into a semi-parallel operation: start playing while still transcoding/copying
|
||||
log.Debug("decode to FLAC", "filename", path)
|
||||
fFmpeg := ffmpeg.New()
|
||||
readCloser, err := fFmpeg.ConvertToFLAC(context.TODO(), path)
|
||||
if err != nil {
|
||||
log.Error("error converting file to FLAC", path, err)
|
||||
return nil, beep.Format{}, "", err
|
||||
}
|
||||
|
||||
tempFile, err := os.CreateTemp("", "*.flac")
|
||||
|
||||
if err != nil {
|
||||
log.Error("error creating temp file", err)
|
||||
return nil, beep.Format{}, "", err
|
||||
}
|
||||
log.Debug("created tempfile", "filename", tempFile.Name())
|
||||
|
||||
written, err := io.Copy(tempFile, readCloser)
|
||||
if err != nil {
|
||||
log.Error("error coping file", "dest", tempFile.Name())
|
||||
}
|
||||
log.Debug("copy pipe into tempfile", "bytes written", written, "filename", tempFile.Name())
|
||||
|
||||
f, err := os.Open(tempFile.Name())
|
||||
if err != nil {
|
||||
log.Error("could not re-open tempfile", "filename", tempFile.Name())
|
||||
return nil, beep.Format{}, "", err
|
||||
}
|
||||
|
||||
s, format, err = flac.Decode(f)
|
||||
return s, format, tempFile.Name(), err
|
||||
}
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
//go:build beep
|
||||
|
||||
package beepaudio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/faiface/beep"
|
||||
"github.com/faiface/beep/effects"
|
||||
"github.com/faiface/beep/speaker"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
)
|
||||
|
||||
type BeepTrack struct {
|
||||
MediaFile model.MediaFile
|
||||
Ctrl *beep.Ctrl
|
||||
Volume *effects.Volume
|
||||
ActiveStream beep.StreamSeekCloser
|
||||
TempfileToCleanup string
|
||||
SampleRate beep.SampleRate
|
||||
PlaybackDone chan bool
|
||||
}
|
||||
|
||||
func NewTrack(playbackDoneChannel chan bool, mf model.MediaFile) (*BeepTrack, error) {
|
||||
t := BeepTrack{}
|
||||
|
||||
contentType := mf.ContentType()
|
||||
log.Debug("loading track", "trackname", mf.Path, "mediatype", contentType)
|
||||
|
||||
var streamer beep.StreamSeekCloser
|
||||
var format beep.Format
|
||||
var err error
|
||||
var tmpfileToCleanup = ""
|
||||
|
||||
switch contentType {
|
||||
case "audio/mpeg":
|
||||
streamer, format, err = DecodeMp3(mf.Path)
|
||||
case "audio/x-wav":
|
||||
streamer, format, err = DecodeWAV(mf.Path)
|
||||
case "audio/mp4":
|
||||
streamer, format, tmpfileToCleanup, err = DecodeFLAC(mf.Path)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported content type: %s", contentType)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// save running stream for closing when switching tracks
|
||||
t.ActiveStream = streamer
|
||||
t.TempfileToCleanup = tmpfileToCleanup
|
||||
|
||||
log.Debug("Setting up audio device")
|
||||
t.Ctrl = &beep.Ctrl{Streamer: streamer, Paused: true}
|
||||
t.Volume = &effects.Volume{Streamer: t.Ctrl, Base: 2}
|
||||
t.SampleRate = format.SampleRate
|
||||
t.PlaybackDone = playbackDoneChannel
|
||||
t.MediaFile = mf
|
||||
|
||||
err = speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
log.Debug("speaker.Init() finished")
|
||||
|
||||
go func() {
|
||||
speaker.Play(beep.Seq(t.Volume, beep.Callback(func() {
|
||||
log.Info("Hitting end-of-stream, signalling on channel")
|
||||
t.PlaybackDone <- true
|
||||
log.Debug("Signalling finished")
|
||||
})))
|
||||
log.Debug("dropping out of speaker.Play()")
|
||||
}()
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (t *BeepTrack) String() string {
|
||||
return fmt.Sprintf("Name: %s", t.MediaFile.Path)
|
||||
}
|
||||
|
||||
func (t *BeepTrack) SetVolume(value float64) {
|
||||
speaker.Lock()
|
||||
t.Volume.Volume += value
|
||||
speaker.Unlock()
|
||||
}
|
||||
|
||||
func (t *BeepTrack) Unpause() {
|
||||
speaker.Lock()
|
||||
if t.Ctrl.Paused {
|
||||
t.Ctrl.Paused = false
|
||||
} else {
|
||||
log.Debug("tried to unpause while not paused")
|
||||
}
|
||||
speaker.Unlock()
|
||||
}
|
||||
|
||||
func (t *BeepTrack) Pause() {
|
||||
speaker.Lock()
|
||||
if t.Ctrl.Paused {
|
||||
log.Debug("tried to pause while already paused")
|
||||
} else {
|
||||
t.Ctrl.Paused = true
|
||||
}
|
||||
speaker.Unlock()
|
||||
}
|
||||
|
||||
func (t *BeepTrack) Close() {
|
||||
if t.ActiveStream != nil {
|
||||
log.Debug("closing activ stream")
|
||||
t.ActiveStream.Close()
|
||||
t.ActiveStream = nil
|
||||
}
|
||||
|
||||
speaker.Close()
|
||||
|
||||
if t.TempfileToCleanup != "" {
|
||||
log.Debug("Removing tempfile", "tmpfilename", t.TempfileToCleanup)
|
||||
err := os.Remove(t.TempfileToCleanup)
|
||||
if err != nil {
|
||||
log.Error("error cleaning up tempfile: ", t.TempfileToCleanup)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Position returns the playback position in seconds
|
||||
func (t *BeepTrack) Position() int {
|
||||
if t.Ctrl.Streamer == nil {
|
||||
log.Debug("streamer is not setup (nil), could not get position")
|
||||
return 0
|
||||
}
|
||||
|
||||
streamer, ok := t.Ctrl.Streamer.(beep.StreamSeeker)
|
||||
if ok {
|
||||
position := t.SampleRate.D(streamer.Position())
|
||||
posSecs := position.Round(time.Second).Seconds()
|
||||
return int(posSecs)
|
||||
} else {
|
||||
log.Debug("streamer is no beep.StreamSeeker, could not get position")
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// offset = pd.PlaybackQueue.Offset
|
||||
func (t *BeepTrack) SetPosition(offset int) error {
|
||||
streamer, ok := t.Ctrl.Streamer.(beep.StreamSeeker)
|
||||
if ok {
|
||||
sampleRatePerSecond := t.SampleRate.N(time.Second)
|
||||
nextPosition := sampleRatePerSecond * offset
|
||||
log.Debug("SetPosition", "samplerate", sampleRatePerSecond, "nextPosition", nextPosition)
|
||||
return streamer.Seek(nextPosition)
|
||||
}
|
||||
return fmt.Errorf("streamer is not seekable")
|
||||
}
|
||||
|
||||
func (t *BeepTrack) IsPlaying() bool {
|
||||
return t.Ctrl != nil && !t.Ctrl.Paused
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package playback
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/navidrome/navidrome/core/playback/mpv"
|
||||
|
|
@ -17,9 +18,10 @@ type Track interface {
|
|||
Position() int
|
||||
SetPosition(offset int) error
|
||||
Close()
|
||||
String() string
|
||||
}
|
||||
|
||||
type PlaybackDevice struct {
|
||||
type playbackDevice struct {
|
||||
ParentPlaybackServer PlaybackServer
|
||||
Default bool
|
||||
User string
|
||||
|
|
@ -43,7 +45,7 @@ const DefaultGain float32 = 1.0
|
|||
|
||||
var EmptyStatus = DeviceStatus{CurrentIndex: -1, Playing: false, Gain: DefaultGain, Position: 0}
|
||||
|
||||
func (pd *PlaybackDevice) getStatus() DeviceStatus {
|
||||
func (pd *playbackDevice) getStatus() DeviceStatus {
|
||||
pos := 0
|
||||
if pd.ActiveTrack != nil {
|
||||
pos = pd.ActiveTrack.Position()
|
||||
|
|
@ -59,8 +61,8 @@ func (pd *PlaybackDevice) getStatus() DeviceStatus {
|
|||
// NewPlaybackDevice creates a new playback device which implements all the basic Jukebox mode commands defined here:
|
||||
// http://www.subsonic.org/pages/api.jsp#jukeboxControl
|
||||
// Starts the trackSwitcher goroutine for the device.
|
||||
func NewPlaybackDevice(playbackServer PlaybackServer, name string, deviceName string) *PlaybackDevice {
|
||||
return &PlaybackDevice{
|
||||
func NewPlaybackDevice(playbackServer PlaybackServer, name string, deviceName string) *playbackDevice {
|
||||
return &playbackDevice{
|
||||
ParentPlaybackServer: playbackServer,
|
||||
User: "",
|
||||
Name: name,
|
||||
|
|
@ -72,22 +74,24 @@ func NewPlaybackDevice(playbackServer PlaybackServer, name string, deviceName st
|
|||
}
|
||||
}
|
||||
|
||||
func (pd *PlaybackDevice) String() string {
|
||||
func (pd *playbackDevice) String() string {
|
||||
return fmt.Sprintf("Name: %s, Gain: %.4f, Loaded track: %s", pd.Name, pd.Gain, pd.ActiveTrack)
|
||||
}
|
||||
|
||||
func (pd *PlaybackDevice) Get(ctx context.Context) (model.MediaFiles, DeviceStatus, error) {
|
||||
log.Debug(ctx, "processing Get action")
|
||||
func (pd *playbackDevice) Get(ctx context.Context) (model.MediaFiles, DeviceStatus, error) {
|
||||
log.Debug(ctx, "Processing Get action", "device", pd)
|
||||
return pd.PlaybackQueue.Get(), pd.getStatus(), nil
|
||||
}
|
||||
|
||||
func (pd *PlaybackDevice) Status(ctx context.Context) (DeviceStatus, error) {
|
||||
func (pd *playbackDevice) Status(ctx context.Context) (DeviceStatus, error) {
|
||||
log.Debug(ctx, fmt.Sprintf("processing Status action on: %s, queue: %s", pd, pd.PlaybackQueue))
|
||||
return pd.getStatus(), nil
|
||||
}
|
||||
|
||||
// set is similar to a clear followed by a add, but will not change the currently playing track.
|
||||
func (pd *PlaybackDevice) Set(ctx context.Context, ids []string) (DeviceStatus, error) {
|
||||
// Set is similar to a clear followed by a add, but will not change the currently playing track.
|
||||
func (pd *playbackDevice) Set(ctx context.Context, ids []string) (DeviceStatus, error) {
|
||||
log.Debug(ctx, "Processing Set action", "ids", ids, "device", pd)
|
||||
|
||||
_, err := pd.Clear(ctx)
|
||||
if err != nil {
|
||||
log.Error(ctx, "error setting tracks", ids)
|
||||
|
|
@ -96,8 +100,8 @@ func (pd *PlaybackDevice) Set(ctx context.Context, ids []string) (DeviceStatus,
|
|||
return pd.Add(ctx, ids)
|
||||
}
|
||||
|
||||
func (pd *PlaybackDevice) Start(ctx context.Context) (DeviceStatus, error) {
|
||||
log.Debug(ctx, "processing Start action")
|
||||
func (pd *playbackDevice) Start(ctx context.Context) (DeviceStatus, error) {
|
||||
log.Debug(ctx, "Processing Start action", "device", pd)
|
||||
|
||||
if !pd.TrackSwitcherStarted {
|
||||
log.Info(ctx, "Starting trackSwitcher goroutine")
|
||||
|
|
@ -127,16 +131,16 @@ func (pd *PlaybackDevice) Start(ctx context.Context) (DeviceStatus, error) {
|
|||
return pd.getStatus(), nil
|
||||
}
|
||||
|
||||
func (pd *PlaybackDevice) Stop(ctx context.Context) (DeviceStatus, error) {
|
||||
log.Debug(ctx, "processing Stop action")
|
||||
func (pd *playbackDevice) Stop(ctx context.Context) (DeviceStatus, error) {
|
||||
log.Debug(ctx, "Processing Stop action", "device", pd)
|
||||
if pd.ActiveTrack != nil {
|
||||
pd.ActiveTrack.Pause()
|
||||
}
|
||||
return pd.getStatus(), nil
|
||||
}
|
||||
|
||||
func (pd *PlaybackDevice) Skip(ctx context.Context, index int, offset int) (DeviceStatus, error) {
|
||||
log.Debug(ctx, "processing Skip action", "index", index, "offset", offset)
|
||||
func (pd *playbackDevice) Skip(ctx context.Context, index int, offset int) (DeviceStatus, error) {
|
||||
log.Debug(ctx, "Processing Skip action", "index", index, "offset", offset, "device", pd)
|
||||
|
||||
wasPlaying := pd.isPlaying()
|
||||
|
||||
|
|
@ -173,8 +177,11 @@ func (pd *PlaybackDevice) Skip(ctx context.Context, index int, offset int) (Devi
|
|||
return pd.getStatus(), nil
|
||||
}
|
||||
|
||||
func (pd *PlaybackDevice) Add(ctx context.Context, ids []string) (DeviceStatus, error) {
|
||||
log.Debug(ctx, "processing Add action")
|
||||
func (pd *playbackDevice) Add(ctx context.Context, ids []string) (DeviceStatus, error) {
|
||||
log.Debug(ctx, "Processing Add action", "ids", ids, "device", pd)
|
||||
if len(ids) < 1 {
|
||||
return pd.getStatus(), nil
|
||||
}
|
||||
|
||||
items := model.MediaFiles{}
|
||||
|
||||
|
|
@ -191,8 +198,8 @@ func (pd *PlaybackDevice) Add(ctx context.Context, ids []string) (DeviceStatus,
|
|||
return pd.getStatus(), nil
|
||||
}
|
||||
|
||||
func (pd *PlaybackDevice) Clear(ctx context.Context) (DeviceStatus, error) {
|
||||
log.Debug(ctx, fmt.Sprintf("processing Clear action on: %s", pd))
|
||||
func (pd *playbackDevice) Clear(ctx context.Context) (DeviceStatus, error) {
|
||||
log.Debug(ctx, "Processing Clear action", "device", pd)
|
||||
if pd.ActiveTrack != nil {
|
||||
pd.ActiveTrack.Pause()
|
||||
pd.ActiveTrack.Close()
|
||||
|
|
@ -202,8 +209,8 @@ func (pd *PlaybackDevice) Clear(ctx context.Context) (DeviceStatus, error) {
|
|||
return pd.getStatus(), nil
|
||||
}
|
||||
|
||||
func (pd *PlaybackDevice) Remove(ctx context.Context, index int) (DeviceStatus, error) {
|
||||
log.Debug(ctx, "processing Remove action")
|
||||
func (pd *playbackDevice) Remove(ctx context.Context, index int) (DeviceStatus, error) {
|
||||
log.Debug(ctx, "Processing Remove action", "index", index, "device", pd)
|
||||
// pausing if attempting to remove running track
|
||||
if pd.isPlaying() && pd.PlaybackQueue.Index == index {
|
||||
_, err := pd.Stop(ctx)
|
||||
|
|
@ -221,17 +228,17 @@ func (pd *PlaybackDevice) Remove(ctx context.Context, index int) (DeviceStatus,
|
|||
return pd.getStatus(), nil
|
||||
}
|
||||
|
||||
func (pd *PlaybackDevice) Shuffle(ctx context.Context) (DeviceStatus, error) {
|
||||
log.Debug(ctx, "processing Shuffle action")
|
||||
func (pd *playbackDevice) Shuffle(ctx context.Context) (DeviceStatus, error) {
|
||||
log.Debug(ctx, "Processing Shuffle action", "device", pd)
|
||||
if pd.PlaybackQueue.Size() > 1 {
|
||||
pd.PlaybackQueue.Shuffle()
|
||||
}
|
||||
return pd.getStatus(), nil
|
||||
}
|
||||
|
||||
// Used to control the playback volume. A float value between 0.0 and 1.0.
|
||||
func (pd *PlaybackDevice) SetGain(ctx context.Context, gain float32) (DeviceStatus, error) {
|
||||
log.Debug(ctx, fmt.Sprintf("processing SetGain action. Actual gain: %f, gain to set: %f", pd.Gain, gain))
|
||||
// SetGain is used to control the playback volume. A float value between 0.0 and 1.0.
|
||||
func (pd *playbackDevice) SetGain(ctx context.Context, gain float32) (DeviceStatus, error) {
|
||||
log.Debug(ctx, "Processing SetGain action", "newGain", gain, "device", pd)
|
||||
|
||||
if pd.ActiveTrack != nil {
|
||||
pd.ActiveTrack.SetVolume(gain)
|
||||
|
|
@ -241,15 +248,15 @@ func (pd *PlaybackDevice) SetGain(ctx context.Context, gain float32) (DeviceStat
|
|||
return pd.getStatus(), nil
|
||||
}
|
||||
|
||||
func (pd *PlaybackDevice) isPlaying() bool {
|
||||
func (pd *playbackDevice) isPlaying() bool {
|
||||
return pd.ActiveTrack != nil && pd.ActiveTrack.IsPlaying()
|
||||
}
|
||||
|
||||
func (pd *PlaybackDevice) trackSwitcherGoroutine() {
|
||||
log.Info("Starting trackSwitcher goroutine")
|
||||
func (pd *playbackDevice) trackSwitcherGoroutine() {
|
||||
log.Debug("Started trackSwitcher goroutine", "device", pd)
|
||||
for {
|
||||
<-pd.PlaybackDone
|
||||
log.Info("track switching detected")
|
||||
log.Debug("Track switching detected")
|
||||
if pd.ActiveTrack != nil {
|
||||
pd.ActiveTrack.Close()
|
||||
pd.ActiveTrack = nil
|
||||
|
|
@ -260,7 +267,7 @@ func (pd *PlaybackDevice) trackSwitcherGoroutine() {
|
|||
log.Debug("Switching to next song", "queue", pd.PlaybackQueue.String())
|
||||
err := pd.switchActiveTrackByIndex(pd.PlaybackQueue.Index)
|
||||
if err != nil {
|
||||
log.Error("error switching track", "error", err)
|
||||
log.Error("Error switching track", err)
|
||||
}
|
||||
pd.ActiveTrack.Unpause()
|
||||
} else {
|
||||
|
|
@ -269,11 +276,11 @@ func (pd *PlaybackDevice) trackSwitcherGoroutine() {
|
|||
}
|
||||
}
|
||||
|
||||
func (pd *PlaybackDevice) switchActiveTrackByIndex(index int) error {
|
||||
func (pd *playbackDevice) switchActiveTrackByIndex(index int) error {
|
||||
pd.PlaybackQueue.SetIndex(index)
|
||||
currentTrack := pd.PlaybackQueue.Current()
|
||||
if currentTrack == nil {
|
||||
return fmt.Errorf("could not get current track")
|
||||
return errors.New("could not get current track")
|
||||
}
|
||||
|
||||
track, err := mpv.NewTrack(pd.PlaybackDone, pd.DeviceName, *currentTrack)
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ func (j *Executor) start() error {
|
|||
j.ctx = ctx
|
||||
cmd := exec.CommandContext(ctx, j.args[0], j.args[1:]...) // #nosec
|
||||
cmd.Stdout = j.out
|
||||
if log.CurrentLevel() >= log.LevelTrace {
|
||||
if log.IsGreaterOrEqualTo(log.LevelTrace) {
|
||||
cmd.Stderr = os.Stderr
|
||||
} else {
|
||||
cmd.Stderr = io.Discard
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import (
|
|||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/DexterLB/mpvipc"
|
||||
"github.com/dexterlb/mpvipc"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
)
|
||||
|
|
@ -25,7 +25,7 @@ type MpvTrack struct {
|
|||
}
|
||||
|
||||
func NewTrack(playbackDoneChannel chan bool, deviceName string, mf model.MediaFile) (*MpvTrack, error) {
|
||||
log.Debug("loading track", "trackname", mf.Path, "mediatype", mf.ContentType())
|
||||
log.Debug("Loading track", "trackPath", mf.Path, "mediaType", mf.ContentType())
|
||||
|
||||
if _, err := mpvCommand(); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -36,14 +36,14 @@ func NewTrack(playbackDoneChannel chan bool, deviceName string, mf model.MediaFi
|
|||
args := createMPVCommand(mpvComdTemplate, deviceName, mf.Path, tmpSocketName)
|
||||
exe, err := start(args)
|
||||
if err != nil {
|
||||
log.Error("error starting mpv process", "error", err)
|
||||
log.Error("Error starting mpv process", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// wait for socket to show up
|
||||
err = waitForFile(tmpSocketName, 3*time.Second, 100*time.Millisecond)
|
||||
err = waitForSocket(tmpSocketName, 3*time.Second, 100*time.Millisecond)
|
||||
if err != nil {
|
||||
log.Error("error or timeout waiting for control socket", "socketname", tmpSocketName, "error", err)
|
||||
log.Error("Error or timeout waiting for control socket", "socketname", tmpSocketName, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
@ -51,7 +51,7 @@ func NewTrack(playbackDoneChannel chan bool, deviceName string, mf model.MediaFi
|
|||
err = conn.Open()
|
||||
|
||||
if err != nil {
|
||||
log.Error("error opening new connection", "error", err)
|
||||
log.Error("Error opening new connection", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
@ -92,7 +92,7 @@ func (t *MpvTrack) Unpause() {
|
|||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
log.Info("unpaused track")
|
||||
log.Debug("Unpaused track", "track", t)
|
||||
}
|
||||
|
||||
func (t *MpvTrack) Pause() {
|
||||
|
|
@ -111,7 +111,7 @@ func (t *MpvTrack) Close() {
|
|||
log.Debug("sending shutdown command")
|
||||
_, err := t.Conn.Call("quit")
|
||||
if err != nil {
|
||||
log.Error("error sending quit command to mpv-ipc socket", "error", err)
|
||||
log.Error("Error sending quit command to mpv-ipc socket", err)
|
||||
|
||||
if t.Exe != nil {
|
||||
log.Debug("cancelling executor")
|
||||
|
|
@ -141,16 +141,16 @@ func (t *MpvTrack) isSocketfilePresent() bool {
|
|||
return err == nil && fileInfo != nil && !fileInfo.IsDir()
|
||||
}
|
||||
|
||||
// Position returns the playback position in seconds
|
||||
// every now and then the mpv IPC interface returns "mpv error: property unavailable"
|
||||
// Position returns the playback position in seconds.
|
||||
// Every now and then the mpv IPC interface returns "mpv error: property unavailable"
|
||||
// in this case we have to retry
|
||||
func (t *MpvTrack) Position() int {
|
||||
retryCount := 0
|
||||
for {
|
||||
position, err := t.Conn.Get("time-pos")
|
||||
if err != nil && err.Error() == "mpv error: property unavailable" {
|
||||
log.Debug("got the mpv error: property unavailable error, retry ...")
|
||||
retryCount += 1
|
||||
log.Debug("Got mpv error, retrying...", "retries", retryCount, err)
|
||||
if retryCount > 5 {
|
||||
return 0
|
||||
}
|
||||
|
|
@ -158,13 +158,13 @@ func (t *MpvTrack) Position() int {
|
|||
}
|
||||
|
||||
if err != nil {
|
||||
log.Error("error getting position in track", "error", err)
|
||||
log.Error("Error getting position in track", err)
|
||||
return 0
|
||||
}
|
||||
|
||||
pos, ok := position.(float64)
|
||||
if !ok {
|
||||
log.Error("could not cast position from mpv into float64")
|
||||
log.Error("Could not cast position from mpv into float64", "position", position)
|
||||
return 0
|
||||
} else {
|
||||
return int(pos)
|
||||
|
|
@ -181,7 +181,7 @@ func (t *MpvTrack) SetPosition(offset int) error {
|
|||
}
|
||||
err := t.Conn.Set("time-pos", float64(offset))
|
||||
if err != nil {
|
||||
log.Error("could not set the position in track", "offset", offset, "error", err)
|
||||
log.Error("could not set the position in track", "offset", offset, err)
|
||||
return err
|
||||
}
|
||||
log.Info("set position", "offset", offset)
|
||||
|
|
@ -191,7 +191,7 @@ func (t *MpvTrack) SetPosition(offset int) error {
|
|||
func (t *MpvTrack) IsPlaying() bool {
|
||||
pausing, err := t.Conn.Get("pause")
|
||||
if err != nil {
|
||||
log.Error("problem getting paused status", "error", err)
|
||||
log.Error("problem getting paused status", err)
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -203,7 +203,7 @@ func (t *MpvTrack) IsPlaying() bool {
|
|||
return !pause
|
||||
}
|
||||
|
||||
func waitForFile(path string, timeout time.Duration, pause time.Duration) error {
|
||||
func waitForSocket(path string, timeout time.Duration, pause time.Duration) error {
|
||||
start := time.Now()
|
||||
end := start.Add(timeout)
|
||||
var retries int = 0
|
||||
|
|
@ -211,7 +211,7 @@ func waitForFile(path string, timeout time.Duration, pause time.Duration) error
|
|||
for {
|
||||
fileInfo, err := os.Stat(path)
|
||||
if err == nil && fileInfo != nil && !fileInfo.IsDir() {
|
||||
log.Debug("file found", "retries", retries, "waittime", time.Since(start).Microseconds())
|
||||
log.Debug("Socket found", "retries", retries, "waitTime", time.Since(start))
|
||||
return nil
|
||||
}
|
||||
if time.Now().After(end) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Package playback implements audio playback using PlaybackDevices. It is used to implement the Jukebox mode in turn.
|
||||
// It makes use of the BEEP library to do the playback. Major parts are:
|
||||
// It makes use of the MPV library to do the playback. Major parts are:
|
||||
// - decoder which includes decoding and transcoding of various audio file formats
|
||||
// - device implementing the basic functions to work with audio devices like set, play, stop, skip, ...
|
||||
// - queue a simple playlist
|
||||
|
|
@ -19,7 +19,7 @@ import (
|
|||
|
||||
type PlaybackServer interface {
|
||||
Run(ctx context.Context) error
|
||||
GetDeviceForUser(user string) (*PlaybackDevice, error)
|
||||
GetDeviceForUser(user string) (*playbackDevice, error)
|
||||
GetMediaFile(id string) (*model.MediaFile, error)
|
||||
GetCtx() *context.Context
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ type PlaybackServer interface {
|
|||
type playbackServer struct {
|
||||
ctx *context.Context
|
||||
datastore model.DataStore
|
||||
playbackDevices []PlaybackDevice
|
||||
playbackDevices []playbackDevice
|
||||
}
|
||||
|
||||
// GetInstance returns the playback-server singleton
|
||||
|
|
@ -63,8 +63,8 @@ func (ps *playbackServer) GetCtx() *context.Context {
|
|||
return ps.ctx
|
||||
}
|
||||
|
||||
func (ps *playbackServer) initDeviceStatus(devices []conf.AudioDeviceDefinition, defaultDevice string) ([]PlaybackDevice, error) {
|
||||
pbDevices := make([]PlaybackDevice, max(1, len(devices)))
|
||||
func (ps *playbackServer) initDeviceStatus(devices []conf.AudioDeviceDefinition, defaultDevice string) ([]playbackDevice, error) {
|
||||
pbDevices := make([]playbackDevice, max(1, len(devices)))
|
||||
defaultDeviceFound := false
|
||||
|
||||
if defaultDevice == "" {
|
||||
|
|
@ -76,13 +76,13 @@ func (ps *playbackServer) initDeviceStatus(devices []conf.AudioDeviceDefinition,
|
|||
// if there is but only one entry and no default given, just use that.
|
||||
if len(devices) == 1 {
|
||||
if len(devices[0]) != 2 {
|
||||
return []PlaybackDevice{}, fmt.Errorf("audio device definition ought to contain 2 fields, found: %d ", len(devices[0]))
|
||||
return []playbackDevice{}, fmt.Errorf("audio device definition ought to contain 2 fields, found: %d ", len(devices[0]))
|
||||
}
|
||||
pbDevices[0] = *NewPlaybackDevice(ps, devices[0][0], devices[0][1])
|
||||
}
|
||||
|
||||
if len(devices) > 1 {
|
||||
return []PlaybackDevice{}, fmt.Errorf("number of audio device found is %d, but no default device defined. Set Jukebox.Default", len(devices))
|
||||
return []playbackDevice{}, fmt.Errorf("number of audio device found is %d, but no default device defined. Set Jukebox.Default", len(devices))
|
||||
}
|
||||
|
||||
pbDevices[0].Default = true
|
||||
|
|
@ -91,7 +91,7 @@ func (ps *playbackServer) initDeviceStatus(devices []conf.AudioDeviceDefinition,
|
|||
|
||||
for idx, audioDevice := range devices {
|
||||
if len(audioDevice) != 2 {
|
||||
return []PlaybackDevice{}, fmt.Errorf("audio device definition ought to contain 2 fields, found: %d ", len(audioDevice))
|
||||
return []playbackDevice{}, fmt.Errorf("audio device definition ought to contain 2 fields, found: %d ", len(audioDevice))
|
||||
}
|
||||
|
||||
pbDevices[idx] = *NewPlaybackDevice(ps, audioDevice[0], audioDevice[1])
|
||||
|
|
@ -103,18 +103,18 @@ func (ps *playbackServer) initDeviceStatus(devices []conf.AudioDeviceDefinition,
|
|||
}
|
||||
|
||||
if !defaultDeviceFound {
|
||||
return []PlaybackDevice{}, fmt.Errorf("default device name not found: %s ", defaultDevice)
|
||||
return []playbackDevice{}, fmt.Errorf("default device name not found: %s ", defaultDevice)
|
||||
}
|
||||
return pbDevices, nil
|
||||
}
|
||||
|
||||
func (ps *playbackServer) getDefaultDevice() (*PlaybackDevice, error) {
|
||||
func (ps *playbackServer) getDefaultDevice() (*playbackDevice, error) {
|
||||
for idx, audioDevice := range ps.playbackDevices {
|
||||
if audioDevice.Default {
|
||||
return &ps.playbackDevices[idx], nil
|
||||
}
|
||||
}
|
||||
return &PlaybackDevice{}, fmt.Errorf("no default device found")
|
||||
return &playbackDevice{}, fmt.Errorf("no default device found")
|
||||
}
|
||||
|
||||
// GetMediaFile retrieves the MediaFile given by the id parameter
|
||||
|
|
@ -123,12 +123,12 @@ func (ps *playbackServer) GetMediaFile(id string) (*model.MediaFile, error) {
|
|||
}
|
||||
|
||||
// GetDeviceForUser returns the audio playback device for the given user. As of now this is but only the default device.
|
||||
func (ps *playbackServer) GetDeviceForUser(user string) (*PlaybackDevice, error) {
|
||||
log.Debug("processing GetDevice")
|
||||
func (ps *playbackServer) GetDeviceForUser(user string) (*playbackDevice, error) {
|
||||
log.Debug("Processing GetDevice", "user", user)
|
||||
// README: here we might plug-in the user-device mapping one fine day
|
||||
device, err := ps.getDefaultDevice()
|
||||
if err != nil {
|
||||
return &PlaybackDevice{}, err
|
||||
return &playbackDevice{}, err
|
||||
}
|
||||
device.User = user
|
||||
return device, nil
|
||||
|
|
|
|||
17
go.mod
17
go.mod
|
|
@ -3,12 +3,12 @@ module github.com/navidrome/navidrome
|
|||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/DexterLB/mpvipc v0.0.0-20230829142118-145d6eabdc37
|
||||
github.com/Masterminds/squirrel v1.5.4
|
||||
github.com/ReneKroon/ttlcache/v2 v2.11.0
|
||||
github.com/bradleyjkemp/cupaloy/v2 v2.8.0
|
||||
github.com/deluan/rest v0.0.0-20211102003136-6260bc399cbf
|
||||
github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1
|
||||
github.com/dexterlb/mpvipc v0.0.0-20230829142118-145d6eabdc37
|
||||
github.com/dhowden/tag v0.0.0-20230630033851-978a0926ee25
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/djherbis/atime v1.1.0
|
||||
|
|
@ -16,7 +16,6 @@ require (
|
|||
github.com/djherbis/stream v1.4.0
|
||||
github.com/djherbis/times v1.6.0
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/faiface/beep v1.1.0
|
||||
github.com/fatih/structs v1.1.0
|
||||
github.com/go-chi/chi/v5 v5.0.10
|
||||
github.com/go-chi/cors v1.2.1
|
||||
|
|
@ -28,12 +27,13 @@ require (
|
|||
github.com/kr/pretty v0.3.1
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.18
|
||||
github.com/matoous/go-nanoid/v2 v2.0.0
|
||||
github.com/mattn/go-sqlite3 v1.14.18
|
||||
github.com/mattn/go-sqlite3 v1.14.19
|
||||
github.com/mattn/go-zglob v0.0.4
|
||||
github.com/microcosm-cc/bluemonday v1.0.26
|
||||
github.com/mileusna/useragent v1.3.4
|
||||
github.com/onsi/ginkgo/v2 v2.13.2
|
||||
github.com/onsi/gomega v1.30.0
|
||||
github.com/pelletier/go-toml/v2 v2.0.6
|
||||
github.com/pocketbase/dbx v1.10.1
|
||||
github.com/pressly/goose/v3 v3.15.1
|
||||
github.com/prometheus/client_golang v1.17.0
|
||||
|
|
@ -65,11 +65,8 @@ require (
|
|||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20230323073829-e72429f035bd // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/hajimehoshi/go-mp3 v0.3.4 // indirect
|
||||
github.com/hajimehoshi/oto v1.0.1 // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/icza/bitio v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
|
|
@ -81,11 +78,7 @@ require (
|
|||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/mewkiz/flac v1.0.7 // indirect
|
||||
github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
|
||||
github.com/prometheus/common v0.44.0 // indirect
|
||||
|
|
@ -99,9 +92,7 @@ require (
|
|||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
go.uber.org/goleak v1.1.11 // indirect
|
||||
golang.org/x/crypto v0.16.0 // indirect
|
||||
golang.org/x/exp/shiny v0.0.0-20230515195305-f3d0a9c9a5cc // indirect
|
||||
golang.org/x/mobile v0.0.0-20230427221453-e8d11dd0ba41 // indirect
|
||||
golang.org/x/crypto v0.17.0 // indirect
|
||||
golang.org/x/net v0.18.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/tools v0.15.0 // indirect
|
||||
|
|
|
|||
51
go.sum
51
go.sum
|
|
@ -38,9 +38,6 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
|
|||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/DexterLB/mpvipc v0.0.0-20230829142118-145d6eabdc37 h1:/oQBAuySCcme0DLhicWkr7FaAT5nh1XbbbnCMR2WdPA=
|
||||
github.com/DexterLB/mpvipc v0.0.0-20230829142118-145d6eabdc37/go.mod h1:nMVB54ifXmC1hpgfq7gTpotbv891pd2wAX/whuUj1q4=
|
||||
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
|
||||
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
|
||||
github.com/ReneKroon/ttlcache/v2 v2.11.0 h1:OvlcYFYi941SBN3v9dsDcC2N8vRxyHcCmJb3Vl4QMoM=
|
||||
|
|
@ -63,7 +60,6 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht
|
|||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
|
@ -74,6 +70,8 @@ github.com/deluan/rest v0.0.0-20211102003136-6260bc399cbf h1:tb246l2Zmpt/GpF9EcH
|
|||
github.com/deluan/rest v0.0.0-20211102003136-6260bc399cbf/go.mod h1:tSgDythFsl0QgS/PFWfIZqcJKnkADWneY80jaVRlqK8=
|
||||
github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1 h1:mGvOb3zxl4vCLv+dbf7JA6CAaM2UH/AGP1KX4DsJmTI=
|
||||
github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1/go.mod h1:ZNCLJfehvEf34B7BbLKjgpsL9lyW7q938w/GY1XgV4E=
|
||||
github.com/dexterlb/mpvipc v0.0.0-20230829142118-145d6eabdc37 h1:s+qNFsO3VsdsKroqapcogQxcQBHrRPDK1nVxGc+HBbg=
|
||||
github.com/dexterlb/mpvipc v0.0.0-20230829142118-145d6eabdc37/go.mod h1:CXCwawNJCtFDip7gvbaQVgw0cGjldpyHDIp7oA5prOg=
|
||||
github.com/dhowden/tag v0.0.0-20230630033851-978a0926ee25 h1:simG0vMYFvNriGhaaat7QVVkaVkXzvqcohaBoLZl9Hg=
|
||||
github.com/dhowden/tag v0.0.0-20230630033851-978a0926ee25/go.mod h1:Z3Lomva4pyMWYezjMAU5QWRh0p1VvO4199OHlFnyKkM=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
|
|
@ -94,19 +92,12 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
|
|||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/faiface/beep v1.1.0 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c=
|
||||
github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
|
||||
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
|
||||
github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
|
||||
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
|
||||
github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE=
|
||||
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
|
||||
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||
|
|
@ -198,14 +189,6 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGa
|
|||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
|
||||
github.com/hajimehoshi/go-mp3 v0.3.4 h1:NUP7pBYH8OguP4diaTZ9wJbUbk3tC0KlfzsEpWmYj68=
|
||||
github.com/hajimehoshi/go-mp3 v0.3.4/go.mod h1:fRtZraRFcWb0pu7ok0LqyFhCUrPeMsGRSVop0eemFmo=
|
||||
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
|
||||
github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
|
||||
github.com/hajimehoshi/oto v1.0.1 h1:8AMnq0Yr2YmzaiqTg/k1Yzd6IygUGk2we9nmjgbgPn4=
|
||||
github.com/hajimehoshi/oto v1.0.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
|
||||
github.com/hajimehoshi/oto/v2 v2.3.1/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo=
|
||||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
|
|
@ -216,14 +199,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
|||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/icza/bitio v1.0.0 h1:squ/m1SHyFeCA6+6Gyol1AxV9nmPPlJFT8c2vKdj3U8=
|
||||
github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
|
||||
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6 h1:8UsGZ2rr2ksmEru6lToqnXgA8Mz1DP11X4zSJ159C3k=
|
||||
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk=
|
||||
github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
|
|
@ -257,7 +234,6 @@ github.com/lestrrat-go/jwx/v2 v2.0.18/go.mod h1:fAJ+k5eTgKdDqanzCuK6DAt3W7n3cs2/
|
|||
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
||||
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U=
|
||||
|
|
@ -265,17 +241,12 @@ github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL
|
|||
github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI=
|
||||
github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
|
||||
github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-zglob v0.0.4 h1:LQi2iOm0/fGgu80AioIJ/1j9w9Oh+9DZ39J4VAGzHQM=
|
||||
github.com/mattn/go-zglob v0.0.4/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/mewkiz/flac v1.0.7 h1:uIXEjnuXqdRaZttmSFM5v5Ukp4U6orrZsnYGGR3yow8=
|
||||
github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU=
|
||||
github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2 h1:EyTNMdePWaoWsRSGQnXiSoQu0r6RS1eA557AwJhlzHU=
|
||||
github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
|
||||
github.com/mileusna/useragent v1.3.4 h1:MiuRRuvGjEie1+yZHO88UBYg8YBC/ddF6T7F56i3PCk=
|
||||
|
|
@ -289,8 +260,6 @@ github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8P
|
|||
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
|
||||
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
|
|
@ -382,8 +351,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
|||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
|
||||
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
|
|
@ -396,9 +366,6 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
|
|||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA=
|
||||
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
|
||||
golang.org/x/exp/shiny v0.0.0-20230515195305-f3d0a9c9a5cc h1:JMi0oO0NoPZTAzHSdkdUoHbdcLfo9nPtK37kzE6I3Hk=
|
||||
golang.org/x/exp/shiny v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0=
|
||||
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
|
|
@ -416,10 +383,7 @@ golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPI
|
|||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mobile v0.0.0-20230427221453-e8d11dd0ba41 h1:539vykMVJsmdiucRtMmdeLLZaTVhWhaAHFcPabj2lws=
|
||||
golang.org/x/mobile v0.0.0-20230427221453-e8d11dd0ba41/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
|
|
@ -499,12 +463,10 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
|
|||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
|
@ -536,7 +498,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ func levelFromString(l string) Level {
|
|||
return level
|
||||
}
|
||||
|
||||
// SetLogLevels sets the log levels for specific paths in the codebase.
|
||||
func SetLogLevels(levels map[string]string) {
|
||||
for k, v := range levels {
|
||||
logLevels = append(logLevels, levelPath{path: k, level: levelFromString(v)})
|
||||
|
|
@ -154,6 +155,11 @@ func CurrentLevel() Level {
|
|||
return currentLevel
|
||||
}
|
||||
|
||||
// IsGreaterOrEqualTo returns true if the caller's current log level is equal or greater than the provided level.
|
||||
func IsGreaterOrEqualTo(level Level) bool {
|
||||
return shouldLog(level)
|
||||
}
|
||||
|
||||
func Fatal(args ...interface{}) {
|
||||
log(LevelFatal, args...)
|
||||
os.Exit(1)
|
||||
|
|
|
|||
|
|
@ -137,6 +137,37 @@ var _ = Describe("Logger", func() {
|
|||
})
|
||||
})
|
||||
|
||||
Describe("IsGreaterOrEqualTo", func() {
|
||||
It("returns false if log level is below provided level", func() {
|
||||
SetLevel(LevelError)
|
||||
Expect(IsGreaterOrEqualTo(LevelWarn)).To(BeFalse())
|
||||
})
|
||||
|
||||
It("returns true if log level is equal to provided level", func() {
|
||||
SetLevel(LevelWarn)
|
||||
Expect(IsGreaterOrEqualTo(LevelWarn)).To(BeTrue())
|
||||
})
|
||||
|
||||
It("returns true if log level is above provided level", func() {
|
||||
SetLevel(LevelTrace)
|
||||
Expect(IsGreaterOrEqualTo(LevelDebug)).To(BeTrue())
|
||||
})
|
||||
|
||||
It("returns true if log level for the current code path is equal provided level", func() {
|
||||
SetLevel(LevelError)
|
||||
SetLogLevels(map[string]string{
|
||||
"log/log_test": "debug",
|
||||
})
|
||||
|
||||
// Need to nest it in a function to get the correct code path
|
||||
var result = func() bool {
|
||||
return IsGreaterOrEqualTo(LevelDebug)
|
||||
}()
|
||||
|
||||
Expect(result).To(BeTrue())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("extractLogger", func() {
|
||||
It("returns an error if the context is nil", func() {
|
||||
_, err := extractLogger(nil)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package model
|
||||
|
||||
type Genre struct {
|
||||
ID string `structs:"id" json:"id"`
|
||||
ID string `structs:"id" json:"id,omitempty" toml:"id,omitempty" yaml:"id,omitempty"`
|
||||
Name string `structs:"name" json:"name"`
|
||||
SongCount int `structs:"-" json:"-"`
|
||||
AlbumCount int `structs:"-" json:"-"`
|
||||
SongCount int `structs:"-" json:"-" toml:"-" yaml:"-"`
|
||||
AlbumCount int `structs:"-" json:"-" toml:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
type Genres []Genre
|
||||
|
|
|
|||
|
|
@ -15,20 +15,20 @@ import (
|
|||
"github.com/navidrome/navidrome/utils"
|
||||
)
|
||||
|
||||
type mediaFileMapper struct {
|
||||
type MediaFileMapper struct {
|
||||
rootFolder string
|
||||
genres model.GenreRepository
|
||||
}
|
||||
|
||||
func newMediaFileMapper(rootFolder string, genres model.GenreRepository) *mediaFileMapper {
|
||||
return &mediaFileMapper{
|
||||
func NewMediaFileMapper(rootFolder string, genres model.GenreRepository) *MediaFileMapper {
|
||||
return &MediaFileMapper{
|
||||
rootFolder: rootFolder,
|
||||
genres: genres,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Move most of these mapping functions to setters in the model.MediaFile
|
||||
func (s mediaFileMapper) toMediaFile(md metadata.Tags) model.MediaFile {
|
||||
func (s MediaFileMapper) ToMediaFile(md metadata.Tags) model.MediaFile {
|
||||
mf := &model.MediaFile{}
|
||||
mf.ID = s.trackID(md)
|
||||
mf.Year, mf.Date, mf.OriginalYear, mf.OriginalDate, mf.ReleaseYear, mf.ReleaseDate = s.mapDates(md)
|
||||
|
|
@ -86,7 +86,7 @@ func sanitizeFieldForSorting(originalValue string) string {
|
|||
return utils.NoArticle(v)
|
||||
}
|
||||
|
||||
func (s mediaFileMapper) mapTrackTitle(md metadata.Tags) string {
|
||||
func (s MediaFileMapper) mapTrackTitle(md metadata.Tags) string {
|
||||
if md.Title() == "" {
|
||||
s := strings.TrimPrefix(md.FilePath(), s.rootFolder+string(os.PathSeparator))
|
||||
e := filepath.Ext(s)
|
||||
|
|
@ -95,7 +95,7 @@ func (s mediaFileMapper) mapTrackTitle(md metadata.Tags) string {
|
|||
return md.Title()
|
||||
}
|
||||
|
||||
func (s mediaFileMapper) mapAlbumArtistName(md metadata.Tags) string {
|
||||
func (s MediaFileMapper) mapAlbumArtistName(md metadata.Tags) string {
|
||||
switch {
|
||||
case md.AlbumArtist() != "":
|
||||
return md.AlbumArtist()
|
||||
|
|
@ -108,14 +108,14 @@ func (s mediaFileMapper) mapAlbumArtistName(md metadata.Tags) string {
|
|||
}
|
||||
}
|
||||
|
||||
func (s mediaFileMapper) mapArtistName(md metadata.Tags) string {
|
||||
func (s MediaFileMapper) mapArtistName(md metadata.Tags) string {
|
||||
if md.Artist() != "" {
|
||||
return md.Artist()
|
||||
}
|
||||
return consts.UnknownArtist
|
||||
}
|
||||
|
||||
func (s mediaFileMapper) mapAlbumName(md metadata.Tags) string {
|
||||
func (s MediaFileMapper) mapAlbumName(md metadata.Tags) string {
|
||||
name := md.Album()
|
||||
if name == "" {
|
||||
return consts.UnknownAlbum
|
||||
|
|
@ -123,11 +123,11 @@ func (s mediaFileMapper) mapAlbumName(md metadata.Tags) string {
|
|||
return name
|
||||
}
|
||||
|
||||
func (s mediaFileMapper) trackID(md metadata.Tags) string {
|
||||
func (s MediaFileMapper) trackID(md metadata.Tags) string {
|
||||
return fmt.Sprintf("%x", md5.Sum([]byte(md.FilePath())))
|
||||
}
|
||||
|
||||
func (s mediaFileMapper) albumID(md metadata.Tags, releaseDate string) string {
|
||||
func (s MediaFileMapper) albumID(md metadata.Tags, releaseDate string) string {
|
||||
albumPath := strings.ToLower(fmt.Sprintf("%s\\%s", s.mapAlbumArtistName(md), s.mapAlbumName(md)))
|
||||
if !conf.Server.Scanner.GroupAlbumReleases {
|
||||
if len(releaseDate) != 0 {
|
||||
|
|
@ -137,15 +137,15 @@ func (s mediaFileMapper) albumID(md metadata.Tags, releaseDate string) string {
|
|||
return fmt.Sprintf("%x", md5.Sum([]byte(albumPath)))
|
||||
}
|
||||
|
||||
func (s mediaFileMapper) artistID(md metadata.Tags) string {
|
||||
func (s MediaFileMapper) artistID(md metadata.Tags) string {
|
||||
return fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(s.mapArtistName(md)))))
|
||||
}
|
||||
|
||||
func (s mediaFileMapper) albumArtistID(md metadata.Tags) string {
|
||||
func (s MediaFileMapper) albumArtistID(md metadata.Tags) string {
|
||||
return fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(s.mapAlbumArtistName(md)))))
|
||||
}
|
||||
|
||||
func (s mediaFileMapper) mapGenres(genres []string) (string, model.Genres) {
|
||||
func (s MediaFileMapper) mapGenres(genres []string) (string, model.Genres) {
|
||||
var result model.Genres
|
||||
unique := map[string]struct{}{}
|
||||
var all []string
|
||||
|
|
@ -174,10 +174,13 @@ func (s mediaFileMapper) mapGenres(genres []string) (string, model.Genres) {
|
|||
return result[0].Name, result
|
||||
}
|
||||
|
||||
func (s mediaFileMapper) mapDates(md metadata.Tags) (int, string, int, string, int, string) {
|
||||
year, date := md.Date()
|
||||
originalYear, originalDate := md.OriginalDate()
|
||||
releaseYear, releaseDate := md.ReleaseDate()
|
||||
func (s MediaFileMapper) mapDates(md metadata.Tags) (year int, date string,
|
||||
originalYear int, originalDate string,
|
||||
releaseYear int, releaseDate string) {
|
||||
// Start with defaults
|
||||
year, date = md.Date()
|
||||
originalYear, originalDate = md.OriginalDate()
|
||||
releaseYear, releaseDate = md.ReleaseDate()
|
||||
|
||||
// MusicBrainz Picard writes the Release Date of an album to the Date tag, and leaves the Release Date tag empty
|
||||
taggedLikePicard := (originalYear != 0) &&
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ import (
|
|||
)
|
||||
|
||||
var _ = Describe("mapping", func() {
|
||||
Describe("mediaFileMapper", func() {
|
||||
var mapper *mediaFileMapper
|
||||
Describe("MediaFileMapper", func() {
|
||||
var mapper *MediaFileMapper
|
||||
Describe("mapTrackTitle", func() {
|
||||
BeforeEach(func() {
|
||||
mapper = newMediaFileMapper("/music", nil)
|
||||
mapper = NewMediaFileMapper("/music", nil)
|
||||
})
|
||||
It("returns the Title when it is available", func() {
|
||||
md := metadata.NewTag("/music/artist/album01/Song.mp3", nil, metadata.ParsedTags{"title": []string{"This is not a love song"}})
|
||||
|
|
@ -37,7 +37,7 @@ var _ = Describe("mapping", func() {
|
|||
ds := &tests.MockDataStore{}
|
||||
gr = ds.Genre(ctx)
|
||||
gr = newCachedGenreRepository(ctx, gr)
|
||||
mapper = newMediaFileMapper("/", gr)
|
||||
mapper = NewMediaFileMapper("/", gr)
|
||||
})
|
||||
|
||||
It("returns empty if no genres are available", func() {
|
||||
|
|
@ -79,7 +79,7 @@ var _ = Describe("mapping", func() {
|
|||
Describe("mapDates", func() {
|
||||
var md metadata.Tags
|
||||
BeforeEach(func() {
|
||||
mapper = newMediaFileMapper("/", nil)
|
||||
mapper = NewMediaFileMapper("/", nil)
|
||||
})
|
||||
Context("when all date fields are provided", func() {
|
||||
BeforeEach(func() {
|
||||
|
|
|
|||
|
|
@ -59,13 +59,39 @@ func Extract(files ...string) (map[string]Tags, error) {
|
|||
}
|
||||
|
||||
func NewTag(filePath string, fileInfo os.FileInfo, tags ParsedTags) Tags {
|
||||
for t, values := range tags {
|
||||
values = removeDuplicatesAndEmpty(values)
|
||||
if len(values) == 0 {
|
||||
delete(tags, t)
|
||||
continue
|
||||
}
|
||||
tags[t] = values
|
||||
}
|
||||
return Tags{
|
||||
filePath: filePath,
|
||||
fileInfo: fileInfo,
|
||||
tags: tags,
|
||||
Tags: tags,
|
||||
}
|
||||
}
|
||||
|
||||
func removeDuplicatesAndEmpty(values []string) []string {
|
||||
encountered := map[string]struct{}{}
|
||||
empty := true
|
||||
var result []string
|
||||
for _, v := range values {
|
||||
if _, ok := encountered[v]; ok {
|
||||
continue
|
||||
}
|
||||
encountered[v] = struct{}{}
|
||||
empty = empty && v == ""
|
||||
result = append(result, v)
|
||||
}
|
||||
if empty {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
type ParsedTags map[string][]string
|
||||
|
||||
func (p ParsedTags) Map(customMappings ParsedTags) ParsedTags {
|
||||
|
|
@ -86,7 +112,7 @@ func (p ParsedTags) Map(customMappings ParsedTags) ParsedTags {
|
|||
type Tags struct {
|
||||
filePath string
|
||||
fileInfo os.FileInfo
|
||||
tags ParsedTags
|
||||
Tags ParsedTags
|
||||
}
|
||||
|
||||
// Common tags
|
||||
|
|
@ -232,7 +258,7 @@ func (t Tags) getPeakValue(tagName string) float64 {
|
|||
|
||||
func (t Tags) getTags(tagNames ...string) []string {
|
||||
for _, tag := range tagNames {
|
||||
if v, ok := t.tags[tag]; ok {
|
||||
if v, ok := t.Tags[tag]; ok {
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
|
@ -250,7 +276,7 @@ func (t Tags) getFirstTagValue(tagNames ...string) string {
|
|||
func (t Tags) getAllTagValues(tagNames ...string) []string {
|
||||
var values []string
|
||||
for _, tag := range tagNames {
|
||||
if v, ok := t.tags[tag]; ok {
|
||||
if v, ok := t.Tags[tag]; ok {
|
||||
values = append(values, v...)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ var _ = Describe("Tags", func() {
|
|||
DescribeTable("getDate",
|
||||
func(tag string, expectedYear int, expectedDate string) {
|
||||
md := &Tags{}
|
||||
md.tags = map[string][]string{"date": {tag}}
|
||||
md.Tags = map[string][]string{"date": {tag}}
|
||||
testYear, testDate := md.Date()
|
||||
Expect(testYear).To(Equal(expectedYear))
|
||||
Expect(testDate).To(Equal(expectedDate))
|
||||
|
|
@ -29,7 +29,7 @@ var _ = Describe("Tags", func() {
|
|||
Describe("getMbzID", func() {
|
||||
It("return a valid MBID", func() {
|
||||
md := &Tags{}
|
||||
md.tags = map[string][]string{
|
||||
md.Tags = map[string][]string{
|
||||
"musicbrainz_trackid": {"8f84da07-09a0-477b-b216-cc982dabcde1"},
|
||||
"musicbrainz_releasetrackid": {"6caf16d3-0b20-3fe6-8020-52e31831bc11"},
|
||||
"musicbrainz_albumid": {"f68c985d-f18b-4f4a-b7f0-87837cf3fbf9"},
|
||||
|
|
@ -44,7 +44,7 @@ var _ = Describe("Tags", func() {
|
|||
})
|
||||
It("return empty string for invalid MBID", func() {
|
||||
md := &Tags{}
|
||||
md.tags = map[string][]string{
|
||||
md.Tags = map[string][]string{
|
||||
"musicbrainz_trackid": {"11406732-6"},
|
||||
"musicbrainz_albumid": {"11406732"},
|
||||
"musicbrainz_artistid": {"200455"},
|
||||
|
|
@ -60,7 +60,7 @@ var _ = Describe("Tags", func() {
|
|||
Describe("getAllTagValues", func() {
|
||||
It("returns values from all tag names", func() {
|
||||
md := &Tags{}
|
||||
md.tags = map[string][]string{
|
||||
md.Tags = map[string][]string{
|
||||
"genre": {"Rock", "Pop", "New Wave"},
|
||||
}
|
||||
|
||||
|
|
@ -68,10 +68,31 @@ var _ = Describe("Tags", func() {
|
|||
})
|
||||
})
|
||||
|
||||
Describe("removeDuplicatesAndEmpty", func() {
|
||||
It("removes duplicates", func() {
|
||||
md := NewTag("/music/artist/album01/Song.mp3", nil, ParsedTags{
|
||||
"genre": []string{"pop", "rock", "pop"},
|
||||
"date": []string{"2023-03-01", "2023-03-01"},
|
||||
"mood": []string{"happy", "sad"},
|
||||
})
|
||||
Expect(md.Tags).To(HaveKeyWithValue("genre", []string{"pop", "rock"}))
|
||||
Expect(md.Tags).To(HaveKeyWithValue("date", []string{"2023-03-01"}))
|
||||
Expect(md.Tags).To(HaveKeyWithValue("mood", []string{"happy", "sad"}))
|
||||
})
|
||||
It("removes empty tags", func() {
|
||||
md := NewTag("/music/artist/album01/Song.mp3", nil, ParsedTags{
|
||||
"genre": []string{"pop", "rock", "pop"},
|
||||
"mood": []string{"", ""},
|
||||
})
|
||||
Expect(md.Tags).To(HaveKeyWithValue("genre", []string{"pop", "rock"}))
|
||||
Expect(md.Tags).ToNot(HaveKey("mood"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Bpm", func() {
|
||||
var t *Tags
|
||||
BeforeEach(func() {
|
||||
t = &Tags{tags: map[string][]string{
|
||||
t = &Tags{Tags: map[string][]string{
|
||||
"fbpm": []string{"141.7"},
|
||||
}}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ package taglib
|
|||
*/
|
||||
import "C"
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
|
|
@ -57,7 +58,13 @@ func Read(filename string) (tags map[string][]string, err error) {
|
|||
case C.TAGLIB_ERR_AUDIO_PROPS:
|
||||
return nil, fmt.Errorf("can't get audio properties from file")
|
||||
}
|
||||
log.Trace("TagLib: read tags", "tags", m, "filename", filename, "id", id)
|
||||
if log.IsGreaterOrEqualTo(log.LevelDebug) {
|
||||
j, _ := json.Marshal(m)
|
||||
log.Trace("TagLib: read tags", "tags", string(j), "filename", filename, "id", id)
|
||||
} else {
|
||||
log.Trace("TagLib: read tags", "tags", m, "filename", filename, "id", id)
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
|
|
@ -114,6 +121,10 @@ func do_put_map(id C.ulong, key string, val *C.char) {
|
|||
m[key] = append(m[key], v)
|
||||
}
|
||||
|
||||
/*
|
||||
As I'm working on the new scanner, I see that the `properties` from TagLib is ill-suited to extract multi-valued ID3 frames. I'll have to change the way we do it for ID3, probably by sending the raw frames to Go and mapping there, instead of relying on the auto-mapped `properties`. I think this would reduce our reliance on C++, while also giving us more flexibility, including parsing the USLT / SYLT frames in Go
|
||||
*/
|
||||
|
||||
//export go_map_put_int
|
||||
func go_map_put_int(id C.ulong, key *C.char, val C.int) {
|
||||
valStr := strconv.Itoa(int(val))
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ type TagScanner struct {
|
|||
ds model.DataStore
|
||||
plsSync *playlistImporter
|
||||
cnt *counters
|
||||
mapper *mediaFileMapper
|
||||
mapper *MediaFileMapper
|
||||
cacheWarmer artwork.CacheWarmer
|
||||
}
|
||||
|
||||
|
|
@ -100,7 +100,7 @@ func (s *TagScanner) Scan(ctx context.Context, lastModifiedSince time.Time, prog
|
|||
var changedDirs []string
|
||||
s.cnt = &counters{}
|
||||
genres := newCachedGenreRepository(ctx, s.ds.Genre(ctx))
|
||||
s.mapper = newMediaFileMapper(s.rootFolder, genres)
|
||||
s.mapper = NewMediaFileMapper(s.rootFolder, genres)
|
||||
refresher := newRefresher(s.ds, s.cacheWarmer, allFSDirs)
|
||||
|
||||
log.Trace(ctx, "Loading directory tree from music folder", "folder", s.rootFolder)
|
||||
|
|
@ -386,7 +386,7 @@ func (s *TagScanner) loadTracks(filePaths []string) (model.MediaFiles, error) {
|
|||
|
||||
var mfs model.MediaFiles
|
||||
for _, md := range mds {
|
||||
mf := s.mapper.toMediaFile(md)
|
||||
mf := s.mapper.ToMediaFile(md)
|
||||
mfs = append(mfs, mf)
|
||||
}
|
||||
return mfs, nil
|
||||
|
|
|
|||
|
|
@ -10,11 +10,12 @@ var _ = Describe("TagScanner", func() {
|
|||
It("return all audio files from the folder", func() {
|
||||
files, err := loadAllAudioFiles("tests/fixtures")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(files).To(HaveLen(11))
|
||||
Expect(files).To(HaveLen(12))
|
||||
Expect(files).To(HaveKey("tests/fixtures/test.aiff"))
|
||||
Expect(files).To(HaveKey("tests/fixtures/test.flac"))
|
||||
Expect(files).To(HaveKey("tests/fixtures/test.m4a"))
|
||||
Expect(files).To(HaveKey("tests/fixtures/test.mp3"))
|
||||
Expect(files).To(HaveKey("tests/fixtures/test.tak"))
|
||||
Expect(files).To(HaveKey("tests/fixtures/test.ogg"))
|
||||
Expect(files).To(HaveKey("tests/fixtures/test.wav"))
|
||||
Expect(files).To(HaveKey("tests/fixtures/test.wma"))
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ var _ = Describe("walk_dir_tree", func() {
|
|||
Expect(collected[baseDir]).To(MatchFields(IgnoreExtras, Fields{
|
||||
"Images": BeEmpty(),
|
||||
"HasPlaylist": BeFalse(),
|
||||
"AudioFilesCount": BeNumerically("==", 12),
|
||||
"AudioFilesCount": BeNumerically("==", 13),
|
||||
}))
|
||||
Expect(collected[filepath.Join(baseDir, "artist", "an-album")]).To(MatchFields(IgnoreExtras, Fields{
|
||||
"Images": ConsistOf("cover.jpg", "front.png", "artist.png"),
|
||||
|
|
|
|||
|
|
@ -42,15 +42,13 @@ type (
|
|||
username string
|
||||
userAgent string
|
||||
clientUniqueId string
|
||||
displayString string
|
||||
msgC chan message
|
||||
}
|
||||
)
|
||||
|
||||
func (c client) String() string {
|
||||
if log.CurrentLevel() >= log.LevelTrace {
|
||||
return fmt.Sprintf("%s (%s - %s - %s - %s)", c.id, c.username, c.address, c.clientUniqueId, c.userAgent)
|
||||
}
|
||||
return fmt.Sprintf("%s (%s - %s - %s)", c.id, c.username, c.address, c.clientUniqueId)
|
||||
return c.displayString
|
||||
}
|
||||
|
||||
type broker struct {
|
||||
|
|
@ -172,6 +170,12 @@ func (b *broker) subscribe(r *http.Request) client {
|
|||
userAgent: r.UserAgent(),
|
||||
clientUniqueId: clientUniqueId,
|
||||
}
|
||||
if log.IsGreaterOrEqualTo(log.LevelTrace) {
|
||||
c.displayString = fmt.Sprintf("%s (%s - %s - %s - %s)", c.id, c.username, c.address, c.clientUniqueId, c.userAgent)
|
||||
} else {
|
||||
c.displayString = fmt.Sprintf("%s (%s - %s - %s)", c.id, c.username, c.address, c.clientUniqueId)
|
||||
}
|
||||
|
||||
c.msgC = make(chan message, bufferSize)
|
||||
|
||||
// Signal the broker that we have a new client
|
||||
|
|
@ -260,7 +264,7 @@ func sendOrDrop(client client, msg message) {
|
|||
select {
|
||||
case client.msgC <- msg:
|
||||
default:
|
||||
if log.CurrentLevel() >= log.LevelTrace {
|
||||
if log.IsGreaterOrEqualTo(log.LevelTrace) {
|
||||
log.Trace("Event dropped because client's channel is full", "event", msg, "client", client.String())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ func requestLogger(next http.Handler) http.Handler {
|
|||
"httpStatus", ww.Status(),
|
||||
"responseSize", ww.BytesWritten(),
|
||||
}
|
||||
if log.CurrentLevel() >= log.LevelDebug {
|
||||
if log.IsGreaterOrEqualTo(log.LevelDebug) {
|
||||
logArgs = append(logArgs, "userAgent", r.UserAgent())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import (
|
|||
"github.com/navidrome/navidrome/core"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
)
|
||||
|
||||
type restHandler = func(rest.RepositoryConstructor, ...rest.Logger) http.HandlerFunc
|
||||
|
|
@ -95,8 +95,9 @@ func handleExportPlaylist(ds model.DataStore) http.HandlerFunc {
|
|||
|
||||
func deleteFromPlaylist(ds model.DataStore) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
playlistId := utils.ParamString(r, ":playlistId")
|
||||
ids := r.URL.Query()["id"]
|
||||
p := req.Params(r)
|
||||
playlistId, _ := p.String(":playlistId")
|
||||
ids, _ := p.Strings("id")
|
||||
err := ds.WithTx(func(tx model.DataStore) error {
|
||||
tracksRepo := tx.Playlist(r.Context()).Tracks(playlistId, true)
|
||||
return tracksRepo.Delete(ids...)
|
||||
|
|
@ -139,7 +140,8 @@ func addToPlaylist(ds model.DataStore) http.HandlerFunc {
|
|||
}
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
playlistId := utils.ParamString(r, ":playlistId")
|
||||
p := req.Params(r)
|
||||
playlistId, _ := p.String(":playlistId")
|
||||
var payload addTracksPayload
|
||||
err := json.NewDecoder(r.Body).Decode(&payload)
|
||||
if err != nil {
|
||||
|
|
@ -183,8 +185,9 @@ func reorderItem(ds model.DataStore) http.HandlerFunc {
|
|||
}
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
playlistId := utils.ParamString(r, ":playlistId")
|
||||
id := utils.ParamInt(r, ":id", 0)
|
||||
p := req.Params(r)
|
||||
playlistId, _ := p.String(":playlistId")
|
||||
id := p.IntOr(":id", 0)
|
||||
if id == 0 {
|
||||
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -2,15 +2,17 @@ package public
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
)
|
||||
|
||||
func (p *Router) handleDownloads(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.URL.Query().Get(":id")
|
||||
if id == "" {
|
||||
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||
func (pub *Router) handleDownloads(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := req.Params(r).String(":id")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err := p.archiver.ZipShare(r.Context(), id, w)
|
||||
err = pub.archiver.ZipShare(r.Context(), id, w)
|
||||
checkShareError(r.Context(), w, err, id)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@ import (
|
|||
"github.com/navidrome/navidrome/core/artwork"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
)
|
||||
|
||||
func (p *Router) handleImages(w http.ResponseWriter, r *http.Request) {
|
||||
func (pub *Router) handleImages(w http.ResponseWriter, r *http.Request) {
|
||||
// If context is already canceled, discard request without further processing
|
||||
if r.Context().Err() != nil {
|
||||
return
|
||||
|
|
@ -21,7 +21,9 @@ 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")
|
||||
|
||||
p := req.Params(r)
|
||||
id, _ := p.String(":id")
|
||||
if id == "" {
|
||||
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||
return
|
||||
|
|
@ -32,9 +34,9 @@ func (p *Router) handleImages(w http.ResponseWriter, r *http.Request) {
|
|||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
size := utils.ParamInt(r, "size", 0)
|
||||
size := p.IntOr("size", 0)
|
||||
|
||||
imgReader, lastUpdate, err := p.artwork.Get(ctx, artId, size)
|
||||
imgReader, lastUpdate, err := pub.artwork.Get(ctx, artId, size)
|
||||
switch {
|
||||
case errors.Is(err, context.Canceled):
|
||||
return
|
||||
|
|
|
|||
|
|
@ -10,31 +10,32 @@ import (
|
|||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/server"
|
||||
"github.com/navidrome/navidrome/ui"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
)
|
||||
|
||||
func (p *Router) handleShares(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.URL.Query().Get(":id")
|
||||
if id == "" {
|
||||
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||
func (pub *Router) handleShares(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := req.Params(r).String(":id")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// If requested file is a UI asset, just serve it
|
||||
_, err := ui.BuildAssets().Open(id)
|
||||
_, err = ui.BuildAssets().Open(id)
|
||||
if err == nil {
|
||||
p.assetsHandler.ServeHTTP(w, r)
|
||||
pub.assetsHandler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// If it is not, consider it a share ID
|
||||
s, err := p.share.Load(r.Context(), id)
|
||||
s, err := pub.share.Load(r.Context(), id)
|
||||
if err != nil {
|
||||
checkShareError(r.Context(), w, err, id)
|
||||
return
|
||||
}
|
||||
|
||||
s = p.mapShareInfo(r, *s)
|
||||
server.IndexWithShare(p.ds, ui.BuildAssets(), s)(w, r)
|
||||
s = pub.mapShareInfo(r, *s)
|
||||
server.IndexWithShare(pub.ds, ui.BuildAssets(), s)(w, r)
|
||||
}
|
||||
|
||||
func checkShareError(ctx context.Context, w http.ResponseWriter, err error, id string) {
|
||||
|
|
@ -54,7 +55,7 @@ func checkShareError(ctx context.Context, w http.ResponseWriter, err error, id s
|
|||
}
|
||||
}
|
||||
|
||||
func (p *Router) mapShareInfo(r *http.Request, s model.Share) *model.Share {
|
||||
func (pub *Router) mapShareInfo(r *http.Request, s model.Share) *model.Share {
|
||||
s.URL = ShareURL(r, s.ID)
|
||||
s.ImageURL = ImageURL(r, s.CoverArtID(), consts.UICoverArtSize)
|
||||
for i := range s.Tracks {
|
||||
|
|
|
|||
|
|
@ -10,12 +10,13 @@ import (
|
|||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
"github.com/navidrome/navidrome/core/auth"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
)
|
||||
|
||||
func (p *Router) handleStream(w http.ResponseWriter, r *http.Request) {
|
||||
func (pub *Router) handleStream(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
tokenId := r.URL.Query().Get(":id")
|
||||
p := req.Params(r)
|
||||
tokenId, _ := p.String(":id")
|
||||
info, err := decodeStreamInfo(tokenId)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error parsing shared stream info", err)
|
||||
|
|
@ -23,7 +24,7 @@ func (p *Router) handleStream(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
stream, err := p.streamer.NewStream(ctx, info.id, info.format, info.bitrate, 0)
|
||||
stream, err := pub.streamer.NewStream(ctx, info.id, info.format, info.bitrate, 0)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error starting shared stream", err)
|
||||
http.Error(w, "invalid request", http.StatusInternalServerError)
|
||||
|
|
@ -31,7 +32,7 @@ func (p *Router) handleStream(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
// 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 {
|
||||
if err := stream.Close(); err != nil && log.IsGreaterOrEqualTo(log.LevelDebug) {
|
||||
log.Error("Error closing shared stream", "id", info.id, "file", stream.Name(), err)
|
||||
}
|
||||
}()
|
||||
|
|
@ -46,7 +47,7 @@ func (p *Router) handleStream(w http.ResponseWriter, r *http.Request) {
|
|||
w.Header().Set("Accept-Ranges", "none")
|
||||
w.Header().Set("Content-Type", stream.ContentType())
|
||||
|
||||
estimateContentLength := utils.ParamBool(r, "estimateContentLength", false)
|
||||
estimateContentLength := p.BoolOr("estimateContentLength", false)
|
||||
|
||||
// if Client requests the estimated content-length, send it
|
||||
if estimateContentLength {
|
||||
|
|
@ -59,7 +60,7 @@ func (p *Router) handleStream(w http.ResponseWriter, r *http.Request) {
|
|||
go func() { _, _ = io.Copy(io.Discard, stream) }()
|
||||
} else {
|
||||
c, err := io.Copy(w, stream)
|
||||
if log.CurrentLevel() >= log.LevelDebug {
|
||||
if log.IsGreaterOrEqualTo(log.LevelDebug) {
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error sending shared transcoded file", "id", info.id, err)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ func New(ds model.DataStore, artwork artwork.Artwork, streamer core.MediaStreame
|
|||
return p
|
||||
}
|
||||
|
||||
func (p *Router) routes() http.Handler {
|
||||
func (pub *Router) routes() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
|
|
@ -48,16 +48,16 @@ func (p *Router) routes() http.Handler {
|
|||
r.Use(middleware.ThrottleBacklog(conf.Server.DevArtworkMaxRequests, conf.Server.DevArtworkThrottleBacklogLimit,
|
||||
conf.Server.DevArtworkThrottleBacklogTimeout))
|
||||
}
|
||||
r.HandleFunc("/img/{id}", p.handleImages)
|
||||
r.HandleFunc("/img/{id}", pub.handleImages)
|
||||
})
|
||||
if conf.Server.EnableSharing {
|
||||
r.HandleFunc("/s/{id}", p.handleStream)
|
||||
r.HandleFunc("/s/{id}", pub.handleStream)
|
||||
if conf.Server.EnableDownloads {
|
||||
r.HandleFunc("/d/{id}", p.handleDownloads)
|
||||
r.HandleFunc("/d/{id}", pub.handleDownloads)
|
||||
}
|
||||
r.HandleFunc("/{id}", p.handleShares)
|
||||
r.HandleFunc("/", p.handleShares)
|
||||
r.Handle("/*", p.assetsHandler)
|
||||
r.HandleFunc("/{id}", pub.handleShares)
|
||||
r.HandleFunc("/", pub.handleShares)
|
||||
r.Handle("/*", pub.assetsHandler)
|
||||
}
|
||||
})
|
||||
return r
|
||||
|
|
|
|||
|
|
@ -10,12 +10,13 @@ import (
|
|||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/server/subsonic/filter"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
"github.com/navidrome/navidrome/utils/number"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
)
|
||||
|
||||
func (api *Router) getAlbumList(r *http.Request) (model.Albums, int64, error) {
|
||||
typ, err := requiredParamString(r, "type")
|
||||
p := req.Params(r)
|
||||
typ, err := p.String("type")
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
|
@ -39,17 +40,17 @@ func (api *Router) getAlbumList(r *http.Request) (model.Albums, int64, error) {
|
|||
case "highest":
|
||||
opts = filter.AlbumsByRating()
|
||||
case "byGenre":
|
||||
genre, err := requiredParamString(r, "genre")
|
||||
genre, err := p.String("genre")
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
opts = filter.AlbumsByGenre(genre)
|
||||
case "byYear":
|
||||
fromYear, err := requiredParamInt(r, "fromYear")
|
||||
fromYear, err := p.Int("fromYear")
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
toYear, err := requiredParamInt(r, "toYear")
|
||||
toYear, err := p.Int("toYear")
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
|
@ -59,18 +60,18 @@ func (api *Router) getAlbumList(r *http.Request) (model.Albums, int64, error) {
|
|||
return nil, 0, newError(responses.ErrorGeneric, "type '%s' not implemented", typ)
|
||||
}
|
||||
|
||||
opts.Offset = utils.ParamInt(r, "offset", 0)
|
||||
opts.Max = number.Min(utils.ParamInt(r, "size", 10), 500)
|
||||
opts.Offset = p.IntOr("offset", 0)
|
||||
opts.Max = number.Min(p.IntOr("size", 10), 500)
|
||||
albums, err := api.ds.Album(r.Context()).GetAllWithoutGenres(opts)
|
||||
|
||||
if err != nil {
|
||||
log.Error(r, "Error retrieving albums", "error", err)
|
||||
log.Error(r, "Error retrieving albums", err)
|
||||
return nil, 0, newError(responses.ErrorGeneric, "internal error")
|
||||
}
|
||||
|
||||
count, err := api.ds.Album(r.Context()).CountAll(opts)
|
||||
if err != nil {
|
||||
log.Error(r, "Error counting albums", "error", err)
|
||||
log.Error(r, "Error counting albums", err)
|
||||
return nil, 0, newError(responses.ErrorGeneric, "internal error")
|
||||
}
|
||||
|
||||
|
|
@ -108,17 +109,17 @@ func (api *Router) GetStarred(r *http.Request) (*responses.Subsonic, error) {
|
|||
options := filter.Starred()
|
||||
artists, err := api.ds.Artist(ctx).GetAll(options)
|
||||
if err != nil {
|
||||
log.Error(r, "Error retrieving starred artists", "error", err)
|
||||
log.Error(r, "Error retrieving starred artists", err)
|
||||
return nil, err
|
||||
}
|
||||
albums, err := api.ds.Album(ctx).GetAllWithoutGenres(options)
|
||||
if err != nil {
|
||||
log.Error(r, "Error retrieving starred albums", "error", err)
|
||||
log.Error(r, "Error retrieving starred albums", err)
|
||||
return nil, err
|
||||
}
|
||||
mediaFiles, err := api.ds.MediaFile(ctx).GetAll(options)
|
||||
if err != nil {
|
||||
log.Error(r, "Error retrieving starred mediaFiles", "error", err)
|
||||
log.Error(r, "Error retrieving starred mediaFiles", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
@ -145,7 +146,7 @@ func (api *Router) GetNowPlaying(r *http.Request) (*responses.Subsonic, error) {
|
|||
ctx := r.Context()
|
||||
npInfo, err := api.scrobbler.GetNowPlaying(ctx)
|
||||
if err != nil {
|
||||
log.Error(r, "Error retrieving now playing list", "error", err)
|
||||
log.Error(r, "Error retrieving now playing list", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
@ -163,14 +164,15 @@ func (api *Router) GetNowPlaying(r *http.Request) (*responses.Subsonic, error) {
|
|||
}
|
||||
|
||||
func (api *Router) GetRandomSongs(r *http.Request) (*responses.Subsonic, error) {
|
||||
size := number.Min(utils.ParamInt(r, "size", 10), 500)
|
||||
genre := utils.ParamString(r, "genre")
|
||||
fromYear := utils.ParamInt(r, "fromYear", 0)
|
||||
toYear := utils.ParamInt(r, "toYear", 0)
|
||||
p := req.Params(r)
|
||||
size := number.Min(p.IntOr("size", 10), 500)
|
||||
genre, _ := p.String("genre")
|
||||
fromYear := p.IntOr("fromYear", 0)
|
||||
toYear := p.IntOr("toYear", 0)
|
||||
|
||||
songs, err := api.getSongs(r.Context(), 0, size, filter.SongsByRandom(genre, fromYear, toYear))
|
||||
if err != nil {
|
||||
log.Error(r, "Error retrieving random songs", "error", err)
|
||||
log.Error(r, "Error retrieving random songs", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
@ -181,13 +183,14 @@ func (api *Router) GetRandomSongs(r *http.Request) (*responses.Subsonic, error)
|
|||
}
|
||||
|
||||
func (api *Router) GetSongsByGenre(r *http.Request) (*responses.Subsonic, error) {
|
||||
count := number.Min(utils.ParamInt(r, "count", 10), 500)
|
||||
offset := utils.ParamInt(r, "offset", 0)
|
||||
genre := utils.ParamString(r, "genre")
|
||||
p := req.Params(r)
|
||||
count := number.Min(p.IntOr("count", 10), 500)
|
||||
offset := p.IntOr("offset", 0)
|
||||
genre, _ := p.String("genre")
|
||||
|
||||
songs, err := api.getSongs(r.Context(), offset, count, filter.SongsByGenre(genre))
|
||||
if err != nil {
|
||||
log.Error(r, "Error retrieving random songs", "error", err)
|
||||
log.Error(r, "Error retrieving random songs", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"net/http/httptest"
|
||||
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
|
|
@ -36,7 +37,7 @@ var _ = Describe("Album Lists", func() {
|
|||
})
|
||||
resp, err := router.GetAlbumList(w, r)
|
||||
|
||||
Expect(err).To(BeNil())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.AlbumList.Album[0].Id).To(Equal("1"))
|
||||
Expect(resp.AlbumList.Album[1].Id).To(Equal("2"))
|
||||
Expect(w.Header().Get("x-total-count")).To(Equal("2"))
|
||||
|
|
@ -47,12 +48,8 @@ var _ = Describe("Album Lists", func() {
|
|||
It("should fail if missing type parameter", func() {
|
||||
r := newGetRequest()
|
||||
_, err := router.GetAlbumList(w, r)
|
||||
var subErr subError
|
||||
isSubError := errors.As(err, &subErr)
|
||||
|
||||
Expect(isSubError).To(BeTrue())
|
||||
Expect(subErr).To(MatchError("required 'type' parameter is missing"))
|
||||
Expect(subErr.code).To(Equal(responses.ErrorMissingParameter))
|
||||
Expect(err).To(MatchError(req.ErrMissingParam))
|
||||
})
|
||||
|
||||
It("should return error if call fails", func() {
|
||||
|
|
@ -61,7 +58,7 @@ var _ = Describe("Album Lists", func() {
|
|||
|
||||
_, err := router.GetAlbumList(w, r)
|
||||
|
||||
Expect(err).ToNot(BeNil())
|
||||
Expect(err).To(MatchError(errSubsonic))
|
||||
var subErr subError
|
||||
errors.As(err, &subErr)
|
||||
Expect(subErr.code).To(Equal(responses.ErrorGeneric))
|
||||
|
|
@ -76,7 +73,7 @@ var _ = Describe("Album Lists", func() {
|
|||
})
|
||||
resp, err := router.GetAlbumList2(w, r)
|
||||
|
||||
Expect(err).To(BeNil())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.AlbumList2.Album[0].Id).To(Equal("1"))
|
||||
Expect(resp.AlbumList2.Album[1].Id).To(Equal("2"))
|
||||
Expect(w.Header().Get("x-total-count")).To(Equal("2"))
|
||||
|
|
@ -86,13 +83,10 @@ var _ = Describe("Album Lists", func() {
|
|||
|
||||
It("should fail if missing type parameter", func() {
|
||||
r := newGetRequest()
|
||||
|
||||
_, err := router.GetAlbumList2(w, r)
|
||||
|
||||
var subErr subError
|
||||
errors.As(err, &subErr)
|
||||
|
||||
Expect(subErr).To(MatchError("required 'type' parameter is missing"))
|
||||
Expect(subErr.code).To(Equal(responses.ErrorMissingParameter))
|
||||
Expect(err).To(MatchError(req.ErrMissingParam))
|
||||
})
|
||||
|
||||
It("should return error if call fails", func() {
|
||||
|
|
@ -101,9 +95,9 @@ var _ = Describe("Album Lists", func() {
|
|||
|
||||
_, err := router.GetAlbumList2(w, r)
|
||||
|
||||
Expect(err).To(MatchError(errSubsonic))
|
||||
var subErr subError
|
||||
errors.As(err, &subErr)
|
||||
Expect(subErr).ToNot(BeNil())
|
||||
Expect(subErr.code).To(Equal(responses.ErrorGeneric))
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import (
|
|||
"github.com/navidrome/navidrome/scanner"
|
||||
"github.com/navidrome/navidrome/server/events"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
)
|
||||
|
||||
const Version = "1.16.1"
|
||||
|
|
@ -212,20 +212,11 @@ func hr(r chi.Router, path string, f handlerRaw) {
|
|||
handle := func(w http.ResponseWriter, r *http.Request) {
|
||||
res, err := f(w, r)
|
||||
if err != nil {
|
||||
// If it is not a Subsonic error, convert it to an ErrorGeneric
|
||||
var subErr subError
|
||||
if !errors.As(err, &subErr) {
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
err = newError(responses.ErrorDataNotFound, "data not found")
|
||||
} else {
|
||||
err = newError(responses.ErrorGeneric, fmt.Sprintf("Internal Server Error: %s", err))
|
||||
}
|
||||
}
|
||||
sendError(w, r, err)
|
||||
return
|
||||
}
|
||||
if r.Context().Err() != nil {
|
||||
if log.CurrentLevel() >= log.LevelDebug {
|
||||
if log.IsGreaterOrEqualTo(log.LevelDebug) {
|
||||
log.Warn(r.Context(), "Request was interrupted", "endpoint", r.URL.Path, r.Context().Err())
|
||||
}
|
||||
return
|
||||
|
|
@ -265,21 +256,35 @@ func addHandler(r chi.Router, path string, handle func(w http.ResponseWriter, r
|
|||
r.HandleFunc("/"+path+".view", handle)
|
||||
}
|
||||
|
||||
func sendError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
response := newResponse()
|
||||
code := responses.ErrorGeneric
|
||||
var subErr subError
|
||||
if errors.As(err, &subErr) {
|
||||
code = subErr.code
|
||||
func mapToSubsonicError(err error) subError {
|
||||
switch {
|
||||
case errors.Is(err, errSubsonic): // do nothing
|
||||
case errors.Is(err, req.ErrMissingParam):
|
||||
err = newError(responses.ErrorMissingParameter, err.Error())
|
||||
case errors.Is(err, req.ErrInvalidParam):
|
||||
err = newError(responses.ErrorGeneric, err.Error())
|
||||
case errors.Is(err, model.ErrNotFound):
|
||||
err = newError(responses.ErrorDataNotFound, "data not found")
|
||||
default:
|
||||
err = newError(responses.ErrorGeneric, fmt.Sprintf("Internal Server Error: %s", err))
|
||||
}
|
||||
var subErr subError
|
||||
errors.As(err, &subErr)
|
||||
return subErr
|
||||
}
|
||||
|
||||
func sendError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
subErr := mapToSubsonicError(err)
|
||||
response := newResponse()
|
||||
response.Status = "failed"
|
||||
response.Error = &responses.Error{Code: int32(code), Message: err.Error()}
|
||||
response.Error = &responses.Error{Code: int32(subErr.code), Message: subErr.Error()}
|
||||
|
||||
sendResponse(w, r, response)
|
||||
}
|
||||
|
||||
func sendResponse(w http.ResponseWriter, r *http.Request, payload *responses.Subsonic) {
|
||||
f := utils.ParamString(r, "f")
|
||||
p := req.Params(r)
|
||||
f, _ := p.String("f")
|
||||
var response []byte
|
||||
switch f {
|
||||
case "json":
|
||||
|
|
@ -288,7 +293,7 @@ func sendResponse(w http.ResponseWriter, r *http.Request, payload *responses.Sub
|
|||
response, _ = json.Marshal(wrapper)
|
||||
case "jsonp":
|
||||
w.Header().Set("Content-Type", "application/javascript")
|
||||
callback := utils.ParamString(r, "callback")
|
||||
callback, _ := p.String("callback")
|
||||
wrapper := &responses.JsonWrapper{Subsonic: *payload}
|
||||
data, _ := json.Marshal(wrapper)
|
||||
response = []byte(fmt.Sprintf("%s(%s)", callback, data))
|
||||
|
|
@ -297,7 +302,7 @@ func sendResponse(w http.ResponseWriter, r *http.Request, payload *responses.Sub
|
|||
response, _ = xml.Marshal(payload)
|
||||
}
|
||||
if payload.Status == "ok" {
|
||||
if log.CurrentLevel() >= log.LevelTrace {
|
||||
if log.IsGreaterOrEqualTo(log.LevelTrace) {
|
||||
log.Debug(r.Context(), "API: Successful response", "endpoint", r.URL.Path, "status", "OK", "body", string(response))
|
||||
} else {
|
||||
log.Debug(r.Context(), "API: Successful response", "endpoint", r.URL.Path, "status", "OK")
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import (
|
|||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
)
|
||||
|
||||
func (api *Router) GetBookmarks(r *http.Request) (*responses.Subsonic, error) {
|
||||
|
|
@ -36,13 +36,14 @@ func (api *Router) GetBookmarks(r *http.Request) (*responses.Subsonic, error) {
|
|||
}
|
||||
|
||||
func (api *Router) CreateBookmark(r *http.Request) (*responses.Subsonic, error) {
|
||||
id, err := requiredParamString(r, "id")
|
||||
p := req.Params(r)
|
||||
id, err := p.String("id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
comment := utils.ParamString(r, "comment")
|
||||
position := utils.ParamInt(r, "position", int64(0))
|
||||
comment, _ := p.String("comment")
|
||||
position := p.Int64Or("position", 0)
|
||||
|
||||
repo := api.ds.MediaFile(r.Context())
|
||||
err = repo.AddBookmark(id, comment, position)
|
||||
|
|
@ -53,7 +54,8 @@ func (api *Router) CreateBookmark(r *http.Request) (*responses.Subsonic, error)
|
|||
}
|
||||
|
||||
func (api *Router) DeleteBookmark(r *http.Request) (*responses.Subsonic, error) {
|
||||
id, err := requiredParamString(r, "id")
|
||||
p := req.Params(r)
|
||||
id, err := p.String("id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -88,13 +90,14 @@ func (api *Router) GetPlayQueue(r *http.Request) (*responses.Subsonic, error) {
|
|||
}
|
||||
|
||||
func (api *Router) SavePlayQueue(r *http.Request) (*responses.Subsonic, error) {
|
||||
ids, err := requiredParamStrings(r, "id")
|
||||
p := req.Params(r)
|
||||
ids, err := p.Strings("id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
current := utils.ParamString(r, "current")
|
||||
position := utils.ParamInt(r, "position", int64(0))
|
||||
current, _ := p.String("current")
|
||||
position := p.Int64Or("position", 0)
|
||||
|
||||
user, _ := request.UserFrom(r.Context())
|
||||
client, _ := request.ClientFrom(r.Context())
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import (
|
|||
"github.com/navidrome/navidrome/server/subsonic/filter"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
)
|
||||
|
||||
func (api *Router) GetMusicFolders(r *http.Request) (*responses.Subsonic, error) {
|
||||
|
|
@ -67,8 +68,9 @@ func (api *Router) getArtistIndex(r *http.Request, mediaFolderId int, ifModified
|
|||
}
|
||||
|
||||
func (api *Router) GetIndexes(r *http.Request) (*responses.Subsonic, error) {
|
||||
musicFolderId := utils.ParamInt(r, "musicFolderId", 0)
|
||||
ifModifiedSince := utils.ParamTime(r, "ifModifiedSince", time.Time{})
|
||||
p := req.Params(r)
|
||||
musicFolderId := p.IntOr("musicFolderId", 0)
|
||||
ifModifiedSince := p.TimeOr("ifModifiedSince", time.Time{})
|
||||
|
||||
res, err := api.getArtistIndex(r, musicFolderId, ifModifiedSince)
|
||||
if err != nil {
|
||||
|
|
@ -81,7 +83,8 @@ func (api *Router) GetIndexes(r *http.Request) (*responses.Subsonic, error) {
|
|||
}
|
||||
|
||||
func (api *Router) GetArtists(r *http.Request) (*responses.Subsonic, error) {
|
||||
musicFolderId := utils.ParamInt(r, "musicFolderId", 0)
|
||||
p := req.Params(r)
|
||||
musicFolderId := p.IntOr("musicFolderId", 0)
|
||||
res, err := api.getArtistIndex(r, musicFolderId, time.Time{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -93,7 +96,8 @@ func (api *Router) GetArtists(r *http.Request) (*responses.Subsonic, error) {
|
|||
}
|
||||
|
||||
func (api *Router) GetMusicDirectory(r *http.Request) (*responses.Subsonic, error) {
|
||||
id := utils.ParamString(r, "id")
|
||||
p := req.Params(r)
|
||||
id, _ := p.String("id")
|
||||
ctx := r.Context()
|
||||
|
||||
entity, err := model.GetEntityByID(ctx, api.ds, id)
|
||||
|
|
@ -129,7 +133,8 @@ func (api *Router) GetMusicDirectory(r *http.Request) (*responses.Subsonic, erro
|
|||
}
|
||||
|
||||
func (api *Router) GetArtist(r *http.Request) (*responses.Subsonic, error) {
|
||||
id := utils.ParamString(r, "id")
|
||||
p := req.Params(r)
|
||||
id, _ := p.String("id")
|
||||
ctx := r.Context()
|
||||
|
||||
artist, err := api.ds.Artist(ctx).Get(id)
|
||||
|
|
@ -151,7 +156,8 @@ func (api *Router) GetArtist(r *http.Request) (*responses.Subsonic, error) {
|
|||
}
|
||||
|
||||
func (api *Router) GetAlbum(r *http.Request) (*responses.Subsonic, error) {
|
||||
id := utils.ParamString(r, "id")
|
||||
p := req.Params(r)
|
||||
id, _ := p.String("id")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
|
|
@ -177,7 +183,8 @@ func (api *Router) GetAlbum(r *http.Request) (*responses.Subsonic, error) {
|
|||
}
|
||||
|
||||
func (api *Router) GetAlbumInfo(r *http.Request) (*responses.Subsonic, error) {
|
||||
id, err := requiredParamString(r, "id")
|
||||
p := req.Params(r)
|
||||
id, err := p.String("id")
|
||||
ctx := r.Context()
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -204,7 +211,8 @@ func (api *Router) GetAlbumInfo(r *http.Request) (*responses.Subsonic, error) {
|
|||
}
|
||||
|
||||
func (api *Router) GetSong(r *http.Request) (*responses.Subsonic, error) {
|
||||
id := utils.ParamString(r, "id")
|
||||
p := req.Params(r)
|
||||
id, _ := p.String("id")
|
||||
ctx := r.Context()
|
||||
|
||||
mf, err := api.ds.MediaFile(ctx).Get(id)
|
||||
|
|
@ -243,12 +251,13 @@ func (api *Router) GetGenres(r *http.Request) (*responses.Subsonic, error) {
|
|||
|
||||
func (api *Router) GetArtistInfo(r *http.Request) (*responses.Subsonic, error) {
|
||||
ctx := r.Context()
|
||||
id, err := requiredParamString(r, "id")
|
||||
p := req.Params(r)
|
||||
id, err := p.String("id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
count := utils.ParamInt(r, "count", 20)
|
||||
includeNotPresent := utils.ParamBool(r, "includeNotPresent", false)
|
||||
count := p.IntOr("count", 20)
|
||||
includeNotPresent := p.BoolOr("includeNotPresent", false)
|
||||
|
||||
artist, err := api.externalMetadata.UpdateArtistInfo(ctx, id, count, includeNotPresent)
|
||||
if err != nil {
|
||||
|
|
@ -295,11 +304,12 @@ func (api *Router) GetArtistInfo2(r *http.Request) (*responses.Subsonic, error)
|
|||
|
||||
func (api *Router) GetSimilarSongs(r *http.Request) (*responses.Subsonic, error) {
|
||||
ctx := r.Context()
|
||||
id, err := requiredParamString(r, "id")
|
||||
p := req.Params(r)
|
||||
id, err := p.String("id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
count := utils.ParamInt(r, "count", 50)
|
||||
count := p.IntOr("count", 50)
|
||||
|
||||
songs, err := api.externalMetadata.SimilarSongs(ctx, id, count)
|
||||
if err != nil {
|
||||
|
|
@ -328,11 +338,12 @@ func (api *Router) GetSimilarSongs2(r *http.Request) (*responses.Subsonic, error
|
|||
|
||||
func (api *Router) GetTopSongs(r *http.Request) (*responses.Subsonic, error) {
|
||||
ctx := r.Context()
|
||||
artist, err := requiredParamString(r, "artist")
|
||||
p := req.Params(r)
|
||||
artist, err := p.String("artist")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
count := utils.ParamInt(r, "count", 50)
|
||||
count := p.IntOr("count", 50)
|
||||
|
||||
songs, err := api.externalMetadata.TopSongs(ctx, artist, count)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@ package subsonic
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
|
|
@ -13,7 +15,6 @@ import (
|
|||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/server/public"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
)
|
||||
|
||||
func newResponse() *responses.Subsonic {
|
||||
|
|
@ -26,30 +27,6 @@ func newResponse() *responses.Subsonic {
|
|||
}
|
||||
}
|
||||
|
||||
func requiredParamString(r *http.Request, param string) (string, error) {
|
||||
p := utils.ParamString(r, param)
|
||||
if p == "" {
|
||||
return "", newError(responses.ErrorMissingParameter, "required '%s' parameter is missing", param)
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func requiredParamStrings(r *http.Request, param string) ([]string, error) {
|
||||
ps := utils.ParamStrings(r, param)
|
||||
if len(ps) == 0 {
|
||||
return nil, newError(responses.ErrorMissingParameter, "required '%s' parameter is missing", param)
|
||||
}
|
||||
return ps, nil
|
||||
}
|
||||
|
||||
func requiredParamInt(r *http.Request, param string) (int, error) {
|
||||
p := utils.ParamString(r, param)
|
||||
if p == "" {
|
||||
return 0, newError(responses.ErrorMissingParameter, "required '%s' parameter is missing", param)
|
||||
}
|
||||
return utils.ParamInt(r, param, 0), nil
|
||||
}
|
||||
|
||||
type subError struct {
|
||||
code int
|
||||
messages []interface{}
|
||||
|
|
@ -62,6 +39,13 @@ func newError(code int, message ...interface{}) error {
|
|||
}
|
||||
}
|
||||
|
||||
// errSubsonic and Unwrap are used to allow `errors.Is(err, errSubsonic)` to work
|
||||
var errSubsonic = errors.New("subsonic API error")
|
||||
|
||||
func (e subError) Unwrap() error {
|
||||
return fmt.Errorf("%w: %d", errSubsonic, e.code)
|
||||
}
|
||||
|
||||
func (e subError) Error() string {
|
||||
var msg string
|
||||
if len(e.messages) == 0 {
|
||||
|
|
@ -261,6 +245,24 @@ func childrenFromAlbums(ctx context.Context, als model.Albums) []responses.Child
|
|||
return children
|
||||
}
|
||||
|
||||
// toItemDate converts a string date in the formats 'YYYY-MM-DD', 'YYYY-MM' or 'YYYY' to an OS ItemDate
|
||||
func toItemDate(date string) responses.ItemDate {
|
||||
itemDate := responses.ItemDate{}
|
||||
if date == "" {
|
||||
return itemDate
|
||||
}
|
||||
parts := strings.Split(date, "-")
|
||||
if len(parts) > 2 {
|
||||
itemDate.Day, _ = strconv.Atoi(parts[2])
|
||||
}
|
||||
if len(parts) > 1 {
|
||||
itemDate.Month, _ = strconv.Atoi(parts[1])
|
||||
}
|
||||
itemDate.Year, _ = strconv.Atoi(parts[0])
|
||||
|
||||
return itemDate
|
||||
}
|
||||
|
||||
func buildItemGenres(genres model.Genres) []responses.ItemGenre {
|
||||
itemGenres := make([]responses.ItemGenre, len(genres))
|
||||
for i, g := range genres {
|
||||
|
|
@ -318,6 +320,7 @@ func buildAlbumID3(ctx context.Context, album model.Album) responses.AlbumID3 {
|
|||
dir.MusicBrainzId = album.MbzAlbumID
|
||||
dir.IsCompilation = album.Compilation
|
||||
dir.SortName = album.SortAlbumName
|
||||
dir.OriginalReleaseDate = toItemDate(album.OriginalDate)
|
||||
return dir
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,4 +57,15 @@ var _ = Describe("helpers", func() {
|
|||
Expect(buildDiscSubtitles(context.Background(), album)).To(Equal(expected))
|
||||
})
|
||||
})
|
||||
|
||||
DescribeTable("toItemDate",
|
||||
func(date string, expected responses.ItemDate) {
|
||||
Expect(toItemDate(date)).To(Equal(expected))
|
||||
},
|
||||
Entry("1994-02-04", "1994-02-04", responses.ItemDate{Year: 1994, Month: 2, Day: 4}),
|
||||
Entry("1994-02", "1994-02", responses.ItemDate{Year: 1994, Month: 2}),
|
||||
Entry("1994", "1994", responses.ItemDate{Year: 1994}),
|
||||
Entry("19940201", "", responses.ItemDate{}),
|
||||
Entry("", "", responses.ItemDate{}),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
package subsonic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/navidrome/navidrome/core/playback"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -27,8 +27,9 @@ const (
|
|||
func (api *Router) JukeboxControl(r *http.Request) (*responses.Subsonic, error) {
|
||||
ctx := r.Context()
|
||||
user := getUser(ctx)
|
||||
p := req.Params(r)
|
||||
|
||||
actionString, err := requiredParamString(r, "action")
|
||||
actionString, err := p.String("action")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -38,7 +39,7 @@ func (api *Router) JukeboxControl(r *http.Request) (*responses.Subsonic, error)
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debug(fmt.Sprintf("processing action: %s", actionString))
|
||||
log.Info(ctx, "JukeboxControl request received", "action", actionString)
|
||||
|
||||
switch actionString {
|
||||
case ActionGet:
|
||||
|
|
@ -58,42 +59,31 @@ func (api *Router) JukeboxControl(r *http.Request) (*responses.Subsonic, error)
|
|||
case ActionStatus:
|
||||
return createResponse(pb.Status(ctx))
|
||||
case ActionSet:
|
||||
ids, err := requiredParamStrings(r, "id")
|
||||
if err != nil {
|
||||
return nil, newError(responses.ErrorMissingParameter, "missing parameter id, err: %s", err)
|
||||
}
|
||||
status, err := pb.Set(ctx, ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return statusResponse(status), nil
|
||||
ids, _ := p.Strings("id")
|
||||
return createResponse(pb.Set(ctx, ids))
|
||||
case ActionStart:
|
||||
return createResponse(pb.Start(ctx))
|
||||
case ActionStop:
|
||||
return createResponse(pb.Stop(ctx))
|
||||
case ActionSkip:
|
||||
index, err := requiredParamInt(r, "index")
|
||||
index, err := p.Int("index")
|
||||
if err != nil {
|
||||
return nil, newError(responses.ErrorMissingParameter, "missing parameter index, err: %s", err)
|
||||
}
|
||||
|
||||
offset, err := requiredParamInt(r, "offset")
|
||||
offset := p.IntOr("offset", 0)
|
||||
if err != nil {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
return createResponse(pb.Skip(ctx, index, offset))
|
||||
case ActionAdd:
|
||||
ids, err := requiredParamStrings(r, "id")
|
||||
if err != nil {
|
||||
return nil, newError(responses.ErrorMissingParameter, "missing parameter id, err: %s", err)
|
||||
}
|
||||
|
||||
ids, _ := p.Strings("id")
|
||||
return createResponse(pb.Add(ctx, ids))
|
||||
case ActionClear:
|
||||
return createResponse(pb.Clear(ctx))
|
||||
case ActionRemove:
|
||||
index, err := requiredParamInt(r, "index")
|
||||
index, err := p.Int("index")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -102,14 +92,14 @@ func (api *Router) JukeboxControl(r *http.Request) (*responses.Subsonic, error)
|
|||
case ActionShuffle:
|
||||
return createResponse(pb.Shuffle(ctx))
|
||||
case ActionSetGain:
|
||||
gainStr, err := requiredParamString(r, "gain")
|
||||
gainStr, err := p.String("gain")
|
||||
if err != nil {
|
||||
return nil, newError(responses.ErrorMissingParameter, "missing parameter gain, err: %s", err)
|
||||
}
|
||||
|
||||
gain, err := strconv.ParseFloat(gainStr, 32)
|
||||
if err != nil {
|
||||
return nil, newError(responses.ErrorMissingParameter, "error parsing gain integer value, err: %s", err)
|
||||
return nil, newError(responses.ErrorMissingParameter, "error parsing gain float value, err: %s", err)
|
||||
}
|
||||
|
||||
return createResponse(pb.SetGain(ctx, float32(gain)))
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
)
|
||||
|
||||
func (api *Router) GetScanStatus(r *http.Request) (*responses.Subsonic, error) {
|
||||
|
|
@ -41,7 +41,8 @@ func (api *Router) StartScan(r *http.Request) (*responses.Subsonic, error) {
|
|||
return nil, newError(responses.ErrorAuthorizationFail)
|
||||
}
|
||||
|
||||
fullScan := utils.ParamBool(r, "fullScan", false)
|
||||
p := req.Params(r)
|
||||
fullScan := p.BoolOr("fullScan", false)
|
||||
|
||||
go func() {
|
||||
start := time.Now()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package subsonic
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
|
@ -13,26 +12,22 @@ import (
|
|||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/server/events"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
)
|
||||
|
||||
func (api *Router) SetRating(r *http.Request) (*responses.Subsonic, error) {
|
||||
id, err := requiredParamString(r, "id")
|
||||
p := req.Params(r)
|
||||
id, err := p.String("id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rating, err := requiredParamInt(r, "rating")
|
||||
rating, err := p.Int("rating")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debug(r, "Setting rating", "rating", rating, "id", id)
|
||||
err = api.setRating(r.Context(), id, rating)
|
||||
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
log.Error(r, err)
|
||||
return nil, newError(responses.ErrorDataNotFound, "ID not found")
|
||||
}
|
||||
if err != nil {
|
||||
log.Error(r, err)
|
||||
return nil, err
|
||||
|
|
@ -70,9 +65,10 @@ func (api *Router) setRating(ctx context.Context, id string, rating int) error {
|
|||
}
|
||||
|
||||
func (api *Router) Star(r *http.Request) (*responses.Subsonic, error) {
|
||||
ids := utils.ParamStrings(r, "id")
|
||||
albumIds := utils.ParamStrings(r, "albumId")
|
||||
artistIds := utils.ParamStrings(r, "artistId")
|
||||
p := req.Params(r)
|
||||
ids, _ := p.Strings("id")
|
||||
albumIds, _ := p.Strings("albumId")
|
||||
artistIds, _ := p.Strings("artistId")
|
||||
if len(ids)+len(albumIds)+len(artistIds) == 0 {
|
||||
return nil, newError(responses.ErrorMissingParameter, "Required id parameter is missing")
|
||||
}
|
||||
|
|
@ -88,9 +84,10 @@ func (api *Router) Star(r *http.Request) (*responses.Subsonic, error) {
|
|||
}
|
||||
|
||||
func (api *Router) Unstar(r *http.Request) (*responses.Subsonic, error) {
|
||||
ids := utils.ParamStrings(r, "id")
|
||||
albumIds := utils.ParamStrings(r, "albumId")
|
||||
artistIds := utils.ParamStrings(r, "artistId")
|
||||
p := req.Params(r)
|
||||
ids, _ := p.Strings("id")
|
||||
albumIds, _ := p.Strings("albumId")
|
||||
artistIds, _ := p.Strings("artistId")
|
||||
if len(ids)+len(albumIds)+len(artistIds) == 0 {
|
||||
return nil, newError(responses.ErrorMissingParameter, "Required id parameter is missing")
|
||||
}
|
||||
|
|
@ -150,11 +147,6 @@ func (api *Router) setStar(ctx context.Context, star bool, ids ...string) error
|
|||
api.broker.SendMessage(ctx, event)
|
||||
return nil
|
||||
})
|
||||
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
log.Error(ctx, err)
|
||||
return newError(responses.ErrorDataNotFound, "ID not found")
|
||||
}
|
||||
if err != nil {
|
||||
log.Error(ctx, err)
|
||||
return err
|
||||
|
|
@ -163,15 +155,16 @@ func (api *Router) setStar(ctx context.Context, star bool, ids ...string) error
|
|||
}
|
||||
|
||||
func (api *Router) Scrobble(r *http.Request) (*responses.Subsonic, error) {
|
||||
ids, err := requiredParamStrings(r, "id")
|
||||
p := req.Params(r)
|
||||
ids, err := p.Strings("id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
times := utils.ParamTimes(r, "time")
|
||||
times, _ := p.Times("time")
|
||||
if len(times) > 0 && len(times) != len(ids) {
|
||||
return nil, newError(responses.ErrorGeneric, "Wrong number of timestamps: %d, should be %d", len(times), len(ids))
|
||||
}
|
||||
submission := utils.ParamBool(r, "submission", true)
|
||||
submission := p.BoolOr("submission", true)
|
||||
ctx := r.Context()
|
||||
|
||||
if submission {
|
||||
|
|
|
|||
|
|
@ -14,15 +14,16 @@ import (
|
|||
"github.com/navidrome/navidrome/resources"
|
||||
"github.com/navidrome/navidrome/server/subsonic/filter"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
"github.com/navidrome/navidrome/utils/gravatar"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
)
|
||||
|
||||
func (api *Router) GetAvatar(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
|
||||
if !conf.Server.EnableGravatar {
|
||||
return api.getPlaceHolderAvatar(w, r)
|
||||
}
|
||||
username, err := requiredParamString(r, "username")
|
||||
p := req.Params(r)
|
||||
username, err := p.String("username")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -60,8 +61,9 @@ func (api *Router) GetCoverArt(w http.ResponseWriter, r *http.Request) (*respons
|
|||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
id := utils.ParamString(r, "id")
|
||||
size := utils.ParamInt(r, "size", 0)
|
||||
p := req.Params(r)
|
||||
id, _ := p.String("id")
|
||||
size := p.IntOr("size", 0)
|
||||
|
||||
imgReader, lastUpdate, err := api.artwork.GetOrPlaceholder(ctx, id, size)
|
||||
w.Header().Set("cache-control", "public, max-age=315360000")
|
||||
|
|
@ -88,8 +90,9 @@ func (api *Router) GetCoverArt(w http.ResponseWriter, r *http.Request) (*respons
|
|||
}
|
||||
|
||||
func (api *Router) GetLyrics(r *http.Request) (*responses.Subsonic, error) {
|
||||
artist := utils.ParamString(r, "artist")
|
||||
title := utils.ParamString(r, "title")
|
||||
p := req.Params(r)
|
||||
artist, _ := p.String("artist")
|
||||
title, _ := p.String("title")
|
||||
response := newResponse()
|
||||
lyrics := responses.Lyrics{}
|
||||
response.Lyrics = &lyrics
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ import (
|
|||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
. "github.com/navidrome/navidrome/utils/gg"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
)
|
||||
|
||||
func postFormToQueryParams(next http.Handler) http.Handler {
|
||||
|
|
@ -45,19 +45,18 @@ func postFormToQueryParams(next http.Handler) http.Handler {
|
|||
func checkRequiredParameters(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requiredParameters := []string{"u", "v", "c"}
|
||||
|
||||
for _, p := range requiredParameters {
|
||||
if utils.ParamString(r, p) == "" {
|
||||
msg := fmt.Sprintf(`Missing required parameter "%s"`, p)
|
||||
log.Warn(r, msg)
|
||||
sendError(w, r, newError(responses.ErrorMissingParameter, msg))
|
||||
p := req.Params(r)
|
||||
for _, param := range requiredParameters {
|
||||
if _, err := p.String(param); err != nil {
|
||||
log.Warn(r, err)
|
||||
sendError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
username := utils.ParamString(r, "u")
|
||||
client := utils.ParamString(r, "c")
|
||||
version := utils.ParamString(r, "v")
|
||||
username, _ := p.String("u")
|
||||
client, _ := p.String("c")
|
||||
version, _ := p.String("v")
|
||||
ctx := r.Context()
|
||||
ctx = request.WithUsername(ctx, username)
|
||||
ctx = request.WithClient(ctx, client)
|
||||
|
|
@ -73,12 +72,13 @@ func authenticate(ds model.DataStore) func(next http.Handler) http.Handler {
|
|||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
username := utils.ParamString(r, "u")
|
||||
p := req.Params(r)
|
||||
username, _ := p.String("u")
|
||||
|
||||
pass := utils.ParamString(r, "p")
|
||||
token := utils.ParamString(r, "t")
|
||||
salt := utils.ParamString(r, "s")
|
||||
jwt := utils.ParamString(r, "jwt")
|
||||
pass, _ := p.String("p")
|
||||
token, _ := p.String("t")
|
||||
salt, _ := p.String("s")
|
||||
jwt, _ := p.String("jwt")
|
||||
|
||||
usr, err := validateUser(ctx, ds, username, pass, token, salt, jwt)
|
||||
if errors.Is(err, model.ErrInvalidAuth) {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import (
|
|||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
)
|
||||
|
||||
func (api *Router) GetPlaylists(r *http.Request) (*responses.Subsonic, error) {
|
||||
|
|
@ -31,7 +31,8 @@ func (api *Router) GetPlaylists(r *http.Request) (*responses.Subsonic, error) {
|
|||
|
||||
func (api *Router) GetPlaylist(r *http.Request) (*responses.Subsonic, error) {
|
||||
ctx := r.Context()
|
||||
id, err := requiredParamString(r, "id")
|
||||
p := req.Params(r)
|
||||
id, err := p.String("id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -84,9 +85,10 @@ func (api *Router) create(ctx context.Context, playlistId, name string, ids []st
|
|||
|
||||
func (api *Router) CreatePlaylist(r *http.Request) (*responses.Subsonic, error) {
|
||||
ctx := r.Context()
|
||||
songIds := utils.ParamStrings(r, "songId")
|
||||
playlistId := utils.ParamString(r, "playlistId")
|
||||
name := utils.ParamString(r, "name")
|
||||
p := req.Params(r)
|
||||
songIds, _ := p.Strings("songId")
|
||||
playlistId, _ := p.String("playlistId")
|
||||
name, _ := p.String("name")
|
||||
if playlistId == "" && name == "" {
|
||||
return nil, errors.New("required parameter name is missing")
|
||||
}
|
||||
|
|
@ -99,7 +101,8 @@ func (api *Router) CreatePlaylist(r *http.Request) (*responses.Subsonic, error)
|
|||
}
|
||||
|
||||
func (api *Router) DeletePlaylist(r *http.Request) (*responses.Subsonic, error) {
|
||||
id, err := requiredParamString(r, "id")
|
||||
p := req.Params(r)
|
||||
id, err := p.String("id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -115,23 +118,23 @@ func (api *Router) DeletePlaylist(r *http.Request) (*responses.Subsonic, error)
|
|||
}
|
||||
|
||||
func (api *Router) UpdatePlaylist(r *http.Request) (*responses.Subsonic, error) {
|
||||
playlistId, err := requiredParamString(r, "playlistId")
|
||||
p := req.Params(r)
|
||||
playlistId, err := p.String("playlistId")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
songsToAdd := utils.ParamStrings(r, "songIdToAdd")
|
||||
songIndexesToRemove := utils.ParamInts(r, "songIndexToRemove")
|
||||
songsToAdd, _ := p.Strings("songIdToAdd")
|
||||
songIndexesToRemove, _ := p.Ints("songIndexToRemove")
|
||||
var plsName *string
|
||||
if s, ok := r.URL.Query()["name"]; ok {
|
||||
plsName = &s[0]
|
||||
if s, err := p.String("name"); err == nil {
|
||||
plsName = &s
|
||||
}
|
||||
var comment *string
|
||||
if c, ok := r.URL.Query()["comment"]; ok {
|
||||
comment = &c[0]
|
||||
if s, err := p.String("comment"); err == nil {
|
||||
comment = &s
|
||||
}
|
||||
var public *bool
|
||||
if _, ok := r.URL.Query()["public"]; ok {
|
||||
p := utils.ParamBool(r, "public", false)
|
||||
if p, err := p.Bool("public"); err == nil {
|
||||
public = &p
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,21 +5,22 @@ import (
|
|||
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
)
|
||||
|
||||
func (api *Router) CreateInternetRadio(r *http.Request) (*responses.Subsonic, error) {
|
||||
streamUrl, err := requiredParamString(r, "streamUrl")
|
||||
p := req.Params(r)
|
||||
streamUrl, err := p.String("streamUrl")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
name, err := requiredParamString(r, "name")
|
||||
name, err := p.String("name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
homepageUrl := utils.ParamString(r, "homepageUrl")
|
||||
homepageUrl, _ := p.String("homepageUrl")
|
||||
ctx := r.Context()
|
||||
|
||||
radio := &model.Radio{
|
||||
|
|
@ -36,7 +37,8 @@ func (api *Router) CreateInternetRadio(r *http.Request) (*responses.Subsonic, er
|
|||
}
|
||||
|
||||
func (api *Router) DeleteInternetRadio(r *http.Request) (*responses.Subsonic, error) {
|
||||
id, err := requiredParamString(r, "id")
|
||||
p := req.Params(r)
|
||||
id, err := p.String("id")
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -75,22 +77,23 @@ func (api *Router) GetInternetRadios(r *http.Request) (*responses.Subsonic, erro
|
|||
}
|
||||
|
||||
func (api *Router) UpdateInternetRadio(r *http.Request) (*responses.Subsonic, error) {
|
||||
id, err := requiredParamString(r, "id")
|
||||
p := req.Params(r)
|
||||
id, err := p.String("id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
streamUrl, err := requiredParamString(r, "streamUrl")
|
||||
streamUrl, err := p.String("streamUrl")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
name, err := requiredParamString(r, "name")
|
||||
name, err := p.String("name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
homepageUrl := utils.ParamString(r, "homepageUrl")
|
||||
homepageUrl, _ := p.String("homepageUrl")
|
||||
ctx := r.Context()
|
||||
|
||||
radio := &model.Radio{
|
||||
|
|
|
|||
|
|
@ -31,6 +31,11 @@
|
|||
"title": "disc 2"
|
||||
}
|
||||
],
|
||||
"originalReleaseDate": {
|
||||
"year": 1994,
|
||||
"month": 2,
|
||||
"day": 4
|
||||
},
|
||||
"song": [
|
||||
{
|
||||
"id": "1",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<genres name="progressive"></genres>
|
||||
<discTitles disc="1" title="disc 1"></discTitles>
|
||||
<discTitles disc="2" title="disc 2"></discTitles>
|
||||
<originalReleaseDate year="1994" month="2" day="4"></originalReleaseDate>
|
||||
<song id="1" isDir="true" title="title" album="album" artist="artist" track="1" year="1985" genre="Rock" coverArt="1" size="8421341" contentType="audio/flac" suffix="flac" starred="2016-03-02T20:30:00Z" transcodedContentType="audio/mpeg" transcodedSuffix="mp3" duration="146" bitRate="320" isVideo="false" bpm="127" comment="a comment" sortName="sorted song" mediaType="song" musicBrainzId="4321">
|
||||
<genres name="rock"></genres>
|
||||
<genres name="progressive"></genres>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
"musicBrainzId": "",
|
||||
"isCompilation": false,
|
||||
"sortName": "",
|
||||
"discTitles": []
|
||||
"discTitles": [],
|
||||
"originalReleaseDate": {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
|
||||
<album id="" name="" userRating="0" musicBrainzId="" isCompilation="false" sortName=""></album>
|
||||
<album id="" name="" userRating="0" musicBrainzId="" isCompilation="false" sortName="">
|
||||
<originalReleaseDate></originalReleaseDate>
|
||||
</album>
|
||||
</subsonic-response>
|
||||
|
|
|
|||
|
|
@ -219,13 +219,14 @@ type AlbumID3 struct {
|
|||
Genre string `xml:"genre,attr,omitempty" json:"genre,omitempty"`
|
||||
|
||||
// OpenSubsonic extensions
|
||||
Played *time.Time `xml:"played,attr,omitempty" json:"played,omitempty"`
|
||||
UserRating int32 `xml:"userRating,attr" json:"userRating"`
|
||||
Genres ItemGenres `xml:"genres" json:"genres"`
|
||||
MusicBrainzId string `xml:"musicBrainzId,attr" json:"musicBrainzId"`
|
||||
IsCompilation bool `xml:"isCompilation,attr" json:"isCompilation"`
|
||||
SortName string `xml:"sortName,attr" json:"sortName"`
|
||||
DiscTitles DiscTitles `xml:"discTitles" json:"discTitles"`
|
||||
Played *time.Time `xml:"played,attr,omitempty" json:"played,omitempty"`
|
||||
UserRating int32 `xml:"userRating,attr" json:"userRating"`
|
||||
Genres ItemGenres `xml:"genres" json:"genres"`
|
||||
MusicBrainzId string `xml:"musicBrainzId,attr" json:"musicBrainzId"`
|
||||
IsCompilation bool `xml:"isCompilation,attr" json:"isCompilation"`
|
||||
SortName string `xml:"sortName,attr" json:"sortName"`
|
||||
DiscTitles DiscTitles `xml:"discTitles" json:"discTitles"`
|
||||
OriginalReleaseDate ItemDate `xml:"originalReleaseDate" json:"originalReleaseDate"`
|
||||
}
|
||||
|
||||
type ArtistWithAlbumsID3 struct {
|
||||
|
|
@ -513,3 +514,9 @@ func marshalJSONArray[T any](v []T) ([]byte, error) {
|
|||
a := v
|
||||
return json.Marshal(a)
|
||||
}
|
||||
|
||||
type ItemDate struct {
|
||||
Year int `xml:"year,attr,omitempty" json:"year,omitempty"`
|
||||
Month int `xml:"month,attr,omitempty" json:"month,omitempty"`
|
||||
Day int `xml:"day,attr,omitempty" json:"day,omitempty"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,7 +176,8 @@ var _ = Describe("Responses", func() {
|
|||
Id: "1", Name: "album", Artist: "artist", Genre: "rock",
|
||||
Genres: []ItemGenre{{Name: "rock"}, {Name: "progressive"}},
|
||||
MusicBrainzId: "1234", IsCompilation: true, SortName: "sorted album",
|
||||
DiscTitles: DiscTitles{{Disc: 1, Title: "disc 1"}, {Disc: 2, Title: "disc 2"}},
|
||||
DiscTitles: DiscTitles{{Disc: 1, Title: "disc 1"}, {Disc: 2, Title: "disc 2"}},
|
||||
OriginalReleaseDate: ItemDate{Year: 1994, Month: 2, Day: 4},
|
||||
}
|
||||
t := time.Date(2016, 03, 2, 20, 30, 0, 0, time.UTC)
|
||||
songs := []Child{{
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import (
|
|||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/server/public"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
)
|
||||
|
||||
type searchParams struct {
|
||||
|
|
@ -28,18 +28,19 @@ type searchParams struct {
|
|||
}
|
||||
|
||||
func (api *Router) getParams(r *http.Request) (*searchParams, error) {
|
||||
p := req.Params(r)
|
||||
var err error
|
||||
sp := &searchParams{}
|
||||
sp.query, err = requiredParamString(r, "query")
|
||||
sp.query, err = p.String("query")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sp.artistCount = utils.ParamInt(r, "artistCount", 20)
|
||||
sp.artistOffset = utils.ParamInt(r, "artistOffset", 0)
|
||||
sp.albumCount = utils.ParamInt(r, "albumCount", 20)
|
||||
sp.albumOffset = utils.ParamInt(r, "albumOffset", 0)
|
||||
sp.songCount = utils.ParamInt(r, "songCount", 20)
|
||||
sp.songOffset = utils.ParamInt(r, "songOffset", 0)
|
||||
sp.artistCount = p.IntOr("artistCount", 20)
|
||||
sp.artistOffset = p.IntOr("artistOffset", 0)
|
||||
sp.albumCount = p.IntOr("albumCount", 20)
|
||||
sp.albumOffset = p.IntOr("albumOffset", 0)
|
||||
sp.songCount = p.IntOr("songCount", 20)
|
||||
sp.songOffset = p.IntOr("songOffset", 0)
|
||||
return sp, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import (
|
|||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/server/public"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
)
|
||||
|
||||
func (api *Router) GetShares(r *http.Request) (*responses.Subsonic, error) {
|
||||
|
|
@ -50,13 +50,14 @@ func (api *Router) buildShare(r *http.Request, share model.Share) responses.Shar
|
|||
}
|
||||
|
||||
func (api *Router) CreateShare(r *http.Request) (*responses.Subsonic, error) {
|
||||
ids := utils.ParamStrings(r, "id")
|
||||
if len(ids) == 0 {
|
||||
return nil, newError(responses.ErrorMissingParameter, "Required id parameter is missing")
|
||||
p := req.Params(r)
|
||||
ids, err := p.Strings("id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
description := utils.ParamString(r, "description")
|
||||
expires := utils.ParamTime(r, "expires", time.Time{})
|
||||
description, _ := p.String("description")
|
||||
expires := p.TimeOr("expires", time.Time{})
|
||||
|
||||
repo := api.share.NewRepository(r.Context())
|
||||
share := &model.Share{
|
||||
|
|
@ -81,13 +82,14 @@ func (api *Router) CreateShare(r *http.Request) (*responses.Subsonic, error) {
|
|||
}
|
||||
|
||||
func (api *Router) UpdateShare(r *http.Request) (*responses.Subsonic, error) {
|
||||
id := utils.ParamString(r, "id")
|
||||
if id == "" {
|
||||
return nil, newError(responses.ErrorMissingParameter, "Required id parameter is missing")
|
||||
p := req.Params(r)
|
||||
id, err := p.String("id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
description := utils.ParamString(r, "description")
|
||||
expires := utils.ParamTime(r, "expires", time.Time{})
|
||||
description, _ := p.String("description")
|
||||
expires := p.TimeOr("expires", time.Time{})
|
||||
|
||||
repo := api.share.NewRepository(r.Context())
|
||||
share := &model.Share{
|
||||
|
|
@ -96,7 +98,7 @@ func (api *Router) UpdateShare(r *http.Request) (*responses.Subsonic, error) {
|
|||
ExpiresAt: expires,
|
||||
}
|
||||
|
||||
err := repo.(rest.Persistable).Update(id, share)
|
||||
err = repo.(rest.Persistable).Update(id, share)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -105,13 +107,14 @@ func (api *Router) UpdateShare(r *http.Request) (*responses.Subsonic, error) {
|
|||
}
|
||||
|
||||
func (api *Router) DeleteShare(r *http.Request) (*responses.Subsonic, error) {
|
||||
id := utils.ParamString(r, "id")
|
||||
if id == "" {
|
||||
return nil, newError(responses.ErrorMissingParameter, "Required id parameter is missing")
|
||||
p := req.Params(r)
|
||||
id, err := p.String("id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repo := api.share.NewRepository(r.Context())
|
||||
err := repo.(rest.Persistable).Delete(id)
|
||||
err = repo.(rest.Persistable).Delete(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import (
|
|||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
)
|
||||
|
||||
func (api *Router) serveStream(ctx context.Context, w http.ResponseWriter, r *http.Request, stream *core.Stream, id string) {
|
||||
|
|
@ -25,7 +25,7 @@ func (api *Router) serveStream(ctx context.Context, w http.ResponseWriter, r *ht
|
|||
w.Header().Set("Accept-Ranges", "none")
|
||||
w.Header().Set("Content-Type", stream.ContentType())
|
||||
|
||||
estimateContentLength := utils.ParamBool(r, "estimateContentLength", false)
|
||||
estimateContentLength := req.Params(r).BoolOr("estimateContentLength", false)
|
||||
|
||||
// if Client requests the estimated content-length, send it
|
||||
if estimateContentLength {
|
||||
|
|
@ -38,7 +38,7 @@ func (api *Router) serveStream(ctx context.Context, w http.ResponseWriter, r *ht
|
|||
go func() { _, _ = io.Copy(io.Discard, stream) }()
|
||||
} else {
|
||||
c, err := io.Copy(w, stream)
|
||||
if log.CurrentLevel() >= log.LevelDebug {
|
||||
if log.IsGreaterOrEqualTo(log.LevelDebug) {
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error sending transcoded file", "id", id, err)
|
||||
} else {
|
||||
|
|
@ -51,13 +51,14 @@ func (api *Router) serveStream(ctx context.Context, w http.ResponseWriter, r *ht
|
|||
|
||||
func (api *Router) Stream(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
|
||||
ctx := r.Context()
|
||||
id, err := requiredParamString(r, "id")
|
||||
p := req.Params(r)
|
||||
id, err := p.String("id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
maxBitRate := utils.ParamInt(r, "maxBitRate", 0)
|
||||
format := utils.ParamString(r, "format")
|
||||
timeOffset := utils.ParamInt(r, "timeOffset", 0)
|
||||
maxBitRate := p.IntOr("maxBitRate", 0)
|
||||
format, _ := p.String("format")
|
||||
timeOffset := p.IntOr("timeOffset", 0)
|
||||
|
||||
stream, err := api.streamer.NewStream(ctx, id, format, maxBitRate, timeOffset)
|
||||
if err != nil {
|
||||
|
|
@ -66,7 +67,7 @@ func (api *Router) Stream(w http.ResponseWriter, r *http.Request) (*responses.Su
|
|||
|
||||
// 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 {
|
||||
if err := stream.Close(); err != nil && log.IsGreaterOrEqualTo(log.LevelDebug) {
|
||||
log.Error("Error closing stream", "id", id, "file", stream.Name(), err)
|
||||
}
|
||||
}()
|
||||
|
|
@ -82,7 +83,8 @@ func (api *Router) Stream(w http.ResponseWriter, r *http.Request) (*responses.Su
|
|||
func (api *Router) Download(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
|
||||
ctx := r.Context()
|
||||
username, _ := request.UsernameFrom(ctx)
|
||||
id, err := requiredParamString(r, "id")
|
||||
p := req.Params(r)
|
||||
id, err := p.String("id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -97,8 +99,8 @@ 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")
|
||||
maxBitRate := p.IntOr("bitrate", 0)
|
||||
format, _ := p.String("format")
|
||||
|
||||
if format == "" {
|
||||
if conf.Server.AutoTranscodeDownload {
|
||||
|
|
@ -134,7 +136,7 @@ func (api *Router) Download(w http.ResponseWriter, r *http.Request) (*responses.
|
|||
|
||||
// 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 {
|
||||
if err := stream.Close(); err != nil && log.IsGreaterOrEqualTo(log.LevelDebug) {
|
||||
log.Error("Error closing stream", "id", id, "file", stream.Name(), err)
|
||||
}
|
||||
}()
|
||||
|
|
|
|||
BIN
tests/fixtures/test.tak
vendored
Normal file
BIN
tests/fixtures/test.tak
vendored
Normal file
Binary file not shown.
168
ui/package-lock.json
generated
168
ui/package-lock.json
generated
|
|
@ -9,16 +9,16 @@
|
|||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.11.4",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@material-ui/icons": "^4.11.3",
|
||||
"@material-ui/lab": "^4.0.0-alpha.58",
|
||||
"@material-ui/styles": "^4.11.5",
|
||||
"blueimp-md5": "^2.19.0",
|
||||
"clsx": "^1.1.1",
|
||||
"clsx": "^2.0.0",
|
||||
"connected-react-router": "^6.9.1",
|
||||
"deepmerge": "^4.2.2",
|
||||
"deepmerge": "^4.3.1",
|
||||
"history": "^4.10.1",
|
||||
"inflection": "^1.13.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lodash.pick": "^4.4.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"navidrome-music-player": "4.25.1",
|
||||
|
|
@ -46,18 +46,18 @@
|
|||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/react-hooks": "^7.0.2",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@testing-library/user-event": "^14.5.1",
|
||||
"css-mediaquery": "^0.1.2",
|
||||
"prettier": "2.8.2",
|
||||
"prettier": "3.1.1",
|
||||
"ra-test": "^3.19.12",
|
||||
"react-scripts": "5.0.1",
|
||||
"workbox-cli": "^6.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@adobe/css-tools": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.1.tgz",
|
||||
"integrity": "sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg==",
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.2.tgz",
|
||||
"integrity": "sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
|
|
@ -3303,15 +3303,34 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@material-ui/core/node_modules/clsx": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@material-ui/icons": {
|
||||
"version": "4.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.2.tgz",
|
||||
"integrity": "sha512-fQNsKX2TxBmqIGJCSi3tGTO/gZ+eJgWmMJkgDiOfyNaunNaxcklJQFaFogYcFl0qFuaEz1qaXYXboa/bUXVSOQ==",
|
||||
"version": "4.11.3",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.3.tgz",
|
||||
"integrity": "sha512-IKHlyx6LDh8n19vzwH5RtHIOHl9Tu90aAAxcbWME6kp4dmvODM3UvOHJeMIDzUbd4muuJKHmlNoBN+mDY4XkBA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.4.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@material-ui/core": "^4.0.0",
|
||||
"@types/react": "^16.8.6 || ^17.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@material-ui/lab": {
|
||||
|
|
@ -3329,6 +3348,14 @@
|
|||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@material-ui/lab/node_modules/clsx": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@material-ui/styles": {
|
||||
"version": "4.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.5.tgz",
|
||||
|
|
@ -3370,6 +3397,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@material-ui/styles/node_modules/clsx": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@material-ui/system": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.12.2.tgz",
|
||||
|
|
@ -4017,15 +4052,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@testing-library/user-event": {
|
||||
"version": "13.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz",
|
||||
"integrity": "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==",
|
||||
"version": "14.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.1.tgz",
|
||||
"integrity": "sha512-UCcUKrUYGj7ClomOo2SpNVvx4/fkd/2BbIHDCle8A0ax+P3bU7yJwDBDrS6ZwdTMARWTGODX1hEsCcO+7beJjg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10",
|
||||
"node": ">=12",
|
||||
"npm": ">=6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
|
@ -6271,9 +6303,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz",
|
||||
"integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
|
||||
"integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
|
|
@ -7236,9 +7268,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
|
||||
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
|
@ -13289,9 +13321,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/jwt-decode": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
|
||||
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
|
||||
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "3.1.0",
|
||||
|
|
@ -16068,15 +16103,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.2.tgz",
|
||||
"integrity": "sha512-BtRV9BcncDyI2tsuS19zzhzoxD8Dh8LiCx7j7tHzrkz8GFXAexeWFdi22mjE1d16dftH2qNaytVxqiRTGlMfpw==",
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz",
|
||||
"integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"prettier": "bin-prettier.js"
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
|
|
@ -20847,9 +20882,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@adobe/css-tools": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.1.tgz",
|
||||
"integrity": "sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg==",
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.2.tgz",
|
||||
"integrity": "sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw==",
|
||||
"dev": true
|
||||
},
|
||||
"@ampproject/remapping": {
|
||||
|
|
@ -23174,12 +23209,19 @@
|
|||
"prop-types": "^15.7.2",
|
||||
"react-is": "^16.8.0 || ^17.0.0",
|
||||
"react-transition-group": "^4.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@material-ui/icons": {
|
||||
"version": "4.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.2.tgz",
|
||||
"integrity": "sha512-fQNsKX2TxBmqIGJCSi3tGTO/gZ+eJgWmMJkgDiOfyNaunNaxcklJQFaFogYcFl0qFuaEz1qaXYXboa/bUXVSOQ==",
|
||||
"version": "4.11.3",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.3.tgz",
|
||||
"integrity": "sha512-IKHlyx6LDh8n19vzwH5RtHIOHl9Tu90aAAxcbWME6kp4dmvODM3UvOHJeMIDzUbd4muuJKHmlNoBN+mDY4XkBA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.4.4"
|
||||
}
|
||||
|
|
@ -23194,6 +23236,13 @@
|
|||
"clsx": "^1.0.4",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-is": "^16.8.0 || ^17.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@material-ui/styles": {
|
||||
|
|
@ -23217,6 +23266,13 @@
|
|||
"jss-plugin-rule-value-function": "^10.5.1",
|
||||
"jss-plugin-vendor-prefixer": "^10.5.1",
|
||||
"prop-types": "^15.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@material-ui/system": {
|
||||
|
|
@ -23713,13 +23769,11 @@
|
|||
}
|
||||
},
|
||||
"@testing-library/user-event": {
|
||||
"version": "13.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz",
|
||||
"integrity": "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==",
|
||||
"version": "14.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.1.tgz",
|
||||
"integrity": "sha512-UCcUKrUYGj7ClomOo2SpNVvx4/fkd/2BbIHDCle8A0ax+P3bU7yJwDBDrS6ZwdTMARWTGODX1hEsCcO+7beJjg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.5"
|
||||
}
|
||||
"requires": {}
|
||||
},
|
||||
"@tootallnate/once": {
|
||||
"version": "1.1.2",
|
||||
|
|
@ -25529,9 +25583,9 @@
|
|||
}
|
||||
},
|
||||
"clsx": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz",
|
||||
"integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA=="
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
|
||||
"integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q=="
|
||||
},
|
||||
"co": {
|
||||
"version": "4.6.0",
|
||||
|
|
@ -26251,9 +26305,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"deepmerge": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
|
||||
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg=="
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="
|
||||
},
|
||||
"default-gateway": {
|
||||
"version": "6.0.3",
|
||||
|
|
@ -30944,9 +30998,9 @@
|
|||
}
|
||||
},
|
||||
"jwt-decode": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
|
||||
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
|
||||
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA=="
|
||||
},
|
||||
"keyv": {
|
||||
"version": "3.1.0",
|
||||
|
|
@ -32869,9 +32923,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"prettier": {
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.2.tgz",
|
||||
"integrity": "sha512-BtRV9BcncDyI2tsuS19zzhzoxD8Dh8LiCx7j7tHzrkz8GFXAexeWFdi22mjE1d16dftH2qNaytVxqiRTGlMfpw==",
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz",
|
||||
"integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==",
|
||||
"dev": true
|
||||
},
|
||||
"pretty-bytes": {
|
||||
|
|
|
|||
|
|
@ -4,16 +4,16 @@
|
|||
"private": true,
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.11.4",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@material-ui/icons": "^4.11.3",
|
||||
"@material-ui/lab": "^4.0.0-alpha.58",
|
||||
"@material-ui/styles": "^4.11.5",
|
||||
"blueimp-md5": "^2.19.0",
|
||||
"clsx": "^1.1.1",
|
||||
"clsx": "^2.0.0",
|
||||
"connected-react-router": "^6.9.1",
|
||||
"deepmerge": "^4.2.2",
|
||||
"deepmerge": "^4.3.1",
|
||||
"history": "^4.10.1",
|
||||
"inflection": "^1.13.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lodash.pick": "^4.4.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"navidrome-music-player": "4.25.1",
|
||||
|
|
@ -41,9 +41,9 @@
|
|||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/react-hooks": "^7.0.2",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@testing-library/user-event": "^14.5.1",
|
||||
"css-mediaquery": "^0.1.2",
|
||||
"prettier": "2.8.2",
|
||||
"prettier": "3.1.1",
|
||||
"ra-test": "^3.19.12",
|
||||
"react-scripts": "5.0.1",
|
||||
"workbox-cli": "^6.3.0"
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ const useStyles = makeStyles(
|
|||
}),
|
||||
{
|
||||
name: 'NDAlbumDetails',
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const AlbumComment = ({ record }) => {
|
||||
|
|
@ -141,7 +141,7 @@ const AlbumComment = ({ record }) => {
|
|||
timeout={'auto'}
|
||||
className={clsx(
|
||||
classes.commentBlock,
|
||||
lines.length > 1 && classes.pointerCursor
|
||||
lines.length > 1 && classes.pointerCursor,
|
||||
)}
|
||||
>
|
||||
<Typography variant={'body1'} onClick={handleExpandClick}>
|
||||
|
|
@ -216,9 +216,9 @@ const Details = (props) => {
|
|||
addDetail(
|
||||
<>
|
||||
{[translate('resources.album.fields.originalDate'), originalDate].join(
|
||||
' '
|
||||
' ',
|
||||
)}
|
||||
</>
|
||||
</>,
|
||||
)
|
||||
|
||||
yearRange && addDetail(<>{['♫', !isXsmall ? date : yearRange].join(' ')}</>)
|
||||
|
|
@ -230,7 +230,7 @@ const Details = (props) => {
|
|||
? [translate('resources.album.fields.releaseDate'), releaseDate]
|
||||
: ['○', record.releaseDate.substring(0, 4)]
|
||||
).join(' ')}
|
||||
</>
|
||||
</>,
|
||||
)
|
||||
|
||||
const showReleases = record.releases > 1
|
||||
|
|
@ -245,7 +245,7 @@ const Details = (props) => {
|
|||
}),
|
||||
].join(' ')
|
||||
: ['(', record.releases, ')))'].join(' ')}
|
||||
</>
|
||||
</>,
|
||||
)
|
||||
|
||||
addDetail(
|
||||
|
|
@ -255,7 +255,7 @@ const Details = (props) => {
|
|||
translate('resources.song.name', {
|
||||
smart_count: record.songCount,
|
||||
})}
|
||||
</>
|
||||
</>,
|
||||
)
|
||||
!isXsmall && addDetail(<DurationField source={'duration'} />)
|
||||
!isXsmall && addDetail(<SizeField source="size" />)
|
||||
|
|
@ -299,7 +299,7 @@ const AlbumDetails = (props) => {
|
|||
const handleOpenLightbox = React.useCallback(() => setLightboxOpen(true), [])
|
||||
const handleCloseLightbox = React.useCallback(
|
||||
() => setLightboxOpen(false),
|
||||
[]
|
||||
[],
|
||||
)
|
||||
return (
|
||||
<Card className={classes.root}>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ const AlbumExternalLinks = (props) => {
|
|||
encodeURIComponent(record.name)
|
||||
}`,
|
||||
'message.openIn.lastfm',
|
||||
<ImLastfm2 className="lastfm-icon" />
|
||||
<ImLastfm2 className="lastfm-icon" />,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -43,7 +43,7 @@ const AlbumExternalLinks = (props) => {
|
|||
addLink(
|
||||
`https://musicbrainz.org/release/${record.mbzAlbumId}`,
|
||||
'message.openIn.musicbrainz',
|
||||
<MusicBrainz className="musicbrainz-icon" />
|
||||
<MusicBrainz className="musicbrainz-icon" />,
|
||||
)
|
||||
|
||||
return <div className={className}>{intersperse(links, ' ')}</div>
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ const useStyles = makeStyles(
|
|||
albumContainer: {},
|
||||
albumPlayButton: { color: 'white' },
|
||||
}),
|
||||
{ name: 'NDAlbumGridView' }
|
||||
{ name: 'NDAlbumGridView' },
|
||||
)
|
||||
|
||||
const useCoverStyles = makeStyles({
|
||||
|
|
@ -98,32 +98,34 @@ const getColsForWidth = (width) => {
|
|||
return 9
|
||||
}
|
||||
|
||||
const Cover = withContentRect('bounds')(
|
||||
({ record, measureRef, contentRect }) => {
|
||||
// Force height to be the same as the width determined by the GridList
|
||||
// noinspection JSSuspiciousNameCombination
|
||||
const classes = useCoverStyles({ height: contentRect.bounds.width })
|
||||
const [, dragAlbumRef] = useDrag(
|
||||
() => ({
|
||||
type: DraggableTypes.ALBUM,
|
||||
item: { albumIds: [record.id] },
|
||||
options: { dropEffect: 'copy' },
|
||||
}),
|
||||
[record]
|
||||
)
|
||||
return (
|
||||
<div ref={measureRef}>
|
||||
<div ref={dragAlbumRef}>
|
||||
<img
|
||||
src={subsonic.getCoverArtUrl(record, 300)}
|
||||
alt={record.name}
|
||||
className={classes.cover}
|
||||
/>
|
||||
</div>
|
||||
const Cover = withContentRect('bounds')(({
|
||||
record,
|
||||
measureRef,
|
||||
contentRect,
|
||||
}) => {
|
||||
// Force height to be the same as the width determined by the GridList
|
||||
// noinspection JSSuspiciousNameCombination
|
||||
const classes = useCoverStyles({ height: contentRect.bounds.width })
|
||||
const [, dragAlbumRef] = useDrag(
|
||||
() => ({
|
||||
type: DraggableTypes.ALBUM,
|
||||
item: { albumIds: [record.id] },
|
||||
options: { dropEffect: 'copy' },
|
||||
}),
|
||||
[record],
|
||||
)
|
||||
return (
|
||||
<div ref={measureRef}>
|
||||
<div ref={dragAlbumRef}>
|
||||
<img
|
||||
src={subsonic.getCoverArtUrl(record, 300)}
|
||||
alt={record.name}
|
||||
className={classes.cover}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const AlbumGridTile = ({ showArtist, record, basePath, ...props }) => {
|
||||
const classes = useStyles()
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ const AlbumList = (props) => {
|
|||
'size',
|
||||
'createdAt',
|
||||
],
|
||||
['createdAt', 'size']
|
||||
['createdAt', 'size'],
|
||||
)
|
||||
|
||||
// If it does not have filter/sort params (usually coming from Menu),
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ const AlbumViewToggler = React.forwardRef(
|
|||
</ButtonGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const AlbumListActions = ({
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const useStyles = makeStyles(
|
|||
}),
|
||||
{
|
||||
name: 'NDAlbumShow',
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const AlbumShowLayout = (props) => {
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ const useStyles = makeStyles(
|
|||
visibility: 'hidden',
|
||||
},
|
||||
}),
|
||||
{ name: 'RaList' }
|
||||
{ name: 'RaList' },
|
||||
)
|
||||
|
||||
const AlbumSongs = (props) => {
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ const AlbumDatagridRow = (props) => {
|
|||
item: { albumIds: [record?.id] },
|
||||
options: { dropEffect: 'copy' },
|
||||
}),
|
||||
[record]
|
||||
[record],
|
||||
)
|
||||
return <DatagridRow ref={dragAlbumRef} {...props} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const ArtistExternalLinks = ({ artistInfo, record }) => {
|
|||
const translate = useTranslate()
|
||||
let linkButtons = []
|
||||
const lastFMlink = artistInfo?.biography?.match(
|
||||
/<a\s+(?:[^>]*?\s+)?href=(["'])(.*?)\1/
|
||||
/<a\s+(?:[^>]*?\s+)?href=(["'])(.*?)\1/,
|
||||
)
|
||||
|
||||
const addLink = (url, title, icon) => {
|
||||
|
|
@ -34,13 +34,13 @@ const ArtistExternalLinks = ({ artistInfo, record }) => {
|
|||
addLink(
|
||||
lastFMlink[2],
|
||||
'message.openIn.lastfm',
|
||||
<ImLastfm2 className="lastfm-icon" />
|
||||
<ImLastfm2 className="lastfm-icon" />,
|
||||
)
|
||||
} else if (artistInfo?.lastFmUrl) {
|
||||
addLink(
|
||||
artistInfo?.lastFmUrl,
|
||||
'message.openIn.lastfm',
|
||||
<ImLastfm2 className="lastfm-icon" />
|
||||
<ImLastfm2 className="lastfm-icon" />,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -49,7 +49,7 @@ const ArtistExternalLinks = ({ artistInfo, record }) => {
|
|||
addLink(
|
||||
`https://musicbrainz.org/artist/${artistInfo.musicBrainzId}`,
|
||||
'message.openIn.musicbrainz',
|
||||
<MusicBrainz className="musicbrainz-icon" />
|
||||
<MusicBrainz className="musicbrainz-icon" />,
|
||||
)
|
||||
|
||||
return <div>{intersperse(linkButtons, ' ')}</div>
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ const ArtistDatagridRow = (props) => {
|
|||
item: { artistIds: [record?.id] },
|
||||
options: { dropEffect: 'copy' },
|
||||
}),
|
||||
[record]
|
||||
[record],
|
||||
)
|
||||
return <DatagridRow ref={dragArtistRef} {...props} />
|
||||
}
|
||||
|
|
@ -132,7 +132,7 @@ const ArtistListView = ({ hasShow, hasEdit, hasList, width, ...rest }) => {
|
|||
resource: 'artist',
|
||||
columns: toggleableFields,
|
||||
},
|
||||
['size']
|
||||
['size'],
|
||||
)
|
||||
|
||||
return isXsmall ? (
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ const useStyles = makeStyles(
|
|||
wordBreak: 'break-word',
|
||||
},
|
||||
}),
|
||||
{ name: 'NDDesktopArtistDetails' }
|
||||
{ name: 'NDDesktopArtistDetails' },
|
||||
)
|
||||
|
||||
const DesktopArtistDetails = ({ artistInfo, record, biography }) => {
|
||||
|
|
@ -77,7 +77,7 @@ const DesktopArtistDetails = ({ artistInfo, record, biography }) => {
|
|||
const handleOpenLightbox = React.useCallback(() => setLightboxOpen(true), [])
|
||||
const handleCloseLightbox = React.useCallback(
|
||||
() => setLightboxOpen(false),
|
||||
[]
|
||||
[],
|
||||
)
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ const useStyles = makeStyles(
|
|||
wordBreak: 'break-word',
|
||||
},
|
||||
}),
|
||||
{ name: 'NDMobileArtistDetails' }
|
||||
{ name: 'NDMobileArtistDetails' },
|
||||
)
|
||||
|
||||
const MobileArtistDetails = ({ artistInfo, biography, record }) => {
|
||||
|
|
@ -85,7 +85,7 @@ const MobileArtistDetails = ({ artistInfo, biography, record }) => {
|
|||
const handleOpenLightbox = React.useCallback(() => setLightboxOpen(true), [])
|
||||
const handleCloseLightbox = React.useCallback(
|
||||
() => setLightboxOpen(false),
|
||||
[]
|
||||
[],
|
||||
)
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ const Player = () => {
|
|||
const isDesktop = useMediaQuery('(min-width:810px)')
|
||||
const isMobilePlayer =
|
||||
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||
navigator.userAgent
|
||||
navigator.userAgent,
|
||||
)
|
||||
|
||||
const { authenticated } = useAuthState()
|
||||
|
|
@ -60,7 +60,7 @@ const Player = () => {
|
|||
enableCoverAnimation: config.enableCoverAnimation,
|
||||
})
|
||||
const showNotifications = useSelector(
|
||||
(state) => state.settings.notifications || false
|
||||
(state) => state.settings.notifications || false,
|
||||
)
|
||||
const gainInfo = useSelector((state) => state.replayGain)
|
||||
const [context, setContext] = useState(null)
|
||||
|
|
@ -100,7 +100,7 @@ const Player = () => {
|
|||
numericGain = calculateReplayGain(
|
||||
gainInfo.preAmp,
|
||||
song.rgAlbumGain,
|
||||
song.rgAlbumPeak
|
||||
song.rgAlbumPeak,
|
||||
)
|
||||
break
|
||||
}
|
||||
|
|
@ -108,7 +108,7 @@ const Player = () => {
|
|||
numericGain = calculateReplayGain(
|
||||
gainInfo.preAmp,
|
||||
song.rgTrackGain,
|
||||
song.rgTrackPeak
|
||||
song.rgTrackPeak,
|
||||
)
|
||||
break
|
||||
}
|
||||
|
|
@ -160,7 +160,7 @@ const Player = () => {
|
|||
),
|
||||
locale: locale(translate),
|
||||
}),
|
||||
[gainInfo, isDesktop, playerTheme, translate]
|
||||
[gainInfo, isDesktop, playerTheme, translate],
|
||||
)
|
||||
|
||||
const options = useMemo(() => {
|
||||
|
|
@ -181,12 +181,12 @@ const Player = () => {
|
|||
|
||||
const onAudioListsChange = useCallback(
|
||||
(_, audioLists, audioInfo) => dispatch(syncQueue(audioInfo, audioLists)),
|
||||
[dispatch]
|
||||
[dispatch],
|
||||
)
|
||||
|
||||
const nextSong = useCallback(() => {
|
||||
const idx = playerState.queue.findIndex(
|
||||
(item) => item.uuid === playerState.current.uuid
|
||||
(item) => item.uuid === playerState.current.uuid,
|
||||
)
|
||||
return idx !== null ? playerState.queue[idx + 1] : null
|
||||
}, [playerState])
|
||||
|
|
@ -221,13 +221,13 @@ const Player = () => {
|
|||
setScrobbled(true)
|
||||
}
|
||||
},
|
||||
[startTime, scrobbled, nextSong, preloaded]
|
||||
[startTime, scrobbled, nextSong, preloaded],
|
||||
)
|
||||
|
||||
const onAudioVolumeChange = useCallback(
|
||||
// sqrt to compensate for the logarithmic volume
|
||||
(volume) => dispatch(setVolume(Math.sqrt(volume))),
|
||||
[dispatch]
|
||||
[dispatch],
|
||||
)
|
||||
|
||||
const onAudioPlay = useCallback(
|
||||
|
|
@ -260,12 +260,12 @@ const Player = () => {
|
|||
sendNotification(
|
||||
song.title,
|
||||
`${song.artist} - ${song.album}`,
|
||||
info.cover
|
||||
info.cover,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
[context, dispatch, showNotifications, startTime]
|
||||
[context, dispatch, showNotifications, startTime],
|
||||
)
|
||||
|
||||
const onAudioPlayTrackChange = useCallback(() => {
|
||||
|
|
@ -279,7 +279,7 @@ const Player = () => {
|
|||
|
||||
const onAudioPause = useCallback(
|
||||
(info) => dispatch(currentPlaying(info)),
|
||||
[dispatch]
|
||||
[dispatch],
|
||||
)
|
||||
|
||||
const onAudioEnded = useCallback(
|
||||
|
|
@ -291,7 +291,7 @@ const Player = () => {
|
|||
.getOne('keepalive', { id: info.trackId })
|
||||
.catch((e) => console.log('Keepalive error:', e))
|
||||
},
|
||||
[dispatch, dataProvider]
|
||||
[dispatch, dataProvider],
|
||||
)
|
||||
|
||||
const onCoverClick = useCallback((mode, audioLists, audioInfo) => {
|
||||
|
|
@ -313,7 +313,7 @@ const Player = () => {
|
|||
|
||||
const handlers = useMemo(
|
||||
() => keyHandlers(audioInstance, playerState),
|
||||
[audioInstance, playerState]
|
||||
[audioInstance, playerState],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
const keyHandlers = (audioInstance, playerState) => {
|
||||
const nextSong = () => {
|
||||
const idx = playerState.queue.findIndex(
|
||||
(item) => item.uuid === playerState.current.uuid
|
||||
(item) => item.uuid === playerState.current.uuid,
|
||||
)
|
||||
return idx !== null ? playerState.queue[idx + 1] : null
|
||||
}
|
||||
|
||||
const prevSong = () => {
|
||||
const idx = playerState.queue.findIndex(
|
||||
(item) => item.uuid === playerState.current.uuid
|
||||
(item) => item.uuid === playerState.current.uuid,
|
||||
)
|
||||
return idx !== null ? playerState.queue[idx - 1] : null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ const useStyle = makeStyles(
|
|||
},
|
||||
},
|
||||
}),
|
||||
{ name: 'NDAudioPlayer' }
|
||||
{ name: 'NDAudioPlayer' },
|
||||
)
|
||||
|
||||
export default useStyle
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import jwtDecode from 'jwt-decode'
|
||||
import { jwtDecode } from 'jwt-decode'
|
||||
import { baseUrl } from './utils'
|
||||
import config from './config'
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ export const AddToPlaylistButton = ({ resource, selectedIds, className }) => {
|
|||
|
||||
const handleClick = () => {
|
||||
dispatch(
|
||||
openAddToPlaylist({ selectedIds, onSuccess: () => unselectAll(resource) })
|
||||
openAddToPlaylist({
|
||||
selectedIds,
|
||||
onSuccess: () => unselectAll(resource),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,28 +14,31 @@ export const useGetHandleArtistClick = (width) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const ArtistLinkField = withWidth()(
|
||||
({ record, className, width, source }) => {
|
||||
const artistLink = useGetHandleArtistClick(width)
|
||||
export const ArtistLinkField = withWidth()(({
|
||||
record,
|
||||
className,
|
||||
width,
|
||||
source,
|
||||
}) => {
|
||||
const artistLink = useGetHandleArtistClick(width)
|
||||
|
||||
const id = record[source + 'Id']
|
||||
return (
|
||||
<>
|
||||
{id ? (
|
||||
<Link
|
||||
to={artistLink(id)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className={className}
|
||||
>
|
||||
{record[source]}
|
||||
</Link>
|
||||
) : (
|
||||
record[source]
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
)
|
||||
const id = record[source + 'Id']
|
||||
return (
|
||||
<>
|
||||
{id ? (
|
||||
<Link
|
||||
to={artistLink(id)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className={className}
|
||||
>
|
||||
{record[source]}
|
||||
</Link>
|
||||
) : (
|
||||
record[source]
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
ArtistLinkField.propTypes = {
|
||||
record: PropTypes.object,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const useStyles = makeStyles(
|
|||
top: '26px',
|
||||
},
|
||||
},
|
||||
{ name: 'RaArtistSimpleList' }
|
||||
{ name: 'RaArtistSimpleList' },
|
||||
)
|
||||
|
||||
export const ArtistSimpleList = ({
|
||||
|
|
@ -69,7 +69,7 @@ export const ArtistSimpleList = ({
|
|||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
</span>
|
||||
)
|
||||
),
|
||||
)}
|
||||
</List>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export const BatchPlayButton = ({
|
|||
// Add tracks to a map for easy lookup by ID, needed for the next step
|
||||
const tracks = response.data.reduce(
|
||||
(acc, cur) => ({ ...acc, [cur.id]: cur }),
|
||||
{}
|
||||
{},
|
||||
)
|
||||
// Add the tracks to the queue in the selection order
|
||||
dispatch(action(tracks, selectedIds))
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ export const BatchShareButton = ({ resource, selectedIds, className }) => {
|
|||
_: 'ra.action.bulk_actions',
|
||||
smart_count: selectedIds.length,
|
||||
}),
|
||||
'message.shareBatchDialogTitle'
|
||||
)
|
||||
'message.shareBatchDialogTitle',
|
||||
),
|
||||
)
|
||||
unselectAll(resource)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,8 +97,8 @@ const ContextMenu = ({
|
|||
record,
|
||||
record.duration !== undefined
|
||||
? DOWNLOAD_MENU_ALBUM
|
||||
: DOWNLOAD_MENU_ARTIST
|
||||
)
|
||||
: DOWNLOAD_MENU_ARTIST,
|
||||
),
|
||||
)
|
||||
},
|
||||
},
|
||||
|
|
@ -127,7 +127,7 @@ const ContextMenu = ({
|
|||
let extractSongsData = function (response) {
|
||||
const data = response.data.reduce(
|
||||
(acc, cur) => ({ ...acc, [cur.id]: cur }),
|
||||
{}
|
||||
{},
|
||||
)
|
||||
const ids = response.data.map((r) => r.id)
|
||||
return { data, ids }
|
||||
|
|
@ -186,7 +186,7 @@ const ContextMenu = ({
|
|||
<MenuItem value={key} key={key} onClick={handleItemClick}>
|
||||
{options[key].label}
|
||||
</MenuItem>
|
||||
)
|
||||
),
|
||||
)}
|
||||
</Menu>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const useStyles = makeStyles(
|
|||
color: theme.palette.primary.main,
|
||||
},
|
||||
}),
|
||||
{ name: 'RaLink' }
|
||||
{ name: 'RaLink' },
|
||||
)
|
||||
|
||||
const Linkify = ({ text, ...rest }) => {
|
||||
|
|
@ -45,7 +45,7 @@ const Linkify = ({ text, ...rest }) => {
|
|||
href={href}
|
||||
>
|
||||
{href}
|
||||
</Link>
|
||||
</Link>,
|
||||
)
|
||||
|
||||
lastIndex = match.index + href.length
|
||||
|
|
@ -57,7 +57,7 @@ const Linkify = ({ text, ...rest }) => {
|
|||
<span
|
||||
key={'last-span-key'}
|
||||
dangerouslySetInnerHTML={{ __html: text.substring(lastIndex) }}
|
||||
/>
|
||||
/>,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export const LoveButton = ({
|
|||
toggleLove()
|
||||
e.stopPropagation()
|
||||
},
|
||||
[toggleLove]
|
||||
[toggleLove],
|
||||
)
|
||||
|
||||
if (!config.enableFavourites) {
|
||||
|
|
|
|||
|
|
@ -39,11 +39,11 @@ export const MultiLineTextField = memo(
|
|||
key={md5(line + idx)}
|
||||
dangerouslySetInnerHTML={{ __html: line }}
|
||||
/>
|
||||
)
|
||||
),
|
||||
)}
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
MultiLineTextField.defaultProps = {
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ describe('<MultiLineTextField />', () => {
|
|||
record={{ id: 123, body }}
|
||||
emptyText="NA"
|
||||
source="body"
|
||||
/>
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByText('NA')).toBeInTheDocument()
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export const PlayButton = ({ record, size, className }) => {
|
|||
let extractSongsData = function (response) {
|
||||
const data = response.data.reduce(
|
||||
(acc, cur) => ({ ...acc, [cur.id]: cur }),
|
||||
{}
|
||||
{},
|
||||
)
|
||||
const ids = response.data.map((r) => r.id)
|
||||
return { data, ids }
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const useStyle = makeStyles(
|
|||
}),
|
||||
{
|
||||
name: 'NDQualityInfo',
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export const QualityInfo = ({ record, size, gainMode, preAmp, className }) => {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ describe('QuickFilter', () => {
|
|||
resource={'song'}
|
||||
source={'name'}
|
||||
label={<StarIcon data-testid="label-icon-test" />}
|
||||
/>
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByTestId('label-icon-test')).not.toBeNull()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export const RatingField = ({
|
|||
(e, val) => {
|
||||
rate(val, e.target.name)
|
||||
},
|
||||
[rate]
|
||||
[rate],
|
||||
)
|
||||
|
||||
return (
|
||||
|
|
@ -50,7 +50,7 @@ export const RatingField = ({
|
|||
className={clsx(
|
||||
className,
|
||||
classes.rating,
|
||||
rating > 0 ? classes.show : classes.hide
|
||||
rating > 0 ? classes.show : classes.hide,
|
||||
)}
|
||||
value={rating}
|
||||
size={size}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const useStyles = makeStyles(
|
|||
},
|
||||
tertiary: { float: 'right', opacity: 0.541176 },
|
||||
},
|
||||
{ name: 'RaSimpleList' }
|
||||
{ name: 'RaSimpleList' },
|
||||
)
|
||||
|
||||
const LinkOrNot = ({
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export const SongContextMenu = ({
|
|||
openAddToPlaylist({
|
||||
selectedIds: [record.mediaFileId || record.id],
|
||||
onSuccess: (id) => onAddToPlaylist(id),
|
||||
})
|
||||
}),
|
||||
),
|
||||
},
|
||||
share: {
|
||||
|
|
@ -69,7 +69,11 @@ export const SongContextMenu = ({
|
|||
label: translate('ra.action.share'),
|
||||
action: (record) =>
|
||||
dispatch(
|
||||
openShareMenu([record.mediaFileId || record.id], 'song', record.title)
|
||||
openShareMenu(
|
||||
[record.mediaFileId || record.id],
|
||||
'song',
|
||||
record.title,
|
||||
),
|
||||
),
|
||||
},
|
||||
download: {
|
||||
|
|
@ -127,7 +131,7 @@ export const SongContextMenu = ({
|
|||
<MenuItem value={key} key={key} onClick={handleItemClick}>
|
||||
{options[key].label}
|
||||
</MenuItem>
|
||||
)
|
||||
),
|
||||
)}
|
||||
</Menu>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ const ReleaseRow = forwardRef(
|
|||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const DiscSubtitleRow = forwardRef(
|
||||
|
|
@ -141,7 +141,7 @@ const DiscSubtitleRow = forwardRef(
|
|||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export const SongDatagridRow = ({
|
||||
|
|
@ -156,7 +156,7 @@ export const SongDatagridRow = ({
|
|||
}) => {
|
||||
const classes = useStyles()
|
||||
const fields = React.Children.toArray(children).filter((c) =>
|
||||
isValidElement(c)
|
||||
isValidElement(c),
|
||||
)
|
||||
|
||||
const [, dragDiscRef] = useDrag(
|
||||
|
|
@ -173,7 +173,7 @@ export const SongDatagridRow = ({
|
|||
},
|
||||
options: { dropEffect: 'copy' },
|
||||
}),
|
||||
[record]
|
||||
[record],
|
||||
)
|
||||
|
||||
const [, dragSongRef] = useDrag(
|
||||
|
|
@ -182,7 +182,7 @@ export const SongDatagridRow = ({
|
|||
item: { ids: [record?.mediaFileId || record?.id] },
|
||||
options: { dropEffect: 'copy' },
|
||||
}),
|
||||
[record]
|
||||
[record],
|
||||
)
|
||||
|
||||
if (!record || !record.title) {
|
||||
|
|
@ -251,14 +251,14 @@ const SongDatagridBody = ({
|
|||
idsToPlay = ids.filter(
|
||||
(id) =>
|
||||
data[id].releaseDate === releaseDate &&
|
||||
data[id].discNumber === discNumber
|
||||
data[id].discNumber === discNumber,
|
||||
)
|
||||
} else {
|
||||
idsToPlay = ids.filter((id) => data[id].releaseDate === releaseDate)
|
||||
}
|
||||
dispatch(playTracks(data, idsToPlay))
|
||||
},
|
||||
[dispatch, data, ids]
|
||||
[dispatch, data, ids],
|
||||
)
|
||||
|
||||
const firstTracksOfDiscs = useMemo(() => {
|
||||
|
|
@ -280,7 +280,7 @@ const SongDatagridBody = ({
|
|||
acc.push(id)
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
}, []),
|
||||
)
|
||||
if (!showDiscSubtitles || (set.size < 2 && !foundSubtitle)) {
|
||||
set.clear()
|
||||
|
|
@ -304,7 +304,7 @@ const SongDatagridBody = ({
|
|||
acc.push(id)
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
}, []),
|
||||
)
|
||||
if (!showReleaseDivider || set.size < 2) {
|
||||
set.clear()
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ const useStyles = makeStyles(
|
|||
top: '26px',
|
||||
},
|
||||
},
|
||||
{ name: 'RaSongSimpleList' }
|
||||
{ name: 'RaSongSimpleList' },
|
||||
)
|
||||
|
||||
export const SongSimpleList = ({
|
||||
|
|
@ -108,7 +108,7 @@ export const SongSimpleList = ({
|
|||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
</span>
|
||||
)
|
||||
),
|
||||
)}
|
||||
</List>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export const ToggleFieldsMenu = ({
|
|||
const dispatch = useDispatch()
|
||||
const translate = useTranslate()
|
||||
const toggleableColumns = useSelector(
|
||||
(state) => state.settings.toggleableFields[resource]
|
||||
(state) => state.settings.toggleableFields[resource],
|
||||
)
|
||||
const omittedColumns =
|
||||
useSelector((state) => state.settings.omittedFields[resource]) || []
|
||||
|
|
@ -58,7 +58,7 @@ export const ToggleFieldsMenu = ({
|
|||
...toggleableColumns,
|
||||
[selectedColumn]: !toggleableColumns[selectedColumn],
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -95,7 +95,7 @@ export const ToggleFieldsMenu = ({
|
|||
<Checkbox checked={val} />
|
||||
{translate(`resources.${resource}.fields.${key}`)}
|
||||
</MenuItem>
|
||||
) : null
|
||||
) : null,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export const Writable = (props) => {
|
|||
const { record = {}, children } = props
|
||||
if (isWritable(record.ownerId)) {
|
||||
return Children.map(children, (child) =>
|
||||
isValidElement(child) ? cloneElement(child, props) : child
|
||||
isValidElement(child) ? cloneElement(child, props) : child,
|
||||
)
|
||||
}
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const getPerPageOptions = (width) => {
|
|||
export const useAlbumsPerPage = (width) => {
|
||||
const perPage =
|
||||
useSelector(
|
||||
(state) => state?.admin.resources?.album?.list?.params?.perPage
|
||||
(state) => state?.admin.resources?.album?.list?.params?.perPage,
|
||||
) || getPerPage(width)
|
||||
|
||||
return [perPage, getPerPageOptions(width)]
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export const useResourceRefresh = (...visibleResources) => {
|
|||
const refresh = useRefresh()
|
||||
const dataProvider = useDataProvider()
|
||||
const refreshData = useSelector(
|
||||
(state) => state.activity?.refresh || { lastReceived: lastTime }
|
||||
(state) => state.activity?.refresh || { lastReceived: lastTime },
|
||||
)
|
||||
const { resources, lastReceived } = refreshData
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue