Scans now mark slave-only songs as missing from the master when the master has their album folder or at least their artist folder. A new Missing from master filter in Review lists them, split by album and artist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
92 lines
2.5 KiB
Go
92 lines
2.5 KiB
Go
package engine
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// A slave-only song is "missing from the master" when the master has its
|
|
// album folder (the song was probably never added there) or at least its
|
|
// artist folder (the album is missing). Folders are compared with disc
|
|
// folders ignored and release labels such as "Deluxe Edition" stripped, so
|
|
// "Robot Face (Deluxe)" in the slave finds "Robot Face" in the master.
|
|
|
|
// Missing levels.
|
|
const (
|
|
MissingAlbum = "album"
|
|
MissingArtist = "artist"
|
|
)
|
|
|
|
// folderNorm is a folder name reduced to letters and digits, release labels removed.
|
|
func folderNorm(name string) string {
|
|
if w := words(stripRelease(name)); len(w) > 0 {
|
|
return strings.Join(w, "")
|
|
}
|
|
return strings.ToLower(name)
|
|
}
|
|
|
|
// folderKeys returns the normalized artist key and album key of a song's
|
|
// folders; the album key is empty for a song directly in the artist folder.
|
|
func folderKeys(rel string) (artist, album string) {
|
|
var parts []string
|
|
for _, p := range strings.Split(filepath.Dir(rel), string(filepath.Separator)) {
|
|
if p != "." && !discFolder.MatchString(p) {
|
|
parts = append(parts, folderNorm(p))
|
|
}
|
|
}
|
|
if len(parts) == 0 {
|
|
return "", ""
|
|
}
|
|
if len(parts) > 1 {
|
|
album = strings.Join(parts, "/")
|
|
}
|
|
return parts[0], album
|
|
}
|
|
|
|
// masterFolders indexes the master's artist and album folders by key, to
|
|
// their real spelling.
|
|
type masterFolders struct {
|
|
artists map[string]string
|
|
albums map[string]string
|
|
}
|
|
|
|
func buildMasterFolders(rels []string) masterFolders {
|
|
mf := masterFolders{artists: map[string]string{}, albums: map[string]string{}}
|
|
for _, rel := range rels {
|
|
artist, album := folderKeys(rel)
|
|
if artist == "" {
|
|
continue
|
|
}
|
|
if _, ok := mf.artists[artist]; !ok {
|
|
mf.artists[artist] = firstComponent(rel)
|
|
}
|
|
if album != "" {
|
|
if _, ok := mf.albums[album]; !ok {
|
|
d := filepath.Dir(rel)
|
|
for d != "." && discFolder.MatchString(filepath.Base(d)) {
|
|
d = filepath.Dir(d)
|
|
}
|
|
mf.albums[album] = d
|
|
}
|
|
}
|
|
}
|
|
return mf
|
|
}
|
|
|
|
// missing reports whether the master has a slave-only song's album or
|
|
// artist, with a reason naming the master's folder.
|
|
func (mf masterFolders) missing(relSlave string) (level, reason string) {
|
|
artist, album := folderKeys(relSlave)
|
|
if artist == "" {
|
|
return "", ""
|
|
}
|
|
if d, ok := mf.albums[album]; ok && album != "" {
|
|
return MissingAlbum, fmt.Sprintf("the master has the album %q but not this song", d)
|
|
}
|
|
if d, ok := mf.artists[artist]; ok {
|
|
return MissingArtist, fmt.Sprintf("the master has the artist %q but not this album", d)
|
|
}
|
|
return "", ""
|
|
}
|