diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d27ff208c..f339f62f7 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,7 +4,7 @@ "dockerfile": "Dockerfile", "args": { // Update the VARIANT arg to pick a version of Go: 1, 1.15, 1.14 - "VARIANT": "1.23", + "VARIANT": "1.24", // Options "INSTALL_NODE": "true", "NODE_VERSION": "v20" diff --git a/Dockerfile b/Dockerfile index 224595d93..9dda15cc6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -61,7 +61,7 @@ COPY --from=ui /build /build ######################################################################################################################## ### Build Navidrome binary -FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/golang:1.23-bookworm AS base +FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/golang:1.24-bookworm AS base RUN apt-get update && apt-get install -y clang lld COPY --from=xx / / WORKDIR /workspace diff --git a/Makefile b/Makefile index dbcb44b94..6c4232623 100644 --- a/Makefile +++ b/Makefile @@ -29,11 +29,11 @@ dev: check_env ##@Development Start Navidrome in development mode, with hot-re .PHONY: dev server: check_go_env buildjs ##@Development Start the backend in development mode - @ND_ENABLEINSIGHTSCOLLECTOR="false" go run github.com/cespare/reflex@latest -d none -c reflex.conf + @ND_ENABLEINSIGHTSCOLLECTOR="false" go tool reflex -d none -c reflex.conf .PHONY: server watch: ##@Development Start Go tests in watch mode (re-run when code changes) - go run github.com/onsi/ginkgo/v2/ginkgo@latest watch -tags=netgo -notify ./... + go tool ginkgo watch -tags=netgo -notify ./... .PHONY: watch test: ##@Development Run Go tests @@ -59,16 +59,16 @@ lintall: lint ##@Development Lint Go and JS code format: ##@Development Format code @(cd ./ui && npm run prettier) - @go run golang.org/x/tools/cmd/goimports@latest -w `find . -name '*.go' | grep -v _gen.go$$` + @go tool goimports -w `find . -name '*.go' | grep -v _gen.go$$` @go mod tidy .PHONY: format wire: check_go_env ##@Development Update Dependency Injection - go run github.com/google/wire/cmd/wire@latest gen -tags=netgo ./... + go tool wire gen -tags=netgo ./... .PHONY: wire snapshots: ##@Development Update (GoLang) Snapshot tests - UPDATE_SNAPSHOTS=true go run github.com/onsi/ginkgo/v2/ginkgo@latest ./server/subsonic/responses/... + UPDATE_SNAPSHOTS=true go tool ginkgo ./server/subsonic/responses/... .PHONY: snapshots migration-sql: ##@Development Create an empty SQL migration file diff --git a/Procfile.dev b/Procfile.dev index 5af64f49b..0c187e811 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -1,2 +1,2 @@ JS: sh -c "cd ./ui && npm start" -GO: go run github.com/cespare/reflex@latest -d none -c reflex.conf +GO: go tool reflex -d none -c reflex.conf diff --git a/consts/consts.go b/consts/consts.go index 7f46fe39a..75271bec8 100644 --- a/consts/consts.go +++ b/consts/consts.go @@ -151,13 +151,17 @@ var ( UnknownArtistID = id.NewHash(strings.ToLower(UnknownArtist)) VariousArtistsMbzId = "89ad4ac3-39f7-470e-963a-56509c546377" - ServerStart = time.Now() + ArtistJoiner = " • " ) -var InContainer = func() bool { - // Check if the /.nddockerenv file exists - if _, err := os.Stat("/.nddockerenv"); err == nil { - return true - } - return false -}() +var ( + ServerStart = time.Now() + + InContainer = func() bool { + // Check if the /.nddockerenv file exists + if _, err := os.Stat("/.nddockerenv"); err == nil { + return true + } + return false + }() +) diff --git a/git/pre-commit b/git/pre-commit index 6bb2b314f..04f87994b 100755 --- a/git/pre-commit +++ b/git/pre-commit @@ -10,7 +10,7 @@ # # This script does not handle file names that contain spaces. -gofmtcmd="go run golang.org/x/tools/cmd/goimports@latest" +gofmtcmd="go tool goimports" gofiles=$(git diff --cached --name-only --diff-filter=ACM | grep '.go$' | grep -v '_gen.go$') [ -z "$gofiles" ] && exit 0 diff --git a/go.mod b/go.mod index a33403a34..13ef754fd 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/navidrome/navidrome -go 1.23.4 +go 1.24.1 // Fork to fix https://github.com/navidrome/navidrome/pull/3254 replace github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 => github.com/deluan/tag v0.0.0-20241002021117-dfe5e6ea396d @@ -72,7 +72,9 @@ require ( github.com/anacrolix/log v0.15.2 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/reflex v0.3.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/creack/pty v1.1.11 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect @@ -81,10 +83,12 @@ require ( github.com/goccy/go-json v0.10.5 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 // indirect + github.com/google/subcommands v1.2.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/klauspost/compress v1.17.11 // indirect github.com/kr/text v0.2.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect @@ -98,6 +102,7 @@ require ( github.com/mfridman/interpolate v0.0.2 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/ogier/pflag v0.0.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.62.0 // indirect @@ -115,8 +120,16 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.36.0 // indirect + golang.org/x/mod v0.24.0 // indirect golang.org/x/tools v0.31.0 // indirect google.golang.org/protobuf v1.36.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect ) + +tool ( + github.com/cespare/reflex + github.com/google/wire/cmd/wire + github.com/onsi/ginkgo/v2/ginkgo + golang.org/x/tools/cmd/goimports +) diff --git a/go.sum b/go.sum index 19e870c13..987868b40 100644 --- a/go.sum +++ b/go.sum @@ -20,10 +20,14 @@ github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= +github.com/cespare/reflex v0.3.1 h1:N4Y/UmRrjwOkNT0oQQnYsdr6YBxvHqtSfPB4mqOyAKk= +github.com/cespare/reflex v0.3.1/go.mod h1:I+0Pnu2W693i7Hv6ZZG76qHTY0mgUa7uCIfCtikXojE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -54,6 +58,7 @@ 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.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= @@ -84,6 +89,7 @@ github.com/google/go-pipeline v0.0.0-20230411140531-6cbedfc1d3fc h1:hd+uUVsB1vdx github.com/google/go-pipeline v0.0.0-20230411140531-6cbedfc1d3fc/go.mod h1:SL66SJVysrh7YbDCP9tH30b8a9o/N2HeiQNUm85EKhc= github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 h1:+J3r2e8+RsmN3vKfo75g0YSY61ms37qzPglu4p0sGro= github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -111,11 +117,16 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kardianos/service v1.2.2 h1:ZvePhAHfvo0A7Mftk/tEzqEZ7Q4lgnR8sGz4xu1YX60= github.com/kardianos/service v1.2.2/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= @@ -156,6 +167,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/ogier/pflag v0.0.1 h1:RW6JSWSu/RkSatfcLtogGfFgpim5p7ARQ10ECk5O750= +github.com/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g= github.com/onsi/ginkgo/v2 v2.23.0 h1:FA1xjp8ieYDzlgS5ABTpdUDB7wtngggONc8a7ku2NqQ= github.com/onsi/ginkgo/v2 v2.23.0/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM= github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= @@ -261,6 +274,8 @@ golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -284,6 +299,7 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/model/metadata/map_mediafile.go b/model/metadata/map_mediafile.go index 53c5a8db2..47d2578ec 100644 --- a/model/metadata/map_mediafile.go +++ b/model/metadata/map_mediafile.go @@ -72,7 +72,7 @@ func (md Metadata) ToMediaFile(libID int, folderID string) model.MediaFile { mf.UpdatedAt = md.ModTime() mf.Participants = md.mapParticipants() - mf.Artist = md.mapDisplayArtist(mf) + mf.Artist = md.mapDisplayArtist() mf.AlbumArtist = md.mapDisplayAlbumArtist(mf) // Persistent IDs diff --git a/model/metadata/map_participants.go b/model/metadata/map_participants.go index 9d47c676d..9305d8791 100644 --- a/model/metadata/map_participants.go +++ b/model/metadata/map_participants.go @@ -2,6 +2,7 @@ package metadata import ( "cmp" + "strings" "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/model" @@ -199,32 +200,28 @@ func (md Metadata) getArtistValues(single, multi model.TagName) []string { return vSingle } -func (md Metadata) getTags(tagNames ...model.TagName) []string { - for _, tagName := range tagNames { - values := md.Strings(tagName) - if len(values) > 0 { - return values - } - } - return nil -} -func (md Metadata) mapDisplayRole(mf model.MediaFile, role model.Role, tagNames ...model.TagName) string { - artistNames := md.getTags(tagNames...) - values := []string{ - "", - mf.Participants.First(role).Name, - consts.UnknownArtist, - } - if len(artistNames) == 1 { - values[0] = artistNames[0] - } - return cmp.Or(values...) +func (md Metadata) mapDisplayName(singularTagName, pluralTagName model.TagName) string { + return cmp.Or( + strings.Join(md.tags[singularTagName], consts.ArtistJoiner), + strings.Join(md.tags[pluralTagName], consts.ArtistJoiner), + ) } -func (md Metadata) mapDisplayArtist(mf model.MediaFile) string { - return md.mapDisplayRole(mf, model.RoleArtist, model.TagTrackArtist, model.TagTrackArtists) +func (md Metadata) mapDisplayArtist() string { + return cmp.Or( + md.mapDisplayName(model.TagTrackArtist, model.TagTrackArtists), + consts.UnknownArtist, + ) } func (md Metadata) mapDisplayAlbumArtist(mf model.MediaFile) string { - return md.mapDisplayRole(mf, model.RoleAlbumArtist, model.TagAlbumArtist, model.TagAlbumArtists) + fallbackName := consts.UnknownArtist + if md.Bool(model.TagCompilation) { + fallbackName = consts.VariousArtists + } + return cmp.Or( + md.mapDisplayName(model.TagAlbumArtist, model.TagAlbumArtists), + mf.Participants.First(model.RoleAlbumArtist).Name, + fallbackName, + ) } diff --git a/model/metadata/map_participants_test.go b/model/metadata/map_participants_test.go index a1c8ed527..5317a4bcf 100644 --- a/model/metadata/map_participants_test.go +++ b/model/metadata/map_participants_test.go @@ -45,6 +45,10 @@ var _ = Describe("Participants", func() { mf = toMediaFile(model.RawTags{}) }) + It("should set the display name to Unknown Artist", func() { + Expect(mf.Artist).To(Equal("[Unknown Artist]")) + }) + It("should set artist to Unknown Artist", func() { Expect(mf.Artist).To(Equal("[Unknown Artist]")) }) @@ -92,6 +96,7 @@ var _ = Describe("Participants", func() { Expect(artist.MbzArtistID).To(Equal(mbid1)) }) }) + Context("Multiple values in a Single-valued ARTIST tags, no ARTISTS tags", func() { BeforeEach(func() { mf = toMediaFile(model.RawTags{ @@ -101,12 +106,13 @@ var _ = Describe("Participants", func() { }) }) - It("should split the tag", func() { - By("keeping the first artist as the display name") + It("should use the full string as display name", func() { Expect(mf.Artist).To(Equal("Artist Name feat. Someone Else")) Expect(mf.SortArtistName).To(Equal("Name, Artist")) Expect(mf.OrderArtistName).To(Equal("artist name")) + }) + It("should split the tag", func() { participants := mf.Participants Expect(participants).To(SatisfyAll( HaveKeyWithValue(model.RoleArtist, HaveLen(2)), @@ -130,6 +136,7 @@ var _ = Describe("Participants", func() { Expect(artist1.SortArtistName).To(Equal("Else, Someone")) Expect(artist1.MbzArtistID).To(BeEmpty()) }) + It("should split the tag using case-insensitive separators", func() { mf = toMediaFile(model.RawTags{ "ARTIST": {"A1 FEAT. A2"}, @@ -167,8 +174,8 @@ var _ = Describe("Participants", func() { }) }) - It("should use the first artist name as display name", func() { - Expect(mf.Artist).To(Equal("First Artist")) + It("should concatenate all ARTIST values as display name", func() { + Expect(mf.Artist).To(Equal("First Artist • Second Artist")) }) It("should populate the participants with all artists", func() { @@ -194,6 +201,101 @@ var _ = Describe("Participants", func() { }) }) + Context("Single-valued ARTIST tag, single-valued ARTISTS tag, same values", func() { + BeforeEach(func() { + mf = toMediaFile(model.RawTags{ + "ARTIST": {"Artist Name"}, + "ARTISTS": {"Artist Name"}, + "ARTISTSORT": {"Name, Artist"}, + "MUSICBRAINZ_ARTISTID": {mbid1}, + }) + }) + + It("should use the ARTIST tag as display name", func() { + Expect(mf.Artist).To(Equal("Artist Name")) + }) + + It("should populate the participants with the ARTIST", func() { + participants := mf.Participants + Expect(participants).To(HaveLen(2)) // ARTIST and ALBUMARTIST + Expect(participants).To(SatisfyAll( + HaveKeyWithValue(model.RoleArtist, HaveLen(1)), + )) + + artist := participants[model.RoleArtist][0] + Expect(artist.ID).ToNot(BeEmpty()) + Expect(artist.Name).To(Equal("Artist Name")) + Expect(artist.OrderArtistName).To(Equal("artist name")) + Expect(artist.SortArtistName).To(Equal("Name, Artist")) + Expect(artist.MbzArtistID).To(Equal(mbid1)) + }) + }) + + Context("Single-valued ARTIST tag, single-valued ARTISTS tag, different values", func() { + BeforeEach(func() { + mf = toMediaFile(model.RawTags{ + "ARTIST": {"Artist Name"}, + "ARTISTS": {"Artist Name 2"}, + "ARTISTSORT": {"Name, Artist"}, + "MUSICBRAINZ_ARTISTID": {mbid1}, + }) + }) + + It("should use the ARTIST tag as display name", func() { + Expect(mf.Artist).To(Equal("Artist Name")) + }) + + It("should use only artists from ARTISTS", func() { + participants := mf.Participants + Expect(participants).To(HaveLen(2)) // ARTIST and ALBUMARTIST + Expect(participants).To(SatisfyAll( + HaveKeyWithValue(model.RoleArtist, HaveLen(1)), + )) + + artist := participants[model.RoleArtist][0] + Expect(artist.ID).ToNot(BeEmpty()) + Expect(artist.Name).To(Equal("Artist Name 2")) + Expect(artist.OrderArtistName).To(Equal("artist name 2")) + Expect(artist.SortArtistName).To(Equal("Name, Artist")) + Expect(artist.MbzArtistID).To(Equal(mbid1)) + }) + }) + + Context("No ARTIST tag, multi-valued ARTISTS tag", func() { + BeforeEach(func() { + mf = toMediaFile(model.RawTags{ + "ARTISTS": {"First Artist", "Second Artist"}, + "ARTISTSSORT": {"Name, First Artist", "Name, Second Artist"}, + }) + }) + + It("should concatenate ARTISTS as display name", func() { + Expect(mf.Artist).To(Equal("First Artist • Second Artist")) + }) + + It("should populate the participants with all artists", func() { + participants := mf.Participants + Expect(participants).To(HaveLen(2)) // ARTIST and ALBUMARTIST + Expect(participants).To(SatisfyAll( + HaveKeyWithValue(model.RoleArtist, HaveLen(2)), + )) + + artist0 := participants[model.RoleArtist][0] + Expect(artist0.ID).ToNot(BeEmpty()) + Expect(artist0.Name).To(Equal("First Artist")) + Expect(artist0.OrderArtistName).To(Equal("first artist")) + Expect(artist0.SortArtistName).To(Equal("Name, First Artist")) + Expect(artist0.MbzArtistID).To(BeEmpty()) + + artist1 := participants[model.RoleArtist][1] + Expect(artist1.ID).ToNot(BeEmpty()) + Expect(artist1.Name).To(Equal("Second Artist")) + Expect(artist1.OrderArtistName).To(Equal("second artist")) + Expect(artist1.SortArtistName).To(Equal("Name, Second Artist")) + Expect(artist1.MbzArtistID).To(BeEmpty()) + }) + }) + Context("Single-valued ARTIST tags, multi-valued ARTISTS tags", func() { BeforeEach(func() { mf = toMediaFile(model.RawTags{ @@ -231,6 +333,7 @@ var _ = Describe("Participants", func() { }) }) + // Not a good tagging strategy, but supported anyway. Context("Multi-valued ARTIST tags, multi-valued ARTISTS tags", func() { BeforeEach(func() { mf = toMediaFile(model.RawTags{ @@ -242,13 +345,8 @@ var _ = Describe("Participants", func() { }) }) - XIt("should use the values concatenated as a display name ", func() { - Expect(mf.Artist).To(Equal("First Artist + Second Artist")) - }) - - // TODO: remove when the above is implemented - It("should use the first artist name as display name", func() { - Expect(mf.Artist).To(Equal("First Artist 2")) + It("should use ARTIST values concatenated as a display name ", func() { + Expect(mf.Artist).To(Equal("First Artist • Second Artist")) }) It("should prioritize ARTISTS tags", func() { @@ -275,6 +373,7 @@ var _ = Describe("Participants", func() { }) Describe("ALBUMARTIST(S) tags", func() { + // Only test specific scenarios for ALBUMARTIST(S) tags, as the logic is the same as for ARTIST(S) tags. Context("No ALBUMARTIST/ALBUMARTISTS tags", func() { When("the COMPILATION tag is not set", func() { BeforeEach(func() { @@ -305,6 +404,35 @@ var _ = Describe("Participants", func() { }) }) + When("the COMPILATION tag is not set and there is no ALBUMARTIST tag", func() { + BeforeEach(func() { + mf = toMediaFile(model.RawTags{ + "ARTIST": {"Artist Name", "Another Artist"}, + "ARTISTSORT": {"Name, Artist", "Artist, Another"}, + }) + }) + + It("should use the first ARTIST as ALBUMARTIST", func() { + Expect(mf.AlbumArtist).To(Equal("Artist Name")) + }) + + It("should add the ARTIST to participants as ALBUMARTIST", func() { + participants := mf.Participants + Expect(participants).To(HaveLen(2)) + Expect(participants).To(SatisfyAll( + HaveKeyWithValue(model.RoleAlbumArtist, HaveLen(2)), + )) + + albumArtist := participants[model.RoleAlbumArtist][0] + Expect(albumArtist.Name).To(Equal("Artist Name")) + Expect(albumArtist.SortArtistName).To(Equal("Name, Artist")) + + albumArtist = participants[model.RoleAlbumArtist][1] + Expect(albumArtist.Name).To(Equal("Another Artist")) + Expect(albumArtist.SortArtistName).To(Equal("Artist, Another")) + }) + }) + When("the COMPILATION tag is true", func() { BeforeEach(func() { mf = toMediaFile(model.RawTags{ @@ -331,6 +459,19 @@ var _ = Describe("Participants", func() { Expect(albumArtist.MbzArtistID).To(Equal(consts.VariousArtistsMbzId)) }) }) + + When("the COMPILATION tag is true and there are ALBUMARTIST tags", func() { + BeforeEach(func() { + mf = toMediaFile(model.RawTags{ + "COMPILATION": {"1"}, + "ALBUMARTIST": {"Album Artist Name 1", "Album Artist Name 2"}, + }) + }) + + It("should use the ALBUMARTIST names as display name", func() { + Expect(mf.AlbumArtist).To(Equal("Album Artist Name 1 • Album Artist Name 2")) + }) + }) }) Context("ALBUMARTIST tag is set", func() { diff --git a/server/subsonic/helpers.go b/server/subsonic/helpers.go index 0fb35a7b7..fa98a985b 100644 --- a/server/subsonic/helpers.go +++ b/server/subsonic/helpers.go @@ -241,7 +241,7 @@ func osChildFromMediaFile(ctx context.Context, mf model.MediaFile) *responses.Op child.DisplayAlbumArtist = mf.AlbumArtist child.AlbumArtists = artistRefs(mf.Participants[model.RoleAlbumArtist]) var contributors []responses.Contributor - child.DisplayComposer = mf.Participants[model.RoleComposer].Join(" • ") + child.DisplayComposer = mf.Participants[model.RoleComposer].Join(consts.ArtistJoiner) for role, participants := range mf.Participants { if role == model.RoleArtist || role == model.RoleAlbumArtist { continue