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/db"
|
||||||
"go.senan.xyz/gonic/fileutil"
|
"go.senan.xyz/gonic/fileutil"
|
||||||
|
"go.senan.xyz/gonic/scanner/coverresolve"
|
||||||
"go.senan.xyz/gonic/tags/tagcommon"
|
"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 tracks []string
|
||||||
var cover string
|
var covers []string
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
absPath := filepath.Join(absPath, item.Name())
|
absPath := filepath.Join(absPath, item.Name())
|
||||||
if s.excludePattern != nil && s.excludePattern.MatchString(absPath) {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if isCover(item.Name()) {
|
if coverresolve.IsCover(item.Name()) {
|
||||||
cover = item.Name()
|
covers = append(covers, item.Name())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if s.tagReader.CanRead(absPath) {
|
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))
|
pdir, pbasename := filepath.Split(filepath.Dir(relPath))
|
||||||
var parent db.Album
|
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 {
|
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
|
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.
|
// 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
|
// 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.
|
// differs from the original. the fields are used for searching.
|
||||||
|
|||||||
Reference in New Issue
Block a user