package jukebox import ( "context" "os/exec" "path/filepath" "testing" "time" ) func genFile(t *testing.T, dir, name string) string { t.Helper() if _, err := exec.LookPath("ffmpeg"); err != nil { t.Skip("ffmpeg not installed") } out := filepath.Join(dir, name) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() cmd := exec.CommandContext(ctx, "ffmpeg", "-f", "lavfi", "-i", "sine=frequency=440:duration=3", "-codec:a", "libmp3lame", out) if combined, err := cmd.CombinedOutput(); err != nil { t.Fatalf("ffmpeg: %v\n%s", err, combined) } return out } func requireMPV(t *testing.T) { t.Helper() if _, err := exec.LookPath("mpv"); err != nil { t.Skip("mpv not installed") } } func TestNewUnavailableWithoutMPV(t *testing.T) { j := New("/nonexistent/mpv/binary") if j.Available { t.Fatal("Available = true for a nonexistent mpv binary") } if _, err := j.Status(); err != ErrUnavailable { t.Fatalf("Status() error = %v, want ErrUnavailable", err) } } func TestJukeboxSetAndStatus(t *testing.T) { requireMPV(t) dir := t.TempDir() song1 := genFile(t, dir, "song1.mp3") song2 := genFile(t, dir, "song2.mp3") j := New("") defer j.Close() if !j.Available { t.Fatal("expected jukebox to be available with mpv installed") } paths := map[string]string{"s1": song1, "s2": song2} resolve := func(id string) (string, error) { return paths[id], nil } state, err := j.Set([]string{"s1", "s2"}, resolve) if err != nil { t.Fatalf("Set: %v", err) } if len(state.SongIDs) != 2 { t.Fatalf("SongIDs = %v, want 2", state.SongIDs) } // Give mpv a beat to actually start playback. time.Sleep(300 * time.Millisecond) state, err = j.Status() if err != nil { t.Fatalf("Status: %v", err) } if !state.Playing { t.Error("expected jukebox to be playing after Set") } } func TestJukeboxStartStop(t *testing.T) { requireMPV(t) dir := t.TempDir() song1 := genFile(t, dir, "song1.mp3") j := New("") defer j.Close() resolve := func(id string) (string, error) { return song1, nil } if _, err := j.Set([]string{"s1"}, resolve); err != nil { t.Fatalf("Set: %v", err) } time.Sleep(200 * time.Millisecond) state, err := j.Stop() if err != nil { t.Fatalf("Stop: %v", err) } if state.Playing { t.Error("expected paused after Stop") } state, err = j.Start() if err != nil { t.Fatalf("Start: %v", err) } time.Sleep(200 * time.Millisecond) state, err = j.Status() if err != nil { t.Fatalf("Status: %v", err) } if !state.Playing { t.Error("expected playing after Start") } } func TestJukeboxAddClearRemove(t *testing.T) { requireMPV(t) dir := t.TempDir() song1 := genFile(t, dir, "song1.mp3") song2 := genFile(t, dir, "song2.mp3") j := New("") defer j.Close() paths := map[string]string{"s1": song1, "s2": song2} resolve := func(id string) (string, error) { return paths[id], nil } if _, err := j.Set([]string{"s1"}, resolve); err != nil { t.Fatalf("Set: %v", err) } state, err := j.Add([]string{"s2"}, resolve) if err != nil { t.Fatalf("Add: %v", err) } if len(state.SongIDs) != 2 { t.Fatalf("SongIDs after Add = %v, want 2", state.SongIDs) } state, err = j.Remove(0) if err != nil { t.Fatalf("Remove: %v", err) } if len(state.SongIDs) != 1 || state.SongIDs[0] != "s2" { t.Fatalf("SongIDs after Remove(0) = %v, want [s2]", state.SongIDs) } state, err = j.Clear() if err != nil { t.Fatalf("Clear: %v", err) } if len(state.SongIDs) != 0 { t.Fatalf("SongIDs after Clear = %v, want empty", state.SongIDs) } } func TestJukeboxSetGain(t *testing.T) { requireMPV(t) dir := t.TempDir() song1 := genFile(t, dir, "song1.mp3") j := New("") defer j.Close() resolve := func(id string) (string, error) { return song1, nil } if _, err := j.Set([]string{"s1"}, resolve); err != nil { t.Fatalf("Set: %v", err) } state, err := j.SetGain(0.5) if err != nil { t.Fatalf("SetGain: %v", err) } if state.Gain != 0.5 { t.Fatalf("Gain = %v, want 0.5", state.Gain) } } func TestJukeboxShuffle(t *testing.T) { requireMPV(t) dir := t.TempDir() song1 := genFile(t, dir, "song1.mp3") song2 := genFile(t, dir, "song2.mp3") j := New("") defer j.Close() paths := map[string]string{"s1": song1, "s2": song2} resolve := func(id string) (string, error) { return paths[id], nil } if _, err := j.Set([]string{"s1", "s2"}, resolve); err != nil { t.Fatalf("Set: %v", err) } state, err := j.Shuffle(resolve) if err != nil { t.Fatalf("Shuffle: %v", err) } if len(state.SongIDs) != 2 { t.Fatalf("SongIDs after Shuffle = %v, want 2 entries preserved", state.SongIDs) } } func TestJukeboxSkipTo(t *testing.T) { requireMPV(t) dir := t.TempDir() song1 := genFile(t, dir, "song1.mp3") song2 := genFile(t, dir, "song2.mp3") j := New("") defer j.Close() paths := map[string]string{"s1": song1, "s2": song2} resolve := func(id string) (string, error) { return paths[id], nil } if _, err := j.Set([]string{"s1", "s2"}, resolve); err != nil { t.Fatalf("Set: %v", err) } state, err := j.SkipTo(1, 0) if err != nil { t.Fatalf("SkipTo: %v", err) } if state.CurrentIndex != 1 { t.Fatalf("CurrentIndex = %d, want 1", state.CurrentIndex) } }