package youtubedl import ( "context" "encoding/json" "fmt" "github.com/asticode/go-astisub" log "github.com/sirupsen/logrus" "google.golang.org/api/googleapi/transport" "google.golang.org/api/option" "google.golang.org/api/youtube/v3" "net/http" "net/url" "os" "os/exec" "path/filepath" "regexp" "strings" ) 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) } func runYoutubeDL(args []string) (output []byte, err error) { cmd := exec.Command("youtube-dl", args...) output, err = cmd.CombinedOutput() 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 } // SubList stores two maps[language][sub-formats] // Automatic Captions are the automatically generated youtube captions // Subtitles are the proper Subtitles type SubList struct { AutomaticCaptions map[string][]string Subtitles map[string][]string } 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() args := []string{ "--skip-download", "--list-subs", "https://youtu.be/" + id, } output, err := runYoutubeDL(args) if err != nil { return nil, err } lines := strings.Split(string(output), "\n") section := "" for _, line := range lines { 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:] } } } } 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 } 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. call := service.PlaylistItems.List("id,contentDetails").PlaylistId(playListID) response, err := call.Do() if err != nil { return nil, err } for _, item := range response.Items { members = append(members, item.ContentDetails.VideoId) } return members, nil }