Remove rutracker parser, replace with GenericParser for all indexer results

This commit is contained in:
Alexander
2026-05-09 21:50:55 +02:00
parent ef75b9bfba
commit 7fa859e815
56 changed files with 3 additions and 5215 deletions
+3 -5
View File
@@ -8,7 +8,7 @@ import (
pb "homelab.lan/music-agregator/gen/music_agregator/indexer/v1"
"homelab.lan/music-agregator/internal/release"
"homelab.lan/music-agregator/internal/tracker/rutracker"
"homelab.lan/music-agregator/internal/tracker"
)
type SearchResult struct {
@@ -90,15 +90,13 @@ func (sr *SearchResponse) ToProto() *pb.SearchResponse {
return &pb.SearchResponse{Result: pbItems}
}
var (
rutrackerParserFactory = rutracker.NewRuTrackerParserFactory()
)
var genericParser = tracker.NewGenericParser()
func (sr *SearchResult) ToSearchResponse() *SearchResponse {
var items []*SearchItemResult
for _, item := range sr.Items {
rel := rutrackerParserFactory.GetParser(item.Categories).Parse(item.Title)
rel := genericParser.Parse(item.Title)
log.Trace().
Str("tracker", item.JackettIndexer.ID).
-5
View File
@@ -1,5 +0,0 @@
package tracker
type ParserFactory interface {
GetParser(categories []string) Parser
}
-7
View File
@@ -1,7 +0,0 @@
package tracker
import "homelab.lan/music-agregator/internal/release"
type Parser interface {
Parse(title string) *release.Release
}
-404
View File
@@ -1,404 +0,0 @@
package rutracker
var RockForumIDs = []int{
1698, // Зарубежный Rock (parent)
1702, // Classic Rock & Hard Rock (lossless)
1703, // Classic Rock & Hard Rock (lossy)
1704, // Progressive & Art-Rock (lossless)
1705, // Progressive & Art-Rock (lossy)
1706, // Folk-Rock (lossless)
1707, // Folk-Rock (lossy)
1708, // Pop-Rock & Soft Rock (lossless)
1709, // Pop-Rock & Soft Rock (lossy)
1710, // Instrumental Guitar Rock (lossless)
1711, // Instrumental Guitar Rock (lossy)
1712, // Rockabilly, Psychobilly, Rock'n'Roll (lossless)
1713, // Rockabilly, Psychobilly, Rock'n'Roll (lossy)
1714, // Восточноазиатский рок (lossless)
1715, // Восточноазиатский рок (lossy)
722, // Отечественный Rock, Metal (parent)
951, // Rock на языках народов xUSSR (lossless)
952, // Rock на языках народов xUSSR (lossy)
172, // Post-Punk, Shoegaze, Garage Rock, Noise Rock (lossless)
236, // Post-Punk, Shoegaze, Garage Rock, Noise Rock (lossy)
2175, // Avant-garde, Experimental Rock (lossless)
2174, // Avant-garde, Experimental Rock (lossy)
2329, // AOR (Melodic Hard Rock, Arena rock) (lossless)
2330, // AOR (Melodic Hard Rock, Arena rock) (lossy)
731, // Сборники зарубежного рока (lossless)
1799, // Сборники зарубежного рока (lossy)
737, // Rock (lossless)
738, // Rock (lossy)
}
var MetalForumIDs = []int{
1716, // Зарубежный Metal (parent)
739, // Metal (lossless)
740, // Metal (lossy)
1719, // Black (lossless)
1778, // Black (lossy)
1720, // Folk, Pagan, Viking (lossless)
798, // Folk, Pagan, Viking (lossy)
1724, // Gothic Metal (lossless)
1725, // Gothic Metal (lossy)
1726, // Heavy, Power, Progressive (lossless)
1727, // Heavy, Power, Progressive (lossy)
1728, // Thrash, Speed (lossless)
1729, // Thrash, Speed (lossy)
1730, // Grind, Brutal Death (lossless)
1731, // Grind, Brutal Death (lossy)
1779, // Death, Doom (lossless)
1780, // Death, Doom (lossy)
1796, // Avant-garde, Experimental Metal (lossless)
1797, // Avant-garde, Experimental Metal (lossy)
1815, // Sludge, Stoner, Post-Metal (lossless)
1816, // Sludge, Stoner, Post-Metal (lossy)
1766, // Зарубежный и Отечественный Metal (оцифровки)
}
var AlternativeForumIDs = []int{
1732, // Зарубежные Alternative, Punk, Independent (parent)
464, // Alternative, Punk, Independent (lossless)
463, // Alternative, Punk, Independent (lossy)
123, // Alternative, Punk, Independent (оцифровки)
1736, // Alternative & Nu-metal (lossless)
1737, // Alternative & Nu-metal (lossy)
1738, // Punk (lossless)
1739, // Punk (lossy)
1740, // Hardcore (lossless)
1741, // Hardcore (lossy)
1742, // Post-Rock (lossless)
1743, // Post-Rock (lossy)
1744, // Industrial & Post-industrial (lossless)
1745, // Industrial & Post-industrial (lossy)
1746, // Emocore, Post-hardcore, Metalcore (lossless)
1747, // Emocore, Post-hardcore, Metalcore (lossy)
1748, // Gothic Rock & Dark Folk (lossless)
1749, // Gothic Rock & Dark Folk (lossy)
1773, // Indie Rock, Indie Pop, Dream Pop, Brit-Pop (lossless)
202, // Indie Rock, Indie Pop, Dream Pop, Brit-Pop (lossy)
466, // Synthwave, Spacesynth, Dreamwave, Retrowave, Outrun (lossless)
465, // Synthwave, Spacesynth, Dreamwave, Retrowave, Outrun (lossy)
}
var PopForumIDs = []int{
2495, // Отечественная поп-музыка (parent)
2497, // Зарубежная поп-музыка (parent)
425, // Популярная музыка России и стран бывшего СССР (lossless)
424, // Популярная музыка России и стран бывшего СССР (lossy)
429, // Зарубежная поп-музыка (lossless)
428, // Зарубежная поп-музыка (lossy)
1753, // Итальянская поп-музыка (lossless)
735, // Итальянская поп-музыка (lossy)
714, // Латиноамериканская поп-музыка (lossless)
2232, // Латиноамериканская поп-музыка (lossy)
1330, // Восточноазиатская поп-музыка (lossless)
1331, // Восточноазиатская поп-музыка (lossy)
1634, // Советская эстрада, ретро, романсы (lossless)
1635, // Советская эстрада, ретро, романсы (lossy)
1361, // Популярная музыка России и стран бывшего СССР (сборники) (lossy)
1362, // Зарубежная поп-музыка (сборники) (lossy)
2270, // Easy Listening, Instrumental Pop (lossless)
2275, // Easy Listening, Instrumental Pop (lossy)
}
var ElectronicForumIDs = []int{
1807, // House, Techno, Hardcore, Hardstyle, Jumpstyle (parent)
1808, // Drum & Bass, Jungle, Breakbeat, Dubstep, IDM, Electro (parent)
1809, // Chillout, Lounge, Downtempo, Trip-Hop (parent)
1810, // Traditional Electronic, Ambient, Modern Classical, Electroacoustic, Experimental (parent)
1811, // Industrial, Noise, EBM, Dark Electro, Aggrotech, Cyberpunk, Synthpop, New Wave (parent)
1821, // Trance, Goa Trance, Psy-Trance, PsyChill, Ambient, Dub (parent)
2499, // Eurodance, Disco, Hi-NRG (parent)
797, // Electro, Electro-Freestyle, Nu Electro (lossless)
1805, // Electro, Electro-Freestyle, Nu Electro (lossy)
1857, // House (lossless)
1858, // House (lossy)
1860, // House (Singles, EPs) (lossy)
840, // House (Проморелизы, сборники) (lossy)
1825, // Techno (lossless)
1826, // Techno (lossy)
1828, // Techno (Singles, EPs) (lossy)
1829, // Hardcore, Hardstyle, Jumpstyle (lossless)
1830, // Hardcore, Hardstyle, Jumpstyle (lossy)
1831, // Hardcore, Hardstyle, Jumpstyle (vinyl, web)
1832, // Drum & Bass, Jungle (lossless)
1833, // Drum & Bass, Jungle (lossy)
1836, // Breakbeat (lossless)
1837, // Breakbeat (lossy)
1839, // Dubstep (lossless)
454, // Dubstep (lossy)
1840, // IDM (lossless)
1841, // IDM (lossy)
2229, // IDM Discography & Collections (lossy)
1818, // Trance (lossless)
1819, // Trance (lossy)
1847, // Trance (Singles, EPs) (lossy)
1844, // Goa Trance, Psy-Trance (lossless)
1822, // Goa Trance, Psy-Trance (lossy)
1894, // PsyChill, Ambient, Dub (lossless)
1895, // PsyChill, Ambient, Dub (lossy)
1861, // Chillout, Lounge, Downtempo (lossless)
1862, // Chillout, Lounge, Downtempo (lossy)
1945, // Trip Hop, Abstract Hip-Hop (lossless)
1944, // Trip Hop, Abstract Hip-Hop (lossy)
1864, // Traditional Electronic, Ambient (lossless)
1865, // Traditional Electronic, Ambient (lossy)
1871, // Modern Classical, Electroacoustic (lossless)
1867, // Modern Classical, Electroacoustic (lossy)
1869, // Experimental (lossless)
1873, // Experimental (lossy)
1866, // Darkwave, Neoclassical, Ethereal, Dungeon Synth (lossless)
406, // Darkwave, Neoclassical, Ethereal, Dungeon Synth (lossy)
1868, // EBM, Dark Electro, Aggrotech (lossless)
1875, // EBM, Dark Electro, Aggrotech (lossy)
1877, // Industrial, Noise (lossless)
1878, // Industrial, Noise (lossy)
1880, // Synthpop, Futurepop, New Wave, Electropop (lossless)
1881, // Synthpop, Futurepop, New Wave, Electropop (lossy)
1907, // Cyberpunk, 8-bit, Chiptune (lossy & lossless)
2500, // Disco, Italo-Disco, Euro-Disco, Hi-NRG (lossless)
2501, // Disco, Italo-Disco, Euro-Disco, Hi-NRG (lossy)
2502, // Eurodance, Euro-House, Technopop (lossless)
2503, // Eurodance, Euro-House, Technopop (lossy)
2504, // Eurodance, Euro-House, Technopop (сборники) (lossy)
2505, // Disco, Italo-Disco, Euro-Disco, Hi-NRG (сборники) (lossy)
}
var HipHopForumIDs = []int{
408, // Рэп, Хип-Хоп, R'n'B (parent)
441, // Отечественный Рэп, Хип-Хоп (lossy)
1486, // Отечественный Рэп, Хип-Хоп, R'n'B (lossless)
446, // Зарубежный Рэп, Хип-Хоп (lossy)
909, // Зарубежный Рэп, Хип-Хоп (lossless)
1665, // Зарубежный R'n'B (lossless)
1172, // Зарубежный R'n'B (lossy)
1173, // Отечественный R'n'B (lossy)
2283, // Funk, Soul, R&B (lossless)
}
var JazzForumIDs = []int{
2267, // Зарубежный джаз (parent)
2269, // Отечественный джаз и блюз (parent)
2277, // Early Jazz, Swing, Gypsy (lossless)
2278, // Bop (lossless)
2279, // Mainstream Jazz, Cool (lossless)
2280, // Jazz Fusion (lossless)
2281, // World Fusion, Ethnic Jazz (lossless)
2282, // Avant-Garde Jazz, Free Improvisation (lossless)
2284, // Smooth, Jazz-Pop (lossless)
2285, // Vocal Jazz (lossless)
2286, // Сборники зарубежного джаза (lossless)
2287, // Зарубежный джаз (lossy)
2353, // Modern Creative, Third Stream (lossless)
1947, // Nu Jazz, Acid Jazz, Future Jazz (lossless)
1946, // Nu Jazz, Acid Jazz, Future Jazz (lossy)
2297, // Отечественный джаз (lossless)
2295, // Отечественный джаз (lossy)
}
var BluesForumIDs = []int{
2268, // Зарубежный блюз (parent)
2290, // Roots, Pre-War Blues, Early R&B, Gospel (lossless)
2292, // Blues-rock (lossless)
2293, // Blues (Texas, Chicago, Modern and Others) (lossless)
2288, // Зарубежный блюз (lossy)
2289, // Зарубежный блюз (сборники; Tribute VA) (lossless)
2296, // Отечественный блюз (lossless)
2298, // Отечественный блюз (lossy)
}
var ClassicalForumIDs = []int{
409, // Классическая и современная академическая музыка (parent)
556, // Вокальная музыка (lossless)
557, // Оркестровая музыка (lossless)
558, // Камерная инструментальная музыка (lossless)
793, // Сольная инструментальная музыка (lossless)
794, // Опера (lossless)
560, // Полные собрания сочинений и многодисковые издания (lossless)
436, // Полные собрания сочинений и многодисковые издания (lossy)
2307, // Хоровая музыка (lossless)
2308, // Концерт для инструмента с оркестром (lossless)
2309, // Вокальная и хоровая музыка (lossy)
2310, // Оркестровая музыка (lossy)
2311, // Камерная и сольная инструментальная музыка (lossy)
969, // Классика в современной обработке, Classical Crossover (lossy и lossless)
}
var FolkForumIDs = []int{
1125, // Фольклор, Народная и Этническая музыка (parent)
1127, // New Age & Meditative (lossless)
1126, // New Age & Meditative (lossy)
1129, // Этническая музыка Сибири, Средней и Восточной Азии (lossless)
1128, // Этническая музыка Сибири, Средней и Восточной Азии (lossy)
1131, // Восточноевропейский фолк (lossless)
1130, // Восточноевропейский фолк (lossy)
1133, // Западноевропейский фолк (lossless)
1132, // Западноевропейский фолк (lossy)
1135, // Фламенко и акустическая гитара (lossless)
1134, // Фламенко и акустическая гитара (lossy)
1137, // Country, Bluegrass (lossless)
1136, // Country, Bluegrass (lossy)
1138, // Этническая музыка Австралии, Тихого и Индийского океанов (lossy и lossless)
1282, // Фольклорная, Народная, Эстрадная музыка Кавказа и Закавказья (lossy и lossless)
2085, // Этническая музыка Африки и Ближнего Востока (lossless)
1283, // Этническая музыка Африки и Ближнего Востока (lossy)
1285, // Этническая музыка Северной и Южной Америки (lossless)
1284, // Этническая музыка Северной и Южной Америки (lossy)
1856, // Этническая музыка Индии (lossy)
2430, // Этническая музыка Индии (lossless)
2084, // Klezmer и Еврейский фольклор (lossy и lossless)
}
var ReggaeForumIDs = []int{
1760, // Reggae, Ska, Dub (parent)
1764, // Rocksteady, Early Reggae, Ska-Jazz, Trad.Ska (lossy и lossless)
1765, // Reggae (lossy)
1768, // Reggae, Dancehall, Dub (lossless)
1769, // Ska-Punk, Ska-Core (lossy)
1770, // Dancehall, Raggamuffin (lossy)
1771, // Dub (lossy)
1772, // Отечественный Reggae, Ska, Dub (lossy и lossless)
1774, // Ska, Ska-Punk, Ska-Jazz (lossless)
1767, // 3rd Wave Ska (lossy)
2233, // Reggae, Ska, Dub (компиляции) (lossy и lossless)
}
var SoundtrackForumIDs = []int{
416, // Саундтреки, караоке и мюзиклы (parent)
691, // Саундтреки к отечественным фильмам (lossless)
469, // Саундтреки к отечественным фильмам (lossy)
786, // Саундтреки к зарубежным фильмам (lossless)
785, // Саундтреки к зарубежным фильмам (lossy)
784, // Саундтреки к играм (lossless)
783, // Саундтреки к играм (lossy)
715, // Саундтреки к мультфильмам (lossy и lossless)
1631, // Саундтреки к сериалам (lossless)
1499, // Саундтреки к сериалам (lossy)
1388, // Саундтреки к аниме (lossless)
282, // Саундтреки к аниме (lossy)
796, // Неофициальные саундтреки к фильмам и сериалам (lossy)
2331, // Неофициальные саундтреки к играм (lossy)
2431, // Аранжировки музыки из игр (lossy и lossless)
880, // Мюзикл (lossy и lossless)
}
var ShansonForumIDs = []int{
1215, // Шансон, Авторская и Военная песня (parent)
1220, // Отечественный шансон (lossless)
1221, // Отечественный шансон (lossy)
1452, // Зарубежный шансон (lossless)
1219, // Зарубежный шансон (lossy)
1216, // Военная песня, марши (lossless)
1223, // Военная песня, марши (lossy)
1224, // Авторская песня (lossless)
1225, // Авторская песня (lossy)
1226, // Менестрели и ролевики (lossy и lossless)
1334, // Сборники отечественного шансона (lossy)
}
var HiResForumIDs = []int{
1299, // Hi-Res stereo и многоканальная музыка (parent)
1755, // Рок-музыка (Hi-Res stereo)
1757, // Рок-музыка (многоканальная музыка)
1884, // Классика и классика в современной обработке (Hi-Res stereo)
1164, // Классика и классика в современной обработке (многоканальная музыка)
1885, // Поп-музыка (Hi-Res stereo)
1163, // Поп-музыка (многоканальная музыка)
1893, // Электронная музыка (Hi-Res stereo)
1890, // Электронная музыка (многоканальная музыка)
2302, // Джаз и Блюз (Hi-Res stereo)
2303, // Джаз и Блюз (многоканальная музыка)
1397, // Саундтреки (Hi-Res stereo и многоканальная музыка)
2512, // Музыка разных жанров (Hi-Res stereo и многоканальная музыка)
2513, // New Age, Relax, Meditative & Flamenco (Hi-Res stereo и многоканальная музыка)
1170, // Конверсии SACD
453, // Конверсии Quadraphonic
1759, // Конверсии Blu-Ray, ADVD и DVD-Audio
1852, // Апмиксы-Upmixes
860, // Неофициальные конверсии цифровых форматов (parent)
}
var DigitizationForumIDs = []int{
2219, // Оцифровки с аналоговых носителей (parent)
239, // Отечественная поп-музыка (оцифровки)
1444, // Зарубежная поп-музыка (оцифровки)
450, // Инструментальная поп-музыка (оцифровки)
1756, // Зарубежная рок-музыка (оцифровки)
1758, // Отечественная рок-музыка (оцифровки)
1754, // Электронная музыка (оцифровки)
1660, // Классика и классика в современной обработке (оцифровки)
506, // Фольклор, народная и этническая музыка (оцифровки)
1835, // Rap, Hip-Hop, R'n'B, Reggae, Ska, Dub (оцифровки)
2301, // Джаз и блюз (оцифровки)
1217, // Шансон, авторские, военные песни и марши (оцифровки)
1625, // Саундтреки и мюзиклы (оцифровки)
2401, // Советская эстрада, ретро, романсы (оцифровки)
974, // Музыка других жанров (оцифровки)
}
var LabelPackForumIDs = []int{
782, // Лейбл- и сцен-паки (parent)
1842, // Лейбл-паки (lossless)
1648, // Лейбл-паки, Сцен-паки (lossy)
134, // Неофициальные сборники и ремастеринги (lossless)
965, // Неофициальные сборники (lossy)
577, // AI-Music (lossy и lossless)
2230, // Сборники (lossless)
2231, // Сборники (lossy)
}
var RadioshowForumIDs = []int{
1859, // House (Radioshow, Podcast, Liveset, Mixes)
1824, // Trance (Radioshows, Podcasts, Live Sets, Mixes) (lossy)
1827, // Techno (Radioshows, Podcasts, Livesets, Mixes)
1834, // Drum & Bass, Jungle (Radioshows, Podcasts, Livesets, Mixes)
1838, // Breakbeat, Dubstep (Radioshows, Podcasts, Livesets, Mixes)
460, // Goa Trance, Psy-Trance, PsyChill, Ambient, Dub (Live Sets, Mixes) (lossy)
}
var AACForumIDs = []int{
2240, // Музыка Lossy (AAC-iTunes)
2244, // Музыка Lossy (AAC) (Singles, EPs)
2248, // Музыка Lossy (AAC)
1927, // Музыка lossless (ALAC)
}
var MiscMusicForumIDs = []int{
1395, // Духовные песнопения и музыка (lossless)
1396, // Духовные песнопения и музыка (lossy)
1351, // Сборники песен для детей (lossy и lossless)
2018, // Музыка для бальных танцев (lossy и lossless)
855, // Звуки природы
1929, // Смешанные стили
}
var AllMusicForumIDs = concat(
RockForumIDs,
MetalForumIDs,
AlternativeForumIDs,
PopForumIDs,
ElectronicForumIDs,
HipHopForumIDs,
JazzForumIDs,
BluesForumIDs,
ClassicalForumIDs,
FolkForumIDs,
ReggaeForumIDs,
SoundtrackForumIDs,
ShansonForumIDs,
HiResForumIDs,
DigitizationForumIDs,
LabelPackForumIDs,
RadioshowForumIDs,
AACForumIDs,
MiscMusicForumIDs,
)
func concat(slices ...[]int) []int {
var result []int
for _, s := range slices {
result = append(result, s...)
}
return result
}
-118
View File
@@ -1,118 +0,0 @@
package rutracker
import (
"strconv"
"homelab.lan/music-agregator/internal/tracker"
"homelab.lan/music-agregator/internal/tracker/rutracker/parser"
)
type parserType int
const (
parserGeneral parserType = iota
parserRock
parserMetal
parserAlternative
parserPop
parserElectronic
parserHipHop
parserJazz
parserBlues
parserClassical
parserFolk
parserReggae
parserSoundtracks
parserShanson
parserHiRes
parserDigitization
parserLabelPacks
parserRadioshow
parserAAC
parserMiscMusic
)
var categoryToParser map[int]parserType
func init() {
categoryToParser = make(map[int]parserType)
categoryToParser[3000] = parserGeneral
categoryToParser[3010] = parserGeneral
categoryToParser[3040] = parserGeneral
registerAll(RockForumIDs, parserRock)
registerAll(MetalForumIDs, parserMetal)
registerAll(AlternativeForumIDs, parserAlternative)
registerAll(PopForumIDs, parserPop)
registerAll(ElectronicForumIDs, parserElectronic)
registerAll(HipHopForumIDs, parserHipHop)
registerAll(JazzForumIDs, parserJazz)
registerAll(BluesForumIDs, parserBlues)
registerAll(ClassicalForumIDs, parserClassical)
registerAll(FolkForumIDs, parserFolk)
registerAll(ReggaeForumIDs, parserReggae)
registerAll(SoundtrackForumIDs, parserSoundtracks)
registerAll(ShansonForumIDs, parserShanson)
registerAll(HiResForumIDs, parserHiRes)
registerAll(DigitizationForumIDs, parserDigitization)
registerAll(LabelPackForumIDs, parserLabelPacks)
registerAll(RadioshowForumIDs, parserRadioshow)
registerAll(AACForumIDs, parserAAC)
registerAll(MiscMusicForumIDs, parserMiscMusic)
}
func registerAll(ids []int, pt parserType) {
for _, id := range ids {
categoryToParser[id] = pt
}
}
type ParserFactory struct {
parsers map[parserType]tracker.Parser
}
func NewRuTrackerParserFactory() *ParserFactory {
return &ParserFactory{
parsers: map[parserType]tracker.Parser{
parserGeneral: parser.NewGeneralParser(),
parserRock: parser.NewRockParser(),
parserMetal: parser.NewMetalParser(),
parserAlternative: parser.NewAlternativeParser(),
parserPop: parser.NewPopParser(),
parserElectronic: parser.NewElectronicParser(),
parserHipHop: parser.NewHipHopParser(),
parserJazz: parser.NewJazzParser(),
parserBlues: parser.NewBluesParser(),
parserClassical: parser.NewClassicalParser(),
parserFolk: parser.NewFolkParser(),
parserReggae: parser.NewReggaeParser(),
parserSoundtracks: parser.NewSoundtracksParser(),
parserShanson: parser.NewShansonParser(),
parserHiRes: parser.NewHiResParser(),
parserDigitization: parser.NewVinylDigitizationParser(),
parserLabelPacks: parser.NewLabelPacksParser(),
parserRadioshow: parser.NewRadioshowParser(),
parserAAC: parser.NewAACParser(),
parserMiscMusic: parser.NewMiscMusicParser(),
},
}
}
const jackettIDOffset = 100000
func (f *ParserFactory) GetParser(categories []string) tracker.Parser {
for _, cat := range categories {
catID, err := strconv.Atoi(cat)
if err != nil {
continue
}
if catID >= jackettIDOffset {
catID -= jackettIDOffset
}
if pt, ok := categoryToParser[catID]; ok {
return f.parsers[pt]
}
}
return f.parsers[parserGeneral]
}
-109
View File
@@ -1,109 +0,0 @@
package rutracker
import (
"testing"
"homelab.lan/music-agregator/internal/tracker"
"homelab.lan/music-agregator/internal/tracker/rutracker/parser"
)
func TestParserFactory_GetParser(t *testing.T) {
f := NewRuTrackerParserFactory()
tests := []struct {
name string
categories []string
wantType string
}{
{"torznab general 3000", []string{"3000"}, "*parser.GeneralParser"},
{"torznab general 3010", []string{"3010"}, "*parser.GeneralParser"},
{"torznab general 3040", []string{"3040"}, "*parser.GeneralParser"},
{"rock forum", []string{"1702"}, "*parser.RockParser"},
{"metal forum raw id", []string{"1728"}, "*parser.MetalParser"},
{"metal forum jackett id", []string{"101728"}, "*parser.MetalParser"},
{"alternative forum", []string{"464"}, "*parser.AlternativeParser"},
{"pop forum", []string{"425"}, "*parser.PopParser"},
{"electronic forum", []string{"1857"}, "*parser.ElectronicParser"},
{"hiphop forum", []string{"909"}, "*parser.HipHopParser"},
{"jazz forum", []string{"2277"}, "*parser.JazzParser"},
{"blues forum", []string{"2292"}, "*parser.BluesParser"},
{"classical forum", []string{"556"}, "*parser.ClassicalParser"},
{"folk forum", []string{"1127"}, "*parser.FolkParser"},
{"reggae forum", []string{"1768"}, "*parser.ReggaeParser"},
{"soundtrack forum", []string{"786"}, "*parser.SoundtracksParser"},
{"shanson forum", []string{"1220"}, "*parser.ShansonParser"},
{"hires forum", []string{"1755"}, "*parser.HiResParser"},
{"digitization forum", []string{"239"}, "*parser.VinylDigitizationParser"},
{"label pack forum", []string{"1842"}, "*parser.LabelPacksParser"},
{"radioshow forum", []string{"1859"}, "*parser.RadioshowParser"},
{"aac forum", []string{"2240"}, "*parser.AACParser"},
{"misc music forum", []string{"1395"}, "*parser.MiscMusicParser"},
{"unknown category falls back to general", []string{"99999"}, "*parser.GeneralParser"},
{"empty categories falls back to general", []string{}, "*parser.GeneralParser"},
{"multiple categories uses first match", []string{"99999", "1728"}, "*parser.MetalParser"},
{"jackett prefixed id stripped", []string{"101719"}, "*parser.MetalParser"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := f.GetParser(tt.categories)
gotType := getParserTypeName(p)
if gotType != tt.wantType {
t.Errorf("GetParser(%v) = %v, want %v", tt.categories, gotType, tt.wantType)
}
})
}
}
func getParserTypeName(p tracker.Parser) string {
switch p.(type) {
case *parser.GeneralParser:
return "*parser.GeneralParser"
case *parser.LosslessParser:
return "*parser.LosslessParser"
case *parser.LossyParser:
return "*parser.LossyParser"
case *parser.HiResParser:
return "*parser.HiResParser"
case *parser.VinylDigitizationParser:
return "*parser.VinylDigitizationParser"
case *parser.ClassicalParser:
return "*parser.ClassicalParser"
case *parser.JazzParser:
return "*parser.JazzParser"
case *parser.MetalParser:
return "*parser.MetalParser"
case *parser.SoundtracksParser:
return "*parser.SoundtracksParser"
case *parser.DiscographyParser:
return "*parser.DiscographyParser"
case *parser.LabelPacksParser:
return "*parser.LabelPacksParser"
case *parser.RockParser:
return "*parser.RockParser"
case *parser.AlternativeParser:
return "*parser.AlternativeParser"
case *parser.PopParser:
return "*parser.PopParser"
case *parser.ElectronicParser:
return "*parser.ElectronicParser"
case *parser.HipHopParser:
return "*parser.HipHopParser"
case *parser.BluesParser:
return "*parser.BluesParser"
case *parser.FolkParser:
return "*parser.FolkParser"
case *parser.ReggaeParser:
return "*parser.ReggaeParser"
case *parser.ShansonParser:
return "*parser.ShansonParser"
case *parser.RadioshowParser:
return "*parser.RadioshowParser"
case *parser.AACParser:
return "*parser.AACParser"
case *parser.MiscMusicParser:
return "*parser.MiscMusicParser"
default:
return "unknown"
}
}
-25
View File
@@ -1,25 +0,0 @@
package rutracker
import "strconv"
type Filter struct{}
func NewFilter() *Filter {
return &Filter{}
}
func (f *Filter) IsKnownCategory(categories []string) bool {
for _, cat := range categories {
catID, err := strconv.Atoi(cat)
if err != nil {
continue
}
if catID >= jackettIDOffset {
catID -= jackettIDOffset
}
if _, ok := categoryToParser[catID]; ok {
return true
}
}
return false
}
-35
View File
@@ -1,35 +0,0 @@
package rutracker
import "testing"
func TestFilter_IsKnownCategory(t *testing.T) {
f := NewFilter()
tests := []struct {
name string
categories []string
want bool
}{
{"torznab lossless", []string{"3040"}, true},
{"torznab lossy", []string{"3010"}, true},
{"torznab general audio", []string{"3000"}, true},
{"rutracker pop forum", []string{"425"}, true},
{"rutracker hires forum", []string{"1755"}, true},
{"rutracker metal forum", []string{"1728"}, true},
{"jackett prefixed id", []string{"101728"}, true},
{"unknown category", []string{"99999"}, false},
{"empty categories", []string{}, false},
{"books category", []string{"7000"}, false},
{"mixed known and unknown", []string{"99999", "3040"}, true},
{"invalid non-numeric", []string{"abc"}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := f.IsKnownCategory(tt.categories)
if got != tt.want {
t.Errorf("IsKnownCategory(%v) = %v, want %v", tt.categories, got, tt.want)
}
})
}
}
-37
View File
@@ -1,37 +0,0 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type AACParser struct {
BaseParser
}
func NewAACParser() *AACParser {
return &AACParser{}
}
func (p *AACParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
if len(r.Genres) == 0 {
}
r.Type = p.DetectType(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Bitrate = p.ExtractBitrate(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.ReleaseCount = p.ExtractReleaseCount(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
@@ -1,142 +0,0 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestAACParser(t *testing.T) {
p := NewAACParser()
tests := []struct {
name string
title string
wantArtist string
wantYear int
wantFormat release.AudioFormat
wantType release.Type
wantBitrate string
wantParseOK bool
}{
{
name: "Pop AAC VBR",
title: "(Pop) Zivert - Айсберг (Apple Music Home Session) - 2022, AAC (tracks), VBR 256 kbps",
wantArtist: "Zivert",
wantYear: 2022,
wantFormat: release.FormatAAC,
wantParseOK: true,
},
{
name: "OST ALAC CD",
title: "(OST) [CD] Rockstar Games Presents Music From And Inspired By Grand Theft Auto IV: Vladivostok FM - 2008, ALAC (tracks+.cue), lossless",
wantArtist: "Rockstar Games Presents Music From And Inspired By Grand Theft Auto IV: Vladivostok FM",
wantYear: 2008,
wantFormat: release.FormatALAC,
wantType: release.TypeSoundtrack,
wantParseOK: true,
},
{
name: "Hip-hop ALAC discography",
title: "(Hip-Hop, rap rock, hardcore rap, chopper) [CD`39|WEB`6] [Strange Music] Tech N9ne - Дискография / Discography - 1999-2025, ALAC (tracks+.cue), lossless",
wantArtist: "Tech N9ne",
wantYear: 1999,
wantFormat: release.FormatALAC,
wantType: release.TypeDiscography,
wantParseOK: true,
},
{
name: "Art rock iTunes AAC",
title: "(Art Rock / Pop Rock) Roxy Music - Дискография / iTunes Discography - 1972-2004 [WEB], AAC (tracks), 256 kbps",
wantArtist: "Roxy Music",
wantYear: 1972,
wantFormat: release.FormatAAC,
wantType: release.TypeDiscography,
wantBitrate: "256 kbps",
wantParseOK: true,
},
{
name: "Jazz AAC 320",
title: "(Jazz, Post-Bop, Modal Music) Masaru Imada + Kenji Kohsei Quartet - All Of A Glow (Hiroshi Murakami, Kenji Kosei, Masaru Imada, Nobuyoshi Ino) - 1978, AAC (tracks), 320 kbps",
wantArtist: "Masaru Imada + Kenji Kohsei Quartet",
wantYear: 1978,
wantFormat: release.FormatAAC,
wantBitrate: "320 kbps",
wantParseOK: true,
},
{
name: "Rock pop ALAC digital master",
title: "(Rock Pop) [WEB] Bryan Adams - Ultimate [Apple Music Digital Master] {24-44.1} - 2017, ALAC (tracks), lossless",
wantArtist: "Bryan Adams",
wantYear: 2017,
wantFormat: release.FormatALAC,
wantParseOK: true,
},
{
name: "Alternative electronic VA AAC",
title: "(Alternative, Electronic) VA - Astralwerks - Music In 20/20 (Feat. The Chemical Brothers, Doves, Swedish House Mafia, Air, Diamond Rings, Hot Chip, Kings Of Convenience, The Kooks, Kraftwerk & more) - 2013, AAC (tracks), TVBR q127",
wantArtist: "VA",
wantYear: 2013,
wantFormat: release.FormatAAC,
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "Eurodance ALAC multi-CD",
title: "(EuroHouse, EuroDance, Other) [CD] VA - Promotion Dance Hits (Snake's Music) (22 CD), 1994-1996, ALAC, (tracks+.cue), lossless [не flac]",
wantArtist: "VA",
wantYear: 1994,
wantFormat: release.FormatALAC,
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "Lounge chill jazz christmas AAC",
title: "(Lounge, Chill Out, Jazz) VA - Christmas Jazz Night 1-7 (Best X-Mas Jazz Music) - 2017-2023, AAC (tracks), TVBR q127 (WEB)",
wantArtist: "VA",
wantYear: 2017,
wantFormat: release.FormatAAC,
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "Electro house dance AAC",
title: "(Electro, House, Dance) VA - Music & Fashion (The Deep-House Shows), Vol. 1-4 - 2023, AAC (tracks), TVBR q127 (WEB)",
wantArtist: "VA",
wantYear: 2023,
wantFormat: release.FormatAAC,
wantType: release.TypeCompilation,
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)
}
})
}
}
@@ -1,38 +0,0 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type AlternativeParser struct {
BaseParser
}
func NewAlternativeParser() *AlternativeParser {
return &AlternativeParser{}
}
func (p *AlternativeParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
if len(r.Genres) == 0 {
r.Genres = []string{"Alternative"}
}
r.Type = p.DetectType(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Bitrate = p.ExtractBitrate(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.ReleaseCount = p.ExtractReleaseCount(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
@@ -1,133 +0,0 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestAlternativeParser(t *testing.T) {
p := NewAlternativeParser()
tests := []struct {
name string
title string
wantArtist string
wantYear int
wantFormat release.AudioFormat
wantType release.Type
wantBitrate string
wantParseOK bool
}{
{
name: "Nu-metal album",
title: "(Nu-Metal, Alternative Metal) [WEB] Korn - Reward the Scars - 2026, FLAC (tracks), lossless",
wantArtist: "Korn",
wantYear: 2026,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Punk rock discography",
title: "(Punk Rock / Alternative Rock) [CD / WEB] Bayside - Дискография - 2001-2025, (21 CD), FLAC (tracks+cue, tracks), lossless",
wantArtist: "Bayside",
wantYear: 2001,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Alternative metal discography",
title: "(Alternative Metal / Nu Metal) [CD / WEB] Sevendust - Дискография - 1997-2026, (35 CD), FLAC (tracks+cue, tracks), lossless",
wantArtist: "Sevendust",
wantYear: 1997,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Alt rock female vocals",
title: "(Alt. Rock / Alternative Metal / Female Vocals) [WEB] EarlyRise - The Flood Is Coming - 2026, FLAC (tracks), lossless",
wantArtist: "EarlyRise",
wantYear: 2026,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Alternative rock electronic",
title: "(Alternative Rock / Post-Hardcore / Electronic) [WEB] Nvtures Ghost - I Have No Moth And I Must Scream - 2026, FLAC (tracks), lossless",
wantArtist: "Nvtures Ghost",
wantYear: 2026,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Alternative discography",
title: "(Alternative) [WEB] KSB muzic - Дискография - 2022-2025, FLAC (tracks), lossless",
wantArtist: "KSB muzic",
wantYear: 2022,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Russian indie discography",
title: "(Russian Indie, Indie, Rock, Punk, Alternative,) [WEB] Полматери -Дискография (15 релизов) - 2019-2026, FLAC (tracks), lossless",
wantArtist: "Полматери",
wantYear: 2019,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Britpop discography",
title: "(Britpop / Alternative Rock / Indie Rock) [CD / WEB] elbow - Дискография - 2001-2025, (51 CD), FLAC (tracks+cue, tracks), lossless",
wantArtist: "elbow",
wantYear: 2001,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Alternative rock CD",
title: "(Alternative Rock) [CD] Foo Fighters - Your Favorite Toy - 2026, FLAC (tracks+.cue), lossless",
wantArtist: "Foo Fighters",
wantYear: 2026,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Smashing Pumpkins multi-genre",
title: "(Alternative Rock, Shoegaze, Noise Rock, Dream Pop, Alternative Metal) [CD] The Smashing Pumpkins - Machina II: The Friends & Enemies Of Modern Music (Q101) - 2000 (2 CD), FLAC (tracks+.cue), lossless",
wantArtist: "The Smashing Pumpkins",
wantYear: 2000,
wantFormat: release.FormatFLAC,
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)
}
})
}
}
-282
View File
@@ -1,282 +0,0 @@
package parser
import (
"strconv"
"strings"
"homelab.lan/music-agregator/internal/release"
)
type BaseParser struct{}
func (p *BaseParser) NewRelease(title string) *release.Release {
return &release.Release{
RawTitle: title,
ParsedSuccessfully: true,
}
}
func (p *BaseParser) ExtractGenres(title string) []string {
match := genrePattern.FindStringSubmatch(title)
if len(match) < 2 {
return nil
}
raw := match[1]
parts := strings.FieldsFunc(raw, func(r rune) bool {
return r == ',' || r == '/' || r == ';'
})
var genres []string
for _, part := range parts {
trimmed := strings.TrimSpace(part)
if trimmed != "" {
genres = append(genres, trimmed)
}
}
return genres
}
func (p *BaseParser) StripGenrePrefix(title string) string {
return genrePattern.ReplaceAllString(title, "")
}
func (p *BaseParser) StripLeadingTags(title string) string {
return leadingTagsPattern.ReplaceAllString(title, "")
}
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
}
year, _ := strconv.Atoi(match[1])
return year
}
func (p *BaseParser) ExtractYearRange(title string) (int, int) {
if match := releaseYearPattern.FindStringSubmatch(title); len(match) >= 2 {
year, _ := strconv.Atoi(match[1])
return year, 0
}
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 {
return release.FormatUnknown
}
format := strings.ToUpper(match[1])
switch {
case format == "FLAC":
return release.FormatFLAC
case format == "MP3":
return release.FormatMP3
case format == "AAC":
return release.FormatAAC
case format == "APE":
return release.FormatAPE
case format == "WV" || format == "WAVPACK":
return release.FormatWavPack
case format == "ALAC":
return release.FormatALAC
case format == "OGG":
return release.FormatOGG
case format == "WAV":
return release.FormatWAV
default:
return release.FormatUnknown
}
}
func (p *BaseParser) ExtractBitrate(title string) string {
if strings.Contains(strings.ToLower(title), "lossless") {
return "lossless"
}
match := bitratePattern.FindStringSubmatch(title)
if len(match) < 2 {
return ""
}
if match[1] != "" {
return match[1] + " kbps"
}
if match[2] != "" {
return "V" + match[2]
}
if match[3] != "" {
return "VBR ~" + match[3] + " kbps"
}
if match[4] != "" && match[5] != "" {
return "VBR " + match[4] + "-" + match[5] + " kbps"
}
return ""
}
func (p *BaseParser) ExtractRipType(title string) string {
match := ripTypePattern.FindStringSubmatch(title)
if len(match) < 2 {
return ""
}
return strings.ToLower(match[1])
}
func (p *BaseParser) ExtractSource(title string) release.Source {
match := sourceTagPattern.FindStringSubmatch(title)
if len(match) < 2 {
if strings.Contains(strings.ToLower(title), "web") {
return release.SourceWEB
}
return release.SourceUnknown
}
tag := strings.ToUpper(match[1])
switch tag {
case "CD":
return release.SourceCD
case "WEB":
return release.SourceWEB
case "LP", "VINYL", "MINI-LP", "EP", "12\"", "10\"", "7\"":
return release.SourceVinyl
case "SACD", "DVDA", "HDAD":
return release.SourceDVD
default:
return release.SourceUnknown
}
}
func (p *BaseParser) ExtractHiRes(title string) (bitDepth int, sampleRate int) {
match := hiResPattern.FindStringSubmatch(title)
if len(match) < 3 {
return 0, 0
}
bitDepth, _ = strconv.Atoi(match[1])
sr := match[2]
if strings.Contains(sr, ".") {
f, _ := strconv.ParseFloat(sr, 64)
sampleRate = int(f * 1000)
} else {
sampleRate, _ = strconv.Atoi(sr)
sampleRate *= 1000
}
return bitDepth, sampleRate
}
func (p *BaseParser) ExtractSpecialTags(title string) []string {
matches := specialTagPattern.FindAllStringSubmatch(title, -1)
var tags []string
for _, match := range matches {
if len(match) >= 2 {
tags = append(tags, match[1])
}
}
return tags
}
func (p *BaseParser) ExtractReleaseCount(title string) int {
match := releaseCountPattern.FindStringSubmatch(title)
if len(match) < 2 {
return 0
}
count, _ := strconv.Atoi(match[1])
return count
}
func (p *BaseParser) ExtractLabel(title string) string {
match := labelPattern.FindStringSubmatch(title)
if len(match) < 2 {
return ""
}
return strings.TrimSpace(match[1])
}
func (p *BaseParser) ExtractCatalogNum(title string) string {
match := catalogNumPattern.FindStringSubmatch(title)
if len(match) < 2 {
return ""
}
return match[1]
}
func (p *BaseParser) DetectType(title string) release.Type {
switch {
case discographyPattern.MatchString(title):
return release.TypeDiscography
case collectionPattern.MatchString(title):
return release.TypeCollection
case bootlegPattern.MatchString(title):
return release.TypeBootleg
case anthologyPattern.MatchString(title):
return release.TypeCollection
case soundtrackPattern.MatchString(title):
return release.TypeSoundtrack
case livePattern.MatchString(title):
return release.TypeLive
case epPattern.MatchString(title):
return release.TypeEP
case singlePattern.MatchString(title):
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 := tagsBeforeGenrePattern.ReplaceAllString(title, "")
cleaned = p.StripGenrePrefix(cleaned)
cleaned = p.StripLeadingTags(cleaned)
cleaned = trailingTechPattern.ReplaceAllString(cleaned, "")
if match := standardTitlePattern.FindStringSubmatch(cleaned); len(match) >= 3 {
return strings.TrimSpace(match[1]), strings.TrimSpace(match[2])
}
if match := altTitlePattern.FindStringSubmatch(cleaned); len(match) >= 3 {
return strings.TrimSpace(match[1]), strings.TrimSpace(match[2])
}
parts := strings.SplitN(cleaned, " - ", 3)
if len(parts) >= 2 {
artist = strings.TrimSpace(parts[0])
albumPart := strings.TrimSpace(parts[1])
albumPart = yearPattern.ReplaceAllString(albumPart, "")
albumPart = strings.Trim(albumPart, " -,")
album = albumPart
}
return artist, album
}
func (p *BaseParser) AddError(r *release.Release, err string) {
r.ParseErrors = append(r.ParseErrors, err)
r.ParsedSuccessfully = false
}
@@ -1,38 +0,0 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type BluesParser struct {
BaseParser
}
func NewBluesParser() *BluesParser {
return &BluesParser{}
}
func (p *BluesParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
if len(r.Genres) == 0 {
r.Genres = []string{"Blues"}
}
r.Type = p.DetectType(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Bitrate = p.ExtractBitrate(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.ReleaseCount = p.ExtractReleaseCount(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
@@ -1,133 +0,0 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestBluesParser(t *testing.T) {
p := NewBluesParser()
tests := []struct {
name string
title string
wantArtist string
wantYear int
wantFormat release.AudioFormat
wantType release.Type
wantBitrate string
wantParseOK bool
}{
{
name: "Blues rock classic rock reissue",
title: "(Blues Rock, Classic Rock) [CD] Rory Gallagher - Against the Grain - 2018 (1975), FLAC (image+.cue), lossless",
wantArtist: "Rory Gallagher",
wantYear: 2018,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Blues album WEB",
title: "(Blues) [WEB] Roger C. Wade & The Houserockers - Shake it loose! - 2026, FLAC (tracks), lossless",
wantArtist: "Roger C. Wade & The Houserockers",
wantYear: 2026,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Blues rock soldier",
title: "(Blues Rock) [WEB] Krissy Matthews - Rock and Roll Soldier - 2026, FLAC (tracks), lossless",
wantArtist: "Krissy Matthews",
wantYear: 2026,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Blues folk album",
title: "(Blues, Folk) [WEB] Gurf Morlix - Cobwebs & Stardust - 2026, FLAC (tracks), lossless",
wantArtist: "Gurf Morlix",
wantYear: 2026,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Blues dan penn",
title: "(Blues) [WEB] Dan Penn - Smoke Filled Room - 2026, FLAC (tracks), lossless",
wantArtist: "Dan Penn",
wantYear: 2026,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Blues rock paradise",
title: "(Blues Rock) [WEB] Catfish John Tisdell - Blues in Paradise - 2026, FLAC (tracks), lossless",
wantArtist: "Catfish John Tisdell",
wantYear: 2026,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Blues shades",
title: "(Blues) [WEB] Carrie Marshall - Shades of Blue - 2026, FLAC (tracks), lossless",
wantArtist: "Carrie Marshall",
wantYear: 2026,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Blues rock dont be mean",
title: "(Blues Rock) [WEB] Boogie Beasts - Don't Be So Mean! - 2026, FLAC (tracks), lossless",
wantArtist: "Boogie Beasts",
wantYear: 2026,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Blues against machine",
title: "(Blues) [WEB] Blues Against The Machine - VOL. II - 2026, FLAC (tracks), lossless",
wantArtist: "Blues Against The Machine",
wantYear: 2026,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Blues bon appetit",
title: "(Blues) [WEB] Andhrea and the Black Cats - Bon Appetit!! - 2026, FLAC (tracks), lossless",
wantArtist: "Andhrea and the Black Cats",
wantYear: 2026,
wantFormat: release.FormatFLAC,
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)
}
})
}
}
@@ -1,38 +0,0 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type ClassicalParser struct {
BaseParser
}
func NewClassicalParser() *ClassicalParser {
return &ClassicalParser{}
}
func (p *ClassicalParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
if len(r.Genres) == 0 {
r.Genres = []string{"Classical"}
}
r.Type = p.DetectType(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Bitrate = p.ExtractBitrate(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.ReleaseCount = p.ExtractReleaseCount(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
@@ -1,130 +0,0 @@
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])
}
}
})
}
}
@@ -1,54 +0,0 @@
package parser
import (
"strings"
"homelab.lan/music-agregator/internal/release"
)
type DiscographyParser struct {
BaseParser
}
func NewDiscographyParser() *DiscographyParser {
return &DiscographyParser{}
}
func (p *DiscographyParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Bitrate = p.ExtractBitrate(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.ReleaseCount = p.ExtractReleaseCount(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
if collectionPattern.MatchString(title) {
r.Type = release.TypeCollection
} else {
r.Type = release.TypeDiscography
}
r.Artist = p.extractDiscographyArtist(title)
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
func (p *DiscographyParser) extractDiscographyArtist(title string) string {
if match := discographyTitlePattern.FindStringSubmatch(title); len(match) >= 2 {
return strings.TrimSpace(match[1])
}
if match := collectionTitlePattern.FindStringSubmatch(title); len(match) >= 2 {
return strings.TrimSpace(match[1])
}
artist, _ := p.ExtractArtistAlbum(title)
return artist
}
@@ -1,152 +0,0 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestDiscographyParser(t *testing.T) {
p := NewDiscographyParser()
tests := []struct {
name string
title string
wantArtist string
wantYear int
wantYearEnd int
wantReleaseCount int
wantType release.Type
wantFormat release.AudioFormat
wantParseOK bool
}{
{
name: "Russian discography with ALAC",
title: "(Metalcore, progressive metalcore, alternative metal, mathcore) [CD`12] Architects - Дискография / Discography - 2006-2025, ALAC (tracks+.cue), lossless",
wantArtist: "Architects",
wantYear: 2006,
wantYearEnd: 2025,
wantType: release.TypeDiscography,
wantFormat: release.FormatALAC,
wantParseOK: true,
},
{
name: "discography with CD count",
title: "(Rock / Hard Rock / Power-Pop) [CD] Cheap Trick - Дискография - 1977-2021 (78 CD), FLAC (image+.cue), lossless",
wantArtist: "Cheap Trick",
wantYear: 1977,
wantYearEnd: 2021,
wantReleaseCount: 78,
wantType: release.TypeDiscography,
wantParseOK: true,
},
{
name: "mixed CD and WEB",
title: "Pompeya - Дискография | Discography (3 CD, 6 WEB) - 2011-2015, FLAC (tracks+.cue, tracks/web), lossless",
wantArtist: "Pompeya",
wantYear: 2011,
wantYearEnd: 2015,
wantType: release.TypeDiscography,
wantParseOK: true,
},
{
name: "large discography with releases count",
title: "(Rock) Александр Башлачёв - Дискография (1994-2025) (35 выпусков, 47 CD / 2 Digital Release), FLAC (image+.cue), lossless",
wantArtist: "Александр Башлачёв",
wantYear: 1994,
wantYearEnd: 2025,
wantType: release.TypeDiscography,
wantParseOK: true,
},
{
name: "very large discography",
title: "(Rock) Аквариум и Борис Гребенщиков (БГ) - Дискография - 1973–2023 (222 издания, 245 CD), FLAC (image+.cue), lossless",
wantArtist: "Аквариум и Борис Гребенщиков (БГ)",
wantYear: 1973,
wantYearEnd: 2023,
wantType: release.TypeDiscography,
wantParseOK: true,
},
{
name: "metal discography",
title: "(Heavy Metal) [CD] Saxon - Дискография (58 CD) - 1979-2024, FLAC (image+.cue), lossless",
wantArtist: "Saxon",
wantYear: 1979,
wantYearEnd: 2024,
wantReleaseCount: 58,
wantType: release.TypeDiscography,
wantParseOK: true,
},
{
name: "detailed Queen discography",
title: "(Progressive Hard Rock Fusion) [CD] Queen The Discography / Дискография (15 Studio, 11 Live, 13 Compilation, 63 Singles, 2 Collaboration, 7 Box Set, 243 issues, 336 CD) - 1973-2015, FLAC (image+.cue), lossless",
wantArtist: "Queen",
wantYear: 1973,
wantYearEnd: 2015,
wantType: release.TypeDiscography,
wantParseOK: true,
},
{
name: "English discography",
title: "(Rock, Pop) [CD] U2 - Discography (1980-2017), FLAC (tracks+.cue), lossless",
wantArtist: "U2",
wantYear: 1980,
wantYearEnd: 2017,
wantType: release.TypeDiscography,
wantParseOK: true,
},
{
name: "death metal discography",
title: "(Technical Brutal Death Metal) [CD] Nile - Discography (1994 - 2024) 13 CD, FLAC (image+.cue), lossless",
wantArtist: "Nile",
wantYear: 1994,
wantYearEnd: 2024,
wantReleaseCount: 13,
wantType: release.TypeDiscography,
wantParseOK: true,
},
{
name: "collection keyword",
title: "(Pop) Madonna - Коллекция / Collection - 65 релизов (2 Albums, 22 Singles, 13 Megamixes, 8 Live, 17 Collections, 3 Bonus) (1982-2012), MP3, 128-320, VBR kbps",
wantArtist: "Madonna",
wantYear: 1982,
wantYearEnd: 2012,
wantType: release.TypeCollection,
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.wantYearEnd != 0 && r.YearEnd != tt.wantYearEnd {
t.Errorf("YearEnd = %d, want %d", r.YearEnd, tt.wantYearEnd)
}
if tt.wantReleaseCount != 0 && r.ReleaseCount != tt.wantReleaseCount {
t.Errorf("ReleaseCount = %d, want %d", r.ReleaseCount, tt.wantReleaseCount)
}
if tt.wantType != release.TypeUnknown && r.Type != tt.wantType {
t.Errorf("Type = %v, want %v", r.Type, tt.wantType)
}
if tt.wantFormat != release.FormatUnknown && r.Format != tt.wantFormat {
t.Errorf("Format = %v, want %v", r.Format, tt.wantFormat)
}
})
}
}
@@ -1,38 +0,0 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type ElectronicParser struct {
BaseParser
}
func NewElectronicParser() *ElectronicParser {
return &ElectronicParser{}
}
func (p *ElectronicParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
if len(r.Genres) == 0 {
r.Genres = []string{"Electronic"}
}
r.Type = p.DetectType(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Bitrate = p.ExtractBitrate(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.ReleaseCount = p.ExtractReleaseCount(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
@@ -1,142 +0,0 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestElectronicParser(t *testing.T) {
p := NewElectronicParser()
tests := []struct {
name string
title string
wantArtist string
wantYear int
wantFormat release.AudioFormat
wantType release.Type
wantBitrate string
wantParseOK bool
}{
{
name: "Progressive house VA",
title: "(Progressive House) [WEB] VA - Augmented 018 / FGA (Mango Alley [ALLEYAUG018]) - 2026, FLAC (tracks), lossless",
wantArtist: "VA",
wantYear: 2026,
wantFormat: release.FormatFLAC,
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "Electro synth-pop tech house",
title: "(Electro, Synth-Pop, Tech House) [CD] VA - Kitsune Maison Compilation 6 - 2008, FLAC (tracks+.cue), lossless",
wantArtist: "VA",
wantYear: 2008,
wantFormat: release.FormatFLAC,
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "Deep house multi-CD",
title: "(Deep House, House, Tech House, Minimal Techno) [2 CD] VA - Freza & Nitrous - Air Trip - 2012, FLAC (tracks+.cue), lossless",
wantArtist: "VA",
wantYear: 2012,
wantFormat: release.FormatFLAC,
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "House fresh majestic",
title: "(House) [2 CD] VA - Fresh & Majestic - defile spb [2005] - 2005, FLAC (image+.cue), lossless",
wantArtist: "VA",
wantYear: 2005,
wantFormat: release.FormatFLAC,
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "Trance breaks house",
title: "(Trance, Breaks, House) [2 CD] VA - Fantazia - Aural Pleasure - 2000, FLAC (tracks+.cue), lossless",
wantArtist: "VA",
wantYear: 2000,
wantFormat: release.FormatFLAC,
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "House klubnyi",
title: "(House) [CD] VA - E Burg KLUBНЫЙ by Smart #5 - 2006, FLAC (image+.cue), lossless",
wantArtist: "VA",
wantYear: 2006,
wantFormat: release.FormatFLAC,
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "House progressive artist release",
title: "(House, Progressive house) [WEB] Thomas Newson - Summer Vibes (Armada Music[ARMAS1092A]) - 2015, FLAC (tracks), lossless",
wantArtist: "Thomas Newson",
wantYear: 2015,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Progressive trance dream",
title: "(Progressive Trance, Euro House, Trance, Dream) [CD] VA - Dream Power 7 - 1997, FLAC (image+.cue), lossless",
wantArtist: "VA",
wantYear: 1997,
wantFormat: release.FormatFLAC,
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "Progressive house hard house",
title: "(Progressive House, Hard House) [CD] VA - Future Russian House - 2001, FLAC (tracks+.cue), lossless",
wantArtist: "VA",
wantYear: 2001,
wantFormat: release.FormatFLAC,
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "Progressive house trance 2002",
title: "(Progressive House, Trance) [CD] VA - Future Russian House 2002 - 2002, FLAC (tracks+.cue), lossless",
wantArtist: "VA",
wantYear: 2002,
wantFormat: release.FormatFLAC,
wantType: release.TypeCompilation,
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)
}
})
}
}
-38
View File
@@ -1,38 +0,0 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type FolkParser struct {
BaseParser
}
func NewFolkParser() *FolkParser {
return &FolkParser{}
}
func (p *FolkParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
if len(r.Genres) == 0 {
r.Genres = []string{"Folk"}
}
r.Type = p.DetectType(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Bitrate = p.ExtractBitrate(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.ReleaseCount = p.ExtractReleaseCount(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
@@ -1,138 +0,0 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestFolkParser(t *testing.T) {
p := NewFolkParser()
tests := []struct {
name string
title string
wantArtist string
wantYear int
wantFormat release.AudioFormat
wantType release.Type
wantBitrate string
wantParseOK bool
}{
{
name: "Folk italo VA compilation",
title: "(Folk, Italo Folk, Italian Folk) [WEB] VA - La musica della mafia - Best of (Uomini d'onore - Men of Honor) - 2011, FLAC (tracks), lossless",
wantArtist: "VA",
wantYear: 2011,
wantFormat: release.FormatFLAC,
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "Russian folk VA",
title: "[RUS](Folk) [CD] VA - Пинежская песня. Том I-III, V, VI - 2011-2016, FLAC (image+.cue), lossless",
wantArtist: "VA",
wantYear: 2011,
wantFormat: release.FormatFLAC,
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "Folk world country best of",
title: "(Folk, World, & Country) [CD] Suzy Bogguss - Greatest Hits - 1994, FLAC (tracks+.cue), lossless",
wantArtist: "Suzy Bogguss",
wantYear: 1994,
wantFormat: release.FormatFLAC,
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "Folk christmas multi-CD",
title: "(Folk) [CD] The Allisons - Sing Christmas (2 CD) - 1995, FLAC (image+.cue), lossless",
wantArtist: "The Allisons",
wantYear: 1995,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Folk world country album",
title: "(Folk, World, & Country) [CD] Maura O'Connell - Helpless Heart - 1989, FLAC (tracks+.cue), lossless",
wantArtist: "Maura O'Connell",
wantYear: 1989,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Country blues folk collection",
title: "(Country, Blues, Folk, Americana, Guitar, Vocal) [WEB] Jack Barksdale - Collection of 2 Albums, 2 EP and 8 singles / Коллекция из 12 релизов - 2017-2023, FLAC (tracks), lossless",
wantArtist: "Jack Barksdale",
wantYear: 2017,
wantFormat: release.FormatFLAC,
wantType: release.TypeCollection,
wantParseOK: true,
},
{
name: "Russian dark folk neofolk collection",
title: "[RUS] (Folk, Dark Folk, Neofolk) [CD] Помни Имя Своё - Коллекция (4 релиза, 6 CD) - 2016-2023, FLAC (image+.cue), lossless",
wantArtist: "Помни Имя Своё",
wantYear: 2016,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Folk country rock collection",
title: "(Folk, Country rock) [CD] Emmylou Harris - коллекция 1975-2008 (23 альбома), FLAC (image+.cue, tracks+.cue), lossless",
wantArtist: "Emmylou Harris",
wantYear: 1975,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Ukrainian folk electronic collection",
title: "[UKR] (Folk, Electronic, Dance, Pop) [WEB] Go A - Collection - 2016-2026, FLAC (tracks), 17 CD (1 Album, 16 Singles), lossless",
wantArtist: "Go A",
wantYear: 2016,
wantFormat: release.FormatFLAC,
wantType: release.TypeCollection,
wantParseOK: true,
},
{
name: "Alternative country folk rock",
title: "(Alternative Country, Folk Rock) [CD] Kathleen Edwards - Asking for Flowers - 2008, FLAC (tracks+.cue), lossless",
wantArtist: "Kathleen Edwards",
wantYear: 2008,
wantFormat: release.FormatFLAC,
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)
}
})
}
}
@@ -1,35 +0,0 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type GeneralParser struct {
BaseParser
}
func NewGeneralParser() *GeneralParser {
return &GeneralParser{}
}
func (p *GeneralParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
r.Type = p.DetectType(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Bitrate = p.ExtractBitrate(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.ReleaseCount = p.ExtractReleaseCount(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
@@ -1,166 +0,0 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestGeneralParser(t *testing.T) {
p := NewGeneralParser()
tests := []struct {
name string
title string
wantArtist string
wantAlbum string
wantYear int
wantFormat release.AudioFormat
wantType release.Type
wantGenres []string
wantSource release.Source
wantRipType string
wantBitrate string
wantParseOK bool
}{
{
name: "standard CD rip with genre",
title: "(Rock) [CD] Thin Lizzy - Acoustic Sessions - 2024 (Decca Records EU 2025), FLAC (image+.cue), lossless",
wantArtist: "Thin Lizzy",
wantAlbum: "Acoustic Sessions",
wantYear: 2024,
wantFormat: release.FormatFLAC,
wantType: release.TypeAlbum,
wantGenres: []string{"Rock"},
wantSource: release.SourceCD,
wantRipType: "image+.cue",
wantBitrate: "lossless",
wantParseOK: true,
},
{
name: "multi-genre CD rip",
title: "(Hard Rock, Glam Rock, Progressive Rock, Art Rock, Heavy Metal) [CD] Queen Queen I (2 CD) 2024 , FLAC (image+.cue), lossless",
wantArtist: "Queen",
wantYear: 2024,
wantFormat: release.FormatFLAC,
wantType: release.TypeAlbum,
wantGenres: []string{"Hard Rock", "Glam Rock", "Progressive Rock", "Art Rock", "Heavy Metal"},
wantSource: release.SourceCD,
wantParseOK: true,
},
{
name: "WEB release with tracks",
title: "(Progressive Rock) [WEB] Opeth - In Cauda Venenum (Extended Edition) - 2019/2022, FLAC (tracks), lossless",
wantArtist: "Opeth",
wantYear: 2019,
wantFormat: release.FormatFLAC,
wantSource: release.SourceWEB,
wantRipType: "tracks",
wantParseOK: true,
},
{
name: "Japan release",
title: "(Pop-Rock Soft-Rock) [CD] Sting - The Soul Cages (Expanded Edition) - 2025 [Japan], FLAC (image+.cue), lossless",
wantArtist: "Sting",
wantYear: 2025,
wantFormat: release.FormatFLAC,
wantSource: release.SourceCD,
wantParseOK: true,
},
{
name: "live album",
title: "(Rock) [CD] Bryan Adams - Live at the Royal Albert Hall - 2024, FLAC (image+.cue), lossless",
wantArtist: "Bryan Adams",
wantType: release.TypeLive,
wantYear: 2024,
wantParseOK: true,
},
{
name: "soundtrack",
title: "(Pop) [CD] Celine Dion - I AM - Celine Dion (Original Motion Picture Soundtrack) - 2024 [Japan], FLAC (image+.cue), lossless",
wantArtist: "Celine Dion",
wantType: release.TypeSoundtrack,
wantYear: 2024,
wantParseOK: true,
},
{
name: "deluxe box set",
title: "(Rock) [CD] Bryan Adams - Roll With The Punches (Deluxe Box Set) - 2025, FLAC (image+.cue), lossless",
wantArtist: "Bryan Adams",
wantYear: 2025,
wantParseOK: true,
},
{
name: "CDS single",
title: "(Heavy Metal) [CDS] Bruce Dickinson - Resurrection Men - 2024, FLAC (image+.cue), lossless",
wantArtist: "Bruce Dickinson",
wantYear: 2024,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "tracks+cue format",
title: "(Classic Rock) [CD] The Who - Who Are You (Super Deluxe Edition) - 2025, FLAC (tracks+cue), lossless",
wantArtist: "The Who",
wantYear: 2025,
wantRipType: "tracks+cue",
wantParseOK: true,
},
{
name: "WEB with special artist name",
title: "(Chamber Pop) [WEB] Florence + the Machine - Ceremonials (Digital Deluxe Edition) - 2011, FLAC (tracks), lossless",
wantArtist: "Florence + the Machine",
wantYear: 2011,
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.wantAlbum != "" && r.Album != tt.wantAlbum {
t.Errorf("Album = %q, want %q", r.Album, tt.wantAlbum)
}
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.wantSource != release.SourceUnknown && r.Source != tt.wantSource {
t.Errorf("Source = %v, want %v", r.Source, tt.wantSource)
}
if tt.wantRipType != "" && r.RipType != tt.wantRipType {
t.Errorf("RipType = %q, want %q", r.RipType, tt.wantRipType)
}
if tt.wantBitrate != "" && r.Bitrate != tt.wantBitrate {
t.Errorf("Bitrate = %q, want %q", r.Bitrate, tt.wantBitrate)
}
if len(tt.wantGenres) > 0 {
if len(r.Genres) != len(tt.wantGenres) {
t.Errorf("Genres count = %d, want %d", len(r.Genres), len(tt.wantGenres))
}
}
})
}
}
@@ -1,38 +0,0 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type HipHopParser struct {
BaseParser
}
func NewHipHopParser() *HipHopParser {
return &HipHopParser{}
}
func (p *HipHopParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
if len(r.Genres) == 0 {
r.Genres = []string{"Hip-Hop"}
}
r.Type = p.DetectType(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Bitrate = p.ExtractBitrate(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.ReleaseCount = p.ExtractReleaseCount(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
@@ -1,138 +0,0 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestHipHopParser(t *testing.T) {
p := NewHipHopParser()
tests := []struct {
name string
title string
wantArtist string
wantYear int
wantFormat release.AudioFormat
wantType release.Type
wantBitrate string
wantParseOK bool
}{
{
name: "Rap underground discography",
title: "(Rap | Underground Hip-Hop) Небро | Честер / Chester (Небро) - Дискография, (при уч. НЕ.KURILI) , (39 Релизов), - 2008-2026, MP3, 256-320 kbps",
wantArtist: "Небро | Честер / Chester (Небро)",
wantYear: 2008,
wantFormat: release.FormatMP3,
wantParseOK: true,
},
{
name: "Rap album MP3",
title: "(Rap) Честер Небро & НЕ.KURILI - Короткометражка - 2026, MP3, 320 kbps",
wantArtist: "Честер Небро & НЕ.KURILI",
wantYear: 2026,
wantFormat: release.FormatMP3,
wantBitrate: "320 kbps",
wantParseOK: true,
},
{
name: "Hip-hop Kanye West",
title: "(Hip-Hop, Rap, Electronic) [CD][LDR] Ye (Kanye West) - Bully - 2026, FLAC (image+.cue), lossless",
wantArtist: "Ye (Kanye West)",
wantYear: 2026,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Rap Russian album",
title: "(Rap) MC Кальмар & Алкоголь После Спорта - Антибиотик - 2025, MP3, 320 kbps",
wantArtist: "MC Кальмар & Алкоголь После Спорта",
wantYear: 2025,
wantFormat: release.FormatMP3,
wantBitrate: "320 kbps",
wantParseOK: true,
},
{
name: "Rap hip-hop discography complex",
title: "(Rap/Hip-Hop) Killer Chem [Razym Garo] (при участии: AnderМаг, 03406(И.С), 2DT, Бандарад Вирши) - Официальная ДискоТрекография (3 альбома, 8 синглов, Трекография) - 2020-2026, MP3, 192-320 Kbps",
wantArtist: "Killer Chem [Razym Garo] (при участии: AnderМаг, 03406(И.С), 2DT, Бандарад Вирши)",
wantYear: 2020,
wantFormat: release.FormatMP3,
wantParseOK: true,
},
{
name: "Rap kapa album",
title: "(Rap) Капа - КАПАкалипсис - 2026, MP3, 320 kbps",
wantArtist: "Капа",
wantYear: 2026,
wantFormat: release.FormatMP3,
wantBitrate: "320 kbps",
wantParseOK: true,
},
{
name: "Rap grot album",
title: "(Rap) Грот - между катастроф - 2026, MP3, 320 kbps",
wantArtist: "Грот",
wantYear: 2026,
wantFormat: release.FormatMP3,
wantBitrate: "320 kbps",
wantParseOK: true,
},
{
name: "Rap hip-hop collection FLAC",
title: "(Rap, Hip-Hop) [CD] [WEB] † Эйсик (Asick) - Коллекция (13 релизов) - 2005-2023, FLAC (tracks+.cue), lossless",
wantArtist: "† Эйсик (Asick)",
wantYear: 2005,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Hip-hop rap WEB",
title: "(Hip-Hop/Rap) [WEB] Aarne - AA LANGUAGE (Uncensored) - 2022, FLAC (tracks), lossless",
wantArtist: "Aarne",
wantYear: 2022,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Rap fast alberto",
title: "(Rap) Фаст Альберто - Ars Longa Vita Brevis - 2026, MP3, 320 kbps",
wantArtist: "Фаст Альберто",
wantYear: 2026,
wantFormat: release.FormatMP3,
wantBitrate: "320 kbps",
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)
}
})
}
}
@@ -1,45 +0,0 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type HiResParser struct {
BaseParser
}
func NewHiResParser() *HiResParser {
return &HiResParser{}
}
func (p *HiResParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
r.Type = p.DetectType(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
if r.Format == release.FormatUnknown {
r.Format = release.FormatFLAC
}
r.Bitrate = "lossless"
if r.BitDepth == 0 {
if dsdMatch := dsdPattern.FindStringSubmatch(title); len(dsdMatch) >= 3 {
r.BitDepth = 1
r.Tags = append(r.Tags, dsdMatch[1]+dsdMatch[2])
}
}
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
@@ -1,133 +0,0 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestHiResParser(t *testing.T) {
p := NewHiResParser()
tests := []struct {
name string
title string
wantArtist string
wantYear int
wantBitDepth int
wantSampleRate int
wantSource release.Source
wantParseOK bool
}{
{
name: "TR24 OF official release",
title: "[TR24][OF] Matteo Mancuso - Route 96 - 2026 (Progressive Rock, Jazz Fusion, Instrumental)",
wantArtist: "Matteo Mancuso",
wantYear: 2026,
wantParseOK: true,
},
{
name: "TR24 OF LDR tag",
title: "[TR24][OF][LDR] Sepultura - The Cloud Of Unknowing - 2026 (Groove Thrash Metal)",
wantArtist: "Sepultura",
wantYear: 2026,
wantParseOK: true,
},
{
name: "24bit 48kHz in title",
title: "[TR24][OF] U2 - Days Of Ash [EP] [24bit-48kHz] - 2026 (Pop Rock, Soft Rock)",
wantArtist: "U2",
wantYear: 2026,
wantBitDepth: 24,
wantSampleRate: 48000,
wantParseOK: true,
},
{
name: "LP 24/192",
title: "(Blues, R&B) [LP] [24/192] Etta James - At Last! - 1960/2026, FLAC (tracks)",
wantArtist: "Etta James",
wantYear: 1960,
wantBitDepth: 24,
wantSampleRate: 192000,
wantSource: release.SourceVinyl,
wantParseOK: true,
},
{
name: "DSD128",
title: "(Progressive rock) [LP] [1/5,64 MHz] The Neal Morse Band L. I. F. T. - 2026, DSD 128 (tracks)",
wantArtist: "The Neal Morse Band",
wantYear: 2026,
wantSource: release.SourceVinyl,
wantParseOK: true,
},
{
name: "DSD256 with label",
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,
},
{
name: "24/96 modal jazz",
title: "(Modal, Jazz) [LP] [24/96] John Coltrane - The Tiberi Tapes: A Preview Of The Mythic Recordings (2026 Record Store Day) - 2026, FLAC (tracks)",
wantArtist: "John Coltrane",
wantYear: 2026,
wantBitDepth: 24,
wantSampleRate: 96000,
wantParseOK: true,
},
{
name: "2xLP compilation",
title: "(Electronic, Funk / Soul, Disco, House) [2xLP] [24/192] Various - The Many Faces Of Daft Punk - 2020( Compilation), FLAC (tracks)",
wantArtist: "Various",
wantYear: 2020,
wantBitDepth: 24,
wantSampleRate: 192000,
wantParseOK: true,
},
{
name: "SACD-R",
title: "[SACD-R][OF] Wynton Marsalis - The London Concert - 2000 (Classical)",
wantArtist: "Wynton Marsalis",
wantYear: 2000,
wantParseOK: true,
},
{
name: "SACD-R DSD",
title: "[SACD-R][DSD][OF]Scott Hamilton, Paolo Birro - Pure Imagination - 2019 (Jazz)",
wantArtist: "Scott Hamilton, Paolo Birro",
wantYear: 2019,
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.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)
}
if tt.wantSource != release.SourceUnknown && r.Source != tt.wantSource {
t.Errorf("Source = %v, want %v", r.Source, tt.wantSource)
}
})
}
}
-38
View File
@@ -1,38 +0,0 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type JazzParser struct {
BaseParser
}
func NewJazzParser() *JazzParser {
return &JazzParser{}
}
func (p *JazzParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
if len(r.Genres) == 0 {
r.Genres = []string{"Jazz"}
}
r.Type = p.DetectType(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Bitrate = p.ExtractBitrate(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.ReleaseCount = p.ExtractReleaseCount(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
@@ -1,149 +0,0 @@
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)
}
})
}
}
@@ -1,33 +0,0 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type LabelPacksParser struct {
BaseParser
}
func NewLabelPacksParser() *LabelPacksParser {
return &LabelPacksParser{}
}
func (p *LabelPacksParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
r.Type = release.TypeCollection
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Bitrate = p.ExtractBitrate(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.ReleaseCount = p.ExtractReleaseCount(title)
r.Label = p.ExtractLabel(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
if r.Label == "" {
p.AddError(r, "failed to extract label name")
}
return r
}
@@ -1,146 +0,0 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestLabelPacksParser(t *testing.T) {
p := NewLabelPacksParser()
tests := []struct {
name string
title string
wantLabel string
wantYear int
wantYearEnd int
wantReleaseCount int
wantFormat release.AudioFormat
wantParseOK bool
}{
{
name: "standard label pack",
title: "(Drum & Bass) [WEB] Label: Metalheadz (370 релизов), 1994-2025, FLAC (tracks), lossless",
wantLabel: "Metalheadz",
wantYear: 1994,
wantYearEnd: 2025,
wantReleaseCount: 370,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
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",
wantYear: 2009,
wantYearEnd: 2023,
wantReleaseCount: 401,
wantParseOK: true,
},
{
name: "small label",
title: "(Trance) [WEB, CD] Label: Solaris Recordings (7 Releases) - 2005-2014, FLAC (tracks, tracks+.cue), lossless",
wantLabel: "Solaris Recordings",
wantYear: 2005,
wantYearEnd: 2014,
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 (65 Releases) - 2010-2021, FLAC (tracks) (tracks+.cue), lossless",
wantLabel: "Auxiliary",
wantYear: 2010,
wantYearEnd: 2021,
wantReleaseCount: 65,
wantParseOK: true,
},
{
name: "Russian release count",
title: "(Techno, Minimal, Deep Tech, Melodic House & Techno) [WEB] Label: FCKNG SERIOUS (121 релиз), 2015-2025, FLAC (tracks, image), lossless",
wantLabel: "FCKNG SERIOUS",
wantYear: 2015,
wantYearEnd: 2025,
wantReleaseCount: 121,
wantParseOK: true,
},
{
name: "progressive house label",
title: "(Progressive House, Trance, Techno) [WEB] Label: Bedrock Records (519 релизов), 1999-2025, (FLAC) lossless (tracks, image)",
wantLabel: "Bedrock Records",
wantYear: 1999,
wantYearEnd: 2025,
wantReleaseCount: 519,
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",
title: "(Trance, Breaks, House) [WEB] Label: Digital Emotions (47 Releases) (Incl. Fonarev pres. F13, Poshout, Second Sine & etc.) - 2010-2025, FLAC (tracks), lossless",
wantLabel: "Digital Emotions",
wantYear: 2010,
wantYearEnd: 2025,
wantParseOK: true,
},
{
name: "bondage music",
title: "(Deep House, Minimal) [WEB] Label: Bondage Music (173 релиза), 2006-2025, (FLAC) lossless (tracks, image)",
wantLabel: "Bondage Music",
wantYear: 2006,
wantYearEnd: 2025,
wantReleaseCount: 173,
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.wantLabel != "" && r.Label != tt.wantLabel {
t.Errorf("Label = %q, want %q", r.Label, tt.wantLabel)
}
if tt.wantYear != 0 && r.Year != tt.wantYear {
t.Errorf("Year = %d, want %d", r.Year, tt.wantYear)
}
if tt.wantYearEnd != 0 && r.YearEnd != tt.wantYearEnd {
t.Errorf("YearEnd = %d, want %d", r.YearEnd, tt.wantYearEnd)
}
if tt.wantReleaseCount != 0 && r.ReleaseCount != tt.wantReleaseCount {
t.Errorf("ReleaseCount = %d, want %d", r.ReleaseCount, tt.wantReleaseCount)
}
if tt.wantFormat != release.FormatUnknown && r.Format != tt.wantFormat {
t.Errorf("Format = %v, want %v", r.Format, tt.wantFormat)
}
if r.Type != release.TypeCollection {
t.Errorf("Type = %v, want Collection", r.Type)
}
})
}
}
@@ -1,37 +0,0 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type LosslessParser struct {
BaseParser
}
func NewLosslessParser() *LosslessParser {
return &LosslessParser{}
}
func (p *LosslessParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
r.Type = p.DetectType(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
if r.Format == release.FormatUnknown {
r.Format = release.FormatFLAC
}
r.Bitrate = "lossless"
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
@@ -1,143 +0,0 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestLosslessParser(t *testing.T) {
p := NewLosslessParser()
tests := []struct {
name string
title string
wantArtist string
wantYear int
wantFormat release.AudioFormat
wantSource release.Source
wantRipType string
wantParseOK bool
}{
{
name: "standard CD FLAC image",
title: "(Rock) [CD] Thin Lizzy - Acoustic Sessions - 2024 (Decca Records EU 2025), FLAC (image+.cue), lossless",
wantArtist: "Thin Lizzy",
wantYear: 2024,
wantFormat: release.FormatFLAC,
wantSource: release.SourceCD,
wantRipType: "image+.cue",
wantParseOK: true,
},
{
name: "WEB release tracks",
title: "(Progressive Rock) [WEB] Opeth - In Cauda Venenum (Extended Edition) - 2019/2022, FLAC (tracks), lossless",
wantArtist: "Opeth",
wantYear: 2019,
wantFormat: release.FormatFLAC,
wantSource: release.SourceWEB,
wantRipType: "tracks",
wantParseOK: true,
},
{
name: "APE format",
title: "(Jazz) [CD] Miles Davis - Kind of Blue - 1959, APE (image+.cue), lossless",
wantArtist: "Miles Davis",
wantYear: 1959,
wantFormat: release.FormatAPE,
wantSource: release.SourceCD,
wantParseOK: true,
},
{
name: "tracks+cue format",
title: "(Classic Rock) [CD] The Who - Who Are You (Super Deluxe Edition) - 2025, FLAC (tracks+cue), lossless",
wantArtist: "The Who",
wantYear: 2025,
wantFormat: release.FormatFLAC,
wantRipType: "tracks+cue",
wantParseOK: true,
},
{
name: "multi-disc set",
title: "(Hard Rock, Glam Rock, Progressive Rock, Art Rock, Heavy Metal) [CD] Queen Queen I (2 CD) 2024 , FLAC (image+.cue), lossless",
wantArtist: "Queen",
wantYear: 2024,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Japan release",
title: "(Pop-Rock Soft-Rock) [CD] Sting - The Soul Cages (Expanded Edition) - 2025 [Japan], FLAC (image+.cue), lossless",
wantArtist: "Sting",
wantYear: 2025,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "WavPack format",
title: "(Progressive Rock) [CD] Yes - Close to the Edge - 1972, WV (image+.cue), lossless",
wantArtist: "Yes",
wantYear: 1972,
wantFormat: release.FormatWavPack,
wantParseOK: true,
},
{
name: "default to FLAC when format not specified",
title: "(Rock) [CD] Pink Floyd - The Wall - 1979 (image+.cue), lossless",
wantArtist: "Pink Floyd",
wantYear: 1979,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "heavy metal WEB",
title: "(Heavy Metal) [WEB] Heaven & Hell - Breaking Out Of Heaven - 2026, FLAC (tracks), lossless",
wantArtist: "Heaven & Hell",
wantYear: 2026,
wantSource: release.SourceWEB,
wantParseOK: true,
},
{
name: "melodic rock WEB",
title: "(Melodic Rock, Progressive Rock) [WEB] James LaBrie - Beautiful Shade Of Grey - 2022, FLAC (tracks), lossless",
wantArtist: "James LaBrie",
wantYear: 2022,
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.wantRipType != "" && r.RipType != tt.wantRipType {
t.Errorf("RipType = %q, want %q", r.RipType, tt.wantRipType)
}
if r.Bitrate != "lossless" {
t.Errorf("Bitrate = %q, want lossless", r.Bitrate)
}
})
}
}
@@ -1,36 +0,0 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type LossyParser struct {
BaseParser
}
func NewLossyParser() *LossyParser {
return &LossyParser{}
}
func (p *LossyParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
r.Type = p.DetectType(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Bitrate = p.ExtractBitrate(title)
r.Source = p.ExtractSource(title)
r.Tags = p.ExtractSpecialTags(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
if r.Format == release.FormatUnknown {
r.Format = release.FormatMP3
}
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
@@ -1,136 +0,0 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestLossyParser(t *testing.T) {
p := NewLossyParser()
tests := []struct {
name string
title string
wantArtist string
wantYear int
wantFormat release.AudioFormat
wantBitrate string
wantType release.Type
wantParseOK bool
}{
{
name: "VBR V0",
title: "(Pop) VA - Pop Classics Top 100 - 2012, MP3, VBR V0",
wantArtist: "VA",
wantYear: 2012,
wantFormat: release.FormatMP3,
wantBitrate: "V0",
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "VBR V0 kbps suffix",
title: "(Pop/Rock) VA - 101 Ultimate 80's (5 CD) - 2011, MP3 (tracks), VBR V0 kbps",
wantArtist: "VA",
wantYear: 2011,
wantFormat: release.FormatMP3,
wantBitrate: "V0",
wantParseOK: true,
},
{
name: "VBR V1",
title: "(Rock) VA - Greatest Ever! Rock The Definitive Collection (3 CD) - 2006, MP3 (tracks), VBR V1 kbps",
wantArtist: "VA",
wantYear: 2006,
wantBitrate: "V1",
wantParseOK: true,
},
{
name: "VBR V2",
title: "(Classic Rock) VA - Twist & Shout - 2005, MP3, VBR V2",
wantArtist: "VA",
wantYear: 2005,
wantBitrate: "V2",
wantParseOK: true,
},
{
name: "VBR range",
title: "(Pop, Rock) VA - The Essential 1980s - 2010, MP3 (tracks), VBR 192-320 kbps",
wantArtist: "VA",
wantYear: 2010,
wantBitrate: "VBR 192-320 kbps",
wantParseOK: true,
},
{
name: "CBR 320",
title: "(Pop) VA - Bravo Hits, Vol. 128 [2 CD] - 2025, MP3, 320 kbps",
wantArtist: "VA",
wantYear: 2025,
wantBitrate: "320 kbps",
wantParseOK: true,
},
{
name: "CBR 256",
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,
},
{
name: "year range in title",
title: "(Pop) VA - Bravo Hits vol. 31-59 - 2000-2007, MP3, VBR 192-320 kbps",
wantArtist: "VA",
wantYear: 2000,
wantParseOK: true,
},
{
name: "discography in lossy",
title: "(Alternative Metal / Post-Grunge) Breaking Benjamin - Discography: 23 Releases, 2001-2024, MP3, VBR V0/320 kbps",
wantArtist: "Breaking Benjamin",
wantYear: 2001,
wantType: release.TypeDiscography,
wantParseOK: true,
},
{
name: "bootleg release",
title: "(Eurodance) VA - Beat Mix Eurodance Vol 1-3 (Bootlegs) - 2009-2011, MP3 (image), VBR V2 / V0",
wantArtist: "VA",
wantYear: 2009,
wantType: release.TypeBootleg,
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.wantBitrate != "" && r.Bitrate != tt.wantBitrate {
t.Errorf("Bitrate = %q, want %q", r.Bitrate, tt.wantBitrate)
}
if tt.wantType != release.TypeUnknown && r.Type != tt.wantType {
t.Errorf("Type = %v, want %v", r.Type, tt.wantType)
}
})
}
}
@@ -1,38 +0,0 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type MetalParser struct {
BaseParser
}
func NewMetalParser() *MetalParser {
return &MetalParser{}
}
func (p *MetalParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
if len(r.Genres) == 0 {
r.Genres = []string{"Metal"}
}
r.Type = p.DetectType(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Bitrate = p.ExtractBitrate(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.ReleaseCount = p.ExtractReleaseCount(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
@@ -1,135 +0,0 @@
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)
}
})
}
}
@@ -1,37 +0,0 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type MiscMusicParser struct {
BaseParser
}
func NewMiscMusicParser() *MiscMusicParser {
return &MiscMusicParser{}
}
func (p *MiscMusicParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
if len(r.Genres) == 0 {
}
r.Type = p.DetectType(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Bitrate = p.ExtractBitrate(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.ReleaseCount = p.ExtractReleaseCount(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
@@ -1,144 +0,0 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestMiscMusicParser(t *testing.T) {
p := NewMiscMusicParser()
tests := []struct {
name string
title string
wantArtist string
wantYear int
wantFormat release.AudioFormat
wantType release.Type
wantBitrate string
wantParseOK bool
}{
{
name: "Spiritual chants FLAC",
title: "(Духовные песнопения) [CD] Хор Сретенского Монастыря - Рождественские песнопения - 2005, FLAC (image+.cue), lossless",
wantArtist: "Хор Сретенского Монастыря",
wantYear: 2005,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Children songs VA",
title: "(Детские песни) [CD] VA - Любимые песни из мультфильмов - 2010, FLAC (tracks), lossless",
wantArtist: "VA",
wantYear: 2010,
wantFormat: release.FormatFLAC,
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "Ballroom latin VA",
title: "(Ballroom, Latin) [CD] VA - Dancelife: The Best Of Latin Music - 2008, FLAC (tracks+.cue), lossless",
wantArtist: "VA",
wantYear: 2008,
wantFormat: release.FormatFLAC,
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "Nature sounds MP3",
title: "(Nature Sounds) VA - Sounds Of Nature: Rainforest - 2005, MP3, 320 kbps",
wantArtist: "VA",
wantYear: 2005,
wantFormat: release.FormatMP3,
wantBitrate: "320 kbps",
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "Spiritual gregorian WEB",
title: "(Spiritual) [WEB] Gregorian - Masters of Chant - 2006, FLAC (tracks), lossless",
wantArtist: "Gregorian",
wantYear: 2006,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Mixed styles VA compilation",
title: "(Mixed Styles) VA - Various Artists Compilation 2024 - 2024, MP3, 320 kbps",
wantArtist: "VA",
wantYear: 2024,
wantFormat: release.FormatMP3,
wantBitrate: "320 kbps",
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "Children lullabies WEB",
title: "(Детские песни) [WEB] VA - Колыбельные для малышей - 2020, FLAC (tracks), lossless",
wantArtist: "VA",
wantYear: 2020,
wantFormat: release.FormatFLAC,
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "Orthodox chants CD",
title: "(Духовная музыка) [CD] VA - Православные песнопения - 2012, FLAC (image+.cue), lossless",
wantArtist: "VA",
wantYear: 2012,
wantFormat: release.FormatFLAC,
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "Ballroom cha cha cha",
title: "(Ballroom) [CD] VA - Strictly Ballroom Dancing - Cha Cha Cha - 2006, FLAC (tracks+.cue), lossless",
wantArtist: "VA",
wantYear: 2006,
wantFormat: release.FormatFLAC,
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "Nature sounds relaxation",
title: "(Nature Sounds, Relaxation) VA - Ocean Waves: Calm & Relax - 2018, MP3, 256 kbps",
wantArtist: "VA",
wantYear: 2018,
wantFormat: release.FormatMP3,
wantBitrate: "256 kbps",
wantType: release.TypeCompilation,
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)
}
})
}
}
@@ -1,109 +0,0 @@
package parser
import "regexp"
var (
// Genre at start: (Rock), (Electronic, Ambient), (Jazz / Blues)
genrePattern = regexp.MustCompile(`^\s*\(([^)]+)\)\s*`)
// Label pack: Label: Name or Label - Name
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(`(?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`)
// Bitrate: 320 kbps, V0, VBR 192-320 kbps, lossless
bitratePattern = regexp.MustCompile(`(?i)(?:(\d{2,3})\s*kbps|V([012])|VBR\s*(?:~?(\d+)|(\d+)-(\d+))\s*kbps|lossless)`)
// Rip type: image+.cue, tracks+.cue, tracks
ripTypePattern = regexp.MustCompile(`(?i)(image\+\.?cue|tracks?\+\.?cue|tracks?)`)
// Hi-Res bit depth / sample rate: [24/96], [24/192], [24bit-48kHz]
hiResPattern = regexp.MustCompile(`\[(\d+)(?:/|bit[/-])(\d+(?:\.\d+)?)\s*(?:kHz)?\]`)
// DSD formats: DSD64, DSD128, DST64
dsdPattern = regexp.MustCompile(`(?i)\b(DSD|DST)(64|128|256|512)\b`)
// Source tags: [CD], [WEB], [LP], [Vinyl], [SACD], [DVDA]
sourceTagPattern = regexp.MustCompile(`(?i)\[(CD|WEB|LP|Vinyl|SACD|DVDA|HDAD|MINI-LP|EP|12"|10"|7")\]`)
// Vinyl condition: [NM], [EX], [VG+], [VG], [G], [Mint], [SS]
vinylConditionPattern = regexp.MustCompile(`\[(Mint|SS|NM|EX|VG\+?|G|F/?P)\]`)
// Special tags: [AI], [WEB], [TR24], [OF], [RM], [restored], [declipped]
specialTagPattern = regexp.MustCompile(`\[(AI|WEB|TR24|OF|RM|restored|declipped)\]`)
// Discography keywords (Russian + English)
discographyPattern = regexp.MustCompile(`(?i)\b([Дд]искографи[яи]|[Dd]iscograph(?:y|ies))\b`)
// Collection keywords
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`)
// Anthology keywords
anthologyPattern = regexp.MustCompile(`(?i)\b([Аа]нтологи[яи]|[Aa]nthology)\b`)
// Best of / Greatest hits keywords
bestOfPattern = regexp.MustCompile(`(?i)\b([Ии]збранное|[Лл]учшее|[Bb]est\s*[Oo]f|[Gg]reatest\s*[Hh]its)\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`)
// Soundtrack keywords
soundtrackPattern = regexp.MustCompile(`(?i)\b(OST|[Ss]oundtrack|[Сс]аундтрек|[Ss]core|[Мм]узыка\s*(?:к|из)\s*фильм[ау])\b`)
// Remaster keywords
remasterPattern = regexp.MustCompile(`(?i)\b([Рр]емастер|[Rr]emaster(?:ed)?|[Пп]ереиздани[ея]|[Rr]e-?issue)\b`)
// EP keywords
epPattern = regexp.MustCompile(`(?i)\b(EP|[Мм]ини[-\s]?[Аа]льбом|[Ee]xtended\s*[Pp]lay)\b`)
// Single keywords
singlePattern = regexp.MustCompile(`(?i)\b([Сс]ингл|[Ss]ingle)\b`)
// Standard title format: Artist - Album - Year or (Genre) Artist - Album - Year
// Captures: artist, album, year
standardTitlePattern = regexp.MustCompile(`^(?:\([^)]+\)\s*)?(?:\[[^\]]+\]\s*)*([^-]+?)\s*[-]\s*(.+?)\s*[-]\s*((?:19|20)\d{2})`)
// Alternative: Artist - Album (Year)
altTitlePattern = regexp.MustCompile(`^(?:\([^)]+\)\s*)?(?:\[[^\]]+\]\s*)*([^-]+?)\s*[-]\s*(.+?)\s*\(((?:19|20)\d{2})\)`)
// Discography title: Artist - Дискография (15 CD) [1990-2020, ...]
discographyTitlePattern = regexp.MustCompile(`^(?:\([^)]+\)\s*)?(?:\[[^\]]+\]\s*)*([^-]+?)\s*[-]\s*(?:[Дд]искографи[яи]|[Dd]iscograph(?:y|ies))`)
// Collection title: Artist - Коллекция (50 CD) [1980-2019, ...]
collectionTitlePattern = regexp.MustCompile(`^(?:\([^)]+\)\s*)?(?:\[[^\]]+\]\s*)*([^-]+?)\s*[-]\s*(?:[Кк]оллекци[яи]|[Cc]ollection)`)
// Label pack title: (Genre) Label: Label Name (releases)
labelPackTitlePattern = regexp.MustCompile(`^(?:\([^)]+\)\s*)?(?i)Label:\s*([^(]+)`)
// 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. (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).*$`)
)
-38
View File
@@ -1,38 +0,0 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type PopParser struct {
BaseParser
}
func NewPopParser() *PopParser {
return &PopParser{}
}
func (p *PopParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
if len(r.Genres) == 0 {
r.Genres = []string{"Pop"}
}
r.Type = p.DetectType(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Bitrate = p.ExtractBitrate(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.ReleaseCount = p.ExtractReleaseCount(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
@@ -1,135 +0,0 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestPopParser(t *testing.T) {
p := NewPopParser()
tests := []struct {
name string
title string
wantArtist string
wantYear int
wantFormat release.AudioFormat
wantType release.Type
wantBitrate string
wantParseOK bool
}{
{
name: "Pop CD album",
title: "(Pop) [CD] Nessa Barrett - AFTERCARE DELUXE - 2025, FLAC (tracks+.cue), lossless",
wantArtist: "Nessa Barrett",
wantYear: 2025,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Pop alternative deluxe",
title: "(Pop) (Alternative) [CD] Melanie Martinez - Cry Baby (Deluxe Edition) - 2015, FLAC (tracks+.cue), lossless",
wantArtist: "Melanie Martinez",
wantYear: 2015,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Pop album",
title: "(Pop) [CD] Ava Max - Diamonds & Dancefloors - 2023, FLAC (tracks+.cue), lossless",
wantArtist: "Ava Max",
wantYear: 2023,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Pop VA compilation",
title: "(Pop) [WEB] VA - Музыка Победы - 2025, FLAC (tracks), lossless",
wantArtist: "VA",
wantYear: 2025,
wantFormat: release.FormatFLAC,
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "Pop limited edition",
title: "(Pop) [CD] Стрелки - Gold [Limited Edition] Maschina Records - 2026, FLAC (image+.cue), lossless",
wantArtist: "Стрелки",
wantYear: 2026,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Pop Europop",
title: "(Pop, Europop) [CD] Pupo - Insieme (2025), FLAC (image+.cue), lossless",
wantArtist: "Pupo",
wantYear: 2025,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Pop VA multi-CD",
title: "(Pop) [CD] VA - The Best Of 1980-1990 Vol. II [3 CD] - 1990, FLAC (tracks+.cue), lossless",
wantArtist: "VA",
wantYear: 1990,
wantFormat: release.FormatFLAC,
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "Pop dont click play",
title: "(Pop) [CD] Ava Max - Don't Click Play - 2025, FLAC (tracks+.cue), lossless",
wantArtist: "Ava Max",
wantYear: 2025,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Pop no good",
title: "(Pop) [CD] Ivy Levan - No Good - 2015, FLAC (tracks+.cue), lossless",
wantArtist: "Ivy Levan",
wantYear: 2015,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Pop vocal soul",
title: "(Pop, Vocal, Soul, R&B) [WEB] Hush Dusty - Love is a Battlefield Tonight - 2026, FLAC (tracks), lossless",
wantArtist: "Hush Dusty",
wantYear: 2026,
wantFormat: release.FormatFLAC,
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)
}
})
}
}
@@ -1,38 +0,0 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type RadioshowParser struct {
BaseParser
}
func NewRadioshowParser() *RadioshowParser {
return &RadioshowParser{}
}
func (p *RadioshowParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
if len(r.Genres) == 0 {
r.Genres = []string{"Electronic"}
}
r.Type = p.DetectType(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Bitrate = p.ExtractBitrate(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.ReleaseCount = p.ExtractReleaseCount(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
@@ -1,143 +0,0 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestRadioshowParser(t *testing.T) {
p := NewRadioshowParser()
tests := []struct {
name string
title string
wantArtist string
wantYear int
wantFormat release.AudioFormat
wantType release.Type
wantBitrate string
wantParseOK bool
}{
{
name: "BBC Essential Mix AAC",
title: "(House, Progressive House, Tech House, Dance, Electro, DnB) BBC Radio One - Essential Mix 2026, AAC (tracks), 320 kbps",
wantArtist: "BBC Radio One",
wantYear: 2026,
wantFormat: release.FormatAAC,
wantBitrate: "320 kbps",
wantParseOK: true,
},
{
name: "Russian mega mix MP3",
title: "(Club House, Progressive House, Russian Pop) Alex Kerdivar - Russian Mega Mix 21 (26.04.2026), MP3, 320 kbps",
wantArtist: "Alex Kerdivar",
wantYear: 2026,
wantFormat: release.FormatMP3,
wantBitrate: "320 kbps",
wantParseOK: true,
},
{
name: "Drum and bass fussy listener",
title: "(Intelligent Drum & Bass) LTJ Bukem - Fussy Listener Mix Vol 3 - 11.02.2026, MP3, 192 kbps",
wantArtist: "LTJ Bukem",
wantYear: 2026,
wantFormat: release.FormatMP3,
wantBitrate: "192 kbps",
wantParseOK: true,
},
{
name: "Neurofunk BBC Radio",
title: "(Neurofunk) Enta - Production Showcase Mix (BBC Radio 1) - 17.11.2024, MP3, 320 kbps",
wantArtist: "Enta",
wantYear: 2024,
wantFormat: release.FormatMP3,
wantBitrate: "320 kbps",
wantParseOK: true,
},
{
name: "Dark techstep methlab radio",
title: "(Drum & Bass, Dark Techstep) Allied - MethLab Radio Guest Mix [MLR040] - 05.11.2015, MP3, 320 kbps",
wantArtist: "Allied",
wantYear: 2015,
wantFormat: release.FormatMP3,
wantBitrate: "320 kbps",
wantParseOK: true,
},
{
name: "Goldie 6 mix VBR",
title: "(Drum & Bass) Goldie - The 6 Mix (BBC Radio 6) - 06-06-2025, MP3, V0",
wantArtist: "Goldie",
wantYear: 2025,
wantFormat: release.FormatMP3,
wantBitrate: "V0",
wantParseOK: true,
},
{
name: "Daphni Essential Mix",
title: "(House, Tech House) Daphni - BBC Radio 1s Essential Mix - 17-01-2026, MP3, V0",
wantArtist: "Daphni",
wantYear: 2026,
wantFormat: release.FormatMP3,
wantBitrate: "V0",
wantParseOK: true,
},
{
name: "Andy C 6 mix",
title: "(Drum & Bass) Andy C - The 6 Mix (BBC Radio 6) 16-01-2026, MP3, V0",
wantArtist: "Andy C",
wantYear: 2026,
wantFormat: release.FormatMP3,
wantBitrate: "V0",
wantParseOK: true,
},
{
name: "Club house Russian rap mix",
title: "(Club House, Russian Rap, Rap, Hip-Hop) Alex Kerdivar - Special Mega Mix 14 (17.01.2026), MP3, 320 kbps",
wantArtist: "Alex Kerdivar",
wantYear: 2026,
wantFormat: release.FormatMP3,
wantBitrate: "320 kbps",
wantParseOK: true,
},
{
name: "Jungle phonica mix series",
title: "(Drum & Bass, Jungle) Tim Reaper - Phonica Mix Series 128 (DJ Mix) - 2025, MP3, 320 kbps",
wantArtist: "Tim Reaper",
wantYear: 2025,
wantFormat: release.FormatMP3,
wantBitrate: "320 kbps",
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)
}
})
}
}
@@ -1,38 +0,0 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type ReggaeParser struct {
BaseParser
}
func NewReggaeParser() *ReggaeParser {
return &ReggaeParser{}
}
func (p *ReggaeParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
if len(r.Genres) == 0 {
r.Genres = []string{"Reggae"}
}
r.Type = p.DetectType(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Bitrate = p.ExtractBitrate(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.ReleaseCount = p.ExtractReleaseCount(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
@@ -1,134 +0,0 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestReggaeParser(t *testing.T) {
p := NewReggaeParser()
tests := []struct {
name string
title string
wantArtist string
wantYear int
wantFormat release.AudioFormat
wantType release.Type
wantBitrate string
wantParseOK bool
}{
{
name: "Reggae funk soul album",
title: "(Reggae, Funk / Soul) [CD] Diana King - Think Like A Girl (CD Album, Enhanced) - 1997, FLAC (tracks+.cue), lossless",
wantArtist: "Diana King",
wantYear: 1997,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Reggae dawn penn",
title: "(Reggae) [CD] Dawn Penn - Come Again [1996] - 1996, FLAC (tracks+.cue), lossless",
wantArtist: "Dawn Penn",
wantYear: 1996,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Reggae ska Bob Marley",
title: "(Reggae, Ska) [CD] Bob Marley & The Wailers - 3 альбома - (1973-1980), FLAC (tracks+.cue), lossless",
wantArtist: "Bob Marley & The Wailers",
wantYear: 1973,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Reggae Bob Marley collection",
title: "(Reggae) [CD] Bob Marley & The Wailers - коллекция 1970-2012 (86 альбомов), FLAC (image+.cue, tracks+.cue), lossless",
wantArtist: "Bob Marley & The Wailers",
wantYear: 1970,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Reggae rock ska",
title: "(Reggae Rock, Ska) [CD] The English Beat - Special Beat Service - 1986, FLAC (tracks+.cue), lossless",
wantArtist: "The English Beat",
wantYear: 1986,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Reggae VA celebration",
title: "(Reggae, Reggae-Pop, Ragga, Euro-House) [CD] VA - Reggae Celebration '97 Vol. 1 - 1997, FLAC (tracks+.cue), lossless",
wantArtist: "VA",
wantYear: 1997,
wantFormat: release.FormatFLAC,
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "Reggae big youth",
title: "(Reggae) [CD] Big Youth - Natty Universal Dread 1973-1979 - 2000, FLAC (tracks+.cue), lossless",
wantArtist: "Big Youth",
wantYear: 2000,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Reggae barrington levy multi-CD",
title: "(Reggae) [CD] Barrington Levy - Sweet Reggae Music 1979-84 (2 CD) - 2012, FLAC (tracks+.cue), lossless",
wantArtist: "Barrington Levy",
wantYear: 2012,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Sega-reggae elijah",
title: "(Sega-Reggae) [CD] ELIJAH - Luveologist - 2006, FLAC (tracks+.cue), lossless",
wantArtist: "ELIJAH",
wantYear: 2006,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Reggae UB40 ultimate edition",
title: "(Reggae) [WEB] UB40 - UB45 [Ultimate Edition] - 2024, FLAC (tracks), lossless",
wantArtist: "UB40",
wantYear: 2024,
wantFormat: release.FormatFLAC,
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)
}
})
}
}
-38
View File
@@ -1,38 +0,0 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type RockParser struct {
BaseParser
}
func NewRockParser() *RockParser {
return &RockParser{}
}
func (p *RockParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
if len(r.Genres) == 0 {
r.Genres = []string{"Rock"}
}
r.Type = p.DetectType(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Bitrate = p.ExtractBitrate(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.ReleaseCount = p.ExtractReleaseCount(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
@@ -1,138 +0,0 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestRockParser(t *testing.T) {
p := NewRockParser()
tests := []struct {
name string
title string
wantArtist string
wantYear int
wantFormat release.AudioFormat
wantType release.Type
wantBitrate string
wantParseOK bool
}{
{
name: "Blues rock single",
title: "(Rock, Blues Rock) [WEB] The Rolling Stones - In The Stars [Single] - 2026, FLAC (tracks), lossless",
wantArtist: "The Rolling Stones",
wantYear: 2026,
wantFormat: release.FormatFLAC,
wantType: release.TypeSingle,
wantParseOK: true,
},
{
name: "Hard rock single",
title: "(Hard Rock) [WEB] J.R. Blackmore - Moments Of Magic (Single) - 2012, FLAC (tracks), lossless",
wantArtist: "J.R. Blackmore",
wantYear: 2012,
wantFormat: release.FormatFLAC,
wantType: release.TypeSingle,
wantParseOK: true,
},
{
name: "Psychedelic rock collection",
title: "(Psychedelic Rock, Hard Rock, Blues Rock, Progressive Rock) [CD] Atomic Rooster - Collection Albums 1970-1973 (8 CD), FLAC (image+.cue), lossless",
wantArtist: "Atomic Rooster",
wantYear: 1970,
wantFormat: release.FormatFLAC,
wantType: release.TypeCollection,
wantParseOK: true,
},
{
name: "Rock discography",
title: "(Rock) [CD] Voice Of The Beehive - Discography - 1988-2022 (18 releases), FLAC (tracks+.cue), lossless",
wantArtist: "Voice Of The Beehive",
wantYear: 1988,
wantFormat: release.FormatFLAC,
wantType: release.TypeDiscography,
wantParseOK: true,
},
{
name: "Indie rock re-release",
title: "(Indie Rock / Indie Pop) [WEB] Easy - Magic Seed - 1990, FLAC (tracks), lossless(Re-release)",
wantArtist: "Easy",
wantYear: 1990,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Progressive rock album",
title: "(Progressive Rock) [WEB] Os Mutantes - De Volta Ao Planeta Dos Mutantes - 2006, FLAC (tracks), lossless",
wantArtist: "Os Mutantes",
wantYear: 2006,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Hard rock reissue",
title: "(Hard Rock) [CD] Gene Simmons - Gene Simmons - 1978 (1991), FLAC (tracks+.cue), lossless",
wantArtist: "Gene Simmons",
wantYear: 1978,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Pop rock collection",
title: "(Pop Rock) [CD] Sneakers (with Sanne Salomonsen) - Collection (1980-1997) (4 releases), FLAC (image+.cue), lossless",
wantArtist: "Sneakers (with Sanne Salomonsen)",
wantYear: 1980,
wantFormat: release.FormatFLAC,
wantType: release.TypeCollection,
wantParseOK: true,
},
{
name: "Southern rock album",
title: "(Southern Rock, Hard Rock, Blues Rock) [WEB] The Cold Stares - Texas - 2026, FLAC (tracks), lossless",
wantArtist: "The Cold Stares",
wantYear: 2026,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Hard rock 70s style",
title: "(Hard Rock, 70's) [WEB] Lynx - Trinity of Suns - 2026, FLAC (tracks), lossless",
wantArtist: "Lynx",
wantYear: 2026,
wantFormat: release.FormatFLAC,
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)
}
})
}
}
@@ -1,38 +0,0 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type ShansonParser struct {
BaseParser
}
func NewShansonParser() *ShansonParser {
return &ShansonParser{}
}
func (p *ShansonParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
if len(r.Genres) == 0 {
r.Genres = []string{"Shanson"}
}
r.Type = p.DetectType(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Bitrate = p.ExtractBitrate(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.ReleaseCount = p.ExtractReleaseCount(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
@@ -1,136 +0,0 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestShansonParser(t *testing.T) {
p := NewShansonParser()
tests := []struct {
name string
title string
wantArtist string
wantYear int
wantFormat release.AudioFormat
wantType release.Type
wantBitrate string
wantParseOK bool
}{
{
name: "French shanson VA multi-CD",
title: "(Shanson) [4 CD] VA - Chansons Francaises - 2011, FLAC (image+.cue), lossless",
wantArtist: "VA",
wantYear: 2011,
wantFormat: release.FormatFLAC,
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "Retro shanson album",
title: "(Retro-Shanson) [CD] Биртман - Следы от компота - 2015, FLAC (tracks+.cue), lossless",
wantArtist: "Биртман",
wantYear: 2015,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Pop shanson collection",
title: "(Pop, Shanson) Sylvie Vartan - Best Artist Collection - 198?, APE (tracks+.cue) lossless",
wantArtist: "Sylvie Vartan",
wantFormat: release.FormatAPE,
wantType: release.TypeCollection,
wantParseOK: true,
},
{
name: "French shanson Piaf",
title: "(French Shanson) Jil Aigrot - Words Of Love: The Voice of Edith Piaf in the Award-winning Film La Vie En Rose - 2008, APE (image+.cue), lossless",
wantArtist: "Jil Aigrot",
wantYear: 2008,
wantFormat: release.FormatAPE,
wantParseOK: true,
},
{
name: "Pop shanson Joe Dassin best of",
title: "(Pop/Shanson) Joe Dassin - Greatest Hits (2 CDs SET DIGIPACK) - 2007, FLAC (image + .cue), lossless",
wantArtist: "Joe Dassin",
wantYear: 2007,
wantFormat: release.FormatFLAC,
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "Shanson Adamo WavPack",
title: "(Shanson) Salvatore Adamo - L'essentiel - 2003, WAVPack (image+.cue), lossless",
wantArtist: "Salvatore Adamo",
wantYear: 2003,
wantFormat: release.FormatWavPack,
wantParseOK: true,
},
{
name: "Lounge shanson french pop",
title: "(Lounge, Shanson, French-Pop) Helena Noguerra - Nee Dans La Nature - 2004, APE (image + .cue), lossless",
wantArtist: "Helena Noguerra",
wantYear: 2004,
wantFormat: release.FormatAPE,
wantParseOK: true,
},
{
name: "Shanson VA blatnaya",
title: "(Shanson) [CD] VA - Блатная Империя 100 лучших Хитов - 2007, FLAC (tracks), lossless",
wantArtist: "VA",
wantYear: 2007,
wantFormat: release.FormatFLAC,
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "Bard song collection",
title: "(Авторская песня) [CD] Булат Окуджава - Коллекция (20 CD) - 1967-2001, FLAC (image+.cue), lossless",
wantArtist: "Булат Окуджава",
wantYear: 1967,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Russian shanson discography MP3",
title: "(Шансон) Михаил Круг - Дискография (34 альбома) - 1994-2009, MP3, 192-320 kbps",
wantArtist: "Михаил Круг",
wantYear: 1994,
wantFormat: release.FormatMP3,
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)
}
})
}
}
@@ -1,34 +0,0 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type SoundtracksParser struct {
BaseParser
}
func NewSoundtracksParser() *SoundtracksParser {
return &SoundtracksParser{}
}
func (p *SoundtracksParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
r.Type = release.TypeSoundtrack
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Bitrate = p.ExtractBitrate(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
@@ -1,139 +0,0 @@
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)
}
})
}
}
@@ -1,42 +0,0 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type VinylDigitizationParser struct {
BaseParser
}
func NewVinylDigitizationParser() *VinylDigitizationParser {
return &VinylDigitizationParser{}
}
func (p *VinylDigitizationParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
r.Type = p.DetectType(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Source = release.SourceVinyl
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
if r.Format == release.FormatUnknown {
r.Format = release.FormatFLAC
}
r.Bitrate = "lossless"
if condMatch := vinylConditionPattern.FindStringSubmatch(title); len(condMatch) >= 2 {
r.Tags = append(r.Tags, "Vinyl:"+condMatch[1])
}
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
@@ -1,147 +0,0 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestVinylDigitizationParser(t *testing.T) {
p := NewVinylDigitizationParser()
tests := []struct {
name string
title string
wantArtist string
wantYear int
wantBitDepth int
wantSampleRate int
wantFormat release.AudioFormat
wantRipType string
wantParseOK bool
}{
{
name: "standard LP 24/192",
title: "(Pop-Rock/Punk) [LP] [24/192] Сектор Газа - Ядрена вошь - 1990, WavPack (image+.cue)",
wantArtist: "Сектор Газа",
wantYear: 1990,
wantBitDepth: 24,
wantSampleRate: 192000,
wantFormat: release.FormatWavPack,
wantRipType: "image+.cue",
wantParseOK: true,
},
{
name: "massive vinyl collection",
title: "(Synth-Pop) [LP/12''/10''/7''] [24/96] Depeche Mode - The Vinyl Collection (17 Albums, 66 Singles, 6 Compilations, 51 Bootlegs) (429 Releases) - 1981-2024, FLAC (tracks) lossless",
wantArtist: "Depeche Mode",
wantYear: 1981,
wantParseOK: true,
},
{
name: "2xLP 32bit",
title: "(Soft Rock, Pop Rock) [2xLP] [32/176.4] Genesis - Turn It On Again - The Hits - 1999(2024,Reissue, 25th anniversary.), WavPack (tracks)",
wantArtist: "Genesis",
wantYear: 1999,
wantBitDepth: 32,
wantSampleRate: 176400,
wantFormat: release.FormatWavPack,
wantParseOK: true,
},
{
name: "32/384 ultra high res",
title: "(Prog Rock) [LP] [32/384] Emerson, Lake & Palmer-Emerson, Lake & Palmer - 2025 (1970), WavPack (tracks)",
wantYear: 2025,
wantBitDepth: 32,
wantSampleRate: 384000,
wantFormat: release.FormatWavPack,
wantParseOK: true,
},
{
name: "soul LP",
title: "(Soul, Funk) [LP] [24/192] Curtis Mayfield - Curtis - 1970/2025, FLAC (tracks)",
wantArtist: "Curtis Mayfield",
wantYear: 1970,
wantBitDepth: 24,
wantSampleRate: 192000,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "16/44 standard",
title: "(Rock) [LP] [16/44] Tony Sheridan - Collection 4LP - 1976-1987, FLAC (image+.cue)",
wantArtist: "Tony Sheridan",
wantYear: 1976,
wantParseOK: true,
},
{
name: "MFSL pressing",
title: "(Rock, Pop Rock) [LP] [24/96] Fleetwood Mac Mirage - 1982 (1984 MFSL 1-119), FLAC (tracks)",
wantArtist: "Fleetwood Mac",
wantYear: 1982,
wantParseOK: true,
},
{
name: "multiple LPs in one",
title: "(Rock) [LP] [24/96] 10cc - 2LP's - 1976, 1977, FLAC (tracks+.cue)",
wantArtist: "10cc",
wantYear: 1976,
wantRipType: "tracks+.cue",
wantParseOK: true,
},
{
name: "collection from vinyl",
title: "(Progressive Rock) [LP] [24/192] Marillion, Fish - Vinyl Collection - 1982-1994 (6 releases), FLAC (image+.cue)",
wantArtist: "Marillion, Fish",
wantYear: 1982,
wantParseOK: true,
},
{
name: "Japan vinyl",
title: "(Pop) [LP] [24/96] ABBA - The Album (Original Japan Vinyl) - 1977, FLAC (tracks)",
wantArtist: "ABBA",
wantYear: 1977,
wantBitDepth: 24,
wantSampleRate: 96000,
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.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)
}
if tt.wantFormat != release.FormatUnknown && r.Format != tt.wantFormat {
t.Errorf("Format = %v, want %v", r.Format, tt.wantFormat)
}
if tt.wantRipType != "" && r.RipType != tt.wantRipType {
t.Errorf("RipType = %q, want %q", r.RipType, tt.wantRipType)
}
if r.Source != release.SourceVinyl {
t.Errorf("Source = %v, want Vinyl", r.Source)
}
})
}
}