O contexto da requisição HTTP era passado para o Enqueue, causando 'context canceled' quando a conexão encerrava antes do INSERT no SQLite completar. Jobs devem ser persistidos independentemente do ciclo de vida da requisição.
141 lines
3.3 KiB
Go
141 lines
3.3 KiB
Go
package web
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"coda/internal/jobs"
|
|
"coda/internal/models"
|
|
)
|
|
|
|
func (h *Handler) Library(w http.ResponseWriter, r *http.Request) {
|
|
entries, err := h.scanner.Browse("")
|
|
if err != nil {
|
|
slog.Warn("library page browse failed, showing empty view", "error", err)
|
|
h.render.Render(w, "library.html", nil)
|
|
return
|
|
}
|
|
h.render.Render(w, "library.html", entries)
|
|
}
|
|
|
|
func (h *Handler) LibraryBrowse(w http.ResponseWriter, r *http.Request) {
|
|
relPath := r.URL.Query().Get("path")
|
|
|
|
entries, err := h.scanner.Browse(relPath)
|
|
if err != nil {
|
|
slog.Warn("library browse failed", "path", relPath, "error", err)
|
|
writeJSON(w, http.StatusOK, []any{})
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, entries)
|
|
}
|
|
|
|
func (h *Handler) LibraryScan(w http.ResponseWriter, r *http.Request) {
|
|
albums, err := h.scanner.Scan()
|
|
if err != nil {
|
|
slog.Error("library scan failed", "error", err)
|
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if r.Header.Get("HX-Request") != "" {
|
|
h.render.Render(w, "scan_results.html", albums)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, albums)
|
|
}
|
|
|
|
type convertRequest struct {
|
|
Paths []string `json:"paths"`
|
|
}
|
|
|
|
func (h *Handler) LibraryConvert(w http.ResponseWriter, r *http.Request) {
|
|
var paths []string
|
|
|
|
if strings.HasPrefix(r.Header.Get("Content-Type"), "application/json") {
|
|
var req convertRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
paths = req.Paths
|
|
} else {
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
paths = r.Form["paths"]
|
|
}
|
|
|
|
if len(paths) == 0 {
|
|
http.Error(w, "no paths provided", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
seen := map[string]struct{}{}
|
|
resolved := make([]string, 0, len(paths))
|
|
for _, relPath := range paths {
|
|
relPath = strings.TrimSpace(relPath)
|
|
if relPath == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[relPath]; ok {
|
|
continue
|
|
}
|
|
seen[relPath] = struct{}{}
|
|
|
|
absPath, err := h.scanner.Resolve(relPath)
|
|
if err != nil {
|
|
slog.Warn("library convert: invalid path", "path", relPath, "error", err)
|
|
http.Error(w, "invalid path: "+relPath, http.StatusBadRequest)
|
|
return
|
|
}
|
|
resolved = append(resolved, absPath)
|
|
}
|
|
if len(resolved) == 0 {
|
|
http.Error(w, "no paths provided", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
var jobIDs []string
|
|
for _, absPath := range resolved {
|
|
album := filepath.Base(absPath)
|
|
artist := filepath.Base(filepath.Dir(absPath))
|
|
|
|
id, err := h.enqueuer.Enqueue(context.Background(), jobs.EnqueueInput{
|
|
Artist: artist,
|
|
Album: album,
|
|
Path: absPath,
|
|
Source: models.SourceManual,
|
|
})
|
|
if err != nil {
|
|
slog.Error("library convert enqueue failed", "path", absPath, "error", err)
|
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
jobIDs = append(jobIDs, id)
|
|
}
|
|
|
|
slog.Info("library convert completed", "jobs", jobIDs)
|
|
|
|
if r.Header.Get("HX-Request") != "" {
|
|
h.render.Render(w, "convert_result.html", len(jobIDs))
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusAccepted, map[string]any{"job_ids": jobIDs})
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, v any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
json.NewEncoder(w).Encode(v)
|
|
}
|