Compare commits
93 Commits
bump-youtu
...
bump-yt-dl
Author | SHA1 | Date | |
---|---|---|---|
0f1ca5fc1d | |||
64f5dfc85b | |||
4046d6dc4b | |||
05498e920a | |||
1fcacaf3f0 | |||
2ec70c51c9 | |||
bc05d02e93 | |||
cef23e8d43 | |||
9810de5ed3 | |||
e54060320d | |||
4e5309b84e | |||
387cad2e2c | |||
f719f0f559 | |||
bc6bc7763f | |||
b7078f00a4 | |||
8b27a8c181 | |||
2afa813da5 | |||
c615f6284d | |||
8e933863a0 | |||
3143935aea | |||
f6e338d60d | |||
81eacb4f26 | |||
05ac24b57f | |||
c7dcb3bb35 | |||
0611ba2d04 | |||
118e1f98d4 | |||
6ab6b5507e | |||
d5ae5299c2 | |||
7d8a5847f7 | |||
6c90f272c9 | |||
4d8def520e | |||
4018d904bc | |||
2e8fab6410 | |||
6cd9352064 | |||
d638b45933 | |||
dda183b617 | |||
ac355a03ba | |||
915966538a | |||
b5e9d3425b | |||
fe5accc6cb | |||
81bf9e4b26 | |||
d010f4f9dd | |||
5eb425b434 | |||
7e5df82593 | |||
9c60d09b15 | |||
33a523a981 | |||
0b500c1d35 | |||
e15c64dce7 | |||
f779a73b4f | |||
b4e0035d5c | |||
2102824caf | |||
6881f9bc43 | |||
adb78c4c98 | |||
0646c79a57 | |||
2caa37324b | |||
70296032d9 | |||
463adcbb9b | |||
e7cb257a02 | |||
9e988d6921 | |||
a29fe0a651 | |||
c41a0e6a19 | |||
be05c3470e | |||
d4d34caf21 | |||
9ca9c8233d | |||
e1eb260ddc | |||
62d33f8235 | |||
8fcad9535c | |||
1c8c4dc203 | |||
bfb0ef399c | |||
1e9e17813e | |||
45cb5c7588 | |||
f40c477662 | |||
cf5e4ca48f | |||
e33f41edf6 | |||
190b16c29e | |||
d6ebdf9ab3 | |||
f10fee0dba | |||
7bb201993e | |||
d78cf8dc83 | |||
34ddeef62d | |||
a59346f789 | |||
3d9c1cacc7 | |||
48f5809cc8 | |||
15d137b9f5 | |||
419f83a417 | |||
a001f00d72 | |||
9747d99d9e | |||
56fa5b44b1 | |||
c2502b86b9 | |||
c80caccf89 | |||
fccbf4869a | |||
039073da1a | |||
bfcb4c9c54 |
14
Dockerfile
14
Dockerfile
@ -1,18 +1,18 @@
|
|||||||
# bump: golang /GOLANG_VERSION=([\d.]+)/ docker:golang|^1
|
# bump: golang /GOLANG_VERSION=([\d.]+)/ docker:golang|^1
|
||||||
# bump: golang link "Release notes" https://golang.org/doc/devel/release.html
|
# bump: golang link "Release notes" https://golang.org/doc/devel/release.html
|
||||||
ARG GOLANG_VERSION=1.17.5
|
ARG GOLANG_VERSION=1.20.5
|
||||||
# bump: youtube-dl /YDL_VERSION=([\d.]+)/ https://github.com/ytdl-org/youtube-dl.git|/^\d/|sort
|
# bump: yt-dlp /YT_DLP=([\d.-]+)/ https://github.com/yt-dlp/yt-dlp.git|/^\d/|sort
|
||||||
# bump: youtube-dl link "Release notes" https://github.com/ytdl-org/youtube-dl/releases/tag/$LATEST
|
# bump: yt-dlp link "Release notes" https://github.com/yt-dlp/yt-dlp/releases/tag/$LATEST
|
||||||
ARG YDL_VERSION=2021.06.06
|
ARG YT_DLP=2023.06.21
|
||||||
|
|
||||||
FROM golang:$GOLANG_VERSION AS base
|
FROM golang:$GOLANG_VERSION AS base
|
||||||
ARG YDL_VERSION
|
ARG YT_DLP
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
apt-get update -q && \
|
apt-get update -q && \
|
||||||
apt-get install -y -q python-is-python3 && \
|
apt-get install -y -q python-is-python3 && \
|
||||||
curl -L -o /usr/local/bin/youtube-dl https://yt-dl.org/downloads/$YDL_VERSION/youtube-dl && \
|
curl -L https://github.com/yt-dlp/yt-dlp/releases/download/$YT_DLP/yt-dlp -o /usr/local/bin/yt-dlp && \
|
||||||
chmod a+x /usr/local/bin/youtube-dl
|
chmod a+x /usr/local/bin/yt-dlp
|
||||||
|
|
||||||
FROM base AS dev
|
FROM base AS dev
|
||||||
|
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
## goutubedl
|
## goutubedl
|
||||||
|
|
||||||
Go wrapper for [youtube-dl](https://github.com/ytdl-org/youtube-dl). API documentation can be found at [godoc.org](https://pkg.go.dev/github.com/wader/goutubedl?tab=doc).
|
Go wrapper for [youtube-dl](https://github.com/ytdl-org/youtube-dl) and [yt-dlp](https://github.com/yt-dlp/yt-dlp), currently tested and
|
||||||
|
developed using yt-dlp.
|
||||||
|
API documentation can be found at [godoc.org](https://pkg.go.dev/github.com/wader/goutubedl?tab=doc).
|
||||||
|
|
||||||
See [youtube-dl documentation](https://github.com/ytdl-org/youtube-dl) for how to
|
See [yt-dlp documentation](https://github.com/yt-dlp/yt-dlp) for how to
|
||||||
install and what is recommended to install in addition to youtube-dl.
|
install and what is recommended to install in addition to youtube-dl.
|
||||||
|
|
||||||
goutubedl default uses `PATH` to find youtube-dl but it can be configured with the `goutubedl.Path`
|
goutubedl default uses `PATH` to find youtube-dl but it can be configured with the `goutubedl.Path`
|
||||||
variable.
|
variable. Default is currently `youtube-dl` for backwards compability. If your using yt-dlp you
|
||||||
|
probably want to set it to `yt-dlp`.
|
||||||
|
|
||||||
Due to the nature and frequent updates of youtube-dl only the latest version
|
Due to the nature and frequent updates of youtube-dl only the latest version
|
||||||
is tested. But it seems to work well with older versions also.
|
is tested. But it seems to work well with older versions also.
|
||||||
|
@ -15,17 +15,23 @@ var dumpFlag = flag.Bool("J", false, "Dump JSON")
|
|||||||
var typeFlag = flag.String("t", "any", "Type")
|
var typeFlag = flag.String("t", "any", "Type")
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
goutubedl.Path = "yt-dlp"
|
||||||
|
|
||||||
log.SetFlags(0)
|
log.SetFlags(0)
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
optType, _ := goutubedl.TypeFromString[*typeFlag]
|
optType := goutubedl.TypeFromString[*typeFlag]
|
||||||
result, err := goutubedl.New(context.Background(), flag.Arg(0), goutubedl.Options{Type: optType})
|
result, err := goutubedl.New(
|
||||||
|
context.Background(),
|
||||||
|
flag.Arg(0),
|
||||||
|
goutubedl.Options{Type: optType, DebugLog: log.Default()},
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if *dumpFlag {
|
if *dumpFlag {
|
||||||
json.NewEncoder(os.Stdout).Encode(result.Info)
|
_ = json.NewEncoder(os.Stdout).Encode(result.Info)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,6 +50,8 @@ func main() {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
io.Copy(f, dr)
|
if _, err := io.Copy(f, dr); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
dr.Close()
|
dr.Close()
|
||||||
}
|
}
|
||||||
|
25
goutubedl.go
25
goutubedl.go
@ -202,10 +202,12 @@ var TypeFromString = map[string]Type{
|
|||||||
// Options for New()
|
// Options for New()
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Type Type
|
Type Type
|
||||||
PlaylistStart uint // --playlist-start
|
PlaylistStart uint // --playlist-start
|
||||||
PlaylistEnd uint // --playlist-end
|
PlaylistEnd uint // --playlist-end
|
||||||
|
Downloader string // --downloader
|
||||||
DownloadThumbnail bool
|
DownloadThumbnail bool
|
||||||
DownloadSubtitles bool
|
DownloadSubtitles bool
|
||||||
|
ProxyUrl string // --proxy URL http://host:port or socks5://host:port
|
||||||
DebugLog Printer
|
DebugLog Printer
|
||||||
StderrFn func(cmd *exec.Cmd) io.Writer // if not nil, function to get Writer for stderr
|
StderrFn func(cmd *exec.Cmd) io.Writer // if not nil, function to get Writer for stderr
|
||||||
HTTPClient *http.Client // Client for download thumbnail and subtitles (nil use http.DefaultClient)
|
HTTPClient *http.Client // Client for download thumbnail and subtitles (nil use http.DefaultClient)
|
||||||
@ -258,6 +260,15 @@ func infoFromURL(ctx context.Context, rawURL string, options Options) (info Info
|
|||||||
"--batch-file", "-",
|
"--batch-file", "-",
|
||||||
"-J",
|
"-J",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if options.ProxyUrl != "" {
|
||||||
|
cmd.Args = append(cmd.Args, "--proxy", options.ProxyUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.Downloader != "" {
|
||||||
|
cmd.Args = append(cmd.Args, "--downloader", options.Downloader)
|
||||||
|
}
|
||||||
|
|
||||||
if options.Type == TypePlaylist {
|
if options.Type == TypePlaylist {
|
||||||
cmd.Args = append(cmd.Args, "--yes-playlist")
|
cmd.Args = append(cmd.Args, "--yes-playlist")
|
||||||
|
|
||||||
@ -455,6 +466,14 @@ func (result Result) Download(ctx context.Context, filter string) (*DownloadResu
|
|||||||
cmd.Args = append(cmd.Args, "-f", filter)
|
cmd.Args = append(cmd.Args, "-f", filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if result.Options.ProxyUrl != "" {
|
||||||
|
cmd.Args = append(cmd.Args, "--proxy", result.Options.ProxyUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Options.Downloader != "" {
|
||||||
|
cmd.Args = append(cmd.Args, "--downloader", result.Options.Downloader)
|
||||||
|
}
|
||||||
|
|
||||||
cmd.Dir = tempPath
|
cmd.Dir = tempPath
|
||||||
var w io.WriteCloser
|
var w io.WriteCloser
|
||||||
dr.reader, w = io.Pipe()
|
dr.reader, w = io.Pipe()
|
||||||
@ -473,7 +492,7 @@ func (result Result) Download(ctx context.Context, filter string) (*DownloadResu
|
|||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
cmd.Wait()
|
_ = cmd.Wait()
|
||||||
w.Close()
|
w.Close()
|
||||||
os.RemoveAll(tempPath)
|
os.RemoveAll(tempPath)
|
||||||
close(dr.waitCh)
|
close(dr.waitCh)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package goutubedl
|
package goutubedl_test
|
||||||
|
|
||||||
// TODO: currently the tests only run on linux as they use osleaktest which only
|
// TODO: currently the tests only run on linux as they use osleaktest which only
|
||||||
// has linux support
|
// has linux support
|
||||||
@ -14,9 +14,15 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/fortytw2/leaktest"
|
"github.com/fortytw2/leaktest"
|
||||||
|
"github.com/wader/goutubedl"
|
||||||
"github.com/wader/osleaktest"
|
"github.com/wader/osleaktest"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// we're using yt-dlp at the moment
|
||||||
|
goutubedl.Path = "yt-dlp"
|
||||||
|
}
|
||||||
|
|
||||||
const testVideoRawURL = "https://www.youtube.com/watch?v=C0DPdy98e4c"
|
const testVideoRawURL = "https://www.youtube.com/watch?v=C0DPdy98e4c"
|
||||||
const playlistRawURL = "https://soundcloud.com/mattheis/sets/kindred-phenomena"
|
const playlistRawURL = "https://soundcloud.com/mattheis/sets/kindred-phenomena"
|
||||||
const subtitlesTestVideoRawURL = "https://www.youtube.com/watch?v=QRS8MkLhQmM"
|
const subtitlesTestVideoRawURL = "https://www.youtube.com/watch?v=QRS8MkLhQmM"
|
||||||
@ -33,10 +39,10 @@ func leakChecks(t *testing.T) func() {
|
|||||||
|
|
||||||
func TestBinaryNotPath(t *testing.T) {
|
func TestBinaryNotPath(t *testing.T) {
|
||||||
defer leakChecks(t)()
|
defer leakChecks(t)()
|
||||||
defer func(orig string) { Path = orig }(Path)
|
defer func(orig string) { goutubedl.Path = orig }(goutubedl.Path)
|
||||||
Path = "/non-existing"
|
goutubedl.Path = "/non-existing"
|
||||||
|
|
||||||
_, versionErr := Version(context.Background())
|
_, versionErr := goutubedl.Version(context.Background())
|
||||||
if versionErr == nil || !strings.Contains(versionErr.Error(), "no such file or directory") {
|
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)
|
t.Fatalf("err should be nil 'no such file or directory': %v", versionErr)
|
||||||
}
|
}
|
||||||
@ -46,7 +52,7 @@ func TestVersion(t *testing.T) {
|
|||||||
defer leakChecks(t)()
|
defer leakChecks(t)()
|
||||||
|
|
||||||
versionRe := regexp.MustCompile(`^\d{4}\.\d{2}.\d{2}.*$`)
|
versionRe := regexp.MustCompile(`^\d{4}\.\d{2}.\d{2}.*$`)
|
||||||
version, versionErr := Version(context.Background())
|
version, versionErr := goutubedl.Version(context.Background())
|
||||||
|
|
||||||
if versionErr != nil {
|
if versionErr != nil {
|
||||||
t.Fatalf("err: %s", versionErr)
|
t.Fatalf("err: %s", versionErr)
|
||||||
@ -61,7 +67,7 @@ func TestDownload(t *testing.T) {
|
|||||||
defer leakChecks(t)()
|
defer leakChecks(t)()
|
||||||
|
|
||||||
stderrBuf := &bytes.Buffer{}
|
stderrBuf := &bytes.Buffer{}
|
||||||
r, err := New(context.Background(), testVideoRawURL, Options{
|
r, err := goutubedl.New(context.Background(), testVideoRawURL, goutubedl.Options{
|
||||||
StderrFn: func(cmd *exec.Cmd) io.Writer {
|
StderrFn: func(cmd *exec.Cmd) io.Writer {
|
||||||
return stderrBuf
|
return stderrBuf
|
||||||
},
|
},
|
||||||
@ -84,8 +90,8 @@ func TestDownload(t *testing.T) {
|
|||||||
t.Errorf("copy n not equal to download buffer: %d!=%d", n, downloadBuf.Len())
|
t.Errorf("copy n not equal to download buffer: %d!=%d", n, downloadBuf.Len())
|
||||||
}
|
}
|
||||||
|
|
||||||
if n < 29000 {
|
if n < 10000 {
|
||||||
t.Errorf("should have copied at least 29000 bytes: %d", n)
|
t.Errorf("should have copied at least 10000 bytes: %d", n)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.Contains(stderrBuf.String(), "Destination") {
|
if !strings.Contains(stderrBuf.String(), "Destination") {
|
||||||
@ -106,7 +112,7 @@ func TestParseInfo(t *testing.T) {
|
|||||||
defer leakChecks(t)()
|
defer leakChecks(t)()
|
||||||
|
|
||||||
ctx, cancelFn := context.WithCancel(context.Background())
|
ctx, cancelFn := context.WithCancel(context.Background())
|
||||||
ydlResult, err := New(ctx, c.url, Options{
|
ydlResult, err := goutubedl.New(ctx, c.url, goutubedl.Options{
|
||||||
DownloadThumbnail: true,
|
DownloadThumbnail: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -156,8 +162,8 @@ func TestParseInfo(t *testing.T) {
|
|||||||
func TestPlaylist(t *testing.T) {
|
func TestPlaylist(t *testing.T) {
|
||||||
defer leakChecks(t)()
|
defer leakChecks(t)()
|
||||||
|
|
||||||
ydlResult, ydlResultErr := New(context.Background(), playlistRawURL, Options{
|
ydlResult, ydlResultErr := goutubedl.New(context.Background(), playlistRawURL, goutubedl.Options{
|
||||||
Type: TypePlaylist,
|
Type: goutubedl.TypePlaylist,
|
||||||
DownloadThumbnail: false,
|
DownloadThumbnail: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -181,14 +187,14 @@ func TestPlaylist(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTestUnsupportedURL(t *testing.T) {
|
func TestUnsupportedURL(t *testing.T) {
|
||||||
defer leaktest.Check(t)()
|
defer leaktest.Check(t)()
|
||||||
|
|
||||||
_, ydlResultErr := New(context.Background(), "https://www.google.com", Options{})
|
_, ydlResultErr := goutubedl.New(context.Background(), "https://www.google.com", goutubedl.Options{})
|
||||||
if ydlResultErr == nil {
|
if ydlResultErr == nil {
|
||||||
t.Errorf("expected unsupported url")
|
t.Errorf("expected unsupported url")
|
||||||
}
|
}
|
||||||
expectedErrPrefix := "Unsupported URL: https://www.google.com"
|
expectedErrPrefix := "Unsupported URL:"
|
||||||
if ydlResultErr != nil && !strings.HasPrefix(ydlResultErr.Error(), expectedErrPrefix) {
|
if ydlResultErr != nil && !strings.HasPrefix(ydlResultErr.Error(), expectedErrPrefix) {
|
||||||
t.Errorf("expected error prefix %q got %q", expectedErrPrefix, ydlResultErr.Error())
|
t.Errorf("expected error prefix %q got %q", expectedErrPrefix, ydlResultErr.Error())
|
||||||
|
|
||||||
@ -199,8 +205,8 @@ func TestPlaylistWithPrivateVideo(t *testing.T) {
|
|||||||
defer leaktest.Check(t)()
|
defer leaktest.Check(t)()
|
||||||
|
|
||||||
playlistRawURL := "https://www.youtube.com/playlist?list=PLX0g748fkegS54oiDN4AXKl7BR7mLIydP"
|
playlistRawURL := "https://www.youtube.com/playlist?list=PLX0g748fkegS54oiDN4AXKl7BR7mLIydP"
|
||||||
ydlResult, ydlResultErr := New(context.Background(), playlistRawURL, Options{
|
ydlResult, ydlResultErr := goutubedl.New(context.Background(), playlistRawURL, goutubedl.Options{
|
||||||
Type: TypePlaylist,
|
Type: goutubedl.TypePlaylist,
|
||||||
DownloadThumbnail: false,
|
DownloadThumbnail: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -218,10 +224,10 @@ func TestPlaylistWithPrivateVideo(t *testing.T) {
|
|||||||
func TestSubtitles(t *testing.T) {
|
func TestSubtitles(t *testing.T) {
|
||||||
defer leakChecks(t)()
|
defer leakChecks(t)()
|
||||||
|
|
||||||
ydlResult, ydlResultErr := New(
|
ydlResult, ydlResultErr := goutubedl.New(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
subtitlesTestVideoRawURL,
|
subtitlesTestVideoRawURL,
|
||||||
Options{
|
goutubedl.Options{
|
||||||
DownloadSubtitles: true,
|
DownloadSubtitles: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -250,11 +256,11 @@ func TestSubtitles(t *testing.T) {
|
|||||||
func TestErrorNotAPlaylist(t *testing.T) {
|
func TestErrorNotAPlaylist(t *testing.T) {
|
||||||
defer leakChecks(t)()
|
defer leakChecks(t)()
|
||||||
|
|
||||||
_, ydlResultErr := New(context.Background(), testVideoRawURL, Options{
|
_, ydlResultErr := goutubedl.New(context.Background(), testVideoRawURL, goutubedl.Options{
|
||||||
Type: TypePlaylist,
|
Type: goutubedl.TypePlaylist,
|
||||||
DownloadThumbnail: false,
|
DownloadThumbnail: false,
|
||||||
})
|
})
|
||||||
if ydlResultErr != ErrNotAPlaylist {
|
if ydlResultErr != goutubedl.ErrNotAPlaylist {
|
||||||
t.Errorf("expected is playlist error, got %s", ydlResultErr)
|
t.Errorf("expected is playlist error, got %s", ydlResultErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -262,11 +268,36 @@ func TestErrorNotAPlaylist(t *testing.T) {
|
|||||||
func TestErrorNotASingleEntry(t *testing.T) {
|
func TestErrorNotASingleEntry(t *testing.T) {
|
||||||
defer leakChecks(t)()
|
defer leakChecks(t)()
|
||||||
|
|
||||||
_, ydlResultErr := New(context.Background(), playlistRawURL, Options{
|
_, ydlResultErr := goutubedl.New(context.Background(), playlistRawURL, goutubedl.Options{
|
||||||
Type: TypeSingle,
|
Type: goutubedl.TypeSingle,
|
||||||
DownloadThumbnail: false,
|
DownloadThumbnail: false,
|
||||||
})
|
})
|
||||||
if ydlResultErr != ErrNotASingleEntry {
|
if ydlResultErr != goutubedl.ErrNotASingleEntry {
|
||||||
t.Errorf("expected is single entry error, got %s", ydlResultErr)
|
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()
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user