Добавил документацию и описание репозитория
This commit is contained in:
@@ -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`).
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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) | — |
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Краткий заголовок решения
|
||||||
|
|
||||||
|
- Дата: ГГГГ-ММ-ДД
|
||||||
|
<!-- Строку статуса добавляют позже, только если запись потеряла силу:
|
||||||
|
- Статус: заменено на ADR-ГГГГ-ММ-ДД-slug
|
||||||
|
- Статус: устарело
|
||||||
|
У активной записи строки статуса нет. -->
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
|
||||||
|
Что вынудило принять решение: проблема, силы и ограничения (ресурсы,
|
||||||
|
стоимость, время на поддержку, существующая архитектура). Пиши так, чтобы
|
||||||
|
через год было понятно «почему это вообще делалось» без чтения переписки.
|
||||||
|
|
||||||
|
## Рассмотренные варианты
|
||||||
|
|
||||||
|
<!-- Опциональная секция. Оставь, только если варианты реально
|
||||||
|
рассматривались. Если решение было единственным очевидным — удали
|
||||||
|
её, а причину объясни в «Решении». -->
|
||||||
|
|
||||||
|
- **Вариант A** — суть, плюсы и минусы.
|
||||||
|
- **Вариант B** — суть, плюсы и минусы.
|
||||||
|
- **Вариант C** — если отвергнут сразу, коротко почему.
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
|
||||||
|
Что именно сделано и — главное — **почему**: какое намерение и какая
|
||||||
|
причина за этим стоят. Если варианты рассматривались — почему выбран
|
||||||
|
этот, а не остальные.
|
||||||
|
|
||||||
|
## Последствия
|
||||||
|
|
||||||
|
- `+` что стало лучше, какие возможности открылись.
|
||||||
|
- `-` чем платим: новые ограничения, риски, регулярная нагрузка на
|
||||||
|
поддержку.
|
||||||
|
- Что нужно сделать как следствие (если есть).
|
||||||
@@ -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 ошибся, а файлы уже скачаны.
|
||||||
@@ -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 поднят.
|
||||||
@@ -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