Update rutracker categories

This commit is contained in:
Alexander
2026-05-06 21:27:03 +02:00
parent 2400c6345a
commit b8fcbacb07
32 changed files with 2709 additions and 108 deletions
+5
View File
@@ -0,0 +1,5 @@
package indexer
type Filter interface {
IsKnownCategory(categories []string) bool
}
+1 -1
View File
@@ -32,7 +32,7 @@ func (indexer *JacketIndexer) Search(query string, limit int32, tracker string)
}
url := indexer.cfg.Indexer.Url
uri := fmt.Sprintf("%v/api/v2.0/indexers/%v/results/torznab?apikey=%v&limit=%d&q=%v&t=search", url, searchTracker, indexer.cfg.Indexer.ApiKey, limit, query)
uri := fmt.Sprintf("%v/api/v2.0/indexers/%v/results/torznab?apikey=%v&limit=%d&cat=3010,3040&q=%v&t=search", url, searchTracker, indexer.cfg.Indexer.ApiKey, limit, query)
log.Debug().Str("uri", uri).Msg("Sending search request")
+14 -7
View File
@@ -7,8 +7,6 @@ import (
"homelab.lan/music-agregator/internal/tracker/rutracker"
)
var parserFactory = rutracker.NewParserFactory()
type SearchResult struct {
XMLName xml.Name `xml:"rss"`
Items []Item `xml:"channel>item"`
@@ -37,10 +35,19 @@ type TorznabAttr struct {
Value string `xml:"value,attr"`
}
func (sr *SearchResult) ToProto() *pb.SearchResponse {
pbItems := make([]*pb.SearchItem, len(sr.Items))
var (
parserFactory = rutracker.NewParserFactory()
filter = rutracker.NewFilter()
)
func (sr *SearchResult) ToProto() *pb.SearchResponse {
var pbItems []*pb.SearchItem
for _, item := range sr.Items {
if !filter.IsKnownCategory(item.Categories) {
continue
}
for i, item := range sr.Items {
pbAttrs := make([]*pb.TorznabAttr, len(item.TorznabAttrs))
for j, attr := range item.TorznabAttrs {
pbAttrs[j] = &pb.TorznabAttr{
@@ -51,7 +58,7 @@ func (sr *SearchResult) ToProto() *pb.SearchResponse {
release := parserFactory.GetParser(item.Categories).Parse(item.Title)
pbItems[i] = &pb.SearchItem{
pbItems = append(pbItems, &pb.SearchItem{
Title: item.Title,
DownloadLink: item.Link,
TorrentPageUrl: item.Guid,
@@ -66,7 +73,7 @@ func (sr *SearchResult) ToProto() *pb.SearchResponse {
},
TorznabAttrs: pbAttrs,
Release: release.ToProto(),
}
})
}
return &pb.SearchResponse{
+404
View File
@@ -0,0 +1,404 @@
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
}
+62 -84
View File
@@ -11,16 +11,25 @@ type parserType int
const (
parserGeneral parserType = iota
parserLossless
parserLossy
parserHiRes
parserVinylDigitization
parserClassical
parserJazz
parserRock
parserMetal
parserAlternative
parserPop
parserElectronic
parserHipHop
parserJazz
parserBlues
parserClassical
parserFolk
parserReggae
parserSoundtracks
parserDiscography
parserShanson
parserHiRes
parserDigitization
parserLabelPacks
parserRadioshow
parserAAC
parserMiscMusic
)
var categoryToParser map[int]parserType
@@ -28,80 +37,35 @@ var categoryToParser map[int]parserType
func init() {
categoryToParser = make(map[int]parserType)
torznabCategories := map[int]parserType{
3000: parserGeneral,
3010: parserLossy,
3040: parserLossless,
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)
}
losslessForumIDs := []int{
425, 429, 1760, 1635, 1634, 2495, 1299, 1141, 1660, 1662, 1661, 1852, 1648,
1851, 1850, 1633, 1632, 1643, 1846, 2219, 2220, 2221, 1647, 1847, 1848, 1653,
738, 739, 740, 1656, 1654, 1655, 1843, 1841, 1842, 408, 1844, 1845, 1849,
1650, 1651, 1652, 1659, 1657, 1658, 445, 1664, 1665, 1666, 1669, 1667, 1668,
1906, 1907, 1908, 1911, 1909, 1910,
}
lossyForumIDs := []int{
424, 428, 1754, 1755, 1756, 1757, 1758, 1759, 1760, 1761, 441, 446,
1765, 1766, 1767, 1768, 1769, 1770, 1771,
}
hiResForumIDs := []int{
1801, 1807, 1808, 1809, 1810, 1811, 1812, 1813, 1814, 1815, 1816, 1817,
2378, 2379, 2380, 2381, 2382, 2383, 2384,
}
vinylForumIDs := []int{
1802, 1803, 1804, 1805, 1806,
}
classicalForumIDs := []int{
436, 969, 1990, 984, 1125, 1126, 1127, 1128, 1129, 1130, 1131, 1132,
1670, 1671, 1672, 1673, 1674, 1675, 1676, 1677,
}
jazzForumIDs := []int{
1698, 1699, 1700, 1701, 1702, 1703, 1704, 1705, 1706, 1707, 1708, 1709,
}
metalForumIDs := []int{
731, 732, 733, 734, 735, 736, 737, 738, 739, 740,
1730, 1731, 1732, 1733, 1734, 1735, 1736, 1737, 1738, 1739,
}
soundtrackForumIDs := []int{
691, 702, 704, 705, 706, 707, 708, 709, 710, 711,
1631, 469, 786,
}
for id, pt := range torznabCategories {
func registerAll(ids []int, pt parserType) {
for _, id := range ids {
categoryToParser[id] = pt
}
for _, id := range losslessForumIDs {
categoryToParser[id] = parserLossless
}
for _, id := range lossyForumIDs {
categoryToParser[id] = parserLossy
}
for _, id := range hiResForumIDs {
categoryToParser[id] = parserHiRes
}
for _, id := range vinylForumIDs {
categoryToParser[id] = parserVinylDigitization
}
for _, id := range classicalForumIDs {
categoryToParser[id] = parserClassical
}
for _, id := range jazzForumIDs {
categoryToParser[id] = parserJazz
}
for _, id := range metalForumIDs {
categoryToParser[id] = parserMetal
}
for _, id := range soundtrackForumIDs {
categoryToParser[id] = parserSoundtracks
}
}
type ParserFactory struct {
@@ -112,26 +76,40 @@ func NewParserFactory() *ParserFactory {
return &ParserFactory{
parsers: map[parserType]tracker.Parser{
parserGeneral: parser.NewGeneralParser(),
parserLossless: parser.NewLosslessParser(),
parserLossy: parser.NewLossyParser(),
parserHiRes: parser.NewHiResParser(),
parserVinylDigitization: parser.NewVinylDigitizationParser(),
parserClassical: parser.NewClassicalParser(),
parserJazz: parser.NewJazzParser(),
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(),
parserDiscography: parser.NewDiscographyParser(),
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]
}
+49 -12
View File
@@ -15,20 +15,33 @@ func TestParserFactory_GetParser(t *testing.T) {
categories []string
wantType string
}{
{"torznab lossless", []string{"3040"}, "*parser.LosslessParser"},
{"torznab lossy", []string{"3010"}, "*parser.LossyParser"},
{"torznab general", []string{"3000"}, "*parser.GeneralParser"},
{"rutracker lossless forum", []string{"425"}, "*parser.LosslessParser"},
{"rutracker lossy forum", []string{"424"}, "*parser.LossyParser"},
{"rutracker hires forum", []string{"1801"}, "*parser.HiResParser"},
{"rutracker vinyl forum", []string{"1802"}, "*parser.VinylDigitizationParser"},
{"rutracker classical forum", []string{"436"}, "*parser.ClassicalParser"},
{"rutracker jazz forum", []string{"1698"}, "*parser.JazzParser"},
{"rutracker metal forum", []string{"731"}, "*parser.MetalParser"},
{"rutracker soundtrack forum", []string{"691"}, "*parser.SoundtracksParser"},
{"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", "3040"}, "*parser.LosslessParser"},
{"multiple categories uses first match", []string{"99999", "1728"}, "*parser.MetalParser"},
{"jackett prefixed id stripped", []string{"101719"}, "*parser.MetalParser"},
}
for _, tt := range tests {
@@ -66,6 +79,30 @@ func getParserTypeName(p tracker.Parser) string {
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
@@ -0,0 +1,25 @@
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
@@ -0,0 +1,35 @@
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
@@ -0,0 +1,37 @@
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
}
@@ -0,0 +1,142 @@
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)
}
})
}
}
@@ -0,0 +1,38 @@
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
}
@@ -0,0 +1,133 @@
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)
}
})
}
}
@@ -0,0 +1,38 @@
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
}
@@ -0,0 +1,133 @@
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)
}
})
}
}
@@ -0,0 +1,38 @@
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
}
@@ -0,0 +1,142 @@
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
@@ -0,0 +1,38 @@
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
}
@@ -0,0 +1,138 @@
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)
}
})
}
}
@@ -0,0 +1,38 @@
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
}
@@ -0,0 +1,138 @@
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)
}
})
}
}
@@ -0,0 +1,37 @@
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
}
@@ -0,0 +1,144 @@
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)
}
})
}
}
+38
View File
@@ -0,0 +1,38 @@
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
}
@@ -0,0 +1,135 @@
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)
}
})
}
}
@@ -0,0 +1,38 @@
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
}
@@ -0,0 +1,143 @@
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)
}
})
}
}
@@ -0,0 +1,38 @@
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
}
@@ -0,0 +1,134 @@
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
@@ -0,0 +1,38 @@
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
}
@@ -0,0 +1,138 @@
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)
}
})
}
}
@@ -0,0 +1,38 @@
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
}
@@ -0,0 +1,136 @@
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)
}
})
}
}