diff --git a/cmd/goutubedl/main.go b/cmd/goutubedl/main.go index 13efbab..1ec1418 100644 --- a/cmd/goutubedl/main.go +++ b/cmd/goutubedl/main.go @@ -7,6 +7,7 @@ import ( "io" "log" "os" + "os/exec" "github.com/wader/goutubedl" ) @@ -24,7 +25,11 @@ func main() { result, err := goutubedl.New( context.Background(), flag.Arg(0), - goutubedl.Options{Type: optType, DebugLog: log.Default()}, + goutubedl.Options{ + Type: optType, + DebugLog: log.Default(), + StderrFn: func(cmd *exec.Cmd) io.Writer { return os.Stderr }, + }, ) if err != nil { log.Fatal(err) diff --git a/goutubedl.go b/goutubedl.go index d76d302..2324365 100644 --- a/goutubedl.go +++ b/goutubedl.go @@ -9,7 +9,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net/http" "os" "os/exec" @@ -129,7 +128,7 @@ type Info struct { WebpageURL string `json:"webpage_url"` Description string `json:"description"` Thumbnail string `json:"thumbnail"` - // not unmarshalled, populated from image thumbnail file + // don't unmarshal, populated from image thumbnail file ThumbnailBytes []byte `json:"-"` Thumbnails []Thumbnail `json:"thumbnails"` @@ -180,7 +179,7 @@ type Subtitle struct { URL string `json:"url"` Ext string `json:"ext"` Language string `json:"-"` - // not unmarshalled, populated from subtitle file + // don't unmarshal, populated from subtitle file Bytes []byte `json:"-"` } @@ -346,15 +345,15 @@ func infoFromURL( case TypeAny: break default: - return Info{}, nil, fmt.Errorf("Unhandle options type value: %d", options.Type) + return Info{}, nil, fmt.Errorf("unhandled options type value: %d", options.Type) } - tempPath, _ := ioutil.TempDir("", "ydls") + tempPath, _ := os.MkdirTemp("", "ydls") defer os.RemoveAll(tempPath) stdoutBuf := &bytes.Buffer{} stderrBuf := &bytes.Buffer{} - stderrWriter := ioutil.Discard + stderrWriter := io.Discard if options.StderrFn != nil { stderrWriter = options.StderrFn(cmd) } @@ -428,7 +427,7 @@ func infoFromURL( if options.DownloadThumbnail && info.Thumbnail != "" { resp, respErr := get(info.Thumbnail) if respErr == nil { - buf, _ := ioutil.ReadAll(resp.Body) + buf, _ := io.ReadAll(resp.Body) resp.Body.Close() info.ThumbnailBytes = buf } @@ -445,7 +444,7 @@ func infoFromURL( for i, subtitle := range subtitles { resp, respErr := get(subtitle.URL) if respErr == nil { - buf, _ := ioutil.ReadAll(resp.Body) + buf, _ := io.ReadAll(resp.Body) resp.Body.Close() subtitles[i].Bytes = buf } @@ -464,21 +463,21 @@ func infoFromURL( // - entries that are more than 2 levels deep (will be missed) // - the ability to restrict entries to a single level (we include both levels) if options.Type == TypePlaylist || options.Type == TypeChannel { - var filteredEntrise []Info + var filteredEntries []Info for _, e := range info.Entries { if e.Type == "playlist" { for _, ee := range e.Entries { if ee.ID == "" { continue } - filteredEntrise = append(filteredEntrise, ee) + filteredEntries = append(filteredEntries, ee) } continue } else if e.ID != "" { - filteredEntrise = append(filteredEntrise, e) + filteredEntries = append(filteredEntries, e) } } - info.Entries = filteredEntrise + info.Entries = filteredEntries } return info, stdoutBuf.Bytes(), nil @@ -533,7 +532,7 @@ func (result Result) DownloadWithOptions( } } - tempPath, tempErr := ioutil.TempDir("", "ydls") + tempPath, tempErr := os.MkdirTemp("", "ydls") if tempErr != nil { return nil, tempErr } @@ -541,7 +540,7 @@ func (result Result) DownloadWithOptions( var jsonTempPath string if !result.Options.noInfoDownload { jsonTempPath = path.Join(tempPath, "info.json") - if err := ioutil.WriteFile(jsonTempPath, result.RawJSON, 0600); err != nil { + if err := os.WriteFile(jsonTempPath, result.RawJSON, 0600); err != nil { os.RemoveAll(tempPath) return nil, err } @@ -624,15 +623,17 @@ func (result Result) DownloadWithOptions( } cmd.Dir = tempPath - var w io.WriteCloser - dr.reader, w = io.Pipe() - - stderrWriter := ioutil.Discard + var stdoutW io.WriteCloser + var stderrW io.WriteCloser + var stderrR io.Reader + dr.reader, stdoutW = io.Pipe() + stderrR, stderrW = io.Pipe() + optStderrWriter := io.Discard if result.Options.StderrFn != nil { - stderrWriter = result.Options.StderrFn(cmd) + optStderrWriter = result.Options.StderrFn(cmd) } - cmd.Stdout = w - cmd.Stderr = stderrWriter + cmd.Stdout = stdoutW + cmd.Stderr = io.MultiWriter(optStderrWriter, stderrW) debugLog.Print("cmd", " ", cmd.Args) if err := cmd.Start(); err != nil { @@ -642,12 +643,32 @@ func (result Result) DownloadWithOptions( go func() { _ = cmd.Wait() - w.Close() + stdoutW.Close() + stderrW.Close() os.RemoveAll(tempPath) close(dr.waitCh) }() - return dr, nil + // blocks return until yt-dlp is downloading or has errored + ytErrCh := make(chan error) + go func() { + stderrLineScanner := bufio.NewScanner(stderrR) + for stderrLineScanner.Scan() { + const downloadPrefix = "[download]" + const errorPrefix = "ERROR: " + line := stderrLineScanner.Text() + if strings.HasPrefix(line, downloadPrefix) { + break + } else if strings.HasPrefix(line, errorPrefix) { + ytErrCh <- errors.New(line[len(errorPrefix):]) + return + } + } + ytErrCh <- nil + _, _ = io.Copy(io.Discard, stderrR) + }() + + return dr, <-ytErrCh } func (dr *DownloadResult) Read(p []byte) (n int, err error) { diff --git a/goutubedl_test.go b/goutubedl_test.go index 711aa40..80dd6dd 100644 --- a/goutubedl_test.go +++ b/goutubedl_test.go @@ -380,7 +380,7 @@ func TestDownloadSections(t *testing.T) { } seconds := int(gotDuration) if seconds != duration { - t.Fatalf("didnot get expected duration of %d, but got %d", duration, seconds) + t.Fatalf("did not get expected duration of %d, but got %d", duration, seconds) } dr.Close() } @@ -552,3 +552,29 @@ func TestDownloadPlaylistEntry(t *testing.T) { t.Error("not the same content between the playlist index entry and the direct link entry") } } + +func TestFormatDownloadError(t *testing.T) { + 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()) + + } +}