Добавил документацию и описание репозитория
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