init
This commit is contained in:
10
.travis.yml
Normal file
10
.travis.yml
Normal file
@ -0,0 +1,10 @@
|
||||
sudo: required
|
||||
services:
|
||||
- docker
|
||||
|
||||
language: go
|
||||
go:
|
||||
- "1.12"
|
||||
|
||||
# a bit strange but want to reuse the test Dockerfile
|
||||
script: docker build --build-arg GO_VERSION=$(go version | cut -d' ' -f 3 | cut -b 3-) .
|
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@ -0,0 +1,17 @@
|
||||
ARG GO_VERSION=1.12
|
||||
ARG YDL_VERSION=2019.07.16
|
||||
|
||||
FROM golang:$GO_VERSION
|
||||
ARG YDL_VERSION
|
||||
|
||||
RUN \
|
||||
curl -L -o /usr/local/bin/youtube-dl https://yt-dl.org/downloads/$YDL_VERSION/youtube-dl && \
|
||||
chmod a+x /usr/local/bin/youtube-dl
|
||||
|
||||
WORKDIR /src
|
||||
COPY go.* *.go ./
|
||||
COPY cmd cmd
|
||||
RUN \
|
||||
go mod download && \
|
||||
go build ./cmd/goutubedl && \
|
||||
go test -v -race -cover
|
19
LICENSE
Normal file
19
LICENSE
Normal file
@ -0,0 +1,19 @@
|
||||
Copyright (c) 2019 Mattias Wadman
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
20
README.md
Normal file
20
README.md
Normal file
@ -0,0 +1,20 @@
|
||||
## goutubedl
|
||||
|
||||
Go wrapper for [youtube-dl](https://github.com/ytdl-org/youtube-dl).
|
||||
|
||||
[API Documentation](https://godoc.org/github.com/wader/goutubedl) can be found godoc.
|
||||
|
||||
See [youtube-dl documentation](https://github.com/ytdl-org/youtube-dl#do-i-need-any-other-programs)
|
||||
for what is recommended to install in addition to youtube-dl.
|
||||
|
||||
### Usage
|
||||
|
||||
```go
|
||||
result, err := goutubedl.New(context.Background(), URL, goutubedl.Options{})
|
||||
downloadResult, err := result.Download(context.Background(), FormatID)
|
||||
io.Copy(ioutil.Discard, downloadResult)
|
||||
dr.Close()
|
||||
```
|
||||
|
||||
See [goutubedl cmd tool](cmd/goutubedl/main.go) or [ydls](https://github.com/wader/ydls)
|
||||
for usage examples.
|
47
cmd/goutubedl/main.go
Normal file
47
cmd/goutubedl/main.go
Normal file
@ -0,0 +1,47 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/wader/goutubedl"
|
||||
)
|
||||
|
||||
var dumpFlag = flag.Bool("J", false, "Dump JSON")
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
flag.Parse()
|
||||
|
||||
result, err := goutubedl.New(context.Background(), flag.Arg(0), goutubedl.Options{})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if *dumpFlag {
|
||||
json.NewEncoder(os.Stdout).Encode(result.Info)
|
||||
return
|
||||
}
|
||||
|
||||
filter := flag.Arg(1)
|
||||
if filter == "" {
|
||||
filter = "best"
|
||||
}
|
||||
|
||||
dr, err := result.Download(context.Background(), filter)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
f, err := os.Create(filter)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
io.Copy(f, dr)
|
||||
dr.Close()
|
||||
}
|
8
go.mod
Normal file
8
go.mod
Normal file
@ -0,0 +1,8 @@
|
||||
module github.com/wader/goutubedl
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/fortytw2/leaktest v1.3.0
|
||||
github.com/wader/osleaktest v0.0.0-20190723190525-c53af4cfc4a3
|
||||
)
|
4
go.sum
Normal file
4
go.sum
Normal file
@ -0,0 +1,4 @@
|
||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||
github.com/wader/osleaktest v0.0.0-20190723190525-c53af4cfc4a3 h1:4WTMRnBIat+s9D1PIpzh2UUZrEjxzRjJI85IdWePjCI=
|
||||
github.com/wader/osleaktest v0.0.0-20190723190525-c53af4cfc4a3/go.mod h1:XD6emOFPHVzb0+qQpiNOdPL2XZ0SRUM0N5JHuq6OmXo=
|
423
goutubedl.go
Normal file
423
goutubedl.go
Normal file
@ -0,0 +1,423 @@
|
||||
// Package goutubedl provides a wrapper for youtube-dl.
|
||||
package goutubedl
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Path to youtube-dl binary. Default look for "youtube-dl" in PATH.
|
||||
var Path = "youtube-dl"
|
||||
|
||||
// Printer is something that can print
|
||||
type Printer interface {
|
||||
Print(v ...interface{})
|
||||
}
|
||||
|
||||
type nopPrinter struct{}
|
||||
|
||||
func (nopPrinter) Print(v ...interface{}) {}
|
||||
|
||||
// Error youtube-dl specific error
|
||||
type Error string
|
||||
|
||||
func (e Error) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
// Info youtube-dl info
|
||||
type Info struct {
|
||||
// Generated from youtube-dl README using:
|
||||
// sed -e 's/ - `\(.*\)` (\(.*\)): \(.*\)/\1 \2 `json:"\1"` \/\/ \3/' | sed -e 's/numeric/float64/' | sed -e 's/boolean/bool/' | sed -e 's/_id/ID/' | sed -e 's/_count/Count/'| sed -e 's/_uploader/Uploader/' | sed -e 's/_key/Key/' | sed -e 's/_year/Year/' | sed -e 's/_title/Title/' | sed -e 's/_rating/Rating/' | sed -e 's/_number/Number/' | awk '{print toupper(substr($0, 0, 1)) substr($0, 2)}'
|
||||
ID string `json:"id"` // Video identifier
|
||||
Title string `json:"title"` // Video title
|
||||
URL string `json:"url"` // Video URL
|
||||
AltTitle string `json:"alt_title"` // A secondary title of the video
|
||||
DisplayID string `json:"display_id"` // An alternative identifier for the video
|
||||
Uploader string `json:"uploader"` // Full name of the video uploader
|
||||
License string `json:"license"` // License name the video is licensed under
|
||||
Creator string `json:"creator"` // The creator of the video
|
||||
ReleaseDate string `json:"release_date"` // The date (YYYYMMDD) when the video was released
|
||||
Timestamp float64 `json:"timestamp"` // UNIX timestamp of the moment the video became available
|
||||
UploadDate string `json:"upload_date"` // Video upload date (YYYYMMDD)
|
||||
UploaderID string `json:"uploader_id"` // Nickname or id of the video uploader
|
||||
Channel string `json:"channel"` // Full name of the channel the video is uploaded on
|
||||
ChannelID string `json:"channel_id"` // Id of the channel
|
||||
Location string `json:"location"` // Physical location where the video was filmed
|
||||
Duration float64 `json:"duration"` // Length of the video in seconds
|
||||
ViewCount float64 `json:"view_count"` // How many users have watched the video on the platform
|
||||
LikeCount float64 `json:"like_count"` // Number of positive ratings of the video
|
||||
DislikeCount float64 `json:"dislike_count"` // Number of negative ratings of the video
|
||||
RepostCount float64 `json:"repost_count"` // Number of reposts of the video
|
||||
AverageRating float64 `json:"average_rating"` // Average rating give by users, the scale used depends on the webpage
|
||||
CommentCount float64 `json:"comment_count"` // Number of comments on the video
|
||||
AgeLimit float64 `json:"age_limit"` // Age restriction for the video (years)
|
||||
IsLive bool `json:"is_live"` // Whether this video is a live stream or a fixed-length video
|
||||
StartTime float64 `json:"start_time"` // Time in seconds where the reproduction should start, as specified in the URL
|
||||
EndTime float64 `json:"end_time"` // Time in seconds where the reproduction should end, as specified in the URL
|
||||
Extractor string `json:"extractor"` // Name of the extractor
|
||||
ExtractorKey string `json:"extractor_key"` // Key name of the extractor
|
||||
Epoch float64 `json:"epoch"` // Unix epoch when creating the file
|
||||
Autonumber float64 `json:"autonumber"` // Five-digit number that will be increased with each download, starting at zero
|
||||
Playlist string `json:"playlist"` // Name or id of the playlist that contains the video
|
||||
PlaylistIndex float64 `json:"playlist_index"` // Index of the video in the playlist padded with leading zeros according to the total length of the playlist
|
||||
PlaylistID string `json:"playlist_id"` // Playlist identifier
|
||||
PlaylistTitle string `json:"playlist_title"` // Playlist title
|
||||
PlaylistUploader string `json:"playlist_uploader"` // Full name of the playlist uploader
|
||||
PlaylistUploaderID string `json:"playlist_uploader_id"` // Nickname or id of the playlist uploader
|
||||
|
||||
// Available for the video that belongs to some logical chapter or section:
|
||||
Chapter string `json:"chapter"` // Name or title of the chapter the video belongs to
|
||||
ChapterNumber float64 `json:"chapter_number"` // Number of the chapter the video belongs to
|
||||
ChapterID string `json:"chapter_id"` // Id of the chapter the video belongs to
|
||||
|
||||
// Available for the video that is an episode of some series or programme:
|
||||
Series string `json:"series"` // Title of the series or programme the video episode belongs to
|
||||
Season string `json:"season"` // Title of the season the video episode belongs to
|
||||
SeasonNumber float64 `json:"season_number"` // Number of the season the video episode belongs to
|
||||
SeasonID string `json:"season_id"` // Id of the season the video episode belongs to
|
||||
Episode string `json:"episode"` // Title of the video episode
|
||||
EpisodeNumber float64 `json:"episode_number"` // Number of the video episode within a season
|
||||
EpisodeID string `json:"episode_id"` // Id of the video episode
|
||||
|
||||
// Available for the media that is a track or a part of a music album:
|
||||
Track string `json:"track"` // Title of the track
|
||||
TrackNumber float64 `json:"track_number"` // Number of the track within an album or a disc
|
||||
TrackID string `json:"track_id"` // Id of the track
|
||||
Artist string `json:"artist"` // Artist(s) of the track
|
||||
Genre string `json:"genre"` // Genre(s) of the track
|
||||
Album string `json:"album"` // Title of the album the track belongs to
|
||||
AlbumType string `json:"album_type"` // Type of the album
|
||||
AlbumArtist string `json:"album_artist"` // List of all artists appeared on the album
|
||||
DiscNumber float64 `json:"disc_number"` // Number of the disc or other physical medium the track belongs to
|
||||
ReleaseYear float64 `json:"release_year"` // Year (YYYY) when the album was released
|
||||
|
||||
Type string `json:"_type"`
|
||||
Direct bool `json:"direct"`
|
||||
WebpageURL string `json:"webpage_url"`
|
||||
Description string `json:"description"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
// not unmarshalled, populated from image thumbnail file
|
||||
ThumbnailBytes []byte `json:"-"`
|
||||
|
||||
Formats []Format `json:"formats"`
|
||||
Subtitles map[string][]Subtitle `json:"subtitles"`
|
||||
|
||||
// Playlist entries if _type is playlist
|
||||
Entries []Info `json:"entries"`
|
||||
|
||||
// Info can also be a mix of Info and one Format
|
||||
Format
|
||||
}
|
||||
|
||||
// Format youtube-dl downloadable format
|
||||
type Format struct {
|
||||
Ext string `json:"ext"` // Video filename extension
|
||||
Format string `json:"format"` // A human-readable description of the format
|
||||
FormatID string `json:"format_id"` // Format code specified by `--format`
|
||||
FormatNote string `json:"format_note"` // Additional info about the format
|
||||
Width float64 `json:"width"` // Width of the video
|
||||
Height float64 `json:"height"` // Height of the video
|
||||
Resolution string `json:"resolution"` // Textual description of width and height
|
||||
TBR float64 `json:"tbr"` // Average bitrate of audio and video in KBit/s
|
||||
ABR float64 `json:"abr"` // Average audio bitrate in KBit/s
|
||||
ACodec string `json:"acodec"` // Name of the audio codec in use
|
||||
ASR float64 `json:"asr"` // Audio sampling rate in Hertz
|
||||
VBR float64 `json:"vbr"` // Average video bitrate in KBit/s
|
||||
FPS float64 `json:"fps"` // Frame rate
|
||||
VCodec string `json:"vcodec"` // Name of the video codec in use
|
||||
Container string `json:"container"` // Name of the container format
|
||||
Filesize float64 `json:"filesize"` // The number of bytes, if known in advance
|
||||
FilesizeApprox float64 `json:"filesize_approx"` // An estimate for the number of bytes
|
||||
Protocol string `json:"protocol"` // The protocol that will be used for the actual download
|
||||
}
|
||||
|
||||
// Subtitle youtube-dl subtitle
|
||||
type Subtitle struct {
|
||||
URL string `json:"url"`
|
||||
Ext string `json:"ext"`
|
||||
Language string `json:"-"`
|
||||
// not unmarshalled, populated from subtitle file
|
||||
Bytes []byte `json:"-"`
|
||||
}
|
||||
|
||||
func (f Format) String() string {
|
||||
return fmt.Sprintf("%s:%s:%s abr:%f vbr:%f tbr:%f",
|
||||
f.FormatID,
|
||||
f.Protocol,
|
||||
f.Ext,
|
||||
f.ABR,
|
||||
f.VBR,
|
||||
f.TBR,
|
||||
)
|
||||
}
|
||||
|
||||
// Options for New()
|
||||
type Options struct {
|
||||
YesPlaylist bool // --yes-playlist
|
||||
PlaylistStart uint // --playlist-start
|
||||
PlaylistEnd uint // --playlist-end
|
||||
DownloadThumbnail bool
|
||||
DownloadSubtitles bool
|
||||
DebugLog Printer
|
||||
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)
|
||||
}
|
||||
|
||||
// Version of youtube-dl.
|
||||
// Might be a good idea to call at start to assert that youtube-dl can be found.
|
||||
func Version(ctx context.Context) (string, error) {
|
||||
cmd := exec.CommandContext(ctx, Path, "--version")
|
||||
versionBytes, cmdErr := cmd.Output()
|
||||
if cmdErr != nil {
|
||||
return "", cmdErr
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(versionBytes)), nil
|
||||
}
|
||||
|
||||
// New downloads metadata for URL
|
||||
func New(ctx context.Context, rawURL string, options Options) (result Result, err error) {
|
||||
if options.DebugLog == nil {
|
||||
options.DebugLog = nopPrinter{}
|
||||
}
|
||||
if options.HTTPClient == nil {
|
||||
options.HTTPClient = http.DefaultClient
|
||||
}
|
||||
|
||||
info, rawJSON, err := infoFromURL(ctx, rawURL, options)
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
|
||||
rawJSONCopy := make([]byte, len(rawJSON))
|
||||
copy(rawJSONCopy, rawJSON)
|
||||
|
||||
return Result{
|
||||
Info: info,
|
||||
RawJSON: rawJSONCopy,
|
||||
Options: options,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func infoFromURL(ctx context.Context, rawURL string, options Options) (info Info, rawJSON []byte, err error) {
|
||||
cmd := exec.CommandContext(
|
||||
ctx,
|
||||
Path,
|
||||
"--no-call-home",
|
||||
"--no-cache-dir",
|
||||
"--skip-download",
|
||||
"--restrict-filenames",
|
||||
// provide URL via stdin for security, youtube-dl has some run command args
|
||||
"--batch-file", "-",
|
||||
"-J",
|
||||
)
|
||||
if options.YesPlaylist {
|
||||
cmd.Args = append(cmd.Args, "--yes-playlist")
|
||||
|
||||
if options.PlaylistStart > 0 {
|
||||
cmd.Args = append(cmd.Args,
|
||||
"--playlist-start", strconv.Itoa(int(options.PlaylistStart)),
|
||||
)
|
||||
}
|
||||
if options.PlaylistEnd > 0 {
|
||||
cmd.Args = append(cmd.Args,
|
||||
"--playlist-end", strconv.Itoa(int(options.PlaylistEnd)),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if options.DownloadSubtitles {
|
||||
cmd.Args = append(cmd.Args,
|
||||
"--all-subs",
|
||||
)
|
||||
}
|
||||
cmd.Args = append(cmd.Args,
|
||||
"--no-playlist",
|
||||
)
|
||||
}
|
||||
|
||||
tempPath, _ := ioutil.TempDir("", "ydls")
|
||||
defer os.RemoveAll(tempPath)
|
||||
|
||||
stdoutBuf := &bytes.Buffer{}
|
||||
stderrBuf := &bytes.Buffer{}
|
||||
stderrWriter := ioutil.Discard
|
||||
if options.StderrFn != nil {
|
||||
stderrWriter = options.StderrFn(cmd)
|
||||
}
|
||||
|
||||
cmd.Dir = tempPath
|
||||
cmd.Stdout = stdoutBuf
|
||||
cmd.Stderr = io.MultiWriter(stderrBuf, stderrWriter)
|
||||
cmd.Stdin = bytes.NewBufferString(rawURL + "\n")
|
||||
|
||||
options.DebugLog.Print("cmd", " ", cmd.Args)
|
||||
cmdErr := cmd.Run()
|
||||
|
||||
stderrLineScanner := bufio.NewScanner(stderrBuf)
|
||||
errMessage := ""
|
||||
for stderrLineScanner.Scan() {
|
||||
const errorPrefix = "ERROR: "
|
||||
line := stderrLineScanner.Text()
|
||||
if strings.HasPrefix(line, errorPrefix) {
|
||||
errMessage = line[len(errorPrefix):]
|
||||
}
|
||||
}
|
||||
|
||||
if errMessage != "" {
|
||||
return Info{}, nil, Error(errMessage)
|
||||
} else if cmdErr != nil {
|
||||
return Info{}, nil, cmdErr
|
||||
}
|
||||
|
||||
if infoErr := json.Unmarshal(stdoutBuf.Bytes(), &info); infoErr != nil {
|
||||
return Info{}, nil, infoErr
|
||||
}
|
||||
|
||||
if options.YesPlaylist && (info.Type != "playlist" || info.Type == "multi_video") {
|
||||
return Info{}, nil, fmt.Errorf("not a playlist")
|
||||
}
|
||||
|
||||
// TODO: use headers from youtube-dl info for thumbnail and subtitle download?
|
||||
if options.DownloadThumbnail && info.Thumbnail != "" {
|
||||
resp, respErr := options.HTTPClient.Get(info.Thumbnail)
|
||||
if respErr == nil {
|
||||
buf, _ := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
info.ThumbnailBytes = buf
|
||||
}
|
||||
}
|
||||
|
||||
for language, subtitles := range info.Subtitles {
|
||||
for i := range subtitles {
|
||||
subtitles[i].Language = language
|
||||
}
|
||||
}
|
||||
|
||||
if options.DownloadSubtitles {
|
||||
for _, subtitles := range info.Subtitles {
|
||||
for i, subtitle := range subtitles {
|
||||
resp, respErr := options.HTTPClient.Get(subtitle.URL)
|
||||
if respErr == nil {
|
||||
buf, _ := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
subtitles[i].Bytes = buf
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return info, stdoutBuf.Bytes(), nil
|
||||
}
|
||||
|
||||
// Result metadata for a URL
|
||||
type Result struct {
|
||||
Info Info
|
||||
RawJSON []byte // saved raw JSON. Used later when downloading
|
||||
Options Options // options passed to New
|
||||
}
|
||||
|
||||
// DownloadResult download result
|
||||
type DownloadResult struct {
|
||||
reader io.ReadCloser
|
||||
waitCh chan struct{}
|
||||
}
|
||||
|
||||
// Download format matched by filter (usually a format id or "best").
|
||||
// Filter should not be a combine filter like "1+2" as then youtube-dl
|
||||
// won't write to stdout.
|
||||
func (result Result) Download(ctx context.Context, filter string) (*DownloadResult, error) {
|
||||
debugLog := result.Options.DebugLog
|
||||
|
||||
if result.Info.Type == "playlist" || result.Info.Type == "multi_video" {
|
||||
return nil, fmt.Errorf("is a playlist")
|
||||
}
|
||||
|
||||
tempPath, tempErr := ioutil.TempDir("", "ydls")
|
||||
if tempErr != nil {
|
||||
return nil, tempErr
|
||||
}
|
||||
jsonTempPath := path.Join(tempPath, "info.json")
|
||||
if err := ioutil.WriteFile(jsonTempPath, result.RawJSON, 0600); err != nil {
|
||||
os.RemoveAll(tempPath)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dr := &DownloadResult{
|
||||
waitCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(
|
||||
ctx,
|
||||
Path,
|
||||
"--no-call-home",
|
||||
"--no-cache-dir",
|
||||
"--ignore-errors",
|
||||
"--newline",
|
||||
"--restrict-filenames",
|
||||
"--load-info", jsonTempPath,
|
||||
"-o", "-",
|
||||
)
|
||||
// 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 {
|
||||
cmd.Args = append(cmd.Args, "-f", filter)
|
||||
}
|
||||
|
||||
cmd.Dir = tempPath
|
||||
var w io.WriteCloser
|
||||
dr.reader, w = io.Pipe()
|
||||
|
||||
stderrWriter := ioutil.Discard
|
||||
if result.Options.StderrFn != nil {
|
||||
stderrWriter = result.Options.StderrFn(cmd)
|
||||
}
|
||||
cmd.Stdout = w
|
||||
cmd.Stderr = stderrWriter
|
||||
|
||||
debugLog.Print("cmd", " ", cmd.Args)
|
||||
if err := cmd.Start(); err != nil {
|
||||
os.RemoveAll(tempPath)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go func() {
|
||||
cmd.Wait()
|
||||
w.Close()
|
||||
os.RemoveAll(tempPath)
|
||||
close(dr.waitCh)
|
||||
}()
|
||||
|
||||
return dr, nil
|
||||
}
|
||||
|
||||
func (dr *DownloadResult) Read(p []byte) (n int, err error) {
|
||||
return dr.reader.Read(p)
|
||||
}
|
||||
|
||||
// Close downloader and wait for resource cleanup
|
||||
func (dr *DownloadResult) Close() error {
|
||||
err := dr.reader.Close()
|
||||
<-dr.waitCh
|
||||
return err
|
||||
}
|
||||
|
||||
// Formats return all formats
|
||||
// helper to take care of mixed info and format
|
||||
func (result Result) Formats() []Format {
|
||||
if len(result.Info.Formats) > 0 {
|
||||
return result.Info.Formats
|
||||
}
|
||||
return []Format{result.Info.Format}
|
||||
}
|
228
goutubedl_test.go
Normal file
228
goutubedl_test.go
Normal file
@ -0,0 +1,228 @@
|
||||
package goutubedl
|
||||
|
||||
// TOOD: currently the tests only run on linux as they use osleaktest which only
|
||||
// has linux support
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/fortytw2/leaktest"
|
||||
"github.com/wader/osleaktest"
|
||||
)
|
||||
|
||||
const testVideoRawURL = "https://www.youtube.com/watch?v=C0DPdy98e4c"
|
||||
const playlistRawURL = "https://soundcloud.com/mattheis/sets/kindred-phenomena"
|
||||
const 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) { Path = orig }(Path)
|
||||
Path = "/non-existing"
|
||||
|
||||
_, versionErr := 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 := Version(context.Background())
|
||||
|
||||
if versionErr != nil {
|
||||
t.Fatalf("err: %s", versionErr)
|
||||
}
|
||||
|
||||
if !versionRe.MatchString(version) {
|
||||
t.Errorf("version %q does match %q", version, versionRe)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownload(t *testing.T) {
|
||||
defer leakChecks(t)()
|
||||
|
||||
stderrBuf := &bytes.Buffer{}
|
||||
r, err := New(context.Background(), testVideoRawURL, 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 < 29000 {
|
||||
t.Errorf("should have copied at least 29000 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"},
|
||||
{"https://www.infoq.com/presentations/Simple-Made-Easy", "Simple Made Easy"},
|
||||
{"https://www.youtube.com/watch?v=uVYWQJ5BB_w", "A Radiolab Producer on the Making of a Podcast"},
|
||||
} {
|
||||
t.Run(c.url, func(t *testing.T) {
|
||||
defer leakChecks(t)()
|
||||
|
||||
ctx, cancelFn := context.WithCancel(context.Background())
|
||||
ydlResult, err := New(ctx, c.url, 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 := New(context.Background(), playlistRawURL, Options{
|
||||
YesPlaylist: true,
|
||||
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 TestPlaylistBadURL(t *testing.T) {
|
||||
defer leakChecks(t)()
|
||||
|
||||
// using a non-playlist url
|
||||
_, ydlResultErr := New(context.Background(), testVideoRawURL, Options{
|
||||
YesPlaylist: true,
|
||||
DownloadThumbnail: false,
|
||||
})
|
||||
|
||||
if ydlResultErr == nil {
|
||||
t.Error("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubtitles(t *testing.T) {
|
||||
defer leakChecks(t)()
|
||||
|
||||
ydlResult, ydlResultErr := New(
|
||||
context.Background(),
|
||||
subtitlesTestVideoRawURL,
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user