package meta import ( "path" "regexp" "strconv" "strings" ) // UnknownArtist — исполнитель, если его нет ни в тегах, ни в пути. const UnknownArtist = "Unknown artist" // Fields — значения метаданных из одного источника (теги файла или шаблон пути). Пусто — нет значения. type Fields struct { Artist string AlbumArtist string Album string Title string Genre string Year string Track string Disc string } // FromPath собирает Fields из групп шаблона. func FromPath(m map[string]string) Fields { return Fields{ Artist: m["artist"], AlbumArtist: m["albumartist"], Album: m["album"], Title: m["title"], Genre: m["genre"], Year: m["year"], Track: m["track"], Disc: m["disc"], } } // Meta — итоговые метаданные файла. type Meta struct { Artist string // поле исполнителя целиком (разбивается SplitArtists) AlbumArtist string Album string Title string Genres []string Year int Track int Disc int } // Merge сводит теги и поля из пути. tagsFirst: тег важнее пути (по умолчанию), иначе наоборот. // Без обоих значений: title — имя файла без номера и расширения, artist — UnknownArtist, // album — имя папки, albumartist — artist. func Merge(tags, fromPath Fields, tagsFirst bool, relPath string) Meta { first, second := tags, fromPath if !tagsFirst { first, second = fromPath, tags } pick := func(a, b string) string { if v := Clean(a); v != "" { return v } return Clean(b) } m := Meta{ Artist: pick(first.Artist, second.Artist), AlbumArtist: pick(first.AlbumArtist, second.AlbumArtist), Album: pick(first.Album, second.Album), Title: pick(first.Title, second.Title), Year: parseYear(pick(first.Year, second.Year)), Track: parseNumber(pick(first.Track, second.Track)), Disc: parseNumber(pick(first.Disc, second.Disc)), } if g := pick(first.Genre, second.Genre); g != "" { m.Genres = splitGenres(g) } if m.Title == "" { m.Title = titleFromFile(relPath) } if m.Artist == "" { m.Artist = m.AlbumArtist } if m.Artist == "" { m.Artist = UnknownArtist } if m.AlbumArtist == "" { m.AlbumArtist = m.Artist } if m.Album == "" { if dir := path.Base(path.Dir(relPath)); dir != "." && dir != "/" { m.Album = Clean(dir) } } return m } var yearRe = regexp.MustCompile(`\b(1[89]\d\d|20\d\d)\b`) // parseYear достаёт год из «2019», «2019-05-01», «(1999)». func parseYear(s string) int { if y := yearRe.FindString(s); y != "" { n, _ := strconv.Atoi(y) return n } return 0 } // parseNumber разбирает «3», «03», «3/12». func parseNumber(s string) int { s, _, _ = strings.Cut(s, "/") n, err := strconv.Atoi(strings.TrimSpace(s)) if err != nil || n < 0 || n > 9999 { return 0 } return n } func splitGenres(s string) []string { var out []string seen := map[string]bool{} for _, g := range splitAny(s, []string{";", "/", ","}) { g = Clean(g) if k := Key(g); k != "" && !seen[k] { seen[k] = true out = append(out, g) } } return out } var leadingNumber = regexp.MustCompile(`^\s*(?:\d{1,2}[-.])?\d{1,3}\s*[-._)]*\s*`) // titleFromFile: «03 - Song name.flac» → «Song name». func titleFromFile(relPath string) string { name := path.Base(relPath) name = strings.TrimSuffix(name, path.Ext(name)) if t := Clean(leadingNumber.ReplaceAllString(name, "")); t != "" { return t } return Clean(name) }