139 lines
4.7 KiB
Go
139 lines
4.7 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/apierror"
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/lidarr"
|
|
)
|
|
|
|
// lidarrSearchResult is the JSON shape returned by GET /api/lidarr/search.
|
|
// Field names follow the lowercase snake_case convention used by all /api/*
|
|
// responses. artist_mbid and album_mbid are the parent identifiers surfaced
|
|
// by LookupAlbum / LookupTrack; they are empty strings on artist results.
|
|
type lidarrSearchResult struct {
|
|
MBID string `json:"mbid"`
|
|
Name string `json:"name"`
|
|
SecondaryText string `json:"secondary_text"`
|
|
ImageURL string `json:"image_url"`
|
|
ArtistMBID string `json:"artist_mbid"`
|
|
AlbumMBID string `json:"album_mbid"`
|
|
InLibrary bool `json:"in_library"`
|
|
Requested bool `json:"requested"`
|
|
}
|
|
|
|
// handleLidarrSearch implements GET /api/lidarr/search?q=&kind=artist|album|track.
|
|
//
|
|
// Steps:
|
|
// 1. Validate kind and q params.
|
|
// 2. Load lidarr config; 503 when disabled.
|
|
// 3. Call the matching Lidarr lookup.
|
|
// 4. For each result, compute in_library (EXISTS by MBID) and requested
|
|
// (HasNonTerminalRequestForMBID).
|
|
// 5. Return normalized JSON array.
|
|
func (h *handlers) handleLidarrSearch(w http.ResponseWriter, r *http.Request) {
|
|
kind := strings.TrimSpace(r.URL.Query().Get("kind"))
|
|
switch kind {
|
|
case "artist", "album", "track":
|
|
// valid
|
|
default:
|
|
writeErr(w, apierror.BadRequest("bad_kind", "kind must be artist, album, or track"))
|
|
return
|
|
}
|
|
|
|
q := strings.TrimSpace(r.URL.Query().Get("q"))
|
|
if q == "" {
|
|
writeErr(w, apierror.BadRequest("missing_query", "q is required"))
|
|
return
|
|
}
|
|
|
|
cfg, err := h.lidarrCfg.Get(r.Context())
|
|
if err != nil {
|
|
h.logger.Error("api: lidarr search: load config", "err", err)
|
|
writeErr(w, &apierror.Error{Status: 500, Code: "lidarr_lookup_failed", Message: "failed to load lidarr config", Cause: err})
|
|
return
|
|
}
|
|
if !cfg.Enabled {
|
|
writeErr(w, &apierror.Error{Status: 503, Code: "lidarr_disabled", Message: "Lidarr integration is not enabled"})
|
|
return
|
|
}
|
|
|
|
client := lidarr.NewClient(cfg.BaseURL, cfg.APIKey)
|
|
|
|
var results []lidarr.LookupResult
|
|
switch kind {
|
|
case "artist":
|
|
results, err = client.LookupArtist(r.Context(), q)
|
|
case "album":
|
|
results, err = client.LookupAlbum(r.Context(), q)
|
|
case "track":
|
|
results, err = client.LookupTrack(r.Context(), q)
|
|
}
|
|
if err != nil {
|
|
switch {
|
|
case errors.Is(err, lidarr.ErrUnreachable):
|
|
writeErr(w, &apierror.Error{Status: 503, Code: "lidarr_unreachable", Message: "Lidarr is unreachable", Cause: err})
|
|
case errors.Is(err, lidarr.ErrAuthFailed):
|
|
writeErr(w, &apierror.Error{Status: 503, Code: "lidarr_auth_failed", Message: "Lidarr authentication failed", Cause: err})
|
|
default:
|
|
h.logger.Error("api: lidarr search: lookup failed", "kind", kind, "err", err)
|
|
writeErr(w, &apierror.Error{Status: 500, Code: "lidarr_lookup_failed", Message: "Lidarr lookup failed", Cause: err})
|
|
}
|
|
return
|
|
}
|
|
|
|
dbQ := dbq.New(h.pool)
|
|
out := make([]lidarrSearchResult, 0, len(results))
|
|
for _, res := range results {
|
|
inLib, libErr := h.lidarrInLibrary(r.Context(), kind, res.MBID)
|
|
if libErr != nil {
|
|
h.logger.Error("api: lidarr search: in_library check failed", "err", libErr)
|
|
writeErr(w, &apierror.Error{Status: 500, Code: "lidarr_lookup_failed", Message: "library check failed", Cause: libErr})
|
|
return
|
|
}
|
|
requested, reqErr := dbQ.HasNonTerminalRequestForMBID(r.Context(), res.MBID)
|
|
if reqErr != nil {
|
|
h.logger.Error("api: lidarr search: requested check failed", "err", reqErr)
|
|
writeErr(w, &apierror.Error{Status: 500, Code: "lidarr_lookup_failed", Message: "request check failed", Cause: reqErr})
|
|
return
|
|
}
|
|
out = append(out, lidarrSearchResult{
|
|
MBID: res.MBID,
|
|
Name: res.Name,
|
|
SecondaryText: res.Secondary,
|
|
ImageURL: res.ImageURL,
|
|
ArtistMBID: res.ArtistMBID,
|
|
AlbumMBID: res.AlbumMBID,
|
|
InLibrary: inLib,
|
|
Requested: requested,
|
|
})
|
|
}
|
|
writeJSON(w, http.StatusOK, out)
|
|
}
|
|
|
|
// lidarrInLibrary checks whether the given MBID exists in the corresponding
|
|
// local library table based on kind. Runs a direct EXISTS query since
|
|
// these per-kind MBID lookups are not in the sqlc-generated query set.
|
|
func (h *handlers) lidarrInLibrary(ctx context.Context, kind, mbid string) (bool, error) {
|
|
var query string
|
|
switch kind {
|
|
case "artist":
|
|
query = `SELECT EXISTS(SELECT 1 FROM artists WHERE mbid = $1)`
|
|
case "album":
|
|
query = `SELECT EXISTS(SELECT 1 FROM albums WHERE mbid = $1)`
|
|
case "track":
|
|
query = `SELECT EXISTS(SELECT 1 FROM tracks WHERE mbid = $1)`
|
|
default:
|
|
return false, nil
|
|
}
|
|
var exists bool
|
|
if err := h.pool.QueryRow(ctx, query, mbid).Scan(&exists); err != nil {
|
|
return false, err
|
|
}
|
|
return exists, nil
|
|
}
|