pure playlist navigation
This commit is contained in:
@@ -0,0 +1,93 @@
|
|||||||
|
package playback
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
type PlaylistCommandKind uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
PlaylistSelect PlaylistCommandKind = iota + 1
|
||||||
|
PlaylistNext
|
||||||
|
PlaylistPrevious
|
||||||
|
)
|
||||||
|
|
||||||
|
type PlaylistCommand struct {
|
||||||
|
Kind PlaylistCommandKind
|
||||||
|
Index int
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlaylistState struct {
|
||||||
|
CurrentIndex int
|
||||||
|
HasSelection bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrPlaylistEmpty = errors.New("playlist is empty")
|
||||||
|
ErrPlaylistIndexOutOfRange = errors.New("playlist index is out of range")
|
||||||
|
ErrUnknownPlaylistCommand = errors.New("unknown playlist command")
|
||||||
|
)
|
||||||
|
|
||||||
|
func ApplyPlaylistCommand(
|
||||||
|
playlist Playlist,
|
||||||
|
current PlaylistState,
|
||||||
|
command PlaylistCommand,
|
||||||
|
) (PlaylistState, error) {
|
||||||
|
if err := playlist.Validate(); err != nil {
|
||||||
|
return current, err
|
||||||
|
}
|
||||||
|
if len(playlist.Entries) == 0 {
|
||||||
|
return current, ErrPlaylistEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
lastIndex := len(playlist.Entries) - 1
|
||||||
|
switch command.Kind {
|
||||||
|
case PlaylistSelect:
|
||||||
|
if command.Index < 0 || command.Index > lastIndex {
|
||||||
|
return current, ErrPlaylistIndexOutOfRange
|
||||||
|
}
|
||||||
|
return PlaylistState{CurrentIndex: command.Index, HasSelection: true}, nil
|
||||||
|
|
||||||
|
case PlaylistNext:
|
||||||
|
if !current.HasSelection {
|
||||||
|
return PlaylistState{CurrentIndex: 0, HasSelection: true}, nil
|
||||||
|
}
|
||||||
|
if current.CurrentIndex < 0 || current.CurrentIndex > lastIndex {
|
||||||
|
return current, ErrPlaylistIndexOutOfRange
|
||||||
|
}
|
||||||
|
if current.CurrentIndex == lastIndex {
|
||||||
|
if playlist.Loop {
|
||||||
|
return PlaylistState{CurrentIndex: 0, HasSelection: true}, nil
|
||||||
|
}
|
||||||
|
return current, nil
|
||||||
|
}
|
||||||
|
return PlaylistState{CurrentIndex: current.CurrentIndex + 1, HasSelection: true}, nil
|
||||||
|
|
||||||
|
case PlaylistPrevious:
|
||||||
|
if !current.HasSelection {
|
||||||
|
index := 0
|
||||||
|
if playlist.Loop {
|
||||||
|
index = lastIndex
|
||||||
|
}
|
||||||
|
return PlaylistState{CurrentIndex: index, HasSelection: true}, nil
|
||||||
|
}
|
||||||
|
if current.CurrentIndex < 0 || current.CurrentIndex > lastIndex {
|
||||||
|
return current, ErrPlaylistIndexOutOfRange
|
||||||
|
}
|
||||||
|
if current.CurrentIndex == 0 {
|
||||||
|
if playlist.Loop {
|
||||||
|
return PlaylistState{CurrentIndex: lastIndex, HasSelection: true}, nil
|
||||||
|
}
|
||||||
|
return current, nil
|
||||||
|
}
|
||||||
|
return PlaylistState{CurrentIndex: current.CurrentIndex - 1, HasSelection: true}, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return current, ErrUnknownPlaylistCommand
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s PlaylistState) Entry(playlist Playlist) (PlaylistEntry, bool) {
|
||||||
|
if !s.HasSelection || s.CurrentIndex < 0 || s.CurrentIndex >= len(playlist.Entries) {
|
||||||
|
return PlaylistEntry{}, false
|
||||||
|
}
|
||||||
|
return playlist.Entries[s.CurrentIndex], true
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
package playback
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func navigationPlaylist(loop bool) Playlist {
|
||||||
|
return Playlist{
|
||||||
|
Entries: []PlaylistEntry{
|
||||||
|
{Name: "first", Video: PlaylistFeed{Domain: "domain", UUID: "video-1"}},
|
||||||
|
{Name: "second", Audio: PlaylistFeed{Domain: "domain", UUID: "audio-2"}},
|
||||||
|
{Name: "third", Video: PlaylistFeed{Domain: "domain", UUID: "video-3"}},
|
||||||
|
},
|
||||||
|
Loop: loop,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyPlaylistCommandSelect(t *testing.T) {
|
||||||
|
current := PlaylistState{CurrentIndex: 1, HasSelection: true}
|
||||||
|
|
||||||
|
got, err := ApplyPlaylistCommand(
|
||||||
|
navigationPlaylist(false),
|
||||||
|
current,
|
||||||
|
PlaylistCommand{Kind: PlaylistSelect, Index: 2},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ApplyPlaylistCommand() error = %v", err)
|
||||||
|
}
|
||||||
|
want := PlaylistState{CurrentIndex: 2, HasSelection: true}
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("ApplyPlaylistCommand() = %#v, want %#v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyPlaylistCommandWithoutSelection(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
loop bool
|
||||||
|
command PlaylistCommandKind
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{name: "next", command: PlaylistNext, want: 0},
|
||||||
|
{name: "previous without loop", command: PlaylistPrevious, want: 0},
|
||||||
|
{name: "previous with loop", loop: true, command: PlaylistPrevious, want: 2},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
got, err := ApplyPlaylistCommand(
|
||||||
|
navigationPlaylist(test.loop),
|
||||||
|
PlaylistState{},
|
||||||
|
PlaylistCommand{Kind: test.command},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ApplyPlaylistCommand() error = %v", err)
|
||||||
|
}
|
||||||
|
want := PlaylistState{CurrentIndex: test.want, HasSelection: true}
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("ApplyPlaylistCommand() = %#v, want %#v", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyPlaylistCommandNavigation(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
loop bool
|
||||||
|
current int
|
||||||
|
command PlaylistCommandKind
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{name: "next", current: 1, command: PlaylistNext, want: 2},
|
||||||
|
{name: "previous", current: 1, command: PlaylistPrevious, want: 0},
|
||||||
|
{name: "next stops at end", current: 2, command: PlaylistNext, want: 2},
|
||||||
|
{name: "previous stops at beginning", current: 0, command: PlaylistPrevious, want: 0},
|
||||||
|
{name: "next wraps", loop: true, current: 2, command: PlaylistNext, want: 0},
|
||||||
|
{name: "previous wraps", loop: true, current: 0, command: PlaylistPrevious, want: 2},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
current := PlaylistState{CurrentIndex: test.current, HasSelection: true}
|
||||||
|
got, err := ApplyPlaylistCommand(
|
||||||
|
navigationPlaylist(test.loop),
|
||||||
|
current,
|
||||||
|
PlaylistCommand{Kind: test.command},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ApplyPlaylistCommand() error = %v", err)
|
||||||
|
}
|
||||||
|
want := PlaylistState{CurrentIndex: test.want, HasSelection: true}
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("ApplyPlaylistCommand() = %#v, want %#v", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyPlaylistCommandErrorsLeaveStateUnchanged(t *testing.T) {
|
||||||
|
current := PlaylistState{CurrentIndex: 1, HasSelection: true}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
playlist Playlist
|
||||||
|
command PlaylistCommand
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty playlist",
|
||||||
|
playlist: Playlist{},
|
||||||
|
command: PlaylistCommand{Kind: PlaylistNext},
|
||||||
|
wantErr: ErrPlaylistEmpty,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative selection",
|
||||||
|
playlist: navigationPlaylist(false),
|
||||||
|
command: PlaylistCommand{Kind: PlaylistSelect, Index: -1},
|
||||||
|
wantErr: ErrPlaylistIndexOutOfRange,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "selection past end",
|
||||||
|
playlist: navigationPlaylist(false),
|
||||||
|
command: PlaylistCommand{Kind: PlaylistSelect, Index: 3},
|
||||||
|
wantErr: ErrPlaylistIndexOutOfRange,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "stale current index",
|
||||||
|
playlist: navigationPlaylist(false),
|
||||||
|
command: PlaylistCommand{Kind: PlaylistNext},
|
||||||
|
wantErr: ErrPlaylistIndexOutOfRange,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unknown command",
|
||||||
|
playlist: navigationPlaylist(false),
|
||||||
|
command: PlaylistCommand{},
|
||||||
|
wantErr: ErrUnknownPlaylistCommand,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid playlist",
|
||||||
|
playlist: Playlist{Entries: []PlaylistEntry{
|
||||||
|
{},
|
||||||
|
}},
|
||||||
|
command: PlaylistCommand{Kind: PlaylistNext},
|
||||||
|
wantErr: ErrPlaylistEntryEmpty,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
state := current
|
||||||
|
if test.name == "stale current index" {
|
||||||
|
state.CurrentIndex = 4
|
||||||
|
}
|
||||||
|
got, err := ApplyPlaylistCommand(test.playlist, state, test.command)
|
||||||
|
if !errors.Is(err, test.wantErr) {
|
||||||
|
t.Fatalf("ApplyPlaylistCommand() error = %v, want %v", err, test.wantErr)
|
||||||
|
}
|
||||||
|
if got != state {
|
||||||
|
t.Fatalf("ApplyPlaylistCommand() = %#v, want unchanged %#v", got, state)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlaylistStateEntry(t *testing.T) {
|
||||||
|
playlist := navigationPlaylist(false)
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
state PlaylistState
|
||||||
|
want string
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{name: "no selection", state: PlaylistState{}},
|
||||||
|
{name: "selected", state: PlaylistState{CurrentIndex: 1, HasSelection: true}, want: "second", ok: true},
|
||||||
|
{name: "negative stale index", state: PlaylistState{CurrentIndex: -1, HasSelection: true}},
|
||||||
|
{name: "stale index", state: PlaylistState{CurrentIndex: 3, HasSelection: true}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
entry, ok := test.state.Entry(playlist)
|
||||||
|
if ok != test.ok {
|
||||||
|
t.Fatalf("Entry() ok = %v, want %v", ok, test.ok)
|
||||||
|
}
|
||||||
|
if entry.Name != test.want {
|
||||||
|
t.Fatalf("Entry() name = %q, want %q", entry.Name, test.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user