diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8123a26 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,56 @@ +# CLAUDE.md + +Памятка для работы над jellybit. Перед задачей прочитай также +[README.md](README.md), [BRIEF.md](BRIEF.md) и +[docs/specs/architecture.md](docs/specs/architecture.md). + +## Что это + +Связующий сервис qBittorrent ↔ Jellyfin: принимает торрент + контекст, +качает, распознаёт фильм/сериал (LLM + контекст + опц. метабазы) и +раскладывает файлы для Jellyfin хардлинками. Деплоится на домашний +медиа-сервер umbar (`/home/av/projects/private/umbar`) — туда копируется +готовый бинарь. + +## Стек и принципы + +- **Go**, один статический бинарь (`CGO_ENABLED=0`). Почему — см. + [ADR-2026-06-13-go-single-binary](docs/adr/ADR-2026-06-13-go-single-binary.md). +- **SQLite** как хранилище (чистый Go-драйвер `modernc.org/sqlite`). +- **Конфигурация — TOML**. **Логи — структурированный JSON** (`log/slog`). +- **Хардлинки, источник не трогаем** — qBittorrent продолжает раздачу, + диск не дублируется. +- **Единое ядро, тонкие транспорты** — вся логика приёма в use-case + `Ingest`; HTTP API, веб-UI и Telegram — лишь обёртки над ним. +- **Минимум компонентов** — в духе umbar, без зоопарка сервисов. Внешние + базы метаданных (TMDB/TVDB) опциональны, включаются конфигом. + +## Документация: три раздела + +- `docs/specs/` — **живые** спецификации целевого состояния. Меняем по + мере развития, держим в соответствии с кодом. +- `docs/adr/` — **неизменяемый** журнал решений, пишется постфактум, + хранит *почему*. Правила — [docs/adr/README.md](docs/adr/README.md). +- `docs/drafts/` — черновики: планы, идеи, ещё не принятые решения. Не + источник истины. + +## Язык + +- Документация, комментарии, сообщения коммитов — **русский**. +- Код и идентификаторы — английский. + +## Команды + +Кода ещё нет (фаза каркаса). По мере появления Ф0: + +- сборка: `go build ./cmd/jellybit` +- тесты: `go test ./...` +- линт: `golangci-lint run` + +## Конвенции кода + +- Раскладка: `cmd/jellybit` (точка входа) + `internal/<пакет>` по + компонентам из [architecture.md](docs/specs/architecture.md). +- Ошибки оборачиваем с контекстом (`fmt.Errorf("...: %w", err)`). +- Логирование только через `slog`, без `fmt.Println`. +- Время — всегда с явным TZ (сервер в `Europe/Moscow`). diff --git a/README.md b/README.md new file mode 100644 index 0000000..c58907a --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# Jellybit + +Jellybit — связующий сервис между qBittorrent и Jellyfin. Принимает +торрент (magnet, `.torrent` или ссылку) вместе с текстовым контекстом, +ставит загрузку в qBittorrent, дожидается её завершения, распознаёт +содержимое (фильм или сериал, сезоны и серии) и раскладывает готовые +файлы по конвенциям библиотеки Jellyfin. + +Полный замысел и причины — в [BRIEF.md](BRIEF.md). + +## Зачем + +Arr-стек (prowlarr/radarr/sonarr) плохо ложится на русские трекеры, +аниме и ручные раздачи. Jellybit намеренно сокращает путь: одна точка +входа → готовая раскладка для Jellyfin, без каталога индексаторов и +сложных правил качества. Распознавание делает LLM, которому помогает +переданный человеком контекст и (опционально) внешние базы метаданных. + +## Как работает + +1. Точка входа принимает torrent/magnet + контекст (HTTP API, веб-UI + или Telegram-бот). +2. Загрузка ставится в qBittorrent в выделенную категорию. +3. Сервис отслеживает завершение загрузки. +4. По именам файлов, контексту и (опц.) базам метаданных определяется + фильм/сериал и нужная раскладка. +5. Файлы **хардлинкаются** в библиотеку Jellyfin — источник остаётся в + раздаче, место на диске не дублируется. + +При высокой уверенности раскладка выполняется автоматически, иначе — +уходит на подтверждение человеку. + +## Статус + +Ранняя разработка. Сейчас зафиксированы архитектура и решения, кода ещё +нет. См. [дорожную карту](docs/drafts/roadmap.md). + +## Документация + +- [docs/specs/](docs/specs/) — спецификации: целевое устройство системы. + Начать с [architecture.md](docs/specs/architecture.md). +- [docs/adr/](docs/adr/) — журнал архитектурных решений (почему так). +- [docs/drafts/](docs/drafts/) — черновики: планы, идеи, нерешённое. + +## Стек + +Go (один статический бинарь), SQLite, конфигурация — TOML, логи — +структурированный JSON. Подробнее — в +[architecture.md](docs/specs/architecture.md). + +## Доставка + +Сборка здесь → готовый бинарь копируется на медиа-сервер umbar +(`/home/av/projects/private/umbar`). Деплой-обвязка живёт в umbar. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..8793aa0 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,17 @@ +# Документация jellybit + +Три раздела с разной ролью — не путать: + +- **[specs/](specs/)** — спецификации. Описывают **целевое и текущее** + устройство системы. Живые и изменяемые: правим по мере развития, + держим в соответствии с кодом. Отвечают на вопрос «как устроено». + +- **[adr/](adr/)** — Architecture Decision Records. **Неизменяемый** + журнал значимых решений, пишется **постфактум**. Хранит главное — + *почему* так сделано. Передумали → не правим старую запись, заводим + новую. Процесс — в [adr/README.md](adr/README.md). + +- **[drafts/](drafts/)** — черновики: заметки, мысли, планы на будущее, + ещё не принятые решения. Не источник истины и ни к чему не обязывают. + Когда черновик становится реальностью — его место в specs (как + устроено) и/или adr (почему решили). diff --git a/docs/adr/ADR-2026-06-13-go-single-binary.md b/docs/adr/ADR-2026-06-13-go-single-binary.md new file mode 100644 index 0000000..6284e7e --- /dev/null +++ b/docs/adr/ADR-2026-06-13-go-single-binary.md @@ -0,0 +1,53 @@ +# Go и доставка одним бинарём + +- Дата: 2026-06-13 + +## Контекст + +Jellybit — новый сервис, который будет жить на домашнем медиа-сервере +umbar. Нужно выбрать язык и способ доставки. Силы и ограничения: + +- Это домашняя лаборатория, хочется максимально простой доставки: собрал + здесь — скопировал готовый артефакт на сервер, без рантайма и лишних + зависимостей на самой машине. +- В umbar уже устоялось разделение: Python+uv используется вместе с + Ansible (инфраструктура), а не для прикладных сервисов. +- У автора уже есть несколько сервисов на Go — это знакомый и привычный + стек именно под сервисы. +- Сервису нужны: клиент qBittorrent, обращения к LLM, опц. клиенты + TMDB/TVDB, разбор имён релизов. + +## Рассмотренные варианты + +- **Go** — один статический бинарь (`CGO_ENABLED=0`), копируется на + сервер; минимальный docker-образ. Знакомый стек. Минус: нет хорошего + аналога питоновского `guessit` для разбора имён релизов. +- **Python + uv** — богатая экосистема распознавания (`guessit`), но + тянет рантайм и зависимости на сервер; в проекте уже занят инфра-ролью + при Ansible. Смешивать прикладной сервис с инфра-тулингом не хочется. +- **TypeScript / Node** — экосистема есть, но рантайм на сервере и не + основной стек автора для сервисов. + +## Решение + +Пишем jellybit на **Go**, доставляем одним статическим бинарём: сборка в +этом репозитории → готовый артефакт с нужной обвязкой копируется на +umbar. На сервере не нужны ни рантайм, ни менеджер пакетов. + +Причина: при домашней лаборатории решающее — простота доставки и +знакомство со стеком, а не богатство библиотек распознавания. Слабость Go +в разборе имён релизов закрываем дешёвым `go-ptn` плюс основной разбор всё +равно делает LLM; при нехватке точности `guessit` можно завернуть лёгким +сервисом-спутником рядом с бинарём (тоже один файл). Python остаётся за +инфраструктурой (umbar, Ansible). + +## Последствия + +- `+` Доставка тривиальна: один файл, без рантайма и зависимостей на + сервере; минимальный docker-образ. +- `+` Стек знаком автору, переиспользуется опыт других Go-сервисов. +- `+` Чёткая граница: Go — прикладные сервисы, Python+uv — инфра. +- `-` Нет первоклассного `guessit`; точность пред-парса ниже. Митигация: + `go-ptn` + LLM, при необходимости — guessit-спутник. +- `-` Часть клиентов (например, TVDB v4) придётся писать руками — зрелых + готовых библиотек меньше, чем в Python. diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 0000000..624b06e --- /dev/null +++ b/docs/adr/README.md @@ -0,0 +1,59 @@ +# Architecture Decision Records (ADR) + +Журнал значимых архитектурных решений по jellybit. Одна запись — одно +решение. ADR пишем **постфактум**, когда решение принято и зафиксировано +в коде/проекте: идеи и неподтверждённые планы живут в `docs/drafts`, а не +в ADR. Записи **неизменяемы**: передумали → не правим старую, заводим +новую и помечаем старую. + +Главная ценность записи — сохранить **почему**: намерение и причинность. +Это важнее аккуратности оформления и полноты остальных секций. + +Формат и процесс унаследованы от соседнего проекта umbar. + +## Когда заводить ADR + +- Выбор технологии или инструмента. +- Структурные решения (хранилище, организация компонентов, протоколы). +- Решения с долгосрочными последствиями или дорогим откатом. +- **Намеренный отказ** от очевидного подхода — чтобы потом не + переоткрывать «а почему мы не сделали X». + +Не заводить для рутины (бамп версии зависимости, добавление эндпоинта по +накатанной схеме) и того, что и так видно из кода и git. + +## Соглашения + +- **Имя файла = идентификатор:** `ADR-ГГГГ-ММ-ДД-kebab-slug.md`. + Идентификатор — имя без `.md`. Slug — латиницей. +- **Дата** — когда решение реально принято. +- Несколько ADR за один день различаются по slug. +- **Заголовок в файле:** `# Человеческий заголовок` (без даты и ID — они + в имени файла и в строке «Дата»). +- Секция **«Рассмотренные варианты» — опциональна**: оставляй её, только + если альтернативы реально рассматривались. +- Шаблон новой записи — [`template.md`](template.md). + +## Статусы + +Активная запись статуса **не имеет**. Статус появляется, только когда +запись теряет силу, и значений всего два: + +- `заменено на ADR-ГГГГ-ММ-ДД-slug` — решение пересмотрено новой ADR. +- `устарело` — решение потеряло смысл и замены нет. + +## Замена и устаревание + +1. Заводим новую ADR; в её «Контексте» — строка + «Заменяет ADR-ГГГГ-ММ-ДД-slug». +2. В старой ADR добавляем строку `- Статус: заменено на ADR-…` сразу под + датой. Тело не трогаем — это часть истории. +3. Обновляем статус старой записи в индексе ниже. + +## Список записей + +Новые сверху. + +| Дата | Запись | Статус | +| ---------- | ---------------------------------------------------------------- | ------ | +| 2026-06-13 | [Go и доставка одним бинарём](ADR-2026-06-13-go-single-binary.md) | — | diff --git a/docs/adr/template.md b/docs/adr/template.md new file mode 100644 index 0000000..98a7210 --- /dev/null +++ b/docs/adr/template.md @@ -0,0 +1,36 @@ +# Краткий заголовок решения + +- Дата: ГГГГ-ММ-ДД + + +## Контекст + +Что вынудило принять решение: проблема, силы и ограничения (ресурсы, +стоимость, время на поддержку, существующая архитектура). Пиши так, чтобы +через год было понятно «почему это вообще делалось» без чтения переписки. + +## Рассмотренные варианты + + + +- **Вариант A** — суть, плюсы и минусы. +- **Вариант B** — суть, плюсы и минусы. +- **Вариант C** — если отвергнут сразу, коротко почему. + +## Решение + +Что именно сделано и — главное — **почему**: какое намерение и какая +причина за этим стоят. Если варианты рассматривались — почему выбран +этот, а не остальные. + +## Последствия + +- `+` что стало лучше, какие возможности открылись. +- `-` чем платим: новые ограничения, риски, регулярная нагрузка на + поддержку. +- Что нужно сделать как следствие (если есть). diff --git a/docs/drafts/ideas.md b/docs/drafts/ideas.md new file mode 100644 index 0000000..8d894de --- /dev/null +++ b/docs/drafts/ideas.md @@ -0,0 +1,40 @@ +# Идеи и нерешённое + +Свалка мыслей на будущее. Ни к чему не обязывает; принятое переезжает в +specs/adr. + +## guessit как сервис-спутник + +`go-ptn` слабее питоновского `guessit`. Если точности пред-парса не +хватит — завернуть `guessit` в крошечный HTTP-сервис (один файл, +поставляется рядом с бинарём jellybit) и спрашивать его на шаге +пред-парса. Сохраняет «доставку копированием»: два файла вместо одного. + +## Аниме с абсолютной нумерацией + +Релизы аниме часто нумеруют серии сквозным числом (`#137`) без сезонов, а +Jellyfin ждёт `SxxEyy`. Нужен пересчёт абсолютной нумерации в +сезон/серию — надёжнее всего через TVDB (там есть absolute order). +Отдельный крайний случай распознавания. + +## Завершение загрузки через webhook + +Сейчас план — поллинг qBittorrent. Альтернатива: «Run external program on +torrent completion» в qBittorrent дёргает эндпоинт jellybit. Реагирует +быстрее, но связывает нас с конфигом qBittorrent. Решим по опыту +эксплуатации. + +## Нотификации о готовности + +Когда раскладка завершена (или нужен review) — уведомить: Telegram, +возможно ntfy/Apprise. Естественно ложится на Telegram-транспорт. + +## Доступ к веб-UI + +Сейчас предполагается доверенная локальная сеть. Если понадобится — +простая авторизация или вынос за reverse-proxy с аутентификацией. + +## Повторный прогон распознавания + +Возможность переоткрыть загрузку, поправить контекст и перераспознать без +перекачивания — полезно, когда LLM ошибся, а файлы уже скачаны. diff --git a/docs/drafts/roadmap.md b/docs/drafts/roadmap.md new file mode 100644 index 0000000..6855b5e --- /dev/null +++ b/docs/drafts/roadmap.md @@ -0,0 +1,31 @@ +# Дорожная карта + +Черновик плана реализации. Ориентир, не обязательство; по ходу +уточняется. Что реализовано и как устроено — в `docs/specs`. + +## Фазы + +- **Ф0 — каркас.** go.mod, раскладка пакетов, загрузка TOML-конфига, + SQLite + миграции, slog-логи, Dockerfile (static → distroless), + golangci-lint, lefthook. Документация (этот этап — частично готов). +- **Ф1 — ingest + tracking (без LLM).** `Ingest()` + добавление в + qBittorrent (категория `jellybit`) + `worker`-поллинг завершения + + машина состояний. Наружу: HTTP API, список в веб-UI, `jellybit add`. +- **Ф2 — распознавание.** `go-ptn` + LLM (structured output) → план + + оценка уверенности. Без записи на диск. +- **Ф3 — раскладка + минимальный review.** Хардлинки по конвенциям + Jellyfin, субтитры, идемпотентность. Авто при высокой уверенности; + низкая → экран подтверждения (htmx). +- **Ф4 — метаданные.** TMDB/TVDB опционально, provider-id в именах, + валидация распознавания против числа серий. +- **Ф5 — Telegram + UX.** Бот-адаптер + парсер сообщений торрент-бота, + подтверждение в боте, триггер скана Jellyfin, нотификации. +- **Ф6 — деплой.** Static-образ/бинарь + обвязка в umbar + (`playbook-jellybit.yml`). + +## Заметки по порядку + +- Минимальный review-экран нужен уже в Ф3 (как только появляется режим + «спросить при сомнении»), полноценный UX — в Ф5. +- Jellyfin в umbar ещё не развёрнут — раскладку файлов это не блокирует, + тестируется без него; триггер скана подключаем, когда Jellyfin поднят. diff --git a/docs/specs/README.md b/docs/specs/README.md new file mode 100644 index 0000000..64b85c5 --- /dev/null +++ b/docs/specs/README.md @@ -0,0 +1,22 @@ +# Спецификации + +Живые документы о том, как устроена система — целевое и актуальное +состояние. В отличие от ADR, спецификации **изменяемы**: их правят по +мере развития проекта и держат в соответствии с кодом. В отличие от +черновиков, описывают принятое и реализуемое, а не идеи. + +## Соглашения + +- Имя файла — `kebab-topic.md`, без дат (дата живёт в git-истории). +- Одна спецификация — одна тема. +- Если решение требует объяснения «почему именно так» с долгим следом — + заведи ADR и сошлись на него из спецификации. + +## Записи + +- [architecture.md](architecture.md) — общее устройство: компоненты, + поток, машина состояний, хранилище, конфигурация. +- [recognition.md](recognition.md) — распознавание контента и модель + уверенности. +- [jellyfin-layout.md](jellyfin-layout.md) — конвенции именования файлов + Jellyfin, в которые раскладываем. diff --git a/docs/specs/architecture.md b/docs/specs/architecture.md new file mode 100644 index 0000000..4d1bd71 --- /dev/null +++ b/docs/specs/architecture.md @@ -0,0 +1,155 @@ +# Архитектура + +## Назначение + +Jellybit принимает торрент с текстовым контекстом, скачивает его через +qBittorrent, определяет содержимое (фильм или сериал с сезонами и +сериями) и раскладывает файлы по конвенциям Jellyfin — хардлинками, не +трогая исходную раздачу. + +## Принципы + +- **Один статический бинарь.** Доставка — копированием на сервер. См. + [ADR-2026-06-13-go-single-binary](../adr/ADR-2026-06-13-go-single-binary.md). +- **Источник не трогаем.** В библиотеку кладём хардлинки; qBittorrent + продолжает раздачу, место на диске не дублируется. +- **Единое ядро, тонкие транспорты.** Вся логика приёма загрузки — в + use-case `Ingest`. HTTP API, веб-UI и Telegram — обёртки над ним. +- **Опциональные внешние зависимости.** Базы метаданных (TMDB/TVDB) + включаются конфигом; без них сервис работает на одном LLM. +- **Минимум компонентов.** В духе umbar — без лишних сервисов. + +## Компоненты + +| Пакет | Ответственность | +| ----------- | ----------------------------------------------------- | +| `ingest` | use-case приёма загрузки, общий для всех транспортов | +| `qbt` | клиент qBittorrent WebUI API | +| `worker` | фоновый цикл: машина состояний, поллинг завершения | +| `recognize` | пред-парс имени + LLM + модель уверенности | +| `metadata` | интерфейс баз метаданных + TMDB/TVDB (опц.) | +| `layout` | конвенции Jellyfin + хардлинкер | +| `store` | SQLite: загрузки, распознавание, ссылки | +| `httpapi` | REST + веб-UI (server-rendered, htmx) | +| `tgbot` | Telegram-адаптер + парсер сообщений торрент-бота | +| `config` | загрузка TOML-конфига | + +## Поток и машина состояний + +``` +ingest → downloading → completed → recognizing ─┬─ уверенно ───────→ linking → done + └─ сомнительно → review → linking → done +любой шаг при ошибке → failed +``` + +- **ingest** — приняли источник + контекст, поставили в qBittorrent + (категория `jellybit`), записали в БД. +- **downloading / completed** — `worker` поллит qBittorrent по категории. +- **recognizing** — `recognize` строит план раскладки и оценку + уверенности (см. [recognition.md](recognition.md)). +- **review** — план уходит человеку (веб-UI / Telegram), ждём решения. +- **linking** — `layout` создаёт хардлинки в библиотеке. +- **done** — опционально дёргаем скан библиотеки Jellyfin. + +Состояние персистентно в SQLite — перезапуск сервиса безопасен, `worker` +продолжает с того же места. + +## Транспорты + +Все три ведут в один `Ingest(req)`: + +- **HTTP API + веб-UI** — форма «добавить», список загрузок, экран + подтверждения раскладки (server-rendered + htmx, без JS-сборки). +- **Telegram-бот** — переслать magnet или сообщение торрент-бота прямо в + jellybit; текст становится контекстом распознавания. +- **CLI** — `jellybit add --context "..."` для отладки. + +## Хранилище + +SQLite, минимум таблиц: + +- `download` — источник, контекст, hash торрента, категория, состояние, + тайминги. +- `recognition` — тип, название, год, сезон, provider-id, оценка + уверенности, сырой ответ LLM. +- `file_link` — соответствие исходный файл → целевой путь, вид + (видео/субтитры), статус. + +## Конфигурация + +TOML, секреты — placeholder'ы; реальный конфиг не коммитим. Пример: + +```toml +[qbittorrent] +url = "http://127.0.0.1:8989" +username = "admin" +password = "" +category = "jellybit" + +[paths] +downloads = "/srv/downloads" +movies = "/srv/media/movies" +series = "/srv/media/series" + +[llm] +provider = "anthropic" +model = "claude-sonnet-4-6" # сложные случаи — claude-opus-4-8 +api_key = "" + +[metadata.tmdb] +enabled = true +api_key = "" + +[metadata.tvdb] +enabled = false +api_key = "" + +[recognition] +auto_confidence_threshold = 0.85 + +[telegram] +enabled = false +token = "" +allowed_user_ids = [] + +[http] +listen = ":8080" + +[log] +level = "info" +format = "json" +``` + +## Логирование + +Структурированный JSON через `log/slog`. Каждая загрузка проходит со +сквозным идентификатором; решения распознавания (почему авто/ревью) +логируются явно. + +## Раскладка файлов + +Хардлинки в `paths.movies` / `paths.series` по конвенциям Jellyfin. +Детали и крайние случаи — в [jellyfin-layout.md](jellyfin-layout.md). +Требование: `downloads` и `media` на одной ФС (иначе хардлинк +невозможен). Если jellybit в docker — смонтировать общего родителя, +чтобы хардлинк работал внутри контейнера. + +## Предполагаемая структура репозитория + +``` +cmd/jellybit/ точка входа, сборка зависимостей +internal/ + ingest/ qbt/ worker/ recognize/ metadata/ + layout/ store/ httpapi/ tgbot/ config/ +migrations/ миграции SQLite +web/templates/ шаблоны веб-UI +docs/ specs / adr / drafts +config.example.toml +``` + +## Открытые вопросы + +- Подтвердить, что `/srv/downloads` и `/srv/media` — одна ФС. +- Способ детекта завершения: поллинг (старт) или webhook qBittorrent + «run on completion» (позже). +- Где хранить секреты при деплое (шаблонизация из umbar). diff --git a/docs/specs/jellyfin-layout.md b/docs/specs/jellyfin-layout.md new file mode 100644 index 0000000..9a92c88 --- /dev/null +++ b/docs/specs/jellyfin-layout.md @@ -0,0 +1,52 @@ +# Конвенции раскладки Jellyfin + +Целевые имена и структура, в которые jellybit раскладывает файлы +хардлинками. Источники: +[Movies](https://jellyfin.org/docs/general/server/media/movies), +[Shows](https://jellyfin.org/docs/general/server/media/shows). + +## Фильмы + +``` +movies/ + Дюна Часть вторая (2024) [tmdbid-693134]/ + Дюна Часть вторая (2024).mkv + Дюна Часть вторая (2024).ru.srt +``` + +- Папка и файл — `Название (Год)`. +- provider-id в имени папки (`[tmdbid-...]`) добавляется при работе с + базой — снимает неоднозначность для русских названий, которые Jellyfin + иначе может опознать неверно. +- Внешние субтитры — `Имя..srt`, при необходимости `.forced`. + +## Сериалы + +``` +series/ + Название (2024) [tvdbid-123456]/ + Season 01/ + Название (2024) S01E01.mkv + Название (2024) S01E02.mkv +``` + +- provider-id — на папке сериала. +- Сезоны — `Season 01`, файлы — `... SxxEyy`. + +## Сопоставление источник → цель + +qBittorrent держит файлы в `paths.downloads`. Для каждого распознанного +файла создаётся **хардлинк** в `paths.movies` / `paths.series` с целевым +именем. Исходный файл остаётся на месте (раздача продолжается), inode +общий — диск не дублируется. + +Требование: целевой и исходный каталоги — на одной ФС. + +## Крайние случаи + +- **Многофайловый фильм** (части) — `... part1`, `... part2` в одной + папке фильма. +- **Сезон-пак** — все серии в один `Season xx`. +- **Несколько аудиодорожек** — обычно внутри mkv, не наша забота. +- **Аниме с абсолютной нумерацией** — требует пересчёта в S·E, отдельная + проработка ([drafts/ideas.md](../drafts/ideas.md)). diff --git a/docs/specs/recognition.md b/docs/specs/recognition.md new file mode 100644 index 0000000..e086cde --- /dev/null +++ b/docs/specs/recognition.md @@ -0,0 +1,72 @@ +# Распознавание контента + +## Задача + +По доступным сигналам определить: это фильм или сериал; каноническое +название и год; для сериала — сезон и соответствие файлов сериям; при +включённых базах — provider-id. На выходе — план раскладки и оценка +уверенности. + +## Сигналы + +- Имя торрента и структура каталогов. +- Список файлов с размерами и расширениями. +- Текстовый контекст от человека. +- Распарсенное сообщение торрент-бота (если пришло через Telegram): + название с годом, качество, переводы, magnet — см. пример в + [BRIEF.md](../../BRIEF.md). + +## Конвейер + +1. **Пред-парс** имени релиза дешёвым парсером (`go-ptn`): черновые + название/год/сезон/серия и качество. Грубо, но бесплатно. +2. **LLM** (Anthropic, structured output): получает все сигналы и + пред-парс, возвращает структурированный план. Хорошо справляется с + русскими релиз-именами, чего не умеет парсер. +3. **Сверка с базой** (опц., если включена TMDB/TVDB): подтверждаем + название+год, берём официальный id и каноническое имя. +4. **Оценка уверенности** и решение: авто-раскладка или ревью. + +## Структура ответа LLM (черновик) + +``` +type movie | series +title каноническое название +original_title оригинальное название (если есть) +year год +season номер сезона (для сериала) +provider_hint подсказка для поиска в базе +files[] { src, role: main|episode|subtitle|extra|sample, + season?, episode? } +confidence 0..1 — самооценка модели по полям +notes пояснения, неоднозначности +``` + +## Модель уверенности + +Авто-раскладка только если выполнено всё: + +1. **Самооценка LLM** ≥ порога (`recognition.auto_confidence_threshold`). +2. **Совпадение с базой** (если включена) — единственный сильный матч по + названию+году. +3. **Структурная валидация** проходит без предупреждений: + - фильм: ровно один основной видеофайл (семплы/экстра отброшены); + - сериал: число серий бьётся с базой (если есть), нумерация S·E + консистентна, без пропусков и дублей. + +Иначе план уходит в **review**. На экране подтверждения всегда видно, +*почему* не авто — это страховка на дорогих файлах. + +## Что делаем с краёв + +- Семплы и «экстра» отбрасываем (эвристики по размеру/имени + LLM). +- Внешние субтитры (`.srt`, `.ass`) привязываем к видео и именуем по + Jellyfin (`*.ru.srt`). +- Сезон-паки разбираем по сериям; аниме с абсолютной нумерацией — + отдельный крайний случай, см. [drafts/ideas.md](../drafts/ideas.md). + +## На будущее + +`go-ptn` слабее питоновского `guessit`. Если точности пред-парса не +хватит — завернуть `guessit` лёгким сервисом-спутником (один файл рядом +с бинарём). См. [drafts/ideas.md](../drafts/ideas.md).