Implement Jackett search entpoint
This commit is contained in:
@@ -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
|
||||
}
|
||||
'''
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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** - XI–XIV века
|
||||
- **Renaissance** - 1400–1600
|
||||
- **Baroque** - 1600–1750
|
||||
- **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
|
||||
@@ -0,0 +1,125 @@
|
||||
# Разъяснения по дискографиям и коллекциям RuTracker
|
||||
|
||||
**Источник:** https://rutracker.org/forum/viewtopic.php?t=6372771
|
||||
|
||||
## Определения
|
||||
|
||||
### Дискография (Discography)
|
||||
- Содержит слово "Дискография" или "Discography"
|
||||
- Включает релизы только под **ОДНИМ** именем исполнителя
|
||||
- Формат количества: `(15 CD)`, `(20 albums)`, `(10 releases)`
|
||||
- Формат годов: `[1990–2020]` (используется длинное тире)
|
||||
|
||||
### Коллекция (Collection)
|
||||
- Содержит слово "Коллекция" или "Collection"
|
||||
- Может включать **несколько** имён/псевдонимов одного артиста
|
||||
- Включает сайд-проекты, сольные работы, коллаборации
|
||||
- Тот же формат количества и годов
|
||||
|
||||
## Ключевые различия
|
||||
|
||||
| Критерий | Дискография | Коллекция |
|
||||
|----------|-------------|-----------|
|
||||
| Имена артиста | Только одно | Несколько разрешено |
|
||||
| Псевдонимы | Не включены | Включены |
|
||||
| Сайд-проекты | Отдельно | Включены |
|
||||
| Коллаборации | Отдельно | Включены |
|
||||
|
||||
## Формат заголовка
|
||||
|
||||
### Дискография:
|
||||
```
|
||||
[Artist] - Дискография (15 CD) [1990–2020, Rock, MP3, 320 kbps]
|
||||
[Artist] - Discography (30 releases) [1985–2023, Electronic, FLAC]
|
||||
```
|
||||
|
||||
### Коллекция:
|
||||
```
|
||||
[Artist] - Коллекция (50 CD) [1980–2019, Various, FLAC]
|
||||
[Artist] - Collection (100 albums) [1975–2025, 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,/]+),
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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]+)\]
|
||||
```
|
||||
@@ -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
|
||||
@@ -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|кГц)
|
||||
```
|
||||
@@ -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\]
|
||||
```
|
||||
@@ -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.]+)*)
|
||||
```
|
||||
@@ -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)
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user