recognize: название из метабазы санитизируется перед попаданием в план

- чистка стоит на каждой точке входа значения метабазы в план — сборка матча,
  копия кандидата для ревью, набор закреплённых значений источника и его
  чтение: гарантия, поставленная только на запись, обходится данными,
  сохранёнными прежними версиями
- название, непригодное как имя каталога (пустое или без единой буквы и
  цифры), не подставляется — раздача уходит в review с названной причиной
- гейт подтверждения матча не сдвинут: сравнение с планом идёт по значениям
  провайдера, чистится только копия, уходящая дальше
This commit is contained in:
av
2026-08-10 10:41:16 +03:00
parent bb278e8744
commit 9aecf757e0
26 changed files with 1653 additions and 53 deletions
+28 -3
View File
@@ -812,7 +812,8 @@ func (w *Worker) chooseCandidateLocked(ctx context.Context, id string, d *store.
if w.recognizer != nil {
director = w.recognizer.Director(ctx, candMediaType(rec), cand.Provider, cand.ProviderID)
}
for field, value := range sourcePins(cand.Provider, cand.ProviderID, title, year, director) {
pins := sourcePins(cand.Provider, cand.ProviderID, title, year, director)
for field, value := range pins {
if err := w.store.SetOverride(ctx, id, field, value); err != nil {
return fmt.Errorf("choose candidate: %w", err)
}
@@ -820,8 +821,13 @@ func (w *Worker) chooseCandidateLocked(ctx context.Context, id string, d *store.
if err := w.store.SetCandidateChosen(ctx, rec.ID, cand.ID); err != nil {
return fmt.Errorf("choose candidate: %w", err)
}
// title_pinned=false означает, что название кандидата не годится как имя
// каталога и в плане осталось название распознавания. Без этого атрибута
// вопрос «почему папка названа догадкой, а не как в базе» по логам не
// разбирается: на авто-пути такой отказ несёт причина решения, здесь её нет.
logctx.From(w.scoped(ctx, capReview, id, d.PrimaryInfohash())).Info("review candidate chosen",
"provider", cand.Provider, "provider_id", cand.ProviderID)
"provider", cand.Provider, "provider_id", cand.ProviderID,
"title_pinned", pins[ovrTitle] != "")
// Подтверждённый матч — переливаем каноническое имя в display_name и в ярлык
// раздачи (best-effort, косметика). Сбой обновления имени не должен ронять
// выбор кандидата: логируем и продолжаем.
@@ -911,6 +917,18 @@ func sourcePins(provider, providerID, title string, year int, director string) m
if year > 0 {
yr = strconv.Itoa(year)
}
// Название источника — недоверенное значение метабазы, и чистится оно здесь,
// на единственном общем доме набора пинов: через sourcePins идут и превью
// источника, и его закрепление, поэтому «превью = применение» держится
// конструкцией, а не памятью. Чистка идемпотентна — на кандидате, записанном
// уже с чисткой, она ничего не меняет, а строку из БД, сохранённую прежней
// версией, приводит в порядок. Непригодное как имя каталога название пином не
// становится: пустое значение очищает пин, и в плане остаётся название
// распознавания.
title = recognize.SanitizeTitle(title)
if !recognize.UsableTitle(title) {
title = ""
}
return map[string]string{
ovrProvider: provider,
ovrProviderID: providerID,
@@ -1297,8 +1315,15 @@ func (w *Worker) resolveFolderBase(ctx context.Context, downloadID, provider, pr
// applyOverrides применяет ручные правки к плану: каноническое имя/год (из
// выбранного кандидата базы) и помечает игнорируемые файлы ролью ignore (их
// раскладка пропустит).
//
// Название чистится здесь, НА ЧТЕНИИ, и это не дубль чистки в sourcePins: та
// держит хранилище чистым, а эта защищает от значений, записанных прежними
// версиями. Пин, закреплённый до появления санитайзинга, иначе доезжает до имени
// каталога дословно при обычном «Применить» — без повторного выбора источника
// запись в хранилище никто не перепишет. Чистка идемпотентна, так что на пинах,
// записанных уже с ней, обе не делают ничего.
func applyOverrides(plan recognize.Plan, overrides map[string]string) recognize.Plan {
if t := overrides[ovrTitle]; t != "" {
if t := recognize.SanitizeTitle(overrides[ovrTitle]); recognize.UsableTitle(t) {
plan.Title = t
}
if y := overrides[ovrYear]; y != "" {