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) }