youtubedl.go 5.95 KB
Newer Older
1
package youtubedl
2
3
4

import (
	"encoding/json"
5
6
7
8
9
	"fmt"
	"github.com/asticode/go-astisub"
	log "github.com/sirupsen/logrus"
	"net/url"
	"os"
10
	"os/exec"
11
12
	"path/filepath"
	"regexp"
13
14
15
	"strings"
)

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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,
}

type ErrYoutubeDL string

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

44
45
func runYoutubeDL(args []string) (output []byte, err error) {
	cmd := exec.Command("youtube-dl", args...)
46
	output, err = cmd.CombinedOutput()
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
	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
}

75
76
77
// SubList stores two  maps[language][sub-formats]
// Automatic Captions are the automatically generated youtube captions
// Subtitles are the proper Subtitles
78
79
80
81
82
type SubList struct {
	AutomaticCaptions map[string][]string
	Subtitles         map[string][]string
}

83
84
85
86
87
88
89
90
91
92
93
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()

94
95
96
97
98
99
	args := []string{
		"--skip-download",
		"--list-subs",
		"https://youtu.be/" + id,
	}
	output, err := runYoutubeDL(args)
100
101
102
	if err != nil {
		return nil, err
	}
103
104
105

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

106
	section := ""
107
	for _, line := range lines {
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
		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:]
				}
			}
		}
137
138
	}

139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
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
	return subs, nil
}

// findSub return nil when subtitle not available in any format
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.
// Downlaods the automaic caption when auto is true.
func GetSubtitle(auto bool, id string, language string) (sub *astisub.Subtitles, err error) {

	formats, err := findSub(id, language)
	if err != nil {
		return nil, err
	}

	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
266
}