youtubedl.go 7.41 KB
Newer Older
1
package youtubedl
2
3

import (
4
	"context"
5
	"encoding/json"
6
7
8
	"fmt"
	"github.com/asticode/go-astisub"
	log "github.com/sirupsen/logrus"
9
10
11
12
	"google.golang.org/api/googleapi/transport"
	"google.golang.org/api/option"
	"google.golang.org/api/youtube/v3"
	"net/http"
13
14
	"net/url"
	"os"
15
	"os/exec"
16
17
	"path/filepath"
	"regexp"
18
19
20
	"strings"
)

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
type SubtitleFormat string

func (sf SubtitleFormat) String() string {
	return string(sf)
}

const (
	SubVTT  SubtitleFormat = "vtt"
	SubSRT  SubtitleFormat = "srt"
	SubSSA  SubtitleFormat = "ssa"
	SubSTL  SubtitleFormat = "stl"
	SubTTML SubtitleFormat = "ttml"
)

var SupportedSubs = map[SubtitleFormat]bool{
	SubVTT:  true,
	SubSRT:  true,
	SubSSA:  true,
	SubSTL:  true,
	SubTTML: true,
}

43
44
45
46
47
48
type ErrYoutubeDLLangNotSupported string

func (e ErrYoutubeDLLangNotSupported) Error() string {
	return string(e)
}

49
50
51
52
53
54
type ErrYoutubeDL string

func (e ErrYoutubeDL) Error() string {
	return string(e)
}

55
56
func runYoutubeDL(args []string) (output []byte, err error) {
	cmd := exec.Command("youtube-dl", args...)
57
	output, err = cmd.CombinedOutput()
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
	if err != nil {
		return nil, err
	}

	return output, nil
}

func getVideoInfo(id string) (info map[string]interface{}, err error) {

	args := []string{
		"--skip-download",
		"-J",
		"https://youtu.be/" + id,
	}

	output, err := runYoutubeDL(args)
	if err != nil {
		return info, err
	}

	err = json.Unmarshal(output, &info)
	if err != nil {
		return info, err
	}

	return info, err
}

86
87
88
// SubList stores two  maps[language][sub-formats]
// Automatic Captions are the automatically generated youtube captions
// Subtitles are the proper Subtitles
89
90
91
92
93
type SubList struct {
	AutomaticCaptions map[string][]string
	Subtitles         map[string][]string
}

94
95
96
97
98
99
100
101
102
103
104
func NewSubList() *SubList {
	return &SubList{
		AutomaticCaptions: make(map[string][]string),
		Subtitles:         make(map[string][]string),
	}
}

func listSubs(id string) (subs *SubList, err error) {

	subs = NewSubList()

105
106
107
108
109
110
	args := []string{
		"--skip-download",
		"--list-subs",
		"https://youtu.be/" + id,
	}
	output, err := runYoutubeDL(args)
111
112
113
	if err != nil {
		return nil, err
	}
114
115
116

	lines := strings.Split(string(output), "\n")

117
	section := ""
118
	for _, line := range lines {
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
		if strings.Contains(strings.ToLower(line), "youtube") {
			continue
		}
		if strings.Contains(strings.ToLower(line), "available automatic captions") {
			section = "automatic captions"
			continue
		}
		if strings.Contains(strings.ToLower(line), "available subtitles") {
			section = "available subtitles"
			continue
		}
		if strings.Contains(strings.ToLower(line), "language formats") {
			continue
		}
		if section != "" {
			space := regexp.MustCompile(`\s+`)
			line := space.ReplaceAllString(line, " ")
			line = strings.ReplaceAll(line, ",", "")
			fields := strings.Split(string(line), " ")
			if len(fields) >= 2 {
				if section == "automatic captions" {
					subs.AutomaticCaptions[fields[0]] = fields[1:]

				}
				if section == "available subtitles" {
					subs.Subtitles[fields[0]] = fields[1:]
				}
			}
		}
148
149
	}

150
151
152
	return subs, nil
}

153
// findSub return nil when subtitle in the requested language is not available in any format
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
func findSub(id string, language string) (formats []string, err error) {
	subList, err := listSubs(id)
	if err != nil {
		return nil, err
	}
	if formats, ok := subList.Subtitles[language]; ok {
		return formats, nil
	}
	return nil, nil
}

//  findAutomaticCaption return nil when subtitle not available in any format
func findAutomaticCaption(id string, language string) (formats []string, err error) {
	subList, err := listSubs(id)
	if err != nil {
		return nil, err
	}
	if formats, ok := subList.AutomaticCaptions[language]; ok {
		return formats, nil
	}
	return nil, nil
}

// downloadSubtitle downloads the subtitle of the given youtube video in the given language and format and saves it as file to $file.$language.$format.
func downloadSubtitle(auto bool, id string, language string, format string, file string) error {
	subType := "--write-sub"
	if auto {
		subType = "--write-auto-sub"
	}

	args := []string{
		"--skip-download",
		subType,
		"--sub-lang",
		language,
		"--sub-format",
		format,
		"https://youtu.be/" + id,
		"-o",
		file,
	}

	output, err := runYoutubeDL(args)
	if err != nil {
		return err
	}
	if strings.Contains(strings.ToLower(string(output)), "subtitles not available") {
		return ErrYoutubeDL(fmt.Sprintf("subtitles language %s not available", language))
	}
	if strings.Contains(strings.ToLower(string(output)), "no subtitle format found") {
		return ErrYoutubeDL(fmt.Sprintf("subtitle format %s not found", format))

		//TODO delete the alternative subtitle file that it downloads automatically
	}

	return nil
}

// GetSubtitle downloads the subtitle of the video id in the passed language.
// Throws an error if subtitle not found.
214
// Downlaods the automatic caption when auto is true.
215
216
func GetSubtitle(auto bool, id string, language string) (sub *astisub.Subtitles, err error) {

217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
	var formats []string
	if auto == false {
		formats, err = findSub(id, language)
		if err != nil {
			return nil, err
		}

		if formats == nil {
			return nil, ErrYoutubeDLLangNotSupported(fmt.Sprintf("No subtitle with language %s for video %s", language, id))
		}
	} else {
		formats, err = findAutomaticCaption(id, language)
		if err != nil {
			return nil, err
		}

		if formats == nil {
			return nil, ErrYoutubeDLLangNotSupported(fmt.Sprintf("No automatic caption with language %s for video %s", language, id))
		}
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
	}

	for _, format := range formats {

		if _, ok := SupportedSubs[SubtitleFormat(format)]; !ok {
			log.Infof("Subtitle %s is not supported", format)
			continue
		}
		filename := filepath.Join("/tmp", "sub-"+id)
		file := filename + "." + language + "." + SubVTT.String()

		err := downloadSubtitle(auto, id, language, SubVTT.String(), filename)
		defer os.Remove(file)
		if err != nil {
			log.Warn(err)
			continue
		}

		sub, err := astisub.OpenFile(file)
		if err != nil {
			log.Warn(err)
			continue
		}
		return sub, nil
	}

	return nil, ErrYoutubeDL("No available subtitle is supported")
}

// GetVideoURL retrieves the videoplayback url of the video and audio stream
func GetVideoURL(id string) (video *url.URL, audio *url.URL, err error) {

	args := []string{
		"-g",
		"https://www.youtube.com/watch?v=" + id,
	}

	// run command
	//log.Println(args)
	cmd := exec.Command("youtube-dl", args...)
	out, err := cmd.CombinedOutput()
	if err != nil {
		return nil, nil, err
	}
	links := strings.Split(strings.Trim(string(out), "\n\t "), "\n")
	if len(links) != 2 {
		return nil, nil, ErrYoutubeDL(fmt.Sprintf("Expected two links but got %v", len(links)))
	}
	video, err = url.Parse(links[0])
	if err != nil {
		return nil, nil, err
	}
	audio, err = url.Parse(links[1])
	if err != nil {
		return nil, nil, err
	}
	return video, audio, nil
293
}
294

295
296
// GetPlaylistVideos returns a list of youtube video id's that are in the given playlist.
// The response is limited to the first 50 videos.
297
298
299
300
301
302
303
304
305
306
func GetPlaylistVideos(playListID string, apiKey string) (members []string, err error) {
	client := &http.Client{
		Transport: &transport.APIKey{Key: apiKey},
	}

	service, err := youtube.NewService(context.Background(), option.WithHTTPClient(client))
	if err != nil {
		return nil, err
	}
	// Make the API call to YouTube.
307
308
309
310
	call := service.PlaylistItems.List("id,contentDetails").
		PlaylistId(playListID).
		MaxResults(50)

311
312
313
314
315
316
317
318
319
320
321
	response, err := call.Do()
	if err != nil {
		return nil, err
	}

	for _, item := range response.Items {
		members = append(members, item.ContentDetails.VideoId)
	}

	return members, nil
}