diff --git a/goutubedl.go b/goutubedl.go index 213b71f..6a14b65 100644 --- a/goutubedl.go +++ b/goutubedl.go @@ -270,8 +270,8 @@ func infoFromURL(ctx context.Context, rawURL string, options Options) (info Info if options.Downloader != "" { cmd.Args = append(cmd.Args, "--downloader", options.Downloader) } - - if options.Type == TypePlaylist { + switch options.Type { + case TypePlaylist: cmd.Args = append(cmd.Args, "--yes-playlist") if options.PlaylistStart > 0 { @@ -284,7 +284,7 @@ func infoFromURL(ctx context.Context, rawURL string, options Options) (info Info "--playlist-end", strconv.Itoa(int(options.PlaylistEnd)), ) } - } else { + case TypeSingle: if options.DownloadSubtitles { cmd.Args = append(cmd.Args, "--all-subs", @@ -293,6 +293,10 @@ func infoFromURL(ctx context.Context, rawURL string, options Options) (info Info cmd.Args = append(cmd.Args, "--no-playlist", ) + case TypeAny: + break + default: + return Info{}, nil, fmt.Errorf("Unhandle options type value: %d", options.Type) } tempPath, _ := ioutil.TempDir("", "ydls") @@ -429,11 +433,27 @@ type DownloadResult struct { // Download format matched by filter (usually a format id or quality designator). // If filter is empty, then youtube-dl will use its default format selector. +// It's a shortcut of DownloadWithOptions where the options use the default value func (result Result) Download(ctx context.Context, filter string) (*DownloadResult, error) { + return result.DownloadWithOptions(ctx, DownloadOptions{ + Filter: filter, + }) +} + +type DownloadOptions struct { + // Download format matched by filter (usually a format id or quality designator). + // If filter is empty, then youtube-dl will use its default format selector. + Filter string + // The index of the entry to download from the playlist that would be + // passed to youtube-dl via --playlist-items. The index value starts at 1 + PlaylistIndex int +} + +func (result Result) DownloadWithOptions(ctx context.Context, options DownloadOptions) (*DownloadResult, error) { debugLog := result.Options.DebugLog - if result.Info.Type == "playlist" || result.Info.Type == "multi_video" { - return nil, fmt.Errorf("can't download a playlist") + if (result.Info.Type == "playlist" || result.Info.Type == "multi_video") && options.PlaylistIndex == 0 { + return nil, fmt.Errorf("can't download a playlist when the playlist index options is not set") } tempPath, tempErr := ioutil.TempDir("", "ydls") @@ -463,8 +483,12 @@ func (result Result) Download(ctx context.Context, filter string) (*DownloadResu ) // don't need to specify if direct as there is only one // also seems to be issues when using filter with generic extractor - if !result.Info.Direct && filter != "" { - cmd.Args = append(cmd.Args, "-f", filter) + if !result.Info.Direct && options.Filter != "" { + cmd.Args = append(cmd.Args, "-f", options.Filter) + } + + if options.PlaylistIndex > 0 { + cmd.Args = append(cmd.Args, "--playlist-items", fmt.Sprint(options.PlaylistIndex)) } if result.Options.ProxyUrl != "" { diff --git a/goutubedl_test.go b/goutubedl_test.go index c4eb51b..54adab9 100644 --- a/goutubedl_test.go +++ b/goutubedl_test.go @@ -301,3 +301,122 @@ func TestOptionDownloader(t *testing.T) { } 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") + } +}