// Package meta — метаданные треков: нормализация имён, шаблоны путей, слияние с тегами // (docs/library-and-storage.md). package meta import ( "strings" "unicode" "golang.org/x/text/unicode/norm" ) // Clean приводит значение к виду для показа: NFC, `_` → пробел, без лишних пробелов. func Clean(s string) string { s = norm.NFC.String(s) s = strings.ReplaceAll(s, "_", " ") return strings.Join(strings.Fields(s), " ") } // Key — ключ сопоставления (name_norm): нижний регистр, ё → е, без пунктуации, пробелы схлопнуты. // «The Beatles», «the beatles!» и «THE BEATLES» дают один ключ. func Key(s string) string { s = norm.NFKC.String(s) var b strings.Builder b.Grow(len(s)) space := false for _, r := range strings.ToLower(s) { switch { case r == 'ё': r = 'е' case unicode.IsPunct(r) || unicode.IsSymbol(r) || unicode.IsSpace(r) || r == '_': space = b.Len() > 0 continue } if space { b.WriteByte(' ') space = false } b.WriteRune(r) } return b.String() } // SortName — имя для сортировки исполнителей: без ведущего артикля «The ». func SortName(name string) string { if rest, ok := strings.CutPrefix(name, "The "); ok && rest != "" { return rest } return name } // Разделители нескольких исполнителей в одном поле, кроме « & ». var artistSeparators = []string{";", " / ", " feat. ", " feat ", " ft. ", " Feat. ", " Ft. ", " featuring "} // ArtistCredit — исполнитель трека и его роль. type ArtistCredit struct { Name string Role string // main | feat } // SplitArtists разбирает поле исполнителя на отдельных. « & » делит имя, только если составного // исполнителя нет в библиотеке (known), — иначе «Simon & Garfunkel» распался бы на двоих. // Всё после feat./ft. — приглашённые исполнители. func SplitArtists(s string, known func(key string) bool) []ArtistCredit { s = Clean(s) if s == "" { return nil } var out []ArtistCredit seen := map[string]bool{} add := func(name, role string) { name = Clean(name) k := Key(name) if k == "" || seen[k] { return } seen[k] = true out = append(out, ArtistCredit{Name: name, Role: role}) } main, feat := s, "" for _, sep := range []string{" feat. ", " feat ", " ft. ", " Feat. ", " Ft. ", " featuring "} { if i := strings.Index(main, sep); i >= 0 { main, feat = main[:i], main[i+len(sep):] break } } for _, part := range splitAny(main, artistSeparators) { for _, seg := range splitComma(part) { if strings.Contains(seg, " & ") && (known == nil || !known(Key(seg))) { for _, p := range strings.Split(seg, " & ") { add(p, "main") } continue } add(seg, "main") } } for _, part := range splitAny(feat, append(artistSeparators, " & ", ", ")) { add(part, "feat") } return out } // splitComma делит по «, » без оглядки на библиотеку: иначе уже склеенное имя («A, B») навсегда // остаётся «известным». Продолжение «the …» — часть имени («Tyler, the Creator»). func splitComma(s string) []string { var out []string for _, p := range strings.Split(s, ", ") { if n := len(out); n > 0 && strings.HasPrefix(strings.ToLower(p), "the ") { out[n-1] += ", " + p continue } out = append(out, p) } return out } func splitAny(s string, seps []string) []string { parts := []string{s} for _, sep := range seps { var next []string for _, p := range parts { next = append(next, strings.Split(p, sep)...) } parts = next } return parts }