From 4faabba6c4ae2b5b84ba6f8d6f25e518102eb1c2 Mon Sep 17 00:00:00 2001 From: Rpsl Date: Thu, 30 Jan 2020 00:03:07 +0300 Subject: [PATCH 1/2] [*] refactoring youtube-dl cli arguments [+] add optional param "max_height" to config for limit of maximal resolution of video [+] test --- README.md | 1 + pkg/config/config.go | 2 + pkg/model/feed.go | 2 + pkg/ytdl/options.go | 39 +++++++++++ pkg/ytdl/options_audio.go | 36 ++++++++++ pkg/ytdl/options_audio_test.go | 74 ++++++++++++++++++++ pkg/ytdl/options_video.go | 57 +++++++++++++++ pkg/ytdl/options_video_test.go | 124 +++++++++++++++++++++++++++++++++ pkg/ytdl/ytdl.go | 69 ++---------------- 9 files changed, 341 insertions(+), 63 deletions(-) create mode 100644 pkg/ytdl/options.go create mode 100644 pkg/ytdl/options_audio.go create mode 100644 pkg/ytdl/options_audio_test.go create mode 100644 pkg/ytdl/options_video.go create mode 100644 pkg/ytdl/options_video_test.go 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/model/feed.go b/pkg/model/feed.go index 756d9449..69ab2cf7 100644 --- a/pkg/model/feed.go +++ b/pkg/model/feed.go @@ -17,6 +17,8 @@ const ( // Format to convert episode when downloading episodes type Format string +type MaxHeight int + const ( FormatAudio = Format("audio") FormatVideo = Format("video") 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..b8af1e11 --- /dev/null +++ b/pkg/ytdl/options_audio.go @@ -0,0 +1,36 @@ +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 = model.QualityHigh + + if feedConfig.Quality == model.QualityLow { + options.quality = model.QualityLow + } + 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..f71271c1 --- /dev/null +++ b/pkg/ytdl/options_audio_test.go @@ -0,0 +1,74 @@ +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 unknown quality", + args{ + feedConfig: &config.Feed{Quality: model.Quality("unknown")}, + }, + &OptionsAudio{quality: model.QualityHigh}, + }, + { + "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..ce2d74c5 --- /dev/null +++ b/pkg/ytdl/options_video.go @@ -0,0 +1,57 @@ +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 = model.QualityHigh + + if feedConfig.Quality == model.QualityLow { + options.quality = model.QualityLow + } + + if feedConfig.MaxHeight > 0 { + 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..c78146c1 --- /dev/null +++ b/pkg/ytdl/options_video_test.go @@ -0,0 +1,124 @@ +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 unknown quality", + args{ + feedConfig: &config.Feed{Quality: model.Quality("unknown")}, + }, + &OptionsVideo{quality: model.QualityHigh}, + }, + { + "Get OptionsVideo in unknown quality with maxheight", + args{ + feedConfig: &config.Feed{Quality: model.Quality("unknown"), MaxHeight: 720}, + }, + &OptionsVideo{quality: model.QualityHigh, maxHeight: 720}, + }, + { + "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) -} From 36d64b778995aa27d3a822ea658214250b62eb18 Mon Sep 17 00:00:00 2001 From: Rpsl Date: Thu, 30 Jan 2020 00:40:51 +0300 Subject: [PATCH 2/2] [*] cleanup after review --- pkg/model/feed.go | 2 -- pkg/ytdl/options_audio.go | 5 +---- pkg/ytdl/options_audio_test.go | 7 ------- pkg/ytdl/options_video.go | 11 ++--------- pkg/ytdl/options_video_test.go | 14 -------------- 5 files changed, 3 insertions(+), 36 deletions(-) diff --git a/pkg/model/feed.go b/pkg/model/feed.go index 69ab2cf7..756d9449 100644 --- a/pkg/model/feed.go +++ b/pkg/model/feed.go @@ -17,8 +17,6 @@ const ( // Format to convert episode when downloading episodes type Format string -type MaxHeight int - const ( FormatAudio = Format("audio") FormatVideo = Format("video") diff --git a/pkg/ytdl/options_audio.go b/pkg/ytdl/options_audio.go index b8af1e11..cf9144aa 100644 --- a/pkg/ytdl/options_audio.go +++ b/pkg/ytdl/options_audio.go @@ -11,11 +11,8 @@ type OptionsAudio struct { func NewOptionsAudio(feedConfig *config.Feed) *OptionsAudio { options := &OptionsAudio{} - options.quality = model.QualityHigh + options.quality = feedConfig.Quality - if feedConfig.Quality == model.QualityLow { - options.quality = model.QualityLow - } return options } diff --git a/pkg/ytdl/options_audio_test.go b/pkg/ytdl/options_audio_test.go index f71271c1..281cbe09 100644 --- a/pkg/ytdl/options_audio_test.go +++ b/pkg/ytdl/options_audio_test.go @@ -17,13 +17,6 @@ func TestNewOptionsAudio(t *testing.T) { args args want *OptionsAudio }{ - { - "Get OptionsAudio in unknown quality", - args{ - feedConfig: &config.Feed{Quality: model.Quality("unknown")}, - }, - &OptionsAudio{quality: model.QualityHigh}, - }, { "Get OptionsAudio in low quality", args{ diff --git a/pkg/ytdl/options_video.go b/pkg/ytdl/options_video.go index ce2d74c5..eee01cc7 100644 --- a/pkg/ytdl/options_video.go +++ b/pkg/ytdl/options_video.go @@ -15,15 +15,8 @@ type OptionsVideo struct { func NewOptionsVideo(feedConfig *config.Feed) *OptionsVideo { options := &OptionsVideo{} - options.quality = model.QualityHigh - - if feedConfig.Quality == model.QualityLow { - options.quality = model.QualityLow - } - - if feedConfig.MaxHeight > 0 { - options.maxHeight = feedConfig.MaxHeight - } + options.quality = feedConfig.Quality + options.maxHeight = feedConfig.MaxHeight return options } diff --git a/pkg/ytdl/options_video_test.go b/pkg/ytdl/options_video_test.go index c78146c1..e0cc7f75 100644 --- a/pkg/ytdl/options_video_test.go +++ b/pkg/ytdl/options_video_test.go @@ -17,20 +17,6 @@ func TestNewOptionsVideo(t *testing.T) { args args want *OptionsVideo }{ - { - "Get OptionsVideo in unknown quality", - args{ - feedConfig: &config.Feed{Quality: model.Quality("unknown")}, - }, - &OptionsVideo{quality: model.QualityHigh}, - }, - { - "Get OptionsVideo in unknown quality with maxheight", - args{ - feedConfig: &config.Feed{Quality: model.Quality("unknown"), MaxHeight: 720}, - }, - &OptionsVideo{quality: model.QualityHigh, maxHeight: 720}, - }, { "Get OptionsVideo in low quality", args{