package lidarr import ( "context" "encoding/json" "fmt" "net/url" "strconv" ) // UnmonitorTrack flips Lidarr's `monitored` flag to false on the track // matching trackMbid (a MusicBrainz id, aka Lidarr's `foreignTrackId`). // Used by the M7 #372 admin "Remove from library" flow when the operator // requests `?unmonitor=true` so Lidarr's monitor doesn't search for a // replacement after we delete the local file + DB rows. // // Lidarr's monitor API takes its own internal numeric id, not the mbid, // so this is a three-step walk: // // 1. GET /api/v1/album?foreignAlbumId={albumMbid} — find the album's // internal Lidarr id. albumMbid is the parent album's mbid; the // caller (tracks service) passes it because looking it up from the // trackMbid alone would mean either a dedicated Lidarr endpoint // that doesn't exist or scanning the entire library. // 2. GET /api/v1/track?albumId={lidarr_album_id} — list the album's // tracks, each with its internal id and foreignTrackId. // 3. PUT /api/v1/track/monitor — body `{trackIds:[id], monitored:false}`. // // Returns ErrNotFound when the album or track isn't in Lidarr's library // (already removed, or the track was never imported). Network / auth / // 5xx errors propagate as typed sentinel errors so the tracks service // can decide whether to surface a `lidarr_unmonitor_failed` flag. func (c *Client) UnmonitorTrack(ctx context.Context, trackMbid, albumMbid string) error { if trackMbid == "" { return fmt.Errorf("lidarr: empty track mbid") } if albumMbid == "" { return fmt.Errorf("lidarr: empty album mbid") } // Step 1: resolve album mbid → Lidarr album id. album, err := c.LookupAlbumByMBID(ctx, albumMbid) if err != nil { return err } if album.ID == 0 { return ErrNotFound } // Step 2: list tracks under that album, find the one whose // foreignTrackId matches. q := url.Values{"albumId": []string{strconv.Itoa(album.ID)}} resp, err := c.get(ctx, "/api/v1/track", q) if err != nil { return err } defer func() { _ = resp.Body.Close() }() var tracks []struct { ID int `json:"id"` ForeignTrackID string `json:"foreignTrackId"` } if err := json.NewDecoder(resp.Body).Decode(&tracks); err != nil { return fmt.Errorf("%w: decode tracks: %v", ErrInvalidPayload, err) } var lidarrTrackID int for _, t := range tracks { if t.ForeignTrackID == trackMbid { lidarrTrackID = t.ID break } } if lidarrTrackID == 0 { return ErrNotFound } // Step 3: PUT the monitor toggle. body, err := json.Marshal(map[string]any{ "trackIds": []int{lidarrTrackID}, "monitored": false, }) if err != nil { return fmt.Errorf("%w: marshal: %v", ErrInvalidPayload, err) } putResp, err := c.put(ctx, "/api/v1/track/monitor", body) if err != nil { return err } _ = putResp.Body.Close() return nil }