Playlist-level retry configuration
This commit is contained in:
@@ -17,6 +17,21 @@ func resolveDomain(shared, override string) string {
|
||||
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 {
|
||||
return playback.SessionConfig{
|
||||
Video: playback.FeedConfig{
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"mxl-player/internal/playback"
|
||||
)
|
||||
|
||||
func TestAppArgsPlaybackConfig(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
configuredPlaylist = playlist
|
||||
retryPolicy = resolveRetryPolicy(
|
||||
retryPolicy,
|
||||
flagSet.Changed("max-attempts"),
|
||||
configuredPlaylist,
|
||||
)
|
||||
}
|
||||
if args.VideoDomain == "" {
|
||||
args.VideoDomain = args.Domain
|
||||
@@ -815,6 +820,19 @@ func main() {
|
||||
"Failure behavior: %s",
|
||||
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 {
|
||||
failure := playlistSnapshot.Failure
|
||||
|
||||
@@ -14,6 +14,13 @@ type playlistFile struct {
|
||||
Entries []playlistFileEntry `json:"entries"`
|
||||
Loop bool `json:"loop"`
|
||||
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 {
|
||||
@@ -73,6 +80,13 @@ func decodePlaylistFile(reader io.Reader) (playback.Playlist, error) {
|
||||
"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 {
|
||||
duration := time.Duration(0)
|
||||
if entry.Duration != "" {
|
||||
@@ -103,6 +117,37 @@ func decodePlaylistFile(reader io.Reader) (playback.Playlist, error) {
|
||||
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 {
|
||||
if feed == nil {
|
||||
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) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -103,6 +138,10 @@ func TestDecodePlaylistFileRejectsInvalidInput(t *testing.T) {
|
||||
{name: "unknown field", input: `{"unknown": true}`, wantText: "unknown field"},
|
||||
{name: "multiple roots", input: `{"entries": []} {"entries": []}`, wantText: "multiple root values"},
|
||||
{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",
|
||||
input: `{"entries":[{"video":{"domain":"/video","uuid":"video"},"duration":"later"}]}`,
|
||||
|
||||
Reference in New Issue
Block a user