- Список: серверные фильтр по группе состояний, поиск и пагинация (GET
f/q/page/all, по 25), сортировка по времени добавления в qBittorrent
(added_on) с фолбеком на created_at и tie-break по id.
- Заголовок загрузки = имя раздачи (display_name) → распознанное название →
усечённый источник; сырой magnet вынесен в блок «Информация о торренте».
- Матч метабазы показан ссылкой на запись (страница загрузки и ревью);
URL берётся у выбранного кандидата либо строится по provider+id и типу.
- Полировка вёрстки; клиентская JS-фильтрация убрана (всё серверное, без JS).
- Миграция 0005 (display_name, source_added_at); воркер однократно
фиксирует source_added_at при поллинге/усыновлении; ER-схема обновлена.
- OpenSpec: дельты влиты в specs/{web-ui,ingest}, change заархивирован.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
110 lines
7.4 KiB
Markdown
110 lines
7.4 KiB
Markdown
# ingest Specification
|
||
|
||
## Purpose
|
||
|
||
Приём загрузки: использование контекста для отображаемого имени торрента в
|
||
qBittorrent. Capability описывает вывод человекочитаемого имени из контекста
|
||
(через LLM или алгоритмический фолбек) и его передачу в qBittorrent.
|
||
|
||
## Requirements
|
||
|
||
### Requirement: Отображаемое имя торрента из контекста
|
||
|
||
При добавлении загрузки в qBittorrent система SHALL выводить из контекста
|
||
загрузки человекочитаемое отображаемое имя и передавать его в qBittorrent
|
||
(параметр `rename` API `/torrents/add`), чтобы задача в списке qBit не
|
||
показывалась безликим `dn` magnet-ссылки. Это же имя система SHALL **сохранять
|
||
у загрузки** (`download.display_name`) для последующего показа заголовком в
|
||
веб-UI.
|
||
|
||
Имя SHALL быть коротким читаемым ярлыком (название, опционально режиссёр и
|
||
год; для сериала — номер сезона, если он определён), а не куском сырого
|
||
контекста. Имя SHALL очищаться от управляющих символов и переводов строк и
|
||
SHALL обрезаться по ограничению длины.
|
||
|
||
Вывод имени SHALL выполняться синхронно перед отдачей источника в
|
||
qBittorrent (параметр `rename` действует только в момент добавления).
|
||
|
||
Отображаемое имя SHALL влиять только на отображение (в qBittorrent и как
|
||
заголовок в веб-UI) и SHALL NOT влиять на пути файлов на диске, распознавание
|
||
или раскладку — реальные пути система по-прежнему читает из qBit API.
|
||
|
||
#### Scenario: Имя из контекста передаётся в qBittorrent
|
||
|
||
- **WHEN** загрузку добавляют с непустым контекстом, из которого удалось
|
||
получить имя
|
||
- **THEN** система передаёт это имя в qBittorrent в параметре `rename`
|
||
- **AND** имя — короткий читаемый ярлык вида «название (режиссёр, год)»,
|
||
где режиссёр и год опциональны
|
||
|
||
#### Scenario: Имя сохраняется у загрузки
|
||
|
||
- **WHEN** при приёме получено непустое отображаемое имя
|
||
- **THEN** система сохраняет его в `download.display_name` вместе с созданием
|
||
загрузки
|
||
- **AND** веб-UI использует его заголовком карточки и страницы загрузки
|
||
|
||
#### Scenario: Контекст пуст или имя не получено
|
||
|
||
- **WHEN** контекста нет либо ни один способ вывода не дал непустого имени
|
||
- **THEN** система добавляет загрузку без параметра `rename`
|
||
- **AND** qBittorrent оставляет собственное имя (из `dn`/торрента)
|
||
- **AND** `download.display_name` остаётся пустым, а веб-UI берёт заголовок из
|
||
фолбека (распознанное название или усечённый источник)
|
||
|
||
### Requirement: Вывод имени через LLM со структурированным выводом
|
||
|
||
Система SHALL строить отображаемое имя с помощью LLM (структурированный
|
||
JSON-вывод), извлекая из контекста тип (movie/series), название, год,
|
||
режиссёра и (для сериала) номер сезона. Год и режиссёр — опциональные поля.
|
||
|
||
Название SHALL быть на русском языке для российского контента и на
|
||
английском (оригинальном) — для остального.
|
||
|
||
Система SHALL предпринять ограниченное число попыток получить от LLM валидный
|
||
результат (корректный JSON с непустым названием); бюджет попыток —
|
||
`[llm].max_retries` (по умолчанию 3). Транспортные ретраи провайдера LLM
|
||
(сетевые сбои, 429, 5xx) в этот счёт не входят.
|
||
|
||
Недоступность или ошибка LLM SHALL NOT прерывать приём загрузки: система
|
||
переходит к алгоритмическому фолбеку.
|
||
|
||
#### Scenario: LLM возвращает структурированное имя
|
||
|
||
- **WHEN** LLM по контексту возвращает валидный JSON с непустым названием
|
||
- **THEN** система формирует отображаемое имя из его полей (название, год,
|
||
для сериала — сезон)
|
||
|
||
#### Scenario: Российский контент — название на русском
|
||
|
||
- **WHEN** контент распознан как российский
|
||
- **THEN** в отображаемом имени используется русское название
|
||
|
||
#### Scenario: Исчерпан бюджет попыток LLM
|
||
|
||
- **WHEN** LLM за отведённые попытки (`[llm].max_retries`) не вернул валидный
|
||
результат либо недоступен
|
||
- **THEN** система не прерывает приём и переходит к алгоритмическому фолбеку
|
||
|
||
### Requirement: Алгоритмический фолбек вывода имени без сети
|
||
|
||
При неудаче LLM система SHALL выводить имя алгоритмически, без сетевых
|
||
запросов: брать первую содержательную строку контекста (без ссылок, команд
|
||
бота и UI-мусора), отсекать технические характеристики, очищать и обрезать
|
||
по длине.
|
||
|
||
Если и фолбек не дал непустого имени, система SHALL добавить загрузку без
|
||
параметра `rename`.
|
||
|
||
#### Scenario: Фолбек извлекает имя из контекста
|
||
|
||
- **WHEN** LLM недоступен или исчерпал попытки, а контекст содержательный
|
||
- **THEN** система берёт первую содержательную строку контекста, отсекает
|
||
технические характеристики и использует результат как отображаемое имя
|
||
- **AND** при этом не делается ни одного сетевого запроса
|
||
|
||
#### Scenario: Фолбек тоже пуст
|
||
|
||
- **WHEN** ни LLM, ни алгоритмический фолбек не дали непустого имени
|
||
- **THEN** система добавляет загрузку без параметра `rename`
|