Правки после ревью документации
This commit is contained in:
@@ -25,6 +25,18 @@
|
|||||||
- **Минимум компонентов** — в духе umbar, без зоопарка сервисов. Внешние
|
- **Минимум компонентов** — в духе umbar, без зоопарка сервисов. Внешние
|
||||||
базы метаданных (TMDB/TVDB) опциональны, включаются конфигом.
|
базы метаданных (TMDB/TVDB) опциональны, включаются конфигом.
|
||||||
|
|
||||||
|
## Инварианты (безопасность данных)
|
||||||
|
|
||||||
|
- **Источник неприкосновенен:** только `mkdir` / `link(2)` / `unlink`
|
||||||
|
своих ссылок; никогда не трогаем файлы под `paths.downloads`.
|
||||||
|
- **Целевой путь санитизируется** и проверяется, что он строго под
|
||||||
|
`paths.movies`/`series` (защита от traversal); существующее не
|
||||||
|
перезаписываем.
|
||||||
|
- **Выход LLM недоверенный** — безопасность на валидации пути, не на
|
||||||
|
промпте. Авто-раскладка только при подтверждённом матче в базе.
|
||||||
|
- **Запуск:** контейнер под `1000:1000`, `network_mode: host`, mount
|
||||||
|
`/srv` + data-том для SQLite/конфига.
|
||||||
|
|
||||||
## Документация: три раздела
|
## Документация: три раздела
|
||||||
|
|
||||||
- `docs/specs/` — **живые** спецификации целевого состояния. Меняем по
|
- `docs/specs/` — **живые** спецификации целевого состояния. Меняем по
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Docker как единица деплоя, образ собирается на сервере
|
||||||
|
|
||||||
|
- Дата: 2026-06-13
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
|
||||||
|
jellybit — статический Go-бинарь (см.
|
||||||
|
[ADR-2026-06-13-go-single-binary](ADR-2026-06-13-go-single-binary.md)). На
|
||||||
|
сервере umbar qBittorrent и (в планах) Jellyfin работают в docker. Нужно
|
||||||
|
выбрать, как запускать jellybit и как доставлять его на сервер.
|
||||||
|
|
||||||
|
## Рассмотренные варианты
|
||||||
|
|
||||||
|
- **Нативный systemd-юнит** — ближе всего к «просто скопировать бинарь»,
|
||||||
|
но это отдельная среда вне docker; разнобой со смежными приложениями
|
||||||
|
(сети, монтирования, политики рестарта).
|
||||||
|
- **Docker, образ собран и запушен из CI/реестра** — каноничнее, но в
|
||||||
|
домашней лаборатории это лишний реестр и пайплайн.
|
||||||
|
- **Docker, образ собирается на сервере из готового бинаря** — на сервер
|
||||||
|
кладутся бинарь + `Dockerfile`, `docker build` выполняется на месте.
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
|
||||||
|
jellybit запускаем в **docker** — в одной среде с qBittorrent/Jellyfin
|
||||||
|
(единый способ управления, сети, монтирований). Образ **собираем на
|
||||||
|
сервере** из доставленного бинаря и `Dockerfile` (копирует бинарь в
|
||||||
|
`distroless/static`). Go-тулчейн и реестр на сервере не нужны. `Dockerfile`
|
||||||
|
(упаковка) живёт в jellybit; оркестрация (доставка, build, compose с
|
||||||
|
`network_mode: host`, `user 1000:1000`, mount `/srv` и data-тома) — в
|
||||||
|
umbar.
|
||||||
|
|
||||||
|
## Последствия
|
||||||
|
|
||||||
|
- `+` Одна среда со смежными сервисами; единые сети/монтирования/политики.
|
||||||
|
- `+` Доставка остаётся дешёвой: бинарь + `Dockerfile`, без реестра.
|
||||||
|
- `+` Образ версионируется (тег по сборке) — есть откат.
|
||||||
|
- `-` Шаг `docker build` на сервере (на Intel N150 дёшево).
|
||||||
|
- `-` Лёгкое отступление от «просто скопировать бинарь» — оправдано
|
||||||
|
единообразием среды с qBittorrent/Jellyfin.
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Хардлинки вместо копирования и симлинков
|
||||||
|
|
||||||
|
- Дата: 2026-06-13
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
|
||||||
|
jellybit раскладывает скачанные qBittorrent'ом файлы в библиотеку
|
||||||
|
Jellyfin. Два требования тянут в разные стороны: раздача должна
|
||||||
|
продолжаться (источник неприкосновенен), а место на диске — не
|
||||||
|
дублироваться. qBittorrent пишет в `/srv/downloads`, Jellyfin читает
|
||||||
|
`/srv/media` — обе ветки на одной ФС.
|
||||||
|
|
||||||
|
## Рассмотренные варианты
|
||||||
|
|
||||||
|
- **Хардлинк** — второе имя того же inode в `/srv/media`. Плюсы: ноль
|
||||||
|
доп. места, раздача цела, файл «настоящий» для Jellyfin. Минусы: только
|
||||||
|
в пределах одной ФС; нельзя линковать каталоги (только файлы).
|
||||||
|
- **Копирование** (поведение radarr/sonarr по умолчанию) — дублирует
|
||||||
|
десятки ГБ на каждый релиз; для домашнего сервера дорого и медленно.
|
||||||
|
- **Симлинк** — место экономит, но ломается при перемещении источника,
|
||||||
|
Jellyfin/плееры иногда плохо дружат с символическими ссылками, а
|
||||||
|
удаление раздачи рвёт библиотеку.
|
||||||
|
- **Перемещение** — убивает раздачу (сид, ratio) и нарушает «источник
|
||||||
|
неприкосновенен».
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
|
||||||
|
Раскладываем **хардлинками**. На одной ФС (`/srv`) это бесплатно по месту,
|
||||||
|
раздача продолжается, файл неотличим от обычного. Линкуем только файлы,
|
||||||
|
целевые каталоги создаём `mkdir`. Жёсткий инвариант: jellybit никогда не
|
||||||
|
перемещает и не удаляет исходные файлы; undo удаляет только свои ссылки
|
||||||
|
под `/srv/media`.
|
||||||
|
|
||||||
|
## Последствия
|
||||||
|
|
||||||
|
- `+` Ноль дублирования, мгновенно, раздача цела.
|
||||||
|
- `+` Простая и безопасная модель операций: только add-link и
|
||||||
|
remove-own-link.
|
||||||
|
- `-` Требуется одна ФС — внутри docker обеспечивается монтированием
|
||||||
|
общего родителя `/srv` (иначе `link(2)` даёт `EXDEV`).
|
||||||
|
- `-` Каталоги хардлинковать нельзя — раскладка пофайловая, целевые папки
|
||||||
|
создаём сами (0755, владелец 1000:1000).
|
||||||
@@ -56,4 +56,6 @@
|
|||||||
|
|
||||||
| Дата | Запись | Статус |
|
| Дата | Запись | Статус |
|
||||||
| ---------- | ---------------------------------------------------------------- | ------ |
|
| ---------- | ---------------------------------------------------------------- | ------ |
|
||||||
|
| 2026-06-13 | [Docker как единица деплоя](ADR-2026-06-13-docker-deploy.md) | — |
|
||||||
|
| 2026-06-13 | [Хардлинки вместо копирования и симлинков](ADR-2026-06-13-hardlinks.md) | — |
|
||||||
| 2026-06-13 | [Go и доставка одним бинарём](ADR-2026-06-13-go-single-binary.md) | — |
|
| 2026-06-13 | [Go и доставка одним бинарём](ADR-2026-06-13-go-single-binary.md) | — |
|
||||||
|
|||||||
@@ -31,8 +31,10 @@ qBittorrent. Решим по опыту эксплуатации.
|
|||||||
|
|
||||||
## Доступ к веб-UI
|
## Доступ к веб-UI
|
||||||
|
|
||||||
Сейчас предполагается доверенная локальная сеть. Если понадобится —
|
Решено для v1: без авторизации в доверенной LAN, опц. allowlist подсетей
|
||||||
простая авторизация или вынос за reverse-proxy с аутентификацией.
|
(`http.trusted_subnets`) — как умеет qBittorrent. На будущее, если
|
||||||
|
понадобится защита: токен/Basic в самом приложении или вынос за
|
||||||
|
reverse-proxy с аутентификацией.
|
||||||
|
|
||||||
## Повторный прогон распознавания
|
## Повторный прогон распознавания
|
||||||
|
|
||||||
|
|||||||
+11
-7
@@ -10,15 +10,18 @@
|
|||||||
копирует готовый бинарь), golangci-lint, lefthook. Документация (этот
|
копирует готовый бинарь), golangci-lint, lefthook. Документация (этот
|
||||||
этап — частично готов).
|
этап — частично готов).
|
||||||
- **Ф1 — ingest + tracking (без LLM).** `Ingest()` + добавление в
|
- **Ф1 — ingest + tracking (без LLM).** `Ingest()` + добавление в
|
||||||
qBittorrent (категория `jellybit`) + `worker`-поллинг завершения +
|
qBittorrent (источник отдаём ему, категория `jellybit`, ключ
|
||||||
машина состояний. Наружу: HTTP API, список в веб-UI, `jellybit add`.
|
идемпотентности по infohash) + `worker`-поллинг завершения (трансляция
|
||||||
|
`path_map`) + машина состояний. Наружу: HTTP API, список в веб-UI,
|
||||||
|
`jellybit add`.
|
||||||
- **Ф2 — распознавание.** `go-ptn` + LLM (structured output) → план +
|
- **Ф2 — распознавание.** `go-ptn` + LLM (structured output) → план +
|
||||||
оценка уверенности. Без записи на диск.
|
оценка уверенности. Без записи на диск.
|
||||||
- **Ф3 — раскладка + минимальный review.** Хардлинки по конвенциям
|
- **Ф3 — раскладка + минимальный review.** Хардлинки по конвенциям
|
||||||
Jellyfin, субтитры, идемпотентность, **undo**. Авто при высокой
|
Jellyfin (санитизация пути, never-overwrite), субтитры, идемпотентность,
|
||||||
уверенности; низкая → review (htmx): подсказка + перераспознавание, из
|
**undo**. Авто только при матче в базе и чистой валидации; иначе → review
|
||||||
ручного — тип, выбор кандидата базы, пометка «игнор». Полный редактор
|
(htmx): подсказка + перераспознавание, из ручного — тип, выбор кандидата
|
||||||
маппинга — Ф5. См. [review-ux.md](../specs/review-ux.md).
|
базы, пометка «игнор». Полный редактор маппинга — Ф5. См.
|
||||||
|
[review-ux.md](../specs/review-ux.md).
|
||||||
- **Ф4 — метаданные.** TMDB/TVDB опционально, provider-id в именах,
|
- **Ф4 — метаданные.** TMDB/TVDB опционально, provider-id в именах,
|
||||||
валидация распознавания против числа серий.
|
валидация распознавания против числа серий.
|
||||||
- **Ф5 — Telegram + UX.** Бот-адаптер + парсер сообщений торрент-бота,
|
- **Ф5 — Telegram + UX.** Бот-адаптер + парсер сообщений торрент-бота,
|
||||||
@@ -27,7 +30,8 @@
|
|||||||
нотификации.
|
нотификации.
|
||||||
- **Ф6 — деплой.** Сборка статического бинаря здесь; доставка бинаря +
|
- **Ф6 — деплой.** Сборка статического бинаря здесь; доставка бинаря +
|
||||||
`Dockerfile` на сервер, `docker build` и запуск на месте; оркестрация —
|
`Dockerfile` на сервер, `docker build` и запуск на месте; оркестрация —
|
||||||
`playbook-jellybit.yml` в umbar.
|
`playbook-jellybit.yml` в umbar: `network_mode: host`, `user 1000:1000`,
|
||||||
|
mount `/srv` + data-том `/srv/applications/jellybit/data`, healthcheck.
|
||||||
|
|
||||||
## Заметки по порядку
|
## Заметки по порядку
|
||||||
|
|
||||||
|
|||||||
+195
-87
@@ -11,109 +11,157 @@ qBittorrent, определяет содержимое (фильм или сер
|
|||||||
|
|
||||||
- **Один статический бинарь.** Доставка — копированием на сервер. См.
|
- **Один статический бинарь.** Доставка — копированием на сервер. См.
|
||||||
[ADR-2026-06-13-go-single-binary](../adr/ADR-2026-06-13-go-single-binary.md).
|
[ADR-2026-06-13-go-single-binary](../adr/ADR-2026-06-13-go-single-binary.md).
|
||||||
- **Источник не трогаем.** В библиотеку кладём хардлинки; qBittorrent
|
- **Источник неприкосновенен** (жёсткий инвариант). jellybit делает
|
||||||
продолжает раздачу, место на диске не дублируется.
|
только `mkdir`, `link(2)` и `unlink` *своих* целевых ссылок (для undo).
|
||||||
- **Единое ядро, тонкие транспорты.** Вся логика приёма загрузки — в
|
Никогда не `unlink`/`rename` под `paths.downloads`. См.
|
||||||
use-case `Ingest`. HTTP API, веб-UI и Telegram — обёртки над ним.
|
[ADR-2026-06-13-hardlinks](../adr/ADR-2026-06-13-hardlinks.md).
|
||||||
|
- **Выход распознавания недоверенный.** Имена файлов, контекст и
|
||||||
|
сообщение бота управляются извне. Целевой путь всегда санитизируется и
|
||||||
|
проверяется, что он строго под `paths.movies`/`paths.series` (см.
|
||||||
|
«Раскладка файлов»). Безопасность держится на валидации, не на промпте.
|
||||||
|
- **Единое ядро, тонкие транспорты.** Логика приёма — в use-case
|
||||||
|
`Ingest`; переходы состояний принадлежат `worker`. HTTP API, веб-UI и
|
||||||
|
Telegram складывают команды, `worker` их сериализует.
|
||||||
- **Опциональные внешние зависимости.** Базы метаданных (TMDB/TVDB)
|
- **Опциональные внешние зависимости.** Базы метаданных (TMDB/TVDB)
|
||||||
включаются конфигом; без них сервис работает на одном LLM.
|
включаются конфигом; без них сервис работает на одном LLM, но
|
||||||
|
авто-раскладка без матча в базе не делается (см. recognition.md).
|
||||||
- **Минимум компонентов.** В духе umbar — без лишних сервисов.
|
- **Минимум компонентов.** В духе umbar — без лишних сервисов.
|
||||||
|
|
||||||
## Компоненты
|
## Компоненты
|
||||||
|
|
||||||
| Пакет | Ответственность |
|
| Пакет | Ответственность |
|
||||||
| ----------- | ----------------------------------------------------- |
|
| ----------- | -------------------------------------------------------- |
|
||||||
| `ingest` | use-case приёма загрузки, общий для всех транспортов |
|
| `ingest` | use-case приёма загрузки, общий для всех транспортов |
|
||||||
| `qbt` | клиент qBittorrent WebUI API |
|
| `qbt` | клиент qBittorrent WebUI API (сессия, добавление, опрос) |
|
||||||
| `worker` | фоновый цикл: машина состояний, поллинг завершения |
|
| `worker` | владелец машины состояний; поллинг, сериализация команд |
|
||||||
| `recognize` | пред-парс имени + вызов LLM + модель уверенности |
|
| `recognize` | пред-парс имени + вызов LLM + модель уверенности |
|
||||||
| `llm` | провайдер LLM за интерфейсом (дискриминатор `type`) |
|
| `llm` | провайдер LLM за интерфейсом (дискриминатор `type`) |
|
||||||
| `metadata` | интерфейс баз метаданных + TMDB/TVDB (опц.) |
|
| `metadata` | интерфейс баз метаданных + TMDB/TVDB (опц.) |
|
||||||
| `layout` | конвенции Jellyfin + хардлинкер |
|
| `layout` | конвенции Jellyfin, санитизация путей, хардлинкер, undo |
|
||||||
| `store` | SQLite: загрузки, распознавание, ссылки |
|
| `store` | SQLite: загрузки, распознавание, подсказки, ссылки |
|
||||||
| `httpapi` | REST + веб-UI (server-rendered, htmx) |
|
| `httpapi` | REST + веб-UI (server-rendered, htmx) |
|
||||||
| `tgbot` | Telegram-адаптер + парсер сообщений торрент-бота |
|
| `tgbot` | Telegram: приём + парсер сообщений бота + исходящие пинги |
|
||||||
| `config` | загрузка TOML-конфига |
|
| `config` | загрузка TOML-конфига |
|
||||||
|
|
||||||
## Поток и машина состояний
|
## Поток и машина состояний
|
||||||
|
|
||||||
```
|
```
|
||||||
ingest → downloading → completed → recognizing ─┬─ уверенно ───────→ linking → done
|
ingest → downloading → completed → recognizing ──┬─ авто ────────────────→ linking → done
|
||||||
└─ сомнительно → review → linking → done
|
│ │ │ └─ review ⇄ recognizing ─→ linking → done
|
||||||
любой шаг при ошибке → failed
|
│ │ └─ moving/checking (ещё не готов)
|
||||||
|
│ └─ stuck (не качается дольше таймаута)
|
||||||
|
└─ failed ⇄ retry
|
||||||
|
|
||||||
|
done → undo → reverted
|
||||||
|
review → «Позже» → deferred → review
|
||||||
|
любой → «Отклонить» → cancelled
|
||||||
```
|
```
|
||||||
|
|
||||||
- **ingest** — приняли источник + контекст, поставили в qBittorrent
|
- **ingest** — приняли источник + контекст, отдали в qBittorrent
|
||||||
(категория `jellybit`), записали в БД.
|
(категория `jellybit`), записали в БД с ключом идемпотентности.
|
||||||
- **downloading / completed** — `worker` поллит qBittorrent по категории
|
- **downloading / completed** — `worker` поллит qBittorrent по категории
|
||||||
(интервал `worker.poll_interval`, по умолчанию 5 с).
|
(`worker.poll_interval`, 5 с). Готовность — только когда файлы на месте
|
||||||
- **recognizing** — `recognize` строит план раскладки и оценку
|
(не `moving`/`checking*`), см. «Завершение в qBittorrent».
|
||||||
уверенности (см. [recognition.md](recognition.md)).
|
- **recognizing** — `recognize` строит план и оценку уверенности
|
||||||
- **review** — план уходит человеку (веб-UI / Telegram), ждём решения;
|
([recognition.md](recognition.md)). Невалидный/непарсящийся ответ LLM →
|
||||||
сценарии — в [review-ux.md](review-ux.md).
|
review (не failed).
|
||||||
- **linking** — `layout` создаёт хардлинки в библиотеке.
|
- **review** — план уходит человеку ([review-ux.md](review-ux.md)); цикл
|
||||||
- **done** — опционально дёргаем скан библиотеки Jellyfin.
|
`review ⇄ recognizing` — перераспознавание по подсказке.
|
||||||
|
- **linking** — `layout` создаёт хардлинки; идемпотентно, батчем.
|
||||||
|
- **done** — опционально дёргаем скан Jellyfin; доступен **undo** →
|
||||||
|
`reverted` (убрать созданные ссылки).
|
||||||
|
- **deferred / cancelled / failed / stuck** — «Позже», «Отклонить»,
|
||||||
|
ошибка (ретраибельна), не качается дольше таймаута.
|
||||||
|
|
||||||
Состояние персистентно в SQLite — перезапуск сервиса безопасен, `worker`
|
Все переходы и команды идут через `worker` под per-download блокировкой —
|
||||||
продолжает с того же места.
|
два транспорта не гонятся за одно состояние. Состояние персистентно в
|
||||||
|
SQLite; на старте `worker` сверяет категорию qBittorrent с БД и
|
||||||
|
продолжает.
|
||||||
|
|
||||||
## Транспорты
|
## Транспорты
|
||||||
|
|
||||||
Все три ведут в один `Ingest(req)`:
|
Все ведут в один `Ingest(req)`; действия пользователя (apply / refine /
|
||||||
|
reject / defer / undo) — команды к `worker`:
|
||||||
|
|
||||||
- **HTTP API + веб-UI** — форма «добавить», список загрузок, экран
|
- **HTTP API + веб-UI** — форма «добавить», список, экран ревью
|
||||||
подтверждения раскладки (server-rendered + htmx, без JS-сборки).
|
(server-rendered + htmx). В v1 **без авторизации** (доверенная LAN), с
|
||||||
- **Telegram-бот** — переслать magnet или сообщение торрент-бота прямо в
|
опциональным allowlist подсетей (`http.trusted_subnets`). Защиту
|
||||||
jellybit; текст становится контекстом распознавания.
|
навесим позже — [drafts/ideas.md](../drafts/ideas.md).
|
||||||
|
- **Telegram-бот** — переслать magnet/сообщение бота; текст становится
|
||||||
|
контекстом. Доступ — по `telegram.allowed_user_ids` (пусто = запрет
|
||||||
|
всем, fail-closed). Бот же шлёт **пинги** о входе в review/готовности.
|
||||||
- **CLI** — `jellybit add <magnet> --context "..."` для отладки.
|
- **CLI** — `jellybit add <magnet> --context "..."` для отладки.
|
||||||
|
|
||||||
|
Источник (magnet / `.torrent` / URL) **отдаём в qBittorrent** — он сам
|
||||||
|
скачивает; jellybit не делает исходящих запросов на пользовательский URL
|
||||||
|
(SSRF исключён).
|
||||||
|
|
||||||
## Хранилище
|
## Хранилище
|
||||||
|
|
||||||
SQLite, минимум таблиц:
|
SQLite. Схема покрывает приём, цикл ревью и откат:
|
||||||
|
|
||||||
- `download` — источник, контекст, hash торрента, категория, состояние,
|
- `download` — `id`, тип и значение источника, контекст, `infohash`,
|
||||||
тайминги.
|
`idempotency_key`, состояние, `error_code`/`error_msg`, тайминги.
|
||||||
- `recognition` — тип, название, год, сезон, provider-id, оценка
|
(infohash может появиться позже приёма — для magnet без метаданных.)
|
||||||
уверенности, сырой ответ LLM.
|
- `recognition` — попытки распознавания: `download_id`, `attempt_no`,
|
||||||
- `file_link` — соответствие исходный файл → целевой путь, вид
|
`is_current`, тип, название, год, `provider` (`tmdb|tvdb|none`),
|
||||||
(видео/субтитры), статус.
|
`provider_id`, `confidence`, причины-не-авто, сырой ответ LLM.
|
||||||
|
- `hint` — накопленные подсказки человека (`download_id`, текст, время).
|
||||||
|
- `override` — запиненные ручные правки полей (перераспознавание не
|
||||||
|
затирает).
|
||||||
|
- `metadata_candidate` — кандидаты базы для выбора (`recognition_id`,
|
||||||
|
provider, id, название, год, выбран ли).
|
||||||
|
- `file_link` — `download_id`, `apply_batch_id`, исходный → целевой путь,
|
||||||
|
вид (видео/субтитры/…), статус, время. Батч нужен для точечного undo.
|
||||||
|
|
||||||
|
Дубликат `infohash` при приёме — повторно не добавляем, ведём к
|
||||||
|
существующей загрузке (идемпотентность).
|
||||||
|
|
||||||
## Конфигурация
|
## Конфигурация
|
||||||
|
|
||||||
TOML. В репозитории — `config.example.toml` с placeholder'ами; реальный
|
TOML. В репозитории — `config.example.toml` с placeholder'ами; реальный
|
||||||
`config.toml` рендерится при деплое Ansible-шаблоном из переменных umbar
|
`config.toml` рендерится при деплое Ansible-шаблоном из переменных umbar
|
||||||
(секреты — в `vars/secrets.yml` под ansible-vault) и не коммитится.
|
(секреты — `vars/secrets.yml` под ansible-vault), на диске **0600**,
|
||||||
Пример:
|
владелец `1000:1000`, не коммитится. Пример:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[qbittorrent]
|
[qbittorrent]
|
||||||
url = "http://127.0.0.1:8989"
|
url = "http://127.0.0.1:8989" # работает при network_mode: host
|
||||||
username = "admin"
|
username = "admin"
|
||||||
password = ""
|
password = ""
|
||||||
category = "jellybit"
|
category = "jellybit"
|
||||||
|
# qBit отдаёт пути своего контейнера ("/downloads/…"); транслируем в хост-путь
|
||||||
|
path_map = { "/downloads" = "/srv/downloads" }
|
||||||
|
|
||||||
[paths]
|
[paths]
|
||||||
|
# хост-пути (видны внутри контейнера jellybit через mount /srv)
|
||||||
downloads = "/srv/downloads"
|
downloads = "/srv/downloads"
|
||||||
movies = "/srv/media/movies"
|
movies = "/srv/media/movies"
|
||||||
series = "/srv/media/series"
|
series = "/srv/media/series"
|
||||||
|
|
||||||
[llm]
|
[llm]
|
||||||
# type — дискриминатор реализации; пока поддерживается "openai-compat"
|
# type — дискриминатор реализации; пока поддерживается "openai-compat"
|
||||||
type = "openai-compat"
|
type = "openai-compat"
|
||||||
base_url = "http://127.0.0.1:1234/v1"
|
base_url = "http://127.0.0.1:1234/v1"
|
||||||
api_key = ""
|
api_key = ""
|
||||||
model = "qwen2.5-32b-instruct"
|
model = "qwen2.5-32b-instruct"
|
||||||
|
timeout = "120s"
|
||||||
|
max_retries = 3 # непарсящийся ответ после ретраев → review
|
||||||
|
|
||||||
[metadata.tmdb]
|
[metadata.tmdb]
|
||||||
enabled = true
|
enabled = false # включается ключом; без матча авто не делаем
|
||||||
api_key = ""
|
api_key = ""
|
||||||
|
timeout = "10s"
|
||||||
|
|
||||||
[metadata.tvdb]
|
[metadata.tvdb]
|
||||||
enabled = false
|
enabled = false
|
||||||
api_key = ""
|
api_key = ""
|
||||||
|
timeout = "10s"
|
||||||
|
|
||||||
[worker]
|
[worker]
|
||||||
poll_interval = "5s" # как часто опрашивать qBittorrent
|
poll_interval = "5s" # как часто опрашивать qBittorrent
|
||||||
|
stuck_after = "1h" # не качается дольше → stuck
|
||||||
|
magnet_timeout = "30m" # magnet без метаданных дольше → failed
|
||||||
|
|
||||||
[recognition]
|
[recognition]
|
||||||
auto_confidence_threshold = 0.85
|
auto_confidence_threshold = 0.85
|
||||||
@@ -121,10 +169,11 @@ auto_confidence_threshold = 0.85
|
|||||||
[telegram]
|
[telegram]
|
||||||
enabled = false
|
enabled = false
|
||||||
token = ""
|
token = ""
|
||||||
allowed_user_ids = []
|
allowed_user_ids = [] # пусто = запрет всем (fail-closed)
|
||||||
|
|
||||||
[http]
|
[http]
|
||||||
listen = ":8080"
|
listen = ":8080"
|
||||||
|
trusted_subnets = [] # опц. allowlist подсетей; пусто = без ограничений
|
||||||
|
|
||||||
[log]
|
[log]
|
||||||
level = "info"
|
level = "info"
|
||||||
@@ -133,36 +182,91 @@ format = "json"
|
|||||||
|
|
||||||
## Логирование
|
## Логирование
|
||||||
|
|
||||||
Структурированный JSON через `log/slog`. Каждая загрузка проходит со
|
Структурированный JSON через `log/slog`, в stdout (docker подбирает).
|
||||||
сквозным идентификатором; решения распознавания (почему авто/ревью)
|
Каждая загрузка проходит со сквозным идентификатором; решения
|
||||||
логируются явно.
|
распознавания (почему авто/ревью) и операции с файлами логируются явно.
|
||||||
|
|
||||||
|
## Завершение в qBittorrent
|
||||||
|
|
||||||
|
`worker` опрашивает qBittorrent по категории и сопоставляет состояния:
|
||||||
|
|
||||||
|
- **готово к раскладке:** `uploading`/`stalledUP`/`pausedUP`/`queuedUP`/
|
||||||
|
`forcedUP` — и **только** когда нет `moving`/`checkingUP`.
|
||||||
|
- **ещё качается:** `downloading`/`stalledDL`/`metaDL`/`queuedDL`/
|
||||||
|
`checkingDL`/`forcedDL`.
|
||||||
|
- **застряло:** `metaDL` дольше `magnet_timeout`, `stalledDL` дольше
|
||||||
|
`stuck_after` → `stuck`/`failed`.
|
||||||
|
- **ошибка:** `error`/`missingFiles` → `failed`.
|
||||||
|
|
||||||
|
Пути файлов берём из API (`save_path`/`content_path` + относительные
|
||||||
|
имена), не из константы, и транслируем `path_map`. Предполагаем, что
|
||||||
|
отдельный «incomplete»-каталог в qBittorrent выключен (иначе путь до
|
||||||
|
завершения иной).
|
||||||
|
|
||||||
|
## Раскладка файлов
|
||||||
|
|
||||||
|
`layout` создаёт хардлинки в `paths.movies`/`paths.series` по конвенциям
|
||||||
|
Jellyfin ([jellyfin-layout.md](jellyfin-layout.md)). Правила:
|
||||||
|
|
||||||
|
- **Линкуем только файлы.** Целевые каталоги создаём `mkdir -p` (режим
|
||||||
|
0755, владелец `1000:1000`); каталог не хардлинкуется.
|
||||||
|
- **Путь сначала санитизируется:** из `title`/сезона/серии убираем
|
||||||
|
разделители пути, `..`, управляющие символы; финальный
|
||||||
|
`filepath.Clean`-путь обязан быть строго под библиотекой, иначе отказ
|
||||||
|
(защита от traversal).
|
||||||
|
- **Никогда не перезаписываем.** Цель существует и это тот же inode →
|
||||||
|
готово (идемпотентно); существует и это другой файл → коллизия → review.
|
||||||
|
- **Батч фиксируется в БД:** статус по каждому файлу; повтор после сбоя
|
||||||
|
доводит начатое (идемпотентно) либо откатывается.
|
||||||
|
- **Undo** удаляет только ссылки своего `apply_batch_id` и только если
|
||||||
|
путь под `paths.movies`/`series` — источник недосягаем.
|
||||||
|
- **Одна ФС обязательна.** `link(2)` через границы ФС даёт `EXDEV` —
|
||||||
|
падаем с понятной ошибкой; по построению этого не должно случаться.
|
||||||
|
|
||||||
|
### Пути и контейнеры (три namespace)
|
||||||
|
|
||||||
|
`/srv/downloads` и `/srv/media` — одна ФС на хосте (подтверждено), но
|
||||||
|
путь существует в трёх пространствах имён:
|
||||||
|
|
||||||
|
- **контейнер qBittorrent** видит только `/downloads` (= хост
|
||||||
|
`/srv/downloads`); `/srv/media` он не монтирует — и не должен.
|
||||||
|
- **контейнер jellybit** монтирует **общего родителя `/srv`** одним
|
||||||
|
bind-mount'ом — так `downloads` и `media` гарантированно на одной ФС
|
||||||
|
внутри него, и хардлинк проходит.
|
||||||
|
- **хост** — где обе ветки реально на одном томе.
|
||||||
|
|
||||||
|
Поэтому путь из qBittorrent (`/downloads/…`) транслируется в хост-путь
|
||||||
|
(`/srv/downloads/…`) по `qbittorrent.path_map`, и уже он используется как
|
||||||
|
источник хардлинка.
|
||||||
|
|
||||||
## Деплой
|
## Деплой
|
||||||
|
|
||||||
Jellybit работает в **docker** — в одной среде с qBittorrent и Jellyfin
|
Jellybit работает в **docker** — в одной среде с qBittorrent и Jellyfin
|
||||||
(они тоже в контейнерах на umbar). Единая среда запуска перевешивает
|
(см. [ADR-2026-06-13-docker-deploy](../adr/ADR-2026-06-13-docker-deploy.md)).
|
||||||
простоту нативного systemd.
|
Сборка: статический бинарь (`GOOS=linux GOARCH=amd64 CGO_ENABLED=0`,
|
||||||
|
сервер на Intel N150) собирается здесь; на сервер во временную build-папку
|
||||||
|
кладутся бинарь + `Dockerfile` (копирует бинарь в `distroless/static`),
|
||||||
|
образ собирается на месте и запускается. Go-тулчейн на сервере не нужен.
|
||||||
|
|
||||||
Сборка — дёшево и сердито: статический бинарь собирается здесь; на сервер
|
Параметры запуска (в umbar-compose):
|
||||||
во временную build-папку кладутся бинарь + `Dockerfile` (он просто
|
|
||||||
копирует бинарь в минимальный образ), образ собирается прямо на сервере и
|
- **`network_mode: host`** — чтобы `127.0.0.1:8989` достучался до
|
||||||
запускается. Go-тулчейн на сервере не нужен — только docker.
|
qBittorrent (он на bridge с проброшенным портом; из отдельного
|
||||||
|
bridge-контейнера `127.0.0.1` — это он сам). Заодно `:8080` слушает на
|
||||||
|
хосте. Прецедент в umbar — glances.
|
||||||
|
- **`user: "1000:1000"`**, UMASK 022 — единый системный пользователь
|
||||||
|
umbar; созданные каталоги 0755, файлы-ссылки наследуют inode источника.
|
||||||
|
- **mount `/srv`** (общий родитель) — для хардлинков (см. выше).
|
||||||
|
- **mount данных** `/srv/applications/jellybit/data` → `/data`: SQLite
|
||||||
|
(`/data/jellybit.db`) и `config.toml`. Без него редеплой стёр бы всё
|
||||||
|
in-flight состояние.
|
||||||
|
- **healthcheck** на `/healthz`.
|
||||||
|
|
||||||
Разделение ответственности:
|
Разделение ответственности:
|
||||||
|
|
||||||
- **jellybit** (этот репозиторий) — производит статический бинарь и
|
- **jellybit** (этот репозиторий) — статический бинарь и `Dockerfile`.
|
||||||
`Dockerfile`.
|
- **umbar** — оркестрация: доставка артефактов, `docker build`, запуск
|
||||||
- **umbar** — оркестрация деплоя: доставка артефактов, `docker build` и
|
через docker compose (`playbook-jellybit.yml`) с параметрами выше.
|
||||||
запуск через docker compose (`playbook-jellybit.yml`).
|
|
||||||
|
|
||||||
## Раскладка файлов
|
|
||||||
|
|
||||||
Хардлинки в `paths.movies` / `paths.series` по конвенциям Jellyfin.
|
|
||||||
Детали и крайние случаи — в [jellyfin-layout.md](jellyfin-layout.md).
|
|
||||||
`/srv/downloads` и `/srv/media` — одна ФС (подтверждено), поэтому
|
|
||||||
хардлинки применимы. Так как jellybit в docker (см. «Деплой»), контейнеру
|
|
||||||
монтируем общего родителя `/srv` — чтобы внутри оба каталога остались на
|
|
||||||
одной ФС и хардлинк проходил.
|
|
||||||
|
|
||||||
## Предполагаемая структура репозитория
|
## Предполагаемая структура репозитория
|
||||||
|
|
||||||
@@ -174,21 +278,25 @@ internal/
|
|||||||
migrations/ миграции SQLite
|
migrations/ миграции SQLite
|
||||||
web/templates/ шаблоны веб-UI
|
web/templates/ шаблоны веб-UI
|
||||||
docs/ specs / adr / drafts
|
docs/ specs / adr / drafts
|
||||||
config.example.toml
|
Dockerfile .dockerignore config.example.toml
|
||||||
```
|
```
|
||||||
|
|
||||||
## Решённые вопросы
|
## Решённые вопросы
|
||||||
|
|
||||||
- `/srv/downloads` и `/srv/media` — одна ФС (подтверждено); хардлинки
|
- Пути/контейнеры — три namespace сведены: qBit отдаёт `/downloads`,
|
||||||
применимы.
|
транслируем в хост через `path_map`; jellybit монтирует `/srv`.
|
||||||
- Детект завершения — поллинг qBittorrent раз в несколько секунд
|
- Сеть jellybit↔qBittorrent — `network_mode: host`.
|
||||||
(`worker.poll_interval`). Webhook — возможная оптимизация на будущее
|
- Состояние — на persistent-томе `/srv/applications/jellybit/data`.
|
||||||
([drafts/ideas.md](../drafts/ideas.md)).
|
- Детект завершения — поллинг; webhook — на будущее (drafts/ideas).
|
||||||
- Секреты — в переменных umbar; `config.toml` рендерится Ansible-шаблоном
|
- Источник (magnet/URL/.torrent) отдаём в qBittorrent — без SSRF.
|
||||||
при деплое.
|
- Авто-раскладка требует подтверждённого матча в базе; иначе review.
|
||||||
- Форма запуска — **docker**, образ собирается на сервере из готового
|
- Веб-UI в v1 без авторизации (доверенная LAN, опц. allowlist подсетей).
|
||||||
бинаря (см. «Деплой»).
|
- Форма запуска — docker, образ собирается на сервере; контейнер под
|
||||||
|
`1000:1000`, `network_mode: host`, mount `/srv` + data-том.
|
||||||
|
|
||||||
## Открытые вопросы
|
## Открытые вопросы
|
||||||
|
|
||||||
Существенных пока нет.
|
- Конфигурация Jellyfin (URL + API-ключ) и триггер скана — когда Jellyfin
|
||||||
|
будет развёрнут в umbar (сейчас его там нет).
|
||||||
|
- Подтвердить, что «incomplete»-каталог qBittorrent выключен (иначе путь
|
||||||
|
файлов до завершения иной).
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ movies/
|
|||||||
- provider-id в имени папки (`[tmdbid-...]`) добавляется при работе с
|
- provider-id в имени папки (`[tmdbid-...]`) добавляется при работе с
|
||||||
базой — снимает неоднозначность для русских названий, которые Jellyfin
|
базой — снимает неоднозначность для русских названий, которые Jellyfin
|
||||||
иначе может опознать неверно.
|
иначе может опознать неверно.
|
||||||
- Внешние субтитры — `Имя.<lang>.srt`, при необходимости `.forced`.
|
- Внешние субтитры — `Имя.<lang>[.flag].srt` (флаги `forced`/`sdh`/
|
||||||
|
`default`/`hi`), напр. `…ru.forced.srt`; база имени совпадает с именем
|
||||||
|
видеофайла. Пары VobSub — `.idx` + `.sub`.
|
||||||
|
|
||||||
## Сериалы
|
## Сериалы
|
||||||
|
|
||||||
@@ -35,18 +37,32 @@ series/
|
|||||||
|
|
||||||
## Сопоставление источник → цель
|
## Сопоставление источник → цель
|
||||||
|
|
||||||
qBittorrent держит файлы в `paths.downloads`. Для каждого распознанного
|
Источник берём по пути из qBittorrent (`save_path`/`content_path` +
|
||||||
файла создаётся **хардлинк** в `paths.movies` / `paths.series` с целевым
|
относительное имя, после трансляции `path_map` в хост-путь). Для каждого
|
||||||
именем. Исходный файл остаётся на месте (раздача продолжается), inode
|
распознанного **файла** (не каталога) создаётся **хардлинк** в
|
||||||
общий — диск не дублируется.
|
`paths.movies`/`paths.series`; целевые каталоги — `mkdir` (0755,
|
||||||
|
`1000:1000`). Исходный файл остаётся на месте (раздача продолжается),
|
||||||
|
inode общий — диск не дублируется.
|
||||||
|
|
||||||
Требование: целевой и исходный каталоги — на одной ФС.
|
Целевое имя строится из распознанных полей и **санитизируется** (без
|
||||||
|
разделителей пути, `..`, управляющих символов); финальный путь обязан
|
||||||
|
быть строго под библиотекой. Существующую цель **не перезаписываем** (тот
|
||||||
|
же inode → готово; другой файл → коллизия → review). Инварианты и undo —
|
||||||
|
в [architecture.md](architecture.md) → «Раскладка файлов».
|
||||||
|
|
||||||
|
Требование: целевой и исходный каталоги — на одной ФС (внутри контейнера
|
||||||
|
jellybit это обеспечивает mount `/srv`).
|
||||||
|
|
||||||
## Крайние случаи
|
## Крайние случаи
|
||||||
|
|
||||||
- **Многофайловый фильм** (части) — `... part1`, `... part2` в одной
|
- **Многофайловый фильм** (части) — стэкинг по точному токену Jellyfin
|
||||||
папке фильма.
|
(`… - part1`/`cd1`); точный формат уточнить при реализации.
|
||||||
- **Сезон-пак** — все серии в один `Season xx`.
|
- **Редакции** — `Имя (Год) [edition-Director's Cut]` либо отдельные
|
||||||
|
версии в папке фильма.
|
||||||
|
- **Двойная серия** в одном файле — `… SxxEyy-Eyy`.
|
||||||
|
- **Спецвыпуски** — `Season 00`.
|
||||||
|
- **Сезон-пак** — серии в один `Season xx`; смешанный пак — по per-file
|
||||||
|
сезонам.
|
||||||
- **Несколько аудиодорожек** — обычно внутри mkv, не наша забота.
|
- **Несколько аудиодорожек** — обычно внутри mkv, не наша забота.
|
||||||
- **Аниме с абсолютной нумерацией** — требует пересчёта в S·E, отдельная
|
- **Аниме с абсолютной нумерацией** — пересчёт в S·E, отдельная проработка
|
||||||
проработка ([drafts/ideas.md](../drafts/ideas.md)).
|
([drafts/ideas.md](../drafts/ideas.md)).
|
||||||
|
|||||||
+75
-53
@@ -2,91 +2,113 @@
|
|||||||
|
|
||||||
## Задача
|
## Задача
|
||||||
|
|
||||||
По доступным сигналам определить: это фильм или сериал; каноническое
|
По доступным сигналам определить: фильм или сериал; каноническое название
|
||||||
название и год; для сериала — сезон и соответствие файлов сериям; при
|
и год; для сериала — сезон(ы) и соответствие файлов сериям; при включённых
|
||||||
включённых базах — provider-id. На выходе — план раскладки и оценка
|
базах — провайдер и его id. На выходе — план раскладки, оценка уверенности
|
||||||
уверенности.
|
и решение «авто или review».
|
||||||
|
|
||||||
## Сигналы
|
## Сигналы
|
||||||
|
|
||||||
- Имя торрента и структура каталогов.
|
- Имя торрента и структура каталогов.
|
||||||
- Список файлов с размерами и расширениями.
|
- Список файлов с размерами и расширениями. Абсолютный путь источника
|
||||||
- Текстовый контекст от человека.
|
восстанавливаем как `save_path`/`content_path` из qBit (после трансляции
|
||||||
- Распарсенное сообщение торрент-бота (если пришло через Telegram):
|
`path_map`) + относительное имя файла; учитываем одно- и многофайловые
|
||||||
название с годом, качество, переводы, magnet — см. пример в
|
торренты.
|
||||||
[BRIEF.md](../../BRIEF.md).
|
- Текстовый контекст человека (+ накопленные подсказки из review).
|
||||||
|
- Распарсенное сообщение торрент-бота (если через Telegram): название с
|
||||||
|
годом, качество, переводы — см. пример в [BRIEF.md](../../BRIEF.md).
|
||||||
|
|
||||||
|
**Все сигналы недоверенные** — имя торрента, сообщение бота и контекст
|
||||||
|
управляются извне и могут содержать инъекции. Выход LLM не отвечает за
|
||||||
|
безопасность: целевой путь всё равно санитизируется и проверяется на
|
||||||
|
выход за пределы библиотеки (см. architecture.md → «Раскладка файлов»).
|
||||||
|
|
||||||
## Конвейер
|
## Конвейер
|
||||||
|
|
||||||
1. **Пред-парс** имени релиза дешёвым парсером (`go-ptn`): черновые
|
1. **Пред-парс** имени релиза (`go-ptn`): черновые название/год/сезон/
|
||||||
название/год/сезон/серия и качество. Грубо, но бесплатно.
|
серия и качество. Грубо, но бесплатно.
|
||||||
2. **LLM** (через провайдер-абстракцию, см. «Провайдер LLM»): получает
|
2. **LLM** (через провайдер-абстракцию, см. ниже): получает сигналы и
|
||||||
все сигналы и пред-парс, возвращает структурированный план в нашей
|
пред-парс, возвращает структурированный план в нашей схеме. Хорошо
|
||||||
схеме. Хорошо справляется с русскими релиз-именами, чего не умеет
|
берёт русские релиз-имена. Длинный список файлов усекаем/семплируем под
|
||||||
парсер.
|
контекст модели.
|
||||||
3. **Сверка с базой** (опц., если включена TMDB/TVDB): подтверждаем
|
3. **Сверка с базой** (если включена TMDB/TVDB): ищем по названию+году,
|
||||||
название+год, берём официальный id и каноническое имя.
|
берём официальный id и каноническое имя, собираем кандидатов.
|
||||||
4. **Оценка уверенности** и решение: авто-раскладка или ревью.
|
4. **Оценка уверенности** и решение: авто или review.
|
||||||
|
|
||||||
## Структура ответа LLM (черновик)
|
## Структура ответа LLM (предварительная)
|
||||||
|
|
||||||
```
|
```
|
||||||
type movie | series
|
type movie | series
|
||||||
title каноническое название
|
title каноническое название
|
||||||
original_title оригинальное название (если есть)
|
original_title оригинальное название (если есть)
|
||||||
year год
|
year год
|
||||||
season номер сезона (для сериала)
|
provider_hint строка для поиска в базе (НЕ итоговый id)
|
||||||
provider_hint подсказка для поиска в базе
|
files[] { src, role: main|episode|subtitle|extra|sample|ignore,
|
||||||
files[] { src, role: main|episode|subtitle|extra|sample,
|
season?, episode? } # season/episode — на файл
|
||||||
season?, episode? }
|
confidence 0..1 — самооценка модели (вспомогательный сигнал)
|
||||||
confidence 0..1 — самооценка модели по полям
|
|
||||||
notes пояснения, неоднозначности
|
notes пояснения, неоднозначности
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Сезон/серия — **на файле**: так выражаются мультисезонные паки,
|
||||||
|
спецвыпуски и смешанные раскладки; отдельного скалярного `season` нет.
|
||||||
|
`provider_hint` — только подсказка для поиска; итоговые `provider`
|
||||||
|
(`tmdb|tvdb|none`) и `provider_id` появляются после сверки с базой и
|
||||||
|
хранятся отдельно.
|
||||||
|
|
||||||
## Провайдер LLM
|
## Провайдер LLM
|
||||||
|
|
||||||
Доступ к LLM — за интерфейсом; конкретная реализация выбирается полем
|
Доступ к LLM — за интерфейсом; реализация выбирается полем `[llm].type`
|
||||||
`[llm].type` в конфиге (дискриминатор). Это позволяет подключать
|
(дискриминатор). Это позволяет подключать локальные модели и сторонние
|
||||||
локальные модели и сторонние (в т.ч. китайские) эндпоинты — ради экономии
|
(в т.ч. китайские) эндпоинты — ради экономии и независимости от вендора.
|
||||||
и независимости от одного вендора.
|
|
||||||
|
|
||||||
- Первый и пока единственный тип — **`openai-compat`**: OpenAI-совместимый
|
- Первый и пока единственный тип — **`openai-compat`**: OpenAI-совместимый
|
||||||
Chat Completions API (`base_url` + `api_key` + `model`). Под него
|
Chat Completions API (`base_url` + `api_key` + `model`). Подходят
|
||||||
подходят локальные серверы (LM Studio, llama.cpp, Ollama) и облачные
|
локальные серверы (LM Studio, llama.cpp, Ollama) и облачные совместимые
|
||||||
совместимые провайдеры (DeepSeek, Qwen и др.).
|
провайдеры (DeepSeek, Qwen и др.).
|
||||||
- Структурированный вывод: запрашиваем JSON по нашей схеме
|
- **Структурированный вывод надёжно:** просим JSON по схеме
|
||||||
(`response_format` со схемой там, где поддерживается; иначе json-режим
|
(`response_format` со схемой где поддерживается; иначе json-режим или
|
||||||
или tool-call), **валидируем в Go** и ретраим при несоответствии —
|
tool-call); на приёме срезаем ```-ограждения и извлекаем JSON,
|
||||||
серверы различаются по поддержке строгих схем.
|
**валидируем в Go**, ретраим со схемой-в-промпте до `llm.max_retries`;
|
||||||
|
если так и не распарсилось — уходим в **review** (не в `failed`) с
|
||||||
|
причиной «ответ LLM не разобран». Серверы заметно различаются по
|
||||||
|
поддержке строгих схем, особенно мелкие локальные модели.
|
||||||
- Новые типы (напр. нативный `anthropic`) добавляются, не трогая
|
- Новые типы (напр. нативный `anthropic`) добавляются, не трогая
|
||||||
`recognize`.
|
`recognize`.
|
||||||
|
|
||||||
## Модель уверенности
|
## Модель уверенности
|
||||||
|
|
||||||
Авто-раскладка только если выполнено всё:
|
Авто-раскладка — только если выполнено **всё**:
|
||||||
|
|
||||||
1. **Самооценка LLM** ≥ порога (`recognition.auto_confidence_threshold`).
|
1. **Подтверждённый матч в базе** — единственный сильный результат
|
||||||
2. **Совпадение с базой** (если включена) — единственный сильный матч по
|
TMDB/TVDB по названию+году, давший `provider_id`. **Нет матча (или
|
||||||
названию+году.
|
база выключена) → всегда review.** Это и закрывает основной кейс
|
||||||
3. **Структурная валидация** проходит без предупреждений:
|
(рус/аниме часто отсутствуют в базах), и снимает риск «LLM придумал».
|
||||||
- фильм: ровно один основной видеофайл (семплы/экстра отброшены);
|
2. **Структурная валидация** без предупреждений:
|
||||||
- сериал: число серий бьётся с базой (если есть), нумерация S·E
|
- фильм: ровно один основной видеофайл (семплы/экстра/ignore отброшены);
|
||||||
консистентна, без пропусков и дублей.
|
- сериал: число серий бьётся с базой, нумерация S·E консистентна, без
|
||||||
|
пропусков, дублей и неоднозначных спецвыпусков.
|
||||||
|
3. **Согласованность сигналов** — пред-парс (`go-ptn`) и LLM не
|
||||||
|
противоречат по типу/названию/году.
|
||||||
|
|
||||||
Иначе план уходит в **review** (сценарии — [review-ux.md](review-ux.md)).
|
Самооценку LLM (`confidence`) учитываем как вспомогательный сигнал, но
|
||||||
На экране подтверждения всегда видно, *почему* не авто — это страховка на
|
**не как единственный гейт**: она плохо откалибрована и поддаётся
|
||||||
дорогих файлах.
|
инъекции. Решают матч в базе и валидация.
|
||||||
|
|
||||||
|
Иначе — **review** ([review-ux.md](review-ux.md)) с явной причиной.
|
||||||
|
|
||||||
## Что делаем с краёв
|
## Что делаем с краёв
|
||||||
|
|
||||||
- Семплы и «экстра» отбрасываем (эвристики по размеру/имени + LLM).
|
- Семплы/«экстра»/мусор → роль `ignore` (эвристики размер/имя + LLM).
|
||||||
- Внешние субтитры (`.srt`, `.ass`) привязываем к видео и именуем по
|
- Внешние субтитры (`.srt`, `.ass`, пары VobSub `.idx`+`.sub`) привязываем
|
||||||
Jellyfin (`*.ru.srt`).
|
к видео и именуем по Jellyfin (`*.ru.srt`).
|
||||||
- Сезон-паки разбираем по сериям; аниме с абсолютной нумерацией —
|
- Сезон-паки разбираем по сериям; смешанные паки, спецвыпуски (`Season
|
||||||
отдельный крайний случай, см. [drafts/ideas.md](../drafts/ideas.md).
|
00`), двойные серии (`SxxEyy-Eyy`) — через per-file season/episode;
|
||||||
|
любая неоднозначность → review.
|
||||||
|
- Аниме с абсолютной нумерацией — отдельный крайний случай, см.
|
||||||
|
[drafts/ideas.md](../drafts/ideas.md).
|
||||||
|
|
||||||
## На будущее
|
## На будущее
|
||||||
|
|
||||||
`go-ptn` слабее питоновского `guessit`. Если точности пред-парса не
|
`go-ptn` слабее питоновского `guessit`. Если точности пред-парса не
|
||||||
хватит — завернуть `guessit` лёгким сервисом-спутником (один файл рядом
|
хватит — завернуть `guessit` лёгким сервисом-спутником (один файл рядом с
|
||||||
с бинарём). См. [drafts/ideas.md](../drafts/ideas.md).
|
бинарём). См. [drafts/ideas.md](../drafts/ideas.md).
|
||||||
|
|||||||
+12
-2
@@ -89,8 +89,14 @@ Fargo.S02.2015.WEB-DL.1080p.rus.eng 🟡 review
|
|||||||
## Разделение труда
|
## Разделение труда
|
||||||
|
|
||||||
Telegram = одобрить / подсказать / выбрать кандидата / эскалировать в
|
Telegram = одобрить / подсказать / выбрать кандидата / эскалировать в
|
||||||
веб. Веб = точные правки. Состояние ревью одно (в SQLite) — действовать
|
веб. Веб = точные правки. Состояние ревью одно (в SQLite); команды из
|
||||||
можно из любого транспорта, последнее слово побеждает.
|
любого транспорта сериализует `worker` под per-download блокировкой —
|
||||||
|
гонки двух транспортов нет, применяется последняя валидная команда.
|
||||||
|
|
||||||
|
**Доступ.** Telegram — по `telegram.allowed_user_ids` (пусто = запрет
|
||||||
|
всем). Веб-UI в v1 без авторизации (доверенная LAN), поэтому deep-link из
|
||||||
|
бота ведёт на открытую страницу — приемлемо по решению; защиту навесим
|
||||||
|
позже.
|
||||||
|
|
||||||
## Крайние сценарии
|
## Крайние сценарии
|
||||||
|
|
||||||
@@ -113,6 +119,10 @@ Telegram = одобрить / подсказать / выбрать кандид
|
|||||||
- После «Применить» показываем, что создано. **Undo** — убрать созданные
|
- После «Применить» показываем, что создано. **Undo** — убрать созданные
|
||||||
хардлинки одной кнопкой (источник цел); страховка от ошибочного
|
хардлинки одной кнопкой (источник цел); страховка от ошибочного
|
||||||
подтверждения.
|
подтверждения.
|
||||||
|
- **«Позже»** паркует загрузку в `deferred` (вернётся в review по
|
||||||
|
действию), **«Отклонить»** → `cancelled` (раскладку не делаем), **undo**
|
||||||
|
после применения → `reverted` (удаляет только ссылки своего батча, под
|
||||||
|
`media`). Полная карта состояний — в [architecture.md](architecture.md).
|
||||||
|
|
||||||
## Объём по версиям
|
## Объём по версиям
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user