diff --git a/internal/tracker/rutracker/parser/base.go b/internal/tracker/rutracker/parser/base.go index 944fdc3..b6e032d 100644 --- a/internal/tracker/rutracker/parser/base.go +++ b/internal/tracker/rutracker/parser/base.go @@ -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,14 +63,29 @@ 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]) - return start, end + + 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 { @@ -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]) } diff --git a/internal/tracker/rutracker/parser/classical_test.go b/internal/tracker/rutracker/parser/classical_test.go new file mode 100644 index 0000000..1c260e4 --- /dev/null +++ b/internal/tracker/rutracker/parser/classical_test.go @@ -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]) + } + } + }) + } +} diff --git a/internal/tracker/rutracker/parser/discography_test.go b/internal/tracker/rutracker/parser/discography_test.go index 72efd8a..3c68c25 100644 --- a/internal/tracker/rutracker/parser/discography_test.go +++ b/internal/tracker/rutracker/parser/discography_test.go @@ -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, diff --git a/internal/tracker/rutracker/parser/hires_test.go b/internal/tracker/rutracker/parser/hires_test.go index 6d539c8..6fa164b 100644 --- a/internal/tracker/rutracker/parser/hires_test.go +++ b/internal/tracker/rutracker/parser/hires_test.go @@ -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, diff --git a/internal/tracker/rutracker/parser/jazz_test.go b/internal/tracker/rutracker/parser/jazz_test.go new file mode 100644 index 0000000..8fa4a63 --- /dev/null +++ b/internal/tracker/rutracker/parser/jazz_test.go @@ -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) + } + }) + } +} diff --git a/internal/tracker/rutracker/parser/label_packs_test.go b/internal/tracker/rutracker/parser/label_packs_test.go index 87e094c..305ae21 100644 --- a/internal/tracker/rutracker/parser/label_packs_test.go +++ b/internal/tracker/rutracker/parser/label_packs_test.go @@ -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, @@ -47,20 +47,22 @@ func TestLabelPacksParser(t *testing.T) { wantParseOK: true, }, { - name: "techno label with brackets", - title: "(Techno, IDM, Experimental) [WEB,CD] Label - Stroboscopic Artefacts (2009-2022) [96 Releases], FLAC (tracks) (tracks+.cue), lossless", - wantLabel: "Stroboscopic Artefacts", - wantYear: 2009, - wantYearEnd: 2022, - wantParseOK: true, + name: "techno label with brackets", + 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", - wantLabel: "Auxiliary", - wantYear: 2010, - wantYearEnd: 2021, - wantParseOK: true, + name: "multi-genre label", + 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, }, { name: "Russian release count", @@ -81,12 +83,13 @@ func TestLabelPacksParser(t *testing.T) { wantParseOK: true, }, { - name: "large techno label", - title: "(Techno) [WEB,CD] Label - Planet Rhythm Records [1994 - 2021] [443xReleases], FLAC (tracks) (tracks+.cue, image+.cue), lossless", - wantLabel: "Planet Rhythm Records", - wantYear: 1994, - wantYearEnd: 2021, - wantParseOK: true, + name: "large techno label", + 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, }, { name: "label with featured artists", diff --git a/internal/tracker/rutracker/parser/lossless_test.go b/internal/tracker/rutracker/parser/lossless_test.go index c6cc765..6964859 100644 --- a/internal/tracker/rutracker/parser/lossless_test.go +++ b/internal/tracker/rutracker/parser/lossless_test.go @@ -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, diff --git a/internal/tracker/rutracker/parser/lossy_test.go b/internal/tracker/rutracker/parser/lossy_test.go index 9c2fef2..155f594 100644 --- a/internal/tracker/rutracker/parser/lossy_test.go +++ b/internal/tracker/rutracker/parser/lossy_test.go @@ -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, diff --git a/internal/tracker/rutracker/parser/metal_test.go b/internal/tracker/rutracker/parser/metal_test.go new file mode 100644 index 0000000..8fb0338 --- /dev/null +++ b/internal/tracker/rutracker/parser/metal_test.go @@ -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) + } + }) + } +} diff --git a/internal/tracker/rutracker/parser/patterns.go b/internal/tracker/rutracker/parser/patterns.go index 241e221..c84b5cb 100644 --- a/internal/tracker/rutracker/parser/patterns.go +++ b/internal/tracker/rutracker/parser/patterns.go @@ -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).*$`) ) diff --git a/internal/tracker/rutracker/parser/soundtracks_test.go b/internal/tracker/rutracker/parser/soundtracks_test.go new file mode 100644 index 0000000..73131bb --- /dev/null +++ b/internal/tracker/rutracker/parser/soundtracks_test.go @@ -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) + } + }) + } +} diff --git a/internal/tracker/rutracker/parser/vinyl_digitization_test.go b/internal/tracker/rutracker/parser/vinyl_digitization_test.go index 49dba94..9da8644 100644 --- a/internal/tracker/rutracker/parser/vinyl_digitization_test.go +++ b/internal/tracker/rutracker/parser/vinyl_digitization_test.go @@ -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,