# TODO Конкретные задачи на будущее, ранжированные по приоритету. Это не план реализации (он — в [drafts/roadmap.md](drafts/roadmap.md)) и не свалка идей ([drafts/ideas.md](drafts/ideas.md)): сюда попадает то, что уже решили сделать, но ещё не сделали. Принятое и реализованное переезжает в `docs/specs`/`docs/adr`. Приоритет — грубая оценка «ценность / стоимость», не обязательство к порядку. ## Высокий ### Проблема второго сезона Если первый сезон сериала уже разложен, а мы добавляем второй/третий/…, распознавание должно привязать новый сезон к **тому же** названию и папке, а не завести рядом почти одинаковую вторую папку. Ключ — стабильный `provider_id`: один и тот же `[tvdbid-…]` → одна папка сериала, новые `Season NN` доливаются внутрь. Нужно: при матче учитывать уже существующие в библиотеке сериалы (или прошлые распознавания с тем же провайдер-id) и склонять LLM/выбор кандидата к согласованности с ними. Связано: [recognition.md](specs/recognition.md) (модель уверенности, матч в базе), [jellyfin-layout.md](specs/jellyfin-layout.md) (папка сериала с провайдер-id). ### Название из контекста при добавлении в qBittorrent При создании magnet-загрузки передавать в qBittorrent человекочитаемое имя из контекста (если оно есть), чтобы в списке qBit не было безликих `rutracker-topic-6852853`. Небольшая задача с заметной отдачей в повседневной эксплуатации. Связано: [architecture.md](specs/architecture.md) → «Транспорты», пакет `ingest`/`qbt`. ### Рассинхрон состояния с реальностью (удалённый торрент / файлы) Состояние jellybit может разойтись с тем, что реально лежит на диске. Несколько сценариев разной остроты: - **Жёсткий — удалён источник.** Раздачу удаляют (вручную или авто по достижении seed limit), и qBittorrent стирает скачанные файлы. Тогда хардлинк в библиотеке становится **последней** ссылкой на inode, и обычный `undo` (`unlink` цели + чистка пустых каталогов) сотрёт единственную копию насовсем — прямая потеря данных. Инвариант «источник неприкосновенен» молчаливо перестаёт держаться: источника уже нет. - **Мягкий — удалена цель.** Файлы убрали из библиотеки Jellyfin (вручную или из самого Jellyfin), а jellybit по-прежнему числит загрузку в `done`. Состояние врёт: ссылок уже нет, а сервис думает, что всё разложено. Нужно продумать сверку записанного состояния (`file_link`, состояние загрузки) с фактом на ФС: - как `worker` реагирует на исчезновение раздачи из qBittorrent (состояние/пометка загрузки); - как `undo` защищается, когда источник недоступен — например, отказываться удалять, если у целевого файла счётчик ссылок == 1 (нет второй копии) или исходный путь не существует, и явно об этом сообщать. Откат снимает **лишний** хардлинк, а не последнюю копию файла; - как ловить пропажу целевых файлов и отражать её в состоянии (напр. периодическая сверка или проверка при показе — «разложено, но файлов нет»), чтобы можно было осознанно перепривязать/переразложить. Связано: [ADR-2026-06-13-hardlinks](adr/ADR-2026-06-13-hardlinks.md), [architecture.md](specs/architecture.md) → «Раскладка файлов» (undo, инвариант источника), [workflow.md](specs/workflow.md) (`done → reverted`). ### Наблюдаемость: метрики и учёт стоимости LLM Сейчас единственное окно в систему — `slog`. Нет быстрых ответов на вопросы «сколько задач висит в review», «сколько токенов и денег съело распознавание», «какова медиана времени ingest → done». Нужны метрики: эндпоинт `/metrics` (Prometheus-формат) со счётчиками загрузок по состояниям, длительностями стадий и расходом LLM (токены/стоимость на задачу). **Расход LLM уже снимается с провода** — `llm.openai` парсит `usage` (prompt/completion/total tokens и `cost`, который отдаёт шлюз) в `llm.Response.Usage` и пишет в лог. Не хватает только **персистентности и отображения**: сохранять `usage` у попытки распознавания (`recognition`) и показывать в карточке загрузки + агрегатом в `/metrics`. Где провайдер не шлёт `cost` — считать из токенов по таблице «модель → цена» в конфиге. Отдельный LLM-прокси (LiteLLM и т.п.) для этого **не нужен** и противоречит принципам «один бинарь» / «минимум компонентов»: подсчёт токенов уже в коде, а роль мульти-модельного шлюза играет используемый OpenAI-совместимый эндпоинт (он и возвращает `cost`); разные модели подключаются сменой `[llm].model` или новым типом провайдера за интерфейсом `llm.Provider`. Связано: [architecture.md](specs/architecture.md) → «Логирование», [recognition.md](specs/recognition.md) (провайдер LLM, `[llm].type`), пакеты `worker`, `llm`, `httpapi`. ### Ретеншн и очистка БД Терминальные задачи (`done`/`cancelled`/`failed`/`reverted`), их попытки `recognition` с сырыми ответами LLM и `metadata_candidate` копятся вечно — со временем БД и список загрузок распухают и становятся нечитаемыми. Нужна авточистка старше N дней (с настройкой в `[storage]` или `[worker]`) и/или ручное удаление. Маленькая задача, но без неё интерфейс деградирует по мере эксплуатации. Связано: [architecture.md](specs/architecture.md) → «Хранилище» (таблицы `download`/`recognition`/`metadata_candidate`/`file_link`), пакет `store`. ### Eval-харнес распознавания (корпус кейсов + метрика точности) Распознавание — ядро продукта, но смена модели или правка промпта сейчас вслепую: регрессий не видно. Нужен корпус размеченных кейсов (русские релизы, аниме, сезон-паки, репаки, спецвыпуски) и прогон распознавания по нему с метрикой точности (тип/название/год/нумерация). Тогда можно сравнивать LLM-провайдеры и версии промпта по числам, а не на ощупь. Прогон — отдельной командой (`jellybit eval` или тестом), на фикстурах, без реального qBittorrent. Связано: [recognition.md](specs/recognition.md) (конвейер, модель уверенности), пакет `recognize`. ## Средний ### Машина состояний на go-библиотеке Сейчас FSM реализована вручную в `worker`. Выбрать подходящую go-библиотеку для описания воркфлоу/машины состояний и перевести переходы на неё — ради декларативности, проверяемости переходов и единого места правды. Кандидаты для оценки: `looplab/fsm`, `qmuntal/stateless` (и аналоги). Граф и переходы уже формализованы — переносим один в один. Связано: [workflow.md](specs/workflow.md) (текущий граф состояний). ### Привязка уведомлений к источнику в ботах (мульти-бот) Уведомления и запросы подтверждения должен получать тот, кто прислал загрузку: автор сообщения о новой раздаче — адресат пингов и ревью по ней. Транспортов-ботов может быть несколько (Telegram, в перспективе Matrix и др.); каждый адресует «своему» отправителю. Веб-интерфейс остаётся **единым для всех** и точкой правды по функциональности (боты — тонкие адаптеры над тем же ядром). Нужно: хранить у загрузки источник/транспорт и идентификатор отправителя, маршрутизировать пинги по нему. Связано: [review-ux.md](specs/review-ux.md) (разделение труда транспортов, веб = точные правки), [architecture.md](specs/architecture.md) → «Транспорты». ### Раздачи с докачиванием (слияние при повторном добавлении) Свежий сериал часто раздают по мере выхода: торрент содержит 5 эпизодов из 10. Позже его перезаливают целиком (или добавляют недостающие серии), и пользователь повторно добавляет тот же торрент. Нужно распознать, что это **та же** раздача/сезон, и повторить раскладку с **слиянием**: доложить недостающие хардлинки, не дублируя уже разложенное и не перезаписывая существующее (инвариант «существующее не трогаем»). Перекликается с «Проблемой второго сезона», но здесь доливаются эпизоды внутри одного сезона, а не новый сезон. Нужно продумать: как опознать повторное добавление (хеш торрента / провайдер-id + сезон), как сверять состав файлов и доливать только новые. Связано: [«Проблема второго сезона»](#проблема-второго-сезона), [jellyfin-layout.md](specs/jellyfin-layout.md) (раскладка, идемпотентность), [workflow.md](specs/workflow.md) (повторный прогон загрузки). ### Улучшения UI клиентов: показывать матч с записью метабазы Во всех транспортах (веб, Telegram) показывать, **с какой именно записью** метабазы (TMDB/TVDB) сматчилась загрузка: название, год, провайдер-id, ссылку. Сейчас результат распознавания непрозрачен — пользователь не видит, к чему привязались, и не может быстро поймать ошибочный матч. Связано: [review-ux.md](specs/review-ux.md), [recognition.md](specs/recognition.md) (матч в базе), [architecture.md](specs/architecture.md) → «Транспорты». ### Добавление торрентов файлом/ссылкой — «единое окно» Поддержать источники помимо magnet: `.torrent`-файл и URL (отдаём их в qBittorrent, без исходящих запросов на пользовательский URL — SSRF исключён). Идеал — одно поле «единого окна»: кидаем туда текст или файл, а сервис сам разбирает, что это (magnet / ссылка / .torrent / сообщение бота), и заводит загрузку. Связано: [architecture.md](specs/architecture.md) → «Транспорты» (`source_type = magnet|torrent|url` уже в схеме), пакет `ingest` (сейчас поддержан только magnet). ### Бэкап SQLite `architecture.md` требует «бекапить data-том», но *как* — не описано. Без понятной стратегии сбой или редеплой стирают всё in-flight состояние. Зафиксировать решение и реализовать: периодический `VACUUM INTO` в `/data/backups` по расписанию (с ротацией) либо потоковая репликация (litestream). Лучше сделать, пока БД маленькая. Связано: [architecture.md](specs/architecture.md) → «Деплой» (data-том, «бекапить-и-не-терять»), пакет `store`. ### Версии/качество одного тайтла (репаки, апгрейд 1080p → 2160p) Фильм уже разложен, позже добавили раздачу лучшего качества — сейчас это просто новая задача, упирающаяся в «коллизию цели → review», без понятия «это та же вещь, заменить версию». Нужно осознанно обработать апгрейд качества: распознать тот же тайтл, предложить замену существующей раскладки либо сосуществование версий (Jellyfin поддерживает несколько версий одного фильма). Близко к «докачиванию», но про качество, а не про эпизоды. Связано: [«Раздачи с докачиванием»](#раздачи-с-докачиванием-слияние-при-повторном-добавлении), [jellyfin-layout.md](specs/jellyfin-layout.md) (never-overwrite, коллизия), [architecture.md](specs/architecture.md) → «Идентификация торрента» (репаки = разные infohash → разные задачи). ### Глубокий healthcheck и статус зависимостей `/healthz` проверяет только сам сервис. Если qBittorrent, LLM или метабаза недоступны — узнаёшь лишь по застрявшим задачам. Нужна readiness-проверка ключевых зависимостей и отражение их состояния в UI (бейдж «qBittorrent недоступен»), чтобы причина простоя была видна сразу. Связано: [architecture.md](specs/architecture.md) → «Деплой» (healthcheck), пакеты `qbt`, `llm`, `metadata`, `httpapi`. ### Обучение на правках человека (few-shot из прошлых ревью) Когда человек поправил матч, тип или нумерацию — сохранять это как пример и подмешивать похожие в будущие промпты. Системно повышает точность на «твоих» трекерах и форматах имён без смены модели. Развитие идеи многоступенчатой верификации, но дешевле: учимся на уже собранных `hint`/`override`. Связано: [recognition.md](specs/recognition.md) (конвейер, промпт), [«Многоступенчатая верификация»](#многоступенчатая-верификация-привязки-тема-для-размышления), [architecture.md](specs/architecture.md) → «Хранилище» (`hint`, `override`). ### Список загрузок: фильтр, поиск, пагинация Прямое следствие роста БД (см. «Ретеншн»): плоский список загрузок со временем становится непригоден. Нужны фильтр по состоянию, поиск по названию и пагинация. Естественно ложится на экран расширенной информации. Связано: [«Расширенная информация о загрузке в web-UI»](#расширенная-информация-о-загрузке-в-web-ui), пакет `httpapi`. ## Низкий ### Многоступенчатая верификация привязки (тема для размышления) Идея: несколько раз извлекать данные из раздачи и контекста разными промптами, искать в метабазах, затем сводить результаты в общий вердикт (голосование/консенсус) — выше точность ценой нескольких вызовов LLM и запросов к базам. Требует проработки: когда включать, как мерджить расхождения, стоимость/латентность. Связано: [recognition.md](specs/recognition.md) (конвейер и модель уверенности). ### Расширенная информация о загрузке в web-UI Экран просмотра деталей одной загрузки: исходный контекст и magnet, лог переходов состояний, распознанные данные и матч в метабазе (см. «показывать матч»), целевые пути и созданные хардлинки. Помогает разбираться, когда что-то пошло не так, без чтения логов сервера. Связано: [review-ux.md](specs/review-ux.md), пакет `httpapi`. ### Выбор из нескольких находок метабазы в Telegram Когда распознавание даёт несколько подходящих кандидатов в метабазе, предлагать их в Telegram списком (кнопки) для ручного выбора, а не молча брать первый/лучший. Веб остаётся точкой точных правок, бот — быстрый выбор из готового короткого списка. Связано: [review-ux.md](specs/review-ux.md) (боты — быстрые действия, веб — точные правки), [recognition.md](specs/recognition.md) (кандидаты матча). ### Проверка свободного места перед copy-fallback Когда хардлинк невозможен (`EXDEV`/`ENOTSUP`/…), `layout` копирует файл, дублируя место на диске. На забитом диске это упрётся в полку посреди раскладки. Перед копированием проверять доступное место и при нехватке внятно уходить в `failed` с понятной причиной, а не падать на полпути. Связано: [architecture.md](specs/architecture.md) → «Раскладка файлов» (фолбэк-копирование), пакет `layout`. ### Кэш метабаз (и опционально LLM) Повторные и ретраящиеся прогоны распознавания бьют TMDB/TVDB/TVMaze одним и тем же запросом. Кэш ответов с TTL экономит лимиты API и ускоряет «Распознать заново»/«Уточнить». При желании — кэш ответов LLM по хешу входа (но он менее полезен, т.к. вход меняется подсказками). Связано: [recognition.md](specs/recognition.md) (сверка с базой), пакеты `metadata`, `llm`. ### Современный Web-UI как PWA Переделать веб-интерфейс в современное PWA-приложение (устанавливаемое, отзывчивое, удобное с телефона). Текущий server-rendered UI функционален, поэтому это улучшение, а не блокер; большой объём работы. Связано: [review-ux.md](specs/review-ux.md) (веб = точные правки), пакет `httpapi`.