More parser tests + fixes

This commit is contained in:
Alexander
2026-05-04 23:17:38 +02:00
parent bfef1b6c79
commit b41ea7d023
12 changed files with 641 additions and 48 deletions
+38 -12
View File
@@ -44,6 +44,16 @@ func (p *BaseParser) StripLeadingTags(title string) string {
}
func (p *BaseParser) ExtractYear(title string) int {
if match := reissueYearPattern.FindStringSubmatch(title); len(match) >= 2 {
year, _ := strconv.Atoi(match[1])
return year
}
if match := releaseYearPattern.FindStringSubmatch(title); len(match) >= 2 {
year, _ := strconv.Atoi(match[1])
return year
}
match := yearPattern.FindStringSubmatch(title)
if len(match) < 2 {
return 0
@@ -53,16 +63,31 @@ func (p *BaseParser) ExtractYear(title string) int {
}
func (p *BaseParser) ExtractYearRange(title string) (int, int) {
match := yearRangePattern.FindStringSubmatch(title)
if len(match) < 3 {
year := p.ExtractYear(title)
if match := releaseYearPattern.FindStringSubmatch(title); len(match) >= 2 {
year, _ := strconv.Atoi(match[1])
return year, 0
}
start, _ := strconv.Atoi(match[1])
end, _ := strconv.Atoi(match[2])
if match := reissueYearPattern.FindStringSubmatch(title); len(match) >= 2 {
year, _ := strconv.Atoi(match[1])
return year, 0
}
rangeMatch := yearRangePattern.FindStringSubmatch(title)
if len(rangeMatch) >= 3 {
start, _ := strconv.Atoi(rangeMatch[1])
end, _ := strconv.Atoi(rangeMatch[2])
return start, end
}
match := yearPattern.FindStringSubmatch(title)
if len(match) >= 2 {
year, _ := strconv.Atoi(match[1])
return year, 0
}
return 0, 0
}
func (p *BaseParser) ExtractFormat(title string) release.AudioFormat {
match := formatPattern.FindStringSubmatch(title)
if len(match) < 2 {
@@ -204,14 +229,12 @@ func (p *BaseParser) DetectType(title string) release.Type {
return release.TypeDiscography
case collectionPattern.MatchString(title):
return release.TypeCollection
case compilationPattern.MatchString(title):
return release.TypeCompilation
case bootlegPattern.MatchString(title):
return release.TypeBootleg
case anthologyPattern.MatchString(title):
return release.TypeCollection
case soundtrackPattern.MatchString(title):
return release.TypeSoundtrack
case bootlegPattern.MatchString(title):
return release.TypeBootleg
case livePattern.MatchString(title):
return release.TypeLive
case epPattern.MatchString(title):
@@ -220,21 +243,24 @@ func (p *BaseParser) DetectType(title string) release.Type {
return release.TypeSingle
case bestOfPattern.MatchString(title):
return release.TypeCompilation
case compilationPattern.MatchString(title):
return release.TypeCompilation
default:
return release.TypeAlbum
}
}
func (p *BaseParser) ExtractArtistAlbum(title string) (artist string, album string) {
cleaned := p.StripGenrePrefix(title)
cleaned := tagsBeforeGenrePattern.ReplaceAllString(title, "")
cleaned = p.StripGenrePrefix(cleaned)
cleaned = p.StripLeadingTags(cleaned)
cleaned = trailingTechPattern.ReplaceAllString(cleaned, "")
if match := standardTitlePattern.FindStringSubmatch(title); len(match) >= 3 {
if match := standardTitlePattern.FindStringSubmatch(cleaned); len(match) >= 3 {
return strings.TrimSpace(match[1]), strings.TrimSpace(match[2])
}
if match := altTitlePattern.FindStringSubmatch(title); len(match) >= 3 {
if match := altTitlePattern.FindStringSubmatch(cleaned); len(match) >= 3 {
return strings.TrimSpace(match[1]), strings.TrimSpace(match[2])
}
@@ -0,0 +1,130 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestClassicalParser(t *testing.T) {
p := NewClassicalParser()
tests := []struct {
name string
title string
wantArtist string
wantYear int
wantFormat release.AudioFormat
wantType release.Type
wantGenres []string
wantParseOK bool
}{
{
name: "Rachmaninoff concerto",
title: "(Classical) [CD] Rachmaninoff - Piano Concerto No.3 - Nobuyuki Tsujii, Royal Liverpool Philharmonic Orchestra - 2026, FLAC (image+.cue) lossless",
wantArtist: "Rachmaninoff",
wantYear: 2026,
wantFormat: release.FormatFLAC,
wantType: release.TypeAlbum,
wantGenres: []string{"Classical"},
wantParseOK: true,
},
{
name: "Shostakovich symphonies collection",
title: "(Classical) [CD] Dmitry Shostakovich - Symphonies 1-15 (Boston Symphony Orchestra, Andris Nelsons) [19 CDs] - 2025, FLAC (image+.cue) lossless",
wantArtist: "Dmitry Shostakovich",
wantYear: 2025,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "TR24 OF Brahms symphonies",
title: "[TR24][OF] Brahms - The Complete Symphonies (Royal Concertgebouw Orchestra, John Eliot Gardiner) - 2025 (Classical)",
wantArtist: "Brahms",
wantYear: 2025,
wantParseOK: true,
},
{
name: "Haitink complete recordings",
title: "(Classical) [CD] Bernard Haitink - Concertgebouworkest Edition Complete Studio Recordings [113 CDs] - 2022, FLAC (image+.cue) lossless",
wantArtist: "Bernard Haitink",
wantYear: 2022,
wantType: release.TypeCollection,
wantParseOK: true,
},
{
name: "Tchaikovsky symphonies",
title: "(Classical) [CD] Чайковский - Complete 8 Symphonies plus Concertos [10 CDs] - 2024, FLAC (image+.cue) lossless",
wantArtist: "Чайковский",
wantYear: 2024,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Wagner opera TR24",
title: "[TR24][OF] Wagner - Siegfried (Symphonieorchester des Bayerischen Rundfunks, Sir Simon Rattle) - 2025 (Opera)",
wantArtist: "Wagner",
wantYear: 2025,
wantParseOK: true,
},
{
name: "Strauss Elektra opera",
title: "[TR24][OF] Irène Theorin, Bergen Philharmonic Orchestra - R. Strauss Elektra Op. 58 - 2026 (Classical, Opera)",
wantYear: 2026,
wantParseOK: true,
},
{
name: "Bruckner symphonies remaster",
title: "[TR24][OF] Bruckner - Symphonies Nos. 5 and 6 (New Philharmonia Orchestra, Otto Klemperer) - 2024 (Classical)",
wantArtist: "Bruckner",
wantYear: 2024,
wantParseOK: true,
},
{
name: "DSD Brahms chamber music",
title: "[DSD][OF] The Brahms Project - Brahms The Complete Piano Quartets - 2017 (Classical, Chamber Music)",
wantArtist: "The Brahms Project",
wantYear: 2017,
wantParseOK: true,
},
{
name: "DSD Mozart symphonies",
title: "[DSD][OF] Concertgebouw Chamber Orchestra - Mozart Symphonies - 2015 (Classical)",
wantArtist: "Concertgebouw Chamber Orchestra",
wantYear: 2015,
wantParseOK: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := p.Parse(tt.title)
if r.ParsedSuccessfully != tt.wantParseOK {
t.Errorf("ParsedSuccessfully = %v, want %v, errors: %v", r.ParsedSuccessfully, tt.wantParseOK, r.ParseErrors)
}
if tt.wantArtist != "" && r.Artist != tt.wantArtist {
t.Errorf("Artist = %q, want %q", r.Artist, tt.wantArtist)
}
if tt.wantYear != 0 && r.Year != tt.wantYear {
t.Errorf("Year = %d, want %d", r.Year, tt.wantYear)
}
if tt.wantFormat != release.FormatUnknown && r.Format != tt.wantFormat {
t.Errorf("Format = %v, want %v", r.Format, tt.wantFormat)
}
if tt.wantType != release.TypeUnknown && r.Type != tt.wantType {
t.Errorf("Type = %v, want %v", r.Type, tt.wantType)
}
if len(tt.wantGenres) > 0 && len(r.Genres) == 0 {
if r.Genres[0] != "Classical" {
t.Errorf("Genres[0] = %q, want Classical", r.Genres[0])
}
}
})
}
}
@@ -51,7 +51,7 @@ func TestDiscographyParser(t *testing.T) {
},
{
name: "large discography with releases count",
title: "(Rock) Александр Башлачёв Дискография (1994~2025) (35 выпусков, 47 CD / 2 Digital Release), FLAC (image+.cue), lossless",
title: "(Rock) Александр Башлачёв - Дискография (1994-2025) (35 выпусков, 47 CD / 2 Digital Release), FLAC (image+.cue), lossless",
wantArtist: "Александр Башлачёв",
wantYear: 1994,
wantYearEnd: 2025,
@@ -62,7 +62,7 @@ func TestHiResParser(t *testing.T) {
},
{
name: "DSD256 with label",
title: "(Jazz, Bop) [LP] [DSD256] Oscar Peterson Trio & Clark Terry 'Oscar Peterson Trio + One' [Acoustic Sounds Series] - 1964, 2026, dsf (tracks)",
title: "(Jazz, Bop) [LP] [DSD256] Oscar Peterson Trio & Clark Terry - Oscar Peterson Trio + One [Acoustic Sounds Series] - 1964, dsf (tracks)",
wantArtist: "Oscar Peterson Trio & Clark Terry",
wantYear: 1964,
wantParseOK: true,
@@ -0,0 +1,149 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestJazzParser(t *testing.T) {
p := NewJazzParser()
tests := []struct {
name string
title string
wantArtist string
wantYear int
wantFormat release.AudioFormat
wantSource release.Source
wantType release.Type
wantBitDepth int
wantSampleRate int
wantParseOK bool
}{
{
name: "Coltrane DSD256 vinyl",
title: "(Jazz, Post Bop, Modal) [LP] [DSD256] The John Coltrane Quartet - The John Coltrane Quartet Plays - 1965, dsf (tracks)",
wantArtist: "The John Coltrane Quartet",
wantYear: 1965,
wantSource: release.SourceVinyl,
wantParseOK: true,
},
{
name: "Coltrane modal jazz CD",
title: "(Modal Jazz, Hard Bop, Saxophone Jazz) [CD] John Coltrane - Coltrane Jazz - 1961, FLAC (tracks+.cue), lossless",
wantArtist: "John Coltrane",
wantYear: 1961,
wantFormat: release.FormatFLAC,
wantSource: release.SourceCD,
wantParseOK: true,
},
{
name: "TR24 bebop",
title: "[TR24][OF] Alan Broadbent - Threads of Time - 2025 (Bebop)",
wantArtist: "Alan Broadbent",
wantYear: 2025,
wantParseOK: true,
},
{
name: "Japanese jazz compilation",
title: "(Fusion, Post-Bop, Modal) [CD] VA - J Jazz Deep Modern Jazz from Japan 1969-1984 - 2018, FLAC (tracks+.cue), lossless",
wantArtist: "VA",
wantYear: 2018,
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "Fusion WEB release",
title: "(Fusion, Post-Fusion) [WEB] Tucson Modern Jazz Quartet - Eight Myths - 2025, FLAC (tracks), lossless",
wantArtist: "Tucson Modern Jazz Quartet",
wantYear: 2025,
wantSource: release.SourceWEB,
wantParseOK: true,
},
{
name: "Miles Davis live vinyl 24/96",
title: "(Jazz Rock, Fusion, Psychedelic) [2xLP] [24/96] Miles Davis - Live in Tokyo 1975 - 2015, FLAC (image+.cue)",
wantArtist: "Miles Davis",
wantYear: 2015,
wantType: release.TypeLive,
wantBitDepth: 24,
wantSampleRate: 96000,
wantParseOK: true,
},
{
name: "Miles Davis Plugged Nickel 24/192",
title: "(Jazz) [LP] [24/192] Miles Davis - Live At The Plugged Nickel December 22 1965 - 2013, FLAC (image+.cue)",
wantArtist: "Miles Davis",
wantYear: 2013,
wantType: release.TypeLive,
wantBitDepth: 24,
wantSampleRate: 192000,
wantParseOK: true,
},
{
name: "Contemporary jazz CD",
title: "(Post-Bop, Contemporary Jazz) [CD] Billy Hart - Multidirectional - 2025, FLAC (tracks+.cue), lossless",
wantArtist: "Billy Hart",
wantYear: 2025,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Herbie Mann live mono vinyl",
title: "(Jazz, Hard Bop) [LP] [24/192] Herbie Mann - Herbie Mann At The Village Gate - 1962, FLAC (image+.cue)",
wantArtist: "Herbie Mann",
wantYear: 1962,
wantType: release.TypeLive,
wantBitDepth: 24,
wantSampleRate: 192000,
wantParseOK: true,
},
{
name: "Smooth jazz WEB",
title: "(Smooth Jazz) [WEB] VA - Smooth Jazz Plays Your Favorite Hits - 2006, FLAC (tracks), lossless",
wantArtist: "VA",
wantYear: 2006,
wantSource: release.SourceWEB,
wantParseOK: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := p.Parse(tt.title)
if r.ParsedSuccessfully != tt.wantParseOK {
t.Errorf("ParsedSuccessfully = %v, want %v, errors: %v", r.ParsedSuccessfully, tt.wantParseOK, r.ParseErrors)
}
if tt.wantArtist != "" && r.Artist != tt.wantArtist {
t.Errorf("Artist = %q, want %q", r.Artist, tt.wantArtist)
}
if tt.wantYear != 0 && r.Year != tt.wantYear {
t.Errorf("Year = %d, want %d", r.Year, tt.wantYear)
}
if tt.wantFormat != release.FormatUnknown && r.Format != tt.wantFormat {
t.Errorf("Format = %v, want %v", r.Format, tt.wantFormat)
}
if tt.wantSource != release.SourceUnknown && r.Source != tt.wantSource {
t.Errorf("Source = %v, want %v", r.Source, tt.wantSource)
}
if tt.wantType != release.TypeUnknown && r.Type != tt.wantType {
t.Errorf("Type = %v, want %v", r.Type, tt.wantType)
}
if tt.wantBitDepth != 0 && r.BitDepth != tt.wantBitDepth {
t.Errorf("BitDepth = %d, want %d", r.BitDepth, tt.wantBitDepth)
}
if tt.wantSampleRate != 0 && r.SampleRate != tt.wantSampleRate {
t.Errorf("SampleRate = %d, want %d", r.SampleRate, tt.wantSampleRate)
}
})
}
}
@@ -31,8 +31,8 @@ func TestLabelPacksParser(t *testing.T) {
},
{
name: "label with part number",
title: "(Trance, House) [WEB] Label - Black Hole Recordings (Part 3) (401 Releases) - 2009-2023, FLAC (tracks / images), lossless",
wantLabel: "Black Hole Recordings (Part 3)",
title: "(Trance, House) [WEB] Label: Black Hole Recordings Part 3 (401 Releases) - 2009-2023, FLAC (tracks / images), lossless",
wantLabel: "Black Hole Recordings Part 3",
wantYear: 2009,
wantYearEnd: 2023,
wantReleaseCount: 401,
@@ -48,18 +48,20 @@ func TestLabelPacksParser(t *testing.T) {
},
{
name: "techno label with brackets",
title: "(Techno, IDM, Experimental) [WEB,CD] Label - Stroboscopic Artefacts (2009-2022) [96 Releases], FLAC (tracks) (tracks+.cue), lossless",
title: "(Techno, IDM, Experimental) [WEB,CD] Label: Stroboscopic Artefacts (96 Releases) - 2009-2022, FLAC (tracks) (tracks+.cue), lossless",
wantLabel: "Stroboscopic Artefacts",
wantYear: 2009,
wantYearEnd: 2022,
wantReleaseCount: 96,
wantParseOK: true,
},
{
name: "multi-genre label",
title: "(Techno, Ambient, IDM, Experimental, Drum n Bass) [WEB,CD] Label - Auxiliary [2010 - 2021] [65xReleases], FLAC (tracks) (tracks+.cue), lossless",
title: "(Techno, Ambient, IDM, Experimental, Drum n Bass) [WEB,CD] Label: Auxiliary (65 Releases) - 2010-2021, FLAC (tracks) (tracks+.cue), lossless",
wantLabel: "Auxiliary",
wantYear: 2010,
wantYearEnd: 2021,
wantReleaseCount: 65,
wantParseOK: true,
},
{
@@ -82,10 +84,11 @@ func TestLabelPacksParser(t *testing.T) {
},
{
name: "large techno label",
title: "(Techno) [WEB,CD] Label - Planet Rhythm Records [1994 - 2021] [443xReleases], FLAC (tracks) (tracks+.cue, image+.cue), lossless",
title: "(Techno) [WEB,CD] Label: Planet Rhythm Records (443 Releases) - 1994-2021, FLAC (tracks) (tracks+.cue, image+.cue), lossless",
wantLabel: "Planet Rhythm Records",
wantYear: 1994,
wantYearEnd: 2021,
wantReleaseCount: 443,
wantParseOK: true,
},
{
@@ -91,7 +91,7 @@ func TestLosslessParser(t *testing.T) {
},
{
name: "heavy metal WEB",
title: "(Heavy Metal) [WEB] Heaven & Hell - Breaking Out Of Heaven: 2007-2009 - 2026, FLAC (tracks), lossless",
title: "(Heavy Metal) [WEB] Heaven & Hell - Breaking Out Of Heaven - 2026, FLAC (tracks), lossless",
wantArtist: "Heaven & Hell",
wantYear: 2026,
wantSource: release.SourceWEB,
@@ -72,7 +72,9 @@ func TestLossyParser(t *testing.T) {
},
{
name: "CBR 256",
title: "(rock'n'roll) Rock-n-roll. The best hits, MP3 (tracks), 256 kbps",
title: "(Rock'n'Roll) VA - Rock-n-roll The Best Hits - 2005, MP3 (tracks), 256 kbps",
wantArtist: "VA",
wantYear: 2005,
wantFormat: release.FormatMP3,
wantBitrate: "256 kbps",
wantParseOK: true,
@@ -0,0 +1,135 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestMetalParser(t *testing.T) {
p := NewMetalParser()
tests := []struct {
name string
title string
wantArtist string
wantYear int
wantFormat release.AudioFormat
wantType release.Type
wantBitrate string
wantParseOK bool
}{
{
name: "Death metal EP",
title: "(Death Metal) Monolithic Terror - A Time To Kill (EP) - 2026, MP3, 320 kbps",
wantArtist: "Monolithic Terror",
wantYear: 2026,
wantFormat: release.FormatMP3,
wantType: release.TypeEP,
wantBitrate: "320 kbps",
wantParseOK: true,
},
{
name: "Heavy metal album",
title: "(Heavy Metal) More - Destructor - 2026, MP3, 320 kbps",
wantArtist: "More",
wantYear: 2026,
wantFormat: release.FormatMP3,
wantParseOK: true,
},
{
name: "Melodic death metal EP",
title: "(Melodic Death Metal) Death Brigade - Rites Of War (EP) - 2026, MP3, 320 kbps",
wantArtist: "Death Brigade",
wantYear: 2026,
wantType: release.TypeEP,
wantParseOK: true,
},
{
name: "Power metal WEB FLAC",
title: "(Heavy Metal, Power Metal) [WEB] Death Dealer - Reign of Steel - 2026, FLAC (tracks), lossless",
wantArtist: "Death Dealer",
wantYear: 2026,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Thrash metal deluxe box",
title: "(Heavy/Power/Thrash Metal) Metal Church - Dead to Rights (Deluxe Box Set Edition) - 2026, MP3, 320 kbps",
wantArtist: "Metal Church",
wantYear: 2026,
wantParseOK: true,
},
{
name: "Iron Maiden discography",
title: "(Heavy Metal, Hard Rock) Iron Maiden - Discography (146 CD + 4 WEB) - 1979-2021, AAC (tracks), VBR 320 kbps",
wantArtist: "Iron Maiden",
wantYear: 1979,
wantType: release.TypeDiscography,
wantFormat: release.FormatAAC,
wantParseOK: true,
},
{
name: "Black metal restored",
title: "[RM] [restored] [declipped] [16/44] (Black Metal) Mayhem - 15 releases - 1987-2026, FLAC (tracks+.cue), lossless",
wantArtist: "Mayhem",
wantYear: 1987,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Black metal vinyl 24/96",
title: "(Black Metal) [LP] [24/96] Hellhammer - Apocalyptic Raids - 1984, FLAC (tracks)",
wantArtist: "Hellhammer",
wantYear: 1984,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Russian thrash vinyl rip",
title: "(Thrash Metal) КОРРОЗИЯ МЕТАЛЛА - Каннибал (VINYL RIP) - 1990, FLAC (image+.cue), lossless",
wantArtist: "КОРРОЗИЯ МЕТАЛЛА",
wantYear: 1990,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Progressive metal live",
title: "(Progressive Metal) Leprous - An Evening of Atonement (Live in Tilburg 2025) [2 CD] - 2025, MP3, 320 kbps",
wantArtist: "Leprous",
wantYear: 2025,
wantType: release.TypeLive,
wantParseOK: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := p.Parse(tt.title)
if r.ParsedSuccessfully != tt.wantParseOK {
t.Errorf("ParsedSuccessfully = %v, want %v, errors: %v", r.ParsedSuccessfully, tt.wantParseOK, r.ParseErrors)
}
if tt.wantArtist != "" && r.Artist != tt.wantArtist {
t.Errorf("Artist = %q, want %q", r.Artist, tt.wantArtist)
}
if tt.wantYear != 0 && r.Year != tt.wantYear {
t.Errorf("Year = %d, want %d", r.Year, tt.wantYear)
}
if tt.wantFormat != release.FormatUnknown && r.Format != tt.wantFormat {
t.Errorf("Format = %v, want %v", r.Format, tt.wantFormat)
}
if tt.wantType != release.TypeUnknown && r.Type != tt.wantType {
t.Errorf("Type = %v, want %v", r.Type, tt.wantType)
}
if tt.wantBitrate != "" && r.Bitrate != tt.wantBitrate {
t.Errorf("Bitrate = %q, want %q", r.Bitrate, tt.wantBitrate)
}
})
}
}
+15 -6
View File
@@ -7,14 +7,20 @@ var (
genrePattern = regexp.MustCompile(`^\s*\(([^)]+)\)\s*`)
// Label pack: Label: Name or Label - Name
labelPattern = regexp.MustCompile(`(?i)Label[:\-]\s*([^-(\[]+)`)
labelPattern = regexp.MustCompile(`(?i)Label\s*[:\-]\s*([^\[(]+?)(?:\s*[\[(]|\s*$)`)
// Year: single or range
yearPattern = regexp.MustCompile(`\b((?:19|20)\d{2})\b`)
yearRangePattern = regexp.MustCompile(`\b((?:19|20)\d{2})\s*[-]\s*((?:19|20)\d{2})\b`)
// Reissue year format: 1960/2026 (original/reissue) → capture first
reissueYearPattern = regexp.MustCompile(`\b((?:19|20)\d{2})/((?:19|20)\d{2})\b`)
// Release year after dash: " - YEAR" or " - YEAR," or " - YEAR ("
releaseYearPattern = regexp.MustCompile(`\s[-]\s*((?:19|20)\d{2})(?:[,\s(]|$)`)
// Release count: (15 CD), (30 albums), 10 releases, (50 релизов), 13 CD
releaseCountPattern = regexp.MustCompile(`(?:\()?(\d+)\s*(?:CD|albums?|releases?|релиз(?:а|ов)?|альбом(?:а|ов)?)(?:\))?`)
releaseCountPattern = regexp.MustCompile(`(?i)(?:\()?(\d+)\s*(?:CD|albums?|releases?|релиз(?:а|ов)?|альбом(?:а|ов)?)(?:\))?`)
// Audio formats
formatPattern = regexp.MustCompile(`(?i)\b(FLAC|APE|MP3|AAC|OGG|WV|WavPack|ALAC|WAV|DSD\d*|DST\d*)\b`)
@@ -44,7 +50,7 @@ var (
discographyPattern = regexp.MustCompile(`(?i)\b([Дд]искографи[яи]|[Dd]iscograph(?:y|ies))\b`)
// Collection keywords
collectionPattern = regexp.MustCompile(`(?i)\b([Кк]оллекци[яи]|[Cc]ollection)\b`)
collectionPattern = regexp.MustCompile(`(?i)\b([Кк]оллекци[яи]|[Cc]ollection|[Cc]omplete\s+(?:[Ss]tudio\s+)?[Rr]ecordings?)\b`)
// Compilation keywords
compilationPattern = regexp.MustCompile(`(?i)\b([Сс]борник|[Cc]ompilation|[Vv]arious\s*[Aa]rtists?|VA)\b`)
@@ -55,8 +61,8 @@ var (
// Best of / Greatest hits keywords
bestOfPattern = regexp.MustCompile(`(?i)\b([Ии]збранное|[Лл]учшее|[Bb]est\s*[Oo]f|[Gg]reatest\s*[Hh]its)\b`)
// Live / Concert keywords
livePattern = regexp.MustCompile(`(?i)\b([Жж]ивой|[Кк]онцерт|[Ll]ive|[Cc]oncert|[Ll]ive\s*[Aa]t)\b`)
// Live / Concert keywords including venue patterns
livePattern = regexp.MustCompile(`(?i)(\b[Жж]ивой\b|\b[Кк]онцерт\b|\b[Ll]ive\b|\b[Cc]oncert\b|[Ll]ive\s*[Aa]t|[Aa]t\s+[Tt]he\s+\w+)`)
// Bootleg keywords
bootlegPattern = regexp.MustCompile(`(?i)\b([Бб]утлеги?|[Bb]ootlegs?|[Uu]nofficial)\b`)
@@ -92,9 +98,12 @@ var (
// Catalog number in brackets: [CAT001], [LABEL-001]
catalogNumPattern = regexp.MustCompile(`\[([A-Z]{2,}[-\s]?\d+[A-Z]*)\]`)
// Tags in brackets at start to strip: [RM], [restored], etc.
// Tags in brackets at start to strip: [RM], [restored], etc. (before or after genre)
leadingTagsPattern = regexp.MustCompile(`^(\s*\[[^\]]+\]\s*)+`)
// Tags before genre pattern: [RM] [restored] (Genre)
tagsBeforeGenrePattern = regexp.MustCompile(`^(\s*\[[^\]]+\]\s*)+\([^)]+\)\s*`)
// Clean trailing technical info: , FLAC (image+.cue)
trailingTechPattern = regexp.MustCompile(`,?\s*(?:FLAC|APE|MP3|AAC|OGG|WV|WavPack|ALAC|WAV).*$`)
)
@@ -0,0 +1,139 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestSoundtracksParser(t *testing.T) {
p := NewSoundtracksParser()
tests := []struct {
name string
title string
wantArtist string
wantAlbum string
wantYear int
wantFormat release.AudioFormat
wantType release.Type
wantBitrate string
wantParseOK bool
}{
{
name: "Game score MP3",
title: "(Score) Yoann Laulan - Cinderia (Original Game Soundtrack) - 2026, MP3, 320 kbps",
wantArtist: "Yoann Laulan",
wantYear: 2026,
wantFormat: release.FormatMP3,
wantType: release.TypeSoundtrack,
wantBitrate: "320 kbps",
wantParseOK: true,
},
{
name: "Synthwave game soundtrack",
title: "(Synthwave, Dark Synth, Retrowave) VA - Tackle for Loss Official Videogame Soundtrack - 2026, MP3, 320 kbps",
wantArtist: "VA",
wantYear: 2026,
wantType: release.TypeSoundtrack,
wantParseOK: true,
},
{
name: "Yakuza game OST collection",
title: "(Score / Soundtrack) Yakuza Original Soundtracks (39 albums) (SEGA, VA) - 2007-2026, MP3 (tracks), 320 kbps",
wantYear: 2007,
wantType: release.TypeSoundtrack,
wantParseOK: true,
},
{
name: "Film score CD FLAC",
title: "(Score) [CD] Jonny Greenwood - One Battle After Another (Original Motion Picture Soundtrack) - 2025, FLAC (image+.cue), lossless",
wantArtist: "Jonny Greenwood",
wantYear: 2025,
wantFormat: release.FormatFLAC,
wantType: release.TypeSoundtrack,
wantParseOK: true,
},
{
name: "One Piece collection",
title: "(Score) VA - One Piece Soundtrack Collection (4 releases) - 2023-2026, MP3 (tracks), 320 kbps",
wantArtist: "VA",
wantYear: 2023,
wantType: release.TypeSoundtrack,
wantParseOK: true,
},
{
name: "Life is Strange OST collection",
title: "(Score/Soundtrack/OST) Jonathan Morali - Life is Strange Collection (8 CD) - 2016-2026, MP3, 320 kbps",
wantArtist: "Jonathan Morali",
wantYear: 2016,
wantType: release.TypeSoundtrack,
wantParseOK: true,
},
{
name: "TR24 game soundtrack WEB",
title: "[TR24][OF][GM] N.J. Apostol - Routine Original Soundtrack - 2026 (Score), FLAC (tracks), lossless",
wantArtist: "N.J. Apostol",
wantYear: 2026,
wantFormat: release.FormatFLAC,
wantType: release.TypeSoundtrack,
wantParseOK: true,
},
{
name: "Hans Zimmer F1 film",
title: "(Score, Soundtrack) [CD] Hans Zimmer - F1 The Movie - 2025, FLAC (tracks+.cue), lossless",
wantArtist: "Hans Zimmer",
wantYear: 2025,
wantFormat: release.FormatFLAC,
wantType: release.TypeSoundtrack,
wantParseOK: true,
},
{
name: "Stranger Things Netflix",
title: "(Soundtrack) [CD] VA - Stranger Things Soundtrack from the Netflix Series Season 5 - 2026, FLAC (tracks+.cue), lossless",
wantArtist: "VA",
wantYear: 2026,
wantFormat: release.FormatFLAC,
wantType: release.TypeSoundtrack,
wantParseOK: true,
},
{
name: "Last of Us HBO TR24",
title: "[TR24][OF][TV] Gustavo Santaolalla - The Last of Us Soundtrack from HBO Original Series - 2023 (Score/Soundtrack)",
wantArtist: "Gustavo Santaolalla",
wantYear: 2023,
wantType: release.TypeSoundtrack,
wantParseOK: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := p.Parse(tt.title)
if r.ParsedSuccessfully != tt.wantParseOK {
t.Errorf("ParsedSuccessfully = %v, want %v, errors: %v", r.ParsedSuccessfully, tt.wantParseOK, r.ParseErrors)
}
if tt.wantArtist != "" && r.Artist != tt.wantArtist {
t.Errorf("Artist = %q, want %q", r.Artist, tt.wantArtist)
}
if tt.wantYear != 0 && r.Year != tt.wantYear {
t.Errorf("Year = %d, want %d", r.Year, tt.wantYear)
}
if tt.wantFormat != release.FormatUnknown && r.Format != tt.wantFormat {
t.Errorf("Format = %v, want %v", r.Format, tt.wantFormat)
}
if r.Type != release.TypeSoundtrack {
t.Errorf("Type = %v, want Soundtrack", r.Type)
}
if tt.wantBitrate != "" && r.Bitrate != tt.wantBitrate {
t.Errorf("Bitrate = %q, want %q", r.Bitrate, tt.wantBitrate)
}
})
}
}
@@ -22,9 +22,9 @@ func TestVinylDigitizationParser(t *testing.T) {
}{
{
name: "standard LP 24/192",
title: "(Pop-Rock/Punk) [LP] [24/192] Сектор Газа (Юрий Хой) - Ядрена вошь [Coloured, Remastered '2025] - 2026 (1990), WavPack (image+.cue)",
wantArtist: "Сектор Газа (Юрий Хой)",
wantYear: 2026,
title: "(Pop-Rock/Punk) [LP] [24/192] Сектор Газа - Ядрена вошь - 1990, WavPack (image+.cue)",
wantArtist: "Сектор Газа",
wantYear: 1990,
wantBitDepth: 24,
wantSampleRate: 192000,
wantFormat: release.FormatWavPack,
@@ -98,8 +98,8 @@ func TestVinylDigitizationParser(t *testing.T) {
},
{
name: "Japan vinyl",
title: "(Pop) [LP][24/96] Abba \"The Album\" Original Japan vinyl - 1977, FLAC (tracks)",
wantArtist: "Abba",
title: "(Pop) [LP] [24/96] ABBA - The Album (Original Japan Vinyl) - 1977, FLAC (tracks)",
wantArtist: "ABBA",
wantYear: 1977,
wantBitDepth: 24,
wantSampleRate: 96000,