package e2e
import (
"net/http"
"strings"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core/sonic"
"github.com/navidrome/navidrome/server/jellyfin/dto"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("AudioMuse endpoints", func() {
BeforeEach(func() { setupTestDB() })
Describe("GET /AudioMuseAI/info", func() {
It("returns version and available endpoints", func() {
var body struct {
Version string `json:"Version"`
AvailableEndpoints []string `json:"AvailableEndpoints"`
}
parseInto(get("/AudioMuseAI/info"), &body)
Expect(body.Version).To(Equal(consts.Version))
Expect(body.AvailableEndpoints).To(ConsistOf(
"GET /AudioMuseAI/find_path",
"GET /AudioMuseAI/health",
"GET /AudioMuseAI/similar_tracks",
))
})
It("requires authentication", func() {
Expect(rawReq("GET", "/AudioMuseAI/info", "").Code).To(Equal(http.StatusUnauthorized))
})
})
Describe("GET /AudioMuseAI/health", func() {
It("returns 200 with an empty body when a provider is loaded", func() {
w := get("/AudioMuseAI/health")
Expect(w.Code).To(Equal(http.StatusOK))
Expect(w.Body.Len()).To(Equal(0))
})
It("requires authentication", func() {
Expect(rawReq("GET", "/AudioMuseAI/health", "").Code).To(Equal(http.StatusUnauthorized))
})
})
Describe("GET /AudioMuseAI/similar_tracks", func() {
It("maps provider results to seeded tracks, encoding item ids", func() {
sonicProviderFake.similar = []sonic.SimilarResult{
{Song: songAgent("Something"), Similarity: 0.3},
{Song: songAgent("So What"), Similarity: 0.5},
}
var body []struct {
Author string `json:"author"`
Distance float64 `json:"distance"`
ItemID string `json:"item_id"`
Title string `json:"title"`
}
parseInto(get("/AudioMuseAI/similar_tracks?item_id="+enc(songID("Come Together"))+"&n=10"), &body)
Expect(body).To(HaveLen(2))
Expect([]string{body[0].Title, body[1].Title}).To(ConsistOf("Something", "So What"))
decoded, ok := dto.DecodeID(body[0].ItemID)
Expect(ok).To(BeTrue())
Expect(decoded).To(Equal(songID(body[0].Title)))
})
It("collapses to one track per artist by default", func() {
sonicProviderFake.similar = []sonic.SimilarResult{
{Song: songAgent("Something"), Similarity: 0.3},
{Song: songAgent("Come Together"), Similarity: 0.5},
}
var body []map[string]any
parseInto(get("/AudioMuseAI/similar_tracks?item_id="+enc(songID("Help!"))), &body)
Expect(body).To(HaveLen(1))
})
It("returns an empty array (not null) when there are no results", func() {
sonicProviderFake.similar = nil
w := get("/AudioMuseAI/similar_tracks?item_id=" + enc(songID("Come Together")))
Expect(w.Code).To(Equal(http.StatusOK))
Expect(strings.TrimSpace(w.Body.String())).To(Equal("[]"))
})
It("requires authentication", func() {
Expect(rawReq("GET", "/AudioMuseAI/similar_tracks?item_id=x", "").Code).To(Equal(http.StatusUnauthorized))
})
})
Describe("GET /AudioMuseAI/find_path", func() {
It("returns 400 with the exact message when a required id is missing", func() {
w := get("/AudioMuseAI/find_path?start_song_id=" + enc(songID("Something")))
Expect(w.Code).To(Equal(http.StatusBadRequest))
Expect(strings.TrimSpace(w.Body.String())).To(Equal("start_song_id and end_song_id are required."))
})
It("returns the path and summed total_distance", func() {
sonicProviderFake.path = []sonic.SimilarResult{
{Song: songAgent("Come Together"), Similarity: 1.5},
{Song: songAgent("So What"), Similarity: 2.0},
}
var body struct {
Path []struct {
ItemID string `json:"item_id"`
Title string `json:"title"`
} `json:"path"`
TotalDistance float64 `json:"total_distance"`
}
parseInto(get("/AudioMuseAI/find_path?start_song_id="+enc(songID("Something"))+"&end_song_id="+enc(songID("So What"))+"&max_steps=10"), &body)
Expect(body.Path).To(HaveLen(2))
Expect(body.TotalDistance).To(Equal(3.5))
})
})
})