Skip to content
This repository has been archived by the owner on Nov 21, 2022. It is now read-only.

Commit

Permalink
Remove ffmpeg requirement and simplify downloader interface
Browse files Browse the repository at this point in the history
- Add goutube package
- remove go-lame
- update unit tests
  • Loading branch information
Nyah Check authored and Nyah committed Apr 9, 2020
1 parent 3a968d4 commit 1dd26b9
Show file tree
Hide file tree
Showing 7 changed files with 48 additions and 279 deletions.
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@

# Build container
FROM golang:1.13-alpine AS go-base
RUN apk add --no-cache git

Expand All @@ -11,7 +13,7 @@ COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"' -o ./youtube-dl download.go main.go


#runtime container
# Runtime container
FROM scratch
RUN echo "Runtime container"
COPY --from=go-base /app/youtube-dl /youtube-dl
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@
Downloaded videos could be converted to `flv` or `mp3` formats.


## Pre-requisites

- Install [FFMPEG](https://github.com/adaptlearning/adapt_authoring/wiki/Installing-FFmpeg)


## Build

```bash
Expand Down Expand Up @@ -50,6 +45,12 @@ Flags:
-h Help page
```

### Example

```console
$ ./youtube-dl -format mp3 https://www.youtube.com/watch?v=jOWsu8ePrbE
```

## Roadmap

* Download youtube video with video id or link and converts to flv or mp3.
Expand Down
221 changes: 15 additions & 206 deletions download.go
Original file line number Diff line number Diff line change
@@ -1,40 +1,18 @@
package main

import (
"bufio"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"context"
"io"
"os"
"os/user"
"path/filepath"
"strings"
"unicode"

"github.com/sirupsen/logrus"
"github.com/viert/go-lame"
"github.com/wader/goutubedl"
)

const (
audioBitRate = 123

streamApiUrl = "http://youtube.com/get_video_info?video_id="
)

type stream map[string]string

type RawVideoStream struct {
VideoId string
VideoInfo string
Title string `json:"title"`
Author string `json:"author"`
URLEncodedFmtStreamMap []stream `json:"url_encoded_fmt_stream_map"`
Status string `json:"status"`
}

// removeWhiteSpace removes white spaces from string
// removeWhiteSpace returns a filename without whitespaces
func removeWhiteSpace(str string) string {
Expand All @@ -58,61 +36,21 @@ func fixExtension(str string) string {
return str
}

// encodeAudioStream consumes a raw data stream and
// encodeAudioStream encodes the data stream in mp3
func encodeAudioStream(file, path, surl string, bitrate uint) error {
data, err := downloadVideoStream(surl)
if err != nil {
log.Printf("Http.Get\nerror: %s\nURL: %s\n", err, surl)
return err
}

tmp, _ := os.OpenFile("_temp_", os.O_CREATE, 0755)
defer tmp.Close()
if _, err := tmp.Write(data); err != nil {
logrus.Errorf("Failed to read response body: %v", err)
return err
}

// Create output file
currentDirectory, err := user.Current()
if err != nil {
logrus.Errorf("Error getting current user directory: %v", err)
return err
}

outputDirectory := currentDirectory.HomeDir + "/Downloads/" + path
outputFile := filepath.Join(outputDirectory, file)
if err := os.MkdirAll(filepath.Dir(outputFile), 0775); err != nil {
logrus.Errorf("Unable to create output directory: %v", err)
}
// decodeVideoStream processes downloaded video stream and
// decodeVideoStream calls helper functions and writes the
// output in the required format
func decodeVideoStream(videoUrl, path, format string) error {

fp, err := os.OpenFile(outputFile, os.O_CREATE, 0755)
// Get video data
res, err := goutubedl.New(context.Background(), videoUrl, goutubedl.Options{})
if err != nil {
logrus.Errorf("Unable to create output file: %v", err)
return err
logrus.Errorf("Unable to create goutube object %s: %v", videoUrl, err)
}
defer fp.Close()

// write audio/video file to output
reader := bufio.NewReader(tmp)
writer := lame.NewEncoder(fp)
defer writer.Close()

writer.SetBrate(int(bitrate))
writer.SetQuality(1)
reader.WriteTo(writer)

return nil
}

// encodeVideoStream consumes video data stream and
// encodeVideoStream encodes the video in flv
func encodeVideoStream(file, path, surl string) error {
data, err := downloadVideoStream(surl)
file := removeWhiteSpace(res.Info.Title) + fixExtension(format)
videoStream, err := res.Download(context.TODO(), format)
if err != nil {
log.Printf("Http.Get\nerror: %s\nURL: %s\n", err, surl)
return err
logrus.Errorf("Unable to download %s stream: %v", format, err)
}

// Create output file
Expand All @@ -135,137 +73,8 @@ func encodeVideoStream(file, path, surl string) error {
}
defer fp.Close()

//saving downloaded file.
if _, err = fp.Write(data); err != nil {
logrus.Errorf("Unable to encode video stream: %s `->` %v", surl, err)
return err
}
return nil
}

// downloadVideoStream downloads video streams from youtube
// downloadVideoStream returns the *http.Reponse body
func downloadVideoStream(url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
logrus.Errorf("Unable to fetch Data stream from URL(%s)\n: %v", url, err)
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != 200 {
logrus.Errorf("Video Download error with status: '%v'", resp.StatusCode)
return nil, errors.New("Non 200 status code received")
}

output, _ := ioutil.ReadAll(resp.Body)

return output, nil
}

// getVideoId extracts the video id string from youtube url
// getVideoId returns a video id string to calling function
func getVideoId(url string) (string, error) {
if len(url) < 15 {
return url, nil
} else {
if !strings.Contains(url, "youtube.com") {
return "", errors.New("Invalid Youtube URL")
}

s := strings.Split(url, "?v=")[1]
if len(s) == 0 {
return s, errors.New("Empty string")
}

return s, nil
}
}

// decodeStream accept Values and decodes them individually
// decodeStream returns the final RawVideoStream object
func decodeStream(values url.Values, streams *RawVideoStream, rawstream []stream) error {
streams.Author = values.Get("author")
streams.Title = values.Get("title")
streamMap := values.Get("url_encoded_fmt_stream_map")

// read and decode streams
streamsList := strings.Split(string(streamMap), ",")
for streamPos, streamRaw := range streamsList {
streamQry, err := url.ParseQuery(streamRaw)
if err != nil {
logrus.Infof("Error occured during stream decoding %d: %s\n", streamPos, err)
continue
}
var sig string
sig = streamQry.Get("sig")
rawstream = append(rawstream, stream{
"quality": streamQry.Get("quality"),
"type": streamQry.Get("type"),
"url": streamQry.Get("url"),
"sig": sig,
"title": values.Get("title"),
"author": values.Get("author"),
})
logrus.Infof("Stream found: quality '%s', format '%s'", streamQry.Get("quality"), streamQry.Get("type"))
}

streams.URLEncodedFmtStreamMap = rawstream
return nil
}

// decodeVideoStream processes downloaded video stream and
// decodeVideoStream calls helper functions and writes the
// output in the required format
func decodeVideoStream(videoId, path, format string, bitrate uint) error {
var decStreams []stream //decoded video streams
rawVideo := new(RawVideoStream) // raw video stream

// Get video data
rawVideo.VideoId = videoId
rawVideo.VideoInfo = streamApiUrl + videoId

data, err := downloadVideoStream(rawVideo.VideoInfo)
if err != nil {
logrus.Errorf("Unable to get video stream: %v", err)
return err
}

parsedResp, err := url.ParseQuery(string(data))
if err != nil {
logrus.Errorf("Error parsing video byte stream: %v", err)
return err
}

status, ok := parsedResp["status"]
if !ok {
return errors.New("No response from server")
}

reason, _ := parsedResp["reason"]
if status[0] == "fail" {
return errors.New(fmt.Sprintf("'fail' response with reason: %s", reason))
} else if status[0] != "ok" {
return errors.New(fmt.Sprintf("'non-success' response with reason: %s", reason))
}

if err := decodeStream(parsedResp, rawVideo, decStreams); err != nil {
return errors.New("Unable to decode raw video streams")
}

file := removeWhiteSpace(rawVideo.Title) + fixExtension(format)
surl := decStreams[0]["url"] + "&signature" + decStreams[0]["sig"]

logrus.Infof("Downloading data to file: %s", file)
if strings.Contains(file, "mp3") {
if err := encodeAudioStream(file, path, surl, bitrate); err != nil {
logrus.Errorf("Unable to encode %s: %v", format, err)
}
} else {
if err := encodeVideoStream(file, path, surl); err != nil {
logrus.Errorf("Unable to encode %s: %v", format, err)
}
}
io.Copy(fp, videoStream)
videoStream.Close()

return nil
}
39 changes: 6 additions & 33 deletions download_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,44 +22,17 @@ func TestApi(t *testing.T) {

// path := "test"
for i, table := range tables {
ID, _ := getVideoId(table.url)
if ID != table.id {
t.Errorf("videoId(%d): expected %q, actual %q", i, table.id, ID)
err := decodeVideoStream(table.url, "~/Downloads", "mp3")
if err != nil {
t.Errorf("videoId(%d): expected %q, actual %q", i, table.id, err)
}

// if ID != "" {
// if err := getVideoStream("mp3", ID, path, 192); err != nil {
// t.Errorf("videoStream(%d): expected %v, actual %v", i, nil, err)
// }
// }
}
}

func TestGetVideoId(t *testing.T) {
urls := []string{"https://www.youtube.com/watch?v=HpNluHOAJFA"}

url, err := getVideoId(urls[0])
if err != nil {
t.Errorf("videoId: expected %q, actual %q", "HpNluHOAJFA", url)
}
}

func BenchmarkVideoId(b *testing.B) {
for n := 0; n < b.N; n++ {
getVideoId(tables[0].url)
if err := decodeVideoStream(tables[0].url, "~/Downloads", "mp3"); err != nil {
b.Errorf("Error downloading video: %v", err)
}
}
}

// func BenchmarkApivideoStream(b *testing.B) {
// for n := 0; n < b.N; n++ {
// getVideoStream("mp3", tables[0].id, "~/Downloads", 192)
// }
// }

/*func BenchmarkApiConvertVideo(b *testing.B) {
path := "~/Downloads/"
for n := 0; n < b.N; n++ {
file := path + tables[0].id + ".mp3"
convertVideo(file, 123, tables[0].id, vid)
}
}*/
7 changes: 4 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ go 1.12

require (
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/kr/pretty v0.2.0 // indirect
github.com/sirupsen/logrus v1.4.2
github.com/stretchr/objx v0.2.0 // indirect
github.com/stretchr/testify v1.4.0 // indirect
github.com/viert/go-lame v0.0.0-20190822173615-801f1be8d24f
golang.org/x/sys v0.0.0-20200103143344-a1369afcdac7 // indirect
github.com/wader/goutubedl v0.0.0-20200115162246-9eae90476a5d
github.com/wader/osleaktest v0.0.0-20191111175233-f643b0fed071 // indirect
golang.org/x/sys v0.0.0-20200117145432-59e60aa80a0c // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v2 v2.2.7 // indirect
)
Loading

0 comments on commit 1dd26b9

Please sign in to comment.