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

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
+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).