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:
@@ -13,6 +13,7 @@ package worker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
@@ -25,6 +26,7 @@ import (
|
||||
"git.vakhrushev.me/av/jellybit/internal/layout"
|
||||
"git.vakhrushev.me/av/jellybit/internal/logctx"
|
||||
"git.vakhrushev.me/av/jellybit/internal/magnet"
|
||||
"git.vakhrushev.me/av/jellybit/internal/naming"
|
||||
"git.vakhrushev.me/av/jellybit/internal/qbt"
|
||||
"git.vakhrushev.me/av/jellybit/internal/recognize"
|
||||
"git.vakhrushev.me/av/jellybit/internal/store"
|
||||
@@ -52,6 +54,7 @@ type Store interface {
|
||||
// SetDisplayName обновляет отображаемое имя постфактум (перелив канонического
|
||||
// имени после распознавания) — без гарда состояния, FSM не двигает.
|
||||
SetDisplayName(ctx context.Context, id, name string) error
|
||||
SetParsedContext(ctx context.Context, id, jsonStr string) error
|
||||
SetSourceMissCount(ctx context.Context, id string, n int) error
|
||||
SetSourceAddedAt(ctx context.Context, id string, t time.Time) error
|
||||
// SetRetriedAt проставляет время ручного retry — сброс базиса отсчёта
|
||||
@@ -104,12 +107,18 @@ type QBittorrent interface {
|
||||
// Recognizer — распознаватель (recognize.Recognizer).
|
||||
type Recognizer interface {
|
||||
Recognize(ctx context.Context, in recognize.Input) (recognize.Result, error)
|
||||
// Director тянет режиссёра выбранного источника по (provider, id) из
|
||||
// метабазы (credits) — для ручного выбора кандидата в ревью. Best-effort:
|
||||
// нет провайдера/не умеет/ошибка → пустая строка.
|
||||
Director(ctx context.Context, mt recognize.MediaType, provider, providerID string) string
|
||||
}
|
||||
|
||||
// Namer выводит человекочитаемое отображаемое имя из контекста (naming.Namer).
|
||||
// Пустой результат → rename в qBittorrent не задаём. nil → имя не выводим.
|
||||
// Derive возвращает имя (пустое → rename в qBittorrent не задаём) и извлечённую
|
||||
// структуру (её JSON сохраняется как parsed_context — базовый слой полей имени).
|
||||
// nil-namer → имя не выводим.
|
||||
type Namer interface {
|
||||
DeriveName(ctx context.Context, contextText, hint string) string
|
||||
Derive(ctx context.Context, contextText, hint string) (name string, fields naming.Fields, ok bool)
|
||||
}
|
||||
|
||||
// Layouter — раскладчик хардлинками (layout.Layouter).
|
||||
@@ -452,7 +461,19 @@ func (w *Worker) processCatched(ctx context.Context) {
|
||||
}
|
||||
var rename string
|
||||
if w.namer != nil {
|
||||
rename = w.namer.DeriveName(cctx, cur.Context, hint)
|
||||
name, fields, extracted := w.namer.Derive(cctx, cur.Context, hint)
|
||||
rename = name
|
||||
// Сохраняем извлечённую структуру как базовый слой полей имени
|
||||
// (parsed_context) — режиссёр/год из контекста не теряются и
|
||||
// переиспользуются при обновлении display_name. Best-effort: сбой не
|
||||
// валит добавление (косметика). Пустая структура → нечего сохранять.
|
||||
if extracted {
|
||||
if blob, mErr := json.Marshal(fields); mErr == nil {
|
||||
if sErr := w.store.SetParsedContext(cctx, cur.ID, string(blob)); sErr != nil {
|
||||
logctx.From(cctx).Warn("catched save parsed context failed", "error", sErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
addReq.Rename = rename
|
||||
addErr := w.qbt.Add(cctx, addReq)
|
||||
|
||||
Reference in New Issue
Block a user