f6975cfad3
Replaces commit 50a231f's wrong-shape Lidarr-routed delete. The previous
design called lidarrquarantine.DeleteViaLidarr for Lidarr-managed tracks,
which deletes the entire **album** in Lidarr (Lidarr is album-granular,
no per-track delete API) — silently dropping sibling tracks the operator
didn't ask to remove.
New shape per spec revision 723eee9:
- Always: os.Remove + DB delete + cascade through Minstrel.
- When unmonitor=true AND track has mbid: call new Lidarr.UnmonitorTrack
primitive so Lidarr won't replace. Failure is non-fatal — file is
already gone, DB consistent; the lidarrUnmonitorFailed bool flows to
the wire response so the UI can surface a follow-up "unmonitor
manually" toast.
Service signature changes from `(trackID, adminID) → (delAlbum, delArtist, err)`
to `(trackID, adminID, unmonitor) → (delAlbum, delArtist, lidarrFailed, err)`.
Adds Lidarr.UnmonitorTrack with full three-step API walk:
1. GET /api/v1/album?foreignAlbumId={mbid} → Lidarr's internal album id
2. GET /api/v1/track?albumId={id} → match track by foreignTrackId
3. PUT /api/v1/track/monitor with {trackIds:[id], monitored:false}
Refactors post() into a shared bodyRequest() so put() reuses the same
status-code → typed-error mapping. The album mbid is captured *before*
the cascade-delete transaction — DeleteAlbumIfEmpty may remove the
album row, after which a post-commit GetAlbumByID returns ErrNoRows
and the unmonitor walk would have nothing to walk against.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
92 lines
2.8 KiB
Go
92 lines
2.8 KiB
Go
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
|
|
}
|