# ingest Specification ## ADDED Requirements ### Requirement: Сохранение извлечённой из контекста структуры имени Система SHALL сохранять структуру имени, извлечённую LLM со структурированным выводом на шаге добавления (тип, название, оригинальное название, год, режиссёр, сезон), у загрузки (`download.parsed_context`, JSON), чтобы её поля могли переиспользоваться при последующем выводе имени без повторного вызова LLM. Алгоритмический фолбек структуры не даёт (его выход — только строка имени), тогда `parsed_context` остаётся пустым — это штатно (нижний слой отсутствует). Сохранённая структура SHALL быть **базовым (наименее доверенным) слоем** источника полей имени: её значения берутся, только если более доверенный слой (распознавание/матч, ручные правки) соответствующего поля не дал. Сохранение SHALL быть best-effort и косметическим: неудача записи `parsed_context` SHALL NOT проваливать добавление загрузки, а сама структура SHALL влиять только на отображаемое имя и SHALL NOT влиять на пути файлов, распознавание или раскладку. Пустая/невыведенная структура (нет контекста и подсказки) SHALL приводить к пустому `parsed_context` (нечего сохранять). #### Scenario: Извлечённый режиссёр сохраняется у загрузки - **GIVEN** контекст загрузки, из которого LLM извлёк режиссёра и год - **WHEN** система выводит имя на шаге добавления - **THEN** извлечённая структура (в т.ч. режиссёр) сохраняется в `download.parsed_context` - **AND** отображаемое имя формируется как и прежде (полный ярлык) #### Scenario: Сбой сохранения структуры не валит добавление - **GIVEN** запись `parsed_context` завершается ошибкой - **WHEN** идёт шаг добавления загрузки - **THEN** загрузка всё равно добавляется в qBittorrent (с `rename`, если имя выведено) - **AND** ошибка логируется, приём/добавление не проваливается #### Scenario: Пустой вход — пустая структура - **GIVEN** пойманная загрузка без контекста и без подсказки из полей источника - **WHEN** выполняется шаг добавления - **THEN** структура не выводится, `download.parsed_context` пуст ## MODIFIED Requirements ### Requirement: Обновление отображаемого имени по распознаванию Система SHALL уметь обновлять отображаемое имя загрузки после того, как распознавание дало каноническое название, — переливая уже вычисленное имя (без нового вызова LLM) в `download.display_name` и в имя раздачи qBittorrent. Источником имени SHALL быть **эффективные поля имени**, разрешённые по слоям сверху вниз (берётся первый непустой слой): (1) ручные правки `override`; (2) распознавание с вложенным подтверждённым матчем (`recognition`, куда матч метабазы уже вложил каноничные название/год/режиссёра); (3) сохранённая структура из контекста (`download.parsed_context`). Так каждое поле берётся из самого доверенного доступного источника, а данные из контекста (например режиссёр) не теряются, если распознавание/матч их не дали. Название из слоя распознавания SHALL совпадать с тем, что использует раскладка (эффективный `title`), чтобы отображаемое имя не расходилось с целевыми путями. Формат SHALL быть тем же полным детерминированным ярлыком, что и на шаге добавления: «Название (режиссёр, год)», где режиссёр и год опциональны, а для сериала добавляется сводка сезонов («. Сезон N» для одного сезона; «. Сезоны …» для многосезонного пака; отметка спецвыпусков) — согласованная со сводкой сезонов на экране просмотра. Применяются та же очистка от управляющих символов и обрезка по ограничению длины. Пустой источник (нет ни распознавания, ни сохранённой структуры, дающих непустое название) SHALL приводить к отсутствию изменений (no-op). Переименование раздачи в qBittorrent SHALL адресоваться по infohash своей раздачи и SHALL быть best-effort: сбой (раздача удалена, qBittorrent недоступен) SHALL NOT проваливать обновление — `download.display_name` обновляется в любом случае, ошибка внешнего вызова логируется. Как и на шаге добавления, отображаемое имя SHALL влиять только на отображение и SHALL NOT влиять на пути файлов, распознавание или раскладку. Обновление имени SHALL иметь две точки входа: **авто** — при подтверждённом матче (см. capability `review`); **ручную** — по явному действию пользователя. Ручное действие SHALL перезаписывать текущее имя всегда; авто SHALL перезаписывать, когда выведенное имя непусто. #### Scenario: Перелив имени в загрузку и раздачу - **GIVEN** загрузка с распознанным непустым каноническим названием и известным режиссёром (из матча или из сохранённого контекста) - **WHEN** запускается обновление отображаемого имени - **THEN** `download.display_name` устанавливается в полный ярлык «Название (режиссёр, год)» (для сериала — со сводкой сезонов) - **AND** раздача в qBittorrent переименовывается в то же имя (по infohash своей раздачи) #### Scenario: Режиссёр из контекста переживает распознавание без матча - **GIVEN** загрузка, где режиссёр был извлечён из контекста, а распознавание прошло без подтверждённого матча (режиссёр из метабазы недоступен) - **WHEN** запускается обновление отображаемого имени - **THEN** в ярлыке используется режиссёр из сохранённого контекста - **AND** название/год берутся из распознавания #### Scenario: Режиссёр из матча бьёт контекстного - **GIVEN** загрузка, где режиссёр есть и в контексте, и в подтверждённом матче - **WHEN** формируется ярлык - **THEN** используется режиссёр из матча (более доверенный слой) #### Scenario: qBittorrent недоступен — имя у загрузки всё равно обновлено - **GIVEN** обновление отображаемого имени с выведенным непустым именем - **WHEN** переименование раздачи в qBittorrent завершается ошибкой (недоступен или раздача удалена) - **THEN** `download.display_name` всё равно обновлён - **AND** ошибка внешнего вызова qBittorrent логируется, операция не проваливается #### Scenario: Нет источника имени — обновление ничего не делает - **GIVEN** загрузка без распознанного названия и без сохранённой структуры (пустой источник имени) - **WHEN** запускается обновление отображаемого имени - **THEN** ни `download.display_name`, ни имя раздачи не меняются (no-op)