Добавил документацию и описание репозитория
This commit is contained in:
@@ -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, в которые раскладываем.
|
||||
@@ -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).
|
||||
@@ -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)).
|
||||
@@ -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).
|
||||
Reference in New Issue
Block a user