display_name: слоистое разрешение полей + сохранение режиссёра из контекста
Единый источник полей отображаемого имени и один рендер полного ярлыка на всех путях (старт и «Обновить имя»/авто-перелив). Раньше старт давал полный «Название (режиссёр, год). Сезон N» но выбрасывал структуру, а перелив по распознаванию — усечённый «Title (Year)». - Слоистое разрешение скаляров имени: override → recognition(+match) → новый базовый слой «контекст» (download.parsed_context, JSON naming.Fields). - naming: публичные Fields/Label/Derive, вынесен единый рендер; удалён FormatTitleYear. Сводка сезонов вынесена в recognize.SeasonSummary. - Режиссёр из метабазы (решение A2): TMDB/TVDB credits через опциональный metadata.DirectorProvider; авто-матч кладёт в plan.Director, ручной выбор кандидата тянет credits и пиннит ovrDirector. Метабаза бьёт контекст. - refreshDisplayNameLocked строит полный ярлык из эффективных полей; инфо-панель ревью показывает загруженного режиссёра. - Миграция 0011_parsed_context + ER-схема. Всё косметика: на пути/раскладку не влияет, приём/вывод имени не валятся (best-effort). Закрывает беклог-задачу «Кнопка „Обновить имя“: полный формат ярлыка». OpenSpec: archive/2026-07-11-field-resolution-display-name (ingest, recognition, metadata-match, review). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,10 +40,10 @@ const systemPrompt = `Ты извлекаешь из текста о торре
|
||||
// Транспортные ошибки провайдера (сеть/429/5xx) гасятся внутри llm.Provider;
|
||||
// здесь повторяем только переразбор невалидного ответа. ok=false, если за
|
||||
// все попытки валидного результата не получено.
|
||||
func (n *Namer) extractViaLLM(ctx context.Context, contextText, hint string) (extracted, bool) {
|
||||
func (n *Namer) extractViaLLM(ctx context.Context, contextText, hint string) (Fields, bool) {
|
||||
contextText = strings.TrimSpace(contextText)
|
||||
if contextText == "" {
|
||||
return extracted{}, false
|
||||
return Fields{}, false
|
||||
}
|
||||
|
||||
user := "Контекст раздачи:\n" + contextText
|
||||
@@ -68,7 +68,7 @@ func (n *Namer) extractViaLLM(ctx context.Context, contextText, hint string) (ex
|
||||
// Транспортная ошибка/таймаут залогирована клиентом LLM (ext.*);
|
||||
// здесь — доменное решение «уходим в фолбек, приём не валим».
|
||||
log.Debug("naming llm failed, using fallback")
|
||||
return extracted{}, false
|
||||
return Fields{}, false
|
||||
}
|
||||
|
||||
ex, perr := parseExtracted(resp.Content)
|
||||
@@ -81,22 +81,22 @@ func (n *Namer) extractViaLLM(ctx context.Context, contextText, hint string) (ex
|
||||
llm.Message{Role: llm.RoleUser, Content: "Ответ невалиден: " + perr.Error() +
|
||||
". Верни строго один JSON-объект по схеме, без пояснений."})
|
||||
}
|
||||
return extracted{}, false
|
||||
return Fields{}, false
|
||||
}
|
||||
|
||||
// parseExtracted вытаскивает JSON-объект из ответа и валидирует минимум:
|
||||
// непустой title. Тип по умолчанию — movie.
|
||||
func parseExtracted(raw string) (extracted, error) {
|
||||
func parseExtracted(raw string) (Fields, error) {
|
||||
jsonStr, err := llm.ExtractJSONObject(raw)
|
||||
if err != nil {
|
||||
return extracted{}, err
|
||||
return Fields{}, err
|
||||
}
|
||||
var ex extracted
|
||||
var ex Fields
|
||||
if err := json.Unmarshal([]byte(jsonStr), &ex); err != nil {
|
||||
return extracted{}, fmt.Errorf("naming: unmarshal: %w", err)
|
||||
return Fields{}, fmt.Errorf("naming: unmarshal: %w", err)
|
||||
}
|
||||
if strings.TrimSpace(ex.Title) == "" {
|
||||
return extracted{}, fmt.Errorf("naming: empty title")
|
||||
return Fields{}, fmt.Errorf("naming: empty title")
|
||||
}
|
||||
if ex.Type != typeMovie && ex.Type != typeSeries {
|
||||
ex.Type = typeMovie
|
||||
|
||||
Reference in New Issue
Block a user