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 }