ci / ts / ts (packages + web) (push) Canceled after 1m0s
ci / api / api (push) Successful in 46s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 56s
ci / db / db (push) Successful in 1m32s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m27s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m31s
Search hits omit album covers and artist images. The worker now reads the full song and artist, stores the description, and leaves Genius image URLs unchanged on mobile. Co-authored-by: Cursor <cursoragent@cursor.com>
99 lines
3.0 KiB
Go
99 lines
3.0 KiB
Go
package genius
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"pipu/api/internal/api/gen"
|
|
"pipu/api/internal/auth"
|
|
"pipu/api/internal/platform/apperr"
|
|
)
|
|
|
|
type Handler struct{ svc *Service }
|
|
|
|
func NewHandler(s *Service) *Handler { return &Handler{svc: s} }
|
|
|
|
func requireAdmin(ctx context.Context) error {
|
|
p, ok := auth.PrincipalFrom(ctx)
|
|
if !ok {
|
|
return apperr.Unauthorized("auth.unauthorized", "authentication required")
|
|
}
|
|
if p.Role != auth.RoleAdmin {
|
|
return apperr.Forbidden("auth.forbidden", "administrator required")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (h *Handler) GetGeniusStats(ctx context.Context, _ gen.GetGeniusStatsRequestObject) (gen.GetGeniusStatsResponseObject, error) {
|
|
if err := requireAdmin(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
stats, err := h.svc.Stats(ctx)
|
|
return gen.GetGeniusStats200JSONResponse(stats), err
|
|
}
|
|
|
|
func (h *Handler) ControlGenius(ctx context.Context, req gen.ControlGeniusRequestObject) (gen.ControlGeniusResponseObject, error) {
|
|
if err := requireAdmin(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
if !h.svc.configured {
|
|
return nil, apperr.Conflict("genius.disabled", "configure GENIUS_ACCESS_TOKEN in deploy/.env")
|
|
}
|
|
if req.Body == nil {
|
|
return nil, apperr.Validation("action", "validation.required", "action required", nil)
|
|
}
|
|
tx, err := h.svc.pool.Begin(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
switch string(req.Body.Action) {
|
|
case "pause":
|
|
_, err = tx.Exec(ctx, `UPDATE genius_state SET paused=true`)
|
|
case "start":
|
|
_, err = tx.Exec(ctx, `UPDATE genius_state SET paused=false,last_error=''`)
|
|
case "retry":
|
|
_, err = tx.Exec(ctx, `UPDATE genius_tracks SET status='pending',next_attempt=now(),last_error='' WHERE status IN ('error','partial','unmatched') AND locked_until<=now()`)
|
|
if err == nil {
|
|
_, err = tx.Exec(ctx, `UPDATE genius_state SET paused=false,last_error=''`)
|
|
}
|
|
default:
|
|
return nil, apperr.Validation("action", "validation.invalid", "unknown action", nil)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err = tx.Commit(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
h.svc.Wake()
|
|
stats, err := h.svc.Stats(ctx)
|
|
return gen.ControlGenius200JSONResponse(stats), err
|
|
}
|
|
|
|
func (h *Handler) GetTrackGenius(ctx context.Context, req gen.GetTrackGeniusRequestObject) (gen.GetTrackGeniusResponseObject, error) {
|
|
if _, ok := auth.PrincipalFrom(ctx); !ok {
|
|
return nil, apperr.Unauthorized("auth.unauthorized", "authentication required")
|
|
}
|
|
content := emptyContent()
|
|
var body []byte
|
|
err := h.svc.pool.QueryRow(ctx, `SELECT COALESCE(g.content,'{}'),COALESCE(g.status,'pending') FROM tracks t LEFT JOIN genius_tracks g ON g.track_id=t.id WHERE t.id=$1`, req.Id).Scan(&body, &content.Status)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, apperr.NotFound("catalog.not_found", "track not found")
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
status := content.Status
|
|
if err = json.Unmarshal(body, &content); err != nil {
|
|
return nil, err
|
|
}
|
|
content.Status = status
|
|
if !h.svc.configured && status == "pending" {
|
|
content.Status = "disabled"
|
|
}
|
|
return gen.GetTrackGenius200JSONResponse(content), nil
|
|
}
|