feat: improve cover selection algorithm
Some checks failed
Some checks failed
This commit is contained in:
83
scanner/coverresolve/cover.go
Normal file
83
scanner/coverresolve/cover.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package coverresolve
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var DefaultKeywords = []string{
|
||||
"cover",
|
||||
"folder",
|
||||
"front",
|
||||
"albumart",
|
||||
"album",
|
||||
"artist",
|
||||
"scan",
|
||||
}
|
||||
|
||||
// Helper function to extract the number from the filename
|
||||
func extractNumber(filename string) int {
|
||||
re := regexp.MustCompile(`\d+`)
|
||||
matches := re.FindAllString(filename, -1)
|
||||
if len(matches) == 0 {
|
||||
return 0
|
||||
}
|
||||
num, _ := strconv.Atoi(matches[0])
|
||||
return num
|
||||
}
|
||||
|
||||
type CoverAlternative struct {
|
||||
Name string
|
||||
Score int
|
||||
}
|
||||
|
||||
func SelectCover(covers []string) string {
|
||||
if len(covers) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
coverAlternatives := make([]CoverAlternative, 0)
|
||||
|
||||
for _, keyword := range DefaultKeywords {
|
||||
if len(coverAlternatives) > 0 {
|
||||
break
|
||||
}
|
||||
|
||||
for _, cover := range covers {
|
||||
if strings.Contains(strings.ToLower(cover), keyword) {
|
||||
coverAlternatives = append(coverAlternatives, CoverAlternative{
|
||||
Name: cover,
|
||||
Score: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parse the integer from the filename
|
||||
// eg. cover(1).jpg will have higher score than cover(114514).jpg
|
||||
for i := range coverAlternatives {
|
||||
coverAlternatives[i].Score -= extractNumber(coverAlternatives[i].Name)
|
||||
}
|
||||
|
||||
// sort by score
|
||||
sort.Slice(coverAlternatives, func(i, j int) bool {
|
||||
return coverAlternatives[i].Score > coverAlternatives[j].Score
|
||||
})
|
||||
|
||||
if len(coverAlternatives) == 0 {
|
||||
return covers[0]
|
||||
}
|
||||
|
||||
return coverAlternatives[0].Name
|
||||
}
|
||||
|
||||
func IsCover(name string) bool {
|
||||
for _, ext := range []string{"jpg", "jpeg", "png", "bmp", "gif"} {
|
||||
if strings.HasSuffix(strings.ToLower(name), "."+ext) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
85
scanner/coverresolve/cover_test.go
Normal file
85
scanner/coverresolve/cover_test.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package coverresolve
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsCover(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
filename string
|
||||
expected bool
|
||||
}{
|
||||
{"JPEG file", "Image.jpg", true},
|
||||
{"JPEG file", "image.jpg", true},
|
||||
{"PNG file", "picture.png", true},
|
||||
{"BMP file", "photo.bmp", true},
|
||||
{"GIF file", "animation.gif", true},
|
||||
{"Non-image file", "document.pdf", false},
|
||||
{"Empty file name", "", false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result := IsCover(test.filename)
|
||||
if result != test.expected {
|
||||
t.Errorf("Expected IsCover(%q) to be %v, but got %v", test.filename, test.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectCover(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
covers []string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Empty covers slice",
|
||||
covers: []string{},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "Covers without keywords or numbers case sensitive",
|
||||
covers: []string{"Cover1.jpg", "cover2.png"},
|
||||
expected: "Cover1.jpg",
|
||||
},
|
||||
{
|
||||
name: "Covers without keywords or numbers",
|
||||
covers: []string{"cover1.jpg", "cover2.png"},
|
||||
expected: "cover1.jpg",
|
||||
},
|
||||
{
|
||||
name: "Covers with keywords and numbers",
|
||||
covers: []string{"cover12.jpg", "cover2.png", "special_cover1.jpg"},
|
||||
expected: "special_cover1.jpg",
|
||||
},
|
||||
{
|
||||
name: "Covers with keywords but without numbers",
|
||||
covers: []string{"cover12.jpg", "cover_keyword.png"},
|
||||
expected: "cover_keyword.png",
|
||||
},
|
||||
{
|
||||
name: "Covers without keywords but with numbers",
|
||||
covers: []string{"cover1.jpg", "cover12.png"},
|
||||
expected: "cover1.jpg",
|
||||
},
|
||||
{
|
||||
name: "Covers with same highest score",
|
||||
covers: []string{"cover1.jpg", "cover2.jpg", "cover_special.jpg"},
|
||||
expected: "cover_special.jpg",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
// Mock the DefaultScoreRules
|
||||
result := SelectCover(test.covers)
|
||||
if result != test.expected {
|
||||
t.Errorf("Expected SelectCover(%v) to be %q, but got %q", test.covers, test.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
|
||||
"go.senan.xyz/gonic/db"
|
||||
"go.senan.xyz/gonic/fileutil"
|
||||
"go.senan.xyz/gonic/scanner/coverresolve"
|
||||
"go.senan.xyz/gonic/tags/tagcommon"
|
||||
)
|
||||
|
||||
@@ -266,7 +267,7 @@ func (s *Scanner) scanDir(tx *db.DB, st *State, absPath string) error {
|
||||
}
|
||||
|
||||
var tracks []string
|
||||
var cover string
|
||||
var covers []string
|
||||
for _, item := range items {
|
||||
absPath := filepath.Join(absPath, item.Name())
|
||||
if s.excludePattern != nil && s.excludePattern.MatchString(absPath) {
|
||||
@@ -277,8 +278,8 @@ func (s *Scanner) scanDir(tx *db.DB, st *State, absPath string) error {
|
||||
continue
|
||||
}
|
||||
|
||||
if isCover(item.Name()) {
|
||||
cover = item.Name()
|
||||
if coverresolve.IsCover(item.Name()) {
|
||||
covers = append(covers, item.Name())
|
||||
continue
|
||||
}
|
||||
if s.tagReader.CanRead(absPath) {
|
||||
@@ -287,6 +288,8 @@ func (s *Scanner) scanDir(tx *db.DB, st *State, absPath string) error {
|
||||
}
|
||||
}
|
||||
|
||||
cover := coverresolve.SelectCover(covers)
|
||||
|
||||
pdir, pbasename := filepath.Split(filepath.Dir(relPath))
|
||||
var parent db.Album
|
||||
if err := tx.Where("root_dir=? AND left_path=? AND right_path=?", musicDir, pdir, pbasename).Assign(db.Album{RootDir: musicDir, LeftPath: pdir, RightPath: pbasename}).FirstOrCreate(&parent).Error; err != nil {
|
||||
@@ -671,26 +674,6 @@ func (s *Scanner) cleanGenres(st *State) error { //nolint:unparam
|
||||
return nil
|
||||
}
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var coverNames = map[string]struct{}{}
|
||||
|
||||
//nolint:gochecknoinits
|
||||
func init() {
|
||||
for _, name := range []string{"cover", "folder", "front", "albumart", "album", "artist"} {
|
||||
for _, ext := range []string{"jpg", "jpeg", "png", "bmp", "gif"} {
|
||||
coverNames[fmt.Sprintf("%s.%s", name, ext)] = struct{}{}
|
||||
for i := 0; i < 3; i++ {
|
||||
coverNames[fmt.Sprintf("%s.%d.%s", name, i, ext)] = struct{}{} // support beets extras
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isCover(name string) bool {
|
||||
_, ok := coverNames[strings.ToLower(name)]
|
||||
return ok
|
||||
}
|
||||
|
||||
// decoded converts a string to it's latin equivalent.
|
||||
// it will be used by the model's *UDec fields, and is only set if it
|
||||
// differs from the original. the fields are used for searching.
|
||||
|
||||
Reference in New Issue
Block a user