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 {
|
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
|
body: none
|
||||||
auth: inherit
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
params:query {
|
params:query {
|
||||||
apikey: 3jfvdvt1etzz36drkw5id5sb95sc47fi
|
apikey: 3jfvdvt1etzz36drkw5id5sb95sc47fi
|
||||||
limit: 1
|
limit: 2
|
||||||
artist: Metallica
|
q: Metallica
|
||||||
t: music
|
t: search
|
||||||
}
|
}
|
||||||
|
|
||||||
settings {
|
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))
|
listener, err := net.Listen("tcp", fmt.Sprintf("%v:%v", config.App.Host, config.App.Port))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to listen on localhost:8081")
|
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
|
package indexer
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
pb "homelab.lan/music-agregator/gen/music_agregator/indexer/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Indexer interface {
|
type Indexer interface {
|
||||||
Search()
|
Search(query string, limit int32, indexer string) (SearchResult, error)
|
||||||
Capabilities(indexerName string) (IndexerCapabilities, 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() {
|
func (indexer *JacketIndexer) Search(query string, limit int32, tracker string) (SearchResult, error) {
|
||||||
log.Warn().Msg("Unimplemented method search on the Jacket Indexer")
|
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) {
|
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"
|
"github.com/rs/zerolog/log"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
pb "homelab.lan/music-agregator/gen/music_agregator/indexer/v1"
|
pb "homelab.lan/music-agregator/gen/music_agregator/indexer/v1"
|
||||||
"homelab.lan/music-agregator/internal/config"
|
"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) {
|
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) {
|
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 {
|
message SearchRequest {
|
||||||
string indexer = 1;
|
string query = 1;
|
||||||
string query = 2;
|
int32 limit = 2;
|
||||||
int32 limit = 3;
|
string tracker = 3;
|
||||||
}
|
}
|
||||||
message SearchResponse {
|
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 {
|
message CapabilitiesRequest {
|
||||||
|
|||||||
Reference in New Issue
Block a user