playlist model
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
package playback
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrPlaylistFeedUUIDRequired = errors.New("playlist feed UUID is required when its domain is configured")
|
||||
ErrPlaylistEntryEmpty = errors.New("playlist entry must contain at least one feed")
|
||||
ErrPlaylistSyncFeedsRequired = errors.New("synchronized playlist entry requires both video and audio feeds")
|
||||
ErrPlaylistDurationNegative = errors.New("playlist entry duration cannot be negative")
|
||||
)
|
||||
|
||||
type PlaylistFeed struct {
|
||||
Domain string
|
||||
UUID string
|
||||
}
|
||||
|
||||
type PlaylistEntry struct {
|
||||
Name string
|
||||
Video PlaylistFeed
|
||||
Audio PlaylistFeed
|
||||
SyncRequested bool
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
type Playlist struct {
|
||||
Entries []PlaylistEntry
|
||||
Loop bool
|
||||
}
|
||||
|
||||
func (f PlaylistFeed) IsConfigured() bool {
|
||||
return f.UUID != ""
|
||||
}
|
||||
|
||||
func (f PlaylistFeed) Validate() error {
|
||||
if f.UUID != "" && f.Domain == "" {
|
||||
return ErrFeedDomainRequired
|
||||
}
|
||||
if f.Domain != "" && f.UUID == "" {
|
||||
return ErrPlaylistFeedUUIDRequired
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e PlaylistEntry) Validate() error {
|
||||
if err := e.Video.Validate(); err != nil {
|
||||
return fmt.Errorf("video: %w", err)
|
||||
}
|
||||
if err := e.Audio.Validate(); err != nil {
|
||||
return fmt.Errorf("audio: %w", err)
|
||||
}
|
||||
if !e.Video.IsConfigured() && !e.Audio.IsConfigured() {
|
||||
return ErrPlaylistEntryEmpty
|
||||
}
|
||||
if e.SyncRequested && (!e.Video.IsConfigured() || !e.Audio.IsConfigured()) {
|
||||
return ErrPlaylistSyncFeedsRequired
|
||||
}
|
||||
if e.Duration < 0 {
|
||||
return ErrPlaylistDurationNegative
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p Playlist) Validate() error {
|
||||
for index, entry := range p.Entries {
|
||||
if err := entry.Validate(); err != nil {
|
||||
return fmt.Errorf("playlist entry %d: %w", index, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e PlaylistEntry) SessionConfig(retry RetryPolicy) SessionConfig {
|
||||
return SessionConfig{
|
||||
Video: FeedConfig{
|
||||
Domain: e.Video.Domain,
|
||||
UUID: e.Video.UUID,
|
||||
Active: e.Video.UUID != "",
|
||||
},
|
||||
Audio: FeedConfig{
|
||||
Domain: e.Audio.Domain,
|
||||
UUID: e.Audio.UUID,
|
||||
Active: e.Audio.UUID != "",
|
||||
},
|
||||
SyncRequested: e.SyncRequested,
|
||||
Retry: retry,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package playback
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPlaylistEntryValidate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
entry PlaylistEntry
|
||||
wantErr error
|
||||
}{
|
||||
{name: "video only", entry: PlaylistEntry{Video: PlaylistFeed{Domain: "video-domain", UUID: "video"}}},
|
||||
{name: "audio only", entry: PlaylistEntry{Audio: PlaylistFeed{Domain: "audio-domain", UUID: "audio"}}},
|
||||
{
|
||||
name: "independent feeds from different domains",
|
||||
entry: PlaylistEntry{
|
||||
Video: PlaylistFeed{Domain: "video-domain", UUID: "video"},
|
||||
Audio: PlaylistFeed{Domain: "audio-domain", UUID: "audio"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "synchronized pair",
|
||||
entry: PlaylistEntry{
|
||||
Video: PlaylistFeed{Domain: "domain", UUID: "video"},
|
||||
Audio: PlaylistFeed{Domain: "domain", UUID: "audio"},
|
||||
SyncRequested: true,
|
||||
Duration: 10 * time.Second,
|
||||
},
|
||||
},
|
||||
{name: "zero duration", entry: PlaylistEntry{Video: PlaylistFeed{Domain: "domain", UUID: "video"}}},
|
||||
{name: "empty entry", entry: PlaylistEntry{}, wantErr: ErrPlaylistEntryEmpty},
|
||||
{
|
||||
name: "video UUID without domain",
|
||||
entry: PlaylistEntry{Video: PlaylistFeed{UUID: "video"}},
|
||||
wantErr: ErrFeedDomainRequired,
|
||||
},
|
||||
{
|
||||
name: "audio domain without UUID",
|
||||
entry: PlaylistEntry{Audio: PlaylistFeed{Domain: "audio-domain"}},
|
||||
wantErr: ErrPlaylistFeedUUIDRequired,
|
||||
},
|
||||
{
|
||||
name: "sync without audio",
|
||||
entry: PlaylistEntry{
|
||||
Video: PlaylistFeed{Domain: "domain", UUID: "video"},
|
||||
SyncRequested: true,
|
||||
},
|
||||
wantErr: ErrPlaylistSyncFeedsRequired,
|
||||
},
|
||||
{
|
||||
name: "sync without video",
|
||||
entry: PlaylistEntry{
|
||||
Audio: PlaylistFeed{Domain: "domain", UUID: "audio"},
|
||||
SyncRequested: true,
|
||||
},
|
||||
wantErr: ErrPlaylistSyncFeedsRequired,
|
||||
},
|
||||
{
|
||||
name: "negative duration",
|
||||
entry: PlaylistEntry{
|
||||
Video: PlaylistFeed{Domain: "domain", UUID: "video"},
|
||||
Duration: -time.Second,
|
||||
},
|
||||
wantErr: ErrPlaylistDurationNegative,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
err := test.entry.Validate()
|
||||
if !errors.Is(err, test.wantErr) {
|
||||
t.Fatalf("Validate() error = %v, want %v", err, test.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaylistEntrySessionConfig(t *testing.T) {
|
||||
retry := RetryPolicy{MaxAttempts: 3, InitialDelay: time.Second, MaxDelay: 5 * time.Second}
|
||||
entry := PlaylistEntry{
|
||||
Video: PlaylistFeed{Domain: "video-domain", UUID: "video"},
|
||||
Audio: PlaylistFeed{Domain: "audio-domain", UUID: "audio"},
|
||||
SyncRequested: true,
|
||||
Duration: 10 * time.Second,
|
||||
}
|
||||
|
||||
got := entry.SessionConfig(retry)
|
||||
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 got != want {
|
||||
t.Fatalf("SessionConfig() = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaylistEntrySessionConfigLeavesMissingFeedInactive(t *testing.T) {
|
||||
entry := PlaylistEntry{Video: PlaylistFeed{Domain: "domain", UUID: "video"}}
|
||||
|
||||
got := entry.SessionConfig(RetryPolicy{})
|
||||
if !got.Video.Active {
|
||||
t.Fatal("SessionConfig() video is inactive, want active")
|
||||
}
|
||||
if got.Audio != (FeedConfig{}) {
|
||||
t.Fatalf("SessionConfig() audio = %#v, want zero value", got.Audio)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaylistValidate(t *testing.T) {
|
||||
valid := PlaylistEntry{Video: PlaylistFeed{Domain: "domain", UUID: "video"}}
|
||||
tests := []struct {
|
||||
name string
|
||||
playlist Playlist
|
||||
wantErr error
|
||||
}{
|
||||
{name: "empty playlist", playlist: Playlist{}},
|
||||
{name: "valid entries", playlist: Playlist{Entries: []PlaylistEntry{valid, valid}, Loop: true}},
|
||||
{
|
||||
name: "invalid entry",
|
||||
playlist: Playlist{Entries: []PlaylistEntry{
|
||||
valid,
|
||||
{Audio: PlaylistFeed{UUID: "audio"}},
|
||||
}},
|
||||
wantErr: ErrFeedDomainRequired,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
err := test.playlist.Validate()
|
||||
if !errors.Is(err, test.wantErr) {
|
||||
t.Fatalf("Validate() error = %v, want %v", err, test.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaylistValidateReportsEntryIndex(t *testing.T) {
|
||||
playlist := Playlist{Entries: []PlaylistEntry{
|
||||
{Video: PlaylistFeed{Domain: "domain", UUID: "video"}},
|
||||
{},
|
||||
}}
|
||||
|
||||
err := playlist.Validate()
|
||||
if err == nil || err.Error() != "playlist entry 1: playlist entry must contain at least one feed" {
|
||||
t.Fatalf("Validate() error = %v, want indexed entry error", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user