Добавил документацию и описание репозитория

This commit is contained in:
2026-06-13 17:10:53 +03:00
parent 82a81c16af
commit 5b53f4e8e8
12 changed files with 647 additions and 0 deletions
+56
View File
@@ -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`).
+54
View File
@@ -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.
+17
View File
@@ -0,0 +1,17 @@
# Документация jellybit
Три раздела с разной ролью — не путать:
- **[specs/](specs/)** — спецификации. Описывают **целевое и текущее**
устройство системы. Живые и изменяемые: правим по мере развития,
держим в соответствии с кодом. Отвечают на вопрос «как устроено».
- **[adr/](adr/)** — Architecture Decision Records. **Неизменяемый**
журнал значимых решений, пишется **постфактум**. Хранит главное —
*почему* так сделано. Передумали → не правим старую запись, заводим
новую. Процесс — в [adr/README.md](adr/README.md).
- **[drafts/](drafts/)** — черновики: заметки, мысли, планы на будущее,
ещё не принятые решения. Не источник истины и ни к чему не обязывают.
Когда черновик становится реальностью — его место в specs (как
устроено) и/или adr (почему решили).
@@ -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.
+59
View File
@@ -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) | — |
+36
View File
@@ -0,0 +1,36 @@
# Краткий заголовок решения
- Дата: ГГГГ-ММ-ДД
<!-- Строку статуса добавляют позже, только если запись потеряла силу:
- Статус: заменено на ADR-ГГГГ-ММ-ДД-slug
- Статус: устарело
У активной записи строки статуса нет. -->
## Контекст
Что вынудило принять решение: проблема, силы и ограничения (ресурсы,
стоимость, время на поддержку, существующая архитектура). Пиши так, чтобы
через год было понятно «почему это вообще делалось» без чтения переписки.
## Рассмотренные варианты
<!-- Опциональная секция. Оставь, только если варианты реально
рассматривались. Если решение было единственным очевидным — удали
её, а причину объясни в «Решении». -->
- **Вариант A** — суть, плюсы и минусы.
- **Вариант B** — суть, плюсы и минусы.
- **Вариант C** — если отвергнут сразу, коротко почему.
## Решение
Что именно сделано и — главное — **почему**: какое намерение и какая
причина за этим стоят. Если варианты рассматривались — почему выбран
этот, а не остальные.
## Последствия
- `+` что стало лучше, какие возможности открылись.
- `-` чем платим: новые ограничения, риски, регулярная нагрузка на
поддержку.
- Что нужно сделать как следствие (если есть).
+40
View File
@@ -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 ошибся, а файлы уже скачаны.
+31
View File
@@ -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 поднят.
+22
View File
@@ -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, в которые раскладываем.
+155
View File
@@ -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 <magnet> --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).
+52
View File
@@ -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
иначе может опознать неверно.
- Внешние субтитры — `Имя.<lang>.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)).
+72
View File
@@ -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).