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 }