fix(player+lidarr): mini-player sync race; durable approve + dedup
player: setQueueFromTracks fast-starts a single source at player-index 0 while the full queue is broadcast, so the transient currentIndexStream→0 emission clobbered the correct mediaItem with queue.value[0] (the first track). Mini bar / playlist marker pinned to the wrong track until a later index event (~the "passive ~30s recovery"). Track a logical-index base so the player→queue mapping stays correct during the fill window; also fixes the latent forward-fill auto-advance off-by-base. lidarr #50: approving no longer fails when Lidarr is down. Approve records the decision durably first, then best-effort adds; the reconciler idempotently (re)sends unconfirmed adds every tick until they stick (new additive lidarr_add_confirmed_at; AddArtist/AddAlbum map Lidarr's "already exists" 400 → ErrAlreadyExists). No failed-state or expiry by design — Lidarr keeps trying, operator monitors. lidarr #51: Create() is now idempotent — a non-terminal request for the same MBID returns the existing row instead of inserting a duplicate. Rewrites the obsolete LidarrUnreachable_503 test to assert the durable- approve contract; threads a client factory into NewReconciler. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -300,12 +301,29 @@ func (c *Client) AddArtist(ctx context.Context, p AddArtistParams) error {
|
||||
}
|
||||
resp, err := c.post(ctx, "/api/v1/artist", body)
|
||||
if err != nil {
|
||||
if isAlreadyExists(err) {
|
||||
return ErrAlreadyExists
|
||||
}
|
||||
return err
|
||||
}
|
||||
_ = resp.Body.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// isAlreadyExists detects Lidarr's 400 "already in your library" rejection
|
||||
// from an add endpoint. The post() helper buckets it as ErrLookupFailed
|
||||
// but embeds Lidarr's errorMessage body snippet; matching the phrase lets
|
||||
// the idempotent retry path treat a re-add as success instead of error.
|
||||
func isAlreadyExists(err error) bool {
|
||||
if !errors.Is(err, ErrLookupFailed) {
|
||||
return false
|
||||
}
|
||||
m := strings.ToLower(err.Error())
|
||||
return strings.Contains(m, "already exists") ||
|
||||
strings.Contains(m, "already been added") ||
|
||||
strings.Contains(m, "already in your library")
|
||||
}
|
||||
|
||||
// AddAlbum posts to POST /api/v1/album. Returns nil on 2xx; typed error
|
||||
// otherwise.
|
||||
func (c *Client) AddAlbum(ctx context.Context, p AddAlbumParams) error {
|
||||
@@ -324,6 +342,9 @@ func (c *Client) AddAlbum(ctx context.Context, p AddAlbumParams) error {
|
||||
}
|
||||
resp, err := c.post(ctx, "/api/v1/album", body)
|
||||
if err != nil {
|
||||
if isAlreadyExists(err) {
|
||||
return ErrAlreadyExists
|
||||
}
|
||||
return err
|
||||
}
|
||||
_ = resp.Body.Close()
|
||||
|
||||
@@ -14,6 +14,12 @@ var (
|
||||
ErrLookupFailed = errors.New("lidarr: lookup failed") // 4xx other than 401/403
|
||||
ErrServerError = errors.New("lidarr: server error") // 5xx
|
||||
ErrInvalidPayload = errors.New("lidarr: invalid payload")
|
||||
// ErrAlreadyExists is returned by AddArtist/AddAlbum when Lidarr
|
||||
// rejects the add with a 400 because the artist/album is already in
|
||||
// its library. This is the desired end state, not a failure — the
|
||||
// idempotent retry path (reconciler re-sending an add after a partial
|
||||
// success) treats it as confirmed rather than spinning forever.
|
||||
ErrAlreadyExists = errors.New("lidarr: already exists")
|
||||
// ErrNotFound is returned by LookupArtistByMBID and LookupAlbumByMBID
|
||||
// when Lidarr returns 200 with an empty array — i.e., the MBID isn't in
|
||||
// Lidarr's monitored set. Distinguished from network/auth errors so admin
|
||||
|
||||
Reference in New Issue
Block a user