playlist-to-session bridge

This commit is contained in:
Dmitry Sergeev
2026-09-01 19:53:01 +03:00
parent a79a2d9c7a
commit ce4ea3fa00
2 changed files with 226 additions and 0 deletions
+37
View File
@@ -23,6 +23,7 @@ type PlaylistState struct {
var ( var (
ErrPlaylistEmpty = errors.New("playlist is empty") ErrPlaylistEmpty = errors.New("playlist is empty")
ErrPlaylistIndexOutOfRange = errors.New("playlist index is out of range") ErrPlaylistIndexOutOfRange = errors.New("playlist index is out of range")
ErrPlaylistNoSelection = errors.New("playlist has no selected entry")
ErrUnknownPlaylistCommand = errors.New("unknown playlist command") ErrUnknownPlaylistCommand = errors.New("unknown playlist command")
) )
@@ -91,3 +92,39 @@ func (s PlaylistState) Entry(playlist Playlist) (PlaylistEntry, bool) {
} }
return playlist.Entries[s.CurrentIndex], true return playlist.Entries[s.CurrentIndex], true
} }
func ApplyPlaylistSelection(
playlist Playlist,
current PlaylistState,
command PlaylistCommand,
retry RetryPolicy,
) (
next PlaylistState,
sessionCommand SessionCommand,
apply bool,
err error,
) {
next, err = ApplyPlaylistCommand(playlist, current, command)
if err != nil {
return current, SessionCommand{}, false, err
}
apply = command.Kind == PlaylistSelect || next != current
if !apply {
return next, SessionCommand{}, false, nil
}
entry, ok := next.Entry(playlist)
if !ok {
return current, SessionCommand{}, false, ErrPlaylistNoSelection
}
session := entry.SessionConfig(retry)
if err := session.Validate(); err != nil {
return current, SessionCommand{}, false, err
}
return next, SessionCommand{
Kind: CommandSetSession,
Session: session,
}, true, nil
}
@@ -0,0 +1,189 @@
package playback
import (
"errors"
"testing"
"time"
)
func TestApplyPlaylistSelectionFirstNext(t *testing.T) {
retry := validPlaylistRetryPolicy()
next, command, apply, err := ApplyPlaylistSelection(
navigationPlaylist(false),
PlaylistState{},
PlaylistCommand{Kind: PlaylistNext},
retry,
)
if err != nil {
t.Fatalf("ApplyPlaylistSelection() error = %v", err)
}
if !apply {
t.Fatal("ApplyPlaylistSelection() apply = false, want true")
}
wantState := PlaylistState{CurrentIndex: 0, HasSelection: true}
if next != wantState {
t.Fatalf("ApplyPlaylistSelection() state = %#v, want %#v", next, wantState)
}
wantCommand := SessionCommand{
Kind: CommandSetSession,
Session: SessionConfig{
Video: FeedConfig{Domain: "domain", UUID: "video-1", Active: true},
Retry: retry,
},
}
if command != wantCommand {
t.Fatalf("ApplyPlaylistSelection() command = %#v, want %#v", command, wantCommand)
}
}
func TestApplyPlaylistSelectionUsesCompleteEntrySession(t *testing.T) {
retry := validPlaylistRetryPolicy()
playlist := Playlist{Entries: []PlaylistEntry{
{
Video: PlaylistFeed{Domain: "video-domain", UUID: "video"},
Audio: PlaylistFeed{Domain: "audio-domain", UUID: "audio"},
SyncRequested: true,
},
{Video: PlaylistFeed{Domain: "next-domain", UUID: "next-video"}},
}}
_, command, apply, err := ApplyPlaylistSelection(
playlist,
PlaylistState{CurrentIndex: 1, HasSelection: true},
PlaylistCommand{Kind: PlaylistSelect, Index: 0},
retry,
)
if err != nil {
t.Fatalf("ApplyPlaylistSelection() error = %v", err)
}
if !apply {
t.Fatal("ApplyPlaylistSelection() apply = false, want true")
}
want := SessionConfig{
Video: FeedConfig{Domain: "video-domain", UUID: "video", Active: true},
Audio: FeedConfig{Domain: "audio-domain", UUID: "audio", Active: true},
SyncRequested: true,
Retry: retry,
}
if command.Kind != CommandSetSession || command.Session != want {
t.Fatalf("ApplyPlaylistSelection() command = %#v, want session %#v", command, want)
}
}
func TestApplyPlaylistSelectionVideoOnlyClearsAudio(t *testing.T) {
_, command, apply, err := ApplyPlaylistSelection(
navigationPlaylist(false),
PlaylistState{CurrentIndex: 1, HasSelection: true},
PlaylistCommand{Kind: PlaylistSelect, Index: 0},
validPlaylistRetryPolicy(),
)
if err != nil {
t.Fatalf("ApplyPlaylistSelection() error = %v", err)
}
if !apply {
t.Fatal("ApplyPlaylistSelection() apply = false, want true")
}
if command.Session.Audio != (FeedConfig{}) {
t.Fatalf("ApplyPlaylistSelection() audio = %#v, want zero value", command.Session.Audio)
}
}
func TestApplyPlaylistSelectionApplicationDecision(t *testing.T) {
tests := []struct {
name string
loop bool
current int
command PlaylistCommand
want int
apply bool
}{
{name: "reselect current", current: 1, command: PlaylistCommand{Kind: PlaylistSelect, Index: 1}, want: 1, apply: true},
{name: "move next", current: 1, command: PlaylistCommand{Kind: PlaylistNext}, want: 2, apply: true},
{name: "next stops at end", current: 2, command: PlaylistCommand{Kind: PlaylistNext}, want: 2},
{name: "previous stops at beginning", current: 0, command: PlaylistCommand{Kind: PlaylistPrevious}, want: 0},
{name: "next wraps", loop: true, current: 2, command: PlaylistCommand{Kind: PlaylistNext}, want: 0, apply: true},
{name: "previous wraps", loop: true, current: 0, command: PlaylistCommand{Kind: PlaylistPrevious}, want: 2, apply: true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
next, command, apply, err := ApplyPlaylistSelection(
navigationPlaylist(test.loop),
PlaylistState{CurrentIndex: test.current, HasSelection: true},
test.command,
validPlaylistRetryPolicy(),
)
if err != nil {
t.Fatalf("ApplyPlaylistSelection() error = %v", err)
}
wantState := PlaylistState{CurrentIndex: test.want, HasSelection: true}
if next != wantState {
t.Fatalf("ApplyPlaylistSelection() state = %#v, want %#v", next, wantState)
}
if apply != test.apply {
t.Fatalf("ApplyPlaylistSelection() apply = %v, want %v", apply, test.apply)
}
if apply && command.Kind != CommandSetSession {
t.Fatalf("ApplyPlaylistSelection() command kind = %v, want %v", command.Kind, CommandSetSession)
}
if !apply && command != (SessionCommand{}) {
t.Fatalf("ApplyPlaylistSelection() command = %#v, want zero value", command)
}
})
}
}
func TestApplyPlaylistSelectionErrorsDoNotApply(t *testing.T) {
current := PlaylistState{CurrentIndex: 1, HasSelection: true}
tests := []struct {
name string
command PlaylistCommand
retry RetryPolicy
wantErr error
}{
{
name: "invalid command",
command: PlaylistCommand{},
retry: validPlaylistRetryPolicy(),
wantErr: ErrUnknownPlaylistCommand,
},
{
name: "invalid retry",
command: PlaylistCommand{Kind: PlaylistNext},
retry: RetryPolicy{},
wantErr: ErrInvalidRetryDelay,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
next, command, apply, err := ApplyPlaylistSelection(
navigationPlaylist(false),
current,
test.command,
test.retry,
)
if !errors.Is(err, test.wantErr) {
t.Fatalf("ApplyPlaylistSelection() error = %v, want %v", err, test.wantErr)
}
if next != current {
t.Fatalf("ApplyPlaylistSelection() state = %#v, want unchanged %#v", next, current)
}
if apply {
t.Fatal("ApplyPlaylistSelection() apply = true, want false")
}
if command != (SessionCommand{}) {
t.Fatalf("ApplyPlaylistSelection() command = %#v, want zero value", command)
}
})
}
}
func validPlaylistRetryPolicy() RetryPolicy {
return RetryPolicy{
MaxAttempts: 3,
InitialDelay: time.Millisecond,
MaxDelay: time.Second,
}
}