mirror of
https://github.com/navidrome/navidrome.git
synced 2025-05-09 23:01:07 +03:00
Merge branch 'master' into dlna-spike
This commit is contained in:
commit
f6d4295d42
@ -129,7 +129,8 @@ type scannerOptions struct {
|
||||
WatcherWait time.Duration
|
||||
ScanOnStartup bool
|
||||
Extractor string
|
||||
GroupAlbumReleases bool // Deprecated: BFR Update docs
|
||||
GenreSeparators string // Deprecated: Use Tags.genre.Split instead
|
||||
GroupAlbumReleases bool // Deprecated: Use PID.Album instead
|
||||
}
|
||||
|
||||
type subsonicOptions struct {
|
||||
@ -140,6 +141,7 @@ type subsonicOptions struct {
|
||||
}
|
||||
|
||||
type TagConf struct {
|
||||
Ignore bool `yaml:"ignore"`
|
||||
Aliases []string `yaml:"aliases"`
|
||||
Type string `yaml:"type"`
|
||||
MaxLength int `yaml:"maxLength"`
|
||||
@ -312,6 +314,7 @@ func Load(noConfigDump bool) {
|
||||
log.Warn(fmt.Sprintf("Extractor '%s' is not implemented, using 'taglib'", Server.Scanner.Extractor))
|
||||
Server.Scanner.Extractor = consts.DefaultScannerExtractor
|
||||
}
|
||||
logDeprecatedOptions("Scanner.GenreSeparators")
|
||||
logDeprecatedOptions("Scanner.GroupAlbumReleases")
|
||||
|
||||
// Call init hooks
|
||||
@ -494,9 +497,10 @@ func init() {
|
||||
viper.SetDefault("scanner.enabled", true)
|
||||
viper.SetDefault("scanner.schedule", "0")
|
||||
viper.SetDefault("scanner.extractor", consts.DefaultScannerExtractor)
|
||||
viper.SetDefault("scanner.groupalbumreleases", false)
|
||||
viper.SetDefault("scanner.watcherwait", consts.DefaultWatcherWait)
|
||||
viper.SetDefault("scanner.scanonstartup", true)
|
||||
viper.SetDefault("scanner.genreseparators", "")
|
||||
viper.SetDefault("scanner.groupalbumreleases", false)
|
||||
|
||||
viper.SetDefault("subsonic.appendsubtitle", true)
|
||||
viper.SetDefault("subsonic.artistparticipations", false)
|
||||
|
@ -172,14 +172,20 @@ join library on media_file.library_id = library.id`, string(os.PathSeparator)))
|
||||
// Finally, walk the in-mem filesystem and insert all folders into the DB.
|
||||
err = fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
// Don't abort the walk, just log the error
|
||||
log.Error("error walking folder to DB", "path", path, err)
|
||||
return nil
|
||||
}
|
||||
if d.IsDir() {
|
||||
f := model.NewFolder(lib, path)
|
||||
_, err = stmt.ExecContext(ctx, f.ID, lib.ID, f.Path, f.Name, f.ParentID)
|
||||
if err != nil {
|
||||
log.Error("error writing folder to DB", "path", path, err)
|
||||
}
|
||||
// Skip entries that are not directories
|
||||
if !d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a folder in the DB
|
||||
f := model.NewFolder(lib, path)
|
||||
_, err = stmt.ExecContext(ctx, f.ID, lib.ID, f.Path, f.Name, f.ParentID)
|
||||
if err != nil {
|
||||
log.Error("error writing folder to DB", "path", path, err)
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
@ -1,6 +1,7 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"maps"
|
||||
"regexp"
|
||||
"slices"
|
||||
@ -175,21 +176,42 @@ func loadTagMappings() {
|
||||
log.Error("No tag mappings found in mappings.yaml, check the format")
|
||||
}
|
||||
|
||||
// Use Scanner.GenreSeparators if specified and Tags.genre is not defined
|
||||
if conf.Server.Scanner.GenreSeparators != "" && len(conf.Server.Tags["genre"].Aliases) == 0 {
|
||||
genreConf := _mappings.Main[TagName("genre")]
|
||||
genreConf.Split = strings.Split(conf.Server.Scanner.GenreSeparators, "")
|
||||
genreConf.SplitRx = compileSplitRegex("genre", genreConf.Split)
|
||||
_mappings.Main[TagName("genre")] = genreConf
|
||||
log.Debug("Loading deprecated list of genre separators", "separators", genreConf.Split)
|
||||
}
|
||||
|
||||
// Overwrite the default mappings with the ones from the config
|
||||
for tag, cfg := range conf.Server.Tags {
|
||||
if len(cfg.Aliases) == 0 {
|
||||
if cfg.Ignore {
|
||||
delete(_mappings.Main, TagName(tag))
|
||||
delete(_mappings.Additional, TagName(tag))
|
||||
continue
|
||||
}
|
||||
c := TagConf{
|
||||
Aliases: cfg.Aliases,
|
||||
Type: TagType(cfg.Type),
|
||||
MaxLength: cfg.MaxLength,
|
||||
Split: cfg.Split,
|
||||
Album: cfg.Album,
|
||||
SplitRx: compileSplitRegex(TagName(tag), cfg.Split),
|
||||
oldValue, ok := _mappings.Main[TagName(tag)]
|
||||
if !ok {
|
||||
oldValue = _mappings.Additional[TagName(tag)]
|
||||
}
|
||||
aliases := cfg.Aliases
|
||||
if len(aliases) == 0 {
|
||||
aliases = oldValue.Aliases
|
||||
}
|
||||
split := cfg.Split
|
||||
if len(split) == 0 {
|
||||
split = oldValue.Split
|
||||
}
|
||||
c := TagConf{
|
||||
Aliases: aliases,
|
||||
Split: split,
|
||||
Type: cmp.Or(TagType(cfg.Type), oldValue.Type),
|
||||
MaxLength: cmp.Or(cfg.MaxLength, oldValue.MaxLength),
|
||||
Album: cmp.Or(cfg.Album, oldValue.Album),
|
||||
}
|
||||
c.SplitRx = compileSplitRegex(TagName(tag), c.Split)
|
||||
if _, ok := _mappings.Main[TagName(tag)]; ok {
|
||||
_mappings.Main[TagName(tag)] = c
|
||||
} else {
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@ -119,11 +120,17 @@ var albumFilters = sync.OnceValue(func() map[string]filterFunc {
|
||||
"has_rating": hasRatingFilter,
|
||||
"missing": booleanFilter,
|
||||
"genre_id": tagIDFilter,
|
||||
"role_total_id": allRolesFilter,
|
||||
}
|
||||
// Add all album tags as filters
|
||||
for tag := range model.AlbumLevelTags() {
|
||||
filters[string(tag)] = tagIDFilter
|
||||
}
|
||||
|
||||
for role := range model.AllRoles {
|
||||
filters["role_"+role+"_id"] = artistRoleFilter
|
||||
}
|
||||
|
||||
return filters
|
||||
})
|
||||
|
||||
@ -153,14 +160,25 @@ func yearFilter(_ string, value interface{}) Sqlizer {
|
||||
}
|
||||
}
|
||||
|
||||
// BFR: Support other roles
|
||||
func artistFilter(_ string, value interface{}) Sqlizer {
|
||||
return Or{
|
||||
Exists("json_tree(Participants, '$.albumartist')", Eq{"value": value}),
|
||||
Exists("json_tree(Participants, '$.artist')", Eq{"value": value}),
|
||||
Exists("json_tree(participants, '$.albumartist')", Eq{"value": value}),
|
||||
Exists("json_tree(participants, '$.artist')", Eq{"value": value}),
|
||||
}
|
||||
// For any role:
|
||||
//return Like{"Participants": fmt.Sprintf(`%%"%s"%%`, value)}
|
||||
}
|
||||
|
||||
func artistRoleFilter(name string, value interface{}) Sqlizer {
|
||||
roleName := strings.TrimSuffix(strings.TrimPrefix(name, "role_"), "_id")
|
||||
|
||||
// Check if the role name is valid. If not, return an invalid filter
|
||||
if _, ok := model.AllRoles[roleName]; !ok {
|
||||
return Gt{"": nil}
|
||||
}
|
||||
return Exists(fmt.Sprintf("json_tree(participants, '$.%s')", roleName), Eq{"value": value})
|
||||
}
|
||||
|
||||
func allRolesFilter(_ string, value interface{}) Sqlizer {
|
||||
return Like{"participants": fmt.Sprintf(`%%"%s"%%`, value)}
|
||||
}
|
||||
|
||||
func (r *albumRepository) CountAll(options ...model.QueryOptions) (int64, error) {
|
||||
|
@ -2,6 +2,7 @@ package persistence
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
@ -236,6 +237,52 @@ var _ = Describe("AlbumRepository", func() {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Describe("artistRoleFilter", func() {
|
||||
DescribeTable("creates correct SQL expressions for artist roles",
|
||||
func(filterName, artistID, expectedSQL string) {
|
||||
sqlizer := artistRoleFilter(filterName, artistID)
|
||||
sql, args, err := sqlizer.ToSql()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(sql).To(Equal(expectedSQL))
|
||||
Expect(args).To(Equal([]interface{}{artistID}))
|
||||
},
|
||||
Entry("artist role", "role_artist_id", "123",
|
||||
"exists (select 1 from json_tree(participants, '$.artist') where value = ?)"),
|
||||
Entry("albumartist role", "role_albumartist_id", "456",
|
||||
"exists (select 1 from json_tree(participants, '$.albumartist') where value = ?)"),
|
||||
Entry("composer role", "role_composer_id", "789",
|
||||
"exists (select 1 from json_tree(participants, '$.composer') where value = ?)"),
|
||||
)
|
||||
|
||||
It("works with the actual filter map", func() {
|
||||
filters := albumFilters()
|
||||
|
||||
for roleName := range model.AllRoles {
|
||||
filterName := "role_" + roleName + "_id"
|
||||
filterFunc, exists := filters[filterName]
|
||||
Expect(exists).To(BeTrue(), fmt.Sprintf("Filter %s should exist", filterName))
|
||||
|
||||
sqlizer := filterFunc(filterName, "test-id")
|
||||
sql, args, err := sqlizer.ToSql()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(sql).To(Equal(fmt.Sprintf("exists (select 1 from json_tree(participants, '$.%s') where value = ?)", roleName)))
|
||||
Expect(args).To(Equal([]interface{}{"test-id"}))
|
||||
}
|
||||
})
|
||||
|
||||
It("rejects invalid roles", func() {
|
||||
sqlizer := artistRoleFilter("role_invalid_id", "123")
|
||||
_, _, err := sqlizer.ToSql()
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("rejects invalid filter names", func() {
|
||||
sqlizer := artistRoleFilter("invalid_name", "123")
|
||||
_, _, err := sqlizer.ToSql()
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
func _p(id, name string, sortName ...string) model.Participant {
|
||||
|
@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/id"
|
||||
@ -53,17 +52,6 @@ var _ = Describe("MediaRepository", func() {
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
})
|
||||
|
||||
XIt("filters by genre", func() {
|
||||
Expect(mr.GetAll(model.QueryOptions{
|
||||
Sort: "genre.name asc, title asc",
|
||||
Filters: squirrel.Eq{"genre.name": "Rock"},
|
||||
})).To(Equal(model.MediaFiles{
|
||||
songDayInALife,
|
||||
songAntenna,
|
||||
songComeTogether,
|
||||
}))
|
||||
})
|
||||
|
||||
Context("Annotations", func() {
|
||||
It("increments play count when the tracks does not have annotations", func() {
|
||||
id := "incplay.firsttime"
|
||||
|
@ -1,6 +1,14 @@
|
||||
#file: noinspection SpellCheckingInspection
|
||||
# Tag mapping adapted from https://picard-docs.musicbrainz.org/downloads/MusicBrainz_Picard_Tag_Map.html
|
||||
#
|
||||
# NOTE FOR USERS:
|
||||
#
|
||||
# This file can be used as a reference to understand how Navidrome maps the tags in your music files to its fields.
|
||||
# If you want to customize these mappings, please refer to https://www.navidrome.org/docs/usage/customtags/
|
||||
#
|
||||
#
|
||||
# NOTE FOR DEVELOPERS:
|
||||
#
|
||||
# This file contains the mapping between the tags in your music files and the fields in Navidrome.
|
||||
# You can add new tags, change the aliases, or add new split characters to the existing tags.
|
||||
# The artists and roles keys are used to define how to split the tag values into multiple values.
|
||||
@ -96,7 +104,7 @@ main:
|
||||
aliases: [ disctotal, totaldiscs ]
|
||||
album: true
|
||||
discsubtitle:
|
||||
aliases: [ tsst, discsubtitle, ----:com.apple.itunes:discsubtitle, wm/setsubtitle ]
|
||||
aliases: [ tsst, discsubtitle, ----:com.apple.itunes:discsubtitle, setsubtitle, wm/setsubtitle ]
|
||||
bpm:
|
||||
aliases: [ tbpm, bpm, tmpo, wm/beatsperminute ]
|
||||
lyrics:
|
||||
|
@ -334,7 +334,8 @@ func (p *phaseFolders) persistChanges(entry *folderEntry) (*folderEntry, error)
|
||||
|
||||
// Save all new/modified artists to DB. Their information will be incomplete, but they will be refreshed later
|
||||
for i := range entry.artists {
|
||||
err = artistRepo.Put(&entry.artists[i], "name", "mbz_artist_id", "sort_artist_name", "order_artist_name")
|
||||
err = artistRepo.Put(&entry.artists[i], "name",
|
||||
"mbz_artist_id", "sort_artist_name", "order_artist_name", "full_text")
|
||||
if err != nil {
|
||||
log.Error(p.ctx, "Scanner: Error persisting artist to DB", "folder", entry.path, "artist", entry.artists[i].Name, err)
|
||||
return err
|
||||
|
@ -145,7 +145,10 @@ func loadIgnoredPatterns(ctx context.Context, fsys fs.FS, currentFolder string,
|
||||
}
|
||||
// If the .ndignore file is empty, mimic the current behavior and ignore everything
|
||||
if len(newPatterns) == 0 {
|
||||
log.Trace(ctx, "Scanner: .ndignore file is empty, ignoring everything", "path", currentFolder)
|
||||
newPatterns = []string{"**/*"}
|
||||
} else {
|
||||
log.Trace(ctx, "Scanner: .ndignore file found ", "path", ignoreFilePath, "patterns", newPatterns)
|
||||
}
|
||||
}
|
||||
// Combine the patterns from the .ndignore file with the ones passed as argument
|
||||
@ -180,7 +183,7 @@ func loadDir(ctx context.Context, job *scanJob, dirPath string, ignorePatterns [
|
||||
children = make([]string, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
entryPath := path.Join(dirPath, entry.Name())
|
||||
if len(ignorePatterns) > 0 && isScanIgnored(ignoreMatcher, entryPath) {
|
||||
if len(ignorePatterns) > 0 && isScanIgnored(ctx, ignoreMatcher, entryPath) {
|
||||
log.Trace(ctx, "Scanner: Ignoring entry", "path", entryPath)
|
||||
continue
|
||||
}
|
||||
@ -309,6 +312,10 @@ func isEntryIgnored(name string) bool {
|
||||
return strings.HasPrefix(name, ".") && !strings.HasPrefix(name, "..")
|
||||
}
|
||||
|
||||
func isScanIgnored(matcher *ignore.GitIgnore, entryPath string) bool {
|
||||
return matcher.MatchesPath(entryPath)
|
||||
func isScanIgnored(ctx context.Context, matcher *ignore.GitIgnore, entryPath string) bool {
|
||||
matches := matcher.MatchesPath(entryPath)
|
||||
if matches {
|
||||
log.Trace(ctx, "Scanner: Ignoring entry matching .ndignore: ", "path", entryPath)
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
@ -101,22 +101,27 @@ func watchLib(ctx context.Context, lib model.Library, watchChan chan struct{}) {
|
||||
log.Error(ctx, "Watcher: Error watching library", "library", lib.ID, "path", lib.Path, err)
|
||||
return
|
||||
}
|
||||
log.Info(ctx, "Watcher started", "library", lib.ID, "path", lib.Path)
|
||||
absLibPath, err := filepath.Abs(lib.Path)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Watcher: Error converting lib.Path to absolute", "library", lib.ID, "path", lib.Path, err)
|
||||
return
|
||||
}
|
||||
log.Info(ctx, "Watcher started", "library", lib.ID, "libPath", lib.Path, "absoluteLibPath", absLibPath)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case path := <-c:
|
||||
path, err = filepath.Rel(lib.Path, path)
|
||||
path, err = filepath.Rel(absLibPath, path)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Watcher: Error getting relative path", "library", lib.ID, "path", path, err)
|
||||
log.Error(ctx, "Watcher: Error getting relative path", "library", lib.ID, "libPath", absLibPath, "path", path, err)
|
||||
continue
|
||||
}
|
||||
if isIgnoredPath(ctx, fsys, path) {
|
||||
log.Trace(ctx, "Watcher: Ignoring change", "library", lib.ID, "path", path)
|
||||
continue
|
||||
}
|
||||
log.Trace(ctx, "Watcher: Detected change", "library", lib.ID, "path", path)
|
||||
log.Trace(ctx, "Watcher: Detected change", "library", lib.ID, "path", path, "libPath", absLibPath)
|
||||
watchChan <- struct{}{}
|
||||
}
|
||||
}
|
||||
|
@ -270,30 +270,43 @@ func (api *Router) GetGenres(r *http.Request) (*responses.Subsonic, error) {
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (api *Router) GetArtistInfo(r *http.Request) (*responses.Subsonic, error) {
|
||||
func (api *Router) getArtistInfo(r *http.Request) (*responses.ArtistInfoBase, *model.Artists, error) {
|
||||
ctx := r.Context()
|
||||
p := req.Params(r)
|
||||
id, err := p.String("id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
count := p.IntOr("count", 20)
|
||||
includeNotPresent := p.BoolOr("includeNotPresent", false)
|
||||
|
||||
artist, err := api.externalMetadata.UpdateArtistInfo(ctx, id, count, includeNotPresent)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
base := responses.ArtistInfoBase{}
|
||||
base.Biography = artist.Biography
|
||||
base.SmallImageUrl = public.ImageURL(r, artist.CoverArtID(), 300)
|
||||
base.MediumImageUrl = public.ImageURL(r, artist.CoverArtID(), 600)
|
||||
base.LargeImageUrl = public.ImageURL(r, artist.CoverArtID(), 1200)
|
||||
base.LastFmUrl = artist.ExternalUrl
|
||||
base.MusicBrainzID = artist.MbzArtistID
|
||||
|
||||
return &base, &artist.SimilarArtists, nil
|
||||
}
|
||||
|
||||
func (api *Router) GetArtistInfo(r *http.Request) (*responses.Subsonic, error) {
|
||||
base, similarArtists, err := api.getArtistInfo(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := newResponse()
|
||||
response.ArtistInfo = &responses.ArtistInfo{}
|
||||
response.ArtistInfo.Biography = artist.Biography
|
||||
response.ArtistInfo.SmallImageUrl = public.ImageURL(r, artist.CoverArtID(), 300)
|
||||
response.ArtistInfo.MediumImageUrl = public.ImageURL(r, artist.CoverArtID(), 600)
|
||||
response.ArtistInfo.LargeImageUrl = public.ImageURL(r, artist.CoverArtID(), 1200)
|
||||
response.ArtistInfo.LastFmUrl = artist.ExternalUrl
|
||||
response.ArtistInfo.MusicBrainzID = artist.MbzArtistID
|
||||
for _, s := range artist.SimilarArtists {
|
||||
response.ArtistInfo.ArtistInfoBase = *base
|
||||
|
||||
for _, s := range *similarArtists {
|
||||
similar := toArtist(r, s)
|
||||
if s.ID == "" {
|
||||
similar.Id = "-1"
|
||||
@ -304,23 +317,20 @@ func (api *Router) GetArtistInfo(r *http.Request) (*responses.Subsonic, error) {
|
||||
}
|
||||
|
||||
func (api *Router) GetArtistInfo2(r *http.Request) (*responses.Subsonic, error) {
|
||||
info, err := api.GetArtistInfo(r)
|
||||
base, similarArtists, err := api.getArtistInfo(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := newResponse()
|
||||
response.ArtistInfo2 = &responses.ArtistInfo2{}
|
||||
response.ArtistInfo2.ArtistInfoBase = info.ArtistInfo.ArtistInfoBase
|
||||
for _, s := range info.ArtistInfo.SimilarArtist {
|
||||
similar := responses.ArtistID3{}
|
||||
similar.Id = s.Id
|
||||
similar.Name = s.Name
|
||||
similar.AlbumCount = s.AlbumCount
|
||||
similar.Starred = s.Starred
|
||||
similar.UserRating = s.UserRating
|
||||
similar.CoverArt = s.CoverArt
|
||||
similar.ArtistImageUrl = s.ArtistImageUrl
|
||||
response.ArtistInfo2.ArtistInfoBase = *base
|
||||
|
||||
for _, s := range *similarArtists {
|
||||
similar := toArtistID3(r, s)
|
||||
if s.ID == "" {
|
||||
similar.Id = "-1"
|
||||
}
|
||||
response.ArtistInfo2.SimilarArtist = append(response.ArtistInfo2.SimilarArtist, similar)
|
||||
}
|
||||
return response, nil
|
||||
|
@ -95,7 +95,7 @@ func SongsByRandom(genre string, fromYear, toYear int) Options {
|
||||
}
|
||||
ff := And{}
|
||||
if genre != "" {
|
||||
ff = append(ff, Eq{"genre.name": genre})
|
||||
ff = append(ff, filterByGenre(genre))
|
||||
}
|
||||
if fromYear != 0 {
|
||||
ff = append(ff, GtOrEq{"year": fromYear})
|
||||
@ -118,11 +118,15 @@ func SongWithLyrics(artist, title string) Options {
|
||||
|
||||
func ByGenre(genre string) Options {
|
||||
return addDefaultFilters(Options{
|
||||
Sort: "name asc",
|
||||
Filters: persistence.Exists("json_tree(tags)", And{
|
||||
Like{"value": genre},
|
||||
NotEq{"atom": nil},
|
||||
}),
|
||||
Sort: "name asc",
|
||||
Filters: filterByGenre(genre),
|
||||
})
|
||||
}
|
||||
|
||||
func filterByGenre(genre string) Sqlizer {
|
||||
return persistence.Exists("json_tree(tags)", And{
|
||||
Like{"value": genre},
|
||||
NotEq{"atom": nil},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -77,11 +77,26 @@ func sortName(sortName, orderName string) string {
|
||||
return orderName
|
||||
}
|
||||
|
||||
func getArtistAlbumCount(a model.Artist) int32 {
|
||||
albumStats := a.Stats[model.RoleAlbumArtist]
|
||||
|
||||
// If ArtistParticipations are set, then `getArtist` will return albums
|
||||
// where the artist is an album artist OR artist. While it may be an underestimate,
|
||||
// guess the count by taking a max of the album artist and artist count. This is
|
||||
// guaranteed to be <= the actual count.
|
||||
// Otherwise, return just the roles as album artist (precise)
|
||||
if conf.Server.Subsonic.ArtistParticipations {
|
||||
artistStats := a.Stats[model.RoleArtist]
|
||||
return int32(max(artistStats.AlbumCount, albumStats.AlbumCount))
|
||||
} else {
|
||||
return int32(albumStats.AlbumCount)
|
||||
}
|
||||
}
|
||||
|
||||
func toArtist(r *http.Request, a model.Artist) responses.Artist {
|
||||
artist := responses.Artist{
|
||||
Id: a.ID,
|
||||
Name: a.Name,
|
||||
AlbumCount: int32(a.AlbumCount),
|
||||
UserRating: int32(a.Rating),
|
||||
CoverArt: a.CoverArtID().String(),
|
||||
ArtistImageUrl: public.ImageURL(r, a.CoverArtID(), 600),
|
||||
@ -96,7 +111,7 @@ func toArtistID3(r *http.Request, a model.Artist) responses.ArtistID3 {
|
||||
artist := responses.ArtistID3{
|
||||
Id: a.ID,
|
||||
Name: a.Name,
|
||||
AlbumCount: int32(a.AlbumCount),
|
||||
AlbumCount: getArtistAlbumCount(a),
|
||||
CoverArt: a.CoverArtID().String(),
|
||||
ArtistImageUrl: public.ImageURL(r, a.CoverArtID(), 600),
|
||||
UserRating: int32(a.Rating),
|
||||
|
@ -10,6 +10,10 @@ import (
|
||||
)
|
||||
|
||||
var _ = Describe("helpers", func() {
|
||||
BeforeEach(func() {
|
||||
DeferCleanup(configtest.SetupConfig())
|
||||
})
|
||||
|
||||
Describe("fakePath", func() {
|
||||
var mf model.MediaFile
|
||||
BeforeEach(func() {
|
||||
@ -134,4 +138,29 @@ var _ = Describe("helpers", func() {
|
||||
Entry("returns \"explicit\" when the db value is \"e\"", "e", "explicit"),
|
||||
Entry("returns an empty string when the db value is \"\"", "", ""),
|
||||
Entry("returns an empty string when there are unexpected values on the db", "abc", ""))
|
||||
|
||||
Describe("getArtistAlbumCount", func() {
|
||||
artist := model.Artist{
|
||||
Stats: map[model.Role]model.ArtistStats{
|
||||
model.RoleAlbumArtist: {
|
||||
AlbumCount: 3,
|
||||
},
|
||||
model.RoleArtist: {
|
||||
AlbumCount: 4,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
It("Handles album count without artist participations", func() {
|
||||
conf.Server.Subsonic.ArtistParticipations = false
|
||||
result := getArtistAlbumCount(artist)
|
||||
Expect(result).To(Equal(int32(3)))
|
||||
})
|
||||
|
||||
It("Handles album count without with participations", func() {
|
||||
conf.Server.Subsonic.ArtistParticipations = true
|
||||
result := getArtistAlbumCount(artist)
|
||||
Expect(result).To(Equal(int32(4)))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -12,7 +12,6 @@
|
||||
{
|
||||
"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"
|
||||
|
@ -1,7 +1,7 @@
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
|
||||
<indexes lastModified="1" ignoredArticles="A">
|
||||
<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"></artist>
|
||||
<artist id="111" name="aaa" starred="2016-03-02T20:30:00Z" userRating="3" artistImageUrl="https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png"></artist>
|
||||
</index>
|
||||
</indexes>
|
||||
</subsonic-response>
|
||||
|
@ -92,7 +92,6 @@ type MusicFolders struct {
|
||||
type Artist struct {
|
||||
Id string `xml:"id,attr" json:"id"`
|
||||
Name string `xml:"name,attr" json:"name"`
|
||||
AlbumCount int32 `xml:"albumCount,attr,omitempty" json:"albumCount,omitempty"`
|
||||
Starred *time.Time `xml:"starred,attr,omitempty" json:"starred,omitempty"`
|
||||
UserRating int32 `xml:"userRating,attr,omitempty" json:"userRating,omitempty"`
|
||||
CoverArt string `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"`
|
||||
@ -233,7 +232,7 @@ type ArtistID3 struct {
|
||||
Id string `xml:"id,attr" json:"id"`
|
||||
Name string `xml:"name,attr" json:"name"`
|
||||
CoverArt string `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"`
|
||||
AlbumCount int32 `xml:"albumCount,attr,omitempty" json:"albumCount,omitempty"`
|
||||
AlbumCount int32 `xml:"albumCount,attr" json:"albumCount"`
|
||||
Starred *time.Time `xml:"starred,attr,omitempty" json:"starred,omitempty"`
|
||||
UserRating int32 `xml:"userRating,attr,omitempty" json:"userRating,omitempty"`
|
||||
ArtistImageUrl string `xml:"artistImageUrl,attr,omitempty" json:"artistImageUrl,omitempty"`
|
||||
|
@ -103,7 +103,6 @@ var _ = Describe("Responses", func() {
|
||||
Name: "aaa",
|
||||
Starred: &t,
|
||||
UserRating: 3,
|
||||
AlbumCount: 2,
|
||||
ArtistImageUrl: "https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png",
|
||||
}
|
||||
index := make([]Index, 1)
|
||||
|
@ -94,7 +94,6 @@ func (api *Router) Search2(r *http.Request) (*responses.Subsonic, error) {
|
||||
a := responses.Artist{
|
||||
Id: artist.ID,
|
||||
Name: artist.Name,
|
||||
AlbumCount: int32(artist.AlbumCount),
|
||||
UserRating: int32(artist.Rating),
|
||||
CoverArt: artist.CoverArtID().String(),
|
||||
ArtistImageUrl: public.ImageURL(r, artist.CoverArtID(), 600),
|
||||
|
Loading…
x
Reference in New Issue
Block a user