Добавил выбор из кандидатов, если LLM не уверена в раскладке
This commit is contained in:
@@ -228,3 +228,92 @@ func (s *Store) DeleteFileLinksByBatch(ctx context.Context, batchID string) erro
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Кандидаты базы метаданных (metadata_candidate) ---
|
||||
|
||||
// MetadataCandidate — строка таблицы metadata_candidate. provider/provider_id
|
||||
// хранят значения для тега Jellyfin (напр. TVMaze отдаёт внешний TVDB-id —
|
||||
// см. recognize), а не обязательно нативный id провайдера поиска.
|
||||
type MetadataCandidate struct {
|
||||
ID int64 `db:"id"`
|
||||
RecognitionID int64 `db:"recognition_id"`
|
||||
Provider string `db:"provider"`
|
||||
ProviderID string `db:"provider_id"`
|
||||
Title sql.NullString `db:"title"`
|
||||
Year sql.NullInt64 `db:"year"`
|
||||
Chosen bool `db:"chosen"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
}
|
||||
|
||||
// CreateCandidates вставляет кандидатов распознавания одной транзакцией.
|
||||
func (s *Store) CreateCandidates(ctx context.Context, cands []MetadataCandidate) error {
|
||||
if len(cands) == 0 {
|
||||
return nil
|
||||
}
|
||||
tx, err := s.DB.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin tx: %w", err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
const q = `
|
||||
INSERT INTO metadata_candidate (recognition_id, provider, provider_id, title, year)
|
||||
VALUES (?, ?, ?, ?, ?)`
|
||||
for _, c := range cands {
|
||||
if _, err := tx.ExecContext(ctx, q,
|
||||
c.RecognitionID, c.Provider, c.ProviderID, c.Title, c.Year); err != nil {
|
||||
return fmt.Errorf("insert candidate: %w", err)
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("commit candidates: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListCandidatesByRecognition возвращает кандидатов попытки распознавания.
|
||||
func (s *Store) ListCandidatesByRecognition(ctx context.Context, recognitionID int64) ([]MetadataCandidate, error) {
|
||||
var out []MetadataCandidate
|
||||
if err := s.DB.SelectContext(ctx, &out,
|
||||
`SELECT * FROM metadata_candidate WHERE recognition_id = ? ORDER BY id`, recognitionID); err != nil {
|
||||
return nil, fmt.Errorf("list candidates: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// GetCandidate возвращает кандидата по id либо (nil, nil).
|
||||
func (s *Store) GetCandidate(ctx context.Context, id int64) (*MetadataCandidate, error) {
|
||||
var c MetadataCandidate
|
||||
err := s.DB.GetContext(ctx, &c, `SELECT * FROM metadata_candidate WHERE id = ?`, id)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get candidate %d: %w", id, err)
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
// SetCandidateChosen помечает кандидата выбранным, снимая отметку с прочих в
|
||||
// той же попытке распознавания.
|
||||
func (s *Store) SetCandidateChosen(ctx context.Context, recognitionID, candidateID int64) error {
|
||||
tx, err := s.DB.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin tx: %w", err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
if _, err := tx.ExecContext(ctx,
|
||||
`UPDATE metadata_candidate SET chosen = 0 WHERE recognition_id = ?`, recognitionID); err != nil {
|
||||
return fmt.Errorf("clear chosen: %w", err)
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx,
|
||||
`UPDATE metadata_candidate SET chosen = 1 WHERE id = ? AND recognition_id = ?`,
|
||||
candidateID, recognitionID); err != nil {
|
||||
return fmt.Errorf("set chosen: %w", err)
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("commit chosen: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -159,6 +159,67 @@ func TestFileLinks_BatchLifecycle(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCandidates_Lifecycle(t *testing.T) {
|
||||
st := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
dl := seedDownload(t, st)
|
||||
recID, err := st.CreateRecognition(ctx, &Recognition{DownloadID: dl}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("create recognition: %v", err)
|
||||
}
|
||||
|
||||
cands := []MetadataCandidate{
|
||||
{RecognitionID: recID, Provider: "tvdb", ProviderID: "269613",
|
||||
Title: NullString("Fargo"), Year: sql.NullInt64{Int64: 2014, Valid: true}},
|
||||
{RecognitionID: recID, Provider: "tmdb", ProviderID: "60622",
|
||||
Title: NullString("Fargo")},
|
||||
}
|
||||
if err := st.CreateCandidates(ctx, cands); err != nil {
|
||||
t.Fatalf("create candidates: %v", err)
|
||||
}
|
||||
|
||||
got, err := st.ListCandidatesByRecognition(ctx, recID)
|
||||
if err != nil {
|
||||
t.Fatalf("list: %v", err)
|
||||
}
|
||||
if len(got) != 2 || got[0].Provider != "tvdb" || got[0].ProviderID != "269613" {
|
||||
t.Fatalf("candidates = %+v", got)
|
||||
}
|
||||
|
||||
chosenID := got[0].ID
|
||||
if err := st.SetCandidateChosen(ctx, recID, chosenID); err != nil {
|
||||
t.Fatalf("set chosen: %v", err)
|
||||
}
|
||||
got, _ = st.ListCandidatesByRecognition(ctx, recID)
|
||||
for _, c := range got {
|
||||
want := c.ID == chosenID
|
||||
if c.Chosen != want {
|
||||
t.Errorf("candidate %d chosen = %v, want %v", c.ID, c.Chosen, want)
|
||||
}
|
||||
}
|
||||
|
||||
// GetCandidate + переотметка.
|
||||
single, err := st.GetCandidate(ctx, got[1].ID)
|
||||
if err != nil || single == nil || single.Provider != "tmdb" {
|
||||
t.Fatalf("get candidate = %+v, %v", single, err)
|
||||
}
|
||||
if err := st.SetCandidateChosen(ctx, recID, got[1].ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, _ = st.ListCandidatesByRecognition(ctx, recID)
|
||||
if got[0].Chosen || !got[1].Chosen {
|
||||
t.Errorf("re-choose failed: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCandidate_None(t *testing.T) {
|
||||
st := newTestStore(t)
|
||||
c, err := st.GetCandidate(context.Background(), 999)
|
||||
if err != nil || c != nil {
|
||||
t.Errorf("want nil,nil; got %+v, %v", c, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLatestBatchID_None(t *testing.T) {
|
||||
st := newTestStore(t)
|
||||
dl := seedDownload(t, st)
|
||||
|
||||
Reference in New Issue
Block a user