From bfef1b6c79d039b486d440bc38c0ae57a5e74236 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 4 May 2026 22:48:14 +0200 Subject: [PATCH] Implement Jackett search entpoint --- api/Indexer/Search.bru | 25 ++ api/Jackett/Search.bru | 8 +- cmd/music-agregator/main.go | 1 - docs/rules/rutracker/classical.md | 224 ++++++++++++++ docs/rules/rutracker/discography.md | 125 ++++++++ docs/rules/rutracker/general.md | 273 ++++++++++++++++++ docs/rules/rutracker/hi-res.md | 163 +++++++++++ docs/rules/rutracker/index.md | 180 ++++++++++++ docs/rules/rutracker/jazz.md | 263 +++++++++++++++++ docs/rules/rutracker/label-packs.md | 185 ++++++++++++ docs/rules/rutracker/lossless.md | 171 +++++++++++ docs/rules/rutracker/lossy.md | 104 +++++++ docs/rules/rutracker/metal.md | 189 ++++++++++++ docs/rules/rutracker/soundtracks.md | 144 +++++++++ docs/rules/rutracker/vinyl-digitization.md | 128 ++++++++ internal/indexer/indexer.go | 104 +------ internal/indexer/indexer_capabilities.go | 103 +++++++ internal/indexer/jackett.go | 39 ++- internal/indexer/search.go | 71 +++++ internal/indexer/server.go | 10 +- internal/release/release.go | 178 ++++++++++++ internal/tracker/rutracker/parser/base.go | 256 ++++++++++++++++ .../tracker/rutracker/parser/classical.go | 40 +++ .../tracker/rutracker/parser/discography.go | 56 ++++ .../rutracker/parser/discography_test.go | 152 ++++++++++ internal/tracker/rutracker/parser/general.go | 37 +++ .../tracker/rutracker/parser/general_test.go | 166 +++++++++++ internal/tracker/rutracker/parser/hires.go | 47 +++ .../tracker/rutracker/parser/hires_test.go | 133 +++++++++ internal/tracker/rutracker/parser/jazz.go | 40 +++ .../tracker/rutracker/parser/label_packs.go | 35 +++ .../rutracker/parser/label_packs_test.go | 143 +++++++++ internal/tracker/rutracker/parser/lossless.go | 39 +++ .../tracker/rutracker/parser/lossless_test.go | 143 +++++++++ internal/tracker/rutracker/parser/lossy.go | 38 +++ .../tracker/rutracker/parser/lossy_test.go | 134 +++++++++ internal/tracker/rutracker/parser/metal.go | 40 +++ internal/tracker/rutracker/parser/parser.go | 7 + internal/tracker/rutracker/parser/patterns.go | 100 +++++++ .../tracker/rutracker/parser/soundtracks.go | 36 +++ .../rutracker/parser/vinyl_digitization.go | 44 +++ .../parser/vinyl_digitization_test.go | 147 ++++++++++ .../music_agregator/indexer/v1/indexer.proto | 30 +- 43 files changed, 4437 insertions(+), 114 deletions(-) create mode 100644 api/Indexer/Search.bru create mode 100644 docs/rules/rutracker/classical.md create mode 100644 docs/rules/rutracker/discography.md create mode 100644 docs/rules/rutracker/general.md create mode 100644 docs/rules/rutracker/hi-res.md create mode 100644 docs/rules/rutracker/index.md create mode 100644 docs/rules/rutracker/jazz.md create mode 100644 docs/rules/rutracker/label-packs.md create mode 100644 docs/rules/rutracker/lossless.md create mode 100644 docs/rules/rutracker/lossy.md create mode 100644 docs/rules/rutracker/metal.md create mode 100644 docs/rules/rutracker/soundtracks.md create mode 100644 docs/rules/rutracker/vinyl-digitization.md create mode 100644 internal/indexer/indexer_capabilities.go create mode 100644 internal/indexer/search.go create mode 100644 internal/release/release.go create mode 100644 internal/tracker/rutracker/parser/base.go create mode 100644 internal/tracker/rutracker/parser/classical.go create mode 100644 internal/tracker/rutracker/parser/discography.go create mode 100644 internal/tracker/rutracker/parser/discography_test.go create mode 100644 internal/tracker/rutracker/parser/general.go create mode 100644 internal/tracker/rutracker/parser/general_test.go create mode 100644 internal/tracker/rutracker/parser/hires.go create mode 100644 internal/tracker/rutracker/parser/hires_test.go create mode 100644 internal/tracker/rutracker/parser/jazz.go create mode 100644 internal/tracker/rutracker/parser/label_packs.go create mode 100644 internal/tracker/rutracker/parser/label_packs_test.go create mode 100644 internal/tracker/rutracker/parser/lossless.go create mode 100644 internal/tracker/rutracker/parser/lossless_test.go create mode 100644 internal/tracker/rutracker/parser/lossy.go create mode 100644 internal/tracker/rutracker/parser/lossy_test.go create mode 100644 internal/tracker/rutracker/parser/metal.go create mode 100644 internal/tracker/rutracker/parser/parser.go create mode 100644 internal/tracker/rutracker/parser/patterns.go create mode 100644 internal/tracker/rutracker/parser/soundtracks.go create mode 100644 internal/tracker/rutracker/parser/vinyl_digitization.go create mode 100644 internal/tracker/rutracker/parser/vinyl_digitization_test.go diff --git a/api/Indexer/Search.bru b/api/Indexer/Search.bru new file mode 100644 index 0000000..3f0e1f9 --- /dev/null +++ b/api/Indexer/Search.bru @@ -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 + } + ''' +} diff --git a/api/Jackett/Search.bru b/api/Jackett/Search.bru index 7f0d565..4772a76 100644 --- a/api/Jackett/Search.bru +++ b/api/Jackett/Search.bru @@ -5,16 +5,16 @@ meta { } get { - url: http://localhost:9117/api/v2.0/indexers/all/results/torznab?apikey=3jfvdvt1etzz36drkw5id5sb95sc47fi&limit=1&artist=Metallica&t=music + url: http://localhost:9117/api/v2.0/indexers/all/results/torznab?apikey=3jfvdvt1etzz36drkw5id5sb95sc47fi&limit=2&q=Metallica&t=search body: none auth: inherit } params:query { apikey: 3jfvdvt1etzz36drkw5id5sb95sc47fi - limit: 1 - artist: Metallica - t: music + limit: 2 + q: Metallica + t: search } settings { diff --git a/cmd/music-agregator/main.go b/cmd/music-agregator/main.go index 74b9c93..5e2465a 100644 --- a/cmd/music-agregator/main.go +++ b/cmd/music-agregator/main.go @@ -69,7 +69,6 @@ func serveGrpc(config config.Config) { } listener, err := net.Listen("tcp", fmt.Sprintf("%v:%v", config.App.Host, config.App.Port)) - if err != nil { log.Fatal().Err(err).Msg("Failed to listen on localhost:8081") } diff --git a/docs/rules/rutracker/classical.md b/docs/rules/rutracker/classical.md new file mode 100644 index 0000000..216e04d --- /dev/null +++ b/docs/rules/rutracker/classical.md @@ -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 diff --git a/docs/rules/rutracker/discography.md b/docs/rules/rutracker/discography.md new file mode 100644 index 0000000..3b9fbd2 --- /dev/null +++ b/docs/rules/rutracker/discography.md @@ -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,/]+), +``` diff --git a/docs/rules/rutracker/general.md b/docs/rules/rutracker/general.md new file mode 100644 index 0000000..4cf1a32 --- /dev/null +++ b/docs/rules/rutracker/general.md @@ -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 +``` diff --git a/docs/rules/rutracker/hi-res.md b/docs/rules/rutracker/hi-res.md new file mode 100644 index 0000000..59f28b6 --- /dev/null +++ b/docs/rules/rutracker/hi-res.md @@ -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 diff --git a/docs/rules/rutracker/index.md b/docs/rules/rutracker/index.md new file mode 100644 index 0000000..1fc7acc --- /dev/null +++ b/docs/rules/rutracker/index.md @@ -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 diff --git a/docs/rules/rutracker/jazz.md b/docs/rules/rutracker/jazz.md new file mode 100644 index 0000000..0423129 --- /dev/null +++ b/docs/rules/rutracker/jazz.md @@ -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]+)\] +``` diff --git a/docs/rules/rutracker/label-packs.md b/docs/rules/rutracker/label-packs.md new file mode 100644 index 0000000..e6add03 --- /dev/null +++ b/docs/rules/rutracker/label-packs.md @@ -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 diff --git a/docs/rules/rutracker/lossless.md b/docs/rules/rutracker/lossless.md new file mode 100644 index 0000000..4303c69 --- /dev/null +++ b/docs/rules/rutracker/lossless.md @@ -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|кГц) +``` diff --git a/docs/rules/rutracker/lossy.md b/docs/rules/rutracker/lossy.md new file mode 100644 index 0000000..eb627f4 --- /dev/null +++ b/docs/rules/rutracker/lossy.md @@ -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\] +``` diff --git a/docs/rules/rutracker/metal.md b/docs/rules/rutracker/metal.md new file mode 100644 index 0000000..9de9ef8 --- /dev/null +++ b/docs/rules/rutracker/metal.md @@ -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.]+)*) +``` diff --git a/docs/rules/rutracker/soundtracks.md b/docs/rules/rutracker/soundtracks.md new file mode 100644 index 0000000..1916100 --- /dev/null +++ b/docs/rules/rutracker/soundtracks.md @@ -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) diff --git a/docs/rules/rutracker/vinyl-digitization.md b/docs/rules/rutracker/vinyl-digitization.md new file mode 100644 index 0000000..a72ecd0 --- /dev/null +++ b/docs/rules/rutracker/vinyl-digitization.md @@ -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) diff --git a/internal/indexer/indexer.go b/internal/indexer/indexer.go index 9fcc962..d53c7cc 100644 --- a/internal/indexer/indexer.go +++ b/internal/indexer/indexer.go @@ -1,108 +1,6 @@ package indexer -import ( - "encoding/xml" - "strings" - - pb "homelab.lan/music-agregator/gen/music_agregator/indexer/v1" -) - type Indexer interface { - Search() + Search(query string, limit int32, indexer string) (SearchResult, error) Capabilities(indexerName string) (IndexerCapabilities, error) } - -type IndexerCapabilities struct { - XMLName xml.Name `xml:"caps"` - Server Server `xml:"server"` - Limits Limits `xml:"limits"` - Searching Searching `xml:"searching"` - Categories []Category `xml:"categories>category"` -} - -type Server struct { - Title string `xml:"title,attr"` -} - -type Limits struct { - Default int `xml:"default,attr"` - Max int `xml:"max,attr"` -} - -type Searching struct { - Search SearchCapability `xml:"search"` - TvSearch SearchCapability `xml:"tv-search"` - MovieSearch SearchCapability `xml:"movie-search"` - MusicSearch SearchCapability `xml:"music-search"` - AudioSearch SearchCapability `xml:"audio-search"` - BookSearch SearchCapability `xml:"book-search"` -} - -type SearchCapability struct { - Available string `xml:"available,attr"` - SupportedParams string `xml:"supportedParams,attr"` - SearchEngine string `xml:"searchEngine,attr"` -} - -type Category struct { - ID int `xml:"id,attr"` - Name string `xml:"name,attr"` - Subcats []Subcat `xml:"subcat"` -} - -type Subcat struct { - ID int `xml:"id,attr"` - Name string `xml:"name,attr"` -} - -func (c *IndexerCapabilities) ToProto() *pb.CapabilitiesResponse { - return &pb.CapabilitiesResponse{ - Server: &pb.Server{ - Title: c.Server.Title, - }, - Limits: &pb.Limits{ - Default: int32(c.Limits.Default), - Max: int32(c.Limits.Max), - }, - Searching: &pb.Searching{ - Search: c.Searching.Search.toProto(), - TvSearch: c.Searching.TvSearch.toProto(), - MovieSearch: c.Searching.MovieSearch.toProto(), - MusicSearch: c.Searching.MusicSearch.toProto(), - AudioSearch: c.Searching.AudioSearch.toProto(), - BookSearch: c.Searching.BookSearch.toProto(), - }, - Categories: c.categoriesToProto(), - } -} - -func (s *SearchCapability) toProto() *pb.SearchCapability { - var params []string - if s.SupportedParams != "" { - params = strings.Split(s.SupportedParams, ",") - } - return &pb.SearchCapability{ - Available: s.Available == "yes", - SupportedParams: params, - SearchEngine: s.SearchEngine, - } -} - -func (c *IndexerCapabilities) categoriesToProto() []*pb.Category { - categories := make([]*pb.Category, len(c.Categories)) - for i, cat := range c.Categories { - subcats := make([]*pb.Subcat, len(cat.Subcats)) - for j, sub := range cat.Subcats { - subcats[j] = &pb.Subcat{ - Id: int32(sub.ID), - Name: sub.Name, - } - } - categories[i] = &pb.Category{ - Id: int32(cat.ID), - Name: cat.Name, - Subcats: subcats, - } - } - return categories -} diff --git a/internal/indexer/indexer_capabilities.go b/internal/indexer/indexer_capabilities.go new file mode 100644 index 0000000..d0970fc --- /dev/null +++ b/internal/indexer/indexer_capabilities.go @@ -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 +} diff --git a/internal/indexer/jackett.go b/internal/indexer/jackett.go index edfc06c..75960d8 100644 --- a/internal/indexer/jackett.go +++ b/internal/indexer/jackett.go @@ -25,8 +25,43 @@ func NewIndexer(cfg config.Config) Indexer { } } -func (indexer *JacketIndexer) Search() { - log.Warn().Msg("Unimplemented method search on the Jacket Indexer") +func (indexer *JacketIndexer) Search(query string, limit int32, tracker string) (SearchResult, error) { + searchTracker := "all" + if len(tracker) != 0 { + searchTracker = tracker + } + + url := indexer.cfg.Indexer.Url + uri := fmt.Sprintf("%v/api/v2.0/indexers/%v/results/torznab?apikey=%v&limit=%d&q=%v&t=search", url, searchTracker, indexer.cfg.Indexer.ApiKey, limit, query) + + log.Debug().Str("uri", uri).Msg("Sending search request") + + req, err := http.NewRequest("GET", uri, nil) + if err != nil { + log.Error().Err(err).Msg("Error creating request") + return SearchResult{}, err + } + + resp, err := indexer.client.Do(req) + if err != nil { + log.Error().Err(err).Msg("Error making search request") + return SearchResult{}, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Error().Err(err).Msg("Error reading search response body") + return SearchResult{}, err + } + + var searchResult SearchResult + if err := xml.Unmarshal(body, &searchResult); err != nil { + log.Error().Err(err).Msg("Error parsing search XML") + return SearchResult{}, err + } + + return searchResult, nil } func (indexer *JacketIndexer) Capabilities(indexerName string) (IndexerCapabilities, error) { diff --git a/internal/indexer/search.go b/internal/indexer/search.go new file mode 100644 index 0000000..0c45199 --- /dev/null +++ b/internal/indexer/search.go @@ -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, + } +} diff --git a/internal/indexer/server.go b/internal/indexer/server.go index 3c4bae7..4c1ee81 100644 --- a/internal/indexer/server.go +++ b/internal/indexer/server.go @@ -6,6 +6,7 @@ import ( "github.com/rs/zerolog/log" "google.golang.org/grpc" + pb "homelab.lan/music-agregator/gen/music_agregator/indexer/v1" "homelab.lan/music-agregator/internal/config" ) @@ -27,7 +28,14 @@ func NewIndexerServer(cfg config.Config) (*IndexerServer, error) { } func (server *IndexerServer) Search(ctx context.Context, req *pb.SearchRequest) (*pb.SearchResponse, error) { - return &pb.SearchResponse{}, nil + log.Debug().Str("query", req.GetQuery()).Int32("limit", req.GetLimit()).Str("indexer", req.GetTracker()).Msg("Running search with these prams") + searchResult, err := server.indexer.Search(req.GetQuery(), req.GetLimit(), req.GetTracker()) + if err != nil { + log.Error().Err(err).Msg("Failed to search in indexer") + return nil, err + } + + return searchResult.ToProto(), nil } func (server *IndexerServer) Capabilities(ctx context.Context, req *pb.CapabilitiesRequest) (*pb.CapabilitiesResponse, error) { diff --git a/internal/release/release.go b/internal/release/release.go new file mode 100644 index 0000000..1298ebe --- /dev/null +++ b/internal/release/release.go @@ -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 +} diff --git a/internal/tracker/rutracker/parser/base.go b/internal/tracker/rutracker/parser/base.go new file mode 100644 index 0000000..944fdc3 --- /dev/null +++ b/internal/tracker/rutracker/parser/base.go @@ -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 +} diff --git a/internal/tracker/rutracker/parser/classical.go b/internal/tracker/rutracker/parser/classical.go new file mode 100644 index 0000000..20c5422 --- /dev/null +++ b/internal/tracker/rutracker/parser/classical.go @@ -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) diff --git a/internal/tracker/rutracker/parser/discography.go b/internal/tracker/rutracker/parser/discography.go new file mode 100644 index 0000000..8c99b15 --- /dev/null +++ b/internal/tracker/rutracker/parser/discography.go @@ -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) diff --git a/internal/tracker/rutracker/parser/discography_test.go b/internal/tracker/rutracker/parser/discography_test.go new file mode 100644 index 0000000..72efd8a --- /dev/null +++ b/internal/tracker/rutracker/parser/discography_test.go @@ -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) + } + }) + } +} diff --git a/internal/tracker/rutracker/parser/general.go b/internal/tracker/rutracker/parser/general.go new file mode 100644 index 0000000..4403b13 --- /dev/null +++ b/internal/tracker/rutracker/parser/general.go @@ -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) diff --git a/internal/tracker/rutracker/parser/general_test.go b/internal/tracker/rutracker/parser/general_test.go new file mode 100644 index 0000000..e57b87d --- /dev/null +++ b/internal/tracker/rutracker/parser/general_test.go @@ -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)) + } + } + }) + } +} diff --git a/internal/tracker/rutracker/parser/hires.go b/internal/tracker/rutracker/parser/hires.go new file mode 100644 index 0000000..614d9a0 --- /dev/null +++ b/internal/tracker/rutracker/parser/hires.go @@ -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) diff --git a/internal/tracker/rutracker/parser/hires_test.go b/internal/tracker/rutracker/parser/hires_test.go new file mode 100644 index 0000000..6d539c8 --- /dev/null +++ b/internal/tracker/rutracker/parser/hires_test.go @@ -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) + } + }) + } +} diff --git a/internal/tracker/rutracker/parser/jazz.go b/internal/tracker/rutracker/parser/jazz.go new file mode 100644 index 0000000..6693a70 --- /dev/null +++ b/internal/tracker/rutracker/parser/jazz.go @@ -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) diff --git a/internal/tracker/rutracker/parser/label_packs.go b/internal/tracker/rutracker/parser/label_packs.go new file mode 100644 index 0000000..42fddaa --- /dev/null +++ b/internal/tracker/rutracker/parser/label_packs.go @@ -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) diff --git a/internal/tracker/rutracker/parser/label_packs_test.go b/internal/tracker/rutracker/parser/label_packs_test.go new file mode 100644 index 0000000..87e094c --- /dev/null +++ b/internal/tracker/rutracker/parser/label_packs_test.go @@ -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) + } + }) + } +} diff --git a/internal/tracker/rutracker/parser/lossless.go b/internal/tracker/rutracker/parser/lossless.go new file mode 100644 index 0000000..b73ba35 --- /dev/null +++ b/internal/tracker/rutracker/parser/lossless.go @@ -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) diff --git a/internal/tracker/rutracker/parser/lossless_test.go b/internal/tracker/rutracker/parser/lossless_test.go new file mode 100644 index 0000000..c6cc765 --- /dev/null +++ b/internal/tracker/rutracker/parser/lossless_test.go @@ -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) + } + }) + } +} diff --git a/internal/tracker/rutracker/parser/lossy.go b/internal/tracker/rutracker/parser/lossy.go new file mode 100644 index 0000000..842b70e --- /dev/null +++ b/internal/tracker/rutracker/parser/lossy.go @@ -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) diff --git a/internal/tracker/rutracker/parser/lossy_test.go b/internal/tracker/rutracker/parser/lossy_test.go new file mode 100644 index 0000000..9c2fef2 --- /dev/null +++ b/internal/tracker/rutracker/parser/lossy_test.go @@ -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) + } + }) + } +} diff --git a/internal/tracker/rutracker/parser/metal.go b/internal/tracker/rutracker/parser/metal.go new file mode 100644 index 0000000..d19db46 --- /dev/null +++ b/internal/tracker/rutracker/parser/metal.go @@ -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) diff --git a/internal/tracker/rutracker/parser/parser.go b/internal/tracker/rutracker/parser/parser.go new file mode 100644 index 0000000..cc61f23 --- /dev/null +++ b/internal/tracker/rutracker/parser/parser.go @@ -0,0 +1,7 @@ +package parser + +import "homelab.lan/music-agregator/internal/release" + +type Parser interface { + Parse(title string) *release.Release +} diff --git a/internal/tracker/rutracker/parser/patterns.go b/internal/tracker/rutracker/parser/patterns.go new file mode 100644 index 0000000..241e221 --- /dev/null +++ b/internal/tracker/rutracker/parser/patterns.go @@ -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).*$`) +) diff --git a/internal/tracker/rutracker/parser/soundtracks.go b/internal/tracker/rutracker/parser/soundtracks.go new file mode 100644 index 0000000..130349b --- /dev/null +++ b/internal/tracker/rutracker/parser/soundtracks.go @@ -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) diff --git a/internal/tracker/rutracker/parser/vinyl_digitization.go b/internal/tracker/rutracker/parser/vinyl_digitization.go new file mode 100644 index 0000000..6e24597 --- /dev/null +++ b/internal/tracker/rutracker/parser/vinyl_digitization.go @@ -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) diff --git a/internal/tracker/rutracker/parser/vinyl_digitization_test.go b/internal/tracker/rutracker/parser/vinyl_digitization_test.go new file mode 100644 index 0000000..49dba94 --- /dev/null +++ b/internal/tracker/rutracker/parser/vinyl_digitization_test.go @@ -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) + } + }) + } +} diff --git a/proto/music_agregator/indexer/v1/indexer.proto b/proto/music_agregator/indexer/v1/indexer.proto index 61a4b16..89a90e2 100644 --- a/proto/music_agregator/indexer/v1/indexer.proto +++ b/proto/music_agregator/indexer/v1/indexer.proto @@ -8,11 +8,35 @@ service IndexerService { } message SearchRequest { - string indexer = 1; - string query = 2; - int32 limit = 3; + string query = 1; + int32 limit = 2; + string tracker = 3; } message SearchResponse { + repeated SearchItem result = 1; +} + +message SearchItem { + string title = 1; + string link = 2; + string guid = 3; + string pub_date = 4; + int64 size = 5; + string description = 6; + repeated string categories = 7; + Enclosure enclosure = 8; + repeated TorznabAttr torznab_attrs = 9; +} + +message Enclosure { + string url = 1; + int64 length = 2; + string type = 3; +} + +message TorznabAttr { + string name = 1; + string value = 2; } message CapabilitiesRequest {