Files
vibesonic/internal/subsonic/similar_songs_sonic_test.go
owenandClaude Sonnet 5 2a69f6dc33 Blend real sonic distance into classic getSimilarSongs ranking
similarSongsFor still filters by genre (unchanged, and getSonicSimilarTracks
remains the pure-sonic endpoint), but when the seed song has been
analyzed, genre-matched candidates that also have stored audio_features
are now ordered by actual feature-space distance from the seed instead
of SQL's random order; candidates without analysis data are left at
the end in their existing relative order rather than dropped or given
a fabricated distance.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014r98AtcBbevpscWLsfwVvv
2026-08-23 08:31:54 -05:00

92 lines
2.7 KiB
Go

package subsonic
import (
"testing"
"git.noroutes.net/NoRoutes/vibetunes/internal/audiofeatures"
"git.noroutes.net/NoRoutes/vibetunes/internal/database"
)
func makeGenreSong(t *testing.T, s *Server, folderID, genreID, title string) *database.Song {
t.Helper()
mf, err := database.CreateMediaFile(s.DB, folderID, title+".mp3", 100, 0, "hash-"+title, "mp3", "song", 10, 128, 44100, 2)
if err != nil {
t.Fatal(err)
}
song, err := database.UpsertSong(s.DB, database.UpsertSongInput{MediaFileID: mf.ID, Title: title})
if err != nil {
t.Fatal(err)
}
if err := database.LinkSongGenre(s.DB, song.ID, genreID); err != nil {
t.Fatal(err)
}
return song
}
func TestSimilarSongsForReordersGenreCandidatesBySonicDistance(t *testing.T) {
s, db := newTestServer(t)
folder, err := database.CreateMusicFolder(db, "Music", "/music", "music")
if err != nil {
t.Fatal(err)
}
genre, err := database.FindOrCreateGenre(db, "Rock")
if err != nil {
t.Fatal(err)
}
seed := makeGenreSong(t, s, folder.ID, genre.ID, "Seed")
if err := database.UpsertAudioFeatures(db, seed.ID, &audiofeatures.Features{TempoBPM: 100, TempoValid: true}); err != nil {
t.Fatal(err)
}
near := makeGenreSong(t, s, folder.ID, genre.ID, "Near")
if err := database.UpsertAudioFeatures(db, near.ID, &audiofeatures.Features{TempoBPM: 105, TempoValid: true}); err != nil {
t.Fatal(err)
}
far := makeGenreSong(t, s, folder.ID, genre.ID, "Far")
if err := database.UpsertAudioFeatures(db, far.ID, &audiofeatures.Features{TempoBPM: 200, TempoValid: true}); err != nil {
t.Fatal(err)
}
_ = makeGenreSong(t, s, folder.ID, genre.ID, "Unanalyzed")
songs, err := similarSongsFor(db, seed.ID, 10)
if err != nil {
t.Fatalf("similarSongsFor: %v", err)
}
if len(songs) != 3 {
t.Fatalf("songs = %v, want 3 (near, far, unanalyzed)", songs)
}
pos := map[string]int{}
for i, sg := range songs {
pos[sg.Title] = i
}
if pos["Near"] >= pos["Far"] {
t.Errorf("expected Near (105bpm) to rank before Far (200bpm), got order %v", songs)
}
if pos["Unanalyzed"] != len(songs)-1 {
t.Errorf("expected the unanalyzed candidate last, got order %v", songs)
}
}
func TestSimilarSongsForNoAudioFeaturesLeavesOrderAlone(t *testing.T) {
s, db := newTestServer(t)
folder, err := database.CreateMusicFolder(db, "Music", "/music", "music")
if err != nil {
t.Fatal(err)
}
genre, err := database.FindOrCreateGenre(db, "Rock")
if err != nil {
t.Fatal(err)
}
seed := makeGenreSong(t, s, folder.ID, genre.ID, "Seed")
makeGenreSong(t, s, folder.ID, genre.ID, "Other")
songs, err := similarSongsFor(db, seed.ID, 10)
if err != nil {
t.Fatalf("similarSongsFor: %v", err)
}
if len(songs) != 1 || songs[0].Title != "Other" {
t.Fatalf("songs = %v, want [Other] (no analysis on either side, no reorder crash)", songs)
}
}