589 lines
15 KiB
Go
589 lines
15 KiB
Go
package goutubedl_test
|
|
|
|
// TODO: currently the tests only run on linux as they use osleaktest which only
|
|
// has linux support
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.kaz62.ru/dilap54/goutubedl"
|
|
"github.com/fortytw2/leaktest"
|
|
"github.com/wader/osleaktest"
|
|
)
|
|
|
|
const (
|
|
testVideoRawURL = "https://vimeo.com/454525548"
|
|
playlistRawURL = "https://soundcloud.com/mattheis/sets/kindred-phenomena"
|
|
channelRawURL = "https://www.youtube.com/channel/UCHDm-DKoMyJxKVgwGmuTaQA"
|
|
subtitlesTestVideoRawURL = "https://www.youtube.com/watch?v=QRS8MkLhQmM"
|
|
)
|
|
|
|
func leakChecks(t *testing.T) func() {
|
|
leakFn := leaktest.Check(t)
|
|
osLeakFn := osleaktest.Check(t)
|
|
|
|
return func() {
|
|
leakFn()
|
|
osLeakFn()
|
|
}
|
|
}
|
|
|
|
func TestBinaryNotPath(t *testing.T) {
|
|
defer leakChecks(t)()
|
|
defer func(orig string) { goutubedl.Path = orig }(goutubedl.Path)
|
|
goutubedl.Path = "/non-existing"
|
|
|
|
_, versionErr := goutubedl.Version(context.Background())
|
|
if versionErr == nil || !strings.Contains(versionErr.Error(), "no such file or directory") {
|
|
t.Fatalf("err should be nil 'no such file or directory': %v", versionErr)
|
|
}
|
|
}
|
|
|
|
func TestVersion(t *testing.T) {
|
|
defer leakChecks(t)()
|
|
|
|
versionRe := regexp.MustCompile(`^\d{4}\.\d{2}.\d{2}.*$`)
|
|
version, versionErr := goutubedl.Version(context.Background())
|
|
|
|
if versionErr != nil {
|
|
t.Fatalf("err: %s", versionErr)
|
|
}
|
|
|
|
if !versionRe.MatchString(version) {
|
|
t.Errorf("version %q does not match %q", version, versionRe)
|
|
}
|
|
}
|
|
|
|
func TestDownload(t *testing.T) {
|
|
defer leakChecks(t)()
|
|
|
|
stderrBuf := &bytes.Buffer{}
|
|
r, err := goutubedl.New(context.Background(), testVideoRawURL, goutubedl.Options{
|
|
StderrFn: func(cmd *exec.Cmd) io.Writer {
|
|
return stderrBuf
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dr, err := r.Download(context.Background(), r.Info.Formats[0].FormatID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
downloadBuf := &bytes.Buffer{}
|
|
n, err := io.Copy(downloadBuf, dr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dr.Close()
|
|
|
|
if n != int64(downloadBuf.Len()) {
|
|
t.Errorf("copy n not equal to download buffer: %d!=%d", n, downloadBuf.Len())
|
|
}
|
|
|
|
if n < 10000 {
|
|
t.Errorf("should have copied at least 10000 bytes: %d", n)
|
|
}
|
|
|
|
if !strings.Contains(stderrBuf.String(), "Destination") {
|
|
t.Errorf("did not find expected log message on stderr: %q", stderrBuf.String())
|
|
}
|
|
}
|
|
|
|
func TestDownloadWithoutInfo(t *testing.T) {
|
|
defer leakChecks(t)()
|
|
|
|
stderrBuf := &bytes.Buffer{}
|
|
dr, err := goutubedl.Download(context.Background(), testVideoRawURL, goutubedl.Options{
|
|
StderrFn: func(cmd *exec.Cmd) io.Writer {
|
|
return stderrBuf
|
|
},
|
|
}, "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
downloadBuf := &bytes.Buffer{}
|
|
n, err := io.Copy(downloadBuf, dr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dr.Close()
|
|
|
|
if n != int64(downloadBuf.Len()) {
|
|
t.Errorf("copy n not equal to download buffer: %d!=%d", n, downloadBuf.Len())
|
|
}
|
|
|
|
if n < 10000 {
|
|
t.Errorf("should have copied at least 10000 bytes: %d", n)
|
|
}
|
|
|
|
if !strings.Contains(stderrBuf.String(), "Destination") {
|
|
t.Errorf("did not find expected log message on stderr: %q", stderrBuf.String())
|
|
}
|
|
}
|
|
|
|
func TestParseInfo(t *testing.T) {
|
|
for _, c := range []struct {
|
|
url string
|
|
expectedTitle string
|
|
}{
|
|
{"https://soundcloud.com/avalonemerson/avalon-emerson-live-at-printworks-london-march-2017", "Avalon Emerson Live at Printworks London 2017"},
|
|
{"https://www.infoq.com/presentations/Simple-Made-Easy", "Simple Made Easy - InfoQ"},
|
|
{"https://vimeo.com/454525548", "Sample Video - 3 minutemp4.mp4"},
|
|
} {
|
|
t.Run(c.url, func(t *testing.T) {
|
|
defer leakChecks(t)()
|
|
|
|
ctx, cancelFn := context.WithCancel(context.Background())
|
|
ydlResult, err := goutubedl.New(ctx, c.url, goutubedl.Options{
|
|
DownloadThumbnail: true,
|
|
})
|
|
if err != nil {
|
|
cancelFn()
|
|
t.Errorf("failed to parse: %v", err)
|
|
return
|
|
}
|
|
cancelFn()
|
|
|
|
yi := ydlResult.Info
|
|
results := ydlResult.Formats()
|
|
|
|
if yi.Title != c.expectedTitle {
|
|
t.Errorf("expected title %q got %q", c.expectedTitle, yi.Title)
|
|
}
|
|
|
|
if yi.Thumbnail != "" && len(yi.ThumbnailBytes) == 0 {
|
|
t.Errorf("expected thumbnail bytes")
|
|
}
|
|
|
|
var dummy map[string]interface{}
|
|
if err := json.Unmarshal(ydlResult.RawJSON, &dummy); err != nil {
|
|
t.Errorf("failed to parse RawJSON")
|
|
}
|
|
|
|
if len(results) == 0 {
|
|
t.Errorf("expected formats")
|
|
}
|
|
|
|
for _, f := range results {
|
|
if f.FormatID == "" {
|
|
t.Errorf("expected to have FormatID")
|
|
}
|
|
if f.Ext == "" {
|
|
t.Errorf("expected to have Ext")
|
|
}
|
|
if (f.ACodec == "" || f.ACodec == "none") &&
|
|
(f.VCodec == "" || f.VCodec == "none") &&
|
|
f.Ext == "" {
|
|
t.Errorf("expected to have some media: audio %q video %q ext %q", f.ACodec, f.VCodec, f.Ext)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPlaylist(t *testing.T) {
|
|
defer leakChecks(t)()
|
|
|
|
ydlResult, ydlResultErr := goutubedl.New(context.Background(), playlistRawURL, goutubedl.Options{
|
|
Type: goutubedl.TypePlaylist,
|
|
DownloadThumbnail: false,
|
|
})
|
|
|
|
if ydlResultErr != nil {
|
|
t.Errorf("failed to download: %s", ydlResultErr)
|
|
}
|
|
|
|
expectedTitle := "Kindred Phenomena"
|
|
if ydlResult.Info.Title != expectedTitle {
|
|
t.Errorf("expected title %q got %q", expectedTitle, ydlResult.Info.Title)
|
|
}
|
|
|
|
expectedEntries := 8
|
|
if len(ydlResult.Info.Entries) != expectedEntries {
|
|
t.Errorf("expected %d entries got %d", expectedEntries, len(ydlResult.Info.Entries))
|
|
}
|
|
|
|
expectedTitleOne := "A1 Mattheis - Herds"
|
|
if ydlResult.Info.Entries[0].Title != expectedTitleOne {
|
|
t.Errorf("expected title %q got %q", expectedTitleOne, ydlResult.Info.Entries[0].Title)
|
|
}
|
|
}
|
|
|
|
func TestChannel(t *testing.T) {
|
|
t.Skip("skip youtube for now")
|
|
|
|
defer leakChecks(t)()
|
|
|
|
ydlResult, ydlResultErr := goutubedl.New(
|
|
context.Background(),
|
|
channelRawURL,
|
|
goutubedl.Options{
|
|
Type: goutubedl.TypeChannel,
|
|
DownloadThumbnail: false,
|
|
},
|
|
)
|
|
|
|
if ydlResultErr != nil {
|
|
t.Errorf("failed to download: %s", ydlResultErr)
|
|
}
|
|
|
|
expectedTitle := "Simon Yapp"
|
|
if ydlResult.Info.Title != expectedTitle {
|
|
t.Errorf("expected title %q got %q", expectedTitle, ydlResult.Info.Title)
|
|
}
|
|
|
|
expectedEntries := 5
|
|
if len(ydlResult.Info.Entries) != expectedEntries {
|
|
t.Errorf("expected %d entries got %d", expectedEntries, len(ydlResult.Info.Entries))
|
|
}
|
|
|
|
expectedTitleOne := "#RNLI Shoreham #LifeBoat demo of launch."
|
|
if ydlResult.Info.Entries[0].Title != expectedTitleOne {
|
|
t.Errorf("expected title %q got %q", expectedTitleOne, ydlResult.Info.Entries[0].Title)
|
|
}
|
|
}
|
|
|
|
func TestUnsupportedURL(t *testing.T) {
|
|
defer leaktest.Check(t)()
|
|
|
|
_, ydlResultErr := goutubedl.New(context.Background(), "https://www.google.com", goutubedl.Options{})
|
|
if ydlResultErr == nil {
|
|
t.Errorf("expected unsupported url")
|
|
}
|
|
expectedErrPrefix := "Unsupported URL:"
|
|
if ydlResultErr != nil && !strings.HasPrefix(ydlResultErr.Error(), expectedErrPrefix) {
|
|
t.Errorf("expected error prefix %q got %q", expectedErrPrefix, ydlResultErr.Error())
|
|
|
|
}
|
|
}
|
|
|
|
func TestPlaylistWithPrivateVideo(t *testing.T) {
|
|
t.Skip("skip youtube for now")
|
|
|
|
defer leaktest.Check(t)()
|
|
|
|
playlistRawURL := "https://www.youtube.com/playlist?list=PLX0g748fkegS54oiDN4AXKl7BR7mLIydP"
|
|
ydlResult, ydlResultErr := goutubedl.New(context.Background(), playlistRawURL, goutubedl.Options{
|
|
Type: goutubedl.TypePlaylist,
|
|
DownloadThumbnail: false,
|
|
})
|
|
|
|
if ydlResultErr != nil {
|
|
t.Errorf("failed to download: %s", ydlResultErr)
|
|
}
|
|
|
|
expectedLen := 2
|
|
actualLen := len(ydlResult.Info.Entries)
|
|
if expectedLen != actualLen {
|
|
t.Errorf("expected len %d got %d", expectedLen, actualLen)
|
|
}
|
|
}
|
|
|
|
func TestSubtitles(t *testing.T) {
|
|
t.Skip("skip youtube for now")
|
|
|
|
defer leakChecks(t)()
|
|
|
|
ydlResult, ydlResultErr := goutubedl.New(
|
|
context.Background(),
|
|
subtitlesTestVideoRawURL,
|
|
goutubedl.Options{
|
|
DownloadSubtitles: true,
|
|
})
|
|
|
|
if ydlResultErr != nil {
|
|
t.Errorf("failed to download: %s", ydlResultErr)
|
|
}
|
|
|
|
for _, subtitles := range ydlResult.Info.Subtitles {
|
|
for _, subtitle := range subtitles {
|
|
if subtitle.Ext == "" {
|
|
t.Errorf("%s: %s: expected extension", ydlResult.Info.URL, subtitle.Language)
|
|
}
|
|
if subtitle.Language == "" {
|
|
t.Errorf("%s: %s: expected language", ydlResult.Info.URL, subtitle.Language)
|
|
}
|
|
if subtitle.URL == "" {
|
|
t.Errorf("%s: %s: expected url", ydlResult.Info.URL, subtitle.Language)
|
|
}
|
|
if len(subtitle.Bytes) == 0 {
|
|
t.Errorf("%s: %s: expected bytes", ydlResult.Info.URL, subtitle.Language)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDownloadSections(t *testing.T) {
|
|
defer leakChecks(t)()
|
|
|
|
fileName := "durationTestingFile"
|
|
duration := 5
|
|
|
|
cmd := exec.Command("ffmpeg", "-version")
|
|
_, err := cmd.Output()
|
|
if err != nil {
|
|
t.Errorf("failed to check ffmpeg installed: %s", err)
|
|
}
|
|
|
|
ydlResult, ydlResultErr := goutubedl.New(
|
|
context.Background(),
|
|
"https://vimeo.com/454525548",
|
|
goutubedl.Options{
|
|
DownloadSections: fmt.Sprintf("*0:0-0:%d", duration),
|
|
})
|
|
|
|
if ydlResult.Options.DownloadSections != "*0:0-0:5" {
|
|
t.Errorf("failed to setup --download-sections")
|
|
}
|
|
if ydlResultErr != nil {
|
|
t.Errorf("failed to download: %s", ydlResultErr)
|
|
}
|
|
dr, err := ydlResult.Download(context.Background(), "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
f, err := os.Create(fileName)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer f.Close()
|
|
_, err = io.Copy(f, dr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cmd = exec.Command("ffprobe", "-v", "quiet", "-show_entries", "format=duration", fileName)
|
|
stdout, err := cmd.Output()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var gotDurationString string
|
|
output := string(stdout)
|
|
for _, line := range strings.Split(output, "\n") {
|
|
if strings.Contains(line, "duration") {
|
|
if d, found := strings.CutPrefix(line, "duration="); found {
|
|
gotDurationString = d
|
|
}
|
|
}
|
|
}
|
|
|
|
gotDuration, err := strconv.ParseFloat(gotDurationString, 32)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
seconds := int(gotDuration)
|
|
if seconds != duration {
|
|
t.Fatalf("did not get expected duration of %d, but got %d", duration, seconds)
|
|
}
|
|
dr.Close()
|
|
}
|
|
|
|
func TestErrorNotAPlaylist(t *testing.T) {
|
|
defer leakChecks(t)()
|
|
|
|
_, ydlResultErr := goutubedl.New(context.Background(), testVideoRawURL, goutubedl.Options{
|
|
Type: goutubedl.TypePlaylist,
|
|
DownloadThumbnail: false,
|
|
})
|
|
if ydlResultErr != goutubedl.ErrNotAPlaylist {
|
|
t.Errorf("expected is playlist error, got %s", ydlResultErr)
|
|
}
|
|
}
|
|
|
|
func TestErrorNotASingleEntry(t *testing.T) {
|
|
defer leakChecks(t)()
|
|
|
|
_, ydlResultErr := goutubedl.New(context.Background(), playlistRawURL, goutubedl.Options{
|
|
Type: goutubedl.TypeSingle,
|
|
DownloadThumbnail: false,
|
|
})
|
|
if ydlResultErr != goutubedl.ErrNotASingleEntry {
|
|
t.Fatalf("expected is single entry error, got %s", ydlResultErr)
|
|
}
|
|
}
|
|
|
|
func TestOptionDownloader(t *testing.T) {
|
|
defer leakChecks(t)()
|
|
|
|
ydlResult, ydlResultErr := goutubedl.New(
|
|
context.Background(),
|
|
testVideoRawURL,
|
|
goutubedl.Options{
|
|
Downloader: "ffmpeg",
|
|
})
|
|
|
|
if ydlResultErr != nil {
|
|
t.Fatalf("failed to download: %s", ydlResultErr)
|
|
}
|
|
dr, err := ydlResult.Download(context.Background(), ydlResult.Info.Formats[0].FormatID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
downloadBuf := &bytes.Buffer{}
|
|
_, err = io.Copy(downloadBuf, dr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dr.Close()
|
|
}
|
|
|
|
func TestInvalidOptionTypeField(t *testing.T) {
|
|
defer leakChecks(t)()
|
|
|
|
_, err := goutubedl.New(context.Background(), playlistRawURL, goutubedl.Options{
|
|
Type: 42,
|
|
})
|
|
if err == nil {
|
|
t.Error("should have failed")
|
|
}
|
|
}
|
|
|
|
func TestDownloadPlaylistEntry(t *testing.T) {
|
|
defer leakChecks(t)()
|
|
// Download file by specifying the playlist index
|
|
stderrBuf := &bytes.Buffer{}
|
|
r, err := goutubedl.New(context.Background(), playlistRawURL, goutubedl.Options{
|
|
StderrFn: func(cmd *exec.Cmd) io.Writer {
|
|
return stderrBuf
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expectedTitle := "Kindred Phenomena"
|
|
if r.Info.Title != expectedTitle {
|
|
t.Errorf("expected title %q got %q", expectedTitle, r.Info.Title)
|
|
}
|
|
|
|
expectedEntries := 8
|
|
if len(r.Info.Entries) != expectedEntries {
|
|
t.Errorf("expected %d entries got %d", expectedEntries, len(r.Info.Entries))
|
|
}
|
|
|
|
expectedTitleOne := "B1 Mattheis - Ben M"
|
|
playlistIndex := 2
|
|
if r.Info.Entries[playlistIndex].Title != expectedTitleOne {
|
|
t.Errorf("expected title %q got %q", expectedTitleOne, r.Info.Entries[playlistIndex].Title)
|
|
}
|
|
|
|
dr, err := r.DownloadWithOptions(context.Background(), goutubedl.DownloadOptions{
|
|
PlaylistIndex: int(r.Info.Entries[playlistIndex].PlaylistIndex),
|
|
Filter: r.Info.Entries[playlistIndex].Formats[0].FormatID,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
playlistBuf := &bytes.Buffer{}
|
|
n, err := io.Copy(playlistBuf, dr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dr.Close()
|
|
|
|
if n != int64(playlistBuf.Len()) {
|
|
t.Errorf("copy n not equal to download buffer: %d!=%d", n, playlistBuf.Len())
|
|
}
|
|
|
|
if n < 10000 {
|
|
t.Errorf("should have copied at least 10000 bytes: %d", n)
|
|
}
|
|
|
|
if !strings.Contains(stderrBuf.String(), "Destination") {
|
|
t.Errorf("did not find expected log message on stderr: %q", stderrBuf.String())
|
|
}
|
|
|
|
// Download the same file but with the direct link
|
|
url := "https://soundcloud.com/mattheis/b1-mattheis-ben-m"
|
|
stderrBuf = &bytes.Buffer{}
|
|
r, err = goutubedl.New(context.Background(), url, goutubedl.Options{
|
|
StderrFn: func(cmd *exec.Cmd) io.Writer {
|
|
return stderrBuf
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if r.Info.Title != expectedTitleOne {
|
|
t.Errorf("expected title %q got %q", expectedTitleOne, r.Info.Title)
|
|
}
|
|
|
|
expectedEntries = 0
|
|
if len(r.Info.Entries) != expectedEntries {
|
|
t.Errorf("expected %d entries got %d", expectedEntries, len(r.Info.Entries))
|
|
}
|
|
|
|
dr, err = r.Download(context.Background(), r.Info.Formats[0].FormatID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
directLinkBuf := &bytes.Buffer{}
|
|
n, err = io.Copy(directLinkBuf, dr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dr.Close()
|
|
|
|
if n != int64(directLinkBuf.Len()) {
|
|
t.Errorf("copy n not equal to download buffer: %d!=%d", n, directLinkBuf.Len())
|
|
}
|
|
|
|
if n < 10000 {
|
|
t.Errorf("should have copied at least 10000 bytes: %d", n)
|
|
}
|
|
|
|
if !strings.Contains(stderrBuf.String(), "Destination") {
|
|
t.Errorf("did not find expected log message on stderr: %q", stderrBuf.String())
|
|
}
|
|
|
|
if directLinkBuf.Len() != playlistBuf.Len() {
|
|
t.Errorf("not the same content size between the playlist index entry and the direct link entry: %d != %d", playlistBuf.Len(), directLinkBuf.Len())
|
|
}
|
|
|
|
if !bytes.Equal(directLinkBuf.Bytes(), playlistBuf.Bytes()) {
|
|
t.Error("not the same content between the playlist index entry and the direct link entry")
|
|
}
|
|
}
|
|
|
|
func TestFormatDownloadError(t *testing.T) {
|
|
t.Skip("test URL broken")
|
|
|
|
defer leaktest.Check(t)()
|
|
|
|
ydl, ydlErr := goutubedl.New(
|
|
context.Background(),
|
|
"https://www.reddit.com/r/newsbabes/s/92rflI0EB0",
|
|
goutubedl.Options{},
|
|
)
|
|
|
|
if ydlErr != nil {
|
|
// reddit seems to not like github action hosts
|
|
if strings.Contains(ydlErr.Error(), "HTTPError 403: Blocked") {
|
|
t.Skip()
|
|
}
|
|
t.Error(ydlErr)
|
|
}
|
|
|
|
// no pre-muxed audio/video format available
|
|
_, ytDlErr := ydl.Download(context.Background(), "best")
|
|
expectedErr := "Requested format is not available"
|
|
if ydlErr != nil && !strings.Contains(ytDlErr.Error(), expectedErr) {
|
|
t.Errorf("expected error prefix %q got %q", expectedErr, ytDlErr.Error())
|
|
|
|
}
|
|
}
|