diff --git a/scanner/coverresolve/cover.go b/scanner/coverresolve/cover.go new file mode 100644 index 00000000..48069250 --- /dev/null +++ b/scanner/coverresolve/cover.go @@ -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 +} diff --git a/scanner/coverresolve/cover_test.go b/scanner/coverresolve/cover_test.go new file mode 100644 index 00000000..27327ca0 --- /dev/null +++ b/scanner/coverresolve/cover_test.go @@ -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) + } + }) + } +} diff --git a/scanner/scanner.go b/scanner/scanner.go index adbd8d4d..d57dc780 100644 --- a/scanner/scanner.go +++ b/scanner/scanner.go @@ -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.