Cleanout #4
@@ -17,6 +17,21 @@ func resolveDomain(shared, override string) string {
|
|||||||
return shared
|
return shared
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveRetryPolicy(
|
||||||
|
cli playback.RetryPolicy,
|
||||||
|
cliMaxAttemptsSet bool,
|
||||||
|
playlist playback.Playlist,
|
||||||
|
) playback.RetryPolicy {
|
||||||
|
if playlist.Retry == nil {
|
||||||
|
return cli
|
||||||
|
}
|
||||||
|
resolved := *playlist.Retry
|
||||||
|
if cliMaxAttemptsSet {
|
||||||
|
resolved.MaxAttempts = cli.MaxAttempts
|
||||||
|
}
|
||||||
|
return resolved
|
||||||
|
}
|
||||||
|
|
||||||
func (a appArgs) playbackConfig() playback.SessionConfig {
|
func (a appArgs) playbackConfig() playback.SessionConfig {
|
||||||
return playback.SessionConfig{
|
return playback.SessionConfig{
|
||||||
Video: playback.FeedConfig{
|
Video: playback.FeedConfig{
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"mxl-player/internal/playback"
|
||||||
|
)
|
||||||
|
|
||||||
func TestAppArgsPlaybackConfig(t *testing.T) {
|
func TestAppArgsPlaybackConfig(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -139,3 +144,42 @@ func TestAppArgsPlaybackConfig(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResolveRetryPolicy(t *testing.T) {
|
||||||
|
cli := playback.RetryPolicy{
|
||||||
|
MaxAttempts: 0, InitialDelay: time.Second, MaxDelay: 5 * time.Second,
|
||||||
|
}
|
||||||
|
fileRetry := playback.RetryPolicy{
|
||||||
|
MaxAttempts: 4, InitialDelay: 250 * time.Millisecond, MaxDelay: 2 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
playlist playback.Playlist
|
||||||
|
cliMaxAttemptsSet bool
|
||||||
|
want playback.RetryPolicy
|
||||||
|
}{
|
||||||
|
{name: "no playlist retry uses CLI", want: cli},
|
||||||
|
{
|
||||||
|
name: "playlist retry is used by default",
|
||||||
|
playlist: playback.Playlist{Retry: &fileRetry},
|
||||||
|
want: fileRetry,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "explicit CLI infinite overrides playlist attempts",
|
||||||
|
playlist: playback.Playlist{Retry: &fileRetry},
|
||||||
|
cliMaxAttemptsSet: true,
|
||||||
|
want: playback.RetryPolicy{
|
||||||
|
MaxAttempts: 0, InitialDelay: fileRetry.InitialDelay, MaxDelay: fileRetry.MaxDelay,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
if got := resolveRetryPolicy(cli, test.cliMaxAttemptsSet, test.playlist); got != test.want {
|
||||||
|
t.Fatalf("resolveRetryPolicy() = %#v, want %#v", got, test.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -176,6 +176,11 @@ func main() {
|
|||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
}
|
}
|
||||||
configuredPlaylist = playlist
|
configuredPlaylist = playlist
|
||||||
|
retryPolicy = resolveRetryPolicy(
|
||||||
|
retryPolicy,
|
||||||
|
flagSet.Changed("max-attempts"),
|
||||||
|
configuredPlaylist,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if args.VideoDomain == "" {
|
if args.VideoDomain == "" {
|
||||||
args.VideoDomain = args.Domain
|
args.VideoDomain = args.Domain
|
||||||
@@ -815,6 +820,19 @@ func main() {
|
|||||||
"Failure behavior: %s",
|
"Failure behavior: %s",
|
||||||
configuredPlaylist.OnFailure,
|
configuredPlaylist.OnFailure,
|
||||||
))
|
))
|
||||||
|
if retryPolicy.MaxAttempts == 0 {
|
||||||
|
cimgui.Text("Retries: infinite")
|
||||||
|
} else {
|
||||||
|
cimgui.Text(fmt.Sprintf(
|
||||||
|
"Attempts per entry: %d",
|
||||||
|
retryPolicy.MaxAttempts,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
cimgui.Text(fmt.Sprintf(
|
||||||
|
"Retry delay: %s to %s",
|
||||||
|
retryPolicy.InitialDelay,
|
||||||
|
retryPolicy.MaxDelay,
|
||||||
|
))
|
||||||
|
|
||||||
if hasPlaylistSnapshot && playlistSnapshot.HasFailure {
|
if hasPlaylistSnapshot && playlistSnapshot.HasFailure {
|
||||||
failure := playlistSnapshot.Failure
|
failure := playlistSnapshot.Failure
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ type playlistFile struct {
|
|||||||
Entries []playlistFileEntry `json:"entries"`
|
Entries []playlistFileEntry `json:"entries"`
|
||||||
Loop bool `json:"loop"`
|
Loop bool `json:"loop"`
|
||||||
OnFailure string `json:"on_failure"`
|
OnFailure string `json:"on_failure"`
|
||||||
|
Retry *playlistFileRetry `json:"retry"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type playlistFileRetry struct {
|
||||||
|
MaxAttempts *int `json:"max_attempts"`
|
||||||
|
InitialDelay string `json:"initial_delay"`
|
||||||
|
MaxDelay string `json:"max_delay"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type playlistFileEntry struct {
|
type playlistFileEntry struct {
|
||||||
@@ -73,6 +80,13 @@ func decodePlaylistFile(reader io.Reader) (playback.Playlist, error) {
|
|||||||
"on_failure %q: %w", file.OnFailure, playback.ErrPlaylistFailurePolicy,
|
"on_failure %q: %w", file.OnFailure, playback.ErrPlaylistFailurePolicy,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if file.Retry != nil {
|
||||||
|
retry, err := decodePlaylistRetry(*file.Retry)
|
||||||
|
if err != nil {
|
||||||
|
return playback.Playlist{}, err
|
||||||
|
}
|
||||||
|
playlist.Retry = &retry
|
||||||
|
}
|
||||||
for index, entry := range file.Entries {
|
for index, entry := range file.Entries {
|
||||||
duration := time.Duration(0)
|
duration := time.Duration(0)
|
||||||
if entry.Duration != "" {
|
if entry.Duration != "" {
|
||||||
@@ -103,6 +117,37 @@ func decodePlaylistFile(reader io.Reader) (playback.Playlist, error) {
|
|||||||
return playlist, nil
|
return playlist, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func decodePlaylistRetry(file playlistFileRetry) (playback.RetryPolicy, error) {
|
||||||
|
retry := playback.RetryPolicy{
|
||||||
|
InitialDelay: initialRetryDelay,
|
||||||
|
MaxDelay: maxRetryDelay,
|
||||||
|
}
|
||||||
|
if file.MaxAttempts != nil {
|
||||||
|
retry.MaxAttempts = *file.MaxAttempts
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
if file.InitialDelay != "" {
|
||||||
|
retry.InitialDelay, err = time.ParseDuration(file.InitialDelay)
|
||||||
|
if err != nil {
|
||||||
|
return playback.RetryPolicy{}, fmt.Errorf(
|
||||||
|
"retry initial_delay %q: %w", file.InitialDelay, err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if file.MaxDelay != "" {
|
||||||
|
retry.MaxDelay, err = time.ParseDuration(file.MaxDelay)
|
||||||
|
if err != nil {
|
||||||
|
return playback.RetryPolicy{}, fmt.Errorf(
|
||||||
|
"retry max_delay %q: %w", file.MaxDelay, err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := retry.Validate(); err != nil {
|
||||||
|
return playback.RetryPolicy{}, fmt.Errorf("validate playlist retry: %w", err)
|
||||||
|
}
|
||||||
|
return retry, nil
|
||||||
|
}
|
||||||
|
|
||||||
func playlistFileFeedToPlayback(feed *playlistFileFeed) playback.PlaylistFeed {
|
func playlistFileFeedToPlayback(feed *playlistFileFeed) playback.PlaylistFeed {
|
||||||
if feed == nil {
|
if feed == nil {
|
||||||
return playback.PlaylistFeed{}
|
return playback.PlaylistFeed{}
|
||||||
|
|||||||
@@ -91,6 +91,41 @@ func TestDecodePlaylistFileAllowsEmptyPlaylist(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDecodePlaylistFileRetry(t *testing.T) {
|
||||||
|
input := `{
|
||||||
|
"retry": {
|
||||||
|
"max_attempts": 3,
|
||||||
|
"initial_delay": "250ms",
|
||||||
|
"max_delay": "2s"
|
||||||
|
},
|
||||||
|
"entries": []
|
||||||
|
}`
|
||||||
|
got, err := decodePlaylistFile(strings.NewReader(input))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decodePlaylistFile() error = %v", err)
|
||||||
|
}
|
||||||
|
if got.Retry == nil {
|
||||||
|
t.Fatal("decodePlaylistFile() retry = nil")
|
||||||
|
}
|
||||||
|
want := playback.RetryPolicy{
|
||||||
|
MaxAttempts: 3, InitialDelay: 250 * time.Millisecond, MaxDelay: 2 * time.Second,
|
||||||
|
}
|
||||||
|
if *got.Retry != want {
|
||||||
|
t.Fatalf("retry = %#v, want %#v", *got.Retry, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodePlaylistFileRetryDefaults(t *testing.T) {
|
||||||
|
got, err := decodePlaylistFile(strings.NewReader(`{"retry":{},"entries":[]}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decodePlaylistFile() error = %v", err)
|
||||||
|
}
|
||||||
|
if got.Retry == nil || got.Retry.MaxAttempts != 0 ||
|
||||||
|
got.Retry.InitialDelay != initialRetryDelay || got.Retry.MaxDelay != maxRetryDelay {
|
||||||
|
t.Fatalf("retry = %#v", got.Retry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDecodePlaylistFileRejectsInvalidInput(t *testing.T) {
|
func TestDecodePlaylistFileRejectsInvalidInput(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -103,6 +138,10 @@ func TestDecodePlaylistFileRejectsInvalidInput(t *testing.T) {
|
|||||||
{name: "unknown field", input: `{"unknown": true}`, wantText: "unknown field"},
|
{name: "unknown field", input: `{"unknown": true}`, wantText: "unknown field"},
|
||||||
{name: "multiple roots", input: `{"entries": []} {"entries": []}`, wantText: "multiple root values"},
|
{name: "multiple roots", input: `{"entries": []} {"entries": []}`, wantText: "multiple root values"},
|
||||||
{name: "invalid failure policy", input: `{"on_failure":"skip","entries":[]}`, wantErr: playback.ErrPlaylistFailurePolicy},
|
{name: "invalid failure policy", input: `{"on_failure":"skip","entries":[]}`, wantErr: playback.ErrPlaylistFailurePolicy},
|
||||||
|
{name: "negative retry attempts", input: `{"retry":{"max_attempts":-1},"entries":[]}`, wantErr: playback.ErrInvalidMaxAttempts},
|
||||||
|
{name: "invalid initial delay", input: `{"retry":{"initial_delay":"soon"},"entries":[]}`, wantText: "retry initial_delay"},
|
||||||
|
{name: "invalid maximum delay", input: `{"retry":{"max_delay":"later"},"entries":[]}`, wantText: "retry max_delay"},
|
||||||
|
{name: "invalid retry range", input: `{"retry":{"initial_delay":"2s","max_delay":"1s"},"entries":[]}`, wantErr: playback.ErrInvalidRetryRange},
|
||||||
{
|
{
|
||||||
name: "invalid duration",
|
name: "invalid duration",
|
||||||
input: `{"entries":[{"video":{"domain":"/video","uuid":"video"},"duration":"later"}]}`,
|
input: `{"entries":[{"video":{"domain":"/video","uuid":"video"},"duration":"later"}]}`,
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ type Playlist struct {
|
|||||||
Entries []PlaylistEntry
|
Entries []PlaylistEntry
|
||||||
Loop bool
|
Loop bool
|
||||||
OnFailure PlaylistFailurePolicy
|
OnFailure PlaylistFailurePolicy
|
||||||
|
Retry *RetryPolicy
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f PlaylistFeed) IsConfigured() bool {
|
func (f PlaylistFeed) IsConfigured() bool {
|
||||||
@@ -95,6 +96,11 @@ func (p Playlist) Validate() error {
|
|||||||
if err := p.OnFailure.Validate(); err != nil {
|
if err := p.OnFailure.Validate(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if p.Retry != nil {
|
||||||
|
if err := p.Retry.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("playlist retry: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
for index, entry := range p.Entries {
|
for index, entry := range p.Entries {
|
||||||
if err := entry.Validate(); err != nil {
|
if err := entry.Validate(); err != nil {
|
||||||
return fmt.Errorf("playlist entry %d: %w", index, err)
|
return fmt.Errorf("playlist entry %d: %w", index, err)
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
{
|
{
|
||||||
"loop": true,
|
"loop": true,
|
||||||
"on_failure": "next",
|
"on_failure": "next",
|
||||||
|
"retry": {
|
||||||
|
"max_attempts": 3,
|
||||||
|
"initial_delay": "500ms",
|
||||||
|
"max_delay": "5s"
|
||||||
|
},
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
{
|
||||||
"name": "timelapse",
|
"name": "timelapse",
|
||||||
|
|||||||
Reference in New Issue
Block a user