diff --git a/README.md b/README.md index 8294e20c..1e26a564 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ vimeo = "{VIMEO_API_TOKEN}" quality = "high" # or "low" format = "video" # or "audio" cover_art = "{IMAGE_URL}" # Optional URL address of an image file + max_height = "720" # Optional maximal height of video, example: 720, 1080, 1440, 2160, ... ``` Episodes files will be kept at: `/path/to/data/directory/ID1`, feed will be accessible from: `http://localhost/ID1.xml` diff --git a/pkg/config/config.go b/pkg/config/config.go index 654cf0c8..5eb21256 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -26,6 +26,8 @@ type Feed struct { UpdatePeriod Duration `toml:"update_period"` // Quality to use for this feed Quality model.Quality `toml:"quality"` + // Maximum height of video + MaxHeight int `toml:"max_height"` // Format to use for this feed Format model.Format `toml:"format"` // Custom image to use diff --git a/pkg/ytdl/options.go b/pkg/ytdl/options.go new file mode 100644 index 00000000..9f78c601 --- /dev/null +++ b/pkg/ytdl/options.go @@ -0,0 +1,39 @@ +package ytdl + +import ( + "fmt" + "path/filepath" + + "github.com/mxpv/podsync/pkg/config" + "github.com/mxpv/podsync/pkg/model" +) + +type Options interface { + GetConfig() []string +} + +type OptionsDl struct{} + +func (o OptionsDl) New(feedConfig *config.Feed, episode *model.Episode, feedPath string) []string { + + var ( + arguments []string + options Options + ) + + if feedConfig.Format == model.FormatVideo { + options = NewOptionsVideo(feedConfig) + } else { + options = NewOptionsAudio(feedConfig) + } + + arguments = options.GetConfig() + arguments = append(arguments, "--output", o.makeOutputTemplate(feedPath, episode), episode.VideoURL) + + return arguments +} + +func (o OptionsDl) makeOutputTemplate(feedPath string, episode *model.Episode) string { + filename := fmt.Sprintf("%s.%s", episode.ID, "%(ext)s") + return filepath.Join(feedPath, filename) +} diff --git a/pkg/ytdl/options_audio.go b/pkg/ytdl/options_audio.go new file mode 100644 index 00000000..cf9144aa --- /dev/null +++ b/pkg/ytdl/options_audio.go @@ -0,0 +1,33 @@ +package ytdl + +import ( + "github.com/mxpv/podsync/pkg/config" + "github.com/mxpv/podsync/pkg/model" +) + +type OptionsAudio struct { + quality model.Quality +} + +func NewOptionsAudio(feedConfig *config.Feed) *OptionsAudio { + options := &OptionsAudio{} + options.quality = feedConfig.Quality + + return options +} + +func (options OptionsAudio) GetConfig() []string { + var arguments []string + + arguments = append(arguments, "--extract-audio", "--audio-format", "mp3") + + switch options.quality { + case model.QualityLow: + // really? somebody use it? + arguments = append(arguments, "--format", "worstaudio") + default: + arguments = append(arguments, "--format", "bestaudio") + } + + return arguments +} diff --git a/pkg/ytdl/options_audio_test.go b/pkg/ytdl/options_audio_test.go new file mode 100644 index 00000000..281cbe09 --- /dev/null +++ b/pkg/ytdl/options_audio_test.go @@ -0,0 +1,67 @@ +package ytdl + +import ( + "reflect" + "testing" + + "github.com/mxpv/podsync/pkg/config" + "github.com/mxpv/podsync/pkg/model" +) + +func TestNewOptionsAudio(t *testing.T) { + type args struct { + feedConfig *config.Feed + } + tests := []struct { + name string + args args + want *OptionsAudio + }{ + { + "Get OptionsAudio in low quality", + args{ + feedConfig: &config.Feed{Quality: model.QualityLow}, + }, + &OptionsAudio{quality: model.QualityLow}, + }, + { + "Get OptionsAudio in high quality", + args{ + feedConfig: &config.Feed{Quality: model.QualityHigh}, + }, + &OptionsAudio{quality: model.QualityHigh}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewOptionsAudio(tt.args.feedConfig); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewOptionsAudio() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestOptionsAudio_GetConfig(t *testing.T) { + type fields struct { + quality model.Quality + } + tests := []struct { + name string + fields fields + want []string + }{ + {"OptionsAudio in unknown quality", fields{quality: model.Quality("unknown")}, []string{"--extract-audio", "--audio-format", "mp3", "--format", "bestaudio"}}, + {"OptionsAudio in low quality", fields{quality: model.Quality("low")}, []string{"--extract-audio", "--audio-format", "mp3", "--format", "worstaudio"}}, + {"OptionsAudio in high quality", fields{quality: model.Quality("high")}, []string{"--extract-audio", "--audio-format", "mp3", "--format", "bestaudio"}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + options := OptionsAudio{ + quality: tt.fields.quality, + } + if got := options.GetConfig(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetConfig() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/ytdl/options_video.go b/pkg/ytdl/options_video.go new file mode 100644 index 00000000..eee01cc7 --- /dev/null +++ b/pkg/ytdl/options_video.go @@ -0,0 +1,50 @@ +package ytdl + +import ( + "fmt" + + "github.com/mxpv/podsync/pkg/config" + "github.com/mxpv/podsync/pkg/model" +) + +type OptionsVideo struct { + quality model.Quality + maxHeight int +} + +func NewOptionsVideo(feedConfig *config.Feed) *OptionsVideo { + options := &OptionsVideo{} + + options.quality = feedConfig.Quality + options.maxHeight = feedConfig.MaxHeight + + return options +} + +func (options OptionsVideo) GetConfig() []string { + var ( + arguments []string + format string + ) + + switch options.quality { + // I think after enabling MaxHeight param QualityLow option don't need. + // If somebody want download video in low quality then can set MaxHeight to 360p + // ¯\_(ツ)_/¯ + case model.QualityLow: + format = "worstvideo[ext=mp4]+worstaudio[ext=m4a]/worst[ext=mp4]/worst" + default: + format = "bestvideo%s[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best" + + if options.maxHeight > 0 { + format = fmt.Sprintf(format, fmt.Sprintf("[height<=%d]", options.maxHeight)) + } else { + // unset replace pattern + format = fmt.Sprintf(format, "") + } + } + + arguments = append(arguments, "--format", format) + + return arguments +} diff --git a/pkg/ytdl/options_video_test.go b/pkg/ytdl/options_video_test.go new file mode 100644 index 00000000..e0cc7f75 --- /dev/null +++ b/pkg/ytdl/options_video_test.go @@ -0,0 +1,110 @@ +package ytdl + +import ( + "reflect" + "testing" + + "github.com/mxpv/podsync/pkg/config" + "github.com/mxpv/podsync/pkg/model" +) + +func TestNewOptionsVideo(t *testing.T) { + type args struct { + feedConfig *config.Feed + } + tests := []struct { + name string + args args + want *OptionsVideo + }{ + { + "Get OptionsVideo in low quality", + args{ + feedConfig: &config.Feed{Quality: model.QualityLow}, + }, + &OptionsVideo{quality: model.QualityLow}, + }, + { + "Get OptionsVideo in low quality with maxheight", + args{ + feedConfig: &config.Feed{Quality: model.QualityLow, MaxHeight: 720}, + }, + &OptionsVideo{quality: model.QualityLow, maxHeight: 720}, + }, + { + "Get OptionsVideo in high quality", + args{ + feedConfig: &config.Feed{Quality: model.QualityHigh}, + }, + &OptionsVideo{quality: model.QualityHigh}, + }, + { + "Get OptionsVideo in high quality with maxheight", + args{ + feedConfig: &config.Feed{Quality: model.QualityHigh, MaxHeight: 720}, + }, + &OptionsVideo{quality: model.QualityHigh, maxHeight: 720}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewOptionsVideo(tt.args.feedConfig); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewOptionsVideo() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestOptionsVideo_GetConfig(t *testing.T) { + type fields struct { + quality model.Quality + maxHeight int + } + tests := []struct { + name string + fields fields + want []string + }{ + { + "OptionsVideo in unknown quality", + fields{quality: model.Quality("unknown")}, + []string{"--format", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best"}, + }, + { + "OptionsVideo in unknown quality with maxheight", + fields{quality: model.Quality("unknown"), maxHeight: 720}, + []string{"--format", "bestvideo[height<=720][ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best"}, + }, + { + "OptionsVideo in low quality", + fields{quality: model.QualityLow}, + []string{"--format", "worstvideo[ext=mp4]+worstaudio[ext=m4a]/worst[ext=mp4]/worst"}, + }, + { + "OptionsVideo in low quality with maxheight", + fields{quality: model.QualityLow, maxHeight: 720}, + []string{"--format", "worstvideo[ext=mp4]+worstaudio[ext=m4a]/worst[ext=mp4]/worst"}, + }, + { + "OptionsVideo in high quality", + fields{quality: model.QualityHigh}, + []string{"--format", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best"}, + }, + { + "OptionsVideo in high quality with maxheight", + fields{quality: model.QualityHigh, maxHeight: 720}, + []string{"--format", "bestvideo[height<=720][ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + options := OptionsVideo{ + quality: tt.fields.quality, + maxHeight: tt.fields.maxHeight, + } + if got := options.GetConfig(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetConfig() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/ytdl/ytdl.go b/pkg/ytdl/ytdl.go index 6e3ed845..e2334ce2 100644 --- a/pkg/ytdl/ytdl.go +++ b/pkg/ytdl/ytdl.go @@ -2,9 +2,7 @@ package ytdl import ( "context" - "fmt" "os/exec" - "path/filepath" "time" "github.com/pkg/errors" @@ -41,62 +39,12 @@ func New(ctx context.Context) (*YoutubeDl, error) { } func (dl YoutubeDl) Download(ctx context.Context, feedConfig *config.Feed, episode *model.Episode, feedPath string) (string, error) { - var ( - outputTemplate = makeOutputTemplate(feedPath, episode) - url = episode.VideoURL - ) - - if feedConfig.Format == model.FormatAudio { - // Audio - if feedConfig.Quality == model.QualityHigh { - // High quality audio (encoded to mp3) - return dl.exec(ctx, - "--extract-audio", - "--audio-format", - "mp3", - "--format", - "bestaudio", - "--output", - outputTemplate, - url, - ) - } else { //nolint - // Low quality audio (encoded to mp3) - return dl.exec(ctx, - "--extract-audio", - "--audio-format", - "mp3", - "--format", - "worstaudio", - "--output", - outputTemplate, - url, - ) - } - } else { - /* - Video - */ - if feedConfig.Quality == model.QualityHigh { - // High quality - return dl.exec(ctx, - "--format", - "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best", - "--output", - outputTemplate, - url, - ) - } else { //nolint - // Low quality - return dl.exec(ctx, - "--format", - "worstvideo[ext=mp4]+worstaudio[ext=m4a]/worst[ext=mp4]/worst", - "--output", - outputTemplate, - url, - ) - } - } + options := &OptionsDl{} + + params := options.New(feedConfig, episode, feedPath) + + return dl.exec(ctx, params...) + } func (YoutubeDl) exec(ctx context.Context, args ...string) (string, error) { @@ -112,8 +60,3 @@ func (YoutubeDl) exec(ctx context.Context, args ...string) (string, error) { return string(output), nil } - -func makeOutputTemplate(feedPath string, episode *model.Episode) string { - filename := fmt.Sprintf("%s.%s", episode.ID, "%(ext)s") - return filepath.Join(feedPath, filename) -}