347f525801
Three-level core resolution with priority overrides → dictionaries/model → suspects: 1. dicts/overrides.txt (highest priority) Point fixes via exact lookup on Raw BEFORE the positional logic. Format: <original line> => artist | title. An override fixes Artist/Title and shifts the dictionary tail zone by the actual core length (overrideCoreLength, fallback 2 tokens). 2. dicts/artists.txt Whitelist of hyphenated artists (Iku-Turso, Post-Mortem, ...): a [0]-[1] hit → Artist = 2 tokens, Title = [2], tail zone shifted. One entry covers all releases of the artist automatically. 3. Suspected title continuation Unknown tokens between the title zone and the tail (followed later by dictionary markers) no longer pollute UnknownTokens; they go to a dedicated TitleExtra field + warning "suspected title continuation" — a reviewer hint that the core is probably longer than 2 tokens. Catalogue numbers (parenthesized tokens) are resolved with FIRST priority and never land in suspects. Type field (normalized base release type): - new normalization map dicts/types.txt (<Base> = variants): EP = EP, CDS, CDM, 7INCH_VINYL, CDEP, ..., REMASTERED_EP; Demo = ...; Split; Single; Compilation; Collaboration (curator-extendable) - tokens matched against edition/source are additionally normalized to the base type (first match wins); edition.txt/source.txt untouched, Source stays raw - TypeDistribution map[string]int added to GroupStats and the report (console/JSON) - OST/INSTRUMENTAL are release traits, not types; excluded from the map Tests: old cases updated to the new suspect semantics; acceptance tests added for overrides (Milicia_Oscura-Anti-Cristo), artist dictionary (Iku-Turso), the suspect section, Type normalization (CDEP → EP with raw Source=CDEP, first-match-wins) and TypeDistribution. go build / go vet / go test ./internal/releaseparser/... — all green.
485 lines
18 KiB
Go
485 lines
18 KiB
Go
package groups
|
||
|
||
import (
|
||
"testing"
|
||
)
|
||
|
||
func TestParseFullTail(t *testing.T) {
|
||
r := Parse("Fane-Dragged_Through_the_Unending-24BIT-WEB-FLAC-2025-MOONBLOOD")
|
||
if r.Artist != "Fane" {
|
||
t.Errorf("Artist = %q, want %q", r.Artist, "Fane")
|
||
}
|
||
if r.Title != "Dragged_Through_the_Unending" {
|
||
t.Errorf("Title = %q, want %q", r.Title, "Dragged_Through_the_Unending")
|
||
}
|
||
if r.Year != 2025 || r.Format != "FLAC" || r.Source != "WEB" || r.BitDepth != "24BIT" {
|
||
t.Errorf("tail fields wrong: %+v", r)
|
||
}
|
||
if len(r.UnknownTokens) != 0 {
|
||
t.Errorf("UnknownTokens = %v, want empty", r.UnknownTokens)
|
||
}
|
||
if r.Confidence < 0.99 {
|
||
t.Errorf("Confidence = %.2f, want ~1.0", r.Confidence)
|
||
}
|
||
}
|
||
|
||
func TestParseEditionAndDemo(t *testing.T) {
|
||
r := Parse("Satans_Grind-Rotten_Demo-DEMO-16BIT-WEB-FLAC-2016-MOONBLOOD")
|
||
if r.Edition != "DEMO" {
|
||
t.Errorf("Edition = %q, want %q", r.Edition, "DEMO")
|
||
}
|
||
if r.Artist != "Satans_Grind" || r.Title != "Rotten_Demo" {
|
||
t.Errorf("Artist/Title = %q/%q", r.Artist, r.Title)
|
||
}
|
||
if len(r.UnknownTokens) != 0 {
|
||
t.Errorf("UnknownTokens = %v, want empty", r.UnknownTokens)
|
||
}
|
||
}
|
||
|
||
func TestParseArtistWithHyphen(t *testing.T) {
|
||
// Artist сам містить дефіс, але Ultra-Violence ще не в словнику:
|
||
// модель [0]/[1] лишається, а All_My_Life (за ним ідуть словникові
|
||
// маркери) — suspected title continuation, не unknown.
|
||
r := Parse("Ultra-Violence-All_My_Life-EP-16BIT-WEB-FLAC-2026-MOONBLOOD")
|
||
if r.Artist != "Ultra" || r.Title != "Violence" {
|
||
t.Errorf("Artist/Title = %q/%q, want Ultra / Violence", r.Artist, r.Title)
|
||
}
|
||
if r.Edition != "EP" || r.BitDepth != "16BIT" || r.Source != "WEB" || r.Year != 2026 {
|
||
t.Errorf("tail fields wrong: %+v", r)
|
||
}
|
||
if len(r.TitleExtra) != 1 || r.TitleExtra[0] != "All_My_Life" {
|
||
t.Errorf("TitleExtra = %v, want [All_My_Life]", r.TitleExtra)
|
||
}
|
||
if !contains(r.Warnings, "suspected title continuation") {
|
||
t.Errorf("Warnings = %v, want suspected title continuation", r.Warnings)
|
||
}
|
||
if len(r.UnknownTokens) != 0 {
|
||
t.Errorf("UnknownTokens = %v, want empty", r.UnknownTokens)
|
||
}
|
||
}
|
||
|
||
func TestParseLanguageAndCDSource(t *testing.T) {
|
||
r := Parse("Burn_Thou_in_Effigy-Memorias_Postumas_do_Imperador-BR-CD-FLAC-2026-MOONBLOOD")
|
||
if r.Language != "BR" || r.Source != "CD" || r.Year != 2026 || r.Format != "FLAC" {
|
||
t.Errorf("tail fields wrong: %+v", r)
|
||
}
|
||
if len(r.UnknownTokens) != 0 {
|
||
t.Errorf("UnknownTokens = %v, want empty", r.UnknownTokens)
|
||
}
|
||
}
|
||
|
||
func TestParseNoYear(t *testing.T) {
|
||
// Without a year the tail still parses; group stays the last token.
|
||
r := Parse("Fane-Gallant_Shroud-EP-16BIT-WEB-FLAC-MOONBLOOD")
|
||
if r.Group != "MOONBLOOD" || r.Year != 0 {
|
||
t.Errorf("Group/Year = %q/%d", r.Group, r.Year)
|
||
}
|
||
if r.Edition != "EP" || r.Source != "WEB" || r.Format != "FLAC" {
|
||
t.Errorf("tail fields wrong: %+v", r)
|
||
}
|
||
if r.Artist != "Fane" || r.Title != "Gallant_Shroud" {
|
||
t.Errorf("Artist/Title = %q/%q", r.Artist, r.Title)
|
||
}
|
||
}
|
||
|
||
func TestParseResolutionSlot(t *testing.T) {
|
||
r := Parse("Ice_Nine_Kills-Last_Chance_To_Make_Amends-REMASTERED-24BIT-44KHZ-WEB-FLAC-2024-FLACCiD")
|
||
if r.Group != "FLACCiD" {
|
||
t.Errorf("Group = %q, want FLACCiD", r.Group)
|
||
}
|
||
if r.Resolution != "44KHZ" {
|
||
t.Errorf("Resolution = %q, want 44KHZ", r.Resolution)
|
||
}
|
||
if r.BitDepth != "24BIT" || r.Edition != "REMASTERED" || r.Source != "WEB" || r.Year != 2024 {
|
||
t.Errorf("tail fields wrong: %+v", r)
|
||
}
|
||
if r.Artist != "Ice_Nine_Kills" || r.Title != "Last_Chance_To_Make_Amends" {
|
||
t.Errorf("Artist/Title = %q/%q", r.Artist, r.Title)
|
||
}
|
||
if len(r.UnknownTokens) != 0 {
|
||
t.Errorf("UnknownTokens = %v, want empty", r.UnknownTokens)
|
||
}
|
||
}
|
||
|
||
func TestParseVinylIsSource(t *testing.T) {
|
||
r := Parse("ROTH-Alles_Endet-DE-VINYL-FLAC-2026-TOTENKVLT")
|
||
if r.Language != "DE" {
|
||
t.Errorf("Language = %q, want DE", r.Language)
|
||
}
|
||
if r.Source != "VINYL" {
|
||
t.Errorf("Source = %q, want VINYL", r.Source)
|
||
}
|
||
if r.Edition != "" {
|
||
t.Errorf("Edition = %q, want empty (VINYL is source, not edition)", r.Edition)
|
||
}
|
||
if len(r.UnknownTokens) != 0 {
|
||
t.Errorf("UnknownTokens = %v, want empty", r.UnknownTokens)
|
||
}
|
||
}
|
||
|
||
func TestParseCaseInsensitiveDictionaries(t *testing.T) {
|
||
r := Parse("Artist-Some_Title-16bit-web-flac-2020-moonblood")
|
||
// Значення зберігаються в оригінальному регістрі (як у рядку),
|
||
// а звірка зі словниками йде регістронезалежно.
|
||
if r.BitDepth != "16bit" || r.Source != "web" || r.Format != "flac" {
|
||
t.Errorf("case-insensitive lookup failed: %+v", r)
|
||
}
|
||
if r.Artist != "Artist" || r.Title != "Some_Title" {
|
||
t.Errorf("Artist/Title = %q/%q", r.Artist, r.Title)
|
||
}
|
||
}
|
||
|
||
func TestParseUnknownTailTokenReported(t *testing.T) {
|
||
// Невідомий токен ПІСЛЯ останнього словникового маркера (за ним уже
|
||
// нічого відомого немає) → у UnknownTokens.
|
||
r := Parse("Artist-Some_Title-16BIT-WEB-FLAC-FOO-2025-MOONBLOOD")
|
||
if len(r.UnknownTokens) != 1 || r.UnknownTokens[0] != "FOO" {
|
||
t.Errorf("UnknownTokens = %v, want [FOO]", r.UnknownTokens)
|
||
}
|
||
if len(r.TitleExtra) != 0 {
|
||
t.Errorf("TitleExtra = %v, want empty", r.TitleExtra)
|
||
}
|
||
if r.Edition != "" || r.BitDepth != "16BIT" || r.Source != "WEB" || r.Year != 2025 {
|
||
t.Errorf("tail fields wrong: %+v", r)
|
||
}
|
||
if r.Artist != "Artist" || r.Title != "Some_Title" {
|
||
t.Errorf("Artist/Title = %q/%q", r.Artist, r.Title)
|
||
}
|
||
}
|
||
|
||
func TestParseSuspectedTitleContinuation(t *testing.T) {
|
||
// Невідомий токен МІЖ title-зоною і хвостом (за ним далі йдуть
|
||
// словникові маркери) → не UnknownTokens, а TitleExtra + warning.
|
||
r := Parse("Artist-Some_Title-SPLIT-FOO-16BIT-WEB-FLAC-2025-MOONBLOOD")
|
||
if len(r.TitleExtra) != 1 || r.TitleExtra[0] != "FOO" {
|
||
t.Errorf("TitleExtra = %v, want [FOO]", r.TitleExtra)
|
||
}
|
||
if len(r.UnknownTokens) != 0 {
|
||
t.Errorf("UnknownTokens = %v, want empty", r.UnknownTokens)
|
||
}
|
||
if !contains(r.Warnings, "suspected title continuation") {
|
||
t.Errorf("Warnings = %v, want suspected title continuation", r.Warnings)
|
||
}
|
||
if r.Edition != "SPLIT" || r.BitDepth != "16BIT" || r.Source != "WEB" || r.Year != 2025 {
|
||
t.Errorf("tail fields wrong: %+v", r)
|
||
}
|
||
}
|
||
|
||
func TestParseCoreTokensNeverReported(t *testing.T) {
|
||
// Артист/тайтл ніколи не потрапляють в UnknownTokens.
|
||
r := Parse("Fane-Dragged_Through_the_Unending-24BIT-WEB-FLAC-2025-MOONBLOOD")
|
||
if len(r.UnknownTokens) != 0 {
|
||
t.Errorf("UnknownTokens = %v, want empty", r.UnknownTokens)
|
||
}
|
||
if r.Artist != "Fane" || r.Title != "Dragged_Through_the_Unending" {
|
||
t.Errorf("Artist/Title = %q/%q", r.Artist, r.Title)
|
||
}
|
||
}
|
||
|
||
func TestParseAnyGroupNoWarning(t *testing.T) {
|
||
cases := []string{
|
||
"Jetsex-Down_Down_Down-24BIT-WEB-FLAC-2025-FLACCiD",
|
||
"Sabaton-The_Last_Stand-24BIT-96KHZ-WEB-FLAC-2016-FLACCiD",
|
||
"Wind_Walkers-What_If_I_Break-16BIT-WEB-FLAC-2023-FLACCiD",
|
||
}
|
||
for _, line := range cases {
|
||
r := Parse(line)
|
||
if r.Group != "FLACCiD" {
|
||
t.Errorf("%s: Group = %q, want FLACCiD", line, r.Group)
|
||
}
|
||
if len(r.Warnings) != 0 {
|
||
t.Errorf("%s: Warnings = %v, want empty", line, r.Warnings)
|
||
}
|
||
if len(r.UnknownTokens) != 0 {
|
||
t.Errorf("%s: UnknownTokens = %v, want empty", line, r.UnknownTokens)
|
||
}
|
||
if r.Year == 0 || r.Format != "FLAC" || r.Source != "WEB" || r.BitDepth == "" {
|
||
t.Errorf("%s: tail fields wrong: %+v", line, r)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestParseCommentExtracted(t *testing.T) {
|
||
r := Parse("Rain_City_Drive-Rain_City_Drive-DELUXE_EDITION-REPACK-16BIT-WEB-FLAC-2023-FLACCiD")
|
||
if r.Comment != "REPACK" {
|
||
t.Errorf("Comment = %q, want REPACK", r.Comment)
|
||
}
|
||
if r.Edition != "DELUXE_EDITION" || r.BitDepth != "16BIT" || r.Source != "WEB" || r.Year != 2023 {
|
||
t.Errorf("tail fields wrong: %+v", r)
|
||
}
|
||
if r.Artist != "Rain_City_Drive" || r.Title != "Rain_City_Drive" {
|
||
t.Errorf("Artist/Title = %q/%q", r.Artist, r.Title)
|
||
}
|
||
if len(r.UnknownTokens) != 0 || len(r.Warnings) != 0 {
|
||
t.Errorf("UnknownTokens=%v Warnings=%v, want empty", r.UnknownTokens, r.Warnings)
|
||
}
|
||
}
|
||
|
||
func TestParseNumericArtistNotAYear(t *testing.T) {
|
||
// [0]=Artist — навіть якщо це число-рік (1914, 1928), а не "рік" токен.
|
||
for _, line := range []string{
|
||
"1914-Eschatology_of_War-24BIT-WEB-FLAC-2015-MOONBLOOD",
|
||
"1928-Lappel_du_Spectre-FR-CD-FLAC-2025-MOONBLOOD",
|
||
} {
|
||
r := Parse(line)
|
||
if r.Artist != "1914" && r.Artist != "1928" {
|
||
t.Errorf("%s: Artist = %q, want numeric artist", line, r.Artist)
|
||
}
|
||
if len(r.UnknownTokens) != 0 {
|
||
t.Errorf("%s: UnknownTokens = %v, want empty", line, r.UnknownTokens)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestParseCatalogNumber(t *testing.T) {
|
||
r := Parse("Antidot-Beyond_Trance_And_Time-(GLOBAL006)-REPACK-EP-FLAC-2026-AFTERHOUR")
|
||
if r.CatalogNo != "GLOBAL006" {
|
||
t.Errorf("CatalogNo = %q, want GLOBAL006", r.CatalogNo)
|
||
}
|
||
if r.Comment != "REPACK" || r.Edition != "EP" || r.Format != "FLAC" || r.Year != 2026 {
|
||
t.Errorf("tail fields wrong: %+v", r)
|
||
}
|
||
if r.Artist != "Antidot" || r.Title != "Beyond_Trance_And_Time" {
|
||
t.Errorf("Artist/Title = %q/%q", r.Artist, r.Title)
|
||
}
|
||
if len(r.UnknownTokens) != 0 {
|
||
t.Errorf("UnknownTokens = %v, want empty", r.UnknownTokens)
|
||
}
|
||
}
|
||
|
||
func TestParseCatalogNumberWithDashesInside(t *testing.T) {
|
||
r := Parse("Murder_Intentions-Excessive_Display_Of_Human_Nature-(BRC30-001-CD-RRR108)-CD-FLAC-2018-86D")
|
||
if r.CatalogNo != "BRC30-001-CD-RRR108" {
|
||
t.Errorf("CatalogNo = %q, want BRC30-001-CD-RRR108", r.CatalogNo)
|
||
}
|
||
if r.Source != "CD" || r.Format != "FLAC" || r.Year != 2018 || r.Group != "86D" {
|
||
t.Errorf("tail fields wrong: %+v", r)
|
||
}
|
||
if r.Artist != "Murder_Intentions" || r.Title != "Excessive_Display_Of_Human_Nature" {
|
||
t.Errorf("Artist/Title = %q/%q", r.Artist, r.Title)
|
||
}
|
||
if len(r.UnknownTokens) != 0 {
|
||
t.Errorf("UnknownTokens = %v, want empty", r.UnknownTokens)
|
||
}
|
||
}
|
||
|
||
func TestParseCatalogNumberWithDotAndRoman(t *testing.T) {
|
||
// Катало у дужках у середній зоні — незалежно від вмісту
|
||
// (крапка, римські, букви, дефіси): все валідне.
|
||
cases := []struct{ line, catno string }{
|
||
{"Nervosa-Perpetual_Chaos-(DYN.3140-2)-LIMITED_EDITION-CD-FLAC-2021-MOONBLOOD", "DYN.3140-2"},
|
||
{"Black_Ceremonial_Kult-Crowned_in_Chaos-(GOWP_DCCCLXXXIX)-CDEP-FLAC-2021-MOONBLOOD", "GOWP_DCCCLXXXIX"},
|
||
}
|
||
for _, c := range cases {
|
||
r := Parse(c.line)
|
||
if r.CatalogNo != c.catno {
|
||
t.Errorf("CatalogNo = %q, want %q (release: %s)", r.CatalogNo, c.catno, c.line)
|
||
}
|
||
if len(r.UnknownTokens) != 0 {
|
||
t.Errorf("UnknownTokens = %v, want empty (release: %s)", r.UnknownTokens, c.line)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestParseSplitKeepsParensAndSplitsTitles(t *testing.T) {
|
||
// [1]=Title (фіксовано), а Evolution — не словниковий токен у середині:
|
||
// за ним ідуть словникові маркери → suspected title continuation.
|
||
// (SMG409) — катало, залишається каталом.
|
||
r := Parse("Ferosity-De-Evolution-(SMG409)-CD-FLAC-2025-86D")
|
||
if r.CatalogNo != "SMG409" {
|
||
t.Errorf("CatalogNo = %q, want SMG409", r.CatalogNo)
|
||
}
|
||
if r.Artist != "Ferosity" || r.Title != "De" {
|
||
t.Errorf("Artist/Title = %q/%q, want Ferosity / De", r.Artist, r.Title)
|
||
}
|
||
if len(r.TitleExtra) != 1 || r.TitleExtra[0] != "Evolution" {
|
||
t.Errorf("TitleExtra = %v, want [Evolution]", r.TitleExtra)
|
||
}
|
||
if len(r.UnknownTokens) != 0 {
|
||
t.Errorf("UnknownTokens = %v, want empty", r.UnknownTokens)
|
||
}
|
||
}
|
||
|
||
func TestParseLanguageRequiresKnownNeighbor(t *testing.T) {
|
||
// Мовний маркер валідний, коли праворуч — відомий хвостовий маркер.
|
||
r := Parse("ROTH-Alles_Endet-DE-VINYL-FLAC-2026-TOTENKVLT")
|
||
if r.Language != "DE" || r.Source != "VINYL" {
|
||
t.Errorf("Language/Source = %q/%q, want DE/VINYL", r.Language, r.Source)
|
||
}
|
||
}
|
||
|
||
func TestParseLanguageBetweenTitleAndFormat(t *testing.T) {
|
||
// A: мовний маркер у зоні між title і format — валідний,
|
||
// все ліворуч від нього — artist+title, нічого в unknown.
|
||
r := Parse("Kollektiver_Brechreiz-Heilig_Scheint_Scheinheilig-(NG207)-DE-CD-FLAC-2009-SHELTER")
|
||
if r.CatalogNo != "NG207" {
|
||
t.Errorf("CatalogNo = %q, want NG207", r.CatalogNo)
|
||
}
|
||
if r.Language != "DE" || r.Source != "CD" || r.Format != "FLAC" || r.Year != 2009 {
|
||
t.Errorf("tail fields wrong: %+v", r)
|
||
}
|
||
if r.Artist != "Kollektiver_Brechreiz" || r.Title != "Heilig_Scheint_Scheinheilig" {
|
||
t.Errorf("Artist/Title = %q/%q", r.Artist, r.Title)
|
||
}
|
||
if len(r.UnknownTokens) != 0 || len(r.Warnings) != 0 {
|
||
t.Errorf("UnknownTokens=%v Warnings=%v, want empty", r.UnknownTokens, r.Warnings)
|
||
}
|
||
}
|
||
|
||
func TestParseLanguageNeverBeforeFormat(t *testing.T) {
|
||
// DE — словниковий токен у середині (language.txt), позиція неважлива:
|
||
// Language=DE.
|
||
r := Parse("Some_Artist-Some_Song-DE-FLAC-2020-SOMEGROUP")
|
||
if r.Language != "DE" {
|
||
t.Errorf("Language = %q, want DE", r.Language)
|
||
}
|
||
if len(r.UnknownTokens) != 0 {
|
||
t.Errorf("UnknownTokens = %v, want empty", r.UnknownTokens)
|
||
}
|
||
if r.Title != "Some_Song" {
|
||
t.Errorf("Title = %q, want Some_Song", r.Title)
|
||
}
|
||
}
|
||
|
||
func TestParseCompoundEditionRecognized(t *testing.T) {
|
||
// REMASTERED_REISSUE — складений маркер у dicts/edition.txt, розпізнається
|
||
// як edition і не йде в UnknownTokens.
|
||
r := Parse("Ominous_Scriptures-Incarnation_of_the_Unheavenly-(LSR07)-REMASTERED_REISSUE-CD-FLAC-2021-86D")
|
||
if r.CatalogNo != "LSR07" {
|
||
t.Errorf("CatalogNo = %q, want LSR07", r.CatalogNo)
|
||
}
|
||
if r.Edition != "REMASTERED_REISSUE" {
|
||
t.Errorf("Edition = %q, want REMASTERED_REISSUE", r.Edition)
|
||
}
|
||
if len(r.UnknownTokens) != 0 {
|
||
t.Errorf("UnknownTokens = %v, want empty", r.UnknownTokens)
|
||
}
|
||
if r.Artist != "Ominous_Scriptures" || r.Title != "Incarnation_of_the_Unheavenly" {
|
||
t.Errorf("Artist/Title = %q/%q", r.Artist, r.Title)
|
||
}
|
||
if r.Source != "CD" || r.Format != "FLAC" || r.Year != 2021 {
|
||
t.Errorf("tail fields wrong: %+v", r)
|
||
}
|
||
}
|
||
|
||
func TestParseMixedCaseTitleNeverReported(t *testing.T) {
|
||
// [1]=Title (один токен). Хвіст назви (_ і Sky_Burial) — не словник,
|
||
// але за ним ідуть словникові маркери → suspected title continuation,
|
||
// не UnknownTokens.
|
||
r := Parse("Enthrallment-Against_the_Will_to_Live_-_Sky_Burial-(RTM103)-CD-FLAC-2021-86D")
|
||
if r.Artist != "Enthrallment" || r.Title != "Against_the_Will_to_Live_" {
|
||
t.Errorf("Artist/Title = %q/%q", r.Artist, r.Title)
|
||
}
|
||
if len(r.UnknownTokens) != 0 {
|
||
t.Errorf("UnknownTokens = %v, want empty", r.UnknownTokens)
|
||
}
|
||
if len(r.TitleExtra) != 1 || r.TitleExtra[0] != "_Sky_Burial" {
|
||
t.Errorf("TitleExtra = %v, want [_Sky_Burial]", r.TitleExtra)
|
||
}
|
||
if r.CatalogNo != "RTM103" {
|
||
t.Errorf("CatalogNo = %q, want RTM103", r.CatalogNo)
|
||
}
|
||
}
|
||
|
||
func contains(list []string, s string) bool {
|
||
for _, v := range list {
|
||
if v == s {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func TestParseParenthesizedTitleNotCatNo(t *testing.T) {
|
||
// Дужки в тайтлі при WEB/BitDepth — не каталономер, лишається в тайтлі.
|
||
r := Parse("The_Amazons-21st_Century_Fiction_(Epilogue)-16BIT-WEB-FLAC-2026-ENViED")
|
||
if r.CatalogNo != "" {
|
||
t.Errorf("CatalogNo = %q, want empty", r.CatalogNo)
|
||
}
|
||
if r.Title != "21st_Century_Fiction_(Epilogue)" {
|
||
t.Errorf("Title = %q, want 21st_Century_Fiction_(Epilogue)", r.Title)
|
||
}
|
||
if r.Source != "WEB" || r.BitDepth != "16BIT" || r.Year != 2026 {
|
||
t.Errorf("tail fields wrong: %+v", r)
|
||
}
|
||
if len(r.UnknownTokens) != 0 {
|
||
t.Errorf("UnknownTokens = %v, want empty", r.UnknownTokens)
|
||
}
|
||
}
|
||
|
||
func TestParseArtistDictionaryCoversReleases(t *testing.T) {
|
||
// artists.txt: "Iku-Turso" у словнику → Artist = 2 токени,
|
||
// Title = [2], хвостова зона зсувається і парситься як завжди.
|
||
r := Parse("Iku-Turso-Ikuinen_Steelfest_MMXXII_Live-24BIT-WEB-FLAC-2016-MOONBLOOD")
|
||
if r.Artist != "Iku-Turso" {
|
||
t.Errorf("Artist = %q, want Iku-Turso", r.Artist)
|
||
}
|
||
if r.Title != "Ikuinen_Steelfest_MMXXII_Live" {
|
||
t.Errorf("Title = %q, want Ikuinen_Steelfest_MMXXII_Live", r.Title)
|
||
}
|
||
if r.BitDepth != "24BIT" || r.Source != "WEB" || r.Format != "FLAC" || r.Year != 2016 {
|
||
t.Errorf("tail fields wrong: %+v", r)
|
||
}
|
||
if len(r.UnknownTokens) != 0 || len(r.TitleExtra) != 0 || len(r.Warnings) != 0 {
|
||
t.Errorf("Unknown=%v Extra=%v Warnings=%v, want empty",
|
||
r.UnknownTokens, r.TitleExtra, r.Warnings)
|
||
}
|
||
}
|
||
|
||
func TestParseOverrideWinsOverPositionalModel(t *testing.T) {
|
||
// overrides.txt (найвищий пріоритет): точний lookup по Raw
|
||
// фіксує Artist/Title; хвіст парситься як завжди.
|
||
raw := "Milicia_Oscura-Anti-Cristo-ES-16BIT-WEB-FLAC-2024-GRAVEWAVE"
|
||
r := Parse(raw)
|
||
if r.Artist != "Milicia_Oscura" {
|
||
t.Errorf("Artist = %q, want Milicia_Oscura", r.Artist)
|
||
}
|
||
if r.Title != "Anti-Cristo" {
|
||
t.Errorf("Title = %q, want Anti-Cristo", r.Title)
|
||
}
|
||
if r.Language != "ES" || r.BitDepth != "16BIT" || r.Source != "WEB" ||
|
||
r.Format != "FLAC" || r.Year != 2024 || r.Group != "GRAVEWAVE" {
|
||
t.Errorf("tail fields wrong: %+v", r)
|
||
}
|
||
if len(r.UnknownTokens) != 0 || len(r.TitleExtra) != 0 || len(r.Warnings) != 0 {
|
||
t.Errorf("Unknown=%v Extra=%v Warnings=%v, want empty",
|
||
r.UnknownTokens, r.TitleExtra, r.Warnings)
|
||
}
|
||
}
|
||
|
||
func TestParseTypeNormalizedSourceStaysRaw(t *testing.T) {
|
||
// CDEP у source.txt: Source лишається сирим, Type нормалізується в EP.
|
||
r := Parse("Black_Ceremonial_Kult-Crowned_in_Chaos-CDEP-FLAC-2021-MOONBLOOD")
|
||
if r.Type != "EP" {
|
||
t.Errorf("Type = %q, want EP", r.Type)
|
||
}
|
||
if r.Source != "CDEP" {
|
||
t.Errorf("Source = %q, want raw CDEP", r.Source)
|
||
}
|
||
}
|
||
|
||
func TestParseTypeFirstMatchWins(t *testing.T) {
|
||
// REMASTERED_EP → EP; наступний типовий токен не перезаписує.
|
||
r := Parse("Some_Artist-Some_Title-REMASTERED_EP-SINGLE-FLAC-2021-MOONBLOOD")
|
||
if r.Type != "EP" {
|
||
t.Errorf("Type = %q, want EP (first match wins)", r.Type)
|
||
}
|
||
if r.Edition != "REMASTERED_EP" {
|
||
t.Errorf("Edition = %q, want REMASTERED_EP", r.Edition)
|
||
}
|
||
}
|
||
|
||
func TestAnalyzeTypeDistribution(t *testing.T) {
|
||
stats := AnalyzeTitles([]string{
|
||
"Some_Artist-Some_Title-CDEP-FLAC-2021-MOONBLOOD",
|
||
"Other_Artist-Other_Title-EP-FLAC-2022-MOONBLOOD",
|
||
"Third_Artist-Third_Title-SPLIT-FLAC-2023-MOONBLOOD",
|
||
})
|
||
if stats.TypeDistribution["EP"] != 2 {
|
||
t.Errorf("TypeDistribution[EP] = %d, want 2", stats.TypeDistribution["EP"])
|
||
}
|
||
if stats.TypeDistribution["Split"] != 1 {
|
||
t.Errorf("TypeDistribution[Split] = %d, want 1", stats.TypeDistribution["Split"])
|
||
}
|
||
}
|