diff --git a/internal/indexer/filter.go b/internal/indexer/filter.go new file mode 100644 index 0000000..ff2ee56 --- /dev/null +++ b/internal/indexer/filter.go @@ -0,0 +1,5 @@ +package indexer + +type Filter interface { + IsKnownCategory(categories []string) bool +} diff --git a/internal/indexer/jackett.go b/internal/indexer/jackett.go index 75960d8..e26e5e3 100644 --- a/internal/indexer/jackett.go +++ b/internal/indexer/jackett.go @@ -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") diff --git a/internal/indexer/search.go b/internal/indexer/search.go index 4f93e2f..5a2c73a 100644 --- a/internal/indexer/search.go +++ b/internal/indexer/search.go @@ -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{ diff --git a/internal/tracker/rutracker/category.go b/internal/tracker/rutracker/category.go new file mode 100644 index 0000000..4133637 --- /dev/null +++ b/internal/tracker/rutracker/category.go @@ -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 +} diff --git a/internal/tracker/rutracker/factory.go b/internal/tracker/rutracker/factory.go index bd0ba17..33ad4e5 100644 --- a/internal/tracker/rutracker/factory.go +++ b/internal/tracker/rutracker/factory.go @@ -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 - 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, - } + 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) +} - 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 { @@ -111,27 +75,41 @@ type ParserFactory struct { 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(), - parserMetal: parser.NewMetalParser(), - parserSoundtracks: parser.NewSoundtracksParser(), - parserDiscography: parser.NewDiscographyParser(), - parserLabelPacks: parser.NewLabelPacksParser(), + 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] } diff --git a/internal/tracker/rutracker/factory_test.go b/internal/tracker/rutracker/factory_test.go index 8083504..5d57d58 100644 --- a/internal/tracker/rutracker/factory_test.go +++ b/internal/tracker/rutracker/factory_test.go @@ -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" } diff --git a/internal/tracker/rutracker/filter.go b/internal/tracker/rutracker/filter.go new file mode 100644 index 0000000..9f7b4a3 --- /dev/null +++ b/internal/tracker/rutracker/filter.go @@ -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 +} diff --git a/internal/tracker/rutracker/filter_test.go b/internal/tracker/rutracker/filter_test.go new file mode 100644 index 0000000..cab079a --- /dev/null +++ b/internal/tracker/rutracker/filter_test.go @@ -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) + } + }) + } +} diff --git a/internal/tracker/rutracker/parser/aac.go b/internal/tracker/rutracker/parser/aac.go new file mode 100644 index 0000000..fad8a10 --- /dev/null +++ b/internal/tracker/rutracker/parser/aac.go @@ -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 +} diff --git a/internal/tracker/rutracker/parser/aac_test.go b/internal/tracker/rutracker/parser/aac_test.go new file mode 100644 index 0000000..6812a5d --- /dev/null +++ b/internal/tracker/rutracker/parser/aac_test.go @@ -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) + } + }) + } +} diff --git a/internal/tracker/rutracker/parser/alternative.go b/internal/tracker/rutracker/parser/alternative.go new file mode 100644 index 0000000..1f3f9d4 --- /dev/null +++ b/internal/tracker/rutracker/parser/alternative.go @@ -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 +} diff --git a/internal/tracker/rutracker/parser/alternative_test.go b/internal/tracker/rutracker/parser/alternative_test.go new file mode 100644 index 0000000..8a7c493 --- /dev/null +++ b/internal/tracker/rutracker/parser/alternative_test.go @@ -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) + } + }) + } +} diff --git a/internal/tracker/rutracker/parser/blues.go b/internal/tracker/rutracker/parser/blues.go new file mode 100644 index 0000000..d1f0d09 --- /dev/null +++ b/internal/tracker/rutracker/parser/blues.go @@ -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 +} diff --git a/internal/tracker/rutracker/parser/blues_test.go b/internal/tracker/rutracker/parser/blues_test.go new file mode 100644 index 0000000..20f573e --- /dev/null +++ b/internal/tracker/rutracker/parser/blues_test.go @@ -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) + } + }) + } +} diff --git a/internal/tracker/rutracker/parser/electronic.go b/internal/tracker/rutracker/parser/electronic.go new file mode 100644 index 0000000..f5025b8 --- /dev/null +++ b/internal/tracker/rutracker/parser/electronic.go @@ -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 +} diff --git a/internal/tracker/rutracker/parser/electronic_test.go b/internal/tracker/rutracker/parser/electronic_test.go new file mode 100644 index 0000000..7c99fa9 --- /dev/null +++ b/internal/tracker/rutracker/parser/electronic_test.go @@ -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) + } + }) + } +} diff --git a/internal/tracker/rutracker/parser/folk.go b/internal/tracker/rutracker/parser/folk.go new file mode 100644 index 0000000..99de775 --- /dev/null +++ b/internal/tracker/rutracker/parser/folk.go @@ -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 +} diff --git a/internal/tracker/rutracker/parser/folk_test.go b/internal/tracker/rutracker/parser/folk_test.go new file mode 100644 index 0000000..52ea1db --- /dev/null +++ b/internal/tracker/rutracker/parser/folk_test.go @@ -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) + } + }) + } +} diff --git a/internal/tracker/rutracker/parser/hiphop.go b/internal/tracker/rutracker/parser/hiphop.go new file mode 100644 index 0000000..2922ca6 --- /dev/null +++ b/internal/tracker/rutracker/parser/hiphop.go @@ -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 +} diff --git a/internal/tracker/rutracker/parser/hiphop_test.go b/internal/tracker/rutracker/parser/hiphop_test.go new file mode 100644 index 0000000..b48d7b4 --- /dev/null +++ b/internal/tracker/rutracker/parser/hiphop_test.go @@ -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) + } + }) + } +} diff --git a/internal/tracker/rutracker/parser/misc_music.go b/internal/tracker/rutracker/parser/misc_music.go new file mode 100644 index 0000000..d6439a1 --- /dev/null +++ b/internal/tracker/rutracker/parser/misc_music.go @@ -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 +} diff --git a/internal/tracker/rutracker/parser/misc_music_test.go b/internal/tracker/rutracker/parser/misc_music_test.go new file mode 100644 index 0000000..6d257d5 --- /dev/null +++ b/internal/tracker/rutracker/parser/misc_music_test.go @@ -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) + } + }) + } +} diff --git a/internal/tracker/rutracker/parser/pop.go b/internal/tracker/rutracker/parser/pop.go new file mode 100644 index 0000000..fb70a1b --- /dev/null +++ b/internal/tracker/rutracker/parser/pop.go @@ -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 +} diff --git a/internal/tracker/rutracker/parser/pop_test.go b/internal/tracker/rutracker/parser/pop_test.go new file mode 100644 index 0000000..d0557ff --- /dev/null +++ b/internal/tracker/rutracker/parser/pop_test.go @@ -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) + } + }) + } +} diff --git a/internal/tracker/rutracker/parser/radioshow.go b/internal/tracker/rutracker/parser/radioshow.go new file mode 100644 index 0000000..b5d25ab --- /dev/null +++ b/internal/tracker/rutracker/parser/radioshow.go @@ -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 +} diff --git a/internal/tracker/rutracker/parser/radioshow_test.go b/internal/tracker/rutracker/parser/radioshow_test.go new file mode 100644 index 0000000..e82494c --- /dev/null +++ b/internal/tracker/rutracker/parser/radioshow_test.go @@ -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) + } + }) + } +} diff --git a/internal/tracker/rutracker/parser/reggae.go b/internal/tracker/rutracker/parser/reggae.go new file mode 100644 index 0000000..b673709 --- /dev/null +++ b/internal/tracker/rutracker/parser/reggae.go @@ -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 +} diff --git a/internal/tracker/rutracker/parser/reggae_test.go b/internal/tracker/rutracker/parser/reggae_test.go new file mode 100644 index 0000000..adc7218 --- /dev/null +++ b/internal/tracker/rutracker/parser/reggae_test.go @@ -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) + } + }) + } +} diff --git a/internal/tracker/rutracker/parser/rock.go b/internal/tracker/rutracker/parser/rock.go new file mode 100644 index 0000000..03fad95 --- /dev/null +++ b/internal/tracker/rutracker/parser/rock.go @@ -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 +} diff --git a/internal/tracker/rutracker/parser/rock_test.go b/internal/tracker/rutracker/parser/rock_test.go new file mode 100644 index 0000000..6bcf505 --- /dev/null +++ b/internal/tracker/rutracker/parser/rock_test.go @@ -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) + } + }) + } +} diff --git a/internal/tracker/rutracker/parser/shanson.go b/internal/tracker/rutracker/parser/shanson.go new file mode 100644 index 0000000..c779cef --- /dev/null +++ b/internal/tracker/rutracker/parser/shanson.go @@ -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 +} diff --git a/internal/tracker/rutracker/parser/shanson_test.go b/internal/tracker/rutracker/parser/shanson_test.go new file mode 100644 index 0000000..2701278 --- /dev/null +++ b/internal/tracker/rutracker/parser/shanson_test.go @@ -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) + } + }) + } +}