package moonblood import ( "bufio" "os" "testing" ) func TestParseMoonblood(t *testing.T) { file, err := os.Open("../../resources/moonblood.txt") if err != nil { t.Skipf("resource file not found (DB mode), skipping: %v", err) t.Fatal(err) } defer file.Close() scanner := bufio.NewScanner(file) total, success := 0, 0 for scanner.Scan() { line := scanner.Text() if line == "" { continue } result := Parse(line) total++ if result.Group == "MOONBLOOD" && result.Year != 0 && result.Format == "FLAC" { success++ } if result.Confidence < 0.7 { t.Logf("Low confidence: %s (%.2f)", line, result.Confidence) } } t.Logf("Parsed %d releases, %d successful (%.1f%%)", total, success, float64(success)/float64(total)*100) } 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) { // [0]=Artist, [1]=Title. Artist сам може містити дефіс — тоді тайтл // стає токеном [1], а {"Violence", "All_My_Life"} щось лишається 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.UnknownTokens) != 1 || r.UnknownTokens[0] != "All_My_Life" { t.Errorf("UnknownTokens = %v, want [All_My_Life]", 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-SPLIT-FOO-16BIT-WEB-FLAC-2025-MOONBLOOD") if len(r.UnknownTokens) != 1 || r.UnknownTokens[0] != "FOO" { t.Errorf("UnknownTokens = %v, want [FOO]", r.UnknownTokens) } if r.Edition != "SPLIT" || 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 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 — не словниковий токен у середині: // потрапляє в UnknownTokens. (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.UnknownTokens) != 1 || r.UnknownTokens[0] != "Evolution" { t.Errorf("UnknownTokens = %v, want [Evolution]", 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 (один токен), решта, що не словник — 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) != 1 || r.UnknownTokens[0] != "_Sky_Burial" { t.Errorf("UnknownTokens = %v, want [_Sky_Burial]", r.UnknownTokens) } } 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) } }