Фикс повторного распознавания

This commit is contained in:
2026-06-15 07:57:22 +03:00
parent 16a82572e7
commit e297f0fb84
9 changed files with 145 additions and 44 deletions
+24 -8
View File
@@ -286,11 +286,11 @@ func (w *Worker) linkPlan(ctx context.Context, d *store.Download, plan recognize
return nil
}
// Relink повторно привязывает откатанную задачу (reverted): возвращает её на
// распознавание, и поллинг-цикл перезапустит recognize. Авто-раскладку при
// этом не делаем — ручная перепривязка всегда проходит через ревью с
// подтверждением (force_review). Источник (раздача в qBittorrent) для этого
// должен быть на месте.
// Relink повторно привязывает откатанную (reverted) или отклонённую
// (cancelled) задачу: возвращает её на распознавание, и поллинг-цикл
// перезапустит recognize. Авто-раскладку при этом не делаем — ручная
// перепривязка всегда проходит через ревью с подтверждением (force_review).
// Источник (раздача в qBittorrent) для этого должен быть на месте.
func (w *Worker) Relink(ctx context.Context, id int64) error {
w.mu.Lock()
defer w.mu.Unlock()
@@ -299,8 +299,8 @@ func (w *Worker) Relink(ctx context.Context, id int64) error {
if err != nil {
return fmt.Errorf("relink: %w", err)
}
if d.State != store.StateReverted {
return fmt.Errorf("relink: download %d is in state %s (expected reverted)", id, d.State)
if d.State != store.StateReverted && d.State != store.StateCancelled {
return fmt.Errorf("relink: download %d is in state %s (expected reverted/cancelled)", id, d.State)
}
if !d.Infohash.Valid {
return fmt.Errorf("relink: download %d has no infohash", id)
@@ -325,7 +325,23 @@ func (w *Worker) Relink(ctx context.Context, id int64) error {
return fmt.Errorf("relink: %w", err)
}
w.transition(ctx, *d, store.StateRecognizing, "", "")
w.log.Info("relink: re-recognizing reverted download", "download_id", id)
w.log.Info("relink: re-recognizing download", "download_id", id, "from", d.State)
return nil
}
// Rerecognize перезапускает распознавание для задачи в review/deferred без
// добавления подсказки: контекст и прежние подсказки уже накоплены. Поллинг-
// цикл проведёт задачу recognizing → review заново.
func (w *Worker) Rerecognize(ctx context.Context, id int64) error {
w.mu.Lock()
defer w.mu.Unlock()
d, err := w.requireReviewable(ctx, id, "rerecognize")
if err != nil {
return err
}
w.log.Info("review: re-recognizing without hint", "download_id", id)
w.transition(ctx, *d, store.StateRecognizing, "", "")
return nil
}
+47 -3
View File
@@ -120,14 +120,58 @@ func TestRelink_RevertedToRecognizing(t *testing.T) {
}
}
func TestRelink_RejectsNonReverted(t *testing.T) {
func TestRelink_CancelledToRecognizing(t *testing.T) {
st := newMemStore()
st.put(completedDownload(1)) // не reverted
d := revertedDownload(1)
d.State = store.StateCancelled
st.put(d)
qb := &fakeQbt{torrents: []qbt.Torrent{{Hash: ihTest, Name: "Show", SavePath: "/d"}}}
w := testWorkerWith(st, qb, &fakeRecognizer{result: seriesResult()}, nil)
if err := w.Relink(context.Background(), 1); err != nil {
t.Fatalf("Relink: %v", err)
}
if st.downloads[1].State != store.StateRecognizing {
t.Fatalf("state = %q, want recognizing", st.downloads[1].State)
}
if st.overrides[1][ovrForceReview] != "1" {
t.Errorf("force_review override = %q, want 1", st.overrides[1][ovrForceReview])
}
}
func TestRelink_RejectsActiveState(t *testing.T) {
st := newMemStore()
st.put(completedDownload(1)) // не reverted/cancelled
qb := &fakeQbt{torrents: []qbt.Torrent{{Hash: ihTest}}}
w := testWorkerWith(st, qb, &fakeRecognizer{}, nil)
if err := w.Relink(context.Background(), 1); err == nil {
t.Fatal("ожидали ошибку для не-reverted задачи, получили nil")
t.Fatal("ожидали ошибку для не-reverted/cancelled задачи, получили nil")
}
}
func TestRerecognize_ReviewToRecognizing(t *testing.T) {
st := newMemStore()
d := completedDownload(1)
d.State = store.StateReview
st.put(d)
w := testWorkerWith(st, &fakeQbt{}, &fakeRecognizer{}, nil)
if err := w.Rerecognize(context.Background(), 1); err != nil {
t.Fatalf("Rerecognize: %v", err)
}
if st.downloads[1].State != store.StateRecognizing {
t.Fatalf("state = %q, want recognizing", st.downloads[1].State)
}
}
func TestRerecognize_RejectsNonReview(t *testing.T) {
st := newMemStore()
st.put(completedDownload(1)) // completed, не review/deferred
w := testWorkerWith(st, &fakeQbt{}, &fakeRecognizer{}, nil)
if err := w.Rerecognize(context.Background(), 1); err == nil {
t.Fatal("ожидали ошибку для не-review задачи, получили nil")
}
}