Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve cover selection algorithm #516

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions scanner/coverresolve/cover.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package coverresolve

import (
"regexp"
"sort"
"strconv"
"strings"
)

var DefaultKeywords = []string{

Check failure on line 10 in scanner/coverresolve/cover.go

View workflow job for this annotation

GitHub Actions / Lint and test

DefaultKeywords is a global variable (gochecknoglobals)
"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

Check failure on line 73 in scanner/coverresolve/cover.go

View workflow job for this annotation

GitHub Actions / Lint and test

G602: Potentially accessing slice out of bounds (gosec)
}

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 changes: 85 additions & 0 deletions scanner/coverresolve/cover_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package coverresolve

import (
"testing"
)

func TestIsCover(t *testing.T) {

Check failure on line 7 in scanner/coverresolve/cover_test.go

View workflow job for this annotation

GitHub Actions / Lint and test

Function TestIsCover missing the call to method parallel
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 {

Check failure on line 22 in scanner/coverresolve/cover_test.go

View workflow job for this annotation

GitHub Actions / Lint and test

Range statement for test TestIsCover missing the call to method parallel in test Run
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) {

Check failure on line 32 in scanner/coverresolve/cover_test.go

View workflow job for this annotation

GitHub Actions / Lint and test

unnecessary leading newline (whitespace)

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 {

Check failure on line 76 in scanner/coverresolve/cover_test.go

View workflow job for this annotation

GitHub Actions / Lint and test

Range statement for test TestSelectCover missing the call to method parallel in test Run
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)
}
})
}
}
29 changes: 6 additions & 23 deletions scanner/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down
Loading