165 lines
4.1 KiB
Go
165 lines
4.1 KiB
Go
|
package ytdl
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"encoding/json"
|
||
|
"errors"
|
||
|
"os/exec"
|
||
|
)
|
||
|
|
||
|
type Extractor struct {
|
||
|
DefaultSearch string
|
||
|
YtdlPath string
|
||
|
}
|
||
|
|
||
|
func NewExtractor(ytdlPath string) *Extractor {
|
||
|
return &Extractor{
|
||
|
DefaultSearch: "ytsearch",
|
||
|
YtdlPath: ytdlPath,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
errInvalidMetadata = errors.New("invalid metadata received from youtube-dl")
|
||
|
)
|
||
|
|
||
|
type Metadata map[string]interface{}
|
||
|
|
||
|
/*// `input` can be a URL or a search query.
|
||
|
// Returns a slice with size 1 if the input is a single media file. Returns a
|
||
|
// larger slice if the input is a playlist.
|
||
|
// Progress sends a struct{} every time a single item has been added.
|
||
|
func (e *Extractor) GetMetadata(input string, progress chan<- struct{}) ([]Metadata, error) {
|
||
|
cmd := exec.Command(e.YtdlPath, "--default-search", e.DefaultSearch, "-j", input)
|
||
|
var out bytes.Buffer
|
||
|
cmd.Stdout = &out
|
||
|
if err := cmd.Start(); err != nil {
|
||
|
return nil, newError(input, err)
|
||
|
}
|
||
|
|
||
|
defer close(progress)
|
||
|
|
||
|
var m sync.Mutex
|
||
|
var done bool
|
||
|
var ret []Metadata
|
||
|
var ytdlErr error
|
||
|
var killedYtdl bool
|
||
|
go func() {
|
||
|
killYtdl := func(err error) {
|
||
|
m.Lock()
|
||
|
ytdlErr = err
|
||
|
cmd.Process.Signal(os.Interrupt)
|
||
|
killedYtdl = true
|
||
|
m.Unlock()
|
||
|
}
|
||
|
|
||
|
dec := json.NewDecoder(&out)
|
||
|
m.Lock()
|
||
|
d := done
|
||
|
m.Unlock()
|
||
|
for !d && dec.More() {
|
||
|
var meta interface{}
|
||
|
if err := dec.Decode(&meta); err != nil {
|
||
|
killYtdl(newError(input, err))
|
||
|
}
|
||
|
progress <- struct{}{}
|
||
|
|
||
|
if m, ok := meta.(map[string]interface{}); ok {
|
||
|
ret = append(ret, m)
|
||
|
} else {
|
||
|
killYtdl(newError(input, errInvalidMetadata))
|
||
|
}
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
err := cmd.Wait()
|
||
|
m.Lock()
|
||
|
done = true
|
||
|
m.Unlock()
|
||
|
if ytdlErr != nil {
|
||
|
return nil, ytdlErr
|
||
|
}
|
||
|
if err != nil && !killedYtdl {
|
||
|
return nil, newError(input, err)
|
||
|
}
|
||
|
return ret, nil
|
||
|
}*/
|
||
|
|
||
|
// `input` can be a URL or a search query.
|
||
|
// Returns a slice with size 1 if the input is a single media file. Returns a
|
||
|
// larger slice if the input is a playlist.
|
||
|
func (e *Extractor) GetMetadata(input string) ([]Metadata, error) {
|
||
|
cmd := exec.Command(e.YtdlPath, "--default-search", e.DefaultSearch, "-j", input)
|
||
|
var out bytes.Buffer
|
||
|
cmd.Stdout = &out
|
||
|
if err := cmd.Run(); err != nil {
|
||
|
return nil, newError(input, err)
|
||
|
}
|
||
|
|
||
|
var ret []Metadata
|
||
|
dec := json.NewDecoder(&out)
|
||
|
for dec.More() {
|
||
|
var meta interface{}
|
||
|
if err := dec.Decode(&meta); err != nil {
|
||
|
return nil, newError(input, err)
|
||
|
}
|
||
|
|
||
|
if m, ok := meta.(map[string]interface{}); ok {
|
||
|
ret = append(ret, m)
|
||
|
} else {
|
||
|
return nil, newError(input, errInvalidMetadata)
|
||
|
}
|
||
|
}
|
||
|
return ret, nil
|
||
|
}
|
||
|
|
||
|
// Returns the best available audio-only format. If the given URL links directly
|
||
|
// to a media file, it just returns that URL.
|
||
|
func GetAudioURL(meta Metadata) (string, error) {
|
||
|
// This function has a lot of code, but what it does is actually pretty
|
||
|
// simple. We just have to do a lot to ensure that there are no integrity
|
||
|
// issues with the metadata.
|
||
|
// 'iSomething' stands for 'interface something' here.
|
||
|
|
||
|
iExtractor, ok := meta["extractor"]
|
||
|
if !ok {
|
||
|
// Extractor not specified.
|
||
|
return "", newError("", errInvalidMetadata)
|
||
|
}
|
||
|
if extractor, ok := iExtractor.(string); ok {
|
||
|
if extractor == "generic" {
|
||
|
url, ok := meta["url"]
|
||
|
if u := url.(string); ok {
|
||
|
return u, nil
|
||
|
} else {
|
||
|
return "", newError("", errors.New("unable to get any audio or video URL"))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
iFormats, ok := meta["formats"]
|
||
|
if !ok {
|
||
|
return "", newError("", errors.New("no format selection available and no raw URL specified"))
|
||
|
}
|
||
|
// Get the best audio format.
|
||
|
if formats, ok := iFormats.([]interface{}); ok {
|
||
|
// In youtube-dl, the last format is always the best one. Here we're
|
||
|
// looking for the last (=best) format that contains no video.
|
||
|
for i := len(formats) - 1; i >= 0; i-- {
|
||
|
iFormat := formats[i]
|
||
|
format, ok := iFormat.(map[string]interface{})
|
||
|
if !ok {
|
||
|
return "", newError("", errInvalidMetadata)
|
||
|
}
|
||
|
if format["vcodec"].(string) == "none" {
|
||
|
return format["url"].(string), nil
|
||
|
}
|
||
|
}
|
||
|
return "", newError("", errors.New("unable to find any audio-only format"))
|
||
|
} else {
|
||
|
return "", newError("", errInvalidMetadata)
|
||
|
}
|
||
|
} else {
|
||
|
return "", newError("", errInvalidMetadata)
|
||
|
}
|
||
|
}
|