Раскладка файлов после распознавния

This commit is contained in:
2026-06-14 14:53:40 +03:00
parent 91c501624a
commit 9c1b178e46
19 changed files with 3001 additions and 38 deletions
+202
View File
@@ -0,0 +1,202 @@
package httpapi
import (
"context"
"database/sql"
"errors"
"net/http"
"net/url"
"strconv"
"git.vakhrushev.me/av/jellybit/internal/worker"
)
// Reviewer — операции ревью и раскладки (worker.Worker).
type Reviewer interface {
ReviewData(ctx context.Context, id int64) (*worker.ReviewData, error)
Apply(ctx context.Context, id int64) error
Refine(ctx context.Context, id int64, hint string) error
SetType(ctx context.Context, id int64, mediaType string) error
IgnoreFile(ctx context.Context, id int64, src string) error
Defer(ctx context.Context, id int64) error
Undo(ctx context.Context, id int64) error
}
// --- Представление страницы ревью ---
type reviewView struct {
ID int64
Source string
Context string
State string
Error string // из ?err=
StateError string // error_msg загрузки (напр. причина коллизии)
MediaType string
IsSeries bool
Title string
OriginalTitle string
Year int
Confidence string
Reasons []string
Hints []string
Files []reviewFileView
Preview []string
HasPlan bool
}
type reviewFileView struct {
Src string
Role string
Season string
Episode string
Ignored bool
}
func (s *server) handleReview(w http.ResponseWriter, r *http.Request) {
id, err := pathID(r)
if err != nil {
http.Error(w, "некорректный id", http.StatusBadRequest)
return
}
rd, err := s.deps.Reviewer.ReviewData(r.Context(), id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
http.Error(w, "задача не найдена", http.StatusNotFound)
return
}
s.deps.Logger.Error("review data", "id", id, "err", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
view := reviewView{
ID: id,
Source: shorten(rd.Download.SourceRef, 80),
Context: rd.Download.Context,
State: string(rd.Download.State),
Error: r.URL.Query().Get("err"),
StateError: rd.Download.ErrorMsg.String,
Hints: rd.Hints,
}
if rec := rd.Recognition; rec != nil {
view.MediaType = string(rd.Plan.Type)
view.IsSeries = rd.Plan.Type == "series"
view.Title = rd.Plan.Title
view.OriginalTitle = rd.Plan.OriginalTitle
view.Year = rd.Plan.Year
view.Reasons = rec.ReasonList()
if rec.Confidence.Valid {
view.Confidence = strconv.FormatFloat(rec.Confidence.Float64, 'f', 2, 64)
}
for _, f := range rd.Plan.Files {
view.Files = append(view.Files, reviewFileView{
Src: f.Src,
Role: string(f.Role),
Season: intPtrStr(f.Season),
Episode: intPtrStr(f.Episode),
Ignored: f.Role == "ignore",
})
}
view.HasPlan = len(rd.Plan.Files) > 0
}
for _, l := range rd.Preview {
view.Preview = append(view.Preview, l.Dst)
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := s.review.Execute(w, view); err != nil {
s.deps.Logger.Error("render review", "err", err)
}
}
// --- Действия ревью (POST → redirect) ---
func (s *server) handleApply(w http.ResponseWriter, r *http.Request) {
id, err := pathID(r)
if err != nil {
redirectErr(w, r, "некорректный id")
return
}
if err := s.deps.Reviewer.Apply(r.Context(), id); err != nil {
redirectReview(w, r, id, err.Error())
return
}
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func (s *server) handleRefine(w http.ResponseWriter, r *http.Request) {
s.reviewAction(w, r, func(ctx context.Context, id int64) error {
_ = r.ParseForm()
return s.deps.Reviewer.Refine(ctx, id, r.PostForm.Get("hint"))
})
}
func (s *server) handleSetType(w http.ResponseWriter, r *http.Request) {
s.reviewAction(w, r, func(ctx context.Context, id int64) error {
_ = r.ParseForm()
return s.deps.Reviewer.SetType(ctx, id, r.PostForm.Get("type"))
})
}
func (s *server) handleIgnore(w http.ResponseWriter, r *http.Request) {
s.reviewAction(w, r, func(ctx context.Context, id int64) error {
_ = r.ParseForm()
return s.deps.Reviewer.IgnoreFile(ctx, id, r.PostForm.Get("src"))
})
}
func (s *server) handleDefer(w http.ResponseWriter, r *http.Request) {
id, err := pathID(r)
if err != nil {
redirectErr(w, r, "некорректный id")
return
}
if err := s.deps.Reviewer.Defer(r.Context(), id); err != nil {
redirectReview(w, r, id, err.Error())
return
}
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func (s *server) handleUndo(w http.ResponseWriter, r *http.Request) {
id, err := pathID(r)
if err != nil {
redirectErr(w, r, "некорректный id")
return
}
if err := s.deps.Reviewer.Undo(r.Context(), id); err != nil {
redirectErr(w, r, err.Error())
return
}
http.Redirect(w, r, "/", http.StatusSeeOther)
}
// reviewAction — общий помощник: выполнить действие и вернуться на страницу
// ревью (с ошибкой в ?err при неудаче).
func (s *server) reviewAction(w http.ResponseWriter, r *http.Request, fn func(context.Context, int64) error) {
id, err := pathID(r)
if err != nil {
redirectErr(w, r, "некорректный id")
return
}
if err := fn(r.Context(), id); err != nil {
redirectReview(w, r, id, err.Error())
return
}
redirectReview(w, r, id, "")
}
func redirectReview(w http.ResponseWriter, r *http.Request, id int64, msg string) {
u := "/review/" + strconv.FormatInt(id, 10)
if msg != "" {
u += "?err=" + url.QueryEscape(msg)
}
http.Redirect(w, r, u, http.StatusSeeOther)
}
func intPtrStr(p *int) string {
if p == nil {
return "—"
}
return strconv.Itoa(*p)
}