Implement Jackett search entpoint

This commit is contained in:
Alexander
2026-05-04 22:48:14 +02:00
parent 8ffa92276e
commit bfef1b6c79
43 changed files with 4437 additions and 114 deletions
+25
View File
@@ -0,0 +1,25 @@
meta {
name: Search
type: grpc
seq: 2
}
grpc {
url: localhost:3000
method: /music_agregator.indexer.v1.IndexerService/Search
body: grpc
protoPath: ../proto/music_agregator/indexer/v1/indexer.proto
auth: inherit
methodType: unary
}
body:grpc {
name: message 1
content: '''
{
"tracker": "",
"query": "Metallica",
"limit": 1
}
'''
}
+4 -4
View File
@@ -5,16 +5,16 @@ meta {
}
get {
url: http://localhost:9117/api/v2.0/indexers/all/results/torznab?apikey=3jfvdvt1etzz36drkw5id5sb95sc47fi&limit=1&artist=Metallica&t=music
url: http://localhost:9117/api/v2.0/indexers/all/results/torznab?apikey=3jfvdvt1etzz36drkw5id5sb95sc47fi&limit=2&q=Metallica&t=search
body: none
auth: inherit
}
params:query {
apikey: 3jfvdvt1etzz36drkw5id5sb95sc47fi
limit: 1
artist: Metallica
t: music
limit: 2
q: Metallica
t: search
}
settings {
-1
View File
@@ -69,7 +69,6 @@ func serveGrpc(config config.Config) {
}
listener, err := net.Listen("tcp", fmt.Sprintf("%v:%v", config.App.Host, config.App.Port))
if err != nil {
log.Fatal().Err(err).Msg("Failed to listen on localhost:8081")
}
+224
View File
@@ -0,0 +1,224 @@
# Общие требования к раздачам в разделе КЛАССИЧЕСКОЙ музыки RuTracker
**Источник:** https://rutracker.org/forum/viewtopic.php?t=773016
Для разделов: lossy, lossless, видео, DVD, Hi-Res, оцифровки, классика в современной обработке.
---
## 1. Общие положения
Данные правила дополняют общие правила музыкальных разделов и применяются ко всем подразделам классической музыки.
## 2. Определение повторов
### 2.1 Проверка перед созданием
Обязательно использовать поиск по трекеру.
### 2.2 Что считается повтором
Материал, не отличающийся в лучшую сторону по качеству.
### 2.3 Что НЕ считается повтором
- lossy при наличии lossless и наоборот
- Rip при наличии DVD и наоборот
- **Одно произведение с разным составом исполнителей**
- **Одно произведение с разной датой записи**
- Одна запись разных лейблов (при существенных отличиях ремастеринга)
### 2.4 Перекрёстные раздачи
🚫 Запрещены.
## 3. Требования к наполнению
### 3.1 Одна раздача = одно издание
🚫 Запрещено помещать несколько официальных изданий в одну lossless-раздачу.
### Многодисковые издания:
- **Box-set** - диски в одной коробке
- **Серия** - с явным указанием принадлежности на обложке/сайте
### 3.2 Исключения
#### 3.2.1 Дискографии исполнителей
Отдельные альбомы популярных академических исполнителей.
> **Альбом** - набор композиций, подобранный специально для издания.
> **Не альбом** - запись отдельного произведения (например, "Риголетто").
#### 3.2.2 Полные циклы
- Все симфонии композитора
- Все концерты
- Все квартеты
- Циклы по замыслу автора ("Кольцо нибелунга", "Времена года")
При одинаковом исполнительском составе.
### 3.3 Сборники
🚫 Запрещены сборники без чёткой темы или с разнородным содержанием.
### 3.4 Минимум треков
🚫 Один трек запрещён (кроме самодостаточных произведений).
### 3.5 Полнота
🚫 Запрещены неполные рипы или разделение диска.
### 3.6 Целостность
🚫 Запрещено разделять произведение на несколько раздач.
### 3.7 Качество записи
🚫 Запрещены записи с телефона/диктофона.
### 3.8 Битрейт
🚫 MP3 < 192 kbps
🚫 Срез частот < 16 кГц
🚫 Частота дискретизации < 44 кГц
✅ Исключение: редкие/раритетные записи (решение модератора).
## 4. Требования к заголовкам
### 4.1 Обязательное содержание
```
Композитор - Название произведения (Исполнитель)
```
### 4.2 Сборники по композитору
```
Композитор - Название сборника, Произведения (Исполнители)
```
### 4.3 Сборники по исполнителю
```
Название сборника - Композиторы, Произведения (Исполнитель)
```
### 4.4 Прочие сборники
```
Название сборника - Композиторы, Произведения
```
### 4.5 Разделитель
Тире между композитором и произведением.
### 4.6 Язык
Язык оригинала (родной язык композитора или авторское название).
### 4.7 Диакритика
Дублировать латиницей при умляутах.
### 4.8 Перевод
Рекомендуется русский перевод через `/`.
### Пример:
```
(Classical, Opera) Rossini - Il barbiere di Siviglia / Россини - Севильский цирюльник (B. Sills, Н. Гедда, LSO, James Levine) - 1975, APE (image+.cue) lossless
```
### 4.9 Запреты
🚫 Сокращения (кроме № и #)
🚫 CAPS LOCK
🚫 Точки (кроме пунктуации)
## 5. Жанры классической музыки
### Обязательный тег
**Classical** - для всех раздач
### Вокальное искусство:
- **Opera** - опера
- **Choral** - хоровая музыка
- **Vocal** - произведения для голоса
### Оркестровая музыка:
- **Symphony** - симфонии
- **Concerto** - концерты для солиста с оркестром
- **Orchestral** - увертюры, сюиты, поэмы, балеты
### Камерная музыка:
- **Chamber** - сонаты, дуэты, трио, квартеты
### Сольная музыка:
- **Piano** - фортепиано
- **Organ** - орган
- **Violin** - скрипка
- **Cello** - виолончель
- **Guitar** - классическая гитара
- **Harp** - арфа
- **Flute** - флейта
### Эпохи:
- **Medieval** - XIXIV века
- **Renaissance** - 14001600
- **Baroque** - 16001750
- **Romantic** - XIX–нач. XX века
- **Avantgarde** - нач.–сер. XX века
- **Minimalism** - с 1964 года
### Видео:
- **Ballet** - классический балет
- **Dance** - современный балет
- **Concert** - концерты
- **Documentary** - фильмы, мастер-классы
## 6. Треклист и исполнители
### 6.1 Обязательность
Треклист и список исполнителей **строго обязательны**.
### 6.2 Соответствие
Должны точно соответствовать содержанию.
### 6.3 Содержание треклиста
- Фамилия композитора
- Название произведения
### 6.4 Формат исполнителей
```
Солисты (партии), Хор, Оркестр/Ансамбль, Дирижёр
```
### 6.5 Крупные формы
Для опер, симфоний - допускается указание диапазона треков:
```
1-4. Chopin: Piano Sonata No.2
```
### 6.6 Фрагменты
Указывать полное название + название фрагмента:
```
01. Nessun dorma! - Turandot (Puccini)
```
### 6.7 Дополнительно
- Лейбл
- Дата и место записи
- Продолжительность (чч:мм:сс)
## Паттерны для парсера
```regex
# Opus номер
Op\.\s*(\d+)(?:\s*[Nn]o\.\s*(\d+))?
# BWV/KV/D номера
(?:BWV|KV|K\.|D\.|Op\.)\s*(\d+)
# Формат названия произведения
(Symphony|Concerto|Sonata|Quartet|Suite|Overture)\s*(?:No\.\s*)?(\d+)
# Тональность
in\s+([A-G])\s*(major|minor|[-♯♭]?\s*(?:dur|moll))?
# Исполнитель с оркестром
([^,]+),\s*([^,]+(?:Orchestra|Philharmonic|Symphony))[,;]\s*([^)]+)
# Композитор - Произведение
^([A-Za-zА-Яа-яёÄÖÜäöüß\s]+)\s*[-]\s*(.+)$
```
## История изменений
- **28.07.2014** - п. 6.5.1
- **17.03.2016** - пп. 3.1.1, 3.1.2, добавлен 3.9
- **11.07.2022** - пп. 4.2.2, 4.2.3
- **07.06.2023** - пп. 4.1, 4.6, 5.2; объединены 4.7-4.9
+125
View File
@@ -0,0 +1,125 @@
# Разъяснения по дискографиям и коллекциям RuTracker
**Источник:** https://rutracker.org/forum/viewtopic.php?t=6372771
## Определения
### Дискография (Discography)
- Содержит слово "Дискография" или "Discography"
- Включает релизы только под **ОДНИМ** именем исполнителя
- Формат количества: `(15 CD)`, `(20 albums)`, `(10 releases)`
- Формат годов: `[19902020]` (используется длинное тире)
### Коллекция (Collection)
- Содержит слово "Коллекция" или "Collection"
- Может включать **несколько** имён/псевдонимов одного артиста
- Включает сайд-проекты, сольные работы, коллаборации
- Тот же формат количества и годов
## Ключевые различия
| Критерий | Дискография | Коллекция |
|----------|-------------|-----------|
| Имена артиста | Только одно | Несколько разрешено |
| Псевдонимы | Не включены | Включены |
| Сайд-проекты | Отдельно | Включены |
| Коллаборации | Отдельно | Включены |
## Формат заголовка
### Дискография:
```
[Artist] - Дискография (15 CD) [19902020, Rock, MP3, 320 kbps]
[Artist] - Discography (30 releases) [19852023, Electronic, FLAC]
```
### Коллекция:
```
[Artist] - Коллекция (50 CD) [19802019, Various, FLAC]
[Artist] - Collection (100 albums) [19752025, Rock, MP3, VBR]
```
## Правила сайд-проектов
### Когда включать в коллекцию:
- Артист "определяет лицо" коллектива
- Артист - основной автор/вокалист
- Проект широко ассоциируется с артистом
### Когда выделять отдельно:
- Равноправное участие нескольких артистов
- Артист - приглашённый участник
- Проект имеет собственную идентичность
## Неофициальные релизы
🚫 **Запрещено** включать в дискографии/коллекции:
- Бутлеги
- Фан-компиляции
- Неофициальные сборники
- Самодельные ремастеры
**Разрешено**:
- Официальные альбомы
- Официальные синглы и EP
- Официальные компиляции
- Официальные переиздания
## Формат папок
```
Artist Name/
├── 1990 - Album One/
├── 1992 - Album Two/
├── 1995 - Album Three (Remaster 2010)/
└── 2020 - Latest Album/
```
## Требования к оформлению
### Обязательно для каждого релиза:
- Название альбома
- Год выпуска
- Каталожный номер (если есть)
- Жанр
- Треклист
- Продолжительность
### Под спойлером для каждого альбома:
```bbcode
[spoiler="Album Name (Year)"]
Каталог: LABEL001
Жанр: Rock
Продолжительность: 45:30
Треклист:
01. Track One (4:30)
02. Track Two (5:15)
...
[/spoiler]
```
## Поглощение
- Дискография поглощает отдельные альбомы того же качества
- Коллекция поглощает дискографию + сайд-проекты
- Более качественная версия поглощает менее качественную
## Паттерны для парсера
```regex
# Дискография
[Дд]искографи[яи]|[Dd]iscograph(?:y|ies)
# Коллекция
[Кк]оллекци[яи]|[Cc]ollection
# Количество релизов
\((\d+)\s*(?:CD|albums?|releases?|релиз(?:а|ов)?|альбом(?:а|ов)?)\)
# Диапазон годов (с длинным тире)
\[(\d{4})[-](\d{4})
# Жанр в скобках
,\s*([A-Za-z\s,/]+),
```
+273
View File
@@ -0,0 +1,273 @@
# Общие правила публикации и оформления раздач RuTracker
**Источник:** https://rutracker.org/forum/viewtopic.php?t=6372776
**Версия:** © 2023
**Последнее обновление:** 08-Дек-25
Действует для категорий: Музыка, Популярная музыка, Джазовая и Блюзовая музыка, Рок-музыка, Электронная музыка, Hi-Res форматы, оцифровки.
---
## 1. Введение
### 1.1 Связанные правила
- [Правила для lossless](https://rutracker.org/forum/viewtopic.php?t=6372775)
- [Правила для lossy (MP3)](https://rutracker.org/forum/viewtopic.php?t=6372774)
- [Правила для Hi-Res](https://rutracker.org/forum/viewtopic.php?t=5093969)
- [Правила оцифровок](https://rutracker.org/forum/viewtopic.php?t=3558517)
---
## 2. Подготовка к созданию раздачи
### 2.1 Проверка жанра и формата
Убедитесь в соответствии выбранному подразделу.
### 2.2 Поиск повторов
**Обязательно** проверить через поиск по трекеру.
Повтор - материал, не отличающийся в лучшую сторону по качеству.
**Не считается повтором:**
- lossy при наличии lossless и наоборот
- Различные варианты мастеринга одного альбома
### 2.3 Запрещённые раздачи
- 🚫 Сборники собственного составления
- 🚫 WEB-сборники для стриминга (Deezer, Spotify)
- 🚫 Единичные треки, не изданные официально
- ✅ Любительские ремастеринги - в соответствующем подразделе
- ✅ AI-музыка - с дополнительными требованиями
### 2.4 Немузыкальный материал
🚫 Интервью - только в подразделе Аудио.
### 2.5 Смешение форматов
🚫 Запрещено объединять lossless и lossy (кроме официальных изданий).
### 2.6 Смешение Hi-Res и стандартного качества
🚫 Запрещено объединять Red Book с Hi-Res и оцифровками.
### 2.7 Неполные релизы
🚫 Запрещены неполные релизы и отдельные части многодисковых изданий.
### 2.8 Транскоды (апконверты)
🚫 Запрещены:
1. lossy → lossless
2. lossy → другой lossy кодек
3. Перекодирование с изменением битрейта
4. lossless низкого качества → lossless высокого (16/44.1 → 24/96)
---
## 3. Требования к содержимому
### 3.1 Запрещённые форматы образов
🚫 *.mdf/*.mds, *.iso (кроме iso.wv), *.nrg, *.bin+*.cue
🚫 Архивы (*.rar, *.zip)
### 3.2 Запрещённые файлы
🚫 *.exe, *.com, *.nfo, *.sfv
🚫 Файлы с рекламой сторонних ресурсов
✅ Плейлисты *.m3u, *.m3u8
### 3.3 Название папки
**Единичный релиз:**
```
Артист - Год - Альбом
Артист - Альбом - (Год)
```
❌ Неправильно: "Артист - Альбом", "ГодАльбом"
**Дискография:**
```
Артист/
├── Год - Альбом/
├── Год - Альбом/
```
### 3.4 Название файлов
**Альбом одного исполнителя:**
```
01 - Название
01. Название
A1 - Название (для винила)
```
❌ Неправильно: "Название - 01", "01Название"
**Сборник разных исполнителей:**
```
01. Исполнитель - Название
01 - Исполнитель - Название
```
**Нумерация:**
- 10+ треков: 01, 02 ... 09, 10
- 100+ треков: 001, 002 ... 099, 100
### 3.5 Теги
**Обязательные для статуса "проверено":**
- Артист
- Номер трека
- Название песни
- Альбом
- Год
**Для image+.cue:**
- REM DATE
- TITLE
- PERFORMER
- TRACK
Рекомендуемая кодировка: **UTF-8**
### 3.6 Соответствие треклисту
❌ "Track01", "Дорожка 1", "Unknown Artist"
✅ Исключение: промо-диски и миксы без треклиста
### 3.7 Транслитерация
🚫 Запрещена для кириллицы
✅ Допускается для иероглифов
### 3.8 Длина пути
Рекомендуется лаконичное именование (~255 символов ограничение ОС).
**Для image+.cue:**
```
CD.flac, CD.cue, CD.log
```
**Для tracks:**
```
02. What Have I Done ... Pt. 1.flac
```
(полное название в теге)
---
## 4. Требования к оформлению
### 4.1 Шаблон
Строго заполнять обязательные поля.
### 4.2 Форматирование
🚫 Нижние подчёркивания, точки (кроме пунктуации)
🚫 CAPS LOCK (кроме оригинального названия)
🚫 Нестандартные шрифты в технических полях
**Технические данные:**
```bbcode
[spoiler="Название"][pre]Текст[/pre][/spoiler]
```
### 4.3 Заголовок темы
Точное название на языке оригинала.
Дублировать латиницей при умляутах/диакритике.
### 4.4 Содержание заголовка
**Единичный релиз:**
- Исполнитель
- Год
- Название
- Формат
- Битрейт
**Сборная раздача:**
- Исполнитель
- Количество релизов
- Годы (1999-2010)
- Форматы (через запятую)
- Битрейт (диапазон: 128-320 kbps)
### 4.5 Треклист
**Обязателен.** Должен соответствовать содержанию.
### 4.6 Формат треклиста
🚫 Скриншоты
🚫 Содержание CUE-файлов
🚫 Обратная сторона обложки
### 4.7 Несколько релизов
Каждый под отдельный спойлер с:
- Техническими данными
- Треклистом
- Обложкой
### 4.8 Транслитерация треклиста
🚫 Для кириллицы
✅ Для иероглифов
### 4.9 Обложка
**Обязательна** (кроме релизов без обложки).
**Требования:**
- Размер: 200x200 - 600x600 px (макс. площадь 360000)
- Файл: до 600 KB
- 🚫 Анимация
- 🚫 Реклама
- Дополнительные обложки под спойлер
### 4.10 Хостинг изображений
Без регистрации и паролей.
Скриншоты спектров - масштабные превью.
### 4.11 Дополнительная информация
Более 8-10 строк - под спойлер.
### 4.12 Продолжительность
**Обязательно указывать** для каждого релиза.
### 4.13 Заголовок
🚫 "Обновлено 01.01.2022"
### 4.14 Денежные переводы
🚫 Номера счетов и кошельков.
### 4.15 Ссылки
🚫 Сторонние ресурсы
✅ Исключения:
- Официальный сайт исполнителя
- Официальный сайт лейбла
- Discogs и каталогизаторы
- Wikipedia
- YouTube
- WEB-магазины для WEB-релизов
### 4.16 Перезалив торрента
Обязательно сообщение о причинах.
---
## История изменений
- **24.11.2023** - пп. 3.4.1 и 4.9
- **03.01.2024** - п. 2.3
- **17.06.2024** - п. 3.7
- **17.01.2025** - пп. 2.3 и 4.2
- **08.12.2025** - п. 4.12
## Паттерны для парсера
```regex
# Стандартный заголовок
^(?:\([^)]+\)\s*)?(.*?)\s*-\s*(.*?)\s*-\s*(\d{4})
# Формат с годом
(\d{4})\s*-\s*(\d{4})|(\d{4})
# Битрейт
(\d+)\s*kbps|V[012]|lossless
# Формат
FLAC|APE|MP3|AAC|OGG|WV
# Тип рипа
image\+\.?cue|tracks\+\.?cue|tracks
```
+163
View File
@@ -0,0 +1,163 @@
# RuTracker Hi-Res Audio Upload Rules
**Source:** https://rutracker.org/forum/viewtopic.php?t=5093969
## 1. Допустимые форматы Hi-Res (Section 2.1.1)
### Стереорелизы из исходников высокого разрешения Hi-Res:
**PCM форматы:**
- **24(32)/192** (24 или 32 бит / 192 кГц)
- **24(32)/176.4** (24 или 32 бит / 176.4 кГц)
- **24(32)/96** (24 или 32 бит / 96 кГц)
- **24(32)/88.2** (24 или 32 бит / 88.2 кГц)
- **24(32)/48** (24 или 32 бит / 48 кГц)
- **24(32)/44.1** (не относится к Hi-Res, но допускается)
- **16/48** (не относится к Hi-Res, но допускается)
**DSD форматы (однобитные):**
- **DSD64** (1 bit / 2.8224 MHz) - базовый формат SACD
- **DSD128** (1 bit / 5.6 MHz) - в 128 раз выше 44.1 кГц
- **DSD256** (DSD265 в документе)
- **DSD512**
## 2. Правила оформления заголовка (Section 3.3-3.5)
### Для официальных Hi-Res релизов:
**Формат заголовка темы:**
```
[TR24][OF] Исполнитель - Название Альбома - Год (Жанр)
```
**Теги:**
- `[TR24]` - стереотреки повышенного и высокого разрешения (24 бит и выше)
- `[OF]` - официальный релиз (official)
### Для оцифровок с аналоговых носителей (винил, магнитные ленты):
**Формат заголовка:**
```
(Жанр) [Источник] [Формат] Исполнитель - Название Альбома - Год, Кодек (тип рипа)
```
**Примеры:**
```
(Hard Rock)[LP][24/96] Whitesnake - Slip Of The Tongue - 1989, WavPack (image+.cue)
(Pop) [LP] [24/96] Hi-Fi - Лучшее (2 LP) - 2015, FLAC (image+.cue)
(Score/Soundtrack) [LP] [24/96] Matt Uelmen 2022 "Diablo II: Resurrected", FLAC (image+.cue)
(Rock Opera) [LP] [24/192] Various (Shiki Theatrical Company) Jesus Christ Superstar (in Japanese) - 1976, FLAC (image+.cue)
```
## 3. Теги источников (Source Tags)
- `[LP]` - винил, пластинка диаметром 12 дюймов, записанная на 33-й скорости
- `[EP]` - Extended Play (мини-альбом)
- `[7"]` - сингл 7 дюймов
- `[2LP]` - двойной винил
- `[LP MONO]` - моно запись с винила
- `[DVDA]` - DVD-Audio формат
- `[SACD]` - Super Audio CD
- `[DSD]` - Direct Stream Digital формат
- `[HDAD]` - High Definition Audio Disc (DVDA с 24/192 или 24/176.4)
## 4. Формат Bit Depth / Sample Rate
**Обозначение в квадратных скобках:**
- `[24/96]` - 24 бит / 96 кГц
- `[24/192]` - 24 бит / 192 кГц
- `[24/48]` - 24 бит / 48 кГц
- `[24/88.2]` - 24 бит / 88.2 кГц
- `[24/176.4]` - 24 бит / 176.4 кГц
- `[16/48]` - 16 бит / 48 кГц
**Формат: [бит/кГц]**
## 5. DSD Notation (Section 2.1.1, 2.4.3)
**Форматы DSD:**
```
DSD64 (1 bit / 2.8224 MHz) - базовый SACD формат
DSD128 (1 bit / 5.6 MHz) - удвоенная частота
DSD256 (1 bit / 11.2 MHz)
DSD512 (1 bit / 22.4 MHz)
```
**Альтернативная запись:**
- `DST64` / `DST128` - lossless сжатие DSD (формат DST - MPEG-4 audio)
## 6. Название папки в раздаче (Section 3.4)
**Формат:**
```
Имя Исполнителя - Название Альбома - (Год - Разрядность-Частоту)
```
**Пример:**
```
Whitesnake - Slip Of The Tongue - (1989 - 24-96)
```
## 7. Обязательные требования к оформлению
### Dynamic Range Meter (DR meter) - Section 3.7:
Обязательно публиковать лог DR под спойлером:
```
[spoiler="Динамический диапазон (DR)"][pre]
Содержимое лога DR
[/pre][/spoiler]
```
### Спектры и графики (для оцифровок) - Section 3.3:
Обязательно показать:
- Не менее одного изображения спектра
- АЧХ (амплитудно-частотная характеристика)
- Уровня записи
## 8. Запрещенные форматы (Section 2.2)
1. **WAV-файлы (PCM Wave), не упакованные lossless-кодеками**
- Степень сжатия FLAC должна быть не менее 5 (рекомендуется 8)
- Для DSD форматом lossless-сжатия является DST (.dff) или WavPack (.wv)
2. **Релизы с разной частотой семплирования** (Section 2.5.1)
3. **Апконверты** (Section 5.2)
- Апконверты 16 > 24 бит и 44.1 > 48 кГц получают статус "# сомнительно"
## 9. Дополнительные теги
- `[RM]` - материал подвергался обработке (Remastered)
- `[restored]` - исправление технических недостатков
- `[declipped]` - удаление клиппинга
## Parser Patterns
```regex
# Official Hi-Res
\[TR24\]\[OF\]
# Vinyl/Analog Digitizations
\[LP\]\s*\[24/\d+\]
\[EP\]\s*\[24/\d+\]
# Bit depth / Sample rate
\[24/96\]
\[24/192\]
\[24/48\]
\[24/88\.2\]
\[24/176\.4\]
# DSD formats
DSD64|DSD128|DSD256|DSD512
DST64|DST128
\[DSD\]
```
## Additional References
- Теги для оцифровок: https://rutracker.org/forum/viewtopic.php?t=2672956
- Правила оцифровок: https://rutracker.org/forum/viewtopic.php?t=3558517
- Общие правила музыкальных категорий: https://rutracker.org/forum/viewtopic.php?t=6372776
+180
View File
@@ -0,0 +1,180 @@
# Интерактивное содержание правил музыкальных категорий RuTracker
**Источник:** https://rutracker.org/forum/viewtopic.php?t=2973838
**Актуально на:** 11.06.2023
---
## Общие правила
| Документ | Topic ID | Локальный файл |
|----------|----------|----------------|
| Общие правила публикации и оформления раздач | t=6372776 | [general.md](general.md) |
| Правила для lossless подразделов | t=6372775 | [lossless.md](lossless.md) |
| Правила для lossy (MP3) подразделов | t=6372774 | [lossy.md](lossy.md) |
| Правила оцифровок с аналоговых носителей | t=3558517 | [vinyl-digitization.md](vinyl-digitization.md) |
| Правила для Hi-Res музыки | t=5093969 | [hi-res.md](hi-res.md) |
| Правила оформления Лейбл-Паков | t=3746663 | [label-packs.md](label-packs.md) |
---
## Специальные правила
| Документ | Topic ID | Локальный файл |
|----------|----------|----------------|
| Разъяснения по дискографиям и коллекциям | t=6372771 | [discography.md](discography.md) |
---
## Локальные правила по жанрам
| Раздел | Topic ID | Локальный файл |
|--------|----------|----------------|
| Классическая музыка | t=773016 | [classical.md](classical.md) |
| Саундтреки | t=2090617, t=2044619 | [soundtracks.md](soundtracks.md) |
| Джаз | t=3510475 | [jazz.md](jazz.md) |
| Зарубежный Metal | t=4481510 | [metal.md](metal.md) |
| Неофициальные сборники (lossless) | t=5555404 | - |
| Неофициальные сборники (lossy) | t=2661504 | - |
---
## Структура общих правил (t=6372776)
### 1. Введение
- 1.1 О музыкальных разделах трекера
- 1.2 На какие разделы распространяются правила
- 1.3 Таблица публикации по форматам
- 1.4 О дополнительных файлах
### 2. Требования к качеству
- 2.1 Источники музыки и форматы
- 2.2 Проверка на повтор
- 2.3 Запрет апконвертов
- 2.4 Запрет автоматического ремастеринга
- 2.5 Запрет смешения lossless и lossy
- 2.6 Запрет смешения Red Book и Hi-Res
- 2.7 Запрет сборников разных жанров
- 2.8 Запрет дискографий (с исключениями)
### 3. Требования к файлам
- 3.1 Запрет data-дорожек
- 3.2 Запрет дефектных файлов
- 3.3 Требования к названию папки
- 3.4 Требования к названию файлов
- 3.5 Обязательные теги
- 3.6 Запрет Track01/Unknown Artist
- 3.7 Запрет транслитерации
- 3.8 Требования к году и обложке
### 4. Требования к оформлению
- 4.1 Заполнение шаблона
- 4.2 Запрет CAPS LOCK
- 4.3 Язык названия
- 4.4 Содержание заголовка
- 4.5 Указание жанра
- 4.6 Транслитерация
- 4.7 Многодисковые издания
- 4.8 Соответствие жанру
- 4.9 Требования к обложке
- 4.10-4.16 Дополнительные требования
---
## Структура правил lossless (t=6372775)
### 2. Требования к материалу
- 2.1 Допустимые форматы (Red Book 16/44.1)
- 2.2 Запрет несжатых WAV
- 2.3 Рекомендуемые программы (EAC, XLD)
- 2.4 Требования к логам
- 2.5 Типы рипов (image+cue, tracks+cue)
- 2.6 Enhanced CD требования
### 3. Оформление
- 3.1 Публикация LOG
- 3.2 Отчёт DR
- 3.3 Указание источника
- 3.4 Публикация CUE
- 3.5 ISO-контейнеры
- 3.6 Запрет замены логов ссылками
### 4. WEB-релизы
- 4.1 Определение
- 4.2 Обязательные требования
- 4.3 Поглощение CD-рипами
### 5. AI-музыка
- 5.1 Определение
- 5.2 Требования (тег [AI], ссылка, DR, "as is")
---
## Структура правил Hi-Res (t=5093969)
### 2. Требования к материалу
- 2.1 Допустимые форматы Hi-Res
- 2.1.1 PCM: 24/48 - 24/192
- 2.1.2 WEB-релизы
- 2.1.3 DVD с LPCM/DSD
- 2.1.4 Многоканальные: SACD, Blu-Ray, DVD-Audio
- 2.2 Запрещённые форматы
- 2.3 Рекомендации по ISO
- 2.4 DTS и извлечённые треки
- 2.5 Не считаются повтором
### 3. Оформление
- 3.1-3.2 Шаблон
- 3.3 Теги в заголовке ([TR24], [OF], etc.)
- 3.4 Название папки
- 3.5 Название файлов
- 3.6 Ссылка на магазин
- 3.7 DR meter
- 3.8 Техническая информация
### 4. Программы
- 4.1 Для DSD
- 4.2 Для остальных форматов
---
## Ключевые URL
```
# Базовый URL
https://rutracker.org/forum/viewtopic.php?t=
# Основные правила
6372776 - Общие правила
6372775 - Lossless
6372774 - Lossy
3558517 - Оцифровки
5093969 - Hi-Res
# Специальные
6372772 - Статус "Сомнительно"
6372771 - Дискографии
3746663 - Лейбл-паки
# Жанровые
773016 - Классика
3510475 - Джаз (стили)
4481510 - Metal (lossless)
2090617 - Саундтреки (жанры)
```
---
## Скачанные документы
1. ✅ [general.md](general.md) - Общие правила
2. ✅ [lossless.md](lossless.md) - Lossless правила
3. ✅ [lossy.md](lossy.md) - Lossy правила
4. ✅ [vinyl-digitization.md](vinyl-digitization.md) - Оцифровки
5. ✅ [hi-res.md](hi-res.md) - Hi-Res правила
6. ✅ [label-packs.md](label-packs.md) - Лейбл-паки
7. ✅ [discography.md](discography.md) - Дискографии
8. ✅ [classical.md](classical.md) - Классика
9. ✅ [soundtracks.md](soundtracks.md) - Саундтреки
10. ✅ [jazz.md](jazz.md) - Джаз
11. ✅ [metal.md](metal.md) - Metal
+263
View File
@@ -0,0 +1,263 @@
# Правила оформления раздач в разделе ДЖАЗА RuTracker
**Источник:** https://rutracker.org/forum/viewtopic.php?t=1412059
**Дополнительные источники:**
- t=1080830 (Требования к раздачам в разделе Джаз и Блюз)
- t=3510475 (Требования по использованию тэгов стилей)
---
## 1. Что можно размещать
### 1.1 Обязательно использовать поиск
Проверить дубликаты по исполнителю и названию.
### 1.2 Названия
На оригинальном языке, без перевода.
Для нелатинских языков - транскрипция в скобках.
### 1.3 Сборники
**Запрещены неофициальные сборники:**
- "Все песни артиста"
- "Все хиты за N-ный год"
- "Танцевальная музыка года"
**Требования к сборникам:**
- Размер: до 700 МБ (lossy)
- Треков: до 200
**Разрешены:**
- Официальные компиляции
- Тематические сборники жанра
---
## 2. Технические требования
### 2.1 Форматы Lossless
**Запрещены:**
- Несжатые образы: WAV, NRG, MDF/MDS
- Архивы: RAR, ZIP
- Исполняемые файлы: EXE, COM
**Рекомендуемые кодеки:**
- FLAC
- APE
- WavPack
### 2.2 Типы рипов
1. **Image+CUE+LOG** - полный образ
2. **Tracks+Non-compliant CUE+LOG** - потреково
### 2.3 LOG-файл
Создавать **EAC** (Exact Audio Copy).
```bbcode
[spoiler="Отчет EAC"]
Содержимое лог-файла
[/spoiler]
```
**Если источник неизвестен:**
```
Рип сделан: неизвестно
Источник: название источника
```
+ скриншот спектра Tau Analyzer или Adobe Audition.
### 2.4 Запрещённые режимы
🚫 Burst (скоростной)
🚫 Paranoid (параноидальный)
🚫 Normalize (нормализация)
✅ Только **Secure** (безопасный)
### 2.5 Апконверты
3 апконвертированных трека = повтор.
Проверять через **Tau Analyzer**.
---
## 3. Оформление раздачи
### 3.1 Запреты в названии
🚫 Только ВЕРХНИЙ РЕГИСТР
🚫 Точки (.) кроме пунктуации
🚫 Нижние подчёркивания (_) вместо пробелов
🚫 Жанр в названии (указывается в поле шаблона)
### 3.2 Обложка
- Размер: **500×500 px** (рекомендуется)
- Минимум: **200×200 px**
- 🚫 Реклама сторонних ресурсов
- Сканирование: 300-600 dpi
### 3.3 Лейбл
По-английски или транслитерация:
- `Blue Note`
- `Melodiya` или `Мелодия`
### 3.4 Информация о записи
**Крайне желательно для джаза:**
```
Recorded: December 9, 1965
Studio: Van Gelder Studio, Englewood Cliffs, NJ
Personnel:
John Coltrane - tenor saxophone
McCoy Tyner - piano
Jimmy Garrison - bass
Elvin Jones - drums
```
---
## 4. Тэги стилей (обязательно!)
**"Jazz" недостаточно** - необходимо указывать конкретный стиль.
### Early Jazz, Swing, Gypsy
- Boogie-Woogie
- Ragtime
- Dixieland
- Classic Jazz
- New Orleans Jazz
- Big Band
- Swing
- Gypsy
### Bop
- Bop
- Hard Bop
- Post-Bop
- Neo-Bop
- Modal Music
- Modern Big Band
### Mainstream Jazz, Cool
- Mainstream Jazz
- Cool
- West Coast Jazz
- Soul-Jazz
- Standards
### Jazz Fusion
- Fusion
- Jazz-Rock
- Jazz-Funk
- Jazzy Blues
### World Fusion, Ethnic Jazz
- World Fusion
- Latin Jazz
- Bossa Nova
- Afro-Cuban Jazz
- Brazilian Jazz
- Tango Nuevo
### Avant-Garde Jazz, Free Improvisation
- Avant-Garde Jazz
- Free Jazz
- Free Funk
- Experimental Jazz
- M-Base
### Modern Creative, Third Stream
- Modern Creative
- Third Stream
- Chamber Jazz
- Progressive Jazz
### Smooth, Jazz-Pop
- Smooth Jazz
- Crossover Jazz
- Jazz-Pop
- Easy Listening
### Vocal Jazz
- Vocal Jazz
- Acapella
### Funk, Soul, R&B
- Funk
- Soul
- R&B
- Gospel
---
## 5. Треклист
### 5.1 Обязательное содержание
- Порядковый номер
- Название
- Продолжительность (желательно)
Для сборников - исполнитель для каждого трека.
### 5.2 50+ треков - под спойлер
```bbcode
[spoiler="Треклист"]
01. Track Name (5:23)
...
[/spoiler]
```
### 5.3 Битрейт
Если разный - указывать для каждого трека.
---
## 6. Название папки
```
Artist Name - Album Title (Year) [Format]
```
Пример:
```
John Coltrane - A Love Supreme (1965) [FLAC]
```
---
## 7. Источник
```
Источник: What.CD
Релизёр: username
```
---
## Паттерны для парсера
```regex
# Джаз стили
(?:Hard |Post-|Neo-)?Bop|Swing|Dixieland|Big\s*Band|Fusion|Cool|Latin\s*Jazz|Bossa\s*Nova|Free\s*Jazz|Smooth\s*Jazz|Soul[\s-]?Jazz|Avant[\s-]?Garde
# Personnel формат
Personnel:\s*\n((?:[^\n]+\s*-\s*[^\n]+\n?)+)
# Recorded дата
Recorded:\s*([A-Za-z]+\s+\d+,?\s+\d{4})
# Studio/Location
(?:Studio|Location):\s*([^\n]+)
# Название папки
([^-]+)\s*-\s*([^(]+)\s*\((\d{4})\)\s*\[([A-Z]+)\]
```
+185
View File
@@ -0,0 +1,185 @@
# Правила оформления раздач Лейбл Паков (Label Pack)
**Источник:** https://rutracker.org/forum/viewtopic.php?t=3746663
---
## Важное примечание
**Создавать раздачу необходимо в жанровом подразделе, в раздел Лейбл Паков она будет перенесена модератором после проверки**
---
## Перед созданием раздачи вы должны:
**1.** Проверить по поиску, не существует ли аналогичная раздача на трекере
[Как пользоваться Поиском?](https://rutracker.org/forum/viewtopic.php?t=101236)
**2.** Ознакомиться с правилами оформления раздач в разделе Музыка
- [Общие правила оформления раздач в разделе Музыка](https://rutracker.org/forum/viewtopic.php?t=6372776)
- [Правила публикации и оформления раздач в lossless разделах категорий Музыка, Рок-музыка, Электронная музыка](https://rutracker.org/forum/viewtopic.php?t=6372775)
---
## Что такое Лейбл Пак?
**Лейбл Пак (Label Pack)** - это сборная раздача, включающая в себя релизы музыкантов, изданные одним лейблом и его суб-лейблами.
---
## Оформление заголовка
Заголовок - это краткая информация по всей вашей раздаче, поэтому заголовок темы должен быть максимально информативным.
### Формат заголовка:
```
(Жанр) Label: Название лейбла (количество релизов), - диапазон лет (Аудиокодек), битрейт (тип рипа)
```
### Компоненты заголовка:
- **(Жанр)** - в скобках указывается жанр, обычно сюда пишется 3 жанра, которые являются преимущественными для вашей раздачи
- **Название лейбла** - Указание на то, что раздача принадлежит к числу Лейбл паков
- Например: `Label: Acroplane Recordings`
- **(количество релизов)** - укажите общее количество альбомов, которые содержит ваша раздача
- **диапазон лет, за период которые собраны релизы**
- Пример: `2000-2010 г.`
- **Аудиокодек** - Кодек, которым сжаты ваши аудиофайлы, самый распространённый - это `.flac` (`*FLAC`). Если в раздаче треки сжаты несколькими кодеками, то их нужно указать через запятую
- **битрейт** - качество релизов в вашей раздаче (для lossless достаточно так и указать - `lossless`)
- **Тип рипа** - потрековый или образом, если типов рипа несколько, то они указываются через запятую
### Пример оформленного заголовка:
```
(IDM, Experimental, Ambient) Label: 33 Recordings (55 releases), 2008 - 2011, (FLAC) lossless (tracks+.cue)
```
---
## Создание и оформление раздачи
### Требования к раздаваемому материалу
#### Обязательное требование к названию папки
Каждый релиз должен быть помещён в отдельную папку, в названии которой должны быть указаны:
```
[каталожный номер] Название исполнителя - Название альбома (год)
```
**Важно:**
- Каталожный номер всегда должен быть прописан в начале названия папки
- Все папки в раздаче должны быть приведены к одному виду. Если, к примеру, вы решили указывать год в круглых скобках, то это должно быть применено для каждой папки. Это так же касается сцен-релизов.
---
### Требования по оформлению раздач
Каждый альбом в раздаче должен быть представлен под отдельным спойлером, в который должна быть включена следующая информация:
1. Название исполнителя
2. Название альбома
3. Год выхода альбома
4. Каталожный номер
5. Жанр
6. Треклист
7. Обложка (Если существует)
8. Битрейт
---
### Пример оформления альбома
```
[spoiler="[SEMANTICA 05] Various - Prologue [2008] - CD"]
[IMG=right]http://i6.imageban.ru/out/2017/09/01/f829816b7a97df780abb366bfcd93792.jpg[/IMG]
[font=mono1][b]Жанр: [/b] Techno, IDM, Dub Techno
[b]Продолжительность[/b]:00:58:58
[b]01.[/b] Julien Neto Reprise [i](03:58)[/i]
[b]02.[/b] Arcanoid Sad (Talking About) [i](07:02)[/i]
[b]03.[/b] Acid Future Overdose 99926 [i](08:10)[/i]
[b]04.[/b] Jimmy Edgar Warm Play Look Away [i](05:12)[/i]
[b]05.[/b] Oscar Mulero Paris, Texas [i](06:32)[/i]
[b]06.[/b] Ed Chamberlain Does Ape [i](06:07)[/i]
[b]07.[/b] Ideograma Alert S.E.T.I. Home [i](06:19)[/i]
[b]08.[/b] Plant43 Extrasolar [i](04:38)[/i]
[b]09.[/b] Annie Hall Wine & Beats [i](04:50)[/i]
[b]10.[/b] Svreca Eye (DisinVectant Optic Nerve Remix) [i](06:10)[/i]
[hr]
[url=https://www.discogs.com/Various-Prologue/release/1521766][img]http://i59.fastpic.ru/big/2013/0914/11/277b80b65490a21939363761aa55d111.png[/img][/url]
[spoiler="Лог создания рипа"][pre]
Exact Audio Copy V0.99 prebeta 5 from 4. May 2009
EAC extraction logfile from 18. November 2010, 17:14
...
[/pre][/spoiler]
[spoiler="Содержание индексной карты (.CUE)"][pre]
REM GENRE Electronic
REM DATE 2008
REM DISCID 880DD10A
...
[/pre][/spoiler]
[/spoiler]
```
---
## Дополнительные требования
### Обязательно:
- **Обязательно** для лейбл паков указывать **порелизовую продолжительность** (продолжительность звучания каждого релиза), так как бывают различные вариации альбомов, когда набор треков одинаков, но длительность бывает различна.
- **Обязательно** указание **потрековой продолжительности** для веб релизов, входящих в состав раздачи
- **Обязательно** указание жанровой принадлежности каждого альбома, так как далеко не все лейблы специализируются на каком-то определенном жанре.
- **Допускается** не указывать жанровую принадлежность для каждого альбома в случаях, когда она одинакова для всех альбомов в раздаче
### Приветствуется:
- **Приветствуется** указание общей продолжительности звучания всех альбомов в раздаче
- **Приветствуется** указание потрековой продолжительности всех альбомов, входящих в состав раздачи, включая CD и Vinyl рипы
- **Приветствуется** указание типа носителя, с которого был снят рип: CD, Vinyl, Cassette, WEB
---
## Дополнительная информация
- В случае, если оформление раздачи превышает допустимый лимит символов в сообщении, то вы можете продолжить оформление в следующем сообщении.
- [Пример оформленной раздачи](https://rutracker.org/forum/viewtopic.php?t=4542920)
- [Ускорение оформления сборных раздач (дискографий, коллекций) при помощи универсальной программы редактирования тегов - mp3tag](https://rutracker.org/forum/viewtopic.php?t=3024659)
- **Одиночные раздачи альбомов, входящих в состав Лейбл пака, не поглощаются**
- **В случае обновления старой раздачи, новая раздача должна полностью соответствовать всем требованиям по оформлению раздач лейбл паков**
---
## Особенности lossless Лейбл паков
Оформление lossless Лейбл пака принципиально не отличается от оформления lossy лейбл пака, за исключением обязательности публикации:
- Логов снятия рипа
- cue-sheet
- Или логов проверки качества (при условии, если лог снятия рипа отсутствует)
---
**Документ создан:** 2026-05-04
**Последнее обновление на RuTracker:** 06-июн-20 22:39
+171
View File
@@ -0,0 +1,171 @@
# Правила для lossless подразделов RuTracker
**Источник:** https://rutracker.org/forum/viewtopic.php?t=6372775
**Версия:** © 2023
**Последнее обновление:** 05.11.2025
## 1. Введение
Настоящие правила являются дополнительными по отношению к правилам, изложенным в общей части, и описывают специфические требования к публикации и оформлению раздач в lossless-разделах категорий форума Музыка, Популярная музыка, Джазовая и Блюзовая музыка, Рок-музыка, Электронная музыка.
## 2. Требования к раздаваемому материалу
### 2.1 Допустимые форматы и источники
В lossless-подразделах раздаётся исключительно:
- **Рипы с CD**
- **WEB-релизы** с характеристиками **16 бит / 44100 Гц** (стандарт Red Book)
**Исключение:** Если релиз содержит 50% и более материала, отличного от Red Book, его следует оформлять в Hi-Res подразделах.
### 2.2 Требования к упаковке
**Категорически запрещается** раздача WAV-файлов, не упакованных lossless-кодеками.
**Степень сжатия FLAC:** не менее 5 (рекомендуется 8)
> ⚠️ Не путать WavPack (*.wv) с PCM Wave (*.wav)
### 2.3 Запрет на виртуальные приводы
**Категорически запрещается** публикация рипов с виртуальных приводов.
### 2.4 Рекомендуемые программы
- **EAC (Exact Audio Copy)** - для Windows и Linux (Wine)
- **XLD (X Lossless Decoder)** - для Mac OS
Только рипы по официальным инструкциям получают статус "проверено".
### 2.5 Равнозначные форматы раздач
Равнозначными считаются:
1. Рипы **image+cue+log**
2. Рипы **tracks+non-compliant cue+log**
Потрековая раздача = повтор при наличии образа, и наоборот.
### 2.6 Равнозначность lossless-кодеков
Все lossless-кодеки равнозначны (FLAC, APE, WavPack, etc.). Рип в одном кодеке не поглощает рип в другом - это повторы.
### 2.7 Enhanced CD (Extra CD)
**Допускается** включать бонусы (видеоклипы, mp3, flash) если они были на оригинальном диске.
Требования:
- 2-3 скриншота для каждого видеофайла
- Отчёт MediaInfo под спойлером
## 3. Требования к оформлению
### 3.1 Публикация лога EAC/XLD
```bbcode
[spoiler="Лог создания рипа"][pre]содержание лог-файла[/pre][/spoiler]
```
### 3.2 Отчёт о динамическом диапазоне (DR)
Обязателен для раздач без лога извлечения (WEB-релизы, авторский материал):
```bbcode
[spoiler="Динамический отчет (DR)"][pre]Содержимое лога[/pre][/spoiler]
```
**Программы для DR:**
- **Windows:** foobar2000 + Dynamic Range Meter plugin
- **Linux:** DeaDBeeF + dr-meter plugin
- **Mac OS:** foobar2000 + plugin
### 3.3 Указание источника рипа
Обязательно указать:
- **"Собственный рип"** или **"Сторонний рип"**
- Для сторонних: название ресурса (без ссылки), ник релизера
### 3.4 Публикация CUE Sheet
Независимо от типа рипа:
```bbcode
[spoiler="Содержание индексной карты (.CUE)"][pre]содержание[/pre][/spoiler]
```
### 3.5 ISO-контейнеры
Для рипов в iso.wv - указать перечень файлов контейнера под отдельным спойлером.
### 3.6 Запрет замены логов ссылками
**Не допускается** заменять лог сообщениями типа "лог внутри раздачи".
## 4. WEB-релизы в lossless
### 4.1 Определение
WEB-релиз - материал в lossless через цифровую дистрибуцию:
- Интернет-магазины (Beatport, Juno)
- Сайты лейблов и исполнителей
- Авторские раздачи
### 4.2 Обязательные требования
1. **Указание [WEB]** в заголовке
2. **Ссылка на источник** (магазин, сайт лейбла)
- Если неизвестен: "WEB-магазин: неизвестен"
- Если авторская: "WEB-магазин: материал предоставлен авторами"
3. **Заполнение поля "Источник"**
- Собственная покупка
- Авторская раздача
- Название ресурса
4. **Отчёт DR**
### 4.3 Поглощение WEB CD-рипами
WEB-релиз в Red Book может быть поглощён правильным CD-рипом при совпадении мастеринга.
**Условия совпадения мастеринга:**
- Совпадение DR value / Samplerate / Bits per sample / Channels
- Погрешность Peak: ±0.2 dB
## 5. AI-музыка в lossless
### 5.1 Определение
AI Generated Music (AIGM) - материалы, сгенерированные с помощью ИИ.
### 5.2 Требования
1. **Тег [AI]** в заголовке
2. **Ссылка на источник** (кроме собственной генерации)
3. **Отчёт DR**
4. **Публикация "as is"** - без ремастеринга
## История изменений
- **24.11.2023** — добавлен п. 2.7
- **17.07.2024** — изменены пп. 3.1, 3.2, 4.2.4, 4.3
- **17.01.2025** — удален п. 2.7, добавлен п. 5
- **27.02.2025** — изменен п. 5.2.2, добавлен п. 5.2.4
- **05.11.2025** — изменен п. 4.3
## Паттерны для парсера
```regex
# WEB тег
\[WEB\]
# AI тег
\[AI\]
# FLAC степень сжатия
FLAC\s*(?:level\s*)?([5-8])
# Источник рипа
(?:Собственный|Сторонний)\s+рип
# Red Book формат
16\s*(?:bit|бит).*44[.,]?1\s*(?:kHz|кГц)
```
+104
View File
@@ -0,0 +1,104 @@
# Правила для lossy (MP3) подразделов RuTracker
**Источник:** https://rutracker.org/forum/viewtopic.php?t=6372774
## 1. Введение
Настоящие правила являются дополнительными по отношению к правилам, изложенным в общей части, и описывают специфические требования к публикации и оформления раздач в lossy-разделах категорий форума Музыка, Популярная музыка, Джазовая и Блюзовая музыка, Рок-музыка, Электронная музыка.
Применимые форматы: MP3, AAC, OggVorbis, Musepack, WMA.
## 2. Требования к раздаваемому материалу
### 2.1 Запрещено включать в раздачу:
- Видеоклипы
- Скринсейверы
- Обои
- RAW/lossless сканы обложек
### 2.2 Разрешено включать:
- Аудио бонусы
- Сканы обложек альбома (JPG)
- Пресс-релизы
### 2.3 Обложки в тегах:
- Максимум 1 изображение на трек
- Максимум 300 KB на изображение
## 3. Требования к оформлению
### 3.1 Обозначение битрейта
**CBR (постоянный битрейт):**
- 320 kbps
- 256 kbps
- 192 kbps
- 128 kbps
**VBR (переменный битрейт) - пресеты:**
- V0 (максимальное качество, ~245 kbps)
- V1 (~225 kbps)
- V2 (~190 kbps)
**VBR (переменный битрейт) - диапазон:**
- VBR 192-320 kbps
- VBR 128-320 kbps
**VBR (переменный битрейт) - средний:**
- VBR ~256 kbps (avg)
### 3.2 Многоальбомные релизы
Каждый альбом должен быть под отдельным спойлером с указанием:
- Год выпуска
- Название альбома
- Битрейт
## 4. Правила AI-музыки (добавлено январь 2025)
### 4.1 Обязательный тег
В заголовке раздачи должен быть тег **[AI]**
### 4.2 Ссылка на источник
Обязательно указать ссылку на страницу покупки (кроме самостоятельно сгенерированного материала)
### 4.3 Публикация "as is"
Материал должен публиковаться без ремастеринга, в исходном виде
## Шаблоны заголовков
### Стандартный формат:
```
(Жанр) Исполнитель - Название Альбома - Год, Формат Битрейт
```
### Примеры:
```
(Rock) Pink Floyd - The Wall - 1979, MP3 320 kbps
(Electronic) Daft Punk - Random Access Memories - 2013, MP3 V0
(Pop) Madonna - Like a Prayer - 1989, MP3 VBR 192-320 kbps
```
### С тегом AI:
```
(Electronic) [AI] AI Artist - Generated Album - 2025, MP3 320 kbps
```
## Паттерны для парсера
```regex
# CBR битрейт
(\d{2,3})\s*kbps
# VBR пресеты
V[012]
# VBR диапазон
VBR\s+(\d+)-(\d+)\s*kbps
# VBR средний
VBR\s+~?(\d+)\s*kbps
# AI тег
\[AI\]
```
+189
View File
@@ -0,0 +1,189 @@
# Правила оформления раздач в разделе ЗАРУБЕЖНЫЙ METAL RuTracker
**Источник:** Compiled from RuTracker.org forum rules
**Примечание:** Прямой доступ к t=1412060 требует авторизации
## 1. Формат заголовка
```
(Жанр/Подстиль) Название группы - Название альбома - Год, Аудиокодек, Битрейт
```
### Примеры:
```
(Heavy/Power/Thrash Metal) Metal Church - Dead to Rights - 2026, MP3, 320 kbps
(Black Metal) Darkthrone - A Blaze in the Northern Sky - 1992, FLAC (image+.cue), lossless
(Death Metal, Doom Metal) Paradise Lost - Gothic - 1991, APE (tracks), lossless
```
## 2. Обозначение жанров Metal
### Основные жанры:
- Heavy Metal
- Power Metal
- Thrash Metal
- Speed Metal
- Progressive Metal
- Death Metal
- Black Metal
- Doom Metal
- Gothic Metal
- Symphonic Metal
- Folk Metal
- Viking Metal
- Pagan Metal
- Grindcore
- Metalcore
- Deathcore
### Формат указания:
- Жанры через запятую: `Death Metal, Doom Metal`
- Жанры через слэш: `Heavy/Power Metal`
- В скобках в начале заголовка
## 3. Обязательные элементы
### В заголовке:
1. Жанр (в скобках)
2. Название группы (оригинальное написание)
3. Название альбома
4. Год издания
5. Аудиокодек (MP3, FLAC, APE)
6. Битрейт/тип рипа
### В описании:
1. Жанр
2. Страна группы
3. Год издания
4. Аудиокодек
5. Тип рипа (tracks / image+cue)
6. Битрейт
7. Продолжительность
8. Наличие сканов
9. Треклист
10. Лейбл
## 4. Требования к тегам
### Обязательные теги:
1. **Название песни** - оригинальное написание
2. **Альбом** - с дополнениями (Single, [EP], Live)
3. **Исполнитель** - оригинальное написание (W.A.S.P., не WASP)
4. **Год** - год альбома, не переиздания
5. **Номер трека**
6. **Номер диска** (для многодисковых)
7. **Жанр**
8. **Обложка** (200×200 - 600×600 px)
## 5. Типы релизов
### Форматы альбомов:
- **Studio Album** - студийный альбом
- **Live** - концертная запись
- **EP** - мини-альбом
- **Single** - сингл
- **Compilation** - сборник
- **Demo** - демо-запись
- **Bootleg** - бутлег
- **Remaster** / **Reissue** - ремастер / переиздание
- **Deluxe Edition** - делюкс издание
- **Box Set** - бокс-сет
### Специальные издания:
- **(Japan Edition)**
- **(Limited Edition)**
- **(Digipak)**
- **(Vinyl)**
- **(Anniversary Edition)**
## 6. Требования к обложке
- Разрешение: 200×200 - 500×500 px (описание)
- Для тегов: 200×200 - 600×600 px
- Размер файла: до 300-500 KB
- 🚫 Анимация запрещена
- 🚫 Реклама запрещена
## 7. Треклист
### Формат:
```
01. Название трека (04:30)
02. Название трека (05:15)
```
### Для сборников:
```
01. Исполнитель - Название трека (04:30)
```
Треклисты более 50 треков - под спойлер.
## 8. Локальные правила lossless
### Критерии собственного рипа:
1. Последняя версия EAC или XLD
2. Режим Test & Copy
3. Учёт нулевых семплов включён
4. Полный комплект сканов
5. Ник релизёра в логе
### Пример комментария в логе:
```
Additional command line options : -T "COMMENT=Ripped by [nickname]" -8 -V %s
```
## 9. Дискографии Metal
### Формат папок:
```
[Каталожный номер] Название группы - Название альбома (год)
```
### Требования:
- Единообразие всех папок
- Каждый альбом под спойлером
- Жанр для каждого альбома (если различаются)
## 10. Запреты
### Запрещено раздавать:
- Неполные релизы
- Транскоды (апконверты)
- Сборники собственного составления
- AI-музыку
### Запрещено объединять:
- Lossless и lossy
- Red Book и Hi-Res
- Официальные и неофициальные релизы
### Запрещено в тегах:
- Графика кроме обложки
- "Трек 1", "CD 1" вместо названий
## 11. Статусы раздач
- **✓ Проверено** - соответствует правилам
- **? Сомнительно** - вопросы к качеству
- **! Не оформлено** - не соответствует требованиям
- **Повтор** - дублирует существующую
## Паттерны для парсера
```regex
# Metal жанры
(?:Heavy|Power|Thrash|Speed|Progressive|Death|Black|Doom|Gothic|Symphonic|Folk|Viking|Pagan|Grind|Metal)(?:core)?(?:\s*Metal)?
# Комбинированные жанры
\(([A-Za-z/,\s]+Metal[A-Za-z/,\s]*)\)
# Тип релиза
\[(EP|Single|Live|Demo|Remaster|Reissue|Deluxe Edition|Box Set)\]
# Специальные издания
\((Japan|Limited|Anniversary)\s*Edition\)|\(Digipak\)|\(Vinyl\)
# Название группы (с точками)
([A-Z][A-Za-z.]+(?:\s+[A-Z][A-Za-z.]+)*)
```
+144
View File
@@ -0,0 +1,144 @@
# Правила оформления раздач саундтреков RuTracker
**Источник:** https://rutracker.org/forum/viewtopic.php?t=3119778
## 1. Общие требования
### 1.1 Поиск перед созданием
Обязательно проверить наличие аналогичной раздачи.
### 1.2 Качество
Раздача более низкого битрейта закрывается как повтор при наличии более качественной.
### 1.3 Минимум треков
Не менее 3 треков в раздаче.
### 1.4 Форматы
🚫 Lossless запрещён для неофициальных саундтреков (только официальные саундтреки в lossless).
## 2. Формат заголовка
### 2.1 Soundtrack (песенная компиляция)
```
(Soundtrack / Unofficial) Название фильма (русское) / Original Title - Год, Формат Битрейт
```
### 2.2 Score (оригинальная инструментальная музыка)
```
(Score / Unofficial) Композитор - Название фильма (русское) / Original Title - Год, Формат Битрейт
```
### Примеры:
```
(Soundtrack / Unofficial) Матрица / The Matrix - 1999, MP3 320 kbps
(Score / Unofficial) Hans Zimmer - Начало / Inception - 2010, MP3 V0
```
## 3. Обязательные поля
### 3.1 Жанр
```
(Soundtrack / Unofficial)
(Score / Unofficial)
```
### 3.2 Название фильма
Формат: Русское название / Оригинальное название
### 3.3 Композитор
**Обязательно только для Score.**
### 3.4 Год
Год выхода фильма/сериала (не саундтрека).
### 3.5 Продюсер/Страна
Информация о производстве фильма.
## 4. Обложка
**Размер:** 200x200 - 500x500 пикселей
Рекомендуется использовать:
- Постер фильма
- Официальную обложку саундтрека (если есть)
## 5. Треклист
### 5.1 Формат
```
01. Исполнитель - Название (продолжительность) - битрейт
02. Исполнитель - Название (продолжительность) - битрейт
```
### 5.2 Для Score
```
01. Название (продолжительность) - битрейт
```
(композитор указывается в заголовке)
## 6. Именование папки
```
Композитор - Название фильма - Unofficial Soundtrack (Год)
```
или
```
Название фильма - Unofficial Soundtrack (Год)
```
## 7. Именование файлов
✅ Правильно:
```
01 - Название
01. Название
01 Название
```
❌ Неправильно:
```
Название - 01
01Название
```
## 8. Различие Soundtrack и Score
### Soundtrack
- Песенная компиляция
- Различные исполнители
- Песни, использованные в фильме
- Не всегда оригинальные записи
### Score
- Оригинальная инструментальная музыка
- Один композитор (или группа)
- Написана специально для фильма
- Фоновая музыка, темы
## Паттерны для парсера
```regex
# Soundtrack тип
\(Soundtrack\s*/?\s*(?:Unofficial|Official)?\)
# Score тип
\(Score\s*/?\s*(?:Unofficial|Official)?\)
# OST тег
\[?OST\]?|O\.?S\.?T\.?
# Название фильма двуязычное
([^/]+)\s*/\s*([^-]+)
# Год фильма
-\s*(\d{4})\s*,
```
## Дополнительные ссылки
- [Официальные саундтреки lossless](https://rutracker.org/forum/viewforum.php?f=691)
- [Официальные саундтреки lossy](https://rutracker.org/forum/viewforum.php?f=469)
- [Саундтреки к играм](https://rutracker.org/forum/viewforum.php?f=786)
- [Саундтреки к аниме](https://rutracker.org/forum/viewforum.php?f=1631)
+128
View File
@@ -0,0 +1,128 @@
# Правила оцифровок с аналоговых носителей RuTracker
**Источник:** https://rutracker.org/forum/viewtopic.php?t=3558517
## 1. Введение
### 1.1 Область применения
Правила для музыкальных материалов, созданных оцифровкой аналогового звукового сигнала с:
- Виниловых пластинок
- Магнитофонных лент
- Других аналоговых носителей
Распространяется только на любительские оцифровки, не изданные официально на audio CD.
### 1.2 Связанные документы
- [Общие правила публикации](https://rutracker.org/forum/viewtopic.php?t=6372776)
- [Правила пользования форумом](https://rutracker.org/forum/viewtopic.php?t=1045)
## 2. Требования к содержанию раздачи
### 2.1 Запрещено включать:
1. **Lossy-форматы** (MP3, WMA, Ogg) - для них отдельные разделы
2. **WAV без lossless-сжатия** - только APE, FLAC (≥5), WV
3. **Многоканальное аудио** (DVD-Audio) - только PCM Wave в lossless
4. **Несколько вариантов одного носителя** - выкладывать отдельными раздачами
5. **Версии "до" и "после" обработки** - только финальная версия
6. **Неполный набор треков** официального носителя
7. **Видео** (кроме музыкальных клипов, караоке, концертов)
8. **Оцифровки с цифровых исходников** - для них раздел "Неофициальные сборники"
9. **PCM с частотой не кратной 44.1 или 48 кГц**
### 2.2 CUE-файл
Для image+.cue обязателен в кодировке Unicode.
## 3. Требования к оформлению
### 3.1 Обязательные поля шаблона
1. **Формат раздачи** - Bit/kHz (например, 24/96)
2. **Формат исходника** - Bit/kHz исходного материала
3. **Марка носителя** - тег носителя (см. ниже)
4. **Состояние проигрывателя** - марка, модель, картридж, игла, усилитель
5. **Дополнительные параметры** - звуковая карта, софт
6. **АЦП** - марка, модель, разрядность, частота записи
7. **Звуковая карта** - параметры оцифровки
8. **Состояние** - физическое состояние носителя
### 3.2 Теги марки носителя
| Тег | Описание |
|-----|----------|
| `[LP]` | Виниловая пластинка 12" (33 об/мин) |
| `[MINI-LP]` | Мини-альбом |
| `[EP]` | Extended Play |
| `[12"]` | 12-дюймовый сингл |
| `[10"]` | 10-дюймовая пластинка |
| `[7"]` | 7-дюймовый сингл (45 об/мин) |
### 3.3 Состояние винила (Grading)
| Код | Название | Описание |
|-----|----------|----------|
| **Mint/SS** | Новая, запечатанная | Идеальное состояние, не проигрывалась |
| **NM** | Near Mint | Почти идеальная, минимальные дефекты |
| **EX** | Excellent | Незначительные царапины, не влияющие на звук |
| **VG+** | Very Good Plus | Поверхностные шумы и щелчки |
| **VG** | Very Good | Заметные царапины, но проигрывается без проблем |
| **G** | Good | Среднее состояние, заметные шумы |
| **F/P** | Fair/Poor | Плохое состояние |
### 3.4 Оцифровки третьих лиц
Если параметры неизвестны, указать:
- Формат раздачи (Bit/kHz)
- Источник
- Автор оцифровки (если известен)
### 3.5 Обязательные графики
**Для image-рипов:** минимум 1 изображение каждого:
- Спектр
- АЧХ
- Уровень записи
**Для потрековых рипов:** минимум 2 изображения (первый и последний трек стороны)
**Требования к скриншотам:**
- Вертикальные и горизонтальные шкалы
- Кликабельные превью под спойлером
- Достаточное разрешение для чтения шкал
### 3.6 CUE для image-рипов
Публикация содержания .CUE в оформлении строго обязательна.
## Примеры заголовков
```
(Rock) [LP] [24/96] Pink Floyd - The Dark Side of the Moon - 1973, FLAC (image+.cue)
(Jazz) [LP] [NM] [24/192] Miles Davis - Kind of Blue - 1959, FLAC (tracks)
(Electronic) [EP] [VG+] [24/48] Kraftwerk - Autobahn - 1974, APE (image+.cue)
```
## Паттерны для парсера
```regex
# Тег носителя
\[(LP|MINI-LP|EP|12"|10"|7")\]
# Состояние винила
\[(Mint|SS|NM|EX|VG\+?|G|F/?P)\]
# Формат Bit/kHz
\[(\d+)/(\d+(?:\.\d+)?)\]
# Пример комбинированный
\[(LP|EP)\]\s*\[(NM|EX|VG\+?)\]\s*\[(\d+)/(\d+)\]
```
## Дополнительные ссылки
- [Теги в названиях тем оцифровок](https://rutracker.org/forum/viewtopic.php?t=2672956)
- [Рекомендации по оцифровке](https://rutracker.org/forum/viewtopic.php?t=966580)
- [Правила Hi-Res музыки](https://rutracker.org/forum/viewtopic.php?t=1422416)
+1 -103
View File
@@ -1,108 +1,6 @@
package indexer
import (
"encoding/xml"
"strings"
pb "homelab.lan/music-agregator/gen/music_agregator/indexer/v1"
)
type Indexer interface {
Search()
Search(query string, limit int32, indexer string) (SearchResult, error)
Capabilities(indexerName string) (IndexerCapabilities, error)
}
type IndexerCapabilities struct {
XMLName xml.Name `xml:"caps"`
Server Server `xml:"server"`
Limits Limits `xml:"limits"`
Searching Searching `xml:"searching"`
Categories []Category `xml:"categories>category"`
}
type Server struct {
Title string `xml:"title,attr"`
}
type Limits struct {
Default int `xml:"default,attr"`
Max int `xml:"max,attr"`
}
type Searching struct {
Search SearchCapability `xml:"search"`
TvSearch SearchCapability `xml:"tv-search"`
MovieSearch SearchCapability `xml:"movie-search"`
MusicSearch SearchCapability `xml:"music-search"`
AudioSearch SearchCapability `xml:"audio-search"`
BookSearch SearchCapability `xml:"book-search"`
}
type SearchCapability struct {
Available string `xml:"available,attr"`
SupportedParams string `xml:"supportedParams,attr"`
SearchEngine string `xml:"searchEngine,attr"`
}
type Category struct {
ID int `xml:"id,attr"`
Name string `xml:"name,attr"`
Subcats []Subcat `xml:"subcat"`
}
type Subcat struct {
ID int `xml:"id,attr"`
Name string `xml:"name,attr"`
}
func (c *IndexerCapabilities) ToProto() *pb.CapabilitiesResponse {
return &pb.CapabilitiesResponse{
Server: &pb.Server{
Title: c.Server.Title,
},
Limits: &pb.Limits{
Default: int32(c.Limits.Default),
Max: int32(c.Limits.Max),
},
Searching: &pb.Searching{
Search: c.Searching.Search.toProto(),
TvSearch: c.Searching.TvSearch.toProto(),
MovieSearch: c.Searching.MovieSearch.toProto(),
MusicSearch: c.Searching.MusicSearch.toProto(),
AudioSearch: c.Searching.AudioSearch.toProto(),
BookSearch: c.Searching.BookSearch.toProto(),
},
Categories: c.categoriesToProto(),
}
}
func (s *SearchCapability) toProto() *pb.SearchCapability {
var params []string
if s.SupportedParams != "" {
params = strings.Split(s.SupportedParams, ",")
}
return &pb.SearchCapability{
Available: s.Available == "yes",
SupportedParams: params,
SearchEngine: s.SearchEngine,
}
}
func (c *IndexerCapabilities) categoriesToProto() []*pb.Category {
categories := make([]*pb.Category, len(c.Categories))
for i, cat := range c.Categories {
subcats := make([]*pb.Subcat, len(cat.Subcats))
for j, sub := range cat.Subcats {
subcats[j] = &pb.Subcat{
Id: int32(sub.ID),
Name: sub.Name,
}
}
categories[i] = &pb.Category{
Id: int32(cat.ID),
Name: cat.Name,
Subcats: subcats,
}
}
return categories
}
+103
View File
@@ -0,0 +1,103 @@
package indexer
import (
"encoding/xml"
"strings"
pb "homelab.lan/music-agregator/gen/music_agregator/indexer/v1"
)
type IndexerCapabilities struct {
XMLName xml.Name `xml:"caps"`
Server Server `xml:"server"`
Limits Limits `xml:"limits"`
Searching Searching `xml:"searching"`
Categories []Category `xml:"categories>category"`
}
type Server struct {
Title string `xml:"title,attr"`
}
type Limits struct {
Default int `xml:"default,attr"`
Max int `xml:"max,attr"`
}
type Searching struct {
Search SearchCapability `xml:"search"`
TvSearch SearchCapability `xml:"tv-search"`
MovieSearch SearchCapability `xml:"movie-search"`
MusicSearch SearchCapability `xml:"music-search"`
AudioSearch SearchCapability `xml:"audio-search"`
BookSearch SearchCapability `xml:"book-search"`
}
type SearchCapability struct {
Available string `xml:"available,attr"`
SupportedParams string `xml:"supportedParams,attr"`
SearchEngine string `xml:"searchEngine,attr"`
}
type Category struct {
ID int `xml:"id,attr"`
Name string `xml:"name,attr"`
Subcats []Subcat `xml:"subcat"`
}
type Subcat struct {
ID int `xml:"id,attr"`
Name string `xml:"name,attr"`
}
func (c *IndexerCapabilities) ToProto() *pb.CapabilitiesResponse {
return &pb.CapabilitiesResponse{
Server: &pb.Server{
Title: c.Server.Title,
},
Limits: &pb.Limits{
Default: int32(c.Limits.Default),
Max: int32(c.Limits.Max),
},
Searching: &pb.Searching{
Search: c.Searching.Search.toProto(),
TvSearch: c.Searching.TvSearch.toProto(),
MovieSearch: c.Searching.MovieSearch.toProto(),
MusicSearch: c.Searching.MusicSearch.toProto(),
AudioSearch: c.Searching.AudioSearch.toProto(),
BookSearch: c.Searching.BookSearch.toProto(),
},
Categories: c.categoriesToProto(),
}
}
func (s *SearchCapability) toProto() *pb.SearchCapability {
var params []string
if s.SupportedParams != "" {
params = strings.Split(s.SupportedParams, ",")
}
return &pb.SearchCapability{
Available: s.Available == "yes",
SupportedParams: params,
SearchEngine: s.SearchEngine,
}
}
func (c *IndexerCapabilities) categoriesToProto() []*pb.Category {
categories := make([]*pb.Category, len(c.Categories))
for i, cat := range c.Categories {
subcats := make([]*pb.Subcat, len(cat.Subcats))
for j, sub := range cat.Subcats {
subcats[j] = &pb.Subcat{
Id: int32(sub.ID),
Name: sub.Name,
}
}
categories[i] = &pb.Category{
Id: int32(cat.ID),
Name: cat.Name,
Subcats: subcats,
}
}
return categories
}
+37 -2
View File
@@ -25,8 +25,43 @@ func NewIndexer(cfg config.Config) Indexer {
}
}
func (indexer *JacketIndexer) Search() {
log.Warn().Msg("Unimplemented method search on the Jacket Indexer")
func (indexer *JacketIndexer) Search(query string, limit int32, tracker string) (SearchResult, error) {
searchTracker := "all"
if len(tracker) != 0 {
searchTracker = tracker
}
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)
log.Debug().Str("uri", uri).Msg("Sending search request")
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
log.Error().Err(err).Msg("Error creating request")
return SearchResult{}, err
}
resp, err := indexer.client.Do(req)
if err != nil {
log.Error().Err(err).Msg("Error making search request")
return SearchResult{}, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Error().Err(err).Msg("Error reading search response body")
return SearchResult{}, err
}
var searchResult SearchResult
if err := xml.Unmarshal(body, &searchResult); err != nil {
log.Error().Err(err).Msg("Error parsing search XML")
return SearchResult{}, err
}
return searchResult, nil
}
func (indexer *JacketIndexer) Capabilities(indexerName string) (IndexerCapabilities, error) {
+71
View File
@@ -0,0 +1,71 @@
package indexer
import (
"encoding/xml"
pb "homelab.lan/music-agregator/gen/music_agregator/indexer/v1"
)
type SearchResult struct {
XMLName xml.Name `xml:"rss"`
Items []Item `xml:"channel>item"` // Directly targets items inside channel
}
type Item struct {
Title string `xml:"title"`
Link string `xml:"link"`
Guid string `xml:"guid"`
PubDate string `xml:"pubDate"`
Size int64 `xml:"size"`
Description string `xml:"description"`
Categories []string `xml:"category"`
Enclosure Enclosure `xml:"enclosure"`
TorznabAttrs []TorznabAttr `xml:"attr"`
}
type Enclosure struct {
URL string `xml:"url,attr"`
Length int64 `xml:"length,attr"`
Type string `xml:"type,attr"`
}
type TorznabAttr struct {
Name string `xml:"name,attr"`
Value string `xml:"value,attr"`
}
func (sr *SearchResult) ToProto() *pb.SearchResponse {
pbItems := make([]*pb.SearchItem, len(sr.Items))
for i, item := range sr.Items {
// Map Torznab Attributes
pbAttrs := make([]*pb.TorznabAttr, len(item.TorznabAttrs))
for j, attr := range item.TorznabAttrs {
pbAttrs[j] = &pb.TorznabAttr{
Name: attr.Name,
Value: attr.Value,
}
}
// Map the Item
pbItems[i] = &pb.SearchItem{
Title: item.Title,
Link: item.Link,
Guid: item.Guid,
PubDate: item.PubDate,
Size: item.Size,
Description: item.Description,
Categories: item.Categories,
Enclosure: &pb.Enclosure{
Url: item.Enclosure.URL,
Length: item.Enclosure.Length,
Type: item.Enclosure.Type,
},
TorznabAttrs: pbAttrs,
}
}
return &pb.SearchResponse{
Result: pbItems,
}
}
+9 -1
View File
@@ -6,6 +6,7 @@ import (
"github.com/rs/zerolog/log"
"google.golang.org/grpc"
pb "homelab.lan/music-agregator/gen/music_agregator/indexer/v1"
"homelab.lan/music-agregator/internal/config"
)
@@ -27,7 +28,14 @@ func NewIndexerServer(cfg config.Config) (*IndexerServer, error) {
}
func (server *IndexerServer) Search(ctx context.Context, req *pb.SearchRequest) (*pb.SearchResponse, error) {
return &pb.SearchResponse{}, nil
log.Debug().Str("query", req.GetQuery()).Int32("limit", req.GetLimit()).Str("indexer", req.GetTracker()).Msg("Running search with these prams")
searchResult, err := server.indexer.Search(req.GetQuery(), req.GetLimit(), req.GetTracker())
if err != nil {
log.Error().Err(err).Msg("Failed to search in indexer")
return nil, err
}
return searchResult.ToProto(), nil
}
func (server *IndexerServer) Capabilities(ctx context.Context, req *pb.CapabilitiesRequest) (*pb.CapabilitiesResponse, error) {
+178
View File
@@ -0,0 +1,178 @@
package release
type Type int
const (
TypeUnknown Type = iota
TypeAlbum
TypeEP
TypeSingle
TypeDiscography
TypeCollection
TypeCompilation
TypeSoundtrack
TypeLive
TypeBootleg
)
func (t Type) String() string {
switch t {
case TypeAlbum:
return "album"
case TypeEP:
return "ep"
case TypeSingle:
return "single"
case TypeDiscography:
return "discography"
case TypeCollection:
return "collection"
case TypeCompilation:
return "compilation"
case TypeSoundtrack:
return "soundtrack"
case TypeLive:
return "live"
case TypeBootleg:
return "bootleg"
default:
return "unknown"
}
}
func (t Type) Priority() int {
switch t {
case TypeAlbum, TypeEP, TypeSingle:
return 1
case TypeLive:
return 2
case TypeSoundtrack:
return 3
case TypeCollection:
return 4
case TypeDiscography:
return 5
case TypeCompilation:
return 6
case TypeBootleg:
return 7
default:
return 99
}
}
type AudioFormat int
const (
FormatUnknown AudioFormat = iota
FormatFLAC
FormatMP3
FormatAAC
FormatAPE
FormatWavPack
FormatALAC
FormatOGG
FormatWAV
)
func (f AudioFormat) String() string {
switch f {
case FormatFLAC:
return "FLAC"
case FormatMP3:
return "MP3"
case FormatAAC:
return "AAC"
case FormatAPE:
return "APE"
case FormatWavPack:
return "WavPack"
case FormatALAC:
return "ALAC"
case FormatOGG:
return "OGG"
case FormatWAV:
return "WAV"
default:
return "unknown"
}
}
func (f AudioFormat) IsLossless() bool {
switch f {
case FormatFLAC, FormatAPE, FormatWavPack, FormatALAC, FormatWAV:
return true
default:
return false
}
}
type Source int
const (
SourceUnknown Source = iota
SourceCD
SourceWEB
SourceVinyl
SourceCassette
SourceDVD
SourceBluRay
)
func (s Source) String() string {
switch s {
case SourceCD:
return "CD"
case SourceWEB:
return "WEB"
case SourceVinyl:
return "Vinyl"
case SourceCassette:
return "Cassette"
case SourceDVD:
return "DVD"
case SourceBluRay:
return "BluRay"
default:
return "unknown"
}
}
type Release struct {
RawTitle string
Artist string
Album string
Year int
YearEnd int
Type Type
Genres []string
Format AudioFormat
Source Source
Bitrate string
BitDepth int
SampleRate int
RipType string
ReleaseCount int
Tags []string
Label string
CatalogNum string
ParsedSuccessfully bool
ParseErrors []string
}
func (r *Release) IsDiscography() bool {
return r.Type == TypeDiscography || r.Type == TypeCollection
}
func (r *Release) IsSingleRelease() bool {
return r.Type == TypeAlbum || r.Type == TypeEP || r.Type == TypeSingle
}
func (r *Release) HasYearRange() bool {
return r.YearEnd > 0 && r.YearEnd != r.Year
}
+256
View File
@@ -0,0 +1,256 @@
package parser
import (
"strconv"
"strings"
"homelab.lan/music-agregator/internal/release"
)
type BaseParser struct{}
func (p *BaseParser) NewRelease(title string) *release.Release {
return &release.Release{
RawTitle: title,
ParsedSuccessfully: true,
}
}
func (p *BaseParser) ExtractGenres(title string) []string {
match := genrePattern.FindStringSubmatch(title)
if len(match) < 2 {
return nil
}
raw := match[1]
parts := strings.FieldsFunc(raw, func(r rune) bool {
return r == ',' || r == '/' || r == ';'
})
var genres []string
for _, part := range parts {
trimmed := strings.TrimSpace(part)
if trimmed != "" {
genres = append(genres, trimmed)
}
}
return genres
}
func (p *BaseParser) StripGenrePrefix(title string) string {
return genrePattern.ReplaceAllString(title, "")
}
func (p *BaseParser) StripLeadingTags(title string) string {
return leadingTagsPattern.ReplaceAllString(title, "")
}
func (p *BaseParser) ExtractYear(title string) int {
match := yearPattern.FindStringSubmatch(title)
if len(match) < 2 {
return 0
}
year, _ := strconv.Atoi(match[1])
return year
}
func (p *BaseParser) ExtractYearRange(title string) (int, int) {
match := yearRangePattern.FindStringSubmatch(title)
if len(match) < 3 {
year := p.ExtractYear(title)
return year, 0
}
start, _ := strconv.Atoi(match[1])
end, _ := strconv.Atoi(match[2])
return start, end
}
func (p *BaseParser) ExtractFormat(title string) release.AudioFormat {
match := formatPattern.FindStringSubmatch(title)
if len(match) < 2 {
return release.FormatUnknown
}
format := strings.ToUpper(match[1])
switch {
case format == "FLAC":
return release.FormatFLAC
case format == "MP3":
return release.FormatMP3
case format == "AAC":
return release.FormatAAC
case format == "APE":
return release.FormatAPE
case format == "WV" || format == "WAVPACK":
return release.FormatWavPack
case format == "ALAC":
return release.FormatALAC
case format == "OGG":
return release.FormatOGG
case format == "WAV":
return release.FormatWAV
default:
return release.FormatUnknown
}
}
func (p *BaseParser) ExtractBitrate(title string) string {
if strings.Contains(strings.ToLower(title), "lossless") {
return "lossless"
}
match := bitratePattern.FindStringSubmatch(title)
if len(match) < 2 {
return ""
}
if match[1] != "" {
return match[1] + " kbps"
}
if match[2] != "" {
return "V" + match[2]
}
if match[3] != "" {
return "VBR ~" + match[3] + " kbps"
}
if match[4] != "" && match[5] != "" {
return "VBR " + match[4] + "-" + match[5] + " kbps"
}
return ""
}
func (p *BaseParser) ExtractRipType(title string) string {
match := ripTypePattern.FindStringSubmatch(title)
if len(match) < 2 {
return ""
}
return strings.ToLower(match[1])
}
func (p *BaseParser) ExtractSource(title string) release.Source {
match := sourceTagPattern.FindStringSubmatch(title)
if len(match) < 2 {
if strings.Contains(strings.ToLower(title), "web") {
return release.SourceWEB
}
return release.SourceUnknown
}
tag := strings.ToUpper(match[1])
switch tag {
case "CD":
return release.SourceCD
case "WEB":
return release.SourceWEB
case "LP", "VINYL", "MINI-LP", "EP", "12\"", "10\"", "7\"":
return release.SourceVinyl
case "SACD", "DVDA", "HDAD":
return release.SourceDVD
default:
return release.SourceUnknown
}
}
func (p *BaseParser) ExtractHiRes(title string) (bitDepth int, sampleRate int) {
match := hiResPattern.FindStringSubmatch(title)
if len(match) < 3 {
return 0, 0
}
bitDepth, _ = strconv.Atoi(match[1])
sr := match[2]
if strings.Contains(sr, ".") {
f, _ := strconv.ParseFloat(sr, 64)
sampleRate = int(f * 1000)
} else {
sampleRate, _ = strconv.Atoi(sr)
sampleRate *= 1000
}
return bitDepth, sampleRate
}
func (p *BaseParser) ExtractSpecialTags(title string) []string {
matches := specialTagPattern.FindAllStringSubmatch(title, -1)
var tags []string
for _, match := range matches {
if len(match) >= 2 {
tags = append(tags, match[1])
}
}
return tags
}
func (p *BaseParser) ExtractReleaseCount(title string) int {
match := releaseCountPattern.FindStringSubmatch(title)
if len(match) < 2 {
return 0
}
count, _ := strconv.Atoi(match[1])
return count
}
func (p *BaseParser) ExtractLabel(title string) string {
match := labelPattern.FindStringSubmatch(title)
if len(match) < 2 {
return ""
}
return strings.TrimSpace(match[1])
}
func (p *BaseParser) ExtractCatalogNum(title string) string {
match := catalogNumPattern.FindStringSubmatch(title)
if len(match) < 2 {
return ""
}
return match[1]
}
func (p *BaseParser) DetectType(title string) release.Type {
switch {
case discographyPattern.MatchString(title):
return release.TypeDiscography
case collectionPattern.MatchString(title):
return release.TypeCollection
case compilationPattern.MatchString(title):
return release.TypeCompilation
case anthologyPattern.MatchString(title):
return release.TypeCollection
case soundtrackPattern.MatchString(title):
return release.TypeSoundtrack
case bootlegPattern.MatchString(title):
return release.TypeBootleg
case livePattern.MatchString(title):
return release.TypeLive
case epPattern.MatchString(title):
return release.TypeEP
case singlePattern.MatchString(title):
return release.TypeSingle
case bestOfPattern.MatchString(title):
return release.TypeCompilation
default:
return release.TypeAlbum
}
}
func (p *BaseParser) ExtractArtistAlbum(title string) (artist string, album string) {
cleaned := p.StripGenrePrefix(title)
cleaned = p.StripLeadingTags(cleaned)
cleaned = trailingTechPattern.ReplaceAllString(cleaned, "")
if match := standardTitlePattern.FindStringSubmatch(title); len(match) >= 3 {
return strings.TrimSpace(match[1]), strings.TrimSpace(match[2])
}
if match := altTitlePattern.FindStringSubmatch(title); len(match) >= 3 {
return strings.TrimSpace(match[1]), strings.TrimSpace(match[2])
}
parts := strings.SplitN(cleaned, " - ", 3)
if len(parts) >= 2 {
artist = strings.TrimSpace(parts[0])
albumPart := strings.TrimSpace(parts[1])
albumPart = yearPattern.ReplaceAllString(albumPart, "")
albumPart = strings.Trim(albumPart, " -,")
album = albumPart
}
return artist, album
}
func (p *BaseParser) AddError(r *release.Release, err string) {
r.ParseErrors = append(r.ParseErrors, err)
r.ParsedSuccessfully = false
}
@@ -0,0 +1,40 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type ClassicalParser struct {
BaseParser
}
func NewClassicalParser() *ClassicalParser {
return &ClassicalParser{}
}
func (p *ClassicalParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
if len(r.Genres) == 0 {
r.Genres = []string{"Classical"}
}
r.Type = p.DetectType(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Bitrate = p.ExtractBitrate(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.ReleaseCount = p.ExtractReleaseCount(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
var _ Parser = (*ClassicalParser)(nil)
@@ -0,0 +1,56 @@
package parser
import (
"strings"
"homelab.lan/music-agregator/internal/release"
)
type DiscographyParser struct {
BaseParser
}
func NewDiscographyParser() *DiscographyParser {
return &DiscographyParser{}
}
func (p *DiscographyParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Bitrate = p.ExtractBitrate(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.ReleaseCount = p.ExtractReleaseCount(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
if collectionPattern.MatchString(title) {
r.Type = release.TypeCollection
} else {
r.Type = release.TypeDiscography
}
r.Artist = p.extractDiscographyArtist(title)
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
func (p *DiscographyParser) extractDiscographyArtist(title string) string {
if match := discographyTitlePattern.FindStringSubmatch(title); len(match) >= 2 {
return strings.TrimSpace(match[1])
}
if match := collectionTitlePattern.FindStringSubmatch(title); len(match) >= 2 {
return strings.TrimSpace(match[1])
}
artist, _ := p.ExtractArtistAlbum(title)
return artist
}
var _ Parser = (*DiscographyParser)(nil)
@@ -0,0 +1,152 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestDiscographyParser(t *testing.T) {
p := NewDiscographyParser()
tests := []struct {
name string
title string
wantArtist string
wantYear int
wantYearEnd int
wantReleaseCount int
wantType release.Type
wantFormat release.AudioFormat
wantParseOK bool
}{
{
name: "Russian discography with ALAC",
title: "(Metalcore, progressive metalcore, alternative metal, mathcore) [CD`12] Architects - Дискография / Discography - 2006-2025, ALAC (tracks+.cue), lossless",
wantArtist: "Architects",
wantYear: 2006,
wantYearEnd: 2025,
wantType: release.TypeDiscography,
wantFormat: release.FormatALAC,
wantParseOK: true,
},
{
name: "discography with CD count",
title: "(Rock / Hard Rock / Power-Pop) [CD] Cheap Trick - Дискография - 1977-2021 (78 CD), FLAC (image+.cue), lossless",
wantArtist: "Cheap Trick",
wantYear: 1977,
wantYearEnd: 2021,
wantReleaseCount: 78,
wantType: release.TypeDiscography,
wantParseOK: true,
},
{
name: "mixed CD and WEB",
title: "Pompeya - Дискография | Discography (3 CD, 6 WEB) - 2011-2015, FLAC (tracks+.cue, tracks/web), lossless",
wantArtist: "Pompeya",
wantYear: 2011,
wantYearEnd: 2015,
wantType: release.TypeDiscography,
wantParseOK: true,
},
{
name: "large discography with releases count",
title: "(Rock) Александр Башлачёв ● Дискография (1994~2025) (35 выпусков, 47 CD / 2 Digital Release), FLAC (image+.cue), lossless",
wantArtist: "Александр Башлачёв",
wantYear: 1994,
wantYearEnd: 2025,
wantType: release.TypeDiscography,
wantParseOK: true,
},
{
name: "very large discography",
title: "(Rock) Аквариум и Борис Гребенщиков (БГ) - Дискография - 1973–2023 (222 издания, 245 CD), FLAC (image+.cue), lossless",
wantArtist: "Аквариум и Борис Гребенщиков (БГ)",
wantYear: 1973,
wantYearEnd: 2023,
wantType: release.TypeDiscography,
wantParseOK: true,
},
{
name: "metal discography",
title: "(Heavy Metal) [CD] Saxon - Дискография (58 CD) - 1979-2024, FLAC (image+.cue), lossless",
wantArtist: "Saxon",
wantYear: 1979,
wantYearEnd: 2024,
wantReleaseCount: 58,
wantType: release.TypeDiscography,
wantParseOK: true,
},
{
name: "detailed Queen discography",
title: "(Progressive Hard Rock Fusion) [CD] Queen The Discography / Дискография (15 Studio, 11 Live, 13 Compilation, 63 Singles, 2 Collaboration, 7 Box Set, 243 issues, 336 CD) - 1973-2015, FLAC (image+.cue), lossless",
wantArtist: "Queen",
wantYear: 1973,
wantYearEnd: 2015,
wantType: release.TypeDiscography,
wantParseOK: true,
},
{
name: "English discography",
title: "(Rock, Pop) [CD] U2 - Discography (1980-2017), FLAC (tracks+.cue), lossless",
wantArtist: "U2",
wantYear: 1980,
wantYearEnd: 2017,
wantType: release.TypeDiscography,
wantParseOK: true,
},
{
name: "death metal discography",
title: "(Technical Brutal Death Metal) [CD] Nile - Discography (1994 - 2024) 13 CD, FLAC (image+.cue), lossless",
wantArtist: "Nile",
wantYear: 1994,
wantYearEnd: 2024,
wantReleaseCount: 13,
wantType: release.TypeDiscography,
wantParseOK: true,
},
{
name: "collection keyword",
title: "(Pop) Madonna - Коллекция / Collection - 65 релизов (2 Albums, 22 Singles, 13 Megamixes, 8 Live, 17 Collections, 3 Bonus) (1982-2012), MP3, 128-320, VBR kbps",
wantArtist: "Madonna",
wantYear: 1982,
wantYearEnd: 2012,
wantType: release.TypeCollection,
wantParseOK: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := p.Parse(tt.title)
if r.ParsedSuccessfully != tt.wantParseOK {
t.Errorf("ParsedSuccessfully = %v, want %v, errors: %v", r.ParsedSuccessfully, tt.wantParseOK, r.ParseErrors)
}
if tt.wantArtist != "" && r.Artist != tt.wantArtist {
t.Errorf("Artist = %q, want %q", r.Artist, tt.wantArtist)
}
if tt.wantYear != 0 && r.Year != tt.wantYear {
t.Errorf("Year = %d, want %d", r.Year, tt.wantYear)
}
if tt.wantYearEnd != 0 && r.YearEnd != tt.wantYearEnd {
t.Errorf("YearEnd = %d, want %d", r.YearEnd, tt.wantYearEnd)
}
if tt.wantReleaseCount != 0 && r.ReleaseCount != tt.wantReleaseCount {
t.Errorf("ReleaseCount = %d, want %d", r.ReleaseCount, tt.wantReleaseCount)
}
if tt.wantType != release.TypeUnknown && r.Type != tt.wantType {
t.Errorf("Type = %v, want %v", r.Type, tt.wantType)
}
if tt.wantFormat != release.FormatUnknown && r.Format != tt.wantFormat {
t.Errorf("Format = %v, want %v", r.Format, tt.wantFormat)
}
})
}
}
@@ -0,0 +1,37 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type GeneralParser struct {
BaseParser
}
func NewGeneralParser() *GeneralParser {
return &GeneralParser{}
}
func (p *GeneralParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
r.Type = p.DetectType(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Bitrate = p.ExtractBitrate(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.ReleaseCount = p.ExtractReleaseCount(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
var _ Parser = (*GeneralParser)(nil)
@@ -0,0 +1,166 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestGeneralParser(t *testing.T) {
p := NewGeneralParser()
tests := []struct {
name string
title string
wantArtist string
wantAlbum string
wantYear int
wantFormat release.AudioFormat
wantType release.Type
wantGenres []string
wantSource release.Source
wantRipType string
wantBitrate string
wantParseOK bool
}{
{
name: "standard CD rip with genre",
title: "(Rock) [CD] Thin Lizzy - Acoustic Sessions - 2024 (Decca Records EU 2025), FLAC (image+.cue), lossless",
wantArtist: "Thin Lizzy",
wantAlbum: "Acoustic Sessions",
wantYear: 2024,
wantFormat: release.FormatFLAC,
wantType: release.TypeAlbum,
wantGenres: []string{"Rock"},
wantSource: release.SourceCD,
wantRipType: "image+.cue",
wantBitrate: "lossless",
wantParseOK: true,
},
{
name: "multi-genre CD rip",
title: "(Hard Rock, Glam Rock, Progressive Rock, Art Rock, Heavy Metal) [CD] Queen Queen I (2 CD) 2024 , FLAC (image+.cue), lossless",
wantArtist: "Queen",
wantYear: 2024,
wantFormat: release.FormatFLAC,
wantType: release.TypeAlbum,
wantGenres: []string{"Hard Rock", "Glam Rock", "Progressive Rock", "Art Rock", "Heavy Metal"},
wantSource: release.SourceCD,
wantParseOK: true,
},
{
name: "WEB release with tracks",
title: "(Progressive Rock) [WEB] Opeth - In Cauda Venenum (Extended Edition) - 2019/2022, FLAC (tracks), lossless",
wantArtist: "Opeth",
wantYear: 2019,
wantFormat: release.FormatFLAC,
wantSource: release.SourceWEB,
wantRipType: "tracks",
wantParseOK: true,
},
{
name: "Japan release",
title: "(Pop-Rock Soft-Rock) [CD] Sting - The Soul Cages (Expanded Edition) - 2025 [Japan], FLAC (image+.cue), lossless",
wantArtist: "Sting",
wantYear: 2025,
wantFormat: release.FormatFLAC,
wantSource: release.SourceCD,
wantParseOK: true,
},
{
name: "live album",
title: "(Rock) [CD] Bryan Adams - Live at the Royal Albert Hall - 2024, FLAC (image+.cue), lossless",
wantArtist: "Bryan Adams",
wantType: release.TypeLive,
wantYear: 2024,
wantParseOK: true,
},
{
name: "soundtrack",
title: "(Pop) [CD] Celine Dion - I AM - Celine Dion (Original Motion Picture Soundtrack) - 2024 [Japan], FLAC (image+.cue), lossless",
wantArtist: "Celine Dion",
wantType: release.TypeSoundtrack,
wantYear: 2024,
wantParseOK: true,
},
{
name: "deluxe box set",
title: "(Rock) [CD] Bryan Adams - Roll With The Punches (Deluxe Box Set) - 2025, FLAC (image+.cue), lossless",
wantArtist: "Bryan Adams",
wantYear: 2025,
wantParseOK: true,
},
{
name: "CDS single",
title: "(Heavy Metal) [CDS] Bruce Dickinson - Resurrection Men - 2024, FLAC (image+.cue), lossless",
wantArtist: "Bruce Dickinson",
wantYear: 2024,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "tracks+cue format",
title: "(Classic Rock) [CD] The Who - Who Are You (Super Deluxe Edition) - 2025, FLAC (tracks+cue), lossless",
wantArtist: "The Who",
wantYear: 2025,
wantRipType: "tracks+cue",
wantParseOK: true,
},
{
name: "WEB with special artist name",
title: "(Chamber Pop) [WEB] Florence + the Machine - Ceremonials (Digital Deluxe Edition) - 2011, FLAC (tracks), lossless",
wantArtist: "Florence + the Machine",
wantYear: 2011,
wantSource: release.SourceWEB,
wantParseOK: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := p.Parse(tt.title)
if r.ParsedSuccessfully != tt.wantParseOK {
t.Errorf("ParsedSuccessfully = %v, want %v, errors: %v", r.ParsedSuccessfully, tt.wantParseOK, r.ParseErrors)
}
if tt.wantArtist != "" && r.Artist != tt.wantArtist {
t.Errorf("Artist = %q, want %q", r.Artist, tt.wantArtist)
}
if tt.wantAlbum != "" && r.Album != tt.wantAlbum {
t.Errorf("Album = %q, want %q", r.Album, tt.wantAlbum)
}
if tt.wantYear != 0 && r.Year != tt.wantYear {
t.Errorf("Year = %d, want %d", r.Year, tt.wantYear)
}
if tt.wantFormat != release.FormatUnknown && r.Format != tt.wantFormat {
t.Errorf("Format = %v, want %v", r.Format, tt.wantFormat)
}
if tt.wantType != release.TypeUnknown && r.Type != tt.wantType {
t.Errorf("Type = %v, want %v", r.Type, tt.wantType)
}
if tt.wantSource != release.SourceUnknown && r.Source != tt.wantSource {
t.Errorf("Source = %v, want %v", r.Source, tt.wantSource)
}
if tt.wantRipType != "" && r.RipType != tt.wantRipType {
t.Errorf("RipType = %q, want %q", r.RipType, tt.wantRipType)
}
if tt.wantBitrate != "" && r.Bitrate != tt.wantBitrate {
t.Errorf("Bitrate = %q, want %q", r.Bitrate, tt.wantBitrate)
}
if len(tt.wantGenres) > 0 {
if len(r.Genres) != len(tt.wantGenres) {
t.Errorf("Genres count = %d, want %d", len(r.Genres), len(tt.wantGenres))
}
}
})
}
}
@@ -0,0 +1,47 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type HiResParser struct {
BaseParser
}
func NewHiResParser() *HiResParser {
return &HiResParser{}
}
func (p *HiResParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
r.Type = p.DetectType(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
if r.Format == release.FormatUnknown {
r.Format = release.FormatFLAC
}
r.Bitrate = "lossless"
if r.BitDepth == 0 {
if dsdMatch := dsdPattern.FindStringSubmatch(title); len(dsdMatch) >= 3 {
r.BitDepth = 1
r.Tags = append(r.Tags, dsdMatch[1]+dsdMatch[2])
}
}
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
var _ Parser = (*HiResParser)(nil)
@@ -0,0 +1,133 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestHiResParser(t *testing.T) {
p := NewHiResParser()
tests := []struct {
name string
title string
wantArtist string
wantYear int
wantBitDepth int
wantSampleRate int
wantSource release.Source
wantParseOK bool
}{
{
name: "TR24 OF official release",
title: "[TR24][OF] Matteo Mancuso - Route 96 - 2026 (Progressive Rock, Jazz Fusion, Instrumental)",
wantArtist: "Matteo Mancuso",
wantYear: 2026,
wantParseOK: true,
},
{
name: "TR24 OF LDR tag",
title: "[TR24][OF][LDR] Sepultura - The Cloud Of Unknowing - 2026 (Groove Thrash Metal)",
wantArtist: "Sepultura",
wantYear: 2026,
wantParseOK: true,
},
{
name: "24bit 48kHz in title",
title: "[TR24][OF] U2 - Days Of Ash [EP] [24bit-48kHz] - 2026 (Pop Rock, Soft Rock)",
wantArtist: "U2",
wantYear: 2026,
wantBitDepth: 24,
wantSampleRate: 48000,
wantParseOK: true,
},
{
name: "LP 24/192",
title: "(Blues, R&B) [LP] [24/192] Etta James - At Last! - 1960/2026, FLAC (tracks)",
wantArtist: "Etta James",
wantYear: 1960,
wantBitDepth: 24,
wantSampleRate: 192000,
wantSource: release.SourceVinyl,
wantParseOK: true,
},
{
name: "DSD128",
title: "(Progressive rock) [LP] [1/5,64 MHz] The Neal Morse Band L. I. F. T. - 2026, DSD 128 (tracks)",
wantArtist: "The Neal Morse Band",
wantYear: 2026,
wantSource: release.SourceVinyl,
wantParseOK: true,
},
{
name: "DSD256 with label",
title: "(Jazz, Bop) [LP] [DSD256] Oscar Peterson Trio & Clark Terry 'Oscar Peterson Trio + One' [Acoustic Sounds Series] - 1964, 2026, dsf (tracks)",
wantArtist: "Oscar Peterson Trio & Clark Terry",
wantYear: 1964,
wantParseOK: true,
},
{
name: "24/96 modal jazz",
title: "(Modal, Jazz) [LP] [24/96] John Coltrane - The Tiberi Tapes: A Preview Of The Mythic Recordings (2026 Record Store Day) - 2026, FLAC (tracks)",
wantArtist: "John Coltrane",
wantYear: 2026,
wantBitDepth: 24,
wantSampleRate: 96000,
wantParseOK: true,
},
{
name: "2xLP compilation",
title: "(Electronic, Funk / Soul, Disco, House) [2xLP] [24/192] Various - The Many Faces Of Daft Punk - 2020( Compilation), FLAC (tracks)",
wantArtist: "Various",
wantYear: 2020,
wantBitDepth: 24,
wantSampleRate: 192000,
wantParseOK: true,
},
{
name: "SACD-R",
title: "[SACD-R][OF] Wynton Marsalis - The London Concert - 2000 (Classical)",
wantArtist: "Wynton Marsalis",
wantYear: 2000,
wantParseOK: true,
},
{
name: "SACD-R DSD",
title: "[SACD-R][DSD][OF]Scott Hamilton, Paolo Birro - Pure Imagination - 2019 (Jazz)",
wantArtist: "Scott Hamilton, Paolo Birro",
wantYear: 2019,
wantParseOK: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := p.Parse(tt.title)
if r.ParsedSuccessfully != tt.wantParseOK {
t.Errorf("ParsedSuccessfully = %v, want %v, errors: %v", r.ParsedSuccessfully, tt.wantParseOK, r.ParseErrors)
}
if tt.wantArtist != "" && r.Artist != tt.wantArtist {
t.Errorf("Artist = %q, want %q", r.Artist, tt.wantArtist)
}
if tt.wantYear != 0 && r.Year != tt.wantYear {
t.Errorf("Year = %d, want %d", r.Year, tt.wantYear)
}
if tt.wantBitDepth != 0 && r.BitDepth != tt.wantBitDepth {
t.Errorf("BitDepth = %d, want %d", r.BitDepth, tt.wantBitDepth)
}
if tt.wantSampleRate != 0 && r.SampleRate != tt.wantSampleRate {
t.Errorf("SampleRate = %d, want %d", r.SampleRate, tt.wantSampleRate)
}
if tt.wantSource != release.SourceUnknown && r.Source != tt.wantSource {
t.Errorf("Source = %v, want %v", r.Source, tt.wantSource)
}
})
}
}
+40
View File
@@ -0,0 +1,40 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type JazzParser struct {
BaseParser
}
func NewJazzParser() *JazzParser {
return &JazzParser{}
}
func (p *JazzParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
if len(r.Genres) == 0 {
r.Genres = []string{"Jazz"}
}
r.Type = p.DetectType(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Bitrate = p.ExtractBitrate(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.ReleaseCount = p.ExtractReleaseCount(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
var _ Parser = (*JazzParser)(nil)
@@ -0,0 +1,35 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type LabelPacksParser struct {
BaseParser
}
func NewLabelPacksParser() *LabelPacksParser {
return &LabelPacksParser{}
}
func (p *LabelPacksParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
r.Type = release.TypeCollection
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Bitrate = p.ExtractBitrate(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.ReleaseCount = p.ExtractReleaseCount(title)
r.Label = p.ExtractLabel(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
if r.Label == "" {
p.AddError(r, "failed to extract label name")
}
return r
}
var _ Parser = (*LabelPacksParser)(nil)
@@ -0,0 +1,143 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestLabelPacksParser(t *testing.T) {
p := NewLabelPacksParser()
tests := []struct {
name string
title string
wantLabel string
wantYear int
wantYearEnd int
wantReleaseCount int
wantFormat release.AudioFormat
wantParseOK bool
}{
{
name: "standard label pack",
title: "(Drum & Bass) [WEB] Label: Metalheadz (370 релизов), 1994-2025, FLAC (tracks), lossless",
wantLabel: "Metalheadz",
wantYear: 1994,
wantYearEnd: 2025,
wantReleaseCount: 370,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "label with part number",
title: "(Trance, House) [WEB] Label - Black Hole Recordings (Part 3) (401 Releases) - 2009-2023, FLAC (tracks / images), lossless",
wantLabel: "Black Hole Recordings (Part 3)",
wantYear: 2009,
wantYearEnd: 2023,
wantReleaseCount: 401,
wantParseOK: true,
},
{
name: "small label",
title: "(Trance) [WEB, CD] Label: Solaris Recordings (7 Releases) - 2005-2014, FLAC (tracks, tracks+.cue), lossless",
wantLabel: "Solaris Recordings",
wantYear: 2005,
wantYearEnd: 2014,
wantParseOK: true,
},
{
name: "techno label with brackets",
title: "(Techno, IDM, Experimental) [WEB,CD] Label - Stroboscopic Artefacts (2009-2022) [96 Releases], FLAC (tracks) (tracks+.cue), lossless",
wantLabel: "Stroboscopic Artefacts",
wantYear: 2009,
wantYearEnd: 2022,
wantParseOK: true,
},
{
name: "multi-genre label",
title: "(Techno, Ambient, IDM, Experimental, Drum n Bass) [WEB,CD] Label - Auxiliary [2010 - 2021] [65xReleases], FLAC (tracks) (tracks+.cue), lossless",
wantLabel: "Auxiliary",
wantYear: 2010,
wantYearEnd: 2021,
wantParseOK: true,
},
{
name: "Russian release count",
title: "(Techno, Minimal, Deep Tech, Melodic House & Techno) [WEB] Label: FCKNG SERIOUS (121 релиз), 2015-2025, FLAC (tracks, image), lossless",
wantLabel: "FCKNG SERIOUS",
wantYear: 2015,
wantYearEnd: 2025,
wantReleaseCount: 121,
wantParseOK: true,
},
{
name: "progressive house label",
title: "(Progressive House, Trance, Techno) [WEB] Label: Bedrock Records (519 релизов), 1999-2025, (FLAC) lossless (tracks, image)",
wantLabel: "Bedrock Records",
wantYear: 1999,
wantYearEnd: 2025,
wantReleaseCount: 519,
wantParseOK: true,
},
{
name: "large techno label",
title: "(Techno) [WEB,CD] Label - Planet Rhythm Records [1994 - 2021] [443xReleases], FLAC (tracks) (tracks+.cue, image+.cue), lossless",
wantLabel: "Planet Rhythm Records",
wantYear: 1994,
wantYearEnd: 2021,
wantParseOK: true,
},
{
name: "label with featured artists",
title: "(Trance, Breaks, House) [WEB] Label: Digital Emotions (47 Releases) (Incl. Fonarev pres. F13, Poshout, Second Sine & etc.) - 2010-2025, FLAC (tracks), lossless",
wantLabel: "Digital Emotions",
wantYear: 2010,
wantYearEnd: 2025,
wantParseOK: true,
},
{
name: "bondage music",
title: "(Deep House, Minimal) [WEB] Label: Bondage Music (173 релиза), 2006-2025, (FLAC) lossless (tracks, image)",
wantLabel: "Bondage Music",
wantYear: 2006,
wantYearEnd: 2025,
wantReleaseCount: 173,
wantParseOK: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := p.Parse(tt.title)
if r.ParsedSuccessfully != tt.wantParseOK {
t.Errorf("ParsedSuccessfully = %v, want %v, errors: %v", r.ParsedSuccessfully, tt.wantParseOK, r.ParseErrors)
}
if tt.wantLabel != "" && r.Label != tt.wantLabel {
t.Errorf("Label = %q, want %q", r.Label, tt.wantLabel)
}
if tt.wantYear != 0 && r.Year != tt.wantYear {
t.Errorf("Year = %d, want %d", r.Year, tt.wantYear)
}
if tt.wantYearEnd != 0 && r.YearEnd != tt.wantYearEnd {
t.Errorf("YearEnd = %d, want %d", r.YearEnd, tt.wantYearEnd)
}
if tt.wantReleaseCount != 0 && r.ReleaseCount != tt.wantReleaseCount {
t.Errorf("ReleaseCount = %d, want %d", r.ReleaseCount, tt.wantReleaseCount)
}
if tt.wantFormat != release.FormatUnknown && r.Format != tt.wantFormat {
t.Errorf("Format = %v, want %v", r.Format, tt.wantFormat)
}
if r.Type != release.TypeCollection {
t.Errorf("Type = %v, want Collection", r.Type)
}
})
}
}
@@ -0,0 +1,39 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type LosslessParser struct {
BaseParser
}
func NewLosslessParser() *LosslessParser {
return &LosslessParser{}
}
func (p *LosslessParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
r.Type = p.DetectType(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
if r.Format == release.FormatUnknown {
r.Format = release.FormatFLAC
}
r.Bitrate = "lossless"
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
var _ Parser = (*LosslessParser)(nil)
@@ -0,0 +1,143 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestLosslessParser(t *testing.T) {
p := NewLosslessParser()
tests := []struct {
name string
title string
wantArtist string
wantYear int
wantFormat release.AudioFormat
wantSource release.Source
wantRipType string
wantParseOK bool
}{
{
name: "standard CD FLAC image",
title: "(Rock) [CD] Thin Lizzy - Acoustic Sessions - 2024 (Decca Records EU 2025), FLAC (image+.cue), lossless",
wantArtist: "Thin Lizzy",
wantYear: 2024,
wantFormat: release.FormatFLAC,
wantSource: release.SourceCD,
wantRipType: "image+.cue",
wantParseOK: true,
},
{
name: "WEB release tracks",
title: "(Progressive Rock) [WEB] Opeth - In Cauda Venenum (Extended Edition) - 2019/2022, FLAC (tracks), lossless",
wantArtist: "Opeth",
wantYear: 2019,
wantFormat: release.FormatFLAC,
wantSource: release.SourceWEB,
wantRipType: "tracks",
wantParseOK: true,
},
{
name: "APE format",
title: "(Jazz) [CD] Miles Davis - Kind of Blue - 1959, APE (image+.cue), lossless",
wantArtist: "Miles Davis",
wantYear: 1959,
wantFormat: release.FormatAPE,
wantSource: release.SourceCD,
wantParseOK: true,
},
{
name: "tracks+cue format",
title: "(Classic Rock) [CD] The Who - Who Are You (Super Deluxe Edition) - 2025, FLAC (tracks+cue), lossless",
wantArtist: "The Who",
wantYear: 2025,
wantFormat: release.FormatFLAC,
wantRipType: "tracks+cue",
wantParseOK: true,
},
{
name: "multi-disc set",
title: "(Hard Rock, Glam Rock, Progressive Rock, Art Rock, Heavy Metal) [CD] Queen Queen I (2 CD) 2024 , FLAC (image+.cue), lossless",
wantArtist: "Queen",
wantYear: 2024,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "Japan release",
title: "(Pop-Rock Soft-Rock) [CD] Sting - The Soul Cages (Expanded Edition) - 2025 [Japan], FLAC (image+.cue), lossless",
wantArtist: "Sting",
wantYear: 2025,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "WavPack format",
title: "(Progressive Rock) [CD] Yes - Close to the Edge - 1972, WV (image+.cue), lossless",
wantArtist: "Yes",
wantYear: 1972,
wantFormat: release.FormatWavPack,
wantParseOK: true,
},
{
name: "default to FLAC when format not specified",
title: "(Rock) [CD] Pink Floyd - The Wall - 1979 (image+.cue), lossless",
wantArtist: "Pink Floyd",
wantYear: 1979,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "heavy metal WEB",
title: "(Heavy Metal) [WEB] Heaven & Hell - Breaking Out Of Heaven: 2007-2009 - 2026, FLAC (tracks), lossless",
wantArtist: "Heaven & Hell",
wantYear: 2026,
wantSource: release.SourceWEB,
wantParseOK: true,
},
{
name: "melodic rock WEB",
title: "(Melodic Rock, Progressive Rock) [WEB] James LaBrie - Beautiful Shade Of Grey - 2022, FLAC (tracks), lossless",
wantArtist: "James LaBrie",
wantYear: 2022,
wantSource: release.SourceWEB,
wantParseOK: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := p.Parse(tt.title)
if r.ParsedSuccessfully != tt.wantParseOK {
t.Errorf("ParsedSuccessfully = %v, want %v, errors: %v", r.ParsedSuccessfully, tt.wantParseOK, r.ParseErrors)
}
if tt.wantArtist != "" && r.Artist != tt.wantArtist {
t.Errorf("Artist = %q, want %q", r.Artist, tt.wantArtist)
}
if tt.wantYear != 0 && r.Year != tt.wantYear {
t.Errorf("Year = %d, want %d", r.Year, tt.wantYear)
}
if tt.wantFormat != release.FormatUnknown && r.Format != tt.wantFormat {
t.Errorf("Format = %v, want %v", r.Format, tt.wantFormat)
}
if tt.wantSource != release.SourceUnknown && r.Source != tt.wantSource {
t.Errorf("Source = %v, want %v", r.Source, tt.wantSource)
}
if tt.wantRipType != "" && r.RipType != tt.wantRipType {
t.Errorf("RipType = %q, want %q", r.RipType, tt.wantRipType)
}
if r.Bitrate != "lossless" {
t.Errorf("Bitrate = %q, want lossless", r.Bitrate)
}
})
}
}
@@ -0,0 +1,38 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type LossyParser struct {
BaseParser
}
func NewLossyParser() *LossyParser {
return &LossyParser{}
}
func (p *LossyParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
r.Type = p.DetectType(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Bitrate = p.ExtractBitrate(title)
r.Source = p.ExtractSource(title)
r.Tags = p.ExtractSpecialTags(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
if r.Format == release.FormatUnknown {
r.Format = release.FormatMP3
}
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
var _ Parser = (*LossyParser)(nil)
@@ -0,0 +1,134 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestLossyParser(t *testing.T) {
p := NewLossyParser()
tests := []struct {
name string
title string
wantArtist string
wantYear int
wantFormat release.AudioFormat
wantBitrate string
wantType release.Type
wantParseOK bool
}{
{
name: "VBR V0",
title: "(Pop) VA - Pop Classics Top 100 - 2012, MP3, VBR V0",
wantArtist: "VA",
wantYear: 2012,
wantFormat: release.FormatMP3,
wantBitrate: "V0",
wantType: release.TypeCompilation,
wantParseOK: true,
},
{
name: "VBR V0 kbps suffix",
title: "(Pop/Rock) VA - 101 Ultimate 80's (5 CD) - 2011, MP3 (tracks), VBR V0 kbps",
wantArtist: "VA",
wantYear: 2011,
wantFormat: release.FormatMP3,
wantBitrate: "V0",
wantParseOK: true,
},
{
name: "VBR V1",
title: "(Rock) VA - Greatest Ever! Rock The Definitive Collection (3 CD) - 2006, MP3 (tracks), VBR V1 kbps",
wantArtist: "VA",
wantYear: 2006,
wantBitrate: "V1",
wantParseOK: true,
},
{
name: "VBR V2",
title: "(Classic Rock) VA - Twist & Shout - 2005, MP3, VBR V2",
wantArtist: "VA",
wantYear: 2005,
wantBitrate: "V2",
wantParseOK: true,
},
{
name: "VBR range",
title: "(Pop, Rock) VA - The Essential 1980s - 2010, MP3 (tracks), VBR 192-320 kbps",
wantArtist: "VA",
wantYear: 2010,
wantBitrate: "VBR 192-320 kbps",
wantParseOK: true,
},
{
name: "CBR 320",
title: "(Pop) VA - Bravo Hits, Vol. 128 [2 CD] - 2025, MP3, 320 kbps",
wantArtist: "VA",
wantYear: 2025,
wantBitrate: "320 kbps",
wantParseOK: true,
},
{
name: "CBR 256",
title: "(rock'n'roll) Rock-n-roll. The best hits, MP3 (tracks), 256 kbps",
wantFormat: release.FormatMP3,
wantBitrate: "256 kbps",
wantParseOK: true,
},
{
name: "year range in title",
title: "(Pop) VA - Bravo Hits vol. 31-59 - 2000-2007, MP3, VBR 192-320 kbps",
wantArtist: "VA",
wantYear: 2000,
wantParseOK: true,
},
{
name: "discography in lossy",
title: "(Alternative Metal / Post-Grunge) Breaking Benjamin - Discography: 23 Releases, 2001-2024, MP3, VBR V0/320 kbps",
wantArtist: "Breaking Benjamin",
wantYear: 2001,
wantType: release.TypeDiscography,
wantParseOK: true,
},
{
name: "bootleg release",
title: "(Eurodance) VA - Beat Mix Eurodance Vol 1-3 (Bootlegs) - 2009-2011, MP3 (image), VBR V2 / V0",
wantArtist: "VA",
wantYear: 2009,
wantType: release.TypeBootleg,
wantParseOK: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := p.Parse(tt.title)
if r.ParsedSuccessfully != tt.wantParseOK {
t.Errorf("ParsedSuccessfully = %v, want %v, errors: %v", r.ParsedSuccessfully, tt.wantParseOK, r.ParseErrors)
}
if tt.wantArtist != "" && r.Artist != tt.wantArtist {
t.Errorf("Artist = %q, want %q", r.Artist, tt.wantArtist)
}
if tt.wantYear != 0 && r.Year != tt.wantYear {
t.Errorf("Year = %d, want %d", r.Year, tt.wantYear)
}
if tt.wantFormat != release.FormatUnknown && r.Format != tt.wantFormat {
t.Errorf("Format = %v, want %v", r.Format, tt.wantFormat)
}
if tt.wantBitrate != "" && r.Bitrate != tt.wantBitrate {
t.Errorf("Bitrate = %q, want %q", r.Bitrate, tt.wantBitrate)
}
if tt.wantType != release.TypeUnknown && r.Type != tt.wantType {
t.Errorf("Type = %v, want %v", r.Type, tt.wantType)
}
})
}
}
@@ -0,0 +1,40 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type MetalParser struct {
BaseParser
}
func NewMetalParser() *MetalParser {
return &MetalParser{}
}
func (p *MetalParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
if len(r.Genres) == 0 {
r.Genres = []string{"Metal"}
}
r.Type = p.DetectType(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Bitrate = p.ExtractBitrate(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.ReleaseCount = p.ExtractReleaseCount(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
var _ Parser = (*MetalParser)(nil)
@@ -0,0 +1,7 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type Parser interface {
Parse(title string) *release.Release
}
@@ -0,0 +1,100 @@
package parser
import "regexp"
var (
// Genre at start: (Rock), (Electronic, Ambient), (Jazz / Blues)
genrePattern = regexp.MustCompile(`^\s*\(([^)]+)\)\s*`)
// Label pack: Label: Name or Label - Name
labelPattern = regexp.MustCompile(`(?i)Label[:\-]\s*([^-(\[]+)`)
// Year: single or range
yearPattern = regexp.MustCompile(`\b((?:19|20)\d{2})\b`)
yearRangePattern = regexp.MustCompile(`\b((?:19|20)\d{2})\s*[-]\s*((?:19|20)\d{2})\b`)
// Release count: (15 CD), (30 albums), 10 releases, (50 релизов), 13 CD
releaseCountPattern = regexp.MustCompile(`(?:\()?(\d+)\s*(?:CD|albums?|releases?|релиз(?:а|ов)?|альбом(?:а|ов)?)(?:\))?`)
// Audio formats
formatPattern = regexp.MustCompile(`(?i)\b(FLAC|APE|MP3|AAC|OGG|WV|WavPack|ALAC|WAV|DSD\d*|DST\d*)\b`)
// Bitrate: 320 kbps, V0, VBR 192-320 kbps, lossless
bitratePattern = regexp.MustCompile(`(?i)(?:(\d{2,3})\s*kbps|V([012])|VBR\s*(?:~?(\d+)|(\d+)-(\d+))\s*kbps|lossless)`)
// Rip type: image+.cue, tracks+.cue, tracks
ripTypePattern = regexp.MustCompile(`(?i)(image\+\.?cue|tracks?\+\.?cue|tracks?)`)
// Hi-Res bit depth / sample rate: [24/96], [24/192], [24bit-48kHz]
hiResPattern = regexp.MustCompile(`\[(\d+)(?:/|bit[/-])(\d+(?:\.\d+)?)\s*(?:kHz)?\]`)
// DSD formats: DSD64, DSD128, DST64
dsdPattern = regexp.MustCompile(`(?i)\b(DSD|DST)(64|128|256|512)\b`)
// Source tags: [CD], [WEB], [LP], [Vinyl], [SACD], [DVDA]
sourceTagPattern = regexp.MustCompile(`(?i)\[(CD|WEB|LP|Vinyl|SACD|DVDA|HDAD|MINI-LP|EP|12"|10"|7")\]`)
// Vinyl condition: [NM], [EX], [VG+], [VG], [G], [Mint], [SS]
vinylConditionPattern = regexp.MustCompile(`\[(Mint|SS|NM|EX|VG\+?|G|F/?P)\]`)
// Special tags: [AI], [WEB], [TR24], [OF], [RM], [restored], [declipped]
specialTagPattern = regexp.MustCompile(`\[(AI|WEB|TR24|OF|RM|restored|declipped)\]`)
// Discography keywords (Russian + English)
discographyPattern = regexp.MustCompile(`(?i)\b([Дд]искографи[яи]|[Dd]iscograph(?:y|ies))\b`)
// Collection keywords
collectionPattern = regexp.MustCompile(`(?i)\b([Кк]оллекци[яи]|[Cc]ollection)\b`)
// Compilation keywords
compilationPattern = regexp.MustCompile(`(?i)\b([Сс]борник|[Cc]ompilation|[Vv]arious\s*[Aa]rtists?|VA)\b`)
// Anthology keywords
anthologyPattern = regexp.MustCompile(`(?i)\b([Аа]нтологи[яи]|[Aa]nthology)\b`)
// Best of / Greatest hits keywords
bestOfPattern = regexp.MustCompile(`(?i)\b([Ии]збранное|[Лл]учшее|[Bb]est\s*[Oo]f|[Gg]reatest\s*[Hh]its)\b`)
// Live / Concert keywords
livePattern = regexp.MustCompile(`(?i)\b([Жж]ивой|[Кк]онцерт|[Ll]ive|[Cc]oncert|[Ll]ive\s*[Aa]t)\b`)
// Bootleg keywords
bootlegPattern = regexp.MustCompile(`(?i)\b([Бб]утлеги?|[Bb]ootlegs?|[Uu]nofficial)\b`)
// Soundtrack keywords
soundtrackPattern = regexp.MustCompile(`(?i)\b(OST|[Ss]oundtrack|[Сс]аундтрек|[Ss]core|[Мм]узыка\s*(?:к|из)\s*фильм[ау])\b`)
// Remaster keywords
remasterPattern = regexp.MustCompile(`(?i)\b([Рр]емастер|[Rr]emaster(?:ed)?|[Пп]ереиздани[ея]|[Rr]e-?issue)\b`)
// EP keywords
epPattern = regexp.MustCompile(`(?i)\b(EP|[Мм]ини[-\s]?[Аа]льбом|[Ee]xtended\s*[Pp]lay)\b`)
// Single keywords
singlePattern = regexp.MustCompile(`(?i)\b([Сс]ингл|[Ss]ingle)\b`)
// Standard title format: Artist - Album - Year or (Genre) Artist - Album - Year
// Captures: artist, album, year
standardTitlePattern = regexp.MustCompile(`^(?:\([^)]+\)\s*)?(?:\[[^\]]+\]\s*)*([^-]+?)\s*[-]\s*(.+?)\s*[-]\s*((?:19|20)\d{2})`)
// Alternative: Artist - Album (Year)
altTitlePattern = regexp.MustCompile(`^(?:\([^)]+\)\s*)?(?:\[[^\]]+\]\s*)*([^-]+?)\s*[-]\s*(.+?)\s*\(((?:19|20)\d{2})\)`)
// Discography title: Artist - Дискография (15 CD) [1990-2020, ...]
discographyTitlePattern = regexp.MustCompile(`^(?:\([^)]+\)\s*)?(?:\[[^\]]+\]\s*)*([^-]+?)\s*[-]\s*(?:[Дд]искографи[яи]|[Dd]iscograph(?:y|ies))`)
// Collection title: Artist - Коллекция (50 CD) [1980-2019, ...]
collectionTitlePattern = regexp.MustCompile(`^(?:\([^)]+\)\s*)?(?:\[[^\]]+\]\s*)*([^-]+?)\s*[-]\s*(?:[Кк]оллекци[яи]|[Cc]ollection)`)
// Label pack title: (Genre) Label: Label Name (releases)
labelPackTitlePattern = regexp.MustCompile(`^(?:\([^)]+\)\s*)?(?i)Label:\s*([^(]+)`)
// Catalog number in brackets: [CAT001], [LABEL-001]
catalogNumPattern = regexp.MustCompile(`\[([A-Z]{2,}[-\s]?\d+[A-Z]*)\]`)
// Tags in brackets at start to strip: [RM], [restored], etc.
leadingTagsPattern = regexp.MustCompile(`^(\s*\[[^\]]+\]\s*)+`)
// Clean trailing technical info: , FLAC (image+.cue)
trailingTechPattern = regexp.MustCompile(`,?\s*(?:FLAC|APE|MP3|AAC|OGG|WV|WavPack|ALAC|WAV).*$`)
)
@@ -0,0 +1,36 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type SoundtracksParser struct {
BaseParser
}
func NewSoundtracksParser() *SoundtracksParser {
return &SoundtracksParser{}
}
func (p *SoundtracksParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
r.Type = release.TypeSoundtrack
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Bitrate = p.ExtractBitrate(title)
r.Source = p.ExtractSource(title)
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
var _ Parser = (*SoundtracksParser)(nil)
@@ -0,0 +1,44 @@
package parser
import "homelab.lan/music-agregator/internal/release"
type VinylDigitizationParser struct {
BaseParser
}
func NewVinylDigitizationParser() *VinylDigitizationParser {
return &VinylDigitizationParser{}
}
func (p *VinylDigitizationParser) Parse(title string) *release.Release {
r := p.NewRelease(title)
r.Genres = p.ExtractGenres(title)
r.Type = p.DetectType(title)
r.Year, r.YearEnd = p.ExtractYearRange(title)
r.Format = p.ExtractFormat(title)
r.Source = release.SourceVinyl
r.RipType = p.ExtractRipType(title)
r.Tags = p.ExtractSpecialTags(title)
r.Label = p.ExtractLabel(title)
r.CatalogNum = p.ExtractCatalogNum(title)
r.Artist, r.Album = p.ExtractArtistAlbum(title)
r.BitDepth, r.SampleRate = p.ExtractHiRes(title)
if r.Format == release.FormatUnknown {
r.Format = release.FormatFLAC
}
r.Bitrate = "lossless"
if condMatch := vinylConditionPattern.FindStringSubmatch(title); len(condMatch) >= 2 {
r.Tags = append(r.Tags, "Vinyl:"+condMatch[1])
}
if r.Artist == "" {
p.AddError(r, "failed to extract artist")
}
return r
}
var _ Parser = (*VinylDigitizationParser)(nil)
@@ -0,0 +1,147 @@
package parser
import (
"testing"
"homelab.lan/music-agregator/internal/release"
)
func TestVinylDigitizationParser(t *testing.T) {
p := NewVinylDigitizationParser()
tests := []struct {
name string
title string
wantArtist string
wantYear int
wantBitDepth int
wantSampleRate int
wantFormat release.AudioFormat
wantRipType string
wantParseOK bool
}{
{
name: "standard LP 24/192",
title: "(Pop-Rock/Punk) [LP] [24/192] Сектор Газа (Юрий Хой) - Ядрена вошь [Coloured, Remastered '2025] - 2026 (1990), WavPack (image+.cue)",
wantArtist: "Сектор Газа (Юрий Хой)",
wantYear: 2026,
wantBitDepth: 24,
wantSampleRate: 192000,
wantFormat: release.FormatWavPack,
wantRipType: "image+.cue",
wantParseOK: true,
},
{
name: "massive vinyl collection",
title: "(Synth-Pop) [LP/12''/10''/7''] [24/96] Depeche Mode - The Vinyl Collection (17 Albums, 66 Singles, 6 Compilations, 51 Bootlegs) (429 Releases) - 1981-2024, FLAC (tracks) lossless",
wantArtist: "Depeche Mode",
wantYear: 1981,
wantParseOK: true,
},
{
name: "2xLP 32bit",
title: "(Soft Rock, Pop Rock) [2xLP] [32/176.4] Genesis - Turn It On Again - The Hits - 1999(2024,Reissue, 25th anniversary.), WavPack (tracks)",
wantArtist: "Genesis",
wantYear: 1999,
wantBitDepth: 32,
wantSampleRate: 176400,
wantFormat: release.FormatWavPack,
wantParseOK: true,
},
{
name: "32/384 ultra high res",
title: "(Prog Rock) [LP] [32/384] Emerson, Lake & Palmer-Emerson, Lake & Palmer - 2025 (1970), WavPack (tracks)",
wantYear: 2025,
wantBitDepth: 32,
wantSampleRate: 384000,
wantFormat: release.FormatWavPack,
wantParseOK: true,
},
{
name: "soul LP",
title: "(Soul, Funk) [LP] [24/192] Curtis Mayfield - Curtis - 1970/2025, FLAC (tracks)",
wantArtist: "Curtis Mayfield",
wantYear: 1970,
wantBitDepth: 24,
wantSampleRate: 192000,
wantFormat: release.FormatFLAC,
wantParseOK: true,
},
{
name: "16/44 standard",
title: "(Rock) [LP] [16/44] Tony Sheridan - Collection 4LP - 1976-1987, FLAC (image+.cue)",
wantArtist: "Tony Sheridan",
wantYear: 1976,
wantParseOK: true,
},
{
name: "MFSL pressing",
title: "(Rock, Pop Rock) [LP] [24/96] Fleetwood Mac Mirage - 1982 (1984 MFSL 1-119), FLAC (tracks)",
wantArtist: "Fleetwood Mac",
wantYear: 1982,
wantParseOK: true,
},
{
name: "multiple LPs in one",
title: "(Rock) [LP] [24/96] 10cc - 2LP's - 1976, 1977, FLAC (tracks+.cue)",
wantArtist: "10cc",
wantYear: 1976,
wantRipType: "tracks+.cue",
wantParseOK: true,
},
{
name: "collection from vinyl",
title: "(Progressive Rock) [LP] [24/192] Marillion, Fish - Vinyl Collection - 1982-1994 (6 releases), FLAC (image+.cue)",
wantArtist: "Marillion, Fish",
wantYear: 1982,
wantParseOK: true,
},
{
name: "Japan vinyl",
title: "(Pop) [LP][24/96] Abba \"The Album\" Original Japan vinyl - 1977, FLAC (tracks)",
wantArtist: "Abba",
wantYear: 1977,
wantBitDepth: 24,
wantSampleRate: 96000,
wantParseOK: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := p.Parse(tt.title)
if r.ParsedSuccessfully != tt.wantParseOK {
t.Errorf("ParsedSuccessfully = %v, want %v, errors: %v", r.ParsedSuccessfully, tt.wantParseOK, r.ParseErrors)
}
if tt.wantArtist != "" && r.Artist != tt.wantArtist {
t.Errorf("Artist = %q, want %q", r.Artist, tt.wantArtist)
}
if tt.wantYear != 0 && r.Year != tt.wantYear {
t.Errorf("Year = %d, want %d", r.Year, tt.wantYear)
}
if tt.wantBitDepth != 0 && r.BitDepth != tt.wantBitDepth {
t.Errorf("BitDepth = %d, want %d", r.BitDepth, tt.wantBitDepth)
}
if tt.wantSampleRate != 0 && r.SampleRate != tt.wantSampleRate {
t.Errorf("SampleRate = %d, want %d", r.SampleRate, tt.wantSampleRate)
}
if tt.wantFormat != release.FormatUnknown && r.Format != tt.wantFormat {
t.Errorf("Format = %v, want %v", r.Format, tt.wantFormat)
}
if tt.wantRipType != "" && r.RipType != tt.wantRipType {
t.Errorf("RipType = %q, want %q", r.RipType, tt.wantRipType)
}
if r.Source != release.SourceVinyl {
t.Errorf("Source = %v, want Vinyl", r.Source)
}
})
}
}
+27 -3
View File
@@ -8,11 +8,35 @@ service IndexerService {
}
message SearchRequest {
string indexer = 1;
string query = 2;
int32 limit = 3;
string query = 1;
int32 limit = 2;
string tracker = 3;
}
message SearchResponse {
repeated SearchItem result = 1;
}
message SearchItem {
string title = 1;
string link = 2;
string guid = 3;
string pub_date = 4;
int64 size = 5;
string description = 6;
repeated string categories = 7;
Enclosure enclosure = 8;
repeated TorznabAttr torznab_attrs = 9;
}
message Enclosure {
string url = 1;
int64 length = 2;
string type = 3;
}
message TorznabAttr {
string name = 1;
string value = 2;
}
message CapabilitiesRequest {