dischord/extractor/ytdl/ytdl.go

136 lines
3.2 KiB
Go

package ytdl
import (
"git.nobrain.org/r4/dischord/extractor"
"bufio"
"encoding/json"
"errors"
"os/exec"
"strings"
"time"
)
var (
ErrUnsupportedUrl = errors.New("unsupported URL")
)
// A very reduced version of the JSON structure returned by youtube-dl
type ytdlMetadata struct {
Title string `json:"title"`
Extractor string `json:"extractor"`
Duration float32 `json:"duration"`
WebpageUrl string `json:"webpage_url"`
Playlist string `json:"playlist"`
Uploader string `json:"uploader"`
Description string `json:"description"`
Formats []struct {
Url string `json:"url"`
Format string `json"format"`
VCodec string `json:"vcodec"`
} `json:"formats"`
}
// Gradually sends all audio URLs through the string channel. If an error occurs, it is sent through the
// error channel. Both channels are closed after either an error occurs or all URLs have been output.
func ytdlGet(youtubeDLPath, input string) (<-chan extractor.Data, <-chan error) {
out := make(chan extractor.Data)
errch := make(chan error, 1)
go func() {
defer close(out)
defer close(errch)
// Set youtube-dl args
var ytdlArgs []string
ytdlArgs = append(ytdlArgs, "-j", input)
// Prepare command for execution
cmd := exec.Command(youtubeDLPath, ytdlArgs...)
cmd.Env = []string{"LC_ALL=en_US.UTF-8"} // Youtube-dl doesn't recognize some chars if LC_ALL=C or not set at all
stdout, err := cmd.StdoutPipe()
if err != nil {
errch <- err
return
}
stderr, err := cmd.StderrPipe()
if err != nil {
errch <- err
return
}
// Catch any errors put out by youtube-dl
stderrReadDoneCh := make(chan struct{})
var ytdlError string
go func() {
sc := bufio.NewScanner(stderr)
for sc.Scan() {
line := sc.Text()
if strings.HasPrefix(line, "ERROR: ") {
ytdlError = strings.TrimPrefix(line, "ERROR: ")
}
}
stderrReadDoneCh <- struct{}{}
}()
// Start youtube-dl
if err := cmd.Start(); err != nil {
errch <- err
return
}
// We want to let our main loop know when youtube-dl is done
donech := make(chan error)
go func() {
donech <- cmd.Wait()
}()
// Main JSON decoder loop
dec := json.NewDecoder(stdout)
for dec.More() {
// Read JSON
var m ytdlMetadata
if err := dec.Decode(&m); err != nil {
errch <- err
return
}
// Extract URL from metadata (the latter formats are always the better with youtube-dl)
for i := len(m.Formats) - 1; i >= 0; i-- {
format := m.Formats[i]
if format.VCodec == "none" {
out <- extractor.Data{
SourceUrl: m.WebpageUrl,
StreamUrl: format.Url,
Title: m.Title,
PlaylistTitle: m.Playlist,
Description: m.Description,
Uploader: m.Uploader,
Duration: int(m.Duration),
Expires: time.Now().Add(10 * 365 * 24 * time.Hour),
}
break
}
}
}
// Wait for command to finish executing and catch any errors
err = <-donech
<-stderrReadDoneCh
if err != nil {
if ytdlError == "" {
errch <- err
} else {
if strings.HasPrefix(ytdlError, "Unsupported URL: ") {
errch <- ErrUnsupportedUrl
} else {
errch <- errors.New("ytdl: " + ytdlError)
}
}
return
}
}()
return out, errch
}