package artwork
import (
"context"
"encoding/json"
"io"
"slices"
"sync"
"github.com/navidrome/navidrome/utils/str"
)
type Outcome string
const (
OutcomeHit Outcome = "hit"
OutcomeMiss Outcome = "miss"
OutcomeUnreadable Outcome = "unreadable"
OutcomeSkipped Outcome = "skipped"
OutcomeError Outcome = "error"
)
const (
externalCandidate = "external"
ExternalPrefix = externalCandidate + ":"
)
type TraceStep struct {
Candidate string
Outcome Outcome
Detail string
}
type ChainTrace struct {
mu sync.Mutex
steps []TraceStep
}
func (t *ChainTrace) add(step TraceStep) {
if t == nil {
return
}
t.mu.Lock()
defer t.mu.Unlock()
t.steps = append(t.steps, step)
}
func (t *ChainTrace) Steps() []TraceStep {
if t == nil {
return nil
}
t.mu.Lock()
defer t.mu.Unlock()
return slices.Clone(t.steps)
}
const maxTraceDetail = 200
type storedStep struct {
C string `json:"c"`
O Outcome `json:"o"`
D string `json:"d,omitempty"`
}
func (t *ChainTrace) encode(sourcePath string) string {
if t == nil {
return encodeSteps(nil, sourcePath)
}
t.mu.Lock()
defer t.mu.Unlock()
return encodeSteps(t.steps, sourcePath)
}
func encodeSteps(steps []TraceStep, sourcePath string) string {
out := make([]storedStep, 0, len(steps))
for _, s := range steps {
d := s.Detail
if s.Outcome == OutcomeHit && d == sourcePath {
d = ""
}
out = append(out, storedStep{C: s.Candidate, O: s.Outcome, D: str.TruncateRunes(d, maxTraceDetail, "...")})
}
b, _ := json.Marshal(out)
return string(b)
}
func DecodeTrace(encoded, sourcePath string) []TraceStep {
if encoded == "" {
return nil
}
var stored []storedStep
if err := json.Unmarshal([]byte(encoded), &stored); err != nil {
return nil
}
steps := make([]TraceStep, 0, len(stored))
for _, s := range stored {
d := s.D
if d == "" && s.O == OutcomeHit {
d = sourcePath
}
steps = append(steps, TraceStep{Candidate: s.C, Outcome: s.O, Detail: d})
}
return steps
}
type traceCtxKey struct{}
func withTrace(ctx context.Context, t *ChainTrace) context.Context {
return context.WithValue(ctx, traceCtxKey{}, t)
}
func traceFrom(ctx context.Context) *ChainTrace {
t, _ := ctx.Value(traceCtxKey{}).(*ChainTrace)
return t
}
func recordAgent(ctx context.Context, name string, r io.ReadCloser, path string, err error) {
t := traceFrom(ctx)
candidate := ExternalPrefix + name
switch {
case r != nil:
t.add(TraceStep{Candidate: candidate, Outcome: OutcomeHit, Detail: path})
case isTransientExternal(err):
t.add(TraceStep{Candidate: candidate, Outcome: OutcomeError, Detail: err.Error()})
default:
t.add(TraceStep{Candidate: candidate, Outcome: OutcomeMiss})
}
}
func traceStage(ctx context.Context, stage string, err error) {
traceFrom(ctx).add(TraceStep{Candidate: stage, Outcome: OutcomeError, Detail: err.Error()})
}