review: убрать явный переключатель типа movie↔series из всех транспортов
Кнопка «Тип» была только в Telegram (доменная SetType = жёсткий override media_type + хинт + перераспознавание), в вебе её нет. Это создавало расхождение поверхностей и внутреннюю противоречивость спеки review. Решение: смена типа — редкий случай, для него достаточно «Уточнить» (перераспознавание с явным указанием типа). Явный переключатель не нужен ни на одной поверхности. Снято: Telegram-кнопка и callback type:, worker.SetType, ставший мёртвым override-плумбинг media_type (ovrMediaType, ветка applyOverrides, хелпер oppositeType) и стейл-хвосты в тестах httpapi. Спека review — три MODIFIED требования (запрет на все поверхности, «фиксация типа» убрана из команд и из быстрых действий Telegram, иллюстрация override заменена на закрепление источника). Синхронизирован docs/specs/review-ux.md. Change заархивирован: openspec/changes/archive/2026-07-18-review-remove-type-switch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -23,7 +23,6 @@ import (
|
||||
|
||||
// Поля override.
|
||||
const (
|
||||
ovrMediaType = "media_type"
|
||||
ovrIgnoredFiles = "ignored_files"
|
||||
ovrProvider = "provider" // выбранная база ("none" = без базы)
|
||||
ovrProviderID = "provider_id" // id в выбранной базе
|
||||
@@ -475,38 +474,6 @@ func (w *Worker) Refine(ctx context.Context, id string, hint string) (err error)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetType фиксирует тип (override) и перезапускает распознавание с подсказкой
|
||||
// — чтобы LLM пересобрал роли файлов под новый тип.
|
||||
func (w *Worker) SetType(ctx context.Context, id string, mediaType string) (err error) {
|
||||
defer func() { w.logCmd(ctx, "set_type", id, err) }()
|
||||
if mediaType != string(recognize.MediaMovie) && mediaType != string(recognize.MediaSeries) {
|
||||
return fmt.Errorf("set type: invalid type %q: %w", mediaType, ErrInvalidInput)
|
||||
}
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
d, err := w.requireReviewable(ctx, id, "set type")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.ensureSourceReady(ctx, d, "set type"); err != nil {
|
||||
return err
|
||||
}
|
||||
ctx = w.scoped(ctx, capReview, id, d.PrimaryInfohash())
|
||||
if err := w.store.SetOverride(ctx, id, ovrMediaType, mediaType); err != nil {
|
||||
return fmt.Errorf("set type: %w", err)
|
||||
}
|
||||
label := "фильм"
|
||||
if mediaType == string(recognize.MediaSeries) {
|
||||
label = "сериал"
|
||||
}
|
||||
if err := w.store.AddHint(ctx, id, "Тип точно: "+label+"."); err != nil {
|
||||
return fmt.Errorf("set type: %w", err)
|
||||
}
|
||||
w.transition(ctx, *d, store.StateRecognizing, "", "")
|
||||
return nil
|
||||
}
|
||||
|
||||
// IgnoreFile помечает файл к игнорированию (не линкуем). Остаёмся в review;
|
||||
// превью пересчитается с учётом правки.
|
||||
func (w *Worker) IgnoreFile(ctx context.Context, id string, src string) (err error) {
|
||||
@@ -1148,8 +1115,7 @@ func (w *Worker) sourceOption(ctx context.Context, downloadID string, kind Sourc
|
||||
}
|
||||
|
||||
// mergeSourceOverrides накладывает пины источника (provider/id/title/year/
|
||||
// director) на неисточниковые overrides (media_type, ignored_files, force_review,
|
||||
// ...).
|
||||
// director) на неисточниковые overrides (ignored_files, force_review, ...).
|
||||
func mergeSourceOverrides(base, pins map[string]string) map[string]string {
|
||||
m := make(map[string]string, len(base)+len(pins))
|
||||
for k, v := range base {
|
||||
@@ -1307,13 +1273,10 @@ func (w *Worker) resolveFolderBase(ctx context.Context, downloadID, provider, pr
|
||||
|
||||
// --- Хелперы преобразования ---
|
||||
|
||||
// applyOverrides применяет ручные правки к плану: форсит тип, каноническое
|
||||
// имя/год (из выбранного кандидата базы) и помечает игнорируемые файлы ролью
|
||||
// ignore (их раскладка пропустит).
|
||||
// applyOverrides применяет ручные правки к плану: каноническое имя/год (из
|
||||
// выбранного кандидата базы) и помечает игнорируемые файлы ролью ignore (их
|
||||
// раскладка пропустит).
|
||||
func applyOverrides(plan recognize.Plan, overrides map[string]string) recognize.Plan {
|
||||
if mt := overrides[ovrMediaType]; mt == string(recognize.MediaMovie) || mt == string(recognize.MediaSeries) {
|
||||
plan.Type = recognize.MediaType(mt)
|
||||
}
|
||||
if t := overrides[ovrTitle]; t != "" {
|
||||
plan.Title = t
|
||||
}
|
||||
|
||||
@@ -300,7 +300,6 @@ func TestReviewCommands_RejectNotReadySource(t *testing.T) {
|
||||
}{
|
||||
{"rerecognize", store.StateDeferred, func(w *Worker) error { return w.Rerecognize(context.Background(), "1") }},
|
||||
{"refine", store.StateReview, func(w *Worker) error { return w.Refine(context.Background(), "1", "подсказка") }},
|
||||
{"set type", store.StateReview, func(w *Worker) error { return w.SetType(context.Background(), "1", "series") }},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
@@ -949,28 +948,6 @@ func TestRefine_AddsHintAndRerecognizes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetType(t *testing.T) {
|
||||
st := newMemStore()
|
||||
d := completedDownload("1")
|
||||
d.State = store.StateReview
|
||||
st.put(d)
|
||||
qb := &fakeQbt{torrents: []qbt.Torrent{{Hash: ihTest, State: "uploading"}}}
|
||||
w := testWorkerWith(st, qb, &fakeRecognizer{}, nil)
|
||||
|
||||
if err := w.SetType(context.Background(), "1", "series"); err != nil {
|
||||
t.Fatalf("SetType: %v", err)
|
||||
}
|
||||
if st.overrides["1"][ovrMediaType] != "series" {
|
||||
t.Errorf("override = %v", st.overrides["1"])
|
||||
}
|
||||
if st.downloads["1"].State != store.StateRecognizing {
|
||||
t.Errorf("state = %q, want recognizing", st.downloads["1"].State)
|
||||
}
|
||||
if err := w.SetType(context.Background(), "1", "cartoon"); err == nil {
|
||||
t.Error("invalid type must be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIgnoreFile(t *testing.T) {
|
||||
st := newMemStore()
|
||||
d := completedDownload("1")
|
||||
@@ -1582,18 +1559,19 @@ func TestReviewData(t *testing.T) {
|
||||
|
||||
func TestApplyOverrides(t *testing.T) {
|
||||
plan := recognize.Plan{
|
||||
Type: recognize.MediaMovie,
|
||||
Type: recognize.MediaMovie,
|
||||
Title: "Сырое название",
|
||||
Files: []recognize.PlanFile{
|
||||
{Src: "a.mkv", Role: recognize.RoleMain},
|
||||
{Src: "b.mkv", Role: recognize.RoleEpisode},
|
||||
},
|
||||
}
|
||||
out := applyOverrides(plan, map[string]string{
|
||||
ovrMediaType: "series",
|
||||
ovrTitle: "Каноническое название",
|
||||
ovrIgnoredFiles: `["a.mkv"]`,
|
||||
})
|
||||
if out.Type != recognize.MediaSeries {
|
||||
t.Errorf("type = %q, want series", out.Type)
|
||||
if out.Title != "Каноническое название" {
|
||||
t.Errorf("title = %q, want override", out.Title)
|
||||
}
|
||||
if out.Files[0].Role != "ignore" {
|
||||
t.Errorf("a.mkv role = %q, want ignore", out.Files[0].Role)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// Ф1 ведёт задачу downloading → completed, плюс stuck/failed по таймаутам и
|
||||
// ошибкам qBittorrent. Ф3 продолжает: completed → recognizing (вызов
|
||||
// recognize) → review; команды ревью (apply/refine/reject/defer/undo,
|
||||
// переключение типа, пометка «игнор») раскладывают файлы хардлинками через
|
||||
// пометка «игнор») раскладывают файлы хардлинками через
|
||||
// layout. Распознавание зовётся в поллинг-цикле, команды — из транспортов;
|
||||
// всё под per-download блокировкой w.mu.
|
||||
package worker
|
||||
|
||||
Reference in New Issue
Block a user