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
92 lines
2.7 KiB
Go
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)
|
|
}
|
|
}
|