Add multiple genres to MediaFile
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ReneKroon/ttlcache/v2"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
)
|
||||
|
||||
func newCachedGenreRepository(ctx context.Context, repo model.GenreRepository) model.GenreRepository {
|
||||
r := &cachedGenreRepo{
|
||||
GenreRepository: repo,
|
||||
ctx: ctx,
|
||||
}
|
||||
genres, err := repo.GetAll()
|
||||
if err != nil {
|
||||
log.Error(ctx, "Could not load genres from DB", err)
|
||||
return repo
|
||||
}
|
||||
|
||||
r.cache = ttlcache.NewCache()
|
||||
for _, g := range genres {
|
||||
_ = r.cache.Set(strings.ToLower(g.Name), g.ID)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
type cachedGenreRepo struct {
|
||||
model.GenreRepository
|
||||
cache *ttlcache.Cache
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (r *cachedGenreRepo) Put(g *model.Genre) error {
|
||||
id, err := r.cache.GetByLoader(strings.ToLower(g.Name), func(key string) (interface{}, time.Duration, error) {
|
||||
err := r.GenreRepository.Put(g)
|
||||
return g.ID, 24 * time.Hour, err
|
||||
})
|
||||
g.ID = id.(string)
|
||||
return err
|
||||
}
|
||||
+38
-5
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/kennygrant/sanitize"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/scanner/metadata"
|
||||
@@ -19,10 +20,15 @@ import (
|
||||
type mediaFileMapper struct {
|
||||
rootFolder string
|
||||
policy *bluemonday.Policy
|
||||
genres model.GenreRepository
|
||||
}
|
||||
|
||||
func newMediaFileMapper(rootFolder string) *mediaFileMapper {
|
||||
return &mediaFileMapper{rootFolder: rootFolder, policy: bluemonday.UGCPolicy()}
|
||||
func newMediaFileMapper(rootFolder string, genres model.GenreRepository) *mediaFileMapper {
|
||||
return &mediaFileMapper{
|
||||
rootFolder: rootFolder,
|
||||
policy: bluemonday.UGCPolicy(),
|
||||
genres: genres,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *mediaFileMapper) toMediaFile(md *metadata.Tags) model.MediaFile {
|
||||
@@ -36,9 +42,7 @@ func (s *mediaFileMapper) toMediaFile(md *metadata.Tags) model.MediaFile {
|
||||
mf.Artist = s.mapArtistName(md)
|
||||
mf.AlbumArtistID = s.albumArtistID(md)
|
||||
mf.AlbumArtist = s.mapAlbumArtistName(md)
|
||||
if len(md.Genres()) > 0 {
|
||||
mf.Genre = md.Genres()[0]
|
||||
}
|
||||
mf.Genre, mf.Genres = s.mapGenres(md.Genres())
|
||||
mf.Compilation = md.Compilation()
|
||||
mf.Year = md.Year()
|
||||
mf.TrackNumber, _ = md.TrackNumber()
|
||||
@@ -131,3 +135,32 @@ func (s *mediaFileMapper) artistID(md *metadata.Tags) string {
|
||||
func (s *mediaFileMapper) albumArtistID(md *metadata.Tags) string {
|
||||
return fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(s.mapAlbumArtistName(md)))))
|
||||
}
|
||||
|
||||
func (s *mediaFileMapper) mapGenres(genres []string) (string, model.Genres) {
|
||||
var result model.Genres
|
||||
unique := map[string]struct{}{}
|
||||
var all []string
|
||||
for i := range genres {
|
||||
gs := strings.FieldsFunc(genres[i], func(r rune) bool {
|
||||
return strings.IndexRune(conf.Server.Scanner.GenreSeparators, r) != -1
|
||||
})
|
||||
for j := range gs {
|
||||
g := strings.TrimSpace(gs[j])
|
||||
key := strings.ToLower(g)
|
||||
if _, ok := unique[key]; ok {
|
||||
continue
|
||||
}
|
||||
all = append(all, g)
|
||||
unique[key] = struct{}{}
|
||||
}
|
||||
}
|
||||
for _, g := range all {
|
||||
genre := model.Genre{Name: g}
|
||||
_ = s.genres.Put(&genre)
|
||||
result = append(result, genre)
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
return result[0].Name, result
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/astaxie/beego/orm"
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/persistence"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
@@ -21,4 +26,40 @@ var _ = Describe("mapping", func() {
|
||||
Expect(sanitizeFieldForSorting("Õ Blésq Blom")).To(Equal("Blesq Blom"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("mapGenres", func() {
|
||||
var mapper *mediaFileMapper
|
||||
var gr model.GenreRepository
|
||||
var ctx context.Context
|
||||
BeforeEach(func() {
|
||||
ctx = context.Background()
|
||||
o := orm.NewOrm()
|
||||
gr = persistence.NewGenreRepository(ctx, o)
|
||||
gr = newCachedGenreRepository(ctx, gr)
|
||||
mapper = newMediaFileMapper("/", gr)
|
||||
})
|
||||
|
||||
It("returns empty if no genres are available", func() {
|
||||
g, gs := mapper.mapGenres(nil)
|
||||
Expect(g).To(BeEmpty())
|
||||
Expect(gs).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("returns genres", func() {
|
||||
g, gs := mapper.mapGenres([]string{"Rock", "Electronic"})
|
||||
Expect(g).To(Equal("Rock"))
|
||||
Expect(gs).To(HaveLen(2))
|
||||
Expect(gs[0].Name).To(Equal("Rock"))
|
||||
Expect(gs[1].Name).To(Equal("Electronic"))
|
||||
})
|
||||
|
||||
It("parses multi-valued genres", func() {
|
||||
g, gs := mapper.mapGenres([]string{"Rock;Dance", "Electronic", "Rock"})
|
||||
Expect(g).To(Equal("Rock"))
|
||||
Expect(gs).To(HaveLen(3))
|
||||
Expect(gs[0].Name).To(Equal("Rock"))
|
||||
Expect(gs[1].Name).To(Equal("Dance"))
|
||||
Expect(gs[2].Name).To(Equal("Electronic"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
)
|
||||
|
||||
type Extractor interface {
|
||||
@@ -138,7 +137,7 @@ func (t *Tags) getAllTagValues(tagNames ...string) []string {
|
||||
values = append(values, v...)
|
||||
}
|
||||
}
|
||||
return utils.UniqueStrings(values)
|
||||
return values
|
||||
}
|
||||
|
||||
func (t *Tags) getSortTag(originalTag string, tagNamess ...string) string {
|
||||
|
||||
@@ -66,7 +66,7 @@ var _ = Describe("Tags", func() {
|
||||
md := &Tags{}
|
||||
md.tags = map[string][]string{
|
||||
"genre": {"Rock", "Pop"},
|
||||
"_genre": {"New Wave", "Rock"},
|
||||
"_genre": {"New Wave"},
|
||||
}
|
||||
md.custom = map[string][]string{"genre": {"_genre"}}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ var _ = Describe("taglibExtractor", func() {
|
||||
Expect(m.Artist()).To(Equal("Artist"))
|
||||
Expect(m.AlbumArtist()).To(Equal("Album Artist"))
|
||||
Expect(m.Compilation()).To(BeTrue())
|
||||
Expect(m.Genres()).To(ConsistOf("Rock"))
|
||||
Expect(m.Genres()).To(ConsistOf("Rock", "Rock"))
|
||||
Expect(m.Year()).To(Equal(2014))
|
||||
n, t := m.TrackNumber()
|
||||
Expect(n).To(Equal(2))
|
||||
|
||||
@@ -3,6 +3,10 @@ package scanner
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/astaxie/beego/orm"
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/db"
|
||||
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/tests"
|
||||
. "github.com/onsi/ginkgo"
|
||||
@@ -11,6 +15,9 @@ import (
|
||||
|
||||
func TestScanner(t *testing.T) {
|
||||
tests.Init(t, true)
|
||||
conf.Server.DbPath = "file::memory:?cache=shared"
|
||||
_ = orm.RegisterDataBase("default", db.Driver, conf.Server.DbPath)
|
||||
db.EnsureLatestVersion()
|
||||
log.SetLevel(log.LevelCritical)
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Scanner Suite")
|
||||
|
||||
@@ -20,16 +20,15 @@ import (
|
||||
type TagScanner struct {
|
||||
rootFolder string
|
||||
ds model.DataStore
|
||||
mapper *mediaFileMapper
|
||||
cacheWarmer core.CacheWarmer
|
||||
plsSync *playlistSync
|
||||
cnt *counters
|
||||
cacheWarmer core.CacheWarmer
|
||||
mapper *mediaFileMapper
|
||||
}
|
||||
|
||||
func NewTagScanner(rootFolder string, ds model.DataStore, cacheWarmer core.CacheWarmer) *TagScanner {
|
||||
return &TagScanner{
|
||||
rootFolder: rootFolder,
|
||||
mapper: newMediaFileMapper(rootFolder),
|
||||
plsSync: newPlaylistSync(ds),
|
||||
ds: ds,
|
||||
cacheWarmer: cacheWarmer,
|
||||
@@ -83,6 +82,8 @@ func (s *TagScanner) Scan(ctx context.Context, lastModifiedSince time.Time, prog
|
||||
allFSDirs := dirMap{}
|
||||
var changedDirs []string
|
||||
s.cnt = &counters{}
|
||||
genres := newCachedGenreRepository(ctx, s.ds.Genre(ctx))
|
||||
s.mapper = newMediaFileMapper(s.rootFolder, genres)
|
||||
|
||||
foldersFound, walkerError := s.getRootFolderWalker(ctx)
|
||||
for {
|
||||
|
||||
Reference in New Issue
Block a user