From 23bebe4e06124becf1000e88472ae71a6ca7de4c Mon Sep 17 00:00:00 2001 From: Deluan Date: Mon, 21 Oct 2024 16:31:56 -0400 Subject: [PATCH 01/22] feat(subsonic): getOpenSubsonicExtensions is now public --- server/subsonic/api.go | 261 ++++++++++++++------------- server/subsonic/opensubsonic_test.go | 44 +++++ 2 files changed, 177 insertions(+), 128 deletions(-) create mode 100644 server/subsonic/opensubsonic_test.go diff --git a/server/subsonic/api.go b/server/subsonic/api.go index 405646b62..dfa945086 100644 --- a/server/subsonic/api.go +++ b/server/subsonic/api.go @@ -68,141 +68,146 @@ func New(ds model.DataStore, artwork artwork.Artwork, streamer core.MediaStreame func (api *Router) routes() http.Handler { r := chi.NewRouter() - r.Use(postFormToQueryParams) - r.Use(checkRequiredParameters) - r.Use(authenticate(api.ds)) - r.Use(server.UpdateLastAccessMiddleware(api.ds)) - // TODO Validate API version? - // Subsonic endpoints, grouped by controller + // Public + h(r, "getOpenSubsonicExtensions", api.GetOpenSubsonicExtensions) + + // Protected r.Group(func(r chi.Router) { - r.Use(getPlayer(api.players)) - h(r, "ping", api.Ping) - h(r, "getLicense", api.GetLicense) - }) - r.Group(func(r chi.Router) { - r.Use(getPlayer(api.players)) - h(r, "getMusicFolders", api.GetMusicFolders) - h(r, "getIndexes", api.GetIndexes) - h(r, "getArtists", api.GetArtists) - h(r, "getGenres", api.GetGenres) - h(r, "getMusicDirectory", api.GetMusicDirectory) - h(r, "getArtist", api.GetArtist) - h(r, "getAlbum", api.GetAlbum) - h(r, "getSong", api.GetSong) - h(r, "getAlbumInfo", api.GetAlbumInfo) - h(r, "getAlbumInfo2", api.GetAlbumInfo) - h(r, "getArtistInfo", api.GetArtistInfo) - h(r, "getArtistInfo2", api.GetArtistInfo2) - h(r, "getTopSongs", api.GetTopSongs) - h(r, "getSimilarSongs", api.GetSimilarSongs) - h(r, "getSimilarSongs2", api.GetSimilarSongs2) - }) - r.Group(func(r chi.Router) { - r.Use(getPlayer(api.players)) - hr(r, "getAlbumList", api.GetAlbumList) - hr(r, "getAlbumList2", api.GetAlbumList2) - h(r, "getStarred", api.GetStarred) - h(r, "getStarred2", api.GetStarred2) - h(r, "getNowPlaying", api.GetNowPlaying) - h(r, "getRandomSongs", api.GetRandomSongs) - h(r, "getSongsByGenre", api.GetSongsByGenre) - }) - r.Group(func(r chi.Router) { - r.Use(getPlayer(api.players)) - h(r, "setRating", api.SetRating) - h(r, "star", api.Star) - h(r, "unstar", api.Unstar) - h(r, "scrobble", api.Scrobble) - }) - r.Group(func(r chi.Router) { - r.Use(getPlayer(api.players)) - h(r, "getPlaylists", api.GetPlaylists) - h(r, "getPlaylist", api.GetPlaylist) - h(r, "createPlaylist", api.CreatePlaylist) - h(r, "deletePlaylist", api.DeletePlaylist) - h(r, "updatePlaylist", api.UpdatePlaylist) - }) - r.Group(func(r chi.Router) { - r.Use(getPlayer(api.players)) - h(r, "getBookmarks", api.GetBookmarks) - h(r, "createBookmark", api.CreateBookmark) - h(r, "deleteBookmark", api.DeleteBookmark) - h(r, "getPlayQueue", api.GetPlayQueue) - h(r, "savePlayQueue", api.SavePlayQueue) - }) - r.Group(func(r chi.Router) { - r.Use(getPlayer(api.players)) - h(r, "search2", api.Search2) - h(r, "search3", api.Search3) - }) - r.Group(func(r chi.Router) { - h(r, "getUser", api.GetUser) - h(r, "getUsers", api.GetUsers) - }) - r.Group(func(r chi.Router) { - h(r, "getScanStatus", api.GetScanStatus) - h(r, "startScan", api.StartScan) - }) - r.Group(func(r chi.Router) { - hr(r, "getAvatar", api.GetAvatar) - h(r, "getLyrics", api.GetLyrics) - h(r, "getLyricsBySongId", api.GetLyricsBySongId) - }) - r.Group(func(r chi.Router) { - // configure request throttling - if conf.Server.DevArtworkMaxRequests > 0 { - log.Debug("Throttling Subsonic getCoverArt endpoint", "maxRequests", conf.Server.DevArtworkMaxRequests, - "backlogLimit", conf.Server.DevArtworkThrottleBacklogLimit, "backlogTimeout", - conf.Server.DevArtworkThrottleBacklogTimeout) - r.Use(middleware.ThrottleBacklog(conf.Server.DevArtworkMaxRequests, conf.Server.DevArtworkThrottleBacklogLimit, - conf.Server.DevArtworkThrottleBacklogTimeout)) + r.Use(checkRequiredParameters) + r.Use(authenticate(api.ds)) + r.Use(server.UpdateLastAccessMiddleware(api.ds)) + + // Subsonic endpoints, grouped by controller + r.Group(func(r chi.Router) { + r.Use(getPlayer(api.players)) + h(r, "ping", api.Ping) + h(r, "getLicense", api.GetLicense) + }) + r.Group(func(r chi.Router) { + r.Use(getPlayer(api.players)) + h(r, "getMusicFolders", api.GetMusicFolders) + h(r, "getIndexes", api.GetIndexes) + h(r, "getArtists", api.GetArtists) + h(r, "getGenres", api.GetGenres) + h(r, "getMusicDirectory", api.GetMusicDirectory) + h(r, "getArtist", api.GetArtist) + h(r, "getAlbum", api.GetAlbum) + h(r, "getSong", api.GetSong) + h(r, "getAlbumInfo", api.GetAlbumInfo) + h(r, "getAlbumInfo2", api.GetAlbumInfo) + h(r, "getArtistInfo", api.GetArtistInfo) + h(r, "getArtistInfo2", api.GetArtistInfo2) + h(r, "getTopSongs", api.GetTopSongs) + h(r, "getSimilarSongs", api.GetSimilarSongs) + h(r, "getSimilarSongs2", api.GetSimilarSongs2) + }) + r.Group(func(r chi.Router) { + r.Use(getPlayer(api.players)) + hr(r, "getAlbumList", api.GetAlbumList) + hr(r, "getAlbumList2", api.GetAlbumList2) + h(r, "getStarred", api.GetStarred) + h(r, "getStarred2", api.GetStarred2) + h(r, "getNowPlaying", api.GetNowPlaying) + h(r, "getRandomSongs", api.GetRandomSongs) + h(r, "getSongsByGenre", api.GetSongsByGenre) + }) + r.Group(func(r chi.Router) { + r.Use(getPlayer(api.players)) + h(r, "setRating", api.SetRating) + h(r, "star", api.Star) + h(r, "unstar", api.Unstar) + h(r, "scrobble", api.Scrobble) + }) + r.Group(func(r chi.Router) { + r.Use(getPlayer(api.players)) + h(r, "getPlaylists", api.GetPlaylists) + h(r, "getPlaylist", api.GetPlaylist) + h(r, "createPlaylist", api.CreatePlaylist) + h(r, "deletePlaylist", api.DeletePlaylist) + h(r, "updatePlaylist", api.UpdatePlaylist) + }) + r.Group(func(r chi.Router) { + r.Use(getPlayer(api.players)) + h(r, "getBookmarks", api.GetBookmarks) + h(r, "createBookmark", api.CreateBookmark) + h(r, "deleteBookmark", api.DeleteBookmark) + h(r, "getPlayQueue", api.GetPlayQueue) + h(r, "savePlayQueue", api.SavePlayQueue) + }) + r.Group(func(r chi.Router) { + r.Use(getPlayer(api.players)) + h(r, "search2", api.Search2) + h(r, "search3", api.Search3) + }) + r.Group(func(r chi.Router) { + r.Use(getPlayer(api.players)) + h(r, "getUser", api.GetUser) + h(r, "getUsers", api.GetUsers) + }) + r.Group(func(r chi.Router) { + r.Use(getPlayer(api.players)) + h(r, "getScanStatus", api.GetScanStatus) + h(r, "startScan", api.StartScan) + }) + r.Group(func(r chi.Router) { + r.Use(getPlayer(api.players)) + hr(r, "getAvatar", api.GetAvatar) + h(r, "getLyrics", api.GetLyrics) + h(r, "getLyricsBySongId", api.GetLyricsBySongId) + hr(r, "stream", api.Stream) + hr(r, "download", api.Download) + }) + r.Group(func(r chi.Router) { + // configure request throttling + if conf.Server.DevArtworkMaxRequests > 0 { + log.Debug("Throttling Subsonic getCoverArt endpoint", "maxRequests", conf.Server.DevArtworkMaxRequests, + "backlogLimit", conf.Server.DevArtworkThrottleBacklogLimit, "backlogTimeout", + conf.Server.DevArtworkThrottleBacklogTimeout) + r.Use(middleware.ThrottleBacklog(conf.Server.DevArtworkMaxRequests, conf.Server.DevArtworkThrottleBacklogLimit, + conf.Server.DevArtworkThrottleBacklogTimeout)) + } + hr(r, "getCoverArt", api.GetCoverArt) + }) + r.Group(func(r chi.Router) { + r.Use(getPlayer(api.players)) + h(r, "createInternetRadioStation", api.CreateInternetRadio) + h(r, "deleteInternetRadioStation", api.DeleteInternetRadio) + h(r, "getInternetRadioStations", api.GetInternetRadios) + h(r, "updateInternetRadioStation", api.UpdateInternetRadio) + }) + if conf.Server.EnableSharing { + r.Group(func(r chi.Router) { + r.Use(getPlayer(api.players)) + h(r, "getShares", api.GetShares) + h(r, "createShare", api.CreateShare) + h(r, "updateShare", api.UpdateShare) + h(r, "deleteShare", api.DeleteShare) + }) + } else { + h501(r, "getShares", "createShare", "updateShare", "deleteShare") } - hr(r, "getCoverArt", api.GetCoverArt) - }) - r.Group(func(r chi.Router) { - r.Use(getPlayer(api.players)) - hr(r, "stream", api.Stream) - hr(r, "download", api.Download) - }) - r.Group(func(r chi.Router) { - h(r, "createInternetRadioStation", api.CreateInternetRadio) - h(r, "deleteInternetRadioStation", api.DeleteInternetRadio) - h(r, "getInternetRadioStations", api.GetInternetRadios) - h(r, "updateInternetRadioStation", api.UpdateInternetRadio) - }) - if conf.Server.EnableSharing { - r.Group(func(r chi.Router) { - h(r, "getShares", api.GetShares) - h(r, "createShare", api.CreateShare) - h(r, "updateShare", api.UpdateShare) - h(r, "deleteShare", api.DeleteShare) - }) - } else { - h501(r, "getShares", "createShare", "updateShare", "deleteShare") - } - r.Group(func(r chi.Router) { - h(r, "getOpenSubsonicExtensions", api.GetOpenSubsonicExtensions) - }) - if conf.Server.Jukebox.Enabled { - r.Group(func(r chi.Router) { - h(r, "jukeboxControl", api.JukeboxControl) - }) - } else { - h501(r, "jukeboxControl") - } + if conf.Server.Jukebox.Enabled { + r.Group(func(r chi.Router) { + r.Use(getPlayer(api.players)) + h(r, "jukeboxControl", api.JukeboxControl) + }) + } else { + h501(r, "jukeboxControl") + } - // Not Implemented (yet?) - h501(r, "getPodcasts", "getNewestPodcasts", "refreshPodcasts", "createPodcastChannel", "deletePodcastChannel", - "deletePodcastEpisode", "downloadPodcastEpisode") - h501(r, "createUser", "updateUser", "deleteUser", "changePassword") + // Not Implemented (yet?) + h501(r, "getPodcasts", "getNewestPodcasts", "refreshPodcasts", "createPodcastChannel", "deletePodcastChannel", + "deletePodcastEpisode", "downloadPodcastEpisode") + h501(r, "createUser", "updateUser", "deleteUser", "changePassword") - // Deprecated/Won't implement/Out of scope endpoints - h410(r, "search") - h410(r, "getChatMessages", "addChatMessage") - h410(r, "getVideos", "getVideoInfo", "getCaptions", "hls") + // Deprecated/Won't implement/Out of scope endpoints + h410(r, "search") + h410(r, "getChatMessages", "addChatMessage") + h410(r, "getVideos", "getVideoInfo", "getCaptions", "hls") + }) return r } diff --git a/server/subsonic/opensubsonic_test.go b/server/subsonic/opensubsonic_test.go new file mode 100644 index 000000000..d92ea4c67 --- /dev/null +++ b/server/subsonic/opensubsonic_test.go @@ -0,0 +1,44 @@ +package subsonic_test + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + + "github.com/navidrome/navidrome/server/subsonic" + "github.com/navidrome/navidrome/server/subsonic/responses" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("GetOpenSubsonicExtensions", func() { + var ( + router *subsonic.Router + w *httptest.ResponseRecorder + r *http.Request + ) + + BeforeEach(func() { + router = subsonic.New(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) + w = httptest.NewRecorder() + r = httptest.NewRequest("GET", "/getOpenSubsonicExtensions?f=json", nil) + }) + + It("should return the correct OpenSubsonicExtensions", func() { + router.ServeHTTP(w, r) + + // Make sure the endpoint is public, by not passing any authentication + Expect(w.Code).To(Equal(http.StatusOK)) + Expect(w.Header().Get("Content-Type")).To(Equal("application/json")) + + var response responses.JsonWrapper + err := json.Unmarshal(w.Body.Bytes(), &response) + Expect(err).NotTo(HaveOccurred()) + Expect(*response.Subsonic.OpenSubsonicExtensions).To(SatisfyAll( + HaveLen(3), + ContainElement(responses.OpenSubsonicExtension{Name: "transcodeOffset", Versions: []int32{1}}), + ContainElement(responses.OpenSubsonicExtension{Name: "formPost", Versions: []int32{1}}), + ContainElement(responses.OpenSubsonicExtension{Name: "songLyrics", Versions: []int32{1}}), + )) + }) +}) From 9c3b4561652a15846993d477003e111f0df0c585 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Tue, 22 Oct 2024 23:32:56 +0000 Subject: [PATCH 02/22] feat(build): MSI installer improvements (#3376) * feat(build): add a make target to build a msi installer locally * Testing wrapping the executable in cmd * build(ci): build msis in parallel * feat(server): add LogFile config option * Revert "Testing wrapping the executable in cmd" This reverts commit be29592254cb903fd4904f2f50d4d1a860795d33. * Adding --log-file for service executable * feat(ini): wip * feat(ini): parse nested ini section * fix(conf): fix fatal error messages * Now navidrome supports INI, we can use the built-in msi ini system and not require the VBScript to convert it into toml * File needs to be called .ini to be parsed as an INI and correct filename needs to be passed to the service * fix(msi): build msi locally * fix(msi): pipeline * fix(msi): pipeline * fix(msi): pipeline * fix(msi): pipeline * fix(msi): pipeline * fix(msi): Makefile * fix(msi): more clean up * fix(log): convert LF to CRLF on Windows * fix(msi): config filename should be case-insensitive * fix(msi): make it a little more idiomatic * Including the latest windows release of ffmpeg into the msi as built by https://www.gyan.dev/ffmpeg/builds/ (linked to on the official ffmpeg source) * This should version independent * Need bash expansion for the * to work * This will run twice, once for x86 and once for x64, I'll make it cache the executable for now as it'll be quicker * Silencing wget * Add ffmpeg path to the config so Navidrome knows where to find it * refactor: download ffmpeg from our repository * When going back from the "Are you ready to install?" it should go back to the Settings dialogue that you just came from * fix: comments --------- Co-authored-by: Deluan --- .dockerignore | 3 +- .github/workflows/pipeline.yml | 45 +++++----------- Makefile | 16 +++++- cmd/root.go | 2 + cmd/svc.go | 13 ++++- conf/configuration.go | 50 +++++++++++++++-- log/formatters.go | 27 ++++++++++ log/formatters_test.go | 38 ++++++++++++- log/log.go | 8 +++ {wix => release/wix}/Navidrome_UI_Flow.wxs | 2 +- {wix => release/wix}/SettingsDlg.wxs | 0 {wix => release/wix}/bmp/banner.bmp | Bin {wix => release/wix}/bmp/dialogue.bmp | Bin release/wix/build_msi.sh | 60 +++++++++++++++++++++ release/wix/msitools.dockerfile | 3 ++ {wix => release/wix}/navidrome.wxs | 27 ++++------ wix/convertIniToToml.vbs | 17 ------ 17 files changed, 233 insertions(+), 78 deletions(-) rename {wix => release/wix}/Navidrome_UI_Flow.wxs (97%) rename {wix => release/wix}/SettingsDlg.wxs (100%) rename {wix => release/wix}/bmp/banner.bmp (100%) rename {wix => release/wix}/bmp/dialogue.bmp (100%) create mode 100755 release/wix/build_msi.sh create mode 100644 release/wix/msitools.dockerfile rename {wix => release/wix}/navidrome.wxs (70%) delete mode 100644 wix/convertIniToToml.vbs diff --git a/.dockerignore b/.dockerignore index 054044784..b53d7842a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -11,6 +11,7 @@ navidrome navidrome.toml tmp !tmp/taglib -dist/* +dist +binaries cache music \ No newline at end of file diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 693fc6441..af936dac0 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -217,7 +217,6 @@ jobs: path: ./output retention-days: 7 - # https://www.perplexity.ai/search/can-i-have-multiple-push-to-di-4P3ToaZFQtmVROuhaZMllQ - name: Build and push image by digest id: push-image if: env.IS_LINUX == 'true' && env.IS_DOCKER_PUSH_CONFIGURED == 'true' && env.IS_ARMV5 == 'false' @@ -307,14 +306,11 @@ jobs: gh api --method DELETE repos/${{ github.repository }}/actions/artifacts/$artifact done - msi: - name: Build Windows Installers + name: Build Windows installers needs: [build, git-version] runs-on: ubuntu-24.04 - env: - GIT_SHA: ${{ needs.git-version.outputs.git_sha }} - GIT_TAG: ${{ needs.git-version.outputs.git_tag }} + steps: - uses: actions/checkout@v4 @@ -324,39 +320,24 @@ jobs: pattern: navidrome-windows* merge-multiple: true - - name: Build MSI files + - name: Install Wix + run: sudo apt-get install -y wixl jq + + - name: Build MSI + env: + GIT_TAG: ${{ needs.git-version.outputs.git_tag }} run: | - sudo apt-get install -y wixl jq + rm -rf binaries/msi + sudo GIT_TAG=$GIT_TAG release/wix/build_msi.sh ${GITHUB_WORKSPACE} 386 + sudo GIT_TAG=$GIT_TAG release/wix/build_msi.sh ${GITHUB_WORKSPACE} amd64 + du -h binaries/msi/*.msi - NAVIDROME_BUILD_VERSION=$(echo $GIT_TAG | sed -e 's/^v//' -e 's/-SNAPSHOT/.1/') - echo $NAVIDROME_BUILD_VERSION - - mkdir -p $GITHUB_WORKSPACE/wix/386 - cp $GITHUB_WORKSPACE/LICENSE $GITHUB_WORKSPACE/wix/386 - cp $GITHUB_WORKSPACE/README.md $GITHUB_WORKSPACE/wix/386 - - cp -r $GITHUB_WORKSPACE/wix/386 $GITHUB_WORKSPACE/wix/amd64 - - cp $GITHUB_WORKSPACE/binaries/windows_386/navidrome.exe $GITHUB_WORKSPACE/wix/386 - cp $GITHUB_WORKSPACE/binaries/windows_amd64/navidrome.exe $GITHUB_WORKSPACE/wix/amd64 - - # workaround for wixl WixVariable not working to override bmp locations - sudo cp $GITHUB_WORKSPACE/wix/bmp/banner.bmp /usr/share/wixl-*/ext/ui/bitmaps/bannrbmp.bmp - sudo cp $GITHUB_WORKSPACE/wix/bmp/dialogue.bmp /usr/share/wixl-*/ext/ui/bitmaps/dlgbmp.bmp - - cd $GITHUB_WORKSPACE/wix/386 - wixl ../navidrome.wxs -D Version=$NAVIDROME_BUILD_VERSION -D Platform=x86 --arch x86 --ext ui --output ../navidrome_386.msi - - cd $GITHUB_WORKSPACE/wix/amd64 - wixl ../navidrome.wxs -D Version=$NAVIDROME_BUILD_VERSION -D Platform=x64 --arch x64 --ext ui --output ../navidrome_amd64.msi - - ls -la $GITHUB_WORKSPACE/wix/*.msi - name: Upload MSI files uses: actions/upload-artifact@v4 with: name: navidrome-windows-installers - path: wix/*.msi + path: binaries/msi/*.msi retention-days: 7 release: diff --git a/Makefile b/Makefile index 46af0edb1..233113d6f 100644 --- a/Makefile +++ b/Makefile @@ -120,7 +120,7 @@ docker-build: ##@Cross_Compilation Cross-compile for any supported platform (che --build-arg GIT_TAG=${GIT_TAG} \ --build-arg GIT_SHA=${GIT_SHA} \ --build-arg CROSS_TAGLIB_VERSION=${CROSS_TAGLIB_VERSION} \ - --output "./dist" --target binary . + --output "./binaries" --target binary . .PHONY: docker-build docker-image: ##@Cross_Compilation Build Docker image, tagged as `deluan/navidrome:develop`, override with DOCKER_TAG var. Use IMAGE_PLATFORMS to specify target platforms @@ -135,6 +135,15 @@ docker-image: ##@Cross_Compilation Build Docker image, tagged as `deluan/navidro --tag $(DOCKER_TAG) . .PHONY: docker-image +docker-msi: ##@Cross_Compilation Build MSI installer for Windows + make docker-build PLATFORMS=windows/386,windows/amd64 + DOCKER_CLI_HINTS=false docker build -q -t navidrome-msi-builder -f release/wix/msitools.dockerfile . + @rm -rf binaries/msi + docker run -it --rm -v $(PWD):/workspace -v $(PWD)/binaries:/workspace/binaries -e GIT_TAG=${GIT_TAG} \ + navidrome-msi-builder sh -c "release/wix/build_msi.sh /workspace 386 && release/wix/build_msi.sh /workspace amd64" + @du -h binaries/msi/*.msi +.PHONY: docker-msi + get-music: ##@Development Download some free music from Navidrome's demo instance mkdir -p music ( cd music; \ @@ -150,6 +159,11 @@ get-music: ##@Development Download some free music from Navidrome's demo instanc ########################################## #### Miscellaneous +clean: + @rm -rf ./binaries ./dist ./ui/build/* + @touch ./ui/build/.gitkeep +.PHONY: clean + release: @if [[ ! "${V}" =~ ^[0-9]+\.[0-9]+\.[0-9]+.*$$ ]]; then echo "Usage: make release V=X.X.X"; exit 1; fi go mod tidy diff --git a/cmd/root.go b/cmd/root.go index b821669c2..9cffec2fd 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -226,11 +226,13 @@ func init() { rootCmd.PersistentFlags().String("datafolder", viper.GetString("datafolder"), "folder to store application data (DB), needs write access") rootCmd.PersistentFlags().String("cachefolder", viper.GetString("cachefolder"), "folder to store cache data (transcoding, images...), needs write access") rootCmd.PersistentFlags().StringP("loglevel", "l", viper.GetString("loglevel"), "log level, possible values: error, info, debug, trace") + rootCmd.PersistentFlags().String("logfile", viper.GetString("logfile"), "log file path, if not set logs will be printed to stderr") _ = viper.BindPFlag("musicfolder", rootCmd.PersistentFlags().Lookup("musicfolder")) _ = viper.BindPFlag("datafolder", rootCmd.PersistentFlags().Lookup("datafolder")) _ = viper.BindPFlag("cachefolder", rootCmd.PersistentFlags().Lookup("cachefolder")) _ = viper.BindPFlag("loglevel", rootCmd.PersistentFlags().Lookup("loglevel")) + _ = viper.BindPFlag("logfile", rootCmd.PersistentFlags().Lookup("logfile")) rootCmd.Flags().StringP("address", "a", viper.GetString("address"), "IP address to bind to") rootCmd.Flags().IntP("port", "p", viper.GetInt("port"), "HTTP port Navidrome will listen to") diff --git a/cmd/svc.go b/cmd/svc.go index 21c9b64cc..219339515 100644 --- a/cmd/svc.go +++ b/cmd/svc.go @@ -73,7 +73,12 @@ var svcInstance = sync.OnceValue(func() service.Service { options["Restart"] = "on-success" options["SuccessExitStatus"] = "1 2 8 SIGKILL" options["UserService"] = false - options["LogDirectory"] = conf.Server.DataFolder + if conf.Server.LogFile != "" { + options["LogOutput"] = false + } else { + options["LogOutput"] = true + options["LogDirectory"] = conf.Server.DataFolder + } svcConfig := &service.Config{ Name: "navidrome", DisplayName: "Navidrome", @@ -117,7 +122,11 @@ func buildInstallCmd() *cobra.Command { println(" working directory: " + executablePath()) println(" music folder: " + conf.Server.MusicFolder) println(" data folder: " + conf.Server.DataFolder) - println(" logs folder: " + conf.Server.DataFolder) + if conf.Server.LogFile != "" { + println(" log file: " + conf.Server.LogFile) + } else { + println(" logs folder: " + conf.Server.DataFolder) + } if cfgFile != "" { conf.Server.ConfigFile, err = filepath.Abs(cfgFile) if err != nil { diff --git a/conf/configuration.go b/conf/configuration.go index e582ad114..e9464af41 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -26,6 +26,7 @@ type configOptions struct { CacheFolder string DbPath string LogLevel string + LogFile string ScanInterval time.Duration ScanSchedule string SessionTimeout time.Duration @@ -176,14 +177,17 @@ func LoadFromFile(confFile string) { } func Load() { + parseIniFileConfiguration() + err := viper.Unmarshal(&Server) if err != nil { _, _ = fmt.Fprintln(os.Stderr, "FATAL: Error parsing config:", err) os.Exit(1) } + err = os.MkdirAll(Server.DataFolder, os.ModePerm) if err != nil { - _, _ = fmt.Fprintln(os.Stderr, "FATAL: Error creating data path:", "path", Server.DataFolder, err) + _, _ = fmt.Fprintln(os.Stderr, "FATAL: Error creating data path:", err) os.Exit(1) } @@ -192,7 +196,7 @@ func Load() { } err = os.MkdirAll(Server.CacheFolder, os.ModePerm) if err != nil { - _, _ = fmt.Fprintln(os.Stderr, "FATAL: Error creating cache path:", "path", Server.CacheFolder, err) + _, _ = fmt.Fprintln(os.Stderr, "FATAL: Error creating cache path:", err) os.Exit(1) } @@ -204,11 +208,21 @@ func Load() { if Server.Backup.Path != "" { err = os.MkdirAll(Server.Backup.Path, os.ModePerm) if err != nil { - _, _ = fmt.Fprintln(os.Stderr, "FATAL: Error creating backup path:", "path", Server.Backup.Path, err) + _, _ = fmt.Fprintln(os.Stderr, "FATAL: Error creating backup path:", err) os.Exit(1) } } + out := os.Stderr + if Server.LogFile != "" { + out, err = os.OpenFile(Server.LogFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "FATAL: Error opening log file %s: %s\n", Server.LogFile, err.Error()) + os.Exit(1) + } + log.SetOutput(out) + } + log.SetLevelString(Server.LogLevel) log.SetLogLevels(Server.DevLogLevels) log.SetLogSourceLine(Server.DevLogSourceLine) @@ -225,7 +239,7 @@ func Load() { if Server.BaseURL != "" { u, err := url.Parse(Server.BaseURL) if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "FATAL: Invalid BaseURL %s: %s\n", Server.BaseURL, err.Error()) + _, _ = fmt.Fprintln(os.Stderr, "FATAL: Invalid BaseURL:", err) os.Exit(1) } Server.BasePath = u.Path @@ -241,7 +255,7 @@ func Load() { if Server.EnableLogRedacting { prettyConf = log.Redact(prettyConf) } - _, _ = fmt.Fprintln(os.Stderr, prettyConf) + _, _ = fmt.Fprintln(out, prettyConf) } if !Server.EnableExternalServices { @@ -254,6 +268,31 @@ func Load() { } } +// parseIniFileConfiguration is used to parse the config file when it is in INI format. For INI files, it +// would require a nested structure, so instead we unmarshal it to a map and then merge the nested [default] +// section into the root level. +func parseIniFileConfiguration() { + cfgFile := viper.ConfigFileUsed() + if strings.ToLower(filepath.Ext(cfgFile)) == ".ini" { + var iniConfig map[string]interface{} + err := viper.Unmarshal(&iniConfig) + if err != nil { + _, _ = fmt.Fprintln(os.Stderr, "FATAL: Error parsing config:", err) + os.Exit(1) + } + cfg, ok := iniConfig["default"].(map[string]any) + if !ok { + _, _ = fmt.Fprintln(os.Stderr, "FATAL: Error parsing config: missing [default] section:", iniConfig) + os.Exit(1) + } + err = viper.MergeConfigMap(cfg) + if err != nil { + _, _ = fmt.Fprintln(os.Stderr, "FATAL: Error parsing config:", err) + os.Exit(1) + } + } +} + func disableExternalServices() { log.Info("All external integrations are DISABLED!") Server.LastFM.Enabled = false @@ -324,6 +363,7 @@ func init() { viper.SetDefault("cachefolder", "") viper.SetDefault("datafolder", ".") viper.SetDefault("loglevel", "info") + viper.SetDefault("logfile", "") viper.SetDefault("address", "0.0.0.0") viper.SetDefault("port", 4533) viper.SetDefault("unixsocketperm", "0660") diff --git a/log/formatters.go b/log/formatters.go index 5cc1ca410..c42282183 100644 --- a/log/formatters.go +++ b/log/formatters.go @@ -1,6 +1,7 @@ package log import ( + "io" "strings" "time" ) @@ -22,3 +23,29 @@ func ShortDur(d time.Duration) string { s = strings.TrimSuffix(s, "0s") return strings.TrimSuffix(s, "0m") } + +func CRLFWriter(w io.Writer) io.Writer { + return &crlfWriter{w: w} +} + +type crlfWriter struct { + w io.Writer + lastByte byte +} + +func (cw *crlfWriter) Write(p []byte) (int, error) { + var written int + for _, b := range p { + if b == '\n' && cw.lastByte != '\r' { + if _, err := cw.w.Write([]byte{'\r'}); err != nil { + return written, err + } + } + if _, err := cw.w.Write([]byte{b}); err != nil { + return written, err + } + written++ + cw.lastByte = b + } + return written, nil +} diff --git a/log/formatters_test.go b/log/formatters_test.go index 087459b5c..0a700288a 100644 --- a/log/formatters_test.go +++ b/log/formatters_test.go @@ -1,15 +1,18 @@ -package log +package log_test import ( + "bytes" + "io" "time" + "github.com/navidrome/navidrome/log" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = DescribeTable("ShortDur", func(d time.Duration, expected string) { - Expect(ShortDur(d)).To(Equal(expected)) + Expect(log.ShortDur(d)).To(Equal(expected)) }, Entry("1ns", 1*time.Nanosecond, "1ns"), Entry("9µs", 9*time.Microsecond, "9µs"), @@ -24,3 +27,34 @@ var _ = DescribeTable("ShortDur", Entry("4h", 4*time.Hour+2*time.Second, "4h"), Entry("4h2m", 4*time.Hour+2*time.Minute+5*time.Second+200*time.Millisecond, "4h2m"), ) + +var _ = Describe("CRLFWriter", func() { + var ( + buffer *bytes.Buffer + writer io.Writer + ) + + BeforeEach(func() { + buffer = new(bytes.Buffer) + writer = log.CRLFWriter(buffer) + }) + + Describe("Write", func() { + It("should convert all LFs to CRLFs", func() { + n, err := writer.Write([]byte("hello\nworld\nagain\n")) + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(Equal(18)) + Expect(buffer.String()).To(Equal("hello\r\nworld\r\nagain\r\n")) + }) + + It("should not convert LF to CRLF if preceded by CR", func() { + n, err := writer.Write([]byte("hello\r")) + Expect(n).To(Equal(6)) + Expect(err).NotTo(HaveOccurred()) + n, err = writer.Write([]byte("\nworld\n")) + Expect(n).To(Equal(7)) + Expect(err).NotTo(HaveOccurred()) + Expect(buffer.String()).To(Equal("hello\r\nworld\r\n")) + }) + }) +}) diff --git a/log/log.go b/log/log.go index fdb295957..c990a5614 100644 --- a/log/log.go +++ b/log/log.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "io" "net/http" "os" "reflect" @@ -128,6 +129,13 @@ func SetRedacting(enabled bool) { } } +func SetOutput(w io.Writer) { + if runtime.GOOS == "windows" { + w = CRLFWriter(w) + } + defaultLogger.SetOutput(w) +} + // Redact applies redaction to a single string func Redact(msg string) string { r, _ := redacted.redact(msg) diff --git a/wix/Navidrome_UI_Flow.wxs b/release/wix/Navidrome_UI_Flow.wxs similarity index 97% rename from wix/Navidrome_UI_Flow.wxs rename to release/wix/Navidrome_UI_Flow.wxs index 2ea38e172..59c2f5184 100644 --- a/wix/Navidrome_UI_Flow.wxs +++ b/release/wix/Navidrome_UI_Flow.wxs @@ -19,7 +19,7 @@ - + diff --git a/wix/SettingsDlg.wxs b/release/wix/SettingsDlg.wxs similarity index 100% rename from wix/SettingsDlg.wxs rename to release/wix/SettingsDlg.wxs diff --git a/wix/bmp/banner.bmp b/release/wix/bmp/banner.bmp similarity index 100% rename from wix/bmp/banner.bmp rename to release/wix/bmp/banner.bmp diff --git a/wix/bmp/dialogue.bmp b/release/wix/bmp/dialogue.bmp similarity index 100% rename from wix/bmp/dialogue.bmp rename to release/wix/bmp/dialogue.bmp diff --git a/release/wix/build_msi.sh b/release/wix/build_msi.sh new file mode 100755 index 000000000..9fc008446 --- /dev/null +++ b/release/wix/build_msi.sh @@ -0,0 +1,60 @@ +#!/bin/sh + +FFMPEG_VERSION="7.1" +FFMPEG_REPOSITORY=navidrome/ffmpeg-windows-builds +DOWNLOAD_FOLDER=/tmp + +#Exit if GIT_TAG is not set +if [ -z "$GIT_TAG" ]; then + echo "GIT_TAG is not set, exiting..." + exit 1 +fi + +set -e + +WORKSPACE=$1 +ARCH=$2 +NAVIDROME_BUILD_VERSION=$(echo "$GIT_TAG" | sed -e 's/^v//' -e 's/-SNAPSHOT/.1/') + +echo "Building MSI package for $ARCH, version $NAVIDROME_BUILD_VERSION" + +MSI_OUTPUT_DIR=$WORKSPACE/binaries/msi +mkdir -p "$MSI_OUTPUT_DIR" +BINARY_DIR=$WORKSPACE/binaries/windows_${ARCH} + +if [ "$ARCH" = "386" ]; then + PLATFORM="x86" + WIN_ARCH="win32" +else + PLATFORM="x64" + WIN_ARCH="win64" +fi + +BINARY=$BINARY_DIR/navidrome.exe +if [ ! -f "$BINARY" ]; then + echo + echo "$BINARY not found!" + echo "Build it with 'make single GOOS=windows GOARCH=${ARCH}'" + exit 1 +fi + +# Download static compiled ffmpeg for Windows +FFMPEG_FILE="ffmpeg-n${FFMPEG_VERSION}-latest-${WIN_ARCH}-gpl-${FFMPEG_VERSION}" +wget --quiet --output-document="${DOWNLOAD_FOLDER}/ffmpeg.zip" \ + "https://github.com/${FFMPEG_REPOSITORY}/releases/download/latest/${FFMPEG_FILE}.zip" +rm -rf "${DOWNLOAD_FOLDER}/extracted_ffmpeg" +unzip -d "${DOWNLOAD_FOLDER}/extracted_ffmpeg" "${DOWNLOAD_FOLDER}/ffmpeg.zip" "*/ffmpeg.exe" +cp "${DOWNLOAD_FOLDER}"/extracted_ffmpeg/${FFMPEG_FILE}/bin/ffmpeg.exe "$MSI_OUTPUT_DIR" + +cp "$WORKSPACE"/LICENSE "$WORKSPACE"/README.md "$MSI_OUTPUT_DIR" +cp "$BINARY" "$MSI_OUTPUT_DIR" + +# workaround for wixl WixVariable not working to override bmp locations +cp "$WORKSPACE"/release/wix/bmp/banner.bmp /usr/share/wixl-*/ext/ui/bitmaps/bannrbmp.bmp +cp "$WORKSPACE"/release/wix/bmp/dialogue.bmp /usr/share/wixl-*/ext/ui/bitmaps/dlgbmp.bmp + +cd "$MSI_OUTPUT_DIR" +rm -f "$MSI_OUTPUT_DIR"/navidrome_"${ARCH}".msi +wixl "$WORKSPACE"/release/wix/navidrome.wxs -D Version="$NAVIDROME_BUILD_VERSION" -D Platform=$PLATFORM --arch $PLATFORM \ + --ext ui --output "$MSI_OUTPUT_DIR"/navidrome_"${ARCH}".msi + diff --git a/release/wix/msitools.dockerfile b/release/wix/msitools.dockerfile new file mode 100644 index 000000000..38364eb47 --- /dev/null +++ b/release/wix/msitools.dockerfile @@ -0,0 +1,3 @@ +FROM public.ecr.aws/docker/library/alpine +RUN apk update && apk add jq msitools +WORKDIR /workspace \ No newline at end of file diff --git a/wix/navidrome.wxs b/release/wix/navidrome.wxs similarity index 70% rename from wix/navidrome.wxs rename to release/wix/navidrome.wxs index ad923a0a4..22ad93f86 100644 --- a/wix/navidrome.wxs +++ b/release/wix/navidrome.wxs @@ -29,8 +29,6 @@ - - @@ -43,14 +41,11 @@ - - - - - - - + + + + @@ -63,31 +58,29 @@ Start='auto' Type='ownProcess' Vital='yes' - Arguments='service execute --configfile "[INSTALLDIR]navidrome.toml"' + Arguments='service execute --configfile "[INSTALLDIR]navidrome.ini" --logfile "[ND_DATAFOLDER]\navidrome.log"' /> + + + + - - Not Installed AND NOT WIX_UPGRADE_DETECTED - - NOT Installed AND NOT REMOVE - - - + diff --git a/wix/convertIniToToml.vbs b/wix/convertIniToToml.vbs deleted file mode 100644 index 1feb7d6d5..000000000 --- a/wix/convertIniToToml.vbs +++ /dev/null @@ -1,17 +0,0 @@ -Const ForReading = 1 -Const ForWriting = 2 - -sSourceFilename = Wscript.Arguments(0) -sTargetFilename = Wscript.Arguments(1) - -Set oFSO = CreateObject("Scripting.FileSystemObject") -Set oFile = oFSO.OpenTextFile(sSourceFilename, ForReading) -sFileContent = oFile.ReadAll -oFile.Close - -sNewFileContent = Replace(sFileContent, "[MSI_PLACEHOLDER_SECTION]" & vbCrLf, "") -If Not ( oFSO.FileExists(sTargetFilename) ) Then - Set oFile = oFSO.CreateTextFile(sTargetFilename) - oFile.Write sNewFileContent - oFile.Close -End If From 0a650de357babdcc8ce910fe37fee84acf4ed2fe Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Wed, 23 Oct 2024 02:00:31 +0000 Subject: [PATCH 03/22] feat(subsonic): add MusicBrainz ID and Sort Name to getArtists --- server/subsonic/browsing.go | 38 +++++++++-- server/subsonic/helpers.go | 8 +++ ... and MBID and Sort Name should match .JSON | 28 ++++++++ ...a and MBID and Sort Name should match .XML | 7 ++ ...ponses Artist with data should match .JSON | 28 ++++++++ ...sponses Artist with data should match .XML | 7 ++ ...ses Artist without data should match .JSON | 11 +++ ...nses Artist without data should match .XML | 3 + server/subsonic/responses/responses.go | 17 ++++- server/subsonic/responses/responses_test.go | 68 ++++++++++++++++++- 10 files changed, 206 insertions(+), 9 deletions(-) create mode 100644 server/subsonic/responses/.snapshots/Responses Artist with data and MBID and Sort Name should match .JSON create mode 100644 server/subsonic/responses/.snapshots/Responses Artist with data and MBID and Sort Name should match .XML create mode 100644 server/subsonic/responses/.snapshots/Responses Artist with data should match .JSON create mode 100644 server/subsonic/responses/.snapshots/Responses Artist with data should match .XML create mode 100644 server/subsonic/responses/.snapshots/Responses Artist without data should match .JSON create mode 100644 server/subsonic/responses/.snapshots/Responses Artist without data should match .XML diff --git a/server/subsonic/browsing.go b/server/subsonic/browsing.go index 954ca1ffc..c0814410d 100644 --- a/server/subsonic/browsing.go +++ b/server/subsonic/browsing.go @@ -27,12 +27,12 @@ func (api *Router) GetMusicFolders(r *http.Request) (*responses.Subsonic, error) return response, nil } -func (api *Router) getArtistIndex(r *http.Request, libId int, ifModifiedSince time.Time) (*responses.Indexes, error) { +func (api *Router) getArtist(r *http.Request, libId int, ifModifiedSince time.Time) (model.ArtistIndexes, int64, error) { ctx := r.Context() lib, err := api.ds.Library(ctx).Get(libId) if err != nil { log.Error(ctx, "Error retrieving Library", "id", libId, err) - return nil, err + return nil, 0, err } var indexes model.ArtistIndexes @@ -40,13 +40,22 @@ func (api *Router) getArtistIndex(r *http.Request, libId int, ifModifiedSince ti indexes, err = api.ds.Artist(ctx).GetIndex() if err != nil { log.Error(ctx, "Error retrieving Indexes", err) - return nil, err + return nil, 0, err } } + return indexes, lib.LastScanAt.UnixMilli(), err +} + +func (api *Router) getArtistIndex(r *http.Request, libId int, ifModifiedSince time.Time) (*responses.Indexes, error) { + indexes, modified, err := api.getArtist(r, libId, ifModifiedSince) + if err != nil { + return nil, err + } + res := &responses.Indexes{ IgnoredArticles: conf.Server.IgnoredArticles, - LastModified: lib.LastScanAt.UnixMilli(), + LastModified: modified, } res.Index = make([]responses.Index, len(indexes)) @@ -57,6 +66,25 @@ func (api *Router) getArtistIndex(r *http.Request, libId int, ifModifiedSince ti return res, nil } +func (api *Router) getArtistIndexID3(r *http.Request, libId int, ifModifiedSince time.Time) (*responses.Artists, error) { + indexes, modified, err := api.getArtist(r, libId, ifModifiedSince) + if err != nil { + return nil, err + } + + res := &responses.Artists{ + IgnoredArticles: conf.Server.IgnoredArticles, + LastModified: modified, + } + + res.Index = make([]responses.IndexID3, len(indexes)) + for i, idx := range indexes { + res.Index[i].Name = idx.ID + res.Index[i].Artists = toArtistsID3(r, idx.Artists) + } + return res, nil +} + func (api *Router) GetIndexes(r *http.Request) (*responses.Subsonic, error) { p := req.Params(r) musicFolderId := p.IntOr("musicFolderId", 1) @@ -75,7 +103,7 @@ func (api *Router) GetIndexes(r *http.Request) (*responses.Subsonic, error) { func (api *Router) GetArtists(r *http.Request) (*responses.Subsonic, error) { p := req.Params(r) musicFolderId := p.IntOr("musicFolderId", 1) - res, err := api.getArtistIndex(r, musicFolderId, time.Time{}) + res, err := api.getArtistIndexID3(r, musicFolderId, time.Time{}) if err != nil { return nil, err } diff --git a/server/subsonic/helpers.go b/server/subsonic/helpers.go index 7afbbbfea..3e3fef4c1 100644 --- a/server/subsonic/helpers.go +++ b/server/subsonic/helpers.go @@ -104,6 +104,14 @@ func toArtistID3(r *http.Request, a model.Artist) responses.ArtistID3 { return artist } +func toArtistsID3(r *http.Request, artists model.Artists) []responses.ArtistID3 { + as := make([]responses.ArtistID3, len(artists)) + for i, artist := range artists { + as[i] = toArtistID3(r, artist) + } + return as +} + func toGenres(genres model.Genres) *responses.Genres { response := make([]responses.Genre, len(genres)) for i, g := range genres { diff --git a/server/subsonic/responses/.snapshots/Responses Artist with data and MBID and Sort Name should match .JSON b/server/subsonic/responses/.snapshots/Responses Artist with data and MBID and Sort Name should match .JSON new file mode 100644 index 000000000..d17c178d4 --- /dev/null +++ b/server/subsonic/responses/.snapshots/Responses Artist with data and MBID and Sort Name should match .JSON @@ -0,0 +1,28 @@ +{ + "status": "ok", + "version": "1.8.0", + "type": "navidrome", + "serverVersion": "v0.0.0", + "openSubsonic": true, + "artists": { + "index": [ + { + "name": "A", + "artist": [ + { + "id": "111", + "name": "aaa", + "albumCount": 2, + "starred": "2016-03-02T20:30:00Z", + "userRating": 3, + "artistImageUrl": "https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png", + "musicBrainzId": "1234", + "sortName": "sort name" + } + ] + } + ], + "lastModified": 1, + "ignoredArticles": "A" + } +} diff --git a/server/subsonic/responses/.snapshots/Responses Artist with data and MBID and Sort Name should match .XML b/server/subsonic/responses/.snapshots/Responses Artist with data and MBID and Sort Name should match .XML new file mode 100644 index 000000000..4ba6a5924 --- /dev/null +++ b/server/subsonic/responses/.snapshots/Responses Artist with data and MBID and Sort Name should match .XML @@ -0,0 +1,7 @@ + + + + + + + diff --git a/server/subsonic/responses/.snapshots/Responses Artist with data should match .JSON b/server/subsonic/responses/.snapshots/Responses Artist with data should match .JSON new file mode 100644 index 000000000..470533668 --- /dev/null +++ b/server/subsonic/responses/.snapshots/Responses Artist with data should match .JSON @@ -0,0 +1,28 @@ +{ + "status": "ok", + "version": "1.8.0", + "type": "navidrome", + "serverVersion": "v0.0.0", + "openSubsonic": true, + "artists": { + "index": [ + { + "name": "A", + "artist": [ + { + "id": "111", + "name": "aaa", + "albumCount": 2, + "starred": "2016-03-02T20:30:00Z", + "userRating": 3, + "artistImageUrl": "https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png", + "musicBrainzId": "", + "sortName": "" + } + ] + } + ], + "lastModified": 1, + "ignoredArticles": "A" + } +} diff --git a/server/subsonic/responses/.snapshots/Responses Artist with data should match .XML b/server/subsonic/responses/.snapshots/Responses Artist with data should match .XML new file mode 100644 index 000000000..7a4149f66 --- /dev/null +++ b/server/subsonic/responses/.snapshots/Responses Artist with data should match .XML @@ -0,0 +1,7 @@ + + + + + + + diff --git a/server/subsonic/responses/.snapshots/Responses Artist without data should match .JSON b/server/subsonic/responses/.snapshots/Responses Artist without data should match .JSON new file mode 100644 index 000000000..b4b504f6e --- /dev/null +++ b/server/subsonic/responses/.snapshots/Responses Artist without data should match .JSON @@ -0,0 +1,11 @@ +{ + "status": "ok", + "version": "1.8.0", + "type": "navidrome", + "serverVersion": "v0.0.0", + "openSubsonic": true, + "artists": { + "lastModified": 1, + "ignoredArticles": "A" + } +} diff --git a/server/subsonic/responses/.snapshots/Responses Artist without data should match .XML b/server/subsonic/responses/.snapshots/Responses Artist without data should match .XML new file mode 100644 index 000000000..01fda5620 --- /dev/null +++ b/server/subsonic/responses/.snapshots/Responses Artist without data should match .XML @@ -0,0 +1,3 @@ + + + diff --git a/server/subsonic/responses/responses.go b/server/subsonic/responses/responses.go index 8e3edaf4f..f1c0b7bc5 100644 --- a/server/subsonic/responses/responses.go +++ b/server/subsonic/responses/responses.go @@ -35,7 +35,7 @@ type Subsonic struct { Genres *Genres `xml:"genres,omitempty" json:"genres,omitempty"` // ID3 - Artist *Indexes `xml:"artists,omitempty" json:"artists,omitempty"` + Artist *Artists `xml:"artists,omitempty" json:"artists,omitempty"` ArtistWithAlbumsID3 *ArtistWithAlbumsID3 `xml:"artist,omitempty" json:"artist,omitempty"` AlbumWithSongsID3 *AlbumWithSongsID3 `xml:"album,omitempty" json:"album,omitempty"` @@ -112,6 +112,17 @@ type Indexes struct { IgnoredArticles string `xml:"ignoredArticles,attr" json:"ignoredArticles"` } +type IndexID3 struct { + Name string `xml:"name,attr" json:"name"` + Artists []ArtistID3 `xml:"artist" json:"artist"` +} + +type Artists struct { + Index []IndexID3 `xml:"index" json:"index,omitempty"` + LastModified int64 `xml:"lastModified,attr" json:"lastModified"` + IgnoredArticles string `xml:"ignoredArticles,attr" json:"ignoredArticles"` +} + type MediaType string const ( @@ -207,8 +218,8 @@ type ArtistID3 struct { ArtistImageUrl string `xml:"artistImageUrl,attr,omitempty" json:"artistImageUrl,omitempty"` // OpenSubsonic extensions - MusicBrainzId string `xml:"musicBrainzId,attr,omitempty" json:"musicBrainzId,omitempty"` - SortName string `xml:"sortName,attr,omitempty" json:"sortName,omitempty"` + MusicBrainzId string `xml:"musicBrainzId,attr" json:"musicBrainzId"` + SortName string `xml:"sortName,attr" json:"sortName"` } type AlbumID3 struct { diff --git a/server/subsonic/responses/responses_test.go b/server/subsonic/responses/responses_test.go index 88fb74050..13eb1f9ba 100644 --- a/server/subsonic/responses/responses_test.go +++ b/server/subsonic/responses/responses_test.go @@ -120,6 +120,73 @@ var _ = Describe("Responses", func() { }) }) + Describe("Artist", func() { + BeforeEach(func() { + response.Artist = &Artists{LastModified: 1, IgnoredArticles: "A"} + }) + + Context("without data", func() { + It("should match .XML", func() { + Expect(xml.MarshalIndent(response, "", " ")).To(MatchSnapshot()) + }) + It("should match .JSON", func() { + Expect(json.MarshalIndent(response, "", " ")).To(MatchSnapshot()) + }) + }) + + Context("with data", func() { + BeforeEach(func() { + artists := make([]ArtistID3, 1) + t := time.Date(2016, 03, 2, 20, 30, 0, 0, time.UTC) + artists[0] = ArtistID3{ + Id: "111", + Name: "aaa", + Starred: &t, + UserRating: 3, + AlbumCount: 2, + ArtistImageUrl: "https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png", + } + index := make([]IndexID3, 1) + index[0] = IndexID3{Name: "A", Artists: artists} + response.Artist.Index = index + }) + + It("should match .XML", func() { + Expect(xml.MarshalIndent(response, "", " ")).To(MatchSnapshot()) + }) + It("should match .JSON", func() { + Expect(json.MarshalIndent(response, "", " ")).To(MatchSnapshot()) + }) + }) + + Context("with data and MBID and Sort Name", func() { + BeforeEach(func() { + artists := make([]ArtistID3, 1) + t := time.Date(2016, 03, 2, 20, 30, 0, 0, time.UTC) + artists[0] = ArtistID3{ + Id: "111", + Name: "aaa", + Starred: &t, + UserRating: 3, + AlbumCount: 2, + ArtistImageUrl: "https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png", + MusicBrainzId: "1234", + SortName: "sort name", + } + index := make([]IndexID3, 1) + index[0] = IndexID3{Name: "A", Artists: artists} + response.Artist.Index = index + }) + + It("should match .XML", func() { + Expect(xml.MarshalIndent(response, "", " ")).To(MatchSnapshot()) + }) + It("should match .JSON", func() { + Expect(json.MarshalIndent(response, "", " ")).To(MatchSnapshot()) + }) + }) + }) + Describe("Child", func() { Context("without data", func() { BeforeEach(func() { @@ -466,7 +533,6 @@ var _ = Describe("Responses", func() { It("should match .JSON", func() { Expect(json.MarshalIndent(response, "", " ")).To(MatchSnapshot()) }) - }) }) From a557f37834072dad0aa6137c9a47ba380d2205c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Tue, 22 Oct 2024 22:54:31 -0400 Subject: [PATCH 04/22] refactor: small improvements and clean up (#3423) * refactor: replace custom map functions with slice.Map * refactor: extract StringerValue function * refactor: removed unnecessary if * chore: removed invalid comment * refactor: replace more map functions * chore: fix FFmpeg typo --- log/formatters.go | 10 +++++ log/formatters_test.go | 10 +++++ log/log.go | 8 +--- persistence/sql_search.go | 7 +--- server/initial_setup.go | 2 +- server/server.go | 2 +- server/subsonic/album_lists.go | 38 ++++++++++-------- server/subsonic/bookmarks.go | 12 +++--- server/subsonic/browsing.go | 17 +++++---- server/subsonic/helpers.go | 68 +++++++-------------------------- server/subsonic/helpers_test.go | 6 +-- server/subsonic/jukebox.go | 3 +- server/subsonic/playlists.go | 26 +++++-------- server/subsonic/searching.go | 24 ++++++------ server/subsonic/sharing.go | 5 ++- utils/slice/slice.go | 6 +++ utils/slice/slice_test.go | 14 +++++++ 17 files changed, 124 insertions(+), 134 deletions(-) diff --git a/log/formatters.go b/log/formatters.go index c42282183..38cb14bab 100644 --- a/log/formatters.go +++ b/log/formatters.go @@ -1,7 +1,9 @@ package log import ( + "fmt" "io" + "reflect" "strings" "time" ) @@ -24,6 +26,14 @@ func ShortDur(d time.Duration) string { return strings.TrimSuffix(s, "0m") } +func StringerValue(s fmt.Stringer) string { + v := reflect.ValueOf(s) + if v.Kind() == reflect.Pointer && v.IsNil() { + return "nil" + } + return s.String() +} + func CRLFWriter(w io.Writer) io.Writer { return &crlfWriter{w: w} } diff --git a/log/formatters_test.go b/log/formatters_test.go index 0a700288a..6ed43a094 100644 --- a/log/formatters_test.go +++ b/log/formatters_test.go @@ -28,6 +28,16 @@ var _ = DescribeTable("ShortDur", Entry("4h2m", 4*time.Hour+2*time.Minute+5*time.Second+200*time.Millisecond, "4h2m"), ) +var _ = Describe("StringerValue", func() { + It("should return the string representation of a fmt.Stringer", func() { + Expect(log.StringerValue(time.Second)).To(Equal("1s")) + }) + It("should return 'nil' for a nil fmt.Stringer", func() { + v := (*time.Time)(nil) + Expect(log.StringerValue(v)).To(Equal("nil")) + }) +}) + var _ = Describe("CRLFWriter", func() { var ( buffer *bytes.Buffer diff --git a/log/log.go b/log/log.go index c990a5614..41b3ee0cf 100644 --- a/log/log.go +++ b/log/log.go @@ -7,7 +7,6 @@ import ( "io" "net/http" "os" - "reflect" "runtime" "sort" "strings" @@ -277,12 +276,7 @@ func addFields(logger *logrus.Entry, keyValuePairs []interface{}) *logrus.Entry case time.Duration: logger = logger.WithField(name, ShortDur(v)) case fmt.Stringer: - vOf := reflect.ValueOf(v) - if vOf.Kind() == reflect.Pointer && vOf.IsNil() { - logger = logger.WithField(name, "nil") - } else { - logger = logger.WithField(name, v.String()) - } + logger = logger.WithField(name, StringerValue(v)) default: logger = logger.WithField(name, v) } diff --git a/persistence/sql_search.go b/persistence/sql_search.go index 98e7760ec..f9a3715ea 100644 --- a/persistence/sql_search.go +++ b/persistence/sql_search.go @@ -25,17 +25,14 @@ func (r sqlRepository) doSearch(q string, offset, size int, results interface{}, filter := fullTextExpr(q) if filter != nil { sq = sq.Where(filter) - if len(orderBys) > 0 { - sq = sq.OrderBy(orderBys...) - } + sq = sq.OrderBy(orderBys...) } else { // If the filter is empty, we sort by id. // This is to speed up the results of `search3?query=""`, for OpenSubsonic sq = sq.OrderBy("id") } sq = sq.Limit(uint64(size)).Offset(uint64(offset)) - err := r.queryAll(sq, results, model.QueryOptions{Offset: offset}) - return err + return r.queryAll(sq, results, model.QueryOptions{Offset: offset}) } func fullTextExpr(value string) Sqlizer { diff --git a/server/initial_setup.go b/server/initial_setup.go index 5f314218f..a61e1a591 100644 --- a/server/initial_setup.go +++ b/server/initial_setup.go @@ -83,7 +83,7 @@ func createJWTSecret(ds model.DataStore) error { return err } -func checkFfmpegInstallation() { +func checkFFmpegInstallation() { f := ffmpeg.New() _, err := f.CmdPath() if err == nil { diff --git a/server/server.go b/server/server.go index aedb91950..a8a89db5b 100644 --- a/server/server.go +++ b/server/server.go @@ -40,7 +40,7 @@ func New(ds model.DataStore, broker events.Broker) *Server { s.initRoutes() s.mountAuthenticationRoutes() s.mountRootRedirector() - checkFfmpegInstallation() + checkFFmpegInstallation() checkExternalCredentials() return s } diff --git a/server/subsonic/album_lists.go b/server/subsonic/album_lists.go index dc6f2094d..f173a73e5 100644 --- a/server/subsonic/album_lists.go +++ b/server/subsonic/album_lists.go @@ -6,11 +6,13 @@ import ( "strconv" "time" + "github.com/navidrome/navidrome/core/scrobbler" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/server/subsonic/filter" "github.com/navidrome/navidrome/server/subsonic/responses" "github.com/navidrome/navidrome/utils/req" + "github.com/navidrome/navidrome/utils/slice" ) func (api *Router) getAlbumList(r *http.Request) (model.Albums, int64, error) { @@ -86,7 +88,9 @@ func (api *Router) GetAlbumList(w http.ResponseWriter, r *http.Request) (*respon w.Header().Set("x-total-count", strconv.Itoa(int(count))) response := newResponse() - response.AlbumList = &responses.AlbumList{Album: childrenFromAlbums(r.Context(), albums)} + response.AlbumList = &responses.AlbumList{ + Album: slice.MapWithArg(albums, r.Context(), childFromAlbum), + } return response, nil } @@ -99,7 +103,9 @@ func (api *Router) GetAlbumList2(w http.ResponseWriter, r *http.Request) (*respo w.Header().Set("x-total-count", strconv.FormatInt(pageCount, 10)) response := newResponse() - response.AlbumList2 = &responses.AlbumList{Album: childrenFromAlbums(r.Context(), albums)} + response.AlbumList2 = &responses.AlbumList{ + Album: slice.MapWithArg(albums, r.Context(), childFromAlbum), + } return response, nil } @@ -124,9 +130,9 @@ func (api *Router) GetStarred(r *http.Request) (*responses.Subsonic, error) { response := newResponse() response.Starred = &responses.Starred{} - response.Starred.Artist = toArtists(r, artists) - response.Starred.Album = childrenFromAlbums(r.Context(), albums) - response.Starred.Song = childrenFromMediaFiles(r.Context(), mediaFiles) + response.Starred.Artist = slice.MapWithArg(artists, r, toArtist) + response.Starred.Album = slice.MapWithArg(albums, ctx, childFromAlbum) + response.Starred.Song = slice.MapWithArg(mediaFiles, ctx, childFromMediaFile) return response, nil } @@ -151,14 +157,16 @@ func (api *Router) GetNowPlaying(r *http.Request) (*responses.Subsonic, error) { response := newResponse() response.NowPlaying = &responses.NowPlaying{} - response.NowPlaying.Entry = make([]responses.NowPlayingEntry, len(npInfo)) - for i, np := range npInfo { - response.NowPlaying.Entry[i].Child = childFromMediaFile(ctx, np.MediaFile) - response.NowPlaying.Entry[i].UserName = np.Username - response.NowPlaying.Entry[i].MinutesAgo = int32(time.Since(np.Start).Minutes()) - response.NowPlaying.Entry[i].PlayerId = int32(i + 1) // Fake numeric playerId, it does not seem to be used for anything - response.NowPlaying.Entry[i].PlayerName = np.PlayerName - } + var i int32 + response.NowPlaying.Entry = slice.Map(npInfo, func(np scrobbler.NowPlayingInfo) responses.NowPlayingEntry { + return responses.NowPlayingEntry{ + Child: childFromMediaFile(ctx, np.MediaFile), + UserName: np.Username, + MinutesAgo: int32(time.Since(np.Start).Minutes()), + PlayerId: i + 1, // Fake numeric playerId, it does not seem to be used for anything + PlayerName: np.PlayerName, + } + }) return response, nil } @@ -177,7 +185,7 @@ func (api *Router) GetRandomSongs(r *http.Request) (*responses.Subsonic, error) response := newResponse() response.RandomSongs = &responses.Songs{} - response.RandomSongs.Songs = childrenFromMediaFiles(r.Context(), songs) + response.RandomSongs.Songs = slice.MapWithArg(songs, r.Context(), childFromMediaFile) return response, nil } @@ -195,7 +203,7 @@ func (api *Router) GetSongsByGenre(r *http.Request) (*responses.Subsonic, error) response := newResponse() response.SongsByGenre = &responses.Songs{} - response.SongsByGenre.Songs = childrenFromMediaFiles(r.Context(), songs) + response.SongsByGenre.Songs = slice.MapWithArg(songs, r.Context(), childFromMediaFile) return response, nil } diff --git a/server/subsonic/bookmarks.go b/server/subsonic/bookmarks.go index 69c303147..f6fd1a99e 100644 --- a/server/subsonic/bookmarks.go +++ b/server/subsonic/bookmarks.go @@ -9,21 +9,22 @@ import ( "github.com/navidrome/navidrome/model/request" "github.com/navidrome/navidrome/server/subsonic/responses" "github.com/navidrome/navidrome/utils/req" + "github.com/navidrome/navidrome/utils/slice" ) func (api *Router) GetBookmarks(r *http.Request) (*responses.Subsonic, error) { user, _ := request.UserFrom(r.Context()) repo := api.ds.MediaFile(r.Context()) - bmks, err := repo.GetBookmarks() + bookmarks, err := repo.GetBookmarks() if err != nil { return nil, err } response := newResponse() response.Bookmarks = &responses.Bookmarks{} - for _, bmk := range bmks { - b := responses.Bookmark{ + response.Bookmarks.Bookmark = slice.Map(bookmarks, func(bmk model.Bookmark) responses.Bookmark { + return responses.Bookmark{ Entry: childFromMediaFile(r.Context(), bmk.Item), Position: bmk.Position, Username: user.UserName, @@ -31,8 +32,7 @@ func (api *Router) GetBookmarks(r *http.Request) (*responses.Subsonic, error) { Created: bmk.CreatedAt, Changed: bmk.UpdatedAt, } - response.Bookmarks.Bookmark = append(response.Bookmarks.Bookmark, b) - } + }) return response, nil } @@ -83,7 +83,7 @@ func (api *Router) GetPlayQueue(r *http.Request) (*responses.Subsonic, error) { response := newResponse() response.PlayQueue = &responses.PlayQueue{ - Entry: childrenFromMediaFiles(r.Context(), pq.Items), + Entry: slice.MapWithArg(pq.Items, r.Context(), childFromMediaFile), Current: pq.Current, Position: pq.Position, Username: user.UserName, diff --git a/server/subsonic/browsing.go b/server/subsonic/browsing.go index c0814410d..16630f7a7 100644 --- a/server/subsonic/browsing.go +++ b/server/subsonic/browsing.go @@ -13,6 +13,7 @@ import ( "github.com/navidrome/navidrome/server/subsonic/filter" "github.com/navidrome/navidrome/server/subsonic/responses" "github.com/navidrome/navidrome/utils/req" + "github.com/navidrome/navidrome/utils/slice" ) func (api *Router) GetMusicFolders(r *http.Request) (*responses.Subsonic, error) { @@ -61,7 +62,7 @@ func (api *Router) getArtistIndex(r *http.Request, libId int, ifModifiedSince ti res.Index = make([]responses.Index, len(indexes)) for i, idx := range indexes { res.Index[i].Name = idx.ID - res.Index[i].Artists = toArtists(r, idx.Artists) + res.Index[i].Artists = slice.MapWithArg(idx.Artists, r, toArtist) } return res, nil } @@ -80,7 +81,7 @@ func (api *Router) getArtistIndexID3(r *http.Request, libId int, ifModifiedSince res.Index = make([]responses.IndexID3, len(indexes)) for i, idx := range indexes { res.Index[i].Name = idx.ID - res.Index[i].Artists = toArtistsID3(r, idx.Artists) + res.Index[i].Artists = slice.MapWithArg(idx.Artists, r, toArtistID3) } return res, nil } @@ -336,7 +337,7 @@ func (api *Router) GetSimilarSongs(r *http.Request) (*responses.Subsonic, error) response := newResponse() response.SimilarSongs = &responses.SimilarSongs{ - Song: childrenFromMediaFiles(ctx, songs), + Song: slice.MapWithArg(songs, ctx, childFromMediaFile), } return response, nil } @@ -370,7 +371,7 @@ func (api *Router) GetTopSongs(r *http.Request) (*responses.Subsonic, error) { response := newResponse() response.TopSongs = &responses.TopSongs{ - Song: childrenFromMediaFiles(ctx, songs), + Song: slice.MapWithArg(songs, ctx, childFromMediaFile), } return response, nil } @@ -394,7 +395,7 @@ func (api *Router) buildArtistDirectory(ctx context.Context, artist *model.Artis return nil, err } - dir.Child = childrenFromAlbums(ctx, albums) + dir.Child = slice.MapWithArg(albums, ctx, childFromAlbum) return dir, nil } @@ -408,7 +409,7 @@ func (api *Router) buildArtist(r *http.Request, artist *model.Artist) (*response return nil, err } - a.Album = childrenFromAlbums(r.Context(), albums) + a.Album = slice.MapWithArg(albums, ctx, childFromAlbum) return a, nil } @@ -433,13 +434,13 @@ func (api *Router) buildAlbumDirectory(ctx context.Context, album *model.Album) return nil, err } - dir.Child = childrenFromMediaFiles(ctx, mfs) + dir.Child = slice.MapWithArg(mfs, ctx, childFromMediaFile) return dir, nil } func (api *Router) buildAlbum(ctx context.Context, album *model.Album, mfs model.MediaFiles) *responses.AlbumWithSongsID3 { dir := &responses.AlbumWithSongsID3{} dir.AlbumID3 = buildAlbumID3(ctx, *album) - dir.Song = childrenFromMediaFiles(ctx, mfs) + dir.Song = slice.MapWithArg(mfs, ctx, childFromMediaFile) return dir } diff --git a/server/subsonic/helpers.go b/server/subsonic/helpers.go index 3e3fef4c1..8b3e4be2c 100644 --- a/server/subsonic/helpers.go +++ b/server/subsonic/helpers.go @@ -64,14 +64,6 @@ func getUser(ctx context.Context) model.User { return model.User{} } -func toArtists(r *http.Request, artists model.Artists) []responses.Artist { - as := make([]responses.Artist, len(artists)) - for i, artist := range artists { - as[i] = toArtist(r, artist) - } - return as -} - func toArtist(r *http.Request, a model.Artist) responses.Artist { artist := responses.Artist{ Id: a.ID, @@ -104,14 +96,6 @@ func toArtistID3(r *http.Request, a model.Artist) responses.ArtistID3 { return artist } -func toArtistsID3(r *http.Request, artists model.Artists) []responses.ArtistID3 { - as := make([]responses.ArtistID3, len(artists)) - for i, artist := range artists { - as[i] = toArtistID3(r, artist) - } - return as -} - func toGenres(genres model.Genres) *responses.Genres { response := make([]responses.Genre, len(genres)) for i, g := range genres { @@ -124,6 +108,14 @@ func toGenres(genres model.Genres) *responses.Genres { return &responses.Genres{Genre: response} } +func toItemGenres(genres model.Genres) []responses.ItemGenre { + itemGenres := make([]responses.ItemGenre, len(genres)) + for i, g := range genres { + itemGenres[i] = responses.ItemGenre{Name: g.Name} + } + return itemGenres +} + func getTranscoding(ctx context.Context) (format string, bitRate int) { if trc, ok := request.TranscodingFrom(ctx); ok { format = trc.TargetFormat @@ -134,8 +126,6 @@ func getTranscoding(ctx context.Context) (format string, bitRate int) { return } -// This seems to be duplicated, but it is an initial step into merging `engine` and the `subsonic` packages, -// In the future there won't be any conversion to/from `engine. Entry` anymore func childFromMediaFile(ctx context.Context, mf model.MediaFile) responses.Child { child := responses.Child{} child.Id = mf.ID @@ -146,7 +136,7 @@ func childFromMediaFile(ctx context.Context, mf model.MediaFile) responses.Child child.Year = int32(mf.Year) child.Artist = mf.Artist child.Genre = mf.Genre - child.Genres = buildItemGenres(mf.Genres) + child.Genres = toItemGenres(mf.Genres) child.Track = int32(mf.TrackNumber) child.Duration = int32(mf.Duration) child.Size = mf.Size @@ -208,14 +198,6 @@ func mapSlashToDash(target string) string { return strings.ReplaceAll(target, "/", "_") } -func childrenFromMediaFiles(ctx context.Context, mfs model.MediaFiles) []responses.Child { - children := make([]responses.Child, len(mfs)) - for i, mf := range mfs { - children[i] = childFromMediaFile(ctx, mf) - } - return children -} - func childFromAlbum(_ context.Context, al model.Album) responses.Child { child := responses.Child{} child.Id = al.ID @@ -226,7 +208,7 @@ func childFromAlbum(_ context.Context, al model.Album) responses.Child { child.Artist = al.AlbumArtist child.Year = int32(al.MaxYear) child.Genre = al.Genre - child.Genres = buildItemGenres(al.Genres) + child.Genres = toItemGenres(al.Genres) child.CoverArt = al.CoverArtID().String() child.Created = &al.CreatedAt child.Parent = al.AlbumArtistID @@ -247,14 +229,6 @@ func childFromAlbum(_ context.Context, al model.Album) responses.Child { return child } -func childrenFromAlbums(ctx context.Context, als model.Albums) []responses.Child { - children := make([]responses.Child, len(als)) - for i, al := range als { - children[i] = childFromAlbum(ctx, al) - } - 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{} @@ -273,15 +247,7 @@ func toItemDate(date string) responses.ItemDate { return itemDate } -func buildItemGenres(genres model.Genres) []responses.ItemGenre { - itemGenres := make([]responses.ItemGenre, len(genres)) - for i, g := range genres { - itemGenres[i] = responses.ItemGenre{Name: g.Name} - } - return itemGenres -} - -func buildDiscSubtitles(_ context.Context, a model.Album) responses.DiscTitles { +func buildDiscSubtitles(a model.Album) responses.DiscTitles { if len(a.Discs) == 0 { return nil } @@ -295,14 +261,6 @@ func buildDiscSubtitles(_ context.Context, a model.Album) responses.DiscTitles { return discTitles } -func buildAlbumsID3(ctx context.Context, albums model.Albums) []responses.AlbumID3 { - res := make([]responses.AlbumID3, len(albums)) - for i, album := range albums { - res[i] = buildAlbumID3(ctx, album) - } - return res -} - func buildAlbumID3(ctx context.Context, album model.Album) responses.AlbumID3 { dir := responses.AlbumID3{} dir.Id = album.ID @@ -318,8 +276,8 @@ func buildAlbumID3(ctx context.Context, album model.Album) responses.AlbumID3 { } dir.Year = int32(album.MaxYear) dir.Genre = album.Genre - dir.Genres = buildItemGenres(album.Genres) - dir.DiscTitles = buildDiscSubtitles(ctx, album) + dir.Genres = toItemGenres(album.Genres) + dir.DiscTitles = buildDiscSubtitles(album) dir.UserRating = int32(album.Rating) if !album.CreatedAt.IsZero() { dir.Created = &album.CreatedAt diff --git a/server/subsonic/helpers_test.go b/server/subsonic/helpers_test.go index 8b33ebda4..a21a614ca 100644 --- a/server/subsonic/helpers_test.go +++ b/server/subsonic/helpers_test.go @@ -1,8 +1,6 @@ package subsonic import ( - "context" - "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/server/subsonic/responses" . "github.com/onsi/ginkgo/v2" @@ -40,7 +38,7 @@ var _ = Describe("helpers", func() { Describe("buildDiscTitles", func() { It("should return nil when album has no discs", func() { album := model.Album{} - Expect(buildDiscSubtitles(context.Background(), album)).To(BeNil()) + Expect(buildDiscSubtitles(album)).To(BeNil()) }) It("should return correct disc titles when album has discs with valid disc numbers", func() { @@ -54,7 +52,7 @@ var _ = Describe("helpers", func() { {Disc: 1, Title: "Disc 1"}, {Disc: 2, Title: "Disc 2"}, } - Expect(buildDiscSubtitles(context.Background(), album)).To(Equal(expected)) + Expect(buildDiscSubtitles(album)).To(Equal(expected)) }) }) diff --git a/server/subsonic/jukebox.go b/server/subsonic/jukebox.go index e6ad979ff..c4bc643ab 100644 --- a/server/subsonic/jukebox.go +++ b/server/subsonic/jukebox.go @@ -9,6 +9,7 @@ import ( "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/server/subsonic/responses" "github.com/navidrome/navidrome/utils/req" + "github.com/navidrome/navidrome/utils/slice" ) const ( @@ -58,7 +59,7 @@ func (api *Router) JukeboxControl(r *http.Request) (*responses.Subsonic, error) playlist := responses.JukeboxPlaylist{ JukeboxStatus: *deviceStatusToJukeboxStatus(status), - Entry: childrenFromMediaFiles(ctx, mediafiles), + Entry: slice.MapWithArg(mediafiles, ctx, childFromMediaFile), } response := newResponse() diff --git a/server/subsonic/playlists.go b/server/subsonic/playlists.go index 00d5861c5..f12c15f94 100644 --- a/server/subsonic/playlists.go +++ b/server/subsonic/playlists.go @@ -11,6 +11,7 @@ import ( "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/server/subsonic/responses" "github.com/navidrome/navidrome/utils/req" + "github.com/navidrome/navidrome/utils/slice" ) func (api *Router) GetPlaylists(r *http.Request) (*responses.Subsonic, error) { @@ -20,12 +21,10 @@ func (api *Router) GetPlaylists(r *http.Request) (*responses.Subsonic, error) { log.Error(r, err) return nil, err } - playlists := make([]responses.Playlist, len(allPls)) - for i, p := range allPls { - playlists[i] = *api.buildPlaylist(p) - } response := newResponse() - response.Playlists = &responses.Playlists{Playlist: playlists} + response.Playlists = &responses.Playlists{ + Playlist: slice.Map(allPls, api.buildPlaylist), + } return response, nil } @@ -51,7 +50,10 @@ func (api *Router) getPlaylist(ctx context.Context, id string) (*responses.Subso } response := newResponse() - response.Playlist = api.buildPlaylistWithSongs(ctx, pls) + response.Playlist = &responses.PlaylistWithSongs{ + Playlist: api.buildPlaylist(*pls), + } + response.Playlist.Entry = slice.MapWithArg(pls.MediaFiles(), ctx, childFromMediaFile) return response, nil } @@ -156,16 +158,8 @@ func (api *Router) UpdatePlaylist(r *http.Request) (*responses.Subsonic, error) return newResponse(), nil } -func (api *Router) buildPlaylistWithSongs(ctx context.Context, p *model.Playlist) *responses.PlaylistWithSongs { - pls := &responses.PlaylistWithSongs{ - Playlist: *api.buildPlaylist(*p), - } - pls.Entry = childrenFromMediaFiles(ctx, p.MediaFiles()) - return pls -} - -func (api *Router) buildPlaylist(p model.Playlist) *responses.Playlist { - pls := &responses.Playlist{} +func (api *Router) buildPlaylist(p model.Playlist) responses.Playlist { + pls := responses.Playlist{} pls.Id = p.ID pls.Name = p.Name pls.Comment = p.Comment diff --git a/server/subsonic/searching.go b/server/subsonic/searching.go index 3437c80cf..2fd3228f0 100644 --- a/server/subsonic/searching.go +++ b/server/subsonic/searching.go @@ -14,6 +14,7 @@ import ( "github.com/navidrome/navidrome/server/public" "github.com/navidrome/navidrome/server/subsonic/responses" "github.com/navidrome/navidrome/utils/req" + "github.com/navidrome/navidrome/utils/slice" "golang.org/x/sync/errgroup" ) @@ -89,9 +90,8 @@ func (api *Router) Search2(r *http.Request) (*responses.Subsonic, error) { response := newResponse() searchResult2 := &responses.SearchResult2{} - searchResult2.Artist = make([]responses.Artist, len(as)) - for i, artist := range as { - searchResult2.Artist[i] = responses.Artist{ + searchResult2.Artist = slice.Map(as, func(artist model.Artist) responses.Artist { + a := responses.Artist{ Id: artist.ID, Name: artist.Name, AlbumCount: int32(artist.AlbumCount), @@ -100,11 +100,12 @@ func (api *Router) Search2(r *http.Request) (*responses.Subsonic, error) { ArtistImageUrl: public.ImageURL(r, artist.CoverArtID(), 600), } if artist.Starred { - searchResult2.Artist[i].Starred = as[i].StarredAt + a.Starred = artist.StarredAt } - } - searchResult2.Album = childrenFromAlbums(ctx, als) - searchResult2.Song = childrenFromMediaFiles(ctx, mfs) + return a + }) + searchResult2.Album = slice.MapWithArg(als, ctx, childFromAlbum) + searchResult2.Song = slice.MapWithArg(mfs, ctx, childFromMediaFile) response.SearchResult2 = searchResult2 return response, nil } @@ -119,12 +120,9 @@ func (api *Router) Search3(r *http.Request) (*responses.Subsonic, error) { response := newResponse() searchResult3 := &responses.SearchResult3{} - searchResult3.Artist = make([]responses.ArtistID3, len(as)) - for i, artist := range as { - searchResult3.Artist[i] = toArtistID3(r, artist) - } - searchResult3.Album = buildAlbumsID3(ctx, als) - searchResult3.Song = childrenFromMediaFiles(ctx, mfs) + searchResult3.Artist = slice.MapWithArg(as, r, toArtistID3) + searchResult3.Album = slice.MapWithArg(als, ctx, buildAlbumID3) + searchResult3.Song = slice.MapWithArg(mfs, ctx, childFromMediaFile) response.SearchResult3 = searchResult3 return response, nil } diff --git a/server/subsonic/sharing.go b/server/subsonic/sharing.go index 0ba6419a4..9848ff510 100644 --- a/server/subsonic/sharing.go +++ b/server/subsonic/sharing.go @@ -11,6 +11,7 @@ import ( "github.com/navidrome/navidrome/server/subsonic/responses" . "github.com/navidrome/navidrome/utils/gg" "github.com/navidrome/navidrome/utils/req" + "github.com/navidrome/navidrome/utils/slice" ) func (api *Router) GetShares(r *http.Request) (*responses.Subsonic, error) { @@ -43,9 +44,9 @@ func (api *Router) buildShare(r *http.Request, share model.Share) responses.Shar resp.Description = share.Contents } if len(share.Albums) > 0 { - resp.Entry = childrenFromAlbums(r.Context(), share.Albums) + resp.Entry = slice.MapWithArg(share.Albums, r.Context(), childFromAlbum) } else { - resp.Entry = childrenFromMediaFiles(r.Context(), share.Tracks) + resp.Entry = slice.MapWithArg(share.Tracks, r.Context(), childFromMediaFile) } return resp } diff --git a/utils/slice/slice.go b/utils/slice/slice.go index b072e7615..54b881431 100644 --- a/utils/slice/slice.go +++ b/utils/slice/slice.go @@ -15,6 +15,12 @@ func Map[T any, R any](t []T, mapFunc func(T) R) []R { return r } +func MapWithArg[I any, O any, A any](t []I, arg A, mapFunc func(A, I) O) []O { + return Map(t, func(e I) O { + return mapFunc(arg, e) + }) +} + func Group[T any, K comparable](s []T, keyFunc func(T) K) map[K][]T { m := map[K][]T{} for _, item := range s { diff --git a/utils/slice/slice_test.go b/utils/slice/slice_test.go index a97e48501..b2d859ef3 100644 --- a/utils/slice/slice_test.go +++ b/utils/slice/slice_test.go @@ -33,6 +33,20 @@ var _ = Describe("Slice Utils", func() { }) }) + Describe("MapWithArg", func() { + It("returns empty slice for an empty input", func() { + mapFunc := func(a int, v int) string { return strconv.Itoa(a + v) } + result := slice.MapWithArg([]int{}, 10, mapFunc) + Expect(result).To(BeEmpty()) + }) + + It("returns a new slice with elements mapped", func() { + mapFunc := func(a int, v int) string { return strconv.Itoa(a + v) } + result := slice.MapWithArg([]int{1, 2, 3, 4}, 10, mapFunc) + Expect(result).To(ConsistOf("11", "12", "13", "14")) + }) + }) + Describe("Group", func() { It("returns empty map for an empty input", func() { keyFunc := func(v int) int { return v % 2 } From 9d8c49750e7800208482bd11478cbda411ce4bbf Mon Sep 17 00:00:00 2001 From: Deluan Date: Wed, 23 Oct 2024 23:00:32 -0400 Subject: [PATCH 05/22] ci: ignore refactor commits in release notes --- release/goreleaser.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/release/goreleaser.yml b/release/goreleaser.yml index 91f9b98bb..e353aecfd 100644 --- a/release/goreleaser.yml +++ b/release/goreleaser.yml @@ -64,6 +64,7 @@ changelog: filters: exclude: - "^test:" + - "^refactor:" - Merge pull request - Merge remote-tracking branch - Merge branch From 67474b776cf01f9b15eea05b3e9b49fb0211d63c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Oct 2024 23:13:30 -0400 Subject: [PATCH 06/22] chore(deps-dev): bump @vitejs/plugin-react from 4.3.2 to 4.3.3 in /ui (#3415) Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 4.3.2 to 4.3.3. - [Release notes](https://github.com/vitejs/vite-plugin-react/releases) - [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite-plugin-react/commits/v4.3.3/packages/plugin-react) --- updated-dependencies: - dependency-name: "@vitejs/plugin-react" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ui/package-lock.json | 9 ++++----- ui/package.json | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index a289b49f2..8f08d0619 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -49,7 +49,7 @@ "@types/react-dom": "^17.0.2", "@typescript-eslint/eslint-plugin": "^6.12.0", "@typescript-eslint/parser": "^6.12.0", - "@vitejs/plugin-react": "^4.3.2", + "@vitejs/plugin-react": "^4.3.3", "@vitest/coverage-v8": "^2.1.3", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", @@ -3574,11 +3574,10 @@ "license": "ISC" }, "node_modules/@vitejs/plugin-react": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.2.tgz", - "integrity": "sha512-hieu+o05v4glEBucTcKMK3dlES0OeJlD9YVOAPraVMOInBCwzumaIFiUjr4bHK7NPgnAHgiskUoceKercrN8vg==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.3.tgz", + "integrity": "sha512-NooDe9GpHGqNns1i8XDERg0Vsg5SSYRhRxxyTGogUdkdNt47jal+fbuYi+Yfq6pzRCKXyoPcWisfxE6RIM3GKA==", "dev": true, - "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-transform-react-jsx-self": "^7.24.7", diff --git a/ui/package.json b/ui/package.json index c04e3b41e..7f7895c94 100644 --- a/ui/package.json +++ b/ui/package.json @@ -58,7 +58,7 @@ "@types/react-dom": "^17.0.2", "@typescript-eslint/eslint-plugin": "^6.12.0", "@typescript-eslint/parser": "^6.12.0", - "@vitejs/plugin-react": "^4.3.2", + "@vitejs/plugin-react": "^4.3.3", "@vitest/coverage-v8": "^2.1.3", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", From b86a69567d25f1390dda04972b6cb3d49d804000 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Oct 2024 23:13:39 -0400 Subject: [PATCH 07/22] chore(deps-dev): bump @types/node from 22.7.6 to 22.7.7 in /ui (#3417) Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.7.6 to 22.7.7. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ui/package-lock.json | 9 ++++----- ui/package.json | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index 8f08d0619..1a47bb2f0 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -44,7 +44,7 @@ "@testing-library/react": "^12.1.5", "@testing-library/react-hooks": "^7.0.2", "@testing-library/user-event": "^14.5.2", - "@types/node": "^22.7.5", + "@types/node": "^22.7.7", "@types/react": "^17.0.2", "@types/react-dom": "^17.0.2", "@typescript-eslint/eslint-plugin": "^6.12.0", @@ -3251,11 +3251,10 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.7.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.6.tgz", - "integrity": "sha512-/d7Rnj0/ExXDMcioS78/kf1lMzYk4BZV8MZGTBKzTGZ6/406ukkbYlIsZmMPhcR5KlkunDHQLrtAVmSq7r+mSw==", + "version": "22.7.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.7.tgz", + "integrity": "sha512-SRxCrrg9CL/y54aiMCG3edPKdprgMVGDXjA3gB8UmmBW5TcXzRUYAh8EWzTnSJFAd1rgImPELza+A3bJ+qxz8Q==", "devOptional": true, - "license": "MIT", "dependencies": { "undici-types": "~6.19.2" } diff --git a/ui/package.json b/ui/package.json index 7f7895c94..2bf0ce46b 100644 --- a/ui/package.json +++ b/ui/package.json @@ -53,7 +53,7 @@ "@testing-library/react": "^12.1.5", "@testing-library/react-hooks": "^7.0.2", "@testing-library/user-event": "^14.5.2", - "@types/node": "^22.7.5", + "@types/node": "^22.7.7", "@types/react": "^17.0.2", "@types/react-dom": "^17.0.2", "@typescript-eslint/eslint-plugin": "^6.12.0", From a9b301dfc50910b0ab426cd40a03cd163c111725 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Oct 2024 23:16:21 -0400 Subject: [PATCH 08/22] chore(deps-dev): bump @testing-library/jest-dom in /ui (#3418) Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 6.6.1 to 6.6.2. - [Release notes](https://github.com/testing-library/jest-dom/releases) - [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md) - [Commits](https://github.com/testing-library/jest-dom/compare/v6.6.1...v6.6.2) --- updated-dependencies: - dependency-name: "@testing-library/jest-dom" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ui/package-lock.json | 9 ++++----- ui/package.json | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index 1a47bb2f0..41cb0e89a 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -40,7 +40,7 @@ "uuid": "^10.0.0" }, "devDependencies": { - "@testing-library/jest-dom": "^6.5.0", + "@testing-library/jest-dom": "^6.6.2", "@testing-library/react": "^12.1.5", "@testing-library/react-hooks": "^7.0.2", "@testing-library/user-event": "^14.5.2", @@ -3013,11 +3013,10 @@ } }, "node_modules/@testing-library/jest-dom": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.1.tgz", - "integrity": "sha512-mNYIiAuP4yJwV2zBRQCV7PHoQwbb6/8TfMpPcwSUzcSVDJHWOXt6hjNtIN1v5knDmimYnjJxKhsoVd4LVGIO+w==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.2.tgz", + "integrity": "sha512-P6GJD4yqc9jZLbe98j/EkyQDTPgqftohZF5FBkHY5BUERZmcf4HeO2k0XaefEg329ux2p21i1A1DmyQ1kKw2Jw==", "dev": true, - "license": "MIT", "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", diff --git a/ui/package.json b/ui/package.json index 2bf0ce46b..6fba6618b 100644 --- a/ui/package.json +++ b/ui/package.json @@ -49,7 +49,7 @@ "uuid": "^10.0.0" }, "devDependencies": { - "@testing-library/jest-dom": "^6.5.0", + "@testing-library/jest-dom": "^6.6.2", "@testing-library/react": "^12.1.5", "@testing-library/react-hooks": "^7.0.2", "@testing-library/user-event": "^14.5.2", From 1c48a5575920ca66fc19e08ca0579f4b939ad748 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Oct 2024 23:16:45 -0400 Subject: [PATCH 09/22] chore(deps-dev): bump eslint-plugin-react-refresh in /ui (#3419) Bumps [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) from 0.4.12 to 0.4.13. - [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases) - [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md) - [Commits](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/compare/v0.4.12...v0.4.13) --- updated-dependencies: - dependency-name: eslint-plugin-react-refresh dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ui/package-lock.json | 9 ++++----- ui/package.json | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index 41cb0e89a..0c72c1136 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -56,7 +56,7 @@ "eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-react": "^7.37.1", "eslint-plugin-react-hooks": "^5.0.0", - "eslint-plugin-react-refresh": "^0.4.4", + "eslint-plugin-react-refresh": "^0.4.13", "happy-dom": "^15.7.4", "jsdom": "^25.0.1", "prettier": "^3.3.3", @@ -5359,11 +5359,10 @@ } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.12", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.12.tgz", - "integrity": "sha512-9neVjoGv20FwYtCP6CB1dzR1vr57ZDNOXst21wd2xJ/cTlM2xLq0GWVlSNTdMn/4BtP6cHYBMCSp1wFBJ9jBsg==", + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.13.tgz", + "integrity": "sha512-f1EppwrpJRWmqDTyvAyomFVDYRtrS7iTEqv3nokETnMiMzs2SSTmKRTACce4O2p4jYyowiSMvpdwC/RLcMFhuQ==", "dev": true, - "license": "MIT", "peerDependencies": { "eslint": ">=7" } diff --git a/ui/package.json b/ui/package.json index 6fba6618b..1e62f3691 100644 --- a/ui/package.json +++ b/ui/package.json @@ -65,7 +65,7 @@ "eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-react": "^7.37.1", "eslint-plugin-react-hooks": "^5.0.0", - "eslint-plugin-react-refresh": "^0.4.4", + "eslint-plugin-react-refresh": "^0.4.13", "happy-dom": "^15.7.4", "jsdom": "^25.0.1", "prettier": "^3.3.3", From 15b2dc6b486acb7938b6060aad8449b039a3409b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Oct 2024 23:24:43 -0400 Subject: [PATCH 10/22] chore(deps-dev): bump eslint-plugin-jsx-a11y in /ui (#3416) Bumps [eslint-plugin-jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y) from 6.10.0 to 6.10.1. - [Release notes](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/releases) - [Changelog](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/CHANGELOG.md) - [Commits](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/compare/v6.10.0...v6.10.1) --- updated-dependencies: - dependency-name: eslint-plugin-jsx-a11y dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ui/package-lock.json | 26 ++++++++++++-------------- ui/package.json | 2 +- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index 0c72c1136..5241a35a1 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -53,7 +53,7 @@ "@vitest/coverage-v8": "^2.1.3", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-jsx-a11y": "^6.8.0", + "eslint-plugin-jsx-a11y": "^6.10.1", "eslint-plugin-react": "^7.37.1", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.13", @@ -5248,13 +5248,12 @@ } }, "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.0.tgz", - "integrity": "sha512-ySOHvXX8eSN6zz8Bywacm7CvGNhUtdjvqfQDVe6020TUK34Cywkw7m0KsCCk1Qtm9G1FayfTN1/7mMYnYO2Bhg==", + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.1.tgz", + "integrity": "sha512-zHByM9WTUMnfsDTafGXRiqxp6lFtNoSOWBY6FonVRn3A+BUwN1L/tdBXT40BcBJi0cZjOGTXZ0eD/rTG9fEJ0g==", "dev": true, - "license": "MIT", "dependencies": { - "aria-query": "~5.1.3", + "aria-query": "^5.3.2", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", @@ -5262,14 +5261,14 @@ "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", - "es-iterator-helpers": "^1.0.19", + "es-iterator-helpers": "^1.1.0", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", - "string.prototype.includes": "^2.0.0" + "string.prototype.includes": "^2.0.1" }, "engines": { "node": ">=4.0" @@ -5279,13 +5278,12 @@ } }, "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "deep-equal": "^2.0.5" + "engines": { + "node": ">= 0.4" } }, "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { diff --git a/ui/package.json b/ui/package.json index 1e62f3691..b80aab4b3 100644 --- a/ui/package.json +++ b/ui/package.json @@ -62,7 +62,7 @@ "@vitest/coverage-v8": "^2.1.3", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-jsx-a11y": "^6.8.0", + "eslint-plugin-jsx-a11y": "^6.10.1", "eslint-plugin-react": "^7.37.1", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.13", From 69e2a6d6209487903dd8b6cb0119c9e1cfa2cf7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Sat, 26 Oct 2024 13:28:23 -0400 Subject: [PATCH 11/22] build(netgo): make sure the project is always compiled with `netgo` build tag (#3428) * build(netgo): make sure the project is always compiled with `netgo` build tag * docs(netgo): better comments --- .github/workflows/pipeline.yml | 2 +- .golangci.yml | 4 ++++ Makefile | 4 ++-- conf/buildtags/buildtags.go | 4 ++++ conf/buildtags/netgo.go | 11 +++++++++++ main.go | 8 ++++++++ 6 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 conf/buildtags/buildtags.go create mode 100644 conf/buildtags/netgo.go diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index af936dac0..0684986e7 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -102,7 +102,7 @@ jobs: - name: Test run: | pkg-config --define-prefix --cflags --libs taglib # for debugging - go test -shuffle=on -race -cover ./... -v + go test -shuffle=on -tags netgo -race -cover ./... -v js: name: Test JS code diff --git a/.golangci.yml b/.golangci.yml index ecd3f79cf..8bb134098 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,3 +1,7 @@ +run: + build-tags: + - netgo + linters: enable: - asasalint diff --git a/Makefile b/Makefile index 233113d6f..234309cdb 100644 --- a/Makefile +++ b/Makefile @@ -33,11 +33,11 @@ server: check_go_env buildjs ##@Development Start the backend in development mod .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 -notify ./... + go run github.com/onsi/ginkgo/v2/ginkgo@latest watch -tags netgo -notify ./... .PHONY: watch test: ##@Development Run Go tests - go test -race -shuffle=on ./... + go test -tags netgo -race -shuffle=on ./... .PHONY: test testall: test ##@Development Run Go and JS tests diff --git a/conf/buildtags/buildtags.go b/conf/buildtags/buildtags.go new file mode 100644 index 000000000..5fc125087 --- /dev/null +++ b/conf/buildtags/buildtags.go @@ -0,0 +1,4 @@ +package buildtags + +// This file is left intentionally empty. It is used to make sure the package is not empty, in the case all +// required build tags are disabled. diff --git a/conf/buildtags/netgo.go b/conf/buildtags/netgo.go new file mode 100644 index 000000000..0062ad2bc --- /dev/null +++ b/conf/buildtags/netgo.go @@ -0,0 +1,11 @@ +//go:build netgo + +package buildtags + +// NOTICE: This file was created to force the inclusion of the `netgo` tag when compiling the project. +// If the tag is not included, the compilation will fail because this variable won't be defined, and the `main.go` +// file requires it. + +// Why this tag is required? See https://github.com/navidrome/navidrome/issues/700 + +var NETGO = true diff --git a/main.go b/main.go index 084cf8f2b..65db162ac 100644 --- a/main.go +++ b/main.go @@ -4,8 +4,16 @@ import ( _ "net/http/pprof" //nolint:gosec "github.com/navidrome/navidrome/cmd" + "github.com/navidrome/navidrome/conf/buildtags" ) +//goland:noinspection GoBoolExpressions func main() { + // This import is used to force the inclusion of the `netgo` tag when compiling the project. + // If you get compilation errors like "undefined: buildtags.NETGO", this means you forgot to specify + // the `netgo` build tag when compiling the project. + // To avoid these kind of errors, you should use `make build` to compile the project. + _ = buildtags.NETGO + cmd.Execute() } From 154e13f7c910cbf429daef1c1049c3b75478d9c7 Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Sat, 26 Oct 2024 17:31:45 +0000 Subject: [PATCH 12/22] build: add packages for deb and rpm to release (#3202) * support packing deb/rpm/archlinux * .-. * initial test * fix postinstall, remove execstop * bash -> sh, create toml manually if it doesn't exist (thanks debian) * don't forget that newline * postrm * comments, contrib -> packaging/linux * contrib > packaging in .goreleaser * actually add toml * openrc/sysv templates * add apk. nothing else yet * wait, we have a ntive uninstall * fix: merge errors, move packaging to release * chore: remove old goreleaser conf * ci: remove `release` dependency on `docker push` * ci: fix release version * ci: upload packages * ci: try to fix json file list * ci: replace the json file list with a txt artifact * postremove -> preremove, skip install/remove error * actually do preremove * better preremove * ci: fix * ci: fix? * ci: clean-up * ci: try to change labels and filenames * ci: fix? * ci: fix? * ci: add `make package` target * ci: make labels more readable hope it doesn't break the pipeline again * build: remove alpine and archlinux packages, for now. --------- Co-authored-by: Deluan --- .github/workflows/pipeline.yml | 63 ++++++++++++++++++++++++++++++++-- .gitignore | 1 + Makefile | 5 +++ cmd/svc.go | 57 +++++++++++++++++++++++++++--- contrib/navidrome.service | 4 +-- release/goreleaser.yml | 52 ++++++++++++++++++++++++++++ release/linux/navidrome.toml | 2 ++ release/linux/postinstall.sh | 25 ++++++++++++++ release/linux/preinstall.sh | 6 ++++ release/linux/preremove.sh | 30 ++++++++++++++++ 10 files changed, 235 insertions(+), 10 deletions(-) create mode 100644 release/linux/navidrome.toml create mode 100644 release/linux/postinstall.sh create mode 100755 release/linux/preinstall.sh create mode 100644 release/linux/preremove.sh diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 0684986e7..98dd00a0b 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -332,7 +332,6 @@ jobs: sudo GIT_TAG=$GIT_TAG release/wix/build_msi.sh ${GITHUB_WORKSPACE} amd64 du -h binaries/msi/*.msi - - name: Upload MSI files uses: actions/upload-artifact@v4 with: @@ -341,11 +340,16 @@ jobs: retention-days: 7 release: - name: Release - needs: [build, msi, push-manifest] + name: Package/Release + needs: [build, msi] runs-on: ubuntu-latest + outputs: + package_list: ${{ steps.set-package-list.outputs.package_list }} steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true - uses: actions/download-artifact@v4 with: @@ -366,3 +370,56 @@ jobs: args: "release --clean -f release/goreleaser.yml ${{ env.RELEASE_FLAGS }}" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Remove build artifacts + run: | + ls -l ./dist + rm ./dist/*.tar.gz ./dist/*.zip + + - name: Upload all-packages artifact + uses: actions/upload-artifact@v4 + with: + name: packages + path: dist/navidrome_v* + + - id: set-package-list + name: Export list of generated packages + run: | + cd dist + set +x + ITEMS=$(ls navidrome_v* | sed 's/^navidrome_v[^_]*_linux_//' | jq -R -s -c 'split("\n")[:-1]') + echo $ITEMS + echo "package_list=${ITEMS}" >> $GITHUB_OUTPUT + + upload-packages: + name: Upload Linux PKG + runs-on: ubuntu-latest + needs: [release] + strategy: + matrix: + item: ${{ fromJson(needs.release.outputs.package_list) }} + steps: + - name: Download all-packages artifact + uses: actions/download-artifact@v4 + with: + name: packages + path: ./dist + + - name: Upload all-packages artifact + uses: actions/upload-artifact@v4 + with: + name: navidrome_linux_${{ matrix.item }} + path: dist/navidrome_v*_linux_${{ matrix.item }} + +# delete-artifacts: +# name: Delete unused artifacts +# runs-on: ubuntu-latest +# needs: [upload-packages] +# steps: +# - name: Delete all-packages artifact +# env: +# GH_TOKEN: ${{ github.token }} +# run: | +# for artifact in $(gh api repos/${{ github.repository }}/actions/artifacts | jq -r '.artifacts[] | select(.name | startswith("packages")) | .id'); do +# gh api --method DELETE repos/${{ github.repository }}/actions/artifacts/$artifact +# done \ No newline at end of file diff --git a/.gitignore b/.gitignore index 37999c3f9..e064113bf 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ wiki TODO.md var navidrome.toml +!release/linux/navidrome.toml master.zip testDB cache/* diff --git a/Makefile b/Makefile index 234309cdb..e581bacd6 100644 --- a/Makefile +++ b/Makefile @@ -144,6 +144,11 @@ docker-msi: ##@Cross_Compilation Build MSI installer for Windows @du -h binaries/msi/*.msi .PHONY: docker-msi +package: docker-build ##@Cross_Compilation Create binaries and packages for ALL supported platforms + @if [ -z `which goreleaser` ]; then echo "Please install goreleaser first: https://goreleaser.com/install/"; exit 1; fi + goreleaser release -f release/goreleaser.yml --clean --skip=publish --snapshot +.PHONY: package + get-music: ##@Development Download some free music from Navidrome's demo instance mkdir -p music ( cd music; \ diff --git a/cmd/svc.go b/cmd/svc.go index 219339515..e277bd459 100644 --- a/cmd/svc.go +++ b/cmd/svc.go @@ -20,6 +20,9 @@ var ( service.StatusStopped: "Stopped", service.StatusRunning: "Running", } + + installUser string + workingDirectory string ) func init() { @@ -70,9 +73,11 @@ func (p *svcControl) Stop(service.Service) error { var svcInstance = sync.OnceValue(func() service.Service { options := make(service.KeyValue) - options["Restart"] = "on-success" + options["Restart"] = "on-failure" options["SuccessExitStatus"] = "1 2 8 SIGKILL" options["UserService"] = false + options["LogDirectory"] = conf.Server.DataFolder + options["SystemdScript"] = systemdScript if conf.Server.LogFile != "" { options["LogOutput"] = false } else { @@ -80,12 +85,13 @@ var svcInstance = sync.OnceValue(func() service.Service { options["LogDirectory"] = conf.Server.DataFolder } svcConfig := &service.Config{ + UserName: installUser, Name: "navidrome", DisplayName: "Navidrome", Description: "Your Personal Streaming Service", Dependencies: []string{ - "Requires=", - "After="}, + "After=remote-fs.target network.target", + }, WorkingDirectory: executablePath(), Option: options, } @@ -108,6 +114,10 @@ func runServiceCmd(cmd *cobra.Command, _ []string) { } func executablePath() string { + if workingDirectory != "" { + return workingDirectory + } + ex, err := os.Executable() if err != nil { log.Fatal(err) @@ -141,11 +151,15 @@ func buildInstallCmd() *cobra.Command { println("Service installed. Use 'navidrome svc start' to start it.") } - return &cobra.Command{ + cmd := &cobra.Command{ Use: "install", Short: "Install Navidrome service.", Run: runInstallCmd, } + cmd.Flags().StringVarP(&installUser, "user", "u", "", "user to run service") + cmd.Flags().StringVarP(&workingDirectory, "working-directory", "w", "", "working directory of service") + + return cmd } func buildUninstallCmd() *cobra.Command { @@ -216,3 +230,38 @@ func buildExecuteCmd() *cobra.Command { }, } } + +const systemdScript = `[Unit] +Description={{.Description}} +ConditionFileIsExecutable={{.Path|cmdEscape}} +{{range $i, $dep := .Dependencies}} +{{$dep}} {{end}} + +[Service] +StartLimitInterval=5 +StartLimitBurst=10 +ExecStart={{.Path|cmdEscape}}{{range .Arguments}} {{.|cmd}}{{end}} +{{if .WorkingDirectory}}WorkingDirectory={{.WorkingDirectory|cmdEscape}}{{end}} +{{if .UserName}}User={{.UserName}}{{end}} +{{if .Restart}}Restart={{.Restart}}{{end}} +{{if .SuccessExitStatus}}SuccessExitStatus={{.SuccessExitStatus}}{{end}} +TimeoutStopSec=20 +RestartSec=120 +EnvironmentFile=-/etc/sysconfig/{{.Name}} + +DevicePolicy=closed +NoNewPrivileges=yes +PrivateTmp=yes +ProtectControlGroups=yes +ProtectKernelModules=yes +ProtectKernelTunables=yes +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 +RestrictNamespaces=yes +RestrictRealtime=yes +SystemCallFilter=~@clock @debug @module @mount @obsolete @reboot @setuid @swap +{{if .WorkingDirectory}}ReadWritePaths={{.WorkingDirectory|cmdEscape}}{{end}} +ProtectSystem=full + +[Install] +WantedBy=multi-user.target +` diff --git a/contrib/navidrome.service b/contrib/navidrome.service index 817ab044c..5e6cbedce 100644 --- a/contrib/navidrome.service +++ b/contrib/navidrome.service @@ -11,15 +11,13 @@ WantedBy=multi-user.target User=navidrome Group=navidrome Type=simple -ExecStart=/usr/bin/navidrome +ExecStart=/usr/bin/navidrome --configfile "/etc/navidrome/navidrome.toml" StateDirectory=navidrome WorkingDirectory=/var/lib/navidrome TimeoutStopSec=20 KillMode=process Restart=on-failure -EnvironmentFile=-/etc/sysconfig/navidrome - # See https://www.freedesktop.org/software/systemd/man/systemd.exec.html CapabilityBoundingSet= DevicePolicy=closed diff --git a/release/goreleaser.yml b/release/goreleaser.yml index e353aecfd..59f724138 100644 --- a/release/goreleaser.yml +++ b/release/goreleaser.yml @@ -33,6 +33,58 @@ checksum: snapshot: version_template: "{{ .Tag }}-SNAPSHOT" +nfpms: + - id: navidrome + package_name: navidrome + + homepage: https://navidrome.org + description: |- + 🎧☁ Your Personal Streaming Service + + maintainer: Deluan Quintão + + license: GPL-3.0 + formats: + - deb + - rpm + + dependencies: + - ffmpeg + + suggests: + - mpv + + overrides: + rpm: + dependencies: + - "(ffmpeg or ffmpeg-free)" + + contents: + - src: release/linux/navidrome.toml + dst: /etc/navidrome/navidrome.toml + type: "config|noreplace" + file_info: + mode: 0644 + owner: navidrome + group: navidrome + + - dst: /var/lib/navidrome + type: dir + file_info: + owner: navidrome + group: navidrome + + - dst: /opt/navidrome/music + type: dir + file_info: + owner: navidrome + group: navidrome + + scripts: + preinstall: "release/linux/preinstall.sh" + postinstall: "release/linux/postinstall.sh" + preremove: "release/linux/preremove.sh" + release: draft: true mode: append diff --git a/release/linux/navidrome.toml b/release/linux/navidrome.toml new file mode 100644 index 000000000..e626c8caa --- /dev/null +++ b/release/linux/navidrome.toml @@ -0,0 +1,2 @@ +DataFolder = "/var/lib/navidrome" +MusicFolder = "/opt/navidrome/music" diff --git a/release/linux/postinstall.sh b/release/linux/postinstall.sh new file mode 100644 index 000000000..da43686b0 --- /dev/null +++ b/release/linux/postinstall.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +# It is possible for a user to delete the configuration file in such a way that +# the package manager (in particular, deb) thinks that the file exists, while it is +# no longer on disk. Specifically, doing a `rm /etc/navidrome/navidrome.toml` +# without something like `apt purge navidrome` will result in the system believing that +# the file still exists. In this case, during isntall it will NOT extract the configuration +# file (as to not override it). Since `navidrome service install` depends on this file existing, +# we will create it with the defaults anyway. +if [ ! -f /etc/navidrome/navidrome.toml ]; then + printf "No navidrome.toml detected, creating in postinstall\n" + printf "DataFolder = \"/var/lib/navidrome\"\n" > /etc/navidrome/navidrome.toml + printf "MusicFolder = \"/opt/navidrome/music\"\n" >> /etc/navidrome/navidrome.toml +fi + +postinstall_flag="/var/lib/navidrome/.installed" + +if [ ! -f "$postinstall_flag" ]; then + # The primary reason why this would fail is if the service was already installed AND + # someone manually removed the .installed flag. In this case, ignore the error + navidrome service install --user navidrome --working-directory /var/lib/navidrome --configfile /etc/navidrome/navidrome.toml || : + touch "$postinstall_flag" +fi + + diff --git a/release/linux/preinstall.sh b/release/linux/preinstall.sh new file mode 100755 index 000000000..aa5850e6e --- /dev/null +++ b/release/linux/preinstall.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +if ! getent passwd navidrome > /dev/null 2>&1; then + printf "Creating default Navidrome user\n" + useradd --home-dir /var/lib/navidrome --create-home --system --user-group navidrome +fi diff --git a/release/linux/preremove.sh b/release/linux/preremove.sh new file mode 100644 index 000000000..0dfcafe60 --- /dev/null +++ b/release/linux/preremove.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +action=$1 + +remove() { + postinstall_flag="/var/lib/navidrome/.installed" + + if [ -f "$postinstall_flag" ]; then + # If this fails, ignore it + navidrome service uninstall || : + rm "$postinstall_flag" + + printf "The following may still be present (especially if you have not done a purge):\n" + printf "1. /etc/navidrome/navidrome.toml (configuration file)\n" + printf "2. /var/lib/navidrome (database/cache)\n" + printf "3. /opt/navidrome (default location for music)\n" + printf "4. The Navidrome user (user name navidrome)\n" + fi +} + +case "$action" in + "1" | "upgrade") + # For an upgrade, do nothing + # Leave the service file untouched + # This is relevant for RPM/DEB-based installs + ;; + *) + remove + ;; +esac From fcb5e1b80624ecb8b7e1d80656f799f0cb2b80f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Sat, 26 Oct 2024 14:06:34 -0400 Subject: [PATCH 13/22] fix(server): fix case-insensitive sort order and add indexes to improve performance (#3425) * refactor(server): better sort mappings * refactor(server): simplify GetIndex * fix: recreate tables and indexes using proper collation Also add tests to ensure proper collation * chore: remove unused method * fix: sort expressions * fix: lint errors * fix: cleanup --- ...5533_fix_sort_tags_collation_and_index.sql | 512 ++++++++++++++++++ model/album.go | 1 - model/mediafile.go | 4 +- model/mediafile_test.go | 1 - persistence/album_repository.go | 30 +- persistence/artist_repository.go | 56 +- persistence/artist_repository_test.go | 260 +++++---- persistence/collation_test.go | 122 +++++ persistence/helpers.go | 10 + persistence/helpers_test.go | 19 + persistence/mediafile_repository.go | 40 +- persistence/player_repository.go | 4 +- persistence/playlist_repository.go | 4 +- persistence/playlist_track_repository.go | 14 +- persistence/radio_repository.go | 3 - persistence/share_repository.go | 6 +- persistence/sql_base_repository.go | 29 +- scanner/playlist_importer_test.go | 17 +- 18 files changed, 861 insertions(+), 271 deletions(-) create mode 100644 db/migrations/20241024125533_fix_sort_tags_collation_and_index.sql create mode 100644 persistence/collation_test.go diff --git a/db/migrations/20241024125533_fix_sort_tags_collation_and_index.sql b/db/migrations/20241024125533_fix_sort_tags_collation_and_index.sql new file mode 100644 index 000000000..d2edde0ec --- /dev/null +++ b/db/migrations/20241024125533_fix_sort_tags_collation_and_index.sql @@ -0,0 +1,512 @@ +-- +goose Up +--region Artist Table +create table artist_dg_tmp +( + id varchar(255) not null + primary key, + name varchar(255) default '' not null, + album_count integer default 0 not null, + full_text varchar(255) default '', + song_count integer default 0 not null, + size integer default 0 not null, + biography varchar(255) default '' not null, + small_image_url varchar(255) default '' not null, + medium_image_url varchar(255) default '' not null, + large_image_url varchar(255) default '' not null, + similar_artists varchar(255) default '' not null, + external_url varchar(255) default '' not null, + external_info_updated_at datetime, + order_artist_name varchar collate NOCASE default '' not null, + sort_artist_name varchar collate NOCASE default '' not null, + mbz_artist_id varchar default '' not null +); + +insert into artist_dg_tmp(id, name, album_count, full_text, song_count, size, biography, small_image_url, + medium_image_url, large_image_url, similar_artists, external_url, external_info_updated_at, + order_artist_name, sort_artist_name, mbz_artist_id) +select id, + name, + album_count, + full_text, + song_count, + size, + biography, + small_image_url, + medium_image_url, + large_image_url, + similar_artists, + external_url, + external_info_updated_at, + order_artist_name, + sort_artist_name, + mbz_artist_id +from artist; + +drop table artist; + +alter table artist_dg_tmp + rename to artist; + +create index artist_full_text + on artist (full_text); + +create index artist_name + on artist (name); + +create index artist_order_artist_name + on artist (order_artist_name); + +create index artist_size + on artist (size); + +create index artist_sort_name + on artist (coalesce(nullif(sort_artist_name,''),order_artist_name) collate NOCASE); + +--endregion + +--region Album Table +create table album_dg_tmp +( + id varchar(255) not null + primary key, + name varchar(255) default '' not null, + artist_id varchar(255) default '' not null, + embed_art_path varchar(255) default '' not null, + artist varchar(255) default '' not null, + album_artist varchar(255) default '' not null, + min_year int default 0 not null, + max_year integer default 0 not null, + compilation bool default FALSE not null, + song_count integer default 0 not null, + duration real default 0 not null, + genre varchar(255) default '' not null, + created_at datetime, + updated_at datetime, + full_text varchar(255) default '', + album_artist_id varchar(255) default '', + size integer default 0 not null, + all_artist_ids varchar, + description varchar(255) default '' not null, + small_image_url varchar(255) default '' not null, + medium_image_url varchar(255) default '' not null, + large_image_url varchar(255) default '' not null, + external_url varchar(255) default '' not null, + external_info_updated_at datetime, + date varchar(255) default '' not null, + min_original_year int default 0 not null, + max_original_year int default 0 not null, + original_date varchar(255) default '' not null, + release_date varchar(255) default '' not null, + releases integer default 0 not null, + image_files varchar default '' not null, + order_album_name varchar collate NOCASE default '' not null, + order_album_artist_name varchar collate NOCASE default '' not null, + sort_album_name varchar collate NOCASE default '' not null, + sort_album_artist_name varchar collate NOCASE default '' not null, + catalog_num varchar default '' not null, + comment varchar default '' not null, + paths varchar default '' not null, + mbz_album_id varchar default '' not null, + mbz_album_artist_id varchar default '' not null, + mbz_album_type varchar default '' not null, + mbz_album_comment varchar default '' not null, + discs jsonb default '{}' not null, + library_id integer default 1 not null + references library + on delete cascade +); + +insert into album_dg_tmp(id, name, artist_id, embed_art_path, artist, album_artist, min_year, max_year, compilation, + song_count, duration, genre, created_at, updated_at, full_text, album_artist_id, size, + all_artist_ids, description, small_image_url, medium_image_url, large_image_url, external_url, + external_info_updated_at, date, min_original_year, max_original_year, original_date, + release_date, releases, image_files, order_album_name, order_album_artist_name, + sort_album_name, sort_album_artist_name, catalog_num, comment, paths, + mbz_album_id, mbz_album_artist_id, mbz_album_type, mbz_album_comment, discs, library_id) +select id, + name, + artist_id, + embed_art_path, + artist, + album_artist, + min_year, + max_year, + compilation, + song_count, + duration, + genre, + created_at, + updated_at, + full_text, + album_artist_id, + size, + all_artist_ids, + description, + small_image_url, + medium_image_url, + large_image_url, + external_url, + external_info_updated_at, + date, + min_original_year, + max_original_year, + original_date, + release_date, + releases, + image_files, + order_album_name, + order_album_artist_name, + sort_album_name, + sort_album_artist_name, + catalog_num, + comment, + paths, + mbz_album_id, + mbz_album_artist_id, + mbz_album_type, + mbz_album_comment, + discs, + library_id +from album; + +drop table album; + +alter table album_dg_tmp + rename to album; + +create index album_all_artist_ids + on album (all_artist_ids); + +create index album_alphabetical_by_artist + on album (compilation, order_album_artist_name, order_album_name); + +create index album_artist + on album (artist); + +create index album_artist_album + on album (artist); + +create index album_artist_album_id + on album (album_artist_id); + +create index album_artist_id + on album (artist_id); + +create index album_created_at + on album (created_at); + +create index album_full_text + on album (full_text); + +create index album_genre + on album (genre); + +create index album_max_year + on album (max_year); + +create index album_mbz_album_type + on album (mbz_album_type); + +create index album_min_year + on album (min_year); + +create index album_name + on album (name); + +create index album_order_album_artist_name + on album (order_album_artist_name); + +create index album_order_album_name + on album (order_album_name); + +create index album_size + on album (size); + +create index album_sort_name + on album (coalesce(nullif(sort_album_name,''),order_album_name) collate NOCASE); + +create index album_sort_album_artist_name + on album (coalesce(nullif(sort_album_artist_name,''),order_album_artist_name) collate NOCASE); + +create index album_updated_at + on album (updated_at); +--endregion + +--region Media File Table +create table media_file_dg_tmp +( + id varchar(255) not null + primary key, + path varchar(255) default '' not null, + title varchar(255) default '' not null, + album varchar(255) default '' not null, + artist varchar(255) default '' not null, + artist_id varchar(255) default '' not null, + album_artist varchar(255) default '' not null, + album_id varchar(255) default '' not null, + has_cover_art bool default FALSE not null, + track_number integer default 0 not null, + disc_number integer default 0 not null, + year integer default 0 not null, + size integer default 0 not null, + suffix varchar(255) default '' not null, + duration real default 0 not null, + bit_rate integer default 0 not null, + genre varchar(255) default '' not null, + compilation bool default FALSE not null, + created_at datetime, + updated_at datetime, + full_text varchar(255) default '', + album_artist_id varchar(255) default '', + date varchar(255) default '' not null, + original_year int default 0 not null, + original_date varchar(255) default '' not null, + release_year int default 0 not null, + release_date varchar(255) default '' not null, + order_album_name varchar collate NOCASE default '' not null, + order_album_artist_name varchar collate NOCASE default '' not null, + order_artist_name varchar collate NOCASE default '' not null, + sort_album_name varchar collate NOCASE default '' not null, + sort_artist_name varchar collate NOCASE default '' not null, + sort_album_artist_name varchar collate NOCASE default '' not null, + sort_title varchar collate NOCASE default '' not null, + disc_subtitle varchar default '' not null, + catalog_num varchar default '' not null, + comment varchar default '' not null, + order_title varchar collate NOCASE default '' not null, + mbz_recording_id varchar default '' not null, + mbz_album_id varchar default '' not null, + mbz_artist_id varchar default '' not null, + mbz_album_artist_id varchar default '' not null, + mbz_album_type varchar default '' not null, + mbz_album_comment varchar default '' not null, + mbz_release_track_id varchar default '' not null, + bpm integer default 0 not null, + channels integer default 0 not null, + rg_album_gain real default 0 not null, + rg_album_peak real default 0 not null, + rg_track_gain real default 0 not null, + rg_track_peak real default 0 not null, + lyrics jsonb default '[]' not null, + sample_rate integer default 0 not null, + library_id integer default 1 not null + references library + on delete cascade +); + +insert into media_file_dg_tmp(id, path, title, album, artist, artist_id, album_artist, album_id, has_cover_art, + track_number, disc_number, year, size, suffix, duration, bit_rate, genre, compilation, + created_at, updated_at, full_text, album_artist_id, date, original_year, original_date, + release_year, release_date, order_album_name, order_album_artist_name, order_artist_name, + sort_album_name, sort_artist_name, sort_album_artist_name, sort_title, disc_subtitle, + catalog_num, comment, order_title, mbz_recording_id, mbz_album_id, mbz_artist_id, + mbz_album_artist_id, mbz_album_type, mbz_album_comment, mbz_release_track_id, bpm, + channels, rg_album_gain, rg_album_peak, rg_track_gain, rg_track_peak, lyrics, sample_rate, + library_id) +select id, + path, + title, + album, + artist, + artist_id, + album_artist, + album_id, + has_cover_art, + track_number, + disc_number, + year, + size, + suffix, + duration, + bit_rate, + genre, + compilation, + created_at, + updated_at, + full_text, + album_artist_id, + date, + original_year, + original_date, + release_year, + release_date, + order_album_name, + order_album_artist_name, + order_artist_name, + sort_album_name, + sort_artist_name, + sort_album_artist_name, + sort_title, + disc_subtitle, + catalog_num, + comment, + order_title, + mbz_recording_id, + mbz_album_id, + mbz_artist_id, + mbz_album_artist_id, + mbz_album_type, + mbz_album_comment, + mbz_release_track_id, + bpm, + channels, + rg_album_gain, + rg_album_peak, + rg_track_gain, + rg_track_peak, + lyrics, + sample_rate, + library_id +from media_file; + +drop table media_file; + +alter table media_file_dg_tmp + rename to media_file; + +create index media_file_album_artist + on media_file (album_artist); + +create index media_file_album_id + on media_file (album_id); + +create index media_file_artist + on media_file (artist); + +create index media_file_artist_album_id + on media_file (album_artist_id); + +create index media_file_artist_id + on media_file (artist_id); + +create index media_file_bpm + on media_file (bpm); + +create index media_file_channels + on media_file (channels); + +create index media_file_created_at + on media_file (created_at); + +create index media_file_duration + on media_file (duration); + +create index media_file_full_text + on media_file (full_text); + +create index media_file_genre + on media_file (genre); + +create index media_file_mbz_track_id + on media_file (mbz_recording_id); + +create index media_file_order_album_name + on media_file (order_album_name); + +create index media_file_order_artist_name + on media_file (order_artist_name); + +create index media_file_order_title + on media_file (order_title); + +create index media_file_path + on media_file (path); + +create index media_file_path_nocase + on media_file (path collate NOCASE); + +create index media_file_sample_rate + on media_file (sample_rate); + +create index media_file_sort_title + on media_file (coalesce(nullif(sort_title,''),order_title) collate NOCASE); + +create index media_file_sort_artist_name + on media_file (coalesce(nullif(sort_artist_name,''),order_artist_name) collate NOCASE); + +create index media_file_sort_album_name + on media_file (coalesce(nullif(sort_album_name,''),order_album_name) collate NOCASE); + +create index media_file_title + on media_file (title); + +create index media_file_track_number + on media_file (disc_number, track_number); + +create index media_file_updated_at + on media_file (updated_at); + +create index media_file_year + on media_file (year); + +--endregion + +--region Radio Table +create table radio_dg_tmp +( + id varchar(255) not null + primary key, + name varchar collate NOCASE not null + unique, + stream_url varchar not null, + home_page_url varchar default '' not null, + created_at datetime, + updated_at datetime +); + +insert into radio_dg_tmp(id, name, stream_url, home_page_url, created_at, updated_at) +select id, name, stream_url, home_page_url, created_at, updated_at +from radio; + +drop table radio; + +alter table radio_dg_tmp + rename to radio; + +create index radio_name + on radio(name); +--endregion + +--region users Table +create table user_dg_tmp +( + id varchar(255) not null + primary key, + user_name varchar(255) default '' not null + unique, + name varchar(255) collate NOCASE default '' not null, + email varchar(255) default '' not null, + password varchar(255) default '' not null, + is_admin bool default FALSE not null, + last_login_at datetime, + last_access_at datetime, + created_at datetime not null, + updated_at datetime not null +); + +insert into user_dg_tmp(id, user_name, name, email, password, is_admin, last_login_at, last_access_at, created_at, + updated_at) +select id, + user_name, + name, + email, + password, + is_admin, + last_login_at, + last_access_at, + created_at, + updated_at +from user; + +drop table user; + +alter table user_dg_tmp + rename to user; + +create index user_username_password + on user(user_name collate NOCASE, password); +--endregion + +-- +goose Down +alter table album + add column sort_artist_name varchar default '' not null; diff --git a/model/album.go b/model/album.go index 05fa8b798..76bd9f4fa 100644 --- a/model/album.go +++ b/model/album.go @@ -38,7 +38,6 @@ type Album struct { Discs Discs `structs:"discs" json:"discs,omitempty"` FullText string `structs:"full_text" json:"-"` SortAlbumName string `structs:"sort_album_name" json:"sortAlbumName,omitempty"` - SortArtistName string `structs:"sort_artist_name" json:"sortArtistName,omitempty"` SortAlbumArtistName string `structs:"sort_album_artist_name" json:"sortAlbumArtistName,omitempty"` OrderAlbumName string `structs:"order_album_name" json:"orderAlbumName"` OrderAlbumArtistName string `structs:"order_album_artist_name" json:"orderAlbumArtistName"` diff --git a/model/mediafile.go b/model/mediafile.go index ed7b063e6..8abc685fd 100644 --- a/model/mediafile.go +++ b/model/mediafile.go @@ -140,7 +140,6 @@ func (mfs MediaFiles) ToAlbum() Album { a.AlbumArtist = m.AlbumArtist a.AlbumArtistID = m.AlbumArtistID a.SortAlbumName = m.SortAlbumName - a.SortArtistName = m.SortArtistName a.SortAlbumArtistName = m.SortAlbumArtistName a.OrderAlbumName = m.OrderAlbumName a.OrderAlbumArtistName = m.OrderAlbumArtistName @@ -261,11 +260,10 @@ type MediaFileRepository interface { GetAll(options ...QueryOptions) (MediaFiles, error) Search(q string, offset int, size int) (MediaFiles, error) Delete(id string) error + FindByPaths(paths []string) (MediaFiles, error) // Queries by path to support the scanner, no Annotations or Bookmarks required in the response FindAllByPath(path string) (MediaFiles, error) - FindByPath(path string) (*MediaFile, error) - FindByPaths(paths []string) (MediaFiles, error) FindPathsRecursively(basePath string) ([]string, error) DeleteByPath(path string) (int64, error) diff --git a/model/mediafile_test.go b/model/mediafile_test.go index fb8aa8a14..0aad38384 100644 --- a/model/mediafile_test.go +++ b/model/mediafile_test.go @@ -43,7 +43,6 @@ var _ = Describe("MediaFiles", func() { Expect(album.AlbumArtist).To(Equal("AlbumArtist")) Expect(album.AlbumArtistID).To(Equal("AlbumArtistID")) Expect(album.SortAlbumName).To(Equal("SortAlbumName")) - Expect(album.SortArtistName).To(Equal("SortArtistName")) Expect(album.SortAlbumArtistName).To(Equal("SortAlbumArtistName")) Expect(album.OrderAlbumName).To(Equal("OrderAlbumName")) Expect(album.OrderAlbumArtistName).To(Equal("OrderAlbumArtistName")) diff --git a/persistence/album_repository.go b/persistence/album_repository.go index 2ee94973b..cfae5c19e 100644 --- a/persistence/album_repository.go +++ b/persistence/album_repository.go @@ -69,27 +69,15 @@ func NewAlbumRepository(ctx context.Context, db dbx.Builder) model.AlbumReposito "has_rating": hasRatingFilter, "genre_id": eqFilter, }) - if conf.Server.PreferSortTags { - r.sortMappings = map[string]string{ - "name": "COALESCE(NULLIF(sort_album_name,''),order_album_name)", - "artist": "compilation asc, COALESCE(NULLIF(sort_album_artist_name,''),order_album_artist_name) asc, COALESCE(NULLIF(sort_album_name,''),order_album_name) asc", - "album_artist": "compilation asc, COALESCE(NULLIF(sort_album_artist_name,''),order_album_artist_name) asc, COALESCE(NULLIF(sort_album_name,''),order_album_name) asc", - "max_year": "coalesce(nullif(original_date,''), cast(max_year as text)), release_date, name, COALESCE(NULLIF(sort_album_name,''),order_album_name) asc", - "random": "random", - "recently_added": recentlyAddedSort(), - "starred_at": "starred, starred_at", - } - } else { - r.sortMappings = map[string]string{ - "name": "order_album_name asc, order_album_artist_name asc", - "artist": "compilation asc, order_album_artist_name asc, order_album_name asc", - "album_artist": "compilation asc, order_album_artist_name asc, order_album_name asc", - "max_year": "coalesce(nullif(original_date,''), cast(max_year as text)), release_date, name, order_album_name asc", - "random": "random", - "recently_added": recentlyAddedSort(), - "starred_at": "starred, starred_at", - } - } + r.setSortMappings(map[string]string{ + "name": "order_album_name, order_album_artist_name", + "artist": "compilation, order_album_artist_name, order_album_name", + "album_artist": "compilation, order_album_artist_name, order_album_name", + "max_year": "coalesce(nullif(original_date,''), cast(max_year as text)), release_date, name", + "random": "random", + "recently_added": recentlyAddedSort(), + "starred_at": "starred, starred_at", + }) return r } diff --git a/persistence/artist_repository.go b/persistence/artist_repository.go index af7c30138..c176ac7a9 100644 --- a/persistence/artist_repository.go +++ b/persistence/artist_repository.go @@ -5,7 +5,7 @@ import ( "context" "fmt" "net/url" - "sort" + "slices" "strings" . "github.com/Masterminds/squirrel" @@ -15,7 +15,7 @@ import ( "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/utils" - "github.com/navidrome/navidrome/utils/str" + "github.com/navidrome/navidrome/utils/slice" "github.com/pocketbase/dbx" ) @@ -67,17 +67,10 @@ func NewArtistRepository(ctx context.Context, db dbx.Builder) model.ArtistReposi "starred": booleanFilter, "genre_id": eqFilter, }) - if conf.Server.PreferSortTags { - r.sortMappings = map[string]string{ - "name": "COALESCE(NULLIF(sort_artist_name,''),order_artist_name)", - "starred_at": "starred, starred_at", - } - } else { - r.sortMappings = map[string]string{ - "name": "order_artist_name", - "starred_at": "starred, starred_at", - } - } + r.setSortMappings(map[string]string{ + "name": "order_artist_name", + "starred_at": "starred, starred_at", + }) return r } @@ -143,15 +136,14 @@ func (r *artistRepository) toModels(dba []dbArtist) model.Artists { return res } -func (r *artistRepository) getIndexKey(a *model.Artist) string { - source := a.Name +func (r *artistRepository) getIndexKey(a model.Artist) string { + source := a.OrderArtistName if conf.Server.PreferSortTags { - source = cmp.Or(a.SortArtistName, a.OrderArtistName, source) + source = cmp.Or(a.SortArtistName, a.OrderArtistName) } - name := strings.ToLower(str.RemoveArticle(source)) + name := strings.ToLower(source) for k, v := range r.indexGroups { - key := strings.ToLower(k) - if strings.HasPrefix(name, key) { + if strings.HasPrefix(name, strings.ToLower(k)) { return v } } @@ -160,32 +152,16 @@ func (r *artistRepository) getIndexKey(a *model.Artist) string { // TODO Cache the index (recalculate when there are changes to the DB) func (r *artistRepository) GetIndex() (model.ArtistIndexes, error) { - sortColumn := "order_artist_name" - if conf.Server.PreferSortTags { - sortColumn = "sort_artist_name, order_artist_name" - } - all, err := r.GetAll(model.QueryOptions{Sort: sortColumn}) + artists, err := r.GetAll(model.QueryOptions{Sort: "name"}) if err != nil { return nil, err } - - fullIdx := make(map[string]*model.ArtistIndex) - for i := range all { - a := all[i] - ax := r.getIndexKey(&a) - idx, ok := fullIdx[ax] - if !ok { - idx = &model.ArtistIndex{ID: ax} - fullIdx[ax] = idx - } - idx.Artists = append(idx.Artists, a) - } var result model.ArtistIndexes - for _, idx := range fullIdx { - result = append(result, *idx) + for k, v := range slice.Group(artists, r.getIndexKey) { + result = append(result, model.ArtistIndex{ID: k, Artists: v}) } - sort.Slice(result, func(i, j int) bool { - return result[i].ID < result[j].ID + slices.SortFunc(result, func(a, b model.ArtistIndex) int { + return cmp.Compare(a.ID, b.ID) }) return result, nil } diff --git a/persistence/artist_repository_test.go b/persistence/artist_repository_test.go index 31b035cec..0de468c02 100644 --- a/persistence/artist_repository_test.go +++ b/persistence/artist_repository_test.go @@ -5,6 +5,7 @@ import ( "github.com/fatih/structs" "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/conf/configtest" "github.com/navidrome/navidrome/db" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" @@ -46,163 +47,146 @@ var _ = Describe("ArtistRepository", func() { }) Describe("GetIndexKey", func() { + // Note: OrderArtistName should never be empty, so we don't need to test for that r := artistRepository{indexGroups: utils.ParseIndexGroups(conf.Server.IndexGroups)} - It("returns the index key when PreferSortTags is true and SortArtistName is not empty", func() { - conf.Server.PreferSortTags = true - a := model.Artist{SortArtistName: "Foo", OrderArtistName: "Bar", Name: "Qux"} - idx := GetIndexKey(&r, &a) // defines export_test.go - Expect(idx).To(Equal("F")) - - a = model.Artist{SortArtistName: "foo", OrderArtistName: "Bar", Name: "Qux"} - idx = GetIndexKey(&r, &a) - Expect(idx).To(Equal("F")) + When("PreferSortTags is false", func() { + BeforeEach(func() { + DeferCleanup(configtest.SetupConfig) + conf.Server.PreferSortTags = false + }) + It("returns the OrderArtistName key is SortArtistName is empty", func() { + conf.Server.PreferSortTags = false + a := model.Artist{SortArtistName: "", OrderArtistName: "Bar", Name: "Qux"} + idx := GetIndexKey(&r, a) + Expect(idx).To(Equal("B")) + }) + It("returns the OrderArtistName key even if SortArtistName is not empty", func() { + a := model.Artist{SortArtistName: "Foo", OrderArtistName: "Bar", Name: "Qux"} + idx := GetIndexKey(&r, a) + Expect(idx).To(Equal("B")) + }) }) - - It("returns the index key when PreferSortTags is true, SortArtistName is empty and OrderArtistName is not empty", func() { - conf.Server.PreferSortTags = true - a := model.Artist{SortArtistName: "", OrderArtistName: "Bar", Name: "Qux"} - idx := GetIndexKey(&r, &a) - Expect(idx).To(Equal("B")) - - a = model.Artist{SortArtistName: "", OrderArtistName: "bar", Name: "Qux"} - idx = GetIndexKey(&r, &a) - Expect(idx).To(Equal("B")) - }) - - It("returns the index key when PreferSortTags is true, both SortArtistName, OrderArtistName are empty", func() { - conf.Server.PreferSortTags = true - a := model.Artist{SortArtistName: "", OrderArtistName: "", Name: "Qux"} - idx := GetIndexKey(&r, &a) - Expect(idx).To(Equal("Q")) - - a = model.Artist{SortArtistName: "", OrderArtistName: "", Name: "qux"} - idx = GetIndexKey(&r, &a) - Expect(idx).To(Equal("Q")) - }) - - It("returns the index key when PreferSortTags is false and SortArtistName is not empty", func() { - conf.Server.PreferSortTags = false - a := model.Artist{SortArtistName: "Foo", OrderArtistName: "Bar", Name: "Qux"} - idx := GetIndexKey(&r, &a) - Expect(idx).To(Equal("Q")) - }) - - It("returns the index key when PreferSortTags is true, SortArtistName is empty and OrderArtistName is not empty", func() { - conf.Server.PreferSortTags = false - a := model.Artist{SortArtistName: "", OrderArtistName: "Bar", Name: "Qux"} - idx := GetIndexKey(&r, &a) - Expect(idx).To(Equal("Q")) - }) - - It("returns the index key when PreferSortTags is true, both sort_artist_name, order_artist_name are empty", func() { - conf.Server.PreferSortTags = false - a := model.Artist{SortArtistName: "", OrderArtistName: "", Name: "Qux"} - idx := GetIndexKey(&r, &a) - Expect(idx).To(Equal("Q")) - - a = model.Artist{SortArtistName: "", OrderArtistName: "", Name: "qux"} - idx = GetIndexKey(&r, &a) - Expect(idx).To(Equal("Q")) + When("PreferSortTags is true", func() { + BeforeEach(func() { + DeferCleanup(configtest.SetupConfig) + conf.Server.PreferSortTags = true + }) + It("returns the SortArtistName key if it is not empty", func() { + a := model.Artist{SortArtistName: "Foo", OrderArtistName: "Bar", Name: "Qux"} + idx := GetIndexKey(&r, a) + Expect(idx).To(Equal("F")) + }) + It("returns the OrderArtistName key if SortArtistName is empty", func() { + a := model.Artist{SortArtistName: "", OrderArtistName: "Bar", Name: "Qux"} + idx := GetIndexKey(&r, a) + Expect(idx).To(Equal("B")) + }) }) }) Describe("GetIndex", func() { - It("returns the index when PreferSortTags is true and SortArtistName is not empty", func() { - conf.Server.PreferSortTags = true + When("PreferSortTags is true", func() { + BeforeEach(func() { + DeferCleanup(configtest.SetupConfig) + conf.Server.PreferSortTags = true + }) + It("returns the index when SortArtistName is not empty", func() { + artistBeatles.SortArtistName = "Foo" + er := repo.Put(&artistBeatles) + Expect(er).To(BeNil()) - artistBeatles.SortArtistName = "Foo" - er := repo.Put(&artistBeatles) - Expect(er).To(BeNil()) - - idx, err := repo.GetIndex() - Expect(err).To(BeNil()) - Expect(idx).To(Equal(model.ArtistIndexes{ - { - ID: "F", - Artists: model.Artists{ - artistBeatles, + idx, err := repo.GetIndex() + Expect(err).To(BeNil()) + Expect(idx).To(Equal(model.ArtistIndexes{ + { + ID: "F", + Artists: model.Artists{ + artistBeatles, + }, }, - }, - { - ID: "K", - Artists: model.Artists{ - artistKraftwerk, + { + ID: "K", + Artists: model.Artists{ + artistKraftwerk, + }, }, - }, - })) + })) - artistBeatles.SortArtistName = "" - er = repo.Put(&artistBeatles) - Expect(er).To(BeNil()) + artistBeatles.SortArtistName = "" + er = repo.Put(&artistBeatles) + Expect(er).To(BeNil()) + }) + + It("returns the index when SortArtistName is empty", func() { + idx, err := repo.GetIndex() + Expect(err).To(BeNil()) + Expect(idx).To(Equal(model.ArtistIndexes{ + { + ID: "B", + Artists: model.Artists{ + artistBeatles, + }, + }, + { + ID: "K", + Artists: model.Artists{ + artistKraftwerk, + }, + }, + })) + }) }) - It("returns the index when PreferSortTags is true and SortArtistName is empty", func() { - conf.Server.PreferSortTags = true - idx, err := repo.GetIndex() - Expect(err).To(BeNil()) - Expect(idx).To(Equal(model.ArtistIndexes{ - { - ID: "B", - Artists: model.Artists{ - artistBeatles, - }, - }, - { - ID: "K", - Artists: model.Artists{ - artistKraftwerk, - }, - }, - })) - }) + When("PreferSortTags is false", func() { + BeforeEach(func() { + DeferCleanup(configtest.SetupConfig) + conf.Server.PreferSortTags = false + }) + It("returns the index when SortArtistName is not empty", func() { + artistBeatles.SortArtistName = "Foo" + er := repo.Put(&artistBeatles) + Expect(er).To(BeNil()) - It("returns the index when PreferSortTags is false and SortArtistName is not empty", func() { - conf.Server.PreferSortTags = false - - artistBeatles.SortArtistName = "Foo" - er := repo.Put(&artistBeatles) - Expect(er).To(BeNil()) - - idx, err := repo.GetIndex() - Expect(err).To(BeNil()) - Expect(idx).To(Equal(model.ArtistIndexes{ - { - ID: "B", - Artists: model.Artists{ - artistBeatles, + idx, err := repo.GetIndex() + Expect(err).To(BeNil()) + Expect(idx).To(Equal(model.ArtistIndexes{ + { + ID: "B", + Artists: model.Artists{ + artistBeatles, + }, }, - }, - { - ID: "K", - Artists: model.Artists{ - artistKraftwerk, + { + ID: "K", + Artists: model.Artists{ + artistKraftwerk, + }, }, - }, - })) + })) - artistBeatles.SortArtistName = "" - er = repo.Put(&artistBeatles) - Expect(er).To(BeNil()) - }) + artistBeatles.SortArtistName = "" + er = repo.Put(&artistBeatles) + Expect(er).To(BeNil()) + }) - It("returns the index when PreferSortTags is false and SortArtistName is empty", func() { - conf.Server.PreferSortTags = false - idx, err := repo.GetIndex() - Expect(err).To(BeNil()) - Expect(idx).To(Equal(model.ArtistIndexes{ - { - ID: "B", - Artists: model.Artists{ - artistBeatles, + It("returns the index when SortArtistName is empty", func() { + idx, err := repo.GetIndex() + Expect(err).To(BeNil()) + Expect(idx).To(Equal(model.ArtistIndexes{ + { + ID: "B", + Artists: model.Artists{ + artistBeatles, + }, }, - }, - { - ID: "K", - Artists: model.Artists{ - artistKraftwerk, + { + ID: "K", + Artists: model.Artists{ + artistKraftwerk, + }, }, - }, - })) + })) + }) }) }) diff --git a/persistence/collation_test.go b/persistence/collation_test.go new file mode 100644 index 000000000..87e582670 --- /dev/null +++ b/persistence/collation_test.go @@ -0,0 +1,122 @@ +package persistence + +import ( + "database/sql" + "errors" + "fmt" + "regexp" + + "github.com/navidrome/navidrome/db" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// When creating migrations that change existing columns, it is easy to miss the original collation of a column. +// These tests enforce that the required collation of the columns and indexes in the database are kept in place. +// This is important to ensure that the database can perform fast case-insensitive searches and sorts. +var _ = Describe("Collation", func() { + conn := db.Db().ReadDB() + DescribeTable("Column collation", + func(table, column string) { + Expect(checkCollation(conn, table, column)).To(Succeed()) + }, + Entry("artist.order_artist_name", "artist", "order_artist_name"), + Entry("artist.sort_artist_name", "artist", "sort_artist_name"), + Entry("album.order_album_name", "album", "order_album_name"), + Entry("album.order_album_artist_name", "album", "order_album_artist_name"), + Entry("album.sort_album_name", "album", "sort_album_name"), + Entry("album.sort_album_artist_name", "album", "sort_album_artist_name"), + Entry("media_file.order_title", "media_file", "order_title"), + Entry("media_file.order_album_name", "media_file", "order_album_name"), + Entry("media_file.order_artist_name", "media_file", "order_artist_name"), + Entry("media_file.sort_title", "media_file", "sort_title"), + Entry("media_file.sort_album_name", "media_file", "sort_album_name"), + Entry("media_file.sort_artist_name", "media_file", "sort_artist_name"), + Entry("radio.name", "radio", "name"), + Entry("user.name", "user", "name"), + ) + + DescribeTable("Index collation", + func(table, column string) { + Expect(checkIndexUsage(conn, table, column)).To(Succeed()) + }, + Entry("artist.order_artist_name", "artist", "order_artist_name collate nocase"), + Entry("artist.sort_artist_name", "artist", "coalesce(nullif(sort_artist_name,''),order_artist_name) collate nocase"), + Entry("album.order_album_name", "album", "order_album_name collate nocase"), + Entry("album.order_album_artist_name", "album", "order_album_artist_name collate nocase"), + Entry("album.sort_album_name", "album", "coalesce(nullif(sort_album_name,''),order_album_name) collate nocase"), + Entry("album.sort_album_artist_name", "album", "coalesce(nullif(sort_album_artist_name,''),order_album_artist_name) collate nocase"), + Entry("media_file.order_title", "media_file", "order_title collate nocase"), + Entry("media_file.order_album_name", "media_file", "order_album_name collate nocase"), + Entry("media_file.order_artist_name", "media_file", "order_artist_name collate nocase"), + Entry("media_file.sort_title", "media_file", "coalesce(nullif(sort_title,''),order_title) collate nocase"), + Entry("media_file.sort_album_name", "media_file", "coalesce(nullif(sort_album_name,''),order_album_name) collate nocase"), + Entry("media_file.sort_artist_name", "media_file", "coalesce(nullif(sort_artist_name,''),order_artist_name) collate nocase"), + Entry("media_file.path", "media_file", "path collate nocase"), + Entry("radio.name", "radio", "name collate nocase"), + Entry("user.user_name", "user", "user_name collate nocase"), + ) +}) + +func checkIndexUsage(conn *sql.DB, table string, column string) error { + rows, err := conn.Query(fmt.Sprintf(` +explain query plan select * from %[1]s +where %[2]s = 'test' +order by %[2]s`, table, column)) + if err != nil { + return err + } + defer rows.Close() + + err = rows.Err() + if err != nil { + return err + } + + if rows.Next() { + var dummy int + var detail string + err = rows.Scan(&dummy, &dummy, &dummy, &detail) + if err != nil { + return nil + } + if ok, _ := regexp.MatchString("SEARCH.*USING INDEX", detail); ok { + return nil + } else { + return fmt.Errorf("INDEX for '%s' not used: %s", column, detail) + } + } + return errors.New("no rows returned") +} + +func checkCollation(conn *sql.DB, table string, column string) error { + rows, err := conn.Query(fmt.Sprintf("SELECT sql FROM sqlite_master WHERE type='table' AND tbl_name='%s'", table)) + if err != nil { + return err + } + defer rows.Close() + + err = rows.Err() + if err != nil { + return err + } + + if rows.Next() { + var res string + err = rows.Scan(&res) + if err != nil { + return err + } + re := regexp.MustCompile(fmt.Sprintf(`(?i)\b%s\b.*varchar`, column)) + if !re.MatchString(res) { + return fmt.Errorf("column '%s' not found in table '%s'", column, table) + } + re = regexp.MustCompile(fmt.Sprintf(`(?i)\b%s\b.*collate\s+NOCASE`, column)) + if re.MatchString(res) { + return nil + } + } else { + return fmt.Errorf("table '%s' not found", table) + } + return fmt.Errorf("column '%s' in table '%s' does not have NOCASE collation", column, table) +} diff --git a/persistence/helpers.go b/persistence/helpers.go index e6edf61d6..72ef0abcc 100644 --- a/persistence/helpers.go +++ b/persistence/helpers.go @@ -81,3 +81,13 @@ func (e existsCond) ToSql() (string, []interface{}, error) { } return sql, args, err } + +var sortOrderRegex = regexp.MustCompile(`order_([a-z_]+)`) + +// Convert the order_* columns to an expression using sort_* columns. Example: +// sort_album_name -> (coalesce(nullif(sort_album_name,”),order_album_name) collate nocase) +// It finds order column names anywhere in the substring +func mapSortOrder(order string) string { + order = strings.ToLower(order) + return sortOrderRegex.ReplaceAllString(order, "(coalesce(nullif(sort_$1,''),order_$1) collate nocase)") +} diff --git a/persistence/helpers_test.go b/persistence/helpers_test.go index f080d1e81..3061c7229 100644 --- a/persistence/helpers_test.go +++ b/persistence/helpers_test.go @@ -83,4 +83,23 @@ var _ = Describe("Helpers", func() { Expect(err).To(BeNil()) }) }) + + Describe("mapSortOrder", func() { + It("does not change the sort string if there are no order columns", func() { + sort := "album_name asc" + mapped := mapSortOrder(sort) + Expect(mapped).To(Equal(sort)) + }) + It("changes order columns to sort expression", func() { + sort := "ORDER_ALBUM_NAME asc" + mapped := mapSortOrder(sort) + Expect(mapped).To(Equal("(coalesce(nullif(sort_album_name,''),order_album_name) collate nocase) asc")) + }) + It("changes multiple order columns to sort expressions", func() { + sort := "compilation, order_title asc, order_album_artist_name desc, year desc" + mapped := mapSortOrder(sort) + Expect(mapped).To(Equal(`compilation, (coalesce(nullif(sort_title,''),order_title) collate nocase) asc,` + + ` (coalesce(nullif(sort_album_artist_name,''),order_album_artist_name) collate nocase) desc, year desc`)) + }) + }) }) diff --git a/persistence/mediafile_repository.go b/persistence/mediafile_repository.go index 952e989bc..134b44cbc 100644 --- a/persistence/mediafile_repository.go +++ b/persistence/mediafile_repository.go @@ -10,7 +10,6 @@ import ( . "github.com/Masterminds/squirrel" "github.com/deluan/rest" - "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/pocketbase/dbx" @@ -31,25 +30,14 @@ func NewMediaFileRepository(ctx context.Context, db dbx.Builder) *mediaFileRepos "starred": booleanFilter, "genre_id": eqFilter, }) - if conf.Server.PreferSortTags { - r.sortMappings = map[string]string{ - "title": "COALESCE(NULLIF(sort_title,''),order_title)", - "artist": "COALESCE(NULLIF(sort_artist_name,''),order_artist_name) asc, COALESCE(NULLIF(sort_album_name,''),order_album_name) asc, release_date asc, disc_number asc, track_number asc", - "album": "COALESCE(NULLIF(sort_album_name,''),order_album_name) asc, release_date asc, disc_number asc, track_number asc, COALESCE(NULLIF(sort_artist_name,''),order_artist_name) asc, COALESCE(NULLIF(sort_title,''),title) asc", - "random": "random", - "created_at": "media_file.created_at", - "starred_at": "starred, starred_at", - } - } else { - r.sortMappings = map[string]string{ - "title": "order_title", - "artist": "order_artist_name asc, order_album_name asc, release_date asc, disc_number asc, track_number asc", - "album": "order_album_name asc, release_date asc, disc_number asc, track_number asc, order_artist_name asc, title asc", - "random": "random", - "created_at": "media_file.created_at", - "starred_at": "starred, starred_at", - } - } + r.setSortMappings(map[string]string{ + "title": "order_title", + "artist": "order_artist_name, order_album_name, release_date, disc_number, track_number", + "album": "order_album_name, release_date, disc_number, track_number, order_artist_name, title", + "random": "random", + "created_at": "media_file.created_at", + "starred_at": "starred, starred_at", + }) return r } @@ -115,18 +103,6 @@ func (r *mediaFileRepository) GetAll(options ...model.QueryOptions) (model.Media return res, err } -func (r *mediaFileRepository) FindByPath(path string) (*model.MediaFile, error) { - sel := r.newSelect().Columns("*").Where(Like{"path": path}) - var res model.MediaFiles - if err := r.queryAll(sel, &res); err != nil { - return nil, err - } - if len(res) == 0 { - return nil, model.ErrNotFound - } - return &res[0], nil -} - func (r *mediaFileRepository) FindByPaths(paths []string) (model.MediaFiles, error) { sel := r.newSelect().Columns("*").Where(Eq{"path collate nocase": paths}) var res model.MediaFiles diff --git a/persistence/player_repository.go b/persistence/player_repository.go index 98c53d141..d3f13e772 100644 --- a/persistence/player_repository.go +++ b/persistence/player_repository.go @@ -21,9 +21,9 @@ func NewPlayerRepository(ctx context.Context, db dbx.Builder) model.PlayerReposi r.registerModel(&model.Player{}, map[string]filterFunc{ "name": containsFilter("player.name"), }) - r.sortMappings = map[string]string{ + r.setSortMappings(map[string]string{ "user_name": "username", //TODO rename all user_name and userName to username - } + }) return r } diff --git a/persistence/playlist_repository.go b/persistence/playlist_repository.go index f6eca0657..47efff5fe 100644 --- a/persistence/playlist_repository.go +++ b/persistence/playlist_repository.go @@ -55,9 +55,9 @@ func NewPlaylistRepository(ctx context.Context, db dbx.Builder) model.PlaylistRe "q": playlistFilter, "smart": smartPlaylistFilter, }) - r.sortMappings = map[string]string{ + r.setSortMappings(map[string]string{ "owner_name": "owner_name", - } + }) return r } diff --git a/persistence/playlist_track_repository.go b/persistence/playlist_track_repository.go index 615366cc7..c04dd0f8d 100644 --- a/persistence/playlist_track_repository.go +++ b/persistence/playlist_track_repository.go @@ -5,7 +5,6 @@ import ( . "github.com/Masterminds/squirrel" "github.com/deluan/rest" - "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/utils/slice" @@ -26,18 +25,13 @@ func (r *playlistRepository) Tracks(playlistId string, refreshSmartPlaylist bool p.db = r.db p.tableName = "playlist_tracks" p.registerModel(&model.PlaylistTrack{}, nil) - p.sortMappings = map[string]string{ + p.setSortMappings(map[string]string{ "id": "playlist_tracks.id", - "artist": "order_artist_name asc", - "album": "order_album_name asc, order_album_artist_name asc", + "artist": "order_artist_name", + "album": "order_album_name, order_album_artist_name", "title": "order_title", "duration": "duration", // To make sure the field will be whitelisted - } - if conf.Server.PreferSortTags { - p.sortMappings["artist"] = "COALESCE(NULLIF(sort_artist_name,''),order_artist_name) asc" - p.sortMappings["album"] = "COALESCE(NULLIF(sort_album_name,''),order_album_name)" - p.sortMappings["title"] = "COALESCE(NULLIF(sort_title,''),title)" - } + }) pls, err := r.Get(playlistId) if err != nil { diff --git a/persistence/radio_repository.go b/persistence/radio_repository.go index 781c50241..a63c1eaf8 100644 --- a/persistence/radio_repository.go +++ b/persistence/radio_repository.go @@ -24,9 +24,6 @@ func NewRadioRepository(ctx context.Context, db dbx.Builder) model.RadioReposito r.registerModel(&model.Radio{}, map[string]filterFunc{ "name": containsFilter("name"), }) - r.sortMappings = map[string]string{ - "name": "(name collate nocase), name", - } return r } diff --git a/persistence/share_repository.go b/persistence/share_repository.go index 6b84243df..692e61403 100644 --- a/persistence/share_repository.go +++ b/persistence/share_repository.go @@ -23,10 +23,10 @@ func NewShareRepository(ctx context.Context, db dbx.Builder) model.ShareReposito r := &shareRepository{} r.ctx = ctx r.db = db - r.registerModel(&model.Share{}, map[string]filterFunc{}) - r.sortMappings = map[string]string{ + r.registerModel(&model.Share{}, nil) + r.setSortMappings(map[string]string{ "username": "username", - } + }) return r } diff --git a/persistence/sql_base_repository.go b/persistence/sql_base_repository.go index f5c33978b..9b8aefbad 100644 --- a/persistence/sql_base_repository.go +++ b/persistence/sql_base_repository.go @@ -27,17 +27,20 @@ import ( // - Call registerModel with the model instance and any possible filters. // - If the model has a different table name than the default (lowercase of the model name), it should be set manually // using the tableName field. -// - Sort mappings should be set in the sortMappings field. If the sort field is not in the map, it will be used as is. +// - Sort mappings must be set with setSortMappings method. If a sort field is not in the map, it will be used as the name of the column. // // All fields in filters and sortMappings must be in snake_case. Only sorts and filters based on real field names or // defined in the mappings will be allowed. type sqlRepository struct { - ctx context.Context - tableName string - db dbx.Builder - sortMappings map[string]string + ctx context.Context + tableName string + db dbx.Builder + + // Do not set these fields manually, they are set by the registerModel method filterMappings map[string]filterFunc isFieldWhiteListed fieldWhiteListedFunc + // Do not set this field manually, it is set by the setSortMappings method + sortMappings map[string]string } const invalidUserId = "-1" @@ -68,6 +71,22 @@ func (r *sqlRepository) registerModel(instance any, filters map[string]filterFun r.filterMappings = filters } +// setSortMappings sets the mappings for the sort fields. If the sort field is not in the map, it will be used as is. +// +// If PreferSortTags is enabled, it will map the order fields to the corresponding sort expression, +// which gives precedence to sort tags. +// Ex: order_title => (coalesce(nullif(sort_title,”),order_title) collate nocase) +// To avoid performance issues, indexes should be created for these sort expressions +func (r *sqlRepository) setSortMappings(mappings map[string]string) { + if conf.Server.PreferSortTags { + for k, v := range mappings { + v = mapSortOrder(v) + mappings[k] = v + } + } + r.sortMappings = mappings +} + func (r sqlRepository) getTableName() string { return r.tableName } diff --git a/scanner/playlist_importer_test.go b/scanner/playlist_importer_test.go index d33e5250d..8b3ae9d5d 100644 --- a/scanner/playlist_importer_test.go +++ b/scanner/playlist_importer_test.go @@ -2,6 +2,7 @@ package scanner import ( "context" + "strconv" "github.com/navidrome/navidrome/core" "github.com/navidrome/navidrome/core/artwork" @@ -70,18 +71,14 @@ type mockedMediaFile struct { model.MediaFileRepository } -func (r *mockedMediaFile) FindByPath(s string) (*model.MediaFile, error) { - return &model.MediaFile{ - ID: "123", - Path: s, - }, nil -} - func (r *mockedMediaFile) FindByPaths(paths []string) (model.MediaFiles, error) { var mfs model.MediaFiles - for _, path := range paths { - mf, _ := r.FindByPath(path) - mfs = append(mfs, *mf) + for i, path := range paths { + mf := model.MediaFile{ + ID: strconv.Itoa(i), + Path: path, + } + mfs = append(mfs, mf) } return mfs, nil } From 3fda7445b02bc54302295d9a78bf604fdac641bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Sun, 27 Oct 2024 21:59:22 -0400 Subject: [PATCH 14/22] fix(server): try to find proper embedded front cover - only for vorbis comments for now (#3348) * fix(artwork): get the first image from vorbis comments, not the last. fixes #3254 This uses a fork for now. * fix(artwork): prioritize getting embedded types that are listed as "front" covers * fix: cleanup --- core/artwork/reader_album.go | 2 +- core/artwork/reader_mediafile.go | 2 +- core/artwork/sources.go | 35 +++++++++++++++++++++++++++++--- go.mod | 3 +++ go.sum | 4 ++-- 5 files changed, 39 insertions(+), 7 deletions(-) diff --git a/core/artwork/reader_album.go b/core/artwork/reader_album.go index dbf1b9fac..9d17e18fc 100644 --- a/core/artwork/reader_album.go +++ b/core/artwork/reader_album.go @@ -63,7 +63,7 @@ func (a *albumArtworkReader) fromCoverArtPriority(ctx context.Context, ffmpeg ff pattern = strings.TrimSpace(pattern) switch { case pattern == "embedded": - ff = append(ff, fromTag(a.album.EmbedArtPath), fromFFmpegTag(ctx, ffmpeg, a.album.EmbedArtPath)) + ff = append(ff, fromTag(ctx, a.album.EmbedArtPath), fromFFmpegTag(ctx, ffmpeg, a.album.EmbedArtPath)) case pattern == "external": ff = append(ff, fromAlbumExternalSource(ctx, a.album, a.em)) case a.album.ImageFiles != "": diff --git a/core/artwork/reader_mediafile.go b/core/artwork/reader_mediafile.go index b86fe7154..72e8a165b 100644 --- a/core/artwork/reader_mediafile.go +++ b/core/artwork/reader_mediafile.go @@ -55,7 +55,7 @@ func (a *mediafileArtworkReader) Reader(ctx context.Context) (io.ReadCloser, str var ff []sourceFunc if a.mediafile.CoverArtID().Kind == model.KindMediaFileArtwork { ff = []sourceFunc{ - fromTag(a.mediafile.Path), + fromTag(ctx, a.mediafile.Path), fromFFmpegTag(ctx, a.a.ffmpeg, a.mediafile.Path), } } diff --git a/core/artwork/sources.go b/core/artwork/sources.go index 625f0e420..30dd88d27 100644 --- a/core/artwork/sources.go +++ b/core/artwork/sources.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" "reflect" + "regexp" "runtime" "strings" "time" @@ -79,7 +80,14 @@ func fromExternalFile(ctx context.Context, files string, pattern string) sourceF } } -func fromTag(path string) sourceFunc { +// These regexes are used to match the picture type in the file, in the order they are listed. +var picTypeRegexes = []*regexp.Regexp{ + regexp.MustCompile(`(?i).*cover.*front.*|.*front.*cover.*`), + regexp.MustCompile(`(?i).*front.*`), + regexp.MustCompile(`(?i).*cover.*`), +} + +func fromTag(ctx context.Context, path string) sourceFunc { return func() (io.ReadCloser, string, error) { if path == "" { return nil, "", nil @@ -95,10 +103,31 @@ func fromTag(path string) sourceFunc { return nil, "", err } - picture := m.Picture() - if picture == nil { + types := m.PictureTypes() + if len(types) == 0 { return nil, "", fmt.Errorf("no embedded image found in %s", path) } + + var picture *tag.Picture + for _, regex := range picTypeRegexes { + for _, t := range types { + if regex.MatchString(t) { + log.Trace(ctx, "Found embedded image", "type", t, "path", path) + picture = m.Pictures(t) + break + } + } + if picture != nil { + break + } + } + if picture == nil { + log.Trace(ctx, "Could not find a front image. Getting the first one", "type", types[0], "path", path) + picture = m.Picture() + } + if picture == nil { + return nil, "", fmt.Errorf("could not load embedded image from %s", path) + } return io.NopCloser(bytes.NewReader(picture.Data)), path, nil } } diff --git a/go.mod b/go.mod index 4bc783e18..78eb617ee 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,9 @@ module github.com/navidrome/navidrome go 1.23.2 +// 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 + require ( github.com/Masterminds/squirrel v1.5.4 github.com/RaveNoX/go-jsoncommentstrip v1.0.0 diff --git a/go.sum b/go.sum index 9d3d60eb0..ca6be4776 100644 --- a/go.sum +++ b/go.sum @@ -24,10 +24,10 @@ 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/deluan/tag v0.0.0-20241002021117-dfe5e6ea396d h1:x/R3+oPEjnisl1zBx2f2v7Gf6f11l0N0JoD6BkwcJyA= +github.com/deluan/tag v0.0.0-20241002021117-dfe5e6ea396d/go.mod h1:apkPC/CR3s48O2D7Y++n1XWEpgPNNCjXYga3PPbJe2E= github.com/dexterlb/mpvipc v0.0.0-20241005113212-7cdefca0e933 h1:r4hxcT6GBIA/j8Ox4OXI5MNgMKfR+9plcAWYi1OnmOg= github.com/dexterlb/mpvipc v0.0.0-20241005113212-7cdefca0e933/go.mod h1:RkQWLNITKkXHLP7LXxZSgEq+uFWU25M5qW7qfEhL9Wc= -github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 h1:OtSeLS5y0Uy01jaKK4mA/WVIYtpzVm63vLVAPzJXigg= -github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8/go.mod h1:apkPC/CR3s48O2D7Y++n1XWEpgPNNCjXYga3PPbJe2E= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/djherbis/atime v1.1.0 h1:rgwVbP/5by8BvvjBNrbh64Qz33idKT3pSnMSJsxhi0g= From 075a7e26404fab17ad4d67a81f5b38cf28b22d2d Mon Sep 17 00:00:00 2001 From: Deluan Date: Mon, 28 Oct 2024 22:02:52 -0400 Subject: [PATCH 15/22] chore(deps): bump go dependencies --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 78eb617ee..1ddf6e6da 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( github.com/jellydator/ttlcache/v3 v3.3.0 github.com/kardianos/service v1.2.2 github.com/kr/pretty v0.3.1 - github.com/lestrrat-go/jwx/v2 v2.1.1 + github.com/lestrrat-go/jwx/v2 v2.1.2 github.com/matoous/go-nanoid/v2 v2.1.0 github.com/mattn/go-sqlite3 v1.14.24 github.com/mattn/go-zglob v0.0.6 @@ -47,7 +47,7 @@ require ( github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 - github.com/unrolled/secure v1.16.0 + github.com/unrolled/secure v1.17.0 github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c golang.org/x/image v0.21.0 diff --git a/go.sum b/go.sum index ca6be4776..a0b39a303 100644 --- a/go.sum +++ b/go.sum @@ -117,8 +117,8 @@ github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCG github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx/v2 v2.1.1 h1:Y2ltVl8J6izLYFs54BVcpXLv5msSW4o8eXwnzZLI32E= -github.com/lestrrat-go/jwx/v2 v2.1.1/go.mod h1:4LvZg7oxu6Q5VJwn7Mk/UwooNRnTHUpXBj2C4j3HNx0= +github.com/lestrrat-go/jwx/v2 v2.1.2 h1:6poete4MPsO8+LAEVhpdrNI4Xp2xdiafgl2RD89moBc= +github.com/lestrrat-go/jwx/v2 v2.1.2/go.mod h1:pO+Gz9whn7MPdbsqSJzG8TlEpMZCwQDXnFJ+zsUVh8Y= 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/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -213,8 +213,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/unrolled/secure v1.16.0 h1:XgdAsS/Zl50ZfZPRJK6WpicFttfrsFYFd0+ONDBJubU= -github.com/unrolled/secure v1.16.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= +github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU= +github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= From cf69df877a713608a2a67644dbbe9e05cebdb43b Mon Sep 17 00:00:00 2001 From: Deluan Date: Mon, 28 Oct 2024 22:07:36 -0400 Subject: [PATCH 16/22] chore(deps): bump js dependencies --- ui/package-lock.json | 227 ++++++++++++++++++++----------------------- ui/package.json | 14 +-- 2 files changed, 113 insertions(+), 128 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index 5241a35a1..ed13b41ed 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -37,32 +37,32 @@ "react-router-dom": "^5.3.4", "redux": "^4.2.0", "redux-saga": "^1.1.3", - "uuid": "^10.0.0" + "uuid": "^11.0.2" }, "devDependencies": { "@testing-library/jest-dom": "^6.6.2", "@testing-library/react": "^12.1.5", "@testing-library/react-hooks": "^7.0.2", "@testing-library/user-event": "^14.5.2", - "@types/node": "^22.7.7", + "@types/node": "^22.8.2", "@types/react": "^17.0.2", "@types/react-dom": "^17.0.2", "@typescript-eslint/eslint-plugin": "^6.12.0", "@typescript-eslint/parser": "^6.12.0", "@vitejs/plugin-react": "^4.3.3", - "@vitest/coverage-v8": "^2.1.3", + "@vitest/coverage-v8": "^2.1.4", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-jsx-a11y": "^6.10.1", - "eslint-plugin-react": "^7.37.1", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-react": "^7.37.2", "eslint-plugin-react-hooks": "^5.0.0", - "eslint-plugin-react-refresh": "^0.4.13", + "eslint-plugin-react-refresh": "^0.4.14", "happy-dom": "^15.7.4", "jsdom": "^25.0.1", "prettier": "^3.3.3", "ra-test": "^3.19.12", "typescript": "^5.6.3", - "vite": "^5.4.9", + "vite": "^5.4.10", "vite-plugin-pwa": "^0.20.5", "vitest": "^2.1.1" } @@ -3250,12 +3250,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.7.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.7.tgz", - "integrity": "sha512-SRxCrrg9CL/y54aiMCG3edPKdprgMVGDXjA3gB8UmmBW5TcXzRUYAh8EWzTnSJFAd1rgImPELza+A3bJ+qxz8Q==", + "version": "22.8.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.2.tgz", + "integrity": "sha512-NzaRNFV+FZkvK/KLCsNdTvID0SThyrs5SHB6tsD/lajr22FGC73N2QeDPM2wHtVde8mgcXuSsHQkH5cX1pbPLw==", "devOptional": true, "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.19.8" } }, "node_modules/@types/prop-types": { @@ -3591,21 +3591,20 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.3.tgz", - "integrity": "sha512-2OJ3c7UPoFSmBZwqD2VEkUw6A/tzPF0LmW0ZZhhB8PFxuc+9IBG/FaSM+RLEenc7ljzFvGN+G0nGQoZnh7sy2A==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.4.tgz", + "integrity": "sha512-FPKQuJfR6VTfcNMcGpqInmtJuVXFSCd9HQltYncfR01AzXhLucMEtQ5SinPdZxsT5x/5BK7I5qFJ5/ApGCmyTQ==", "dev": true, - "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.6", + "debug": "^4.3.7", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.11", - "magicast": "^0.3.4", + "magic-string": "^0.30.12", + "magicast": "^0.3.5", "std-env": "^3.7.0", "test-exclude": "^7.0.1", "tinyrainbow": "^1.2.0" @@ -3614,8 +3613,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "2.1.3", - "vitest": "2.1.3" + "@vitest/browser": "2.1.4", + "vitest": "2.1.4" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -3624,15 +3623,14 @@ } }, "node_modules/@vitest/expect": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.3.tgz", - "integrity": "sha512-SNBoPubeCJhZ48agjXruCI57DvxcsivVDdWz+SSsmjTT4QN/DfHk3zB/xKsJqMs26bLZ/pNRLnCf0j679i0uWQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.4.tgz", + "integrity": "sha512-DOETT0Oh1avie/D/o2sgMHGrzYUFFo3zqESB2Hn70z6QB1HrS2IQ9z5DfyTqU8sg4Bpu13zZe9V4+UTNQlUeQA==", "dev": true, - "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.3", - "@vitest/utils": "2.1.3", - "chai": "^5.1.1", + "@vitest/spy": "2.1.4", + "@vitest/utils": "2.1.4", + "chai": "^5.1.2", "tinyrainbow": "^1.2.0" }, "funding": { @@ -3640,22 +3638,20 @@ } }, "node_modules/@vitest/mocker": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.3.tgz", - "integrity": "sha512-eSpdY/eJDuOvuTA3ASzCjdithHa+GIF1L4PqtEELl6Qa3XafdMLBpBlZCIUCX2J+Q6sNmjmxtosAG62fK4BlqQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.4.tgz", + "integrity": "sha512-Ky/O1Lc0QBbutJdW0rqLeFNbuLEyS+mIPiNdlVlp2/yhJ0SbyYqObS5IHdhferJud8MbbwMnexg4jordE5cCoQ==", "dev": true, - "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.3", + "@vitest/spy": "2.1.4", "estree-walker": "^3.0.3", - "magic-string": "^0.30.11" + "magic-string": "^0.30.12" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/spy": "2.1.3", - "msw": "^2.3.5", + "msw": "^2.4.9", "vite": "^5.0.0" }, "peerDependenciesMeta": { @@ -3668,11 +3664,10 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.3.tgz", - "integrity": "sha512-XH1XdtoLZCpqV59KRbPrIhFCOO0hErxrQCMcvnQete3Vibb9UeIOX02uFPfVn3Z9ZXsq78etlfyhnkmIZSzIwQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.4.tgz", + "integrity": "sha512-L95zIAkEuTDbUX1IsjRl+vyBSLh3PwLLgKpghl37aCK9Jvw0iP+wKwIFhfjdUtA2myLgjrG6VU6JCFLv8q/3Ww==", "dev": true, - "license": "MIT", "dependencies": { "tinyrainbow": "^1.2.0" }, @@ -3681,13 +3676,12 @@ } }, "node_modules/@vitest/runner": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.3.tgz", - "integrity": "sha512-JGzpWqmFJ4fq5ZKHtVO3Xuy1iF2rHGV4d/pdzgkYHm1+gOzNZtqjvyiaDGJytRyMU54qkxpNzCx+PErzJ1/JqQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.4.tgz", + "integrity": "sha512-sKRautINI9XICAMl2bjxQM8VfCMTB0EbsBc/EDFA57V6UQevEKY/TOPOF5nzcvCALltiLfXWbq4MaAwWx/YxIA==", "dev": true, - "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.3", + "@vitest/utils": "2.1.4", "pathe": "^1.1.2" }, "funding": { @@ -3695,14 +3689,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.3.tgz", - "integrity": "sha512-qWC2mWc7VAXmjAkEKxrScWHWFyCQx/cmiZtuGqMi+WwqQJ2iURsVY4ZfAK6dVo6K2smKRU6l3BPwqEBvhnpQGg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.4.tgz", + "integrity": "sha512-3Kab14fn/5QZRog5BPj6Rs8dc4B+mim27XaKWFWHWA87R56AKjHTGcBFKpvZKDzC4u5Wd0w/qKsUIio3KzWW4Q==", "dev": true, - "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.3", - "magic-string": "^0.30.11", + "@vitest/pretty-format": "2.1.4", + "magic-string": "^0.30.12", "pathe": "^1.1.2" }, "funding": { @@ -3710,27 +3703,25 @@ } }, "node_modules/@vitest/spy": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.3.tgz", - "integrity": "sha512-Nb2UzbcUswzeSP7JksMDaqsI43Sj5+Kry6ry6jQJT4b5gAK+NS9NED6mDb8FlMRCX8m5guaHCDZmqYMMWRy5nQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.4.tgz", + "integrity": "sha512-4JOxa+UAizJgpZfaCPKK2smq9d8mmjZVPMt2kOsg/R8QkoRzydHH1qHxIYNvr1zlEaFj4SXiaaJWxq/LPLKaLg==", "dev": true, - "license": "MIT", "dependencies": { - "tinyspy": "^3.0.0" + "tinyspy": "^3.0.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.3.tgz", - "integrity": "sha512-xpiVfDSg1RrYT0tX6czgerkpcKFmFOF/gCr30+Mve5V2kewCy4Prn1/NDMSRwaSmT7PRaOF83wu+bEtsY1wrvA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.4.tgz", + "integrity": "sha512-MXDnZn0Awl2S86PSNIim5PWXgIAx8CIkzu35mBdSApUip6RFOGXBCf3YFyeEu8n1IHk4bWD46DeYFu9mQlFIRg==", "dev": true, - "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.3", - "loupe": "^3.1.1", + "@vitest/pretty-format": "2.1.4", + "loupe": "^3.1.2", "tinyrainbow": "^1.2.0" }, "funding": { @@ -3985,7 +3976,6 @@ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" } @@ -4224,7 +4214,6 @@ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } @@ -4280,11 +4269,10 @@ "license": "CC-BY-4.0" }, "node_modules/chai": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", - "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", + "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==", "dev": true, - "license": "MIT", "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", @@ -4318,7 +4306,6 @@ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 16" } @@ -4660,7 +4647,6 @@ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } @@ -5248,9 +5234,9 @@ } }, "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.1.tgz", - "integrity": "sha512-zHByM9WTUMnfsDTafGXRiqxp6lFtNoSOWBY6FonVRn3A+BUwN1L/tdBXT40BcBJi0cZjOGTXZ0eD/rTG9fEJ0g==", + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", "dev": true, "dependencies": { "aria-query": "^5.3.2", @@ -5261,7 +5247,6 @@ "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", - "es-iterator-helpers": "^1.1.0", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", @@ -5311,18 +5296,17 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.37.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz", - "integrity": "sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg==", + "version": "7.37.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.2.tgz", + "integrity": "sha512-EsTAnj9fLVr/GZleBLFbj/sSuXeWmp1eXIN60ceYnZveqEaUCyW4X+Vh4WTdUhCkW4xutXYqTXCUSyqD4rB75w==", "dev": true, - "license": "MIT", "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.2", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.19", + "es-iterator-helpers": "^1.1.0", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", @@ -5357,9 +5341,9 @@ } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.13", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.13.tgz", - "integrity": "sha512-f1EppwrpJRWmqDTyvAyomFVDYRtrS7iTEqv3nokETnMiMzs2SSTmKRTACce4O2p4jYyowiSMvpdwC/RLcMFhuQ==", + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.14.tgz", + "integrity": "sha512-aXvzCTK7ZBv1e7fahFuR3Z/fyQQSIQ711yPgYRj+Oj64tyTgO4iQIDmYXDBqvSWQ/FA4OSCsXOStlF+noU0/NA==", "dev": true, "peerDependencies": { "eslint": ">=7" @@ -5554,7 +5538,6 @@ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, - "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" } @@ -5581,6 +5564,15 @@ "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==", "license": "BSD-3-Clause" }, + "node_modules/expect-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", + "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -7389,8 +7381,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/lru-cache": { "version": "5.1.1", @@ -7910,15 +7901,13 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/pathval": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 14.16" } @@ -9982,7 +9971,6 @@ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -10359,16 +10347,15 @@ } }, "node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.2.tgz", + "integrity": "sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], - "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/value-equal": { @@ -10378,11 +10365,10 @@ "license": "MIT" }, "node_modules/vite": { - "version": "5.4.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.9.tgz", - "integrity": "sha512-20OVpJHh0PAM0oSOELa5GaZNWeDjcAvQjGXy2Uyr+Tp+/D2/Hdz6NLgpJLsarPTA2QJ6v8mX2P1ZfbsSKvdMkg==", + "version": "5.4.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.10.tgz", + "integrity": "sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==", "dev": true, - "license": "MIT", "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -10438,14 +10424,13 @@ } }, "node_modules/vite-node": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.3.tgz", - "integrity": "sha512-I1JadzO+xYX887S39Do+paRePCKoiDrWRRjp9kkG5he0t7RXNvPAJPCQSJqbGN4uCrFFeS3Kj3sLqY8NMYBEdA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.4.tgz", + "integrity": "sha512-kqa9v+oi4HwkG6g8ufRnb5AeplcRw8jUF6/7/Qz1qRQOXHImG8YnLbB+LLszENwFnoBl9xIf9nVdCFzNd7GQEg==", "dev": true, - "license": "MIT", "dependencies": { "cac": "^6.7.14", - "debug": "^4.3.6", + "debug": "^4.3.7", "pathe": "^1.1.2", "vite": "^5.0.0" }, @@ -10491,30 +10476,30 @@ } }, "node_modules/vitest": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.3.tgz", - "integrity": "sha512-Zrxbg/WiIvUP2uEzelDNTXmEMJXuzJ1kCpbDvaKByFA9MNeO95V+7r/3ti0qzJzrxdyuUw5VduN7k+D3VmVOSA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.4.tgz", + "integrity": "sha512-eDjxbVAJw1UJJCHr5xr/xM86Zx+YxIEXGAR+bmnEID7z9qWfoxpHw0zdobz+TQAFOLT+nEXz3+gx6nUJ7RgmlQ==", "dev": true, - "license": "MIT", "dependencies": { - "@vitest/expect": "2.1.3", - "@vitest/mocker": "2.1.3", - "@vitest/pretty-format": "^2.1.3", - "@vitest/runner": "2.1.3", - "@vitest/snapshot": "2.1.3", - "@vitest/spy": "2.1.3", - "@vitest/utils": "2.1.3", - "chai": "^5.1.1", - "debug": "^4.3.6", - "magic-string": "^0.30.11", + "@vitest/expect": "2.1.4", + "@vitest/mocker": "2.1.4", + "@vitest/pretty-format": "^2.1.4", + "@vitest/runner": "2.1.4", + "@vitest/snapshot": "2.1.4", + "@vitest/spy": "2.1.4", + "@vitest/utils": "2.1.4", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", "pathe": "^1.1.2", "std-env": "^3.7.0", "tinybench": "^2.9.0", - "tinyexec": "^0.3.0", - "tinypool": "^1.0.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "2.1.3", + "vite-node": "2.1.4", "why-is-node-running": "^2.3.0" }, "bin": { @@ -10529,8 +10514,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.3", - "@vitest/ui": "2.1.3", + "@vitest/browser": "2.1.4", + "@vitest/ui": "2.1.4", "happy-dom": "*", "jsdom": "*" }, diff --git a/ui/package.json b/ui/package.json index b80aab4b3..459841041 100644 --- a/ui/package.json +++ b/ui/package.json @@ -46,32 +46,32 @@ "react-router-dom": "^5.3.4", "redux": "^4.2.0", "redux-saga": "^1.1.3", - "uuid": "^10.0.0" + "uuid": "^11.0.2" }, "devDependencies": { "@testing-library/jest-dom": "^6.6.2", "@testing-library/react": "^12.1.5", "@testing-library/react-hooks": "^7.0.2", "@testing-library/user-event": "^14.5.2", - "@types/node": "^22.7.7", + "@types/node": "^22.8.2", "@types/react": "^17.0.2", "@types/react-dom": "^17.0.2", "@typescript-eslint/eslint-plugin": "^6.12.0", "@typescript-eslint/parser": "^6.12.0", "@vitejs/plugin-react": "^4.3.3", - "@vitest/coverage-v8": "^2.1.3", + "@vitest/coverage-v8": "^2.1.4", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-jsx-a11y": "^6.10.1", - "eslint-plugin-react": "^7.37.1", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-react": "^7.37.2", "eslint-plugin-react-hooks": "^5.0.0", - "eslint-plugin-react-refresh": "^0.4.13", + "eslint-plugin-react-refresh": "^0.4.14", "happy-dom": "^15.7.4", "jsdom": "^25.0.1", "prettier": "^3.3.3", "ra-test": "^3.19.12", "typescript": "^5.6.3", - "vite": "^5.4.9", + "vite": "^5.4.10", "vite-plugin-pwa": "^0.20.5", "vitest": "^2.1.1" }, From faed2ea8d765f5b152ee3ad43e07dcf90f6bb51f Mon Sep 17 00:00:00 2001 From: Deluan Date: Mon, 4 Nov 2024 12:24:05 -0500 Subject: [PATCH 17/22] chore(deps): bump Go dependencies Signed-off-by: Deluan --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 1ddf6e6da..3b7820512 100644 --- a/go.mod +++ b/go.mod @@ -36,8 +36,8 @@ require ( github.com/mattn/go-zglob v0.0.6 github.com/microcosm-cc/bluemonday v1.0.27 github.com/mileusna/useragent v1.3.5 - github.com/onsi/ginkgo/v2 v2.20.2 - github.com/onsi/gomega v1.34.2 + github.com/onsi/ginkgo/v2 v2.21.0 + github.com/onsi/gomega v1.35.1 github.com/pelletier/go-toml/v2 v2.2.3 github.com/pocketbase/dbx v1.10.1 github.com/pressly/goose/v3 v3.22.1 @@ -68,7 +68,7 @@ require ( github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/goccy/go-json v0.10.3 // indirect github.com/google/go-cmp v0.6.0 // indirect - github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 // indirect + github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // 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 @@ -106,7 +106,7 @@ require ( golang.org/x/net v0.30.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/tools v0.26.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect + google.golang.org/protobuf v1.35.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect ) diff --git a/go.sum b/go.sum index a0b39a303..3764affbf 100644 --- a/go.sum +++ b/go.sum @@ -67,8 +67,8 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= -github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 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= @@ -143,10 +143,10 @@ 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/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= -github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= -github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= -github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -299,8 +299,8 @@ golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From 6ff7ab52f4a62e65d59b6c6eb9b9f47eb976c43b Mon Sep 17 00:00:00 2001 From: Deluan Date: Mon, 4 Nov 2024 12:27:55 -0500 Subject: [PATCH 18/22] chore(deps): bump JS dependencies Signed-off-by: Deluan --- ui/package-lock.json | 25 ++++++++++++------------- ui/package.json | 6 +++--- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index ed13b41ed..9c854f865 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -40,11 +40,11 @@ "uuid": "^11.0.2" }, "devDependencies": { - "@testing-library/jest-dom": "^6.6.2", + "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^12.1.5", "@testing-library/react-hooks": "^7.0.2", "@testing-library/user-event": "^14.5.2", - "@types/node": "^22.8.2", + "@types/node": "^22.8.7", "@types/react": "^17.0.2", "@types/react-dom": "^17.0.2", "@typescript-eslint/eslint-plugin": "^6.12.0", @@ -57,7 +57,7 @@ "eslint-plugin-react": "^7.37.2", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.14", - "happy-dom": "^15.7.4", + "happy-dom": "^15.8.3", "jsdom": "^25.0.1", "prettier": "^3.3.3", "ra-test": "^3.19.12", @@ -3013,9 +3013,9 @@ } }, "node_modules/@testing-library/jest-dom": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.2.tgz", - "integrity": "sha512-P6GJD4yqc9jZLbe98j/EkyQDTPgqftohZF5FBkHY5BUERZmcf4HeO2k0XaefEg329ux2p21i1A1DmyQ1kKw2Jw==", + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", + "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", "dev": true, "dependencies": { "@adobe/css-tools": "^4.4.0", @@ -3250,9 +3250,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.8.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.2.tgz", - "integrity": "sha512-NzaRNFV+FZkvK/KLCsNdTvID0SThyrs5SHB6tsD/lajr22FGC73N2QeDPM2wHtVde8mgcXuSsHQkH5cX1pbPLw==", + "version": "22.8.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.7.tgz", + "integrity": "sha512-LidcG+2UeYIWcMuMUpBKOnryBWG/rnmOHQR5apjn8myTQcx3rinFRn7DcIFhMnS0PPFSC6OafdIKEad0lj6U0Q==", "devOptional": true, "dependencies": { "undici-types": "~6.19.8" @@ -6077,11 +6077,10 @@ "license": "MIT" }, "node_modules/happy-dom": { - "version": "15.7.4", - "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-15.7.4.tgz", - "integrity": "sha512-r1vadDYGMtsHAAsqhDuk4IpPvr6N8MGKy5ntBo7tSdim+pWDxus2PNqOcOt8LuDZ4t3KJHE+gCuzupcx/GKnyQ==", + "version": "15.8.3", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-15.8.3.tgz", + "integrity": "sha512-YR9nUWN/T2bH7pPLEYMhTp4DQExPH+mC4KulJDgimCb+FY3Er0Vp6SOOcBXrNfMTri3lAk9uSZqUTG2hgZOYwg==", "dev": true, - "license": "MIT", "dependencies": { "entities": "^4.5.0", "webidl-conversions": "^7.0.0", diff --git a/ui/package.json b/ui/package.json index 459841041..359687d83 100644 --- a/ui/package.json +++ b/ui/package.json @@ -49,11 +49,11 @@ "uuid": "^11.0.2" }, "devDependencies": { - "@testing-library/jest-dom": "^6.6.2", + "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^12.1.5", "@testing-library/react-hooks": "^7.0.2", "@testing-library/user-event": "^14.5.2", - "@types/node": "^22.8.2", + "@types/node": "^22.8.7", "@types/react": "^17.0.2", "@types/react-dom": "^17.0.2", "@typescript-eslint/eslint-plugin": "^6.12.0", @@ -66,7 +66,7 @@ "eslint-plugin-react": "^7.37.2", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.14", - "happy-dom": "^15.7.4", + "happy-dom": "^15.8.3", "jsdom": "^25.0.1", "prettier": "^3.3.3", "ra-test": "^3.19.12", From 6c6223f2f9db2c8c253e0d40a192e3519c9037d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Tue, 5 Nov 2024 20:39:05 -0500 Subject: [PATCH 19/22] fix: forcing transcoding when client does not specify transcoding options (#3455) * fix: wip Signed-off-by: Deluan * fix: revert #3227 It is not respecting the server configured transcoding for the player Signed-off-by: Deluan --------- Signed-off-by: Deluan --- core/media_streamer.go | 97 +++++++++++++--------------- core/media_streamer_Internal_test.go | 9 +-- 2 files changed, 49 insertions(+), 57 deletions(-) diff --git a/core/media_streamer.go b/core/media_streamer.go index 6cfff6f17..40326c34a 100644 --- a/core/media_streamer.go +++ b/core/media_streamer.go @@ -1,7 +1,6 @@ package core import ( - "cmp" "context" "fmt" "io" @@ -128,64 +127,56 @@ func (s *Stream) EstimatedContentLength() int { return int(s.mf.Duration * float32(s.bitRate) / 8 * 1024) } -// selectTranscodingOptions selects the appropriate transcoding options based on the requested format and bitrate. -// If the requested format is "raw" or matches the media file's suffix and the requested bitrate is 0, it returns the -// original format and bitrate. -// Otherwise, it determines the format and bitrate using determineFormatAndBitRate and findTranscoding functions. -// -// NOTE: It is easier to follow the tests in core/media_streamer_internal_test.go to understand the different scenarios. -func selectTranscodingOptions(ctx context.Context, ds model.DataStore, mf *model.MediaFile, reqFormat string, reqBitRate int) (string, int) { - if reqFormat == "raw" || reqFormat == mf.Suffix && reqBitRate == 0 { - return "raw", mf.BitRate +// TODO This function deserves some love (refactoring) +func selectTranscodingOptions(ctx context.Context, ds model.DataStore, mf *model.MediaFile, reqFormat string, reqBitRate int) (format string, bitRate int) { + format = "raw" + if reqFormat == "raw" { + return format, 0 } - - format, bitRate := determineFormatAndBitRate(ctx, mf.BitRate, reqFormat, reqBitRate) - if format == "" && bitRate == 0 { - return "raw", 0 + if reqFormat == mf.Suffix && reqBitRate == 0 { + bitRate = mf.BitRate + return format, bitRate } - - return findTranscoding(ctx, ds, mf, format, bitRate) -} - -// determineFormatAndBitRate determines the format and bitrate for transcoding based on the requested format and bitrate. -// If the requested format is not empty, it returns the requested format and bitrate. -// Otherwise, it checks for default transcoding settings from the context or server configuration. -func determineFormatAndBitRate(ctx context.Context, srcBitRate int, reqFormat string, reqBitRate int) (string, int) { + trc, hasDefault := request.TranscodingFrom(ctx) + var cFormat string + var cBitRate int if reqFormat != "" { - return reqFormat, reqBitRate - } - - format, bitRate := "", 0 - if trc, hasDefault := request.TranscodingFrom(ctx); hasDefault { - format = trc.TargetFormat - bitRate = trc.DefaultBitRate - - if p, ok := request.PlayerFrom(ctx); ok && p.MaxBitRate > 0 && p.MaxBitRate < bitRate { - bitRate = p.MaxBitRate + cFormat = reqFormat + } else { + if hasDefault { + cFormat = trc.TargetFormat + cBitRate = trc.DefaultBitRate + if p, ok := request.PlayerFrom(ctx); ok { + cBitRate = p.MaxBitRate + } + } else if reqBitRate > 0 && reqBitRate < mf.BitRate && conf.Server.DefaultDownsamplingFormat != "" { + // If no format is specified and no transcoding associated to the player, but a bitrate is specified, + // and there is no transcoding set for the player, we use the default downsampling format. + // But only if the requested bitRate is lower than the original bitRate. + log.Debug("Default Downsampling", "Using default downsampling format", conf.Server.DefaultDownsamplingFormat) + cFormat = conf.Server.DefaultDownsamplingFormat } - } else if reqBitRate > 0 && reqBitRate < srcBitRate && conf.Server.DefaultDownsamplingFormat != "" { - // If no format is specified and no transcoding associated to the player, but a bitrate is specified, - // and there is no transcoding set for the player, we use the default downsampling format. - // But only if the requested bitRate is lower than the original bitRate. - log.Debug(ctx, "Using default downsampling format", "format", conf.Server.DefaultDownsamplingFormat) - format = conf.Server.DefaultDownsamplingFormat } - - return format, cmp.Or(reqBitRate, bitRate) -} - -// findTranscoding finds the appropriate transcoding settings for the given format and bitrate. -// If the format matches the media file's suffix and the bitrate is greater than or equal to the original bitrate, -// it returns the original format and bitrate. -// Otherwise, it returns the target format and bitrate from the -// transcoding settings. -func findTranscoding(ctx context.Context, ds model.DataStore, mf *model.MediaFile, format string, bitRate int) (string, int) { - t, err := ds.Transcoding(ctx).FindByFormat(format) - if err != nil || t == nil || format == mf.Suffix && bitRate >= mf.BitRate { - return "raw", 0 + if reqBitRate > 0 { + cBitRate = reqBitRate } - - return t.TargetFormat, cmp.Or(bitRate, t.DefaultBitRate) + if cBitRate == 0 && cFormat == "" { + return format, bitRate + } + t, err := ds.Transcoding(ctx).FindByFormat(cFormat) + if err == nil { + format = t.TargetFormat + if cBitRate != 0 { + bitRate = cBitRate + } else { + bitRate = t.DefaultBitRate + } + } + if format == mf.Suffix && bitRate >= mf.BitRate { + format = "raw" + bitRate = 0 + } + return format, bitRate } var ( diff --git a/core/media_streamer_Internal_test.go b/core/media_streamer_Internal_test.go index 001037167..44fbf701c 100644 --- a/core/media_streamer_Internal_test.go +++ b/core/media_streamer_Internal_test.go @@ -122,10 +122,11 @@ var _ = Describe("MediaStreamer", func() { Expect(bitRate).To(Equal(0)) }) }) + Context("player has maxBitRate configured", func() { BeforeEach(func() { t := model.Transcoding{ID: "oga1", TargetFormat: "oga", DefaultBitRate: 96} - p := model.Player{ID: "player1", TranscodingId: t.ID, MaxBitRate: 80} + p := model.Player{ID: "player1", TranscodingId: t.ID, MaxBitRate: 192} ctx = request.WithTranscoding(ctx, t) ctx = request.WithPlayer(ctx, p) }) @@ -140,7 +141,7 @@ var _ = Describe("MediaStreamer", func() { mf.BitRate = 1000 format, bitRate := selectTranscodingOptions(ctx, ds, mf, "", 0) Expect(format).To(Equal("oga")) - Expect(bitRate).To(Equal(80)) + Expect(bitRate).To(Equal(192)) }) It("returns requested format", func() { mf.Suffix = "flac" @@ -152,9 +153,9 @@ var _ = Describe("MediaStreamer", func() { It("returns requested bitrate", func() { mf.Suffix = "flac" mf.BitRate = 1000 - format, bitRate := selectTranscodingOptions(ctx, ds, mf, "", 80) + format, bitRate := selectTranscodingOptions(ctx, ds, mf, "", 160) Expect(format).To(Equal("oga")) - Expect(bitRate).To(Equal(80)) + Expect(bitRate).To(Equal(160)) }) }) }) From cd0cf7c12be08d8035c038138d25e3f6431df021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Fri, 8 Nov 2024 20:23:30 -0500 Subject: [PATCH 20/22] feat: cache login background images (#3462) * feat: use direct links to unsplash for background images Signed-off-by: Deluan * feat: cache images from unsplash Signed-off-by: Deluan * refactor: use cache.HTTPClient to reduce complexity Signed-off-by: Deluan * refactor: remove magic numbers Signed-off-by: Deluan --------- Signed-off-by: Deluan --- .gitignore | 3 +- server/backgrounds/handler.go | 110 ++++++++++++++++++++++++---------- utils/cache/file_caches.go | 43 +++++++++++++ 3 files changed, 122 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index e064113bf..b9d673d30 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ music docker-compose.yml !contrib/docker-compose.yml binaries -taglib \ No newline at end of file +taglib +navidrome-master \ No newline at end of file diff --git a/server/backgrounds/handler.go b/server/backgrounds/handler.go index 5f6a0a02d..87f99b767 100644 --- a/server/backgrounds/handler.go +++ b/server/backgrounds/handler.go @@ -4,43 +4,96 @@ import ( "context" "encoding/base64" "errors" + "fmt" + "io" "net/http" - "net/url" - "path" - "sync" + "strings" "time" "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/log" + "github.com/navidrome/navidrome/utils/cache" "github.com/navidrome/navidrome/utils/random" "gopkg.in/yaml.v3" ) +const ( + //imageHostingUrl = "https://unsplash.com/photos/%s/download?fm=jpg&w=1600&h=900&fit=max" + imageHostingUrl = "https://www.navidrome.org/images/%s.jpg" + imageListURL = "https://www.navidrome.org/images/index.yml" + imageListTTL = 24 * time.Hour + imageCacheDir = "backgrounds" + imageCacheSize = "100MB" + imageCacheMaxItems = 1000 + imageRequestTimeout = 5 * time.Second +) + type Handler struct { - list []string - lock sync.RWMutex + httpClient *cache.HTTPClient + cache cache.FileCache } func NewHandler() *Handler { h := &Handler{} + h.httpClient = cache.NewHTTPClient(&http.Client{Timeout: 5 * time.Second}, imageListTTL) + h.cache = cache.NewFileCache(imageCacheDir, imageCacheSize, imageCacheDir, imageCacheMaxItems, h.serveImage) go func() { _, _ = h.getImageList(log.NewContext(context.Background())) }() return h } -const ndImageServiceURL = "https://www.navidrome.org/images" +type cacheKey string + +func (k cacheKey) Key() string { + return string(k) +} func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { image, err := h.getRandomImage(r.Context()) if err != nil { - defaultImage, _ := base64.StdEncoding.DecodeString(consts.DefaultUILoginBackgroundOffline) - w.Header().Set("content-type", "image/png") - _, _ = w.Write(defaultImage) + h.serveDefaultImage(w) return } + s, err := h.cache.Get(r.Context(), cacheKey(image)) + if err != nil { + h.serveDefaultImage(w) + return + } + defer s.Close() - http.Redirect(w, r, buildPath(ndImageServiceURL, image), http.StatusFound) + w.Header().Set("content-type", "image/jpeg") + _, _ = io.Copy(w, s.Reader) +} + +func (h *Handler) serveDefaultImage(w http.ResponseWriter) { + defaultImage, _ := base64.StdEncoding.DecodeString(consts.DefaultUILoginBackgroundOffline) + w.Header().Set("content-type", "image/png") + _, _ = w.Write(defaultImage) +} + +func (h *Handler) serveImage(ctx context.Context, item cache.Item) (io.Reader, error) { + start := time.Now() + image := item.Key() + if image == "" { + return nil, errors.New("empty image name") + } + c := http.Client{Timeout: imageRequestTimeout} + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, imageURL(image), nil) + resp, err := c.Do(req) //nolint:bodyclose // No need to close resp.Body, it will be closed via the CachedStream wrapper + if errors.Is(err, context.DeadlineExceeded) { + defaultImage, _ := base64.StdEncoding.DecodeString(consts.DefaultUILoginBackgroundOffline) + return strings.NewReader(string(defaultImage)), nil + } + if err != nil { + return nil, fmt.Errorf("could not get background image from hosting service: %w", err) + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code getting background image from hosting service: %d", resp.StatusCode) + } + log.Debug(ctx, "Got background image from hosting service", "image", image, "elapsed", time.Since(start)) + + return resp.Body, nil } func (h *Handler) getRandomImage(ctx context.Context) (string, error) { @@ -56,37 +109,28 @@ func (h *Handler) getRandomImage(ctx context.Context) (string, error) { } func (h *Handler) getImageList(ctx context.Context) ([]string, error) { - h.lock.RLock() - if len(h.list) > 0 { - defer h.lock.RUnlock() - return h.list, nil - } - - h.lock.RUnlock() - h.lock.Lock() - defer h.lock.Unlock() start := time.Now() - c := http.Client{ - Timeout: time.Minute, - } - - req, _ := http.NewRequestWithContext(ctx, http.MethodGet, buildPath(ndImageServiceURL, "index.yml"), nil) - resp, err := c.Do(req) + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, imageListURL, nil) + resp, err := h.httpClient.Do(req) if err != nil { log.Warn(ctx, "Could not get background images from image service", err) return nil, err } defer resp.Body.Close() + + var list []string dec := yaml.NewDecoder(resp.Body) - err = dec.Decode(&h.list) - log.Debug(ctx, "Loaded background images from image service", "total", len(h.list), "elapsed", time.Since(start)) - return h.list, err + err = dec.Decode(&list) + if err != nil { + log.Warn(ctx, "Could not decode background images from image service", err) + return nil, err + } + log.Debug(ctx, "Loaded background images from image service", "total", len(list), "elapsed", time.Since(start)) + return list, nil } -func buildPath(baseURL string, endpoint ...string) string { - u, _ := url.Parse(baseURL) - p := path.Join(endpoint...) - u.Path = path.Join(u.Path, p) - return u.String() +func imageURL(imageName string) string { + imageName = strings.TrimSuffix(imageName, ".jpg") + return fmt.Sprintf(imageHostingUrl, imageName) } diff --git a/utils/cache/file_caches.go b/utils/cache/file_caches.go index 9fa0e0168..a765fa2a4 100644 --- a/utils/cache/file_caches.go +++ b/utils/cache/file_caches.go @@ -17,17 +17,59 @@ import ( "github.com/navidrome/navidrome/log" ) +// Item represents an item that can be cached. It must implement the Key method that returns a unique key for a +// given item. type Item interface { Key() string } +// ReadFunc is a function that retrieves the data to be cached. It receives the Item to be cached and returns +// an io.Reader with the data and an error. type ReadFunc func(ctx context.Context, item Item) (io.Reader, error) +// FileCache is designed to cache data on the filesystem to improve performance by avoiding repeated data +// retrieval operations. +// +// Errors are handled gracefully. If the cache is not initialized or an error occurs during data +// retrieval, it will log the error and proceed without caching. type FileCache interface { + + // Get retrieves data from the cache. This method checks if the data is already cached. If it is, it + // returns the cached data. If not, it retrieves the data using the provided getReader function and caches it. + // + // Example Usage: + // + // s, err := fc.Get(context.Background(), cacheKey("testKey")) + // if err != nil { + // log.Fatal(err) + // } + // defer s.Close() + // + // data, err := io.ReadAll(s) + // if err != nil { + // log.Fatal(err) + // } + // fmt.Println(string(data)) Get(ctx context.Context, item Item) (*CachedStream, error) + + // Available checks if the cache is available Available(ctx context.Context) bool } +// NewFileCache creates a new FileCache. This function initializes the cache and starts it in the background. +// +// name: A string representing the name of the cache. +// cacheSize: A string representing the maximum size of the cache (e.g., "1KB", "10MB"). +// cacheFolder: A string representing the folder where the cache files will be stored. +// maxItems: An integer representing the maximum number of items the cache can hold. +// getReader: A function of type ReadFunc that retrieves the data to be cached. +// +// Example Usage: +// +// fc := NewFileCache("exampleCache", "10MB", "cacheFolder", 100, func(ctx context.Context, item Item) (io.Reader, error) { +// // Implement the logic to retrieve the data for the given item +// return strings.NewReader(item.Key()), nil +// }) func NewFileCache(name, cacheSize, cacheFolder string, maxItems int, getReader ReadFunc) FileCache { fc := &fileCache{ name: name, @@ -150,6 +192,7 @@ func (fc *fileCache) Get(ctx context.Context, arg Item) (*CachedStream, error) { return &CachedStream{Reader: r, Cached: cached}, nil } +// CachedStream is a wrapper around an io.ReadCloser that allows reading from a cache. type CachedStream struct { io.Reader io.Seeker From 3671598121742c1263dd2818c55bb3e8cc89f519 Mon Sep 17 00:00:00 2001 From: Deluan Date: Mon, 11 Nov 2024 13:30:09 -0500 Subject: [PATCH 21/22] chore(deps): bump Go dependencies Signed-off-by: Deluan --- go.mod | 18 +++++++++--------- go.sum | 36 ++++++++++++++++++------------------ 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/go.mod b/go.mod index 3b7820512..40d12e6ac 100644 --- a/go.mod +++ b/go.mod @@ -49,11 +49,11 @@ require ( github.com/stretchr/testify v1.9.0 github.com/unrolled/secure v1.17.0 github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 - golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c - golang.org/x/image v0.21.0 - golang.org/x/sync v0.8.0 - golang.org/x/text v0.19.0 - golang.org/x/time v0.7.0 + golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f + golang.org/x/image v0.22.0 + golang.org/x/sync v0.9.0 + golang.org/x/text v0.20.0 + golang.org/x/time v0.8.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -102,10 +102,10 @@ require ( github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.28.0 // indirect - golang.org/x/net v0.30.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/tools v0.26.0 // indirect + golang.org/x/crypto v0.29.0 // indirect + golang.org/x/net v0.31.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/tools v0.27.0 // indirect google.golang.org/protobuf v1.35.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect diff --git a/go.sum b/go.sum index 3764affbf..c64803117 100644 --- a/go.sum +++ b/go.sum @@ -226,13 +226,13 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= -golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= -golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= +golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s= -golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78= +golang.org/x/image v0.22.0 h1:UtK5yLUzilVrkjMAZAZ34DXGpASN8i8pj8g+O+yd10g= +golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -246,15 +246,15 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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= golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -268,8 +268,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -284,10 +284,10 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= -golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -295,8 +295,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= +golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= From c952dc343afb7e9e64acaf1a1b64d87ac13a57cf Mon Sep 17 00:00:00 2001 From: Deluan Date: Mon, 11 Nov 2024 13:47:01 -0500 Subject: [PATCH 22/22] chore(deps): bump JS dependencies Signed-off-by: Deluan --- ui/package-lock.json | 34 +++++++++++++++++----------------- ui/package.json | 10 +++++----- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index 9c854f865..5798b26c4 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -37,17 +37,17 @@ "react-router-dom": "^5.3.4", "redux": "^4.2.0", "redux-saga": "^1.1.3", - "uuid": "^11.0.2" + "uuid": "^11.0.3" }, "devDependencies": { "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^12.1.5", "@testing-library/react-hooks": "^7.0.2", "@testing-library/user-event": "^14.5.2", - "@types/node": "^22.8.7", + "@types/node": "^22.9.0", "@types/react": "^17.0.2", "@types/react-dom": "^17.0.2", - "@typescript-eslint/eslint-plugin": "^6.12.0", + "@typescript-eslint/eslint-plugin": "^6.19.1", "@typescript-eslint/parser": "^6.12.0", "@vitejs/plugin-react": "^4.3.3", "@vitest/coverage-v8": "^2.1.4", @@ -57,12 +57,12 @@ "eslint-plugin-react": "^7.37.2", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.14", - "happy-dom": "^15.8.3", + "happy-dom": "^15.11.0", "jsdom": "^25.0.1", "prettier": "^3.3.3", "ra-test": "^3.19.12", "typescript": "^5.6.3", - "vite": "^5.4.10", + "vite": "^5.4.11", "vite-plugin-pwa": "^0.20.5", "vitest": "^2.1.1" } @@ -3250,9 +3250,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.8.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.7.tgz", - "integrity": "sha512-LidcG+2UeYIWcMuMUpBKOnryBWG/rnmOHQR5apjn8myTQcx3rinFRn7DcIFhMnS0PPFSC6OafdIKEad0lj6U0Q==", + "version": "22.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", + "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", "devOptional": true, "dependencies": { "undici-types": "~6.19.8" @@ -6077,9 +6077,9 @@ "license": "MIT" }, "node_modules/happy-dom": { - "version": "15.8.3", - "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-15.8.3.tgz", - "integrity": "sha512-YR9nUWN/T2bH7pPLEYMhTp4DQExPH+mC4KulJDgimCb+FY3Er0Vp6SOOcBXrNfMTri3lAk9uSZqUTG2hgZOYwg==", + "version": "15.11.0", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-15.11.0.tgz", + "integrity": "sha512-/zyxHbXriYJ8b9Urh43ILk/jd9tC07djURnJuAimJ3tJCOLOzOUp7dEHDwJOZyzROlrrooUhr/0INZIDBj1Bjw==", "dev": true, "dependencies": { "entities": "^4.5.0", @@ -10346,9 +10346,9 @@ } }, "node_modules/uuid": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.2.tgz", - "integrity": "sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", + "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -10364,9 +10364,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "5.4.10", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.10.tgz", - "integrity": "sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==", + "version": "5.4.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", + "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", "dev": true, "dependencies": { "esbuild": "^0.21.3", diff --git a/ui/package.json b/ui/package.json index 359687d83..479427fa9 100644 --- a/ui/package.json +++ b/ui/package.json @@ -46,17 +46,17 @@ "react-router-dom": "^5.3.4", "redux": "^4.2.0", "redux-saga": "^1.1.3", - "uuid": "^11.0.2" + "uuid": "^11.0.3" }, "devDependencies": { "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^12.1.5", "@testing-library/react-hooks": "^7.0.2", "@testing-library/user-event": "^14.5.2", - "@types/node": "^22.8.7", + "@types/node": "^22.9.0", "@types/react": "^17.0.2", "@types/react-dom": "^17.0.2", - "@typescript-eslint/eslint-plugin": "^6.12.0", + "@typescript-eslint/eslint-plugin": "^6.19.1", "@typescript-eslint/parser": "^6.12.0", "@vitejs/plugin-react": "^4.3.3", "@vitest/coverage-v8": "^2.1.4", @@ -66,12 +66,12 @@ "eslint-plugin-react": "^7.37.2", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.14", - "happy-dom": "^15.8.3", + "happy-dom": "^15.11.0", "jsdom": "^25.0.1", "prettier": "^3.3.3", "ra-test": "^3.19.12", "typescript": "^5.6.3", - "vite": "^5.4.10", + "vite": "^5.4.11", "vite-plugin-pwa": "^0.20.5", "vitest": "^2.1.1" },