Skip to content

Commit 2e06f1f

Browse files
committed
Rewrite format filters and allow filtering by langauge
1 parent 56da946 commit 2e06f1f

File tree

13 files changed

+214
-269
lines changed

13 files changed

+214
-269
lines changed

client_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,30 @@ func TestGetVideoWithManifestURL(t *testing.T) {
156156
assert.NotZero(size)
157157
}
158158

159+
func TestGetVideo_MultiLanguage(t *testing.T) {
160+
assert, require := assert.New(t), require.New(t)
161+
video, err := testClient.GetVideo("https://www.youtube.com/watch?v=pU9sHwNKc2c")
162+
require.NoError(err)
163+
require.NotNil(video)
164+
165+
// collect languages
166+
var languageNames, lanaguageIDs []string
167+
for _, format := range video.Formats {
168+
if format.AudioTrack != nil {
169+
languageNames = append(languageNames, format.LanguageDisplayName())
170+
lanaguageIDs = append(lanaguageIDs, format.AudioTrack.ID)
171+
}
172+
}
173+
174+
assert.Contains(languageNames, "English original")
175+
assert.Contains(languageNames, "Portuguese (Brazil)")
176+
assert.Contains(lanaguageIDs, "en.4")
177+
assert.Contains(lanaguageIDs, "pt-BR.3")
178+
179+
assert.Empty(video.Formats.Language("Does not exist"))
180+
assert.NotEmpty(video.Formats.Language("English original"))
181+
}
182+
159183
func TestGetStream(t *testing.T) {
160184
assert, require := assert.New(t), require.New(t)
161185

cmd/youtubedr/download.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@ func init() {
3232

3333
downloadCmd.Flags().StringVarP(&outputFile, "filename", "o", "", "The output file, the default is genated by the video title.")
3434
downloadCmd.Flags().StringVarP(&outputDir, "directory", "d", ".", "The output directory.")
35-
addQualityFlag(downloadCmd.Flags())
36-
addMimeTypeFlag(downloadCmd.Flags())
35+
addVideoSelectionFlags(downloadCmd.Flags())
3736
}
3837

3938
func download(id string) error {
@@ -48,7 +47,7 @@ func download(id string) error {
4847
if err := checkFFMPEG(); err != nil {
4948
return err
5049
}
51-
return downloader.DownloadComposite(context.Background(), outputFile, video, outputQuality, mimetype)
50+
return downloader.DownloadComposite(context.Background(), outputFile, video, outputQuality, mimetype, language)
5251
}
5352

5453
return downloader.Download(context.Background(), video, format, outputFile)

cmd/youtubedr/downloader.go

Lines changed: 24 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package main
22

33
import (
44
"crypto/tls"
5-
"errors"
65
"fmt"
76
"net"
87
"net/http"
@@ -20,16 +19,15 @@ import (
2019
var (
2120
insecureSkipVerify bool // skip TLS server validation
2221
outputQuality string // itag number or quality string
23-
mimetype string // mimetype
22+
mimetype string
23+
language string
2424
downloader *ytdl.Downloader
2525
)
2626

27-
func addQualityFlag(flagSet *pflag.FlagSet) {
27+
func addVideoSelectionFlags(flagSet *pflag.FlagSet) {
2828
flagSet.StringVarP(&outputQuality, "quality", "q", "medium", "The itag number or quality label (hd720, medium)")
29-
}
30-
31-
func addMimeTypeFlag(flagSet *pflag.FlagSet) {
32-
flagSet.StringVarP(&mimetype, "mimetype", "m", "mp4", "Mime-Type to filter (mp4, webm, av01, avc1) - applicable if --quality used is quality label")
29+
flagSet.StringVarP(&mimetype, "mimetype", "m", "", "Mime-Type to filter (mp4, webm, av01, avc1) - applicable if --quality used is quality label")
30+
flagSet.StringVarP(&language, "language", "l", "", "Language to filter")
3331
}
3432

3533
func getDownloader() *ytdl.Downloader {
@@ -70,41 +68,34 @@ func getDownloader() *ytdl.Downloader {
7068
return downloader
7169
}
7270

73-
func getVideoWithFormat(id string) (*youtube.Video, *youtube.Format, error) {
71+
func getVideoWithFormat(videoID string) (*youtube.Video, *youtube.Format, error) {
7472
dl := getDownloader()
75-
video, err := dl.GetVideo(id)
73+
video, err := dl.GetVideo(videoID)
7674
if err != nil {
7775
return nil, nil, err
7876
}
77+
78+
itag, _ := strconv.Atoi(outputQuality)
7979
formats := video.Formats
80+
81+
if language != "" {
82+
formats = formats.Language(language)
83+
}
8084
if mimetype != "" {
8185
formats = formats.Type(mimetype)
8286
}
83-
if len(formats) == 0 {
84-
return nil, nil, errors.New("no formats found")
87+
if outputQuality != "" {
88+
formats = formats.Quality(outputQuality)
8589
}
86-
87-
var format *youtube.Format
88-
itag, _ := strconv.Atoi(outputQuality)
89-
switch {
90-
case itag > 0:
91-
// When an itag is specified, do not filter format with mime-type
92-
format = video.Formats.FindByItag(itag)
93-
if format == nil {
94-
return nil, nil, fmt.Errorf("unable to find format with itag %d", itag)
95-
}
96-
97-
case outputQuality != "":
98-
format = formats.FindByQuality(outputQuality)
99-
if format == nil {
100-
return nil, nil, fmt.Errorf("unable to find format with quality %s", outputQuality)
101-
}
102-
103-
default:
104-
// select the first format
105-
formats.Sort()
106-
format = &formats[0]
90+
if itag > 0 {
91+
formats = formats.Itag(itag)
92+
}
93+
if formats == nil {
94+
return nil, nil, fmt.Errorf("unable to find the specified format")
10795
}
10896

109-
return video, format, nil
97+
formats.Sort()
98+
99+
// select the first format
100+
return video, &formats[0], nil
110101
}

cmd/youtubedr/info.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type VideoFormat struct {
1818
VideoQuality string
1919
AudioQuality string
2020
AudioChannels int
21+
Language string
2122
Size int64
2223
Bitrate int
2324
MimeType string
@@ -73,6 +74,7 @@ var infoCmd = &cobra.Command{
7374
Size: size,
7475
Bitrate: bitrate,
7576
MimeType: format.MimeType,
77+
Language: format.LanguageDisplayName(),
7678
})
7779
}
7880

@@ -102,6 +104,7 @@ func writeInfoOutput(w io.Writer, info *VideoInfo) {
102104
"size [MB]",
103105
"bitrate",
104106
"MimeType",
107+
"language",
105108
})
106109

107110
for _, format := range info.Formats {
@@ -114,6 +117,7 @@ func writeInfoOutput(w io.Writer, info *VideoInfo) {
114117
fmt.Sprintf("%0.1f", float64(format.Size)/1024/1024),
115118
strconv.Itoa(format.Bitrate),
116119
format.MimeType,
120+
format.Language,
117121
})
118122
}
119123

cmd/youtubedr/url.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ var urlCmd = &cobra.Command{
2323
}
2424

2525
func init() {
26-
addQualityFlag(urlCmd.Flags())
27-
addMimeTypeFlag(urlCmd.Flags())
26+
addVideoSelectionFlags(urlCmd.Flags())
2827
rootCmd.AddCommand(urlCmd)
2928
}

downloader/downloader.go

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package downloader
22

33
import (
44
"context"
5-
"fmt"
5+
"errors"
66
"io"
77
"os"
88
"os/exec"
@@ -59,8 +59,8 @@ func (dl *Downloader) Download(ctx context.Context, v *youtube.Video, format *yo
5959
}
6060

6161
// DownloadComposite : Downloads audio and video streams separately and merges them via ffmpeg.
62-
func (dl *Downloader) DownloadComposite(ctx context.Context, outputFile string, v *youtube.Video, quality string, mimetype string) error {
63-
videoFormat, audioFormat, err1 := getVideoAudioFormats(v, quality, mimetype)
62+
func (dl *Downloader) DownloadComposite(ctx context.Context, outputFile string, v *youtube.Video, quality string, mimetype, language string) error {
63+
videoFormat, audioFormat, err1 := getVideoAudioFormats(v, quality, mimetype, language)
6464
if err1 != nil {
6565
return err1
6666
}
@@ -122,8 +122,7 @@ func (dl *Downloader) DownloadComposite(ctx context.Context, outputFile string,
122122
return ffmpegVersionCmd.Run()
123123
}
124124

125-
func getVideoAudioFormats(v *youtube.Video, quality string, mimetype string) (*youtube.Format, *youtube.Format, error) {
126-
var videoFormat, audioFormat *youtube.Format
125+
func getVideoAudioFormats(v *youtube.Video, quality string, mimetype, language string) (*youtube.Format, *youtube.Format, error) {
127126
var videoFormats, audioFormats youtube.FormatList
128127

129128
formats := v.Formats
@@ -138,25 +137,22 @@ func getVideoAudioFormats(v *youtube.Video, quality string, mimetype string) (*y
138137
videoFormats = videoFormats.Quality(quality)
139138
}
140139

141-
if len(videoFormats) > 0 {
142-
videoFormats.Sort()
143-
videoFormat = &videoFormats[0]
140+
if language != "" {
141+
audioFormats = audioFormats.Language(language)
144142
}
145143

146-
if len(audioFormats) > 0 {
147-
audioFormats.Sort()
148-
audioFormat = &audioFormats[0]
144+
if len(videoFormats) == 0 {
145+
return nil, nil, errors.New("no video format found after filtering")
149146
}
150147

151-
if videoFormat == nil {
152-
return nil, nil, fmt.Errorf("no video format found after filtering")
148+
if len(audioFormats) == 0 {
149+
return nil, nil, errors.New("no audio format found after filtering")
153150
}
154151

155-
if audioFormat == nil {
156-
return nil, nil, fmt.Errorf("no audio format found after filtering")
157-
}
152+
videoFormats.Sort()
153+
audioFormats.Sort()
158154

159-
return videoFormat, audioFormat, nil
155+
return &videoFormats[0], &audioFormats[0], nil
160156
}
161157

162158
func (dl *Downloader) videoDLWorker(ctx context.Context, out *os.File, video *youtube.Video, format *youtube.Format) error {

downloader/downloader_hq_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,5 @@ func TestDownload_HighQuality(t *testing.T) {
1616

1717
video, err := testDownloader.Client.GetVideoContext(ctx, "BaW_jenozKc")
1818
require.NoError(err)
19-
20-
require.NoError(testDownloader.DownloadComposite(ctx, "", video, "hd1080", "mp4"))
19+
require.NoError(testDownloader.DownloadComposite(ctx, "", video, "hd1080", "mp4", ""))
2120
}

downloader/downloader_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func TestYoutube_DownloadWithHighQualityFails(t *testing.T) {
6969
Formats: tt.formats,
7070
}
7171

72-
err := testDownloader.DownloadComposite(context.Background(), "", video, "hd1080", "")
72+
err := testDownloader.DownloadComposite(context.Background(), "", video, "hd1080", "", "")
7373
assert.EqualError(t, err, tt.message)
7474
})
7575
}
@@ -101,7 +101,7 @@ func Test_getVideoAudioFormats(t *testing.T) {
101101
{ItagNo: 249, MimeType: "audio/webm; codecs=\"opus\"", Quality: "tiny", Bitrate: 72862, FPS: 0, Width: 0, Height: 0, LastModified: "1540474783513282", ContentLength: 24839529, QualityLabel: "", ProjectionType: "RECTANGULAR", AverageBitrate: 55914, AudioQuality: "AUDIO_QUALITY_LOW", ApproxDurationMs: "3553941", AudioSampleRate: "48000", AudioChannels: 2},
102102
}}
103103
{
104-
videoFormat, audioFormat, err := getVideoAudioFormats(v, "hd720", "mp4")
104+
videoFormat, audioFormat, err := getVideoAudioFormats(v, "hd720", "mp4", "")
105105
require.NoError(err)
106106
require.NotNil(videoFormat)
107107
require.Equal(398, videoFormat.ItagNo)
@@ -110,7 +110,7 @@ func Test_getVideoAudioFormats(t *testing.T) {
110110
}
111111

112112
{
113-
videoFormat, audioFormat, err := getVideoAudioFormats(v, "large", "webm")
113+
videoFormat, audioFormat, err := getVideoAudioFormats(v, "large", "webm", "")
114114
require.NoError(err)
115115
require.NotNil(videoFormat)
116116
require.Equal(244, videoFormat.ItagNo)

errors_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
)
99

1010
func TestErrors(t *testing.T) {
11+
t.Parallel()
12+
1113
tests := []struct {
1214
err error
1315
expected string

format_list.go

Lines changed: 36 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,68 +8,59 @@ import (
88

99
type FormatList []Format
1010

11-
// FindByQuality returns the first format matching Quality or QualityLabel
12-
//
13-
// Examples: tiny, small, medium, large, 720p, hd720, hd1080
14-
func (list FormatList) FindByQuality(quality string) *Format {
11+
// Type returns a new FormatList filtered by itag
12+
func (list FormatList) Select(f func(Format) bool) (result FormatList) {
1513
for i := range list {
16-
if list[i].Quality == quality || list[i].QualityLabel == quality {
17-
return &list[i]
14+
if f(list[i]) {
15+
result = append(result, list[i])
1816
}
1917
}
20-
return nil
18+
return result
2119
}
2220

23-
// FindByItag returns the first format matching the itag number
24-
func (list FormatList) FindByItag(itagNo int) *Format {
25-
for i := range list {
26-
if list[i].ItagNo == itagNo {
27-
return &list[i]
28-
}
29-
}
30-
return nil
21+
// Type returns a new FormatList filtered by itag
22+
func (list FormatList) Itag(itagNo int) FormatList {
23+
return list.Select(func(f Format) bool {
24+
return f.ItagNo == itagNo
25+
})
3126
}
3227

33-
// Type returns a new FormatList filtered by mime type of video
34-
func (list FormatList) Type(t string) (result FormatList) {
35-
for i := range list {
36-
if strings.Contains(list[i].MimeType, t) {
37-
result = append(result, list[i])
38-
}
39-
}
40-
return result
28+
// Type returns a new FormatList filtered by mime type
29+
func (list FormatList) Type(value string) FormatList {
30+
return list.Select(func(f Format) bool {
31+
return strings.Contains(f.MimeType, value)
32+
})
33+
}
34+
35+
// Type returns a new FormatList filtered by display name
36+
func (list FormatList) Language(displayName string) FormatList {
37+
return list.Select(func(f Format) bool {
38+
return f.LanguageDisplayName() == displayName
39+
})
4140
}
4241

4342
// Quality returns a new FormatList filtered by quality, quality label or itag,
4443
// but not audio quality
45-
func (list FormatList) Quality(quality string) (result FormatList) {
46-
for _, f := range list {
47-
itag, _ := strconv.Atoi(quality)
48-
if itag == f.ItagNo || strings.Contains(f.Quality, quality) || strings.Contains(f.QualityLabel, quality) {
49-
result = append(result, f)
50-
}
51-
}
52-
return result
44+
func (list FormatList) Quality(quality string) FormatList {
45+
itag, _ := strconv.Atoi(quality)
46+
47+
return list.Select(func(f Format) bool {
48+
return itag == f.ItagNo || strings.Contains(f.Quality, quality) || strings.Contains(f.QualityLabel, quality)
49+
})
5350
}
5451

5552
// AudioChannels returns a new FormatList filtered by the matching AudioChannels
56-
func (list FormatList) AudioChannels(n int) (result FormatList) {
57-
for _, f := range list {
58-
if f.AudioChannels == n {
59-
result = append(result, f)
60-
}
61-
}
62-
return result
53+
func (list FormatList) AudioChannels(n int) FormatList {
54+
return list.Select(func(f Format) bool {
55+
return f.AudioChannels == n
56+
})
6357
}
6458

6559
// AudioChannels returns a new FormatList filtered by the matching AudioChannels
66-
func (list FormatList) WithAudioChannels() (result FormatList) {
67-
for _, f := range list {
68-
if f.AudioChannels > 0 {
69-
result = append(result, f)
70-
}
71-
}
72-
return result
60+
func (list FormatList) WithAudioChannels() FormatList {
61+
return list.Select(func(f Format) bool {
62+
return f.AudioChannels > 0
63+
})
7364
}
7465

7566
// FilterQuality reduces the format list to formats matching the quality

0 commit comments

Comments
 (0)