Уточнение деталей архитектуры
This commit is contained in:
@@ -19,10 +19,10 @@ Jellyfin ждёт `SxxEyy`. Нужен пересчёт абсолютной н
|
||||
|
||||
## Завершение загрузки через webhook
|
||||
|
||||
Сейчас план — поллинг qBittorrent. Альтернатива: «Run external program on
|
||||
torrent completion» в qBittorrent дёргает эндпоинт jellybit. Реагирует
|
||||
быстрее, но связывает нас с конфигом qBittorrent. Решим по опыту
|
||||
эксплуатации.
|
||||
Сейчас принято — поллинг qBittorrent раз в несколько секунд.
|
||||
Альтернатива: «Run external program on torrent completion» в qBittorrent
|
||||
дёргает эндпоинт jellybit. Реагирует быстрее, но связывает нас с конфигом
|
||||
qBittorrent. Решим по опыту эксплуатации.
|
||||
|
||||
## Нотификации о готовности
|
||||
|
||||
|
||||
+13
-7
@@ -6,22 +6,28 @@
|
||||
## Фазы
|
||||
|
||||
- **Ф0 — каркас.** go.mod, раскладка пакетов, загрузка TOML-конфига,
|
||||
SQLite + миграции, slog-логи, Dockerfile (static → distroless),
|
||||
golangci-lint, lefthook. Документация (этот этап — частично готов).
|
||||
SQLite + миграции, slog-логи, `Dockerfile` (минимальный рантайм-образ,
|
||||
копирует готовый бинарь), 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).
|
||||
Jellyfin, субтитры, идемпотентность, **undo**. Авто при высокой
|
||||
уверенности; низкая → review (htmx): подсказка + перераспознавание, из
|
||||
ручного — тип, выбор кандидата базы, пометка «игнор». Полный редактор
|
||||
маппинга — Ф5. См. [review-ux.md](../specs/review-ux.md).
|
||||
- **Ф4 — метаданные.** TMDB/TVDB опционально, provider-id в именах,
|
||||
валидация распознавания против числа серий.
|
||||
- **Ф5 — Telegram + UX.** Бот-адаптер + парсер сообщений торрент-бота,
|
||||
подтверждение в боте, триггер скана Jellyfin, нотификации.
|
||||
- **Ф6 — деплой.** Static-образ/бинарь + обвязка в umbar
|
||||
(`playbook-jellybit.yml`).
|
||||
подтверждение в боте (карточка + кнопки + reply-подсказка, эскалация в
|
||||
веб), полный редактор маппинга «файл → серия», триггер скана Jellyfin,
|
||||
нотификации.
|
||||
- **Ф6 — деплой.** Сборка статического бинаря здесь; доставка бинаря +
|
||||
`Dockerfile` на сервер, `docker build` и запуск на месте; оркестрация —
|
||||
`playbook-jellybit.yml` в umbar.
|
||||
|
||||
## Заметки по порядку
|
||||
|
||||
|
||||
@@ -18,5 +18,7 @@
|
||||
поток, машина состояний, хранилище, конфигурация.
|
||||
- [recognition.md](recognition.md) — распознавание контента и модель
|
||||
уверенности.
|
||||
- [review-ux.md](review-ux.md) — ревью раскладки человеком: UI/UX-сценарии
|
||||
на случай, когда система не уверена.
|
||||
- [jellyfin-layout.md](jellyfin-layout.md) — конвенции именования файлов
|
||||
Jellyfin, в которые раскладываем.
|
||||
|
||||
+53
-14
@@ -26,7 +26,8 @@ qBittorrent, определяет содержимое (фильм или сер
|
||||
| `ingest` | use-case приёма загрузки, общий для всех транспортов |
|
||||
| `qbt` | клиент qBittorrent WebUI API |
|
||||
| `worker` | фоновый цикл: машина состояний, поллинг завершения |
|
||||
| `recognize` | пред-парс имени + LLM + модель уверенности |
|
||||
| `recognize` | пред-парс имени + вызов LLM + модель уверенности |
|
||||
| `llm` | провайдер LLM за интерфейсом (дискриминатор `type`) |
|
||||
| `metadata` | интерфейс баз метаданных + TMDB/TVDB (опц.) |
|
||||
| `layout` | конвенции Jellyfin + хардлинкер |
|
||||
| `store` | SQLite: загрузки, распознавание, ссылки |
|
||||
@@ -44,10 +45,12 @@ ingest → downloading → completed → recognizing ─┬─ уверенно
|
||||
|
||||
- **ingest** — приняли источник + контекст, поставили в qBittorrent
|
||||
(категория `jellybit`), записали в БД.
|
||||
- **downloading / completed** — `worker` поллит qBittorrent по категории.
|
||||
- **downloading / completed** — `worker` поллит qBittorrent по категории
|
||||
(интервал `worker.poll_interval`, по умолчанию 5 с).
|
||||
- **recognizing** — `recognize` строит план раскладки и оценку
|
||||
уверенности (см. [recognition.md](recognition.md)).
|
||||
- **review** — план уходит человеку (веб-UI / Telegram), ждём решения.
|
||||
- **review** — план уходит человеку (веб-UI / Telegram), ждём решения;
|
||||
сценарии — в [review-ux.md](review-ux.md).
|
||||
- **linking** — `layout` создаёт хардлинки в библиотеке.
|
||||
- **done** — опционально дёргаем скан библиотеки Jellyfin.
|
||||
|
||||
@@ -77,7 +80,10 @@ SQLite, минимум таблиц:
|
||||
|
||||
## Конфигурация
|
||||
|
||||
TOML, секреты — placeholder'ы; реальный конфиг не коммитим. Пример:
|
||||
TOML. В репозитории — `config.example.toml` с placeholder'ами; реальный
|
||||
`config.toml` рендерится при деплое Ansible-шаблоном из переменных umbar
|
||||
(секреты — в `vars/secrets.yml` под ansible-vault) и не коммитится.
|
||||
Пример:
|
||||
|
||||
```toml
|
||||
[qbittorrent]
|
||||
@@ -92,9 +98,11 @@ movies = "/srv/media/movies"
|
||||
series = "/srv/media/series"
|
||||
|
||||
[llm]
|
||||
provider = "anthropic"
|
||||
model = "claude-sonnet-4-6" # сложные случаи — claude-opus-4-8
|
||||
# type — дискриминатор реализации; пока поддерживается "openai-compat"
|
||||
type = "openai-compat"
|
||||
base_url = "http://127.0.0.1:1234/v1"
|
||||
api_key = ""
|
||||
model = "qwen2.5-32b-instruct"
|
||||
|
||||
[metadata.tmdb]
|
||||
enabled = true
|
||||
@@ -104,6 +112,9 @@ api_key = ""
|
||||
enabled = false
|
||||
api_key = ""
|
||||
|
||||
[worker]
|
||||
poll_interval = "5s" # как часто опрашивать qBittorrent
|
||||
|
||||
[recognition]
|
||||
auto_confidence_threshold = 0.85
|
||||
|
||||
@@ -126,20 +137,39 @@ format = "json"
|
||||
сквозным идентификатором; решения распознавания (почему авто/ревью)
|
||||
логируются явно.
|
||||
|
||||
## Деплой
|
||||
|
||||
Jellybit работает в **docker** — в одной среде с qBittorrent и Jellyfin
|
||||
(они тоже в контейнерах на umbar). Единая среда запуска перевешивает
|
||||
простоту нативного systemd.
|
||||
|
||||
Сборка — дёшево и сердито: статический бинарь собирается здесь; на сервер
|
||||
во временную build-папку кладутся бинарь + `Dockerfile` (он просто
|
||||
копирует бинарь в минимальный образ), образ собирается прямо на сервере и
|
||||
запускается. Go-тулчейн на сервере не нужен — только docker.
|
||||
|
||||
Разделение ответственности:
|
||||
|
||||
- **jellybit** (этот репозиторий) — производит статический бинарь и
|
||||
`Dockerfile`.
|
||||
- **umbar** — оркестрация деплоя: доставка артефактов, `docker build` и
|
||||
запуск через docker compose (`playbook-jellybit.yml`).
|
||||
|
||||
## Раскладка файлов
|
||||
|
||||
Хардлинки в `paths.movies` / `paths.series` по конвенциям Jellyfin.
|
||||
Детали и крайние случаи — в [jellyfin-layout.md](jellyfin-layout.md).
|
||||
Требование: `downloads` и `media` на одной ФС (иначе хардлинк
|
||||
невозможен). Если jellybit в docker — смонтировать общего родителя,
|
||||
чтобы хардлинк работал внутри контейнера.
|
||||
`/srv/downloads` и `/srv/media` — одна ФС (подтверждено), поэтому
|
||||
хардлинки применимы. Так как jellybit в docker (см. «Деплой»), контейнеру
|
||||
монтируем общего родителя `/srv` — чтобы внутри оба каталога остались на
|
||||
одной ФС и хардлинк проходил.
|
||||
|
||||
## Предполагаемая структура репозитория
|
||||
|
||||
```
|
||||
cmd/jellybit/ точка входа, сборка зависимостей
|
||||
internal/
|
||||
ingest/ qbt/ worker/ recognize/ metadata/
|
||||
ingest/ qbt/ worker/ recognize/ llm/ metadata/
|
||||
layout/ store/ httpapi/ tgbot/ config/
|
||||
migrations/ миграции SQLite
|
||||
web/templates/ шаблоны веб-UI
|
||||
@@ -147,9 +177,18 @@ docs/ specs / adr / drafts
|
||||
config.example.toml
|
||||
```
|
||||
|
||||
## Решённые вопросы
|
||||
|
||||
- `/srv/downloads` и `/srv/media` — одна ФС (подтверждено); хардлинки
|
||||
применимы.
|
||||
- Детект завершения — поллинг qBittorrent раз в несколько секунд
|
||||
(`worker.poll_interval`). Webhook — возможная оптимизация на будущее
|
||||
([drafts/ideas.md](../drafts/ideas.md)).
|
||||
- Секреты — в переменных umbar; `config.toml` рендерится Ansible-шаблоном
|
||||
при деплое.
|
||||
- Форма запуска — **docker**, образ собирается на сервере из готового
|
||||
бинаря (см. «Деплой»).
|
||||
|
||||
## Открытые вопросы
|
||||
|
||||
- Подтвердить, что `/srv/downloads` и `/srv/media` — одна ФС.
|
||||
- Способ детекта завершения: поллинг (старт) или webhook qBittorrent
|
||||
«run on completion» (позже).
|
||||
- Где хранить секреты при деплое (шаблонизация из umbar).
|
||||
Существенных пока нет.
|
||||
|
||||
@@ -20,9 +20,10 @@
|
||||
|
||||
1. **Пред-парс** имени релиза дешёвым парсером (`go-ptn`): черновые
|
||||
название/год/сезон/серия и качество. Грубо, но бесплатно.
|
||||
2. **LLM** (Anthropic, structured output): получает все сигналы и
|
||||
пред-парс, возвращает структурированный план. Хорошо справляется с
|
||||
русскими релиз-именами, чего не умеет парсер.
|
||||
2. **LLM** (через провайдер-абстракцию, см. «Провайдер LLM»): получает
|
||||
все сигналы и пред-парс, возвращает структурированный план в нашей
|
||||
схеме. Хорошо справляется с русскими релиз-именами, чего не умеет
|
||||
парсер.
|
||||
3. **Сверка с базой** (опц., если включена TMDB/TVDB): подтверждаем
|
||||
название+год, берём официальный id и каноническое имя.
|
||||
4. **Оценка уверенности** и решение: авто-раскладка или ревью.
|
||||
@@ -42,6 +43,24 @@ confidence 0..1 — самооценка модели по полям
|
||||
notes пояснения, неоднозначности
|
||||
```
|
||||
|
||||
## Провайдер LLM
|
||||
|
||||
Доступ к LLM — за интерфейсом; конкретная реализация выбирается полем
|
||||
`[llm].type` в конфиге (дискриминатор). Это позволяет подключать
|
||||
локальные модели и сторонние (в т.ч. китайские) эндпоинты — ради экономии
|
||||
и независимости от одного вендора.
|
||||
|
||||
- Первый и пока единственный тип — **`openai-compat`**: OpenAI-совместимый
|
||||
Chat Completions API (`base_url` + `api_key` + `model`). Под него
|
||||
подходят локальные серверы (LM Studio, llama.cpp, Ollama) и облачные
|
||||
совместимые провайдеры (DeepSeek, Qwen и др.).
|
||||
- Структурированный вывод: запрашиваем JSON по нашей схеме
|
||||
(`response_format` со схемой там, где поддерживается; иначе json-режим
|
||||
или tool-call), **валидируем в Go** и ретраим при несоответствии —
|
||||
серверы различаются по поддержке строгих схем.
|
||||
- Новые типы (напр. нативный `anthropic`) добавляются, не трогая
|
||||
`recognize`.
|
||||
|
||||
## Модель уверенности
|
||||
|
||||
Авто-раскладка только если выполнено всё:
|
||||
@@ -54,8 +73,9 @@ notes пояснения, неоднозначности
|
||||
- сериал: число серий бьётся с базой (если есть), нумерация S·E
|
||||
консистентна, без пропусков и дублей.
|
||||
|
||||
Иначе план уходит в **review**. На экране подтверждения всегда видно,
|
||||
*почему* не авто — это страховка на дорогих файлах.
|
||||
Иначе план уходит в **review** (сценарии — [review-ux.md](review-ux.md)).
|
||||
На экране подтверждения всегда видно, *почему* не авто — это страховка на
|
||||
дорогих файлах.
|
||||
|
||||
## Что делаем с краёв
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
# Ревью раскладки человеком
|
||||
|
||||
Что происходит, когда система не уверена в распознавании и не
|
||||
раскладывает файлы автоматически. Когда именно наступает ревью — см.
|
||||
[recognition.md](recognition.md); конвенции целевых имён —
|
||||
[jellyfin-layout.md](jellyfin-layout.md).
|
||||
|
||||
Главный принцип: ревью — это **петля «догадка → подсказка человека →
|
||||
перераспознавание»**, а не статичное «ок/нет». Человек остаётся
|
||||
супервизором, а не оператором ручного ввода.
|
||||
|
||||
## Когда наступает
|
||||
|
||||
Загрузка уходит в `review`, если сработал любой триггер модели
|
||||
уверенности: низкая самооценка LLM; нет матча в базе (или несколько
|
||||
кандидатов); структурная валидация ругается (у фильма >1 основного
|
||||
файла; число серий не бьётся с базой; дыры/дубли в нумерации S·E).
|
||||
В интерфейсе всегда видна **конкретная причина**, а не просто «не уверен».
|
||||
|
||||
## Поверхность решения (едина для всех транспортов)
|
||||
|
||||
1. **Источник:** имя торрента, переданный контекст, дерево файлов с
|
||||
размерами, (если из бота) распарсенное сообщение.
|
||||
2. **Догадка системы:** тип, название, год, сезон, матч базы и
|
||||
**превью целевой раскладки** — буквальные пути, которые создадутся.
|
||||
3. **Причина сомнения.**
|
||||
|
||||
## Действия
|
||||
|
||||
- **Применить** — сделать хардлинки по плану.
|
||||
- **Уточнить и перераспознать** — добавить подсказку текстом → LLM
|
||||
перезапускается с исходными сигналами и накопленными подсказками →
|
||||
новый план. Главный путь, когда «LLM не справился».
|
||||
- **Поправить вручную** — объём зависит от версии (см. ниже).
|
||||
- **Выбрать кандидата базы** / ввести id / «без базы».
|
||||
- **Отклонить** / **Позже**.
|
||||
|
||||
**Подсказка vs override.** Подсказка мягкая — LLM её интерпретирует.
|
||||
Ручная правка поля — жёсткий **override**: система берёт значение как
|
||||
есть и «пиннит» его, перераспознавание не затирает уже поправленное.
|
||||
|
||||
## Веб-UI — точные правки
|
||||
|
||||
```
|
||||
Fargo.S02.2015.WEB-DL.1080p.rus.eng 🟡 review
|
||||
Причины: нет в TMDB · уверенность 0.46
|
||||
|
||||
Контекст: «второй сезон, рус+англ дорожки» [+ добавить → 🔁 перераспознать]
|
||||
|
||||
Тип: ( ) фильм (•) сериал Название: Фарго Год: 2015 Сезон: 02
|
||||
База: [TMDB поиск…] [TVDB поиск…] выбрано: — (без базы) [ввести id]
|
||||
|
||||
Файлы → серии:
|
||||
# | файл | размер | роль | S | E
|
||||
1 | Fargo.S02E01.rus.mkv | 3.1 GB | эпизод | 02 | 01
|
||||
… [нумеровать подряд] [сброс]
|
||||
9 | sample.mkv | 40 MB | игнор | – | –
|
||||
|
||||
Превью:
|
||||
series/Фарго (2015)/Season 02/Фарго (2015) S02E01.mkv ← #1
|
||||
[ Применить ] [ Отклонить ] [ Позже ]
|
||||
```
|
||||
|
||||
Ядро экрана для сериала — таблица «файл → серия» с живой валидацией
|
||||
дыр/дублей и кнопкой «нумеровать подряд» (частый случай: файлы по
|
||||
порядку, но подписаны криво). Для фильма проще: выбрать основной файл,
|
||||
остальное — extra/sample/субтитры/игнор.
|
||||
|
||||
## Telegram — быстро, где пользователь и так есть
|
||||
|
||||
```
|
||||
🟡 Нужно подтверждение
|
||||
Источник: Fargo.S02.2015.WEB-DL.1080p
|
||||
Похоже на: 📺 сериал «Фарго», сезон 2 (2015)
|
||||
База: TMDB не найдено · уверенность низкая
|
||||
План: 10 видео → series/Фарго (2015)/Season 02/…E01–E10
|
||||
|
||||
[✅ Применить] [📺↔🎬 Тип]
|
||||
[🔢 Выбрать в базе] [🔁 Уточнить]
|
||||
[🌐 Открыть в вебе] [❌ Отклонить]
|
||||
```
|
||||
|
||||
- **🔁 Уточнить** → бот просит подсказку ответом → перераспознаёт →
|
||||
редактирует то же сообщение новым планом. Петля коррекции прямо в чате.
|
||||
- **🔢 Выбрать в базе** → кнопки по кандидатам (название · год · id).
|
||||
- Точечное переназначение файлов в чат не помещается → **🌐 Открыть в
|
||||
вебе** (deep-link на ту же страницу).
|
||||
|
||||
## Разделение труда
|
||||
|
||||
Telegram = одобрить / подсказать / выбрать кандидата / эскалировать в
|
||||
веб. Веб = точные правки. Состояние ревью одно (в SQLite) — действовать
|
||||
можно из любого транспорта, последнее слово побеждает.
|
||||
|
||||
## Крайние сценарии
|
||||
|
||||
- **База неоднозначна** → выбор кандидата (часто чинит всё разом: пиннит
|
||||
provider-id и каноническое имя).
|
||||
- **База пустая (рус/аниме)** → «без базы» или ручной id/url. Аниме с
|
||||
абсолютной нумерацией → веб-хелпер «absolute → S·E» (см.
|
||||
[drafts/ideas.md](../drafts/ideas.md)).
|
||||
- **Не тот тип (movie↔series)** → переключатель пересобирает форму плана.
|
||||
- **Мусор (sample/extra/дубли дорожек)** → роль «игнор».
|
||||
- **Полный провал** (LLM ничего не вытащил) → веб-«ручной режим»: выбрать
|
||||
тип, ввести название/год, разложить файлы руками; в Telegram — сразу
|
||||
эскалация в веб.
|
||||
|
||||
## Вход в ревью и откат
|
||||
|
||||
- Переход в `review` **пингует** (сообщение в Telegram / бейдж в вебе) —
|
||||
пользователя зовут, а не он опрашивает. Таймера нет, источник
|
||||
продолжает сидировать.
|
||||
- После «Применить» показываем, что создано. **Undo** — убрать созданные
|
||||
хардлинки одной кнопкой (источник цел); страховка от ошибочного
|
||||
подтверждения.
|
||||
|
||||
## Объём по версиям
|
||||
|
||||
- **Ф3 (первая версия):** подсказка + перераспознавание; из ручного —
|
||||
переключатель типа, выбор кандидата базы, пометка файла «игнор». Undo —
|
||||
есть.
|
||||
- **Ф5:** полный редактор маппинга «файл → серия», ручной режим,
|
||||
подтверждение в Telegram с reply-подсказкой и эскалацией в веб.
|
||||
Reference in New Issue
Block a user