🏡 index : quine-global/quintodrome.git

package jellyfin

import (
	"net/http"
	"strings"

	"github.com/navidrome/navidrome/consts"
	"github.com/navidrome/navidrome/model/request"
	"github.com/navidrome/navidrome/server/jellyfin/dto"
	"github.com/navidrome/navidrome/utils/req"
)

// audioMuseEndpoints is what /AudioMuseAI/info advertises; it omits info itself, like the plugin,
// and is sorted the same way (the plugin builds it with OrderBy).
var audioMuseEndpoints = []string{
	"GET /AudioMuseAI/find_path",
	"GET /AudioMuseAI/health",
	"GET /AudioMuseAI/similar_tracks",
}

type audioMuseInfoResponse struct {
	Version            string   `json:"Version"`
	AvailableEndpoints []string `json:"AvailableEndpoints"`
}

func (api *Router) audioMuseInfo(w http.ResponseWriter, r *http.Request) {
	endpoints := []string{} // non-nil so an empty list serializes as [], not null
	if api.sonic != nil && api.sonic.HasProvider() {
		endpoints = audioMuseEndpoints
	}
	api.ok(w, r, audioMuseInfoResponse{
		Version:            consts.Version,
		AvailableEndpoints: endpoints,
	})
}

// audioMuseHealth is a liveness probe: 200 with an empty body when a sonic provider is loaded, else
// 404 — mirroring the reference plugin, which returns 200 when its backend is reachable.
func (api *Router) audioMuseHealth(w http.ResponseWriter, r *http.Request) {
	if api.sonic == nil || !api.sonic.HasProvider() {
		api.notFound(w, r)
		return
	}
	w.WriteHeader(http.StatusOK)
}

type audioMuseSimilarTrack struct {
	Author   string  `json:"author"`
	Distance float64 `json:"distance"`
	ItemID   string  `json:"item_id"`
	Title    string  `json:"title"`
}

func (api *Router) audioMuseSimilarTracks(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	// 404 without a provider, like the Subsonic sonicSimilarity handlers.
	if api.sonic == nil || !api.sonic.HasProvider() {
		api.notFound(w, r)
		return
	}
	p := req.Params(r)
	tracks := []audioMuseSimilarTrack{}

	itemID := p.StringOr("item_id", "")
	id, ok := dto.DecodeID(itemID)
	if !ok {
		// Every other failure here (no provider, no match) degrades to an empty list rather than an
		// error, so a malformed id does too instead of being the one path that 404s.
		api.ok(w, r, tracks)
		return
	}
	n := min(p.IntOr("n", 10), maxSimilarLimit) // cap a user-controlled count, like clampLimit
	eliminateDuplicates := p.BoolOr("eliminate_duplicates", true)

	matches, err := api.sonic.GetSonicSimilarTracks(ctx, id, n)
	if err != nil {
		api.ok(w, r, tracks)
		return
	}

	u, _ := request.UserFrom(ctx)
	seenArtists := make(map[string]bool, len(matches))
	for _, m := range matches {
		mf := m.MediaFile
		if !u.HasLibraryAccess(mf.LibraryID) {
			continue
		}
		if eliminateDuplicates {
			key := strings.ToLower(mf.Artist)
			if seenArtists[key] {
				continue
			}
			seenArtists[key] = true
		}
		tracks = append(tracks, audioMuseSimilarTrack{
			Author:   mf.Artist,
			Distance: m.Similarity,
			ItemID:   dto.EncodeID(mf.ID),
			Title:    mf.Title,
		})
	}
	api.ok(w, r, tracks)
}

type audioMusePathTrack struct {
	Author string   `json:"author"`
	ItemID string   `json:"item_id"`
	Title  string   `json:"title"`
	Tempo  *float64 `json:"tempo,omitempty"`
}

type audioMusePathResponse struct {
	Path          []audioMusePathTrack `json:"path"`
	TotalDistance float64              `json:"total_distance"`
}

func (api *Router) audioMuseFindPath(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	if api.sonic == nil || !api.sonic.HasProvider() {
		api.notFound(w, r)
		return
	}
	p := req.Params(r)

	startID := p.StringOr("start_song_id", "")
	endID := p.StringOr("end_song_id", "")
	if startID == "" || endID == "" {
		http.Error(w, "start_song_id and end_song_id are required.", http.StatusBadRequest)
		return
	}

	resp := audioMusePathResponse{Path: []audioMusePathTrack{}}
	startDecoded, startOk := dto.DecodeID(startID)
	endDecoded, endOk := dto.DecodeID(endID)
	if !startOk || !endOk {
		// This endpoint already 400s when an endpoint id is absent, so an unusable one reports the
		// same way rather than looking like a successful search that found no path.
		http.Error(w, "start_song_id and end_song_id must be valid item ids.", http.StatusBadRequest)
		return
	}
	maxSteps := min(p.IntOr("max_steps", 25), maxSimilarLimit) // cap a user-controlled count
	matches, err := api.sonic.FindSonicPath(ctx, startDecoded, endDecoded, maxSteps)
	if err != nil {
		api.ok(w, r, resp)
		return
	}

	u, _ := request.UserFrom(ctx)
	for _, m := range matches {
		mf := m.MediaFile
		if !u.HasLibraryAccess(mf.LibraryID) {
			continue
		}
		track := audioMusePathTrack{
			Author: mf.Artist,
			ItemID: dto.EncodeID(mf.ID),
			Title:  mf.Title,
		}
		if mf.BPM != nil {
			tempo := float64(*mf.BPM)
			track.Tempo = &tempo
		}
		resp.Path = append(resp.Path, track)
		resp.TotalDistance += m.Similarity
	}
	api.ok(w, r, resp)
}