init
This commit is contained in:
12
vendor/github.com/rylio/ytdl/Dockerfile
generated
vendored
Normal file
12
vendor/github.com/rylio/ytdl/Dockerfile
generated
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM golang:alpine
|
||||
|
||||
COPY . $GOPATH/src/github.com/rylio/ytdl/
|
||||
RUN apk update && apk upgrade && \
|
||||
apk add --no-cache git
|
||||
RUN go get -d github.com/rylio/ytdl/cmd/ytdl/
|
||||
RUN apk del git
|
||||
WORKDIR $GOPATH/src/github.com/rylio/ytdl/cmd/ytdl/
|
||||
RUN go build -o /go/bin/ytdl
|
||||
WORKDIR /ytdl/
|
||||
|
||||
ENTRYPOINT ["/go/bin/ytdl"]
|
25
vendor/github.com/rylio/ytdl/LICENSE
generated
vendored
Normal file
25
vendor/github.com/rylio/ytdl/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
Copyright (c) <2015> <Ryan Coffman>
|
||||
|
||||
|
||||
|
||||
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.
|
120
vendor/github.com/rylio/ytdl/README.md
generated
vendored
Normal file
120
vendor/github.com/rylio/ytdl/README.md
generated
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
# ytdl [](https://travis-ci.org/rylio/ytdl) [](https://godoc.org/github.com/rylio/ytdl)
|
||||
|
||||
Go library for downloading YouTube videos
|
||||
|
||||
[Documentation: https://godoc.org/github.com/rylio/ytdl](https://godoc.org/github.com/rylio/ytdl "ytdl")
|
||||
|
||||
## Example
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/otium/ytdl"
|
||||
"os"
|
||||
)
|
||||
|
||||
vid, err := ytdl.GetVideoInfo("https://www.youtube.com/watch?v=1rZ-JorHJEY")
|
||||
file, _ = os.Create(vid.Title + ".mp4")
|
||||
defer file.Close()
|
||||
vid.Download(file)
|
||||
```
|
||||
|
||||
## ytdl CLI
|
||||
|
||||
- To install: `go get -u github.com/rylio/ytdl/...`
|
||||
|
||||
- Or use Docker image `docker pull brucwangno1/ytdl:1.0`
|
||||
|
||||
### Usage
|
||||
|
||||
- `ytdl [global options] [youtube url or video id]`
|
||||
- Or using Docker: `docker run -it --rm -v /directory/you/want/to/save/the/download/:/ytdl/ brucewangno1/ytdl:1.0 [global options] "[youtube url or video id]"`
|
||||
|
||||
### Options
|
||||
|
||||
- `--help, -h` - show help
|
||||
- `--filter, -f` - Filter out formats
|
||||
- Syntax: `-f key:value1,value2,...,valueN`
|
||||
- Shortcuts for best/worst(e.g. `-f best`)
|
||||
- `best`/`worst` - best/worst video and audio
|
||||
- `best-video`/`worst-video` - best/worst video
|
||||
- `best-fps`/`worst-fps` - best/worst video with fps as the first priority
|
||||
- `best-audio`/`worst-audio` - best/worst audio
|
||||
- To exclude: -f !key:value1,...
|
||||
- Available keys (See format.go for available values):
|
||||
- `ext` - extension of video
|
||||
- `res` - resolution of video
|
||||
- `videnc` - video encoding
|
||||
- `audenc` - audio encoding
|
||||
- `prof` - youtube video profile
|
||||
- `audbr` - audio bitrate
|
||||
- Default filters
|
||||
- `ext:mp4`
|
||||
- `!videnc:`
|
||||
- `!audenc:`
|
||||
- `best`
|
||||
- `--output, -o` - Output to specific path
|
||||
- Supports templates, ex: {{.Title}}.{{.Ext}}
|
||||
- Defaults to `{{.Title}}.{{.Ext}}`
|
||||
- Supported template variables are Title, Ext, DatePublished, Resolution
|
||||
- Pass - to output to stdout, former stdout output is redirected to stderr
|
||||
- `--info, -i` - Just gets video info, outputs to stdout
|
||||
- `--no-progress` - Disables the progress bar
|
||||
- `--silent, -s` - Disables all output, except for fatal errors
|
||||
- `--debug, -d` - Output debug logs
|
||||
- `--append, -a` - append to output file, instead of truncating
|
||||
- `--range, -r` - specify a range of bytes, placed in http range header, ex: 0-100
|
||||
- `--download-url, -u` - just print download url to, don't do anything else
|
||||
- `--version, -v` - print out ytdl cli version
|
||||
- `--start-offset` - offset the beginning of the video by a duration of time(e.g. 20s or 1m)
|
||||
- `--download-option, -p` - Print video and audio download options and accept input interactively
|
||||
|
||||
|
||||
### Examples
|
||||
|
||||
Download content based on itag
|
||||
|
||||
```bash
|
||||
ytdl -f itag:22 'https://www.youtube.com/watch?v=9bZkp7q19f0'
|
||||
```
|
||||
|
||||
Download content with the best fps
|
||||
|
||||
```bash
|
||||
ytdl -f best-fps 'https://www.youtube.com/watch?v=9bZkp7q19f0'
|
||||
```
|
||||
|
||||
Get all download formats (Requires [jq](https://github.com/stedolan/jq) to be installed)
|
||||
|
||||
```bash
|
||||
./ytdl -j 'http://youtube.com/watch?v=9bZkp7q19f0' | jq ".formats"
|
||||
```
|
||||
|
||||
Extract title of the video (Requires [jq](https://github.com/stedolan/jq) to be installed)
|
||||
|
||||
```bash
|
||||
ytdl -j 'http://youtube.com/watch?v=9bZkp7q19f0' | jq ".title"
|
||||
```
|
||||
|
||||
Print download url without downloading the content
|
||||
|
||||
```bash
|
||||
ytdl -f itag:22 --download-url 'https://www.youtube.com/watch?v=9bZkp7q19f0'
|
||||
```
|
||||
|
||||
Print video and audio download options and accept input interactively
|
||||
|
||||
```bash
|
||||
ytdl -p 'https://www.youtube.com/watch?v=9bZkp7q19f0'
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork it
|
||||
2. Create your feature branch (`git checkout -b my-new-feature`)
|
||||
3. Commit your changes (`git commit -am 'Added some feature'`)
|
||||
4. Push to the branch (`git push origin my-new-feature`)
|
||||
5. Create new Pull Request
|
||||
|
||||
## License
|
||||
|
||||
ytdl is released under the MIT License, see LICENSE for more details.
|
583
vendor/github.com/rylio/ytdl/format.go
generated
vendored
Normal file
583
vendor/github.com/rylio/ytdl/format.go
generated
vendored
Normal file
@@ -0,0 +1,583 @@
|
||||
package ytdl
|
||||
|
||||
import "strconv"
|
||||
|
||||
// FormatKey is a string type containing a key in a video format map
|
||||
type FormatKey string
|
||||
|
||||
// Available format Keys
|
||||
const (
|
||||
FormatExtensionKey FormatKey = "ext"
|
||||
FormatResolutionKey FormatKey = "res"
|
||||
FormatVideoEncodingKey FormatKey = "videnc"
|
||||
FormatAudioEncodingKey FormatKey = "audenc"
|
||||
FormatItagKey FormatKey = "itag"
|
||||
FormatAudioBitrateKey FormatKey = "audbr"
|
||||
FormatFPSKey FormatKey = "fps"
|
||||
)
|
||||
|
||||
// Format is a youtube is a static youtube video format
|
||||
type Format struct {
|
||||
Itag int `json:"itag"`
|
||||
Extension string `json:"extension"`
|
||||
Resolution string `json:"resolution"`
|
||||
VideoEncoding string `json:"videoEncoding"`
|
||||
AudioEncoding string `json:"audioEncoding"`
|
||||
AudioBitrate int `json:"audioBitrate"`
|
||||
meta map[string]interface{}
|
||||
}
|
||||
|
||||
func newFormat(itag int) (Format, bool) {
|
||||
if f, ok := FORMATS[itag]; ok {
|
||||
f.meta = make(map[string]interface{})
|
||||
return f, true
|
||||
}
|
||||
return Format{}, false
|
||||
}
|
||||
|
||||
// ValueForKey gets the format value for a format key, used for filtering
|
||||
func (f Format) ValueForKey(key FormatKey) interface{} {
|
||||
switch key {
|
||||
case FormatItagKey:
|
||||
return f.Itag
|
||||
case FormatExtensionKey:
|
||||
return f.Extension
|
||||
case FormatResolutionKey:
|
||||
return f.Resolution
|
||||
case FormatVideoEncodingKey:
|
||||
return f.VideoEncoding
|
||||
case FormatAudioEncodingKey:
|
||||
return f.AudioEncoding
|
||||
case FormatAudioBitrateKey:
|
||||
return f.AudioBitrate
|
||||
default:
|
||||
if f.meta != nil {
|
||||
return f.meta[string(key)]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (f Format) CompareKey(other Format, key FormatKey) int {
|
||||
switch key {
|
||||
case FormatResolutionKey:
|
||||
res := f.ValueForKey(key).(string)
|
||||
res1, res2 := 0, 0
|
||||
if res != "" {
|
||||
res1, _ = strconv.Atoi(res[0 : len(res)-2])
|
||||
}
|
||||
res = other.ValueForKey(key).(string)
|
||||
if res != "" {
|
||||
res2, _ = strconv.Atoi(res[0 : len(res)-2])
|
||||
}
|
||||
return res1 - res2
|
||||
case FormatAudioBitrateKey:
|
||||
return f.ValueForKey(key).(int) - other.ValueForKey(key).(int)
|
||||
case FormatFPSKey:
|
||||
if f.ValueForKey(key) == nil {
|
||||
return -1
|
||||
} else if other.ValueForKey(key) == nil {
|
||||
return 1
|
||||
} else {
|
||||
a, _ := strconv.Atoi(f.ValueForKey(key).(string))
|
||||
b, _ := strconv.Atoi(other.ValueForKey(key).(string))
|
||||
return a - b
|
||||
}
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// FORMATS is a map of all itags and their formats
|
||||
var FORMATS = map[int]Format{
|
||||
5: Format{
|
||||
Extension: "flv",
|
||||
Resolution: "240p",
|
||||
VideoEncoding: "Sorenson H.283",
|
||||
AudioEncoding: "mp3",
|
||||
Itag: 5,
|
||||
AudioBitrate: 64,
|
||||
},
|
||||
6: Format{
|
||||
Extension: "flv",
|
||||
Resolution: "270p",
|
||||
VideoEncoding: "Sorenson H.263",
|
||||
AudioEncoding: "mp3",
|
||||
Itag: 6,
|
||||
AudioBitrate: 64,
|
||||
},
|
||||
13: Format{
|
||||
Extension: "3gp",
|
||||
Resolution: "",
|
||||
VideoEncoding: "MPEG-4 Visual",
|
||||
AudioEncoding: "aac",
|
||||
Itag: 13,
|
||||
AudioBitrate: 0,
|
||||
},
|
||||
17: Format{
|
||||
Extension: "3gp",
|
||||
Resolution: "144p",
|
||||
VideoEncoding: "MPEG-4 Visual",
|
||||
AudioEncoding: "aac",
|
||||
Itag: 17,
|
||||
AudioBitrate: 24,
|
||||
},
|
||||
18: Format{
|
||||
Extension: "mp4",
|
||||
Resolution: "360p",
|
||||
VideoEncoding: "H.264",
|
||||
AudioEncoding: "aac",
|
||||
Itag: 18,
|
||||
AudioBitrate: 96,
|
||||
},
|
||||
22: Format{
|
||||
Extension: "mp4",
|
||||
Resolution: "720p",
|
||||
VideoEncoding: "H.264",
|
||||
AudioEncoding: "aac",
|
||||
Itag: 22,
|
||||
AudioBitrate: 192,
|
||||
},
|
||||
34: Format{
|
||||
Extension: "flv",
|
||||
Resolution: "480p",
|
||||
VideoEncoding: "H.264",
|
||||
AudioEncoding: "aac",
|
||||
Itag: 34,
|
||||
AudioBitrate: 128,
|
||||
},
|
||||
35: Format{
|
||||
Extension: "flv",
|
||||
Resolution: "360p",
|
||||
VideoEncoding: "H.264",
|
||||
AudioEncoding: "aac",
|
||||
Itag: 35,
|
||||
AudioBitrate: 128,
|
||||
},
|
||||
36: Format{
|
||||
Extension: "3gp",
|
||||
Resolution: "240p",
|
||||
VideoEncoding: "MPEG-4 Visual",
|
||||
AudioEncoding: "aac",
|
||||
Itag: 36,
|
||||
AudioBitrate: 36,
|
||||
},
|
||||
37: Format{
|
||||
Extension: "mp4",
|
||||
Resolution: "1080p",
|
||||
VideoEncoding: "H.264",
|
||||
AudioEncoding: "aac",
|
||||
Itag: 37,
|
||||
AudioBitrate: 192,
|
||||
},
|
||||
38: Format{
|
||||
Extension: "mp4",
|
||||
Resolution: "3072p",
|
||||
VideoEncoding: "H.264",
|
||||
AudioEncoding: "aac",
|
||||
Itag: 38,
|
||||
AudioBitrate: 192,
|
||||
},
|
||||
43: Format{
|
||||
Extension: "webm",
|
||||
Resolution: "360p",
|
||||
VideoEncoding: "VP8",
|
||||
AudioEncoding: "vorbis",
|
||||
Itag: 43,
|
||||
AudioBitrate: 128,
|
||||
},
|
||||
44: Format{
|
||||
Extension: "webm",
|
||||
Resolution: "480p",
|
||||
VideoEncoding: "VP8",
|
||||
AudioEncoding: "vorbis",
|
||||
Itag: 44,
|
||||
AudioBitrate: 128,
|
||||
},
|
||||
45: Format{
|
||||
Extension: "webm",
|
||||
Resolution: "720p",
|
||||
VideoEncoding: "VP8",
|
||||
AudioEncoding: "vorbis",
|
||||
Itag: 45,
|
||||
AudioBitrate: 192,
|
||||
},
|
||||
46: Format{
|
||||
Extension: "webm",
|
||||
Resolution: "1080p",
|
||||
VideoEncoding: "VP8",
|
||||
AudioEncoding: "vorbis",
|
||||
Itag: 46,
|
||||
AudioBitrate: 192,
|
||||
},
|
||||
82: Format{
|
||||
Extension: "mp4",
|
||||
Resolution: "360p",
|
||||
VideoEncoding: "H.264",
|
||||
Itag: 82,
|
||||
AudioBitrate: 96,
|
||||
},
|
||||
83: Format{
|
||||
Extension: "mp4",
|
||||
Resolution: "240p",
|
||||
VideoEncoding: "H.264",
|
||||
AudioEncoding: "aac",
|
||||
Itag: 83,
|
||||
AudioBitrate: 96,
|
||||
},
|
||||
84: Format{
|
||||
Extension: "mp4",
|
||||
Resolution: "720p",
|
||||
VideoEncoding: "H.264",
|
||||
AudioEncoding: "aac",
|
||||
Itag: 84,
|
||||
AudioBitrate: 192,
|
||||
},
|
||||
85: Format{
|
||||
Extension: "mp4",
|
||||
Resolution: "1080p",
|
||||
VideoEncoding: "H.264",
|
||||
AudioEncoding: "aac",
|
||||
Itag: 85,
|
||||
AudioBitrate: 192,
|
||||
},
|
||||
100: Format{
|
||||
Extension: "webm",
|
||||
Resolution: "360p",
|
||||
VideoEncoding: "VP8",
|
||||
AudioEncoding: "vorbis",
|
||||
Itag: 100,
|
||||
AudioBitrate: 128,
|
||||
},
|
||||
101: Format{
|
||||
Extension: "webm",
|
||||
Resolution: "360p",
|
||||
VideoEncoding: "VP8",
|
||||
AudioEncoding: "vorbis",
|
||||
Itag: 101,
|
||||
AudioBitrate: 192,
|
||||
},
|
||||
102: Format{
|
||||
Extension: "webm",
|
||||
Resolution: "720p",
|
||||
VideoEncoding: "VP8",
|
||||
AudioEncoding: "vorbis",
|
||||
Itag: 102,
|
||||
AudioBitrate: 192,
|
||||
},
|
||||
// DASH (video only)
|
||||
133: Format{
|
||||
Extension: "mp4",
|
||||
Resolution: "240p",
|
||||
VideoEncoding: "H.264",
|
||||
AudioEncoding: "",
|
||||
Itag: 133,
|
||||
AudioBitrate: 0,
|
||||
},
|
||||
134: Format{
|
||||
Extension: "mp4",
|
||||
Resolution: "360p",
|
||||
VideoEncoding: "H.264",
|
||||
AudioEncoding: "",
|
||||
Itag: 134,
|
||||
AudioBitrate: 0,
|
||||
},
|
||||
135: Format{
|
||||
Extension: "mp4",
|
||||
Resolution: "480p",
|
||||
VideoEncoding: "H.264",
|
||||
AudioEncoding: "",
|
||||
Itag: 135,
|
||||
AudioBitrate: 0,
|
||||
},
|
||||
136: Format{
|
||||
Extension: "mp4",
|
||||
Resolution: "720p",
|
||||
VideoEncoding: "H.264",
|
||||
AudioEncoding: "",
|
||||
Itag: 136,
|
||||
AudioBitrate: 0,
|
||||
},
|
||||
137: Format{
|
||||
Extension: "mp4",
|
||||
Resolution: "1080p",
|
||||
VideoEncoding: "H.264",
|
||||
AudioEncoding: "",
|
||||
Itag: 137,
|
||||
AudioBitrate: 0,
|
||||
},
|
||||
138: Format{
|
||||
Extension: "mp4",
|
||||
Resolution: "2160p",
|
||||
VideoEncoding: "H.264",
|
||||
AudioEncoding: "",
|
||||
Itag: 138,
|
||||
AudioBitrate: 0,
|
||||
},
|
||||
160: Format{
|
||||
Extension: "mp4",
|
||||
Resolution: "144p",
|
||||
VideoEncoding: "H.264",
|
||||
AudioEncoding: "",
|
||||
Itag: 160,
|
||||
AudioBitrate: 0,
|
||||
},
|
||||
242: Format{
|
||||
Extension: "webm",
|
||||
Resolution: "240p",
|
||||
VideoEncoding: "VP9",
|
||||
AudioEncoding: "",
|
||||
Itag: 242,
|
||||
AudioBitrate: 0,
|
||||
},
|
||||
243: Format{
|
||||
Extension: "webm",
|
||||
Resolution: "360p",
|
||||
VideoEncoding: "VP9",
|
||||
AudioEncoding: "",
|
||||
Itag: 243,
|
||||
AudioBitrate: 0,
|
||||
},
|
||||
244: Format{
|
||||
Extension: "webm",
|
||||
Resolution: "480p",
|
||||
VideoEncoding: "VP9",
|
||||
AudioEncoding: "",
|
||||
Itag: 244,
|
||||
AudioBitrate: 0,
|
||||
},
|
||||
247: Format{
|
||||
Extension: "webm",
|
||||
Resolution: "720p",
|
||||
VideoEncoding: "VP9",
|
||||
AudioEncoding: "",
|
||||
Itag: 247,
|
||||
AudioBitrate: 0,
|
||||
},
|
||||
248: Format{
|
||||
Extension: "webm",
|
||||
Resolution: "1080p",
|
||||
VideoEncoding: "VP9",
|
||||
AudioEncoding: "",
|
||||
Itag: 248,
|
||||
AudioBitrate: 9,
|
||||
},
|
||||
264: Format{
|
||||
Extension: "mp4",
|
||||
Resolution: "1440p",
|
||||
VideoEncoding: "H.264",
|
||||
AudioEncoding: "",
|
||||
Itag: 264,
|
||||
AudioBitrate: 0,
|
||||
},
|
||||
266: Format{
|
||||
Extension: "mp4",
|
||||
Resolution: "2160p",
|
||||
VideoEncoding: "H.264",
|
||||
AudioEncoding: "",
|
||||
Itag: 266,
|
||||
AudioBitrate: 0,
|
||||
},
|
||||
271: Format{
|
||||
Extension: "webm",
|
||||
Resolution: "1440p",
|
||||
VideoEncoding: "VP9",
|
||||
AudioEncoding: "",
|
||||
Itag: 271,
|
||||
AudioBitrate: 0,
|
||||
},
|
||||
272: Format{
|
||||
Extension: "webm",
|
||||
Resolution: "2160p",
|
||||
VideoEncoding: "VP9",
|
||||
AudioEncoding: "",
|
||||
Itag: 272,
|
||||
AudioBitrate: 0,
|
||||
},
|
||||
278: Format{
|
||||
Extension: "webm",
|
||||
Resolution: "144p",
|
||||
VideoEncoding: "VP9",
|
||||
AudioEncoding: "",
|
||||
Itag: 278,
|
||||
AudioBitrate: 0,
|
||||
},
|
||||
298: Format{
|
||||
Extension: "mp4",
|
||||
Resolution: "720p",
|
||||
VideoEncoding: "H.264",
|
||||
AudioEncoding: "",
|
||||
Itag: 298,
|
||||
AudioBitrate: 0,
|
||||
},
|
||||
299: Format{
|
||||
Extension: "mp4",
|
||||
Resolution: "1080p",
|
||||
VideoEncoding: "H.264",
|
||||
AudioEncoding: "",
|
||||
Itag: 299,
|
||||
AudioBitrate: 0,
|
||||
},
|
||||
302: Format{
|
||||
Extension: "webm",
|
||||
Resolution: "720p",
|
||||
VideoEncoding: "VP9",
|
||||
AudioEncoding: "",
|
||||
Itag: 302,
|
||||
AudioBitrate: 0,
|
||||
},
|
||||
303: Format{
|
||||
Extension: "webm",
|
||||
Resolution: "1080p",
|
||||
VideoEncoding: "VP9",
|
||||
AudioEncoding: "",
|
||||
Itag: 303,
|
||||
AudioBitrate: 0,
|
||||
},
|
||||
// DASH (audio only)
|
||||
139: Format{
|
||||
Extension: "mp4",
|
||||
Resolution: "",
|
||||
VideoEncoding: "",
|
||||
AudioEncoding: "aac",
|
||||
Itag: 139,
|
||||
AudioBitrate: 48,
|
||||
},
|
||||
140: Format{
|
||||
Extension: "mp4",
|
||||
Resolution: "",
|
||||
VideoEncoding: "",
|
||||
AudioEncoding: "aac",
|
||||
Itag: 140,
|
||||
AudioBitrate: 128,
|
||||
},
|
||||
141: Format{
|
||||
Extension: "mp4",
|
||||
Resolution: "",
|
||||
VideoEncoding: "",
|
||||
AudioEncoding: "aac",
|
||||
Itag: 141,
|
||||
AudioBitrate: 256,
|
||||
},
|
||||
171: Format{
|
||||
Extension: "webm",
|
||||
Resolution: "",
|
||||
VideoEncoding: "",
|
||||
AudioEncoding: "vorbis",
|
||||
Itag: 171,
|
||||
AudioBitrate: 128,
|
||||
},
|
||||
172: Format{
|
||||
Extension: "webm",
|
||||
Resolution: "",
|
||||
VideoEncoding: "",
|
||||
AudioEncoding: "vorbis",
|
||||
Itag: 172,
|
||||
AudioBitrate: 192,
|
||||
},
|
||||
249: Format{
|
||||
Extension: "webm",
|
||||
Resolution: "",
|
||||
VideoEncoding: "",
|
||||
AudioEncoding: "opus",
|
||||
Itag: 249,
|
||||
AudioBitrate: 50,
|
||||
},
|
||||
250: Format{
|
||||
Extension: "webm",
|
||||
Resolution: "",
|
||||
VideoEncoding: "",
|
||||
AudioEncoding: "opus",
|
||||
Itag: 250,
|
||||
AudioBitrate: 70,
|
||||
},
|
||||
251: Format{
|
||||
Extension: "webm",
|
||||
Resolution: "",
|
||||
VideoEncoding: "",
|
||||
AudioEncoding: "opus",
|
||||
Itag: 251,
|
||||
AudioBitrate: 160,
|
||||
},
|
||||
// Live streaming
|
||||
92: Format{
|
||||
Extension: "ts",
|
||||
Resolution: "240p",
|
||||
VideoEncoding: "H.264",
|
||||
AudioEncoding: "aac",
|
||||
Itag: 92,
|
||||
AudioBitrate: 48,
|
||||
},
|
||||
93: Format{
|
||||
Extension: "ts",
|
||||
Resolution: "480p",
|
||||
VideoEncoding: "H.264",
|
||||
AudioEncoding: "aac",
|
||||
Itag: 93,
|
||||
AudioBitrate: 128,
|
||||
},
|
||||
94: Format{
|
||||
Extension: "ts",
|
||||
Resolution: "720p",
|
||||
VideoEncoding: "H.264",
|
||||
AudioEncoding: "aac",
|
||||
Itag: 94,
|
||||
AudioBitrate: 128,
|
||||
},
|
||||
95: Format{
|
||||
Extension: "ts",
|
||||
Resolution: "1080p",
|
||||
VideoEncoding: "H.264",
|
||||
AudioEncoding: "aac",
|
||||
Itag: 95,
|
||||
AudioBitrate: 256,
|
||||
},
|
||||
96: Format{
|
||||
Extension: "ts",
|
||||
Resolution: "720p",
|
||||
VideoEncoding: "H.264",
|
||||
AudioEncoding: "aac",
|
||||
Itag: 96,
|
||||
AudioBitrate: 256,
|
||||
},
|
||||
120: Format{
|
||||
Extension: "flv",
|
||||
Resolution: "720p",
|
||||
VideoEncoding: "H.264",
|
||||
AudioEncoding: "aac",
|
||||
Itag: 120,
|
||||
AudioBitrate: 128,
|
||||
},
|
||||
127: Format{
|
||||
Extension: "ts",
|
||||
Resolution: "",
|
||||
VideoEncoding: "",
|
||||
AudioEncoding: "aac",
|
||||
Itag: 127,
|
||||
AudioBitrate: 96,
|
||||
},
|
||||
128: Format{
|
||||
Extension: "ts",
|
||||
Resolution: "",
|
||||
VideoEncoding: "",
|
||||
AudioEncoding: "aac",
|
||||
Itag: 128,
|
||||
AudioBitrate: 96,
|
||||
},
|
||||
132: Format{
|
||||
Extension: "ts",
|
||||
Resolution: "240p",
|
||||
VideoEncoding: "H.264",
|
||||
AudioEncoding: "aac",
|
||||
Itag: 132,
|
||||
AudioBitrate: 48,
|
||||
},
|
||||
151: Format{
|
||||
Extension: "ts",
|
||||
Resolution: "720p",
|
||||
VideoEncoding: "H.264",
|
||||
AudioEncoding: "aac",
|
||||
Itag: 151,
|
||||
AudioBitrate: 24,
|
||||
},
|
||||
}
|
93
vendor/github.com/rylio/ytdl/format_list.go
generated
vendored
Normal file
93
vendor/github.com/rylio/ytdl/format_list.go
generated
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
package ytdl
|
||||
|
||||
import "sort"
|
||||
|
||||
// FormatList is a slice of formats with filtering functionality
|
||||
type FormatList []Format
|
||||
|
||||
func (formats FormatList) Filter(key FormatKey, values []interface{}) FormatList {
|
||||
var dst FormatList
|
||||
for _, v := range values {
|
||||
for _, f := range formats {
|
||||
if interfaceToString(f.ValueForKey(key)) == interfaceToString(v) {
|
||||
dst = append(dst, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dst
|
||||
}
|
||||
|
||||
func (formats FormatList) Extremes(key FormatKey, best bool) FormatList {
|
||||
dst := formats.Copy()
|
||||
if len(dst) > 1 {
|
||||
dst.Sort(key, best)
|
||||
first := dst[0]
|
||||
var i int
|
||||
for i = 0; i < len(dst)-1; i++ {
|
||||
if first.CompareKey(dst[i+1], key) != 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
i++
|
||||
dst = dst[0:i]
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func (formats FormatList) Best(key FormatKey) FormatList {
|
||||
return formats.Extremes(key, true)
|
||||
}
|
||||
|
||||
func (formats FormatList) Worst(key FormatKey) FormatList {
|
||||
return formats.Extremes(key, false)
|
||||
}
|
||||
|
||||
func (formats FormatList) Sort(key FormatKey, reverse bool) {
|
||||
wrapper := formatsSortWrapper{formats, key}
|
||||
if !reverse {
|
||||
sort.Stable(wrapper)
|
||||
} else {
|
||||
sort.Stable(sort.Reverse(wrapper))
|
||||
}
|
||||
}
|
||||
|
||||
func (formats FormatList) Subtract(other FormatList) FormatList {
|
||||
var dst FormatList
|
||||
for _, f := range formats {
|
||||
include := true
|
||||
for _, f2 := range other {
|
||||
if f2.Itag == f.Itag {
|
||||
include = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if include {
|
||||
dst = append(dst, f)
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func (formats FormatList) Copy() FormatList {
|
||||
dst := make(FormatList, len(formats))
|
||||
copy(dst, formats)
|
||||
return dst
|
||||
}
|
||||
|
||||
type formatsSortWrapper struct {
|
||||
formats FormatList
|
||||
key FormatKey
|
||||
}
|
||||
|
||||
func (s formatsSortWrapper) Len() int {
|
||||
return len(s.formats)
|
||||
}
|
||||
|
||||
func (s formatsSortWrapper) Less(i, j int) bool {
|
||||
return s.formats[i].CompareKey(s.formats[j], s.key) < 0
|
||||
}
|
||||
|
||||
func (s formatsSortWrapper) Swap(i, j int) {
|
||||
s.formats[i], s.formats[j] = s.formats[j], s.formats[i]
|
||||
}
|
9
vendor/github.com/rylio/ytdl/goreleaser.yml
generated
vendored
Normal file
9
vendor/github.com/rylio/ytdl/goreleaser.yml
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
builds:
|
||||
- main: ./cmd/ytdl/main.go
|
||||
binary: ytdl
|
||||
goos:
|
||||
- windows
|
||||
- darwin
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
183
vendor/github.com/rylio/ytdl/signature.go
generated
vendored
Normal file
183
vendor/github.com/rylio/ytdl/signature.go
generated
vendored
Normal file
@@ -0,0 +1,183 @@
|
||||
package ytdl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func getDownloadURL(format Format, htmlPlayerFile string) (*url.URL, error) {
|
||||
var sig string
|
||||
if s, ok := format.meta["s"]; ok && len(s.(string)) > 0 {
|
||||
tokens, err := getSigTokens(htmlPlayerFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sig = decipherTokens(tokens, s.(string))
|
||||
} else {
|
||||
if s, ok := format.meta["sig"]; ok {
|
||||
sig = s.(string)
|
||||
}
|
||||
}
|
||||
var urlString string
|
||||
if s, ok := format.meta["url"]; ok {
|
||||
urlString = s.(string)
|
||||
} else if s, ok := format.meta["stream"]; ok {
|
||||
if c, ok := format.meta["conn"]; ok {
|
||||
urlString = c.(string)
|
||||
if urlString[len(urlString)-1] != '/' {
|
||||
urlString += "/"
|
||||
}
|
||||
}
|
||||
urlString += s.(string)
|
||||
} else {
|
||||
return nil, fmt.Errorf("Couldn't extract url from format")
|
||||
}
|
||||
urlString, err := url.QueryUnescape(urlString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u, err := url.Parse(urlString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := u.Query()
|
||||
query.Set("ratebypass", "yes")
|
||||
if len(sig) > 0 {
|
||||
query.Set("signature", sig)
|
||||
}
|
||||
u.RawQuery = query.Encode()
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func decipherTokens(tokens []string, sig string) string {
|
||||
var pos int
|
||||
sigSplit := strings.Split(sig, "")
|
||||
for i, l := 0, len(tokens); i < l; i++ {
|
||||
tok := tokens[i]
|
||||
if len(tok) > 1 {
|
||||
pos, _ = strconv.Atoi(string(tok[1:]))
|
||||
pos = ^^pos
|
||||
}
|
||||
switch string(tok[0]) {
|
||||
case "r":
|
||||
reverseStringSlice(sigSplit)
|
||||
case "w":
|
||||
s := sigSplit[0]
|
||||
sigSplit[0] = sigSplit[pos]
|
||||
sigSplit[pos] = s
|
||||
case "s":
|
||||
sigSplit = sigSplit[pos:]
|
||||
case "p":
|
||||
sigSplit = sigSplit[pos:]
|
||||
}
|
||||
}
|
||||
return strings.Join(sigSplit, "")
|
||||
}
|
||||
|
||||
const (
|
||||
jsvarStr = "[a-zA-Z_\\$][a-zA-Z_0-9]*"
|
||||
reverseStr = ":function\\(a\\)\\{" +
|
||||
"(?:return )?a\\.reverse\\(\\)" +
|
||||
"\\}"
|
||||
sliceStr = ":function\\(a,b\\)\\{" +
|
||||
"return a\\.slice\\(b\\)" +
|
||||
"\\}"
|
||||
spliceStr = ":function\\(a,b\\)\\{" +
|
||||
"a\\.splice\\(0,b\\)" +
|
||||
"\\}"
|
||||
swapStr = ":function\\(a,b\\)\\{" +
|
||||
"var c=a\\[0\\];a\\[0\\]=a\\[b(?:%a\\.length)?\\];a\\[b(?:%a\\.length)?\\]=c(?:;return a)?" +
|
||||
"\\}"
|
||||
)
|
||||
|
||||
var actionsObjRegexp = regexp.MustCompile(fmt.Sprintf(
|
||||
"var (%s)=\\{((?:(?:%s%s|%s%s|%s%s|%s%s),?\\n?)+)\\};", jsvarStr, jsvarStr, reverseStr, jsvarStr, sliceStr, jsvarStr, spliceStr, jsvarStr, swapStr))
|
||||
|
||||
var actionsFuncRegexp = regexp.MustCompile(fmt.Sprintf(
|
||||
"function(?: %s)?\\(a\\)\\{"+
|
||||
"a=a\\.split\\(\"\"\\);\\s*"+
|
||||
"((?:(?:a=)?%s\\.%s\\(a,\\d+\\);)+)"+
|
||||
"return a\\.join\\(\"\"\\)"+
|
||||
"\\}", jsvarStr, jsvarStr, jsvarStr))
|
||||
|
||||
var reverseRegexp = regexp.MustCompile(fmt.Sprintf(
|
||||
"(?m)(?:^|,)(%s)%s", jsvarStr, reverseStr))
|
||||
var sliceRegexp = regexp.MustCompile(fmt.Sprintf(
|
||||
"(?m)(?:^|,)(%s)%s", jsvarStr, sliceStr))
|
||||
var spliceRegexp = regexp.MustCompile(fmt.Sprintf(
|
||||
"(?m)(?:^|,)(%s)%s", jsvarStr, spliceStr))
|
||||
var swapRegexp = regexp.MustCompile(fmt.Sprintf(
|
||||
"(?m)(?:^|,)(%s)%s", jsvarStr, swapStr))
|
||||
|
||||
func getSigTokens(htmlPlayerFile string) ([]string, error) {
|
||||
u, _ := url.Parse(youtubeBaseURL)
|
||||
p, err := url.Parse(htmlPlayerFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := http.Get(u.ResolveReference(p).String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("Error fetching signature tokens, status code %d", resp.StatusCode)
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
bodyString := string(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
objResult := actionsObjRegexp.FindStringSubmatch(bodyString)
|
||||
funcResult := actionsFuncRegexp.FindStringSubmatch(bodyString)
|
||||
|
||||
if len(objResult) < 3 || len(funcResult) < 2 {
|
||||
return nil, fmt.Errorf("Error parsing signature tokens")
|
||||
}
|
||||
obj := strings.Replace(objResult[1], "$", "\\$", -1)
|
||||
objBody := strings.Replace(objResult[2], "$", "\\$", -1)
|
||||
funcBody := strings.Replace(funcResult[1], "$", "\\$", -1)
|
||||
|
||||
var reverseKey, sliceKey, spliceKey, swapKey string
|
||||
var result []string
|
||||
|
||||
if result = reverseRegexp.FindStringSubmatch(objBody); len(result) > 1 {
|
||||
reverseKey = strings.Replace(result[1], "$", "\\$", -1)
|
||||
}
|
||||
if result = sliceRegexp.FindStringSubmatch(objBody); len(result) > 1 {
|
||||
sliceKey = strings.Replace(result[1], "$", "\\$", -1)
|
||||
}
|
||||
if result = spliceRegexp.FindStringSubmatch(objBody); len(result) > 1 {
|
||||
spliceKey = strings.Replace(result[1], "$", "\\$", -1)
|
||||
}
|
||||
if result = swapRegexp.FindStringSubmatch(objBody); len(result) > 1 {
|
||||
swapKey = strings.Replace(result[1], "$", "\\$", -1)
|
||||
}
|
||||
|
||||
keys := []string{reverseKey, sliceKey, spliceKey, swapKey}
|
||||
regex, err := regexp.Compile(fmt.Sprintf("(?:a=)?%s\\.(%s)\\(a,(\\d+)\\)", obj, strings.Join(keys, "|")))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results := regex.FindAllStringSubmatch(funcBody, -1)
|
||||
var tokens []string
|
||||
for _, s := range results {
|
||||
switch s[1] {
|
||||
case swapKey:
|
||||
tokens = append(tokens, "w"+s[2])
|
||||
case reverseKey:
|
||||
tokens = append(tokens, "r")
|
||||
case sliceKey:
|
||||
tokens = append(tokens, "s"+s[2])
|
||||
case spliceKey:
|
||||
tokens = append(tokens, "p"+s[2])
|
||||
}
|
||||
}
|
||||
return tokens, nil
|
||||
}
|
21
vendor/github.com/rylio/ytdl/thumbnail.go
generated
vendored
Normal file
21
vendor/github.com/rylio/ytdl/thumbnail.go
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
package ytdl
|
||||
|
||||
// From http://stackoverflow.com/questions/2068344/how-do-i-get-a-youtube-video-thumbnail-from-the-youtube-api
|
||||
|
||||
// ThumbnailQuality is a youtube thumbnail quality option
|
||||
type ThumbnailQuality string
|
||||
|
||||
// ThumbnailQualityHigh is the high quality thumbnail jpg
|
||||
const ThumbnailQualityHigh ThumbnailQuality = "hqdefault"
|
||||
|
||||
// ThumbnailQualityDefault is the default quality thumbnail jpg
|
||||
const ThumbnailQualityDefault ThumbnailQuality = "default"
|
||||
|
||||
// ThumbnailQualityMedium is the medium quality thumbnail jpg
|
||||
const ThumbnailQualityMedium ThumbnailQuality = "mqdefault"
|
||||
|
||||
// ThumbnailQualitySD is the standard def quality thumbnail jpg
|
||||
const ThumbnailQualitySD ThumbnailQuality = "sddefault"
|
||||
|
||||
// ThumbnailQualityMaxRes is the maximum resolution quality jpg
|
||||
const ThumbnailQualityMaxRes ThumbnailQuality = "maxresdefault"
|
13
vendor/github.com/rylio/ytdl/utils.go
generated
vendored
Normal file
13
vendor/github.com/rylio/ytdl/utils.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
package ytdl
|
||||
|
||||
import "fmt"
|
||||
|
||||
func reverseStringSlice(s []string) {
|
||||
for i, j := 0, len(s)-1; i < len(s)/2; i, j = i+1, j-1 {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
}
|
||||
|
||||
func interfaceToString(val interface{}) string {
|
||||
return fmt.Sprintf("%v", val)
|
||||
}
|
402
vendor/github.com/rylio/ytdl/video_info.go
generated
vendored
Normal file
402
vendor/github.com/rylio/ytdl/video_info.go
generated
vendored
Normal file
@@ -0,0 +1,402 @@
|
||||
package ytdl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const youtubeBaseURL = "https://www.youtube.com/watch"
|
||||
const youtubeEmbededBaseURL = "https://www.youtube.com/embed/"
|
||||
const youtubeVideoEURL = "https://youtube.googleapis.com/v/"
|
||||
const youtubeVideoInfoURL = "https://www.youtube.com/get_video_info"
|
||||
const youtubeDateFormat = "2006-01-02"
|
||||
|
||||
// VideoInfo contains the info a youtube video
|
||||
type VideoInfo struct {
|
||||
// The video ID
|
||||
ID string `json:"id"`
|
||||
// The video title
|
||||
Title string `json:"title"`
|
||||
// The video description
|
||||
Description string `json:"description"`
|
||||
// The date the video was published
|
||||
DatePublished time.Time `json:"datePublished"`
|
||||
// Formats the video is available in
|
||||
Formats FormatList `json:"formats"`
|
||||
// List of keywords associated with the video
|
||||
Keywords []string `json:"keywords"`
|
||||
// Author of the video
|
||||
Author string `json:"author"`
|
||||
// Duration of the video
|
||||
Duration time.Duration
|
||||
|
||||
htmlPlayerFile string
|
||||
}
|
||||
|
||||
// GetVideoInfo fetches info from a url string, url object, or a url string
|
||||
func GetVideoInfo(value interface{}) (*VideoInfo, error) {
|
||||
switch t := value.(type) {
|
||||
case *url.URL:
|
||||
return GetVideoInfoFromURL(t)
|
||||
case string:
|
||||
u, err := url.ParseRequestURI(t)
|
||||
if err != nil {
|
||||
return GetVideoInfoFromID(t)
|
||||
}
|
||||
if u.Host == "youtu.be" {
|
||||
return GetVideoInfoFromShortURL(u)
|
||||
}
|
||||
return GetVideoInfoFromURL(u)
|
||||
default:
|
||||
return nil, fmt.Errorf("Identifier type must be a string, *url.URL, or []byte")
|
||||
}
|
||||
}
|
||||
|
||||
// GetVideoInfoFromURL fetches video info from a youtube url
|
||||
func GetVideoInfoFromURL(u *url.URL) (*VideoInfo, error) {
|
||||
videoID := u.Query().Get("v")
|
||||
if len(videoID) == 0 {
|
||||
return nil, fmt.Errorf("Invalid youtube url, no video id")
|
||||
}
|
||||
return GetVideoInfoFromID(videoID)
|
||||
}
|
||||
|
||||
// GetVideoInfoFromShortURL fetches video info from a short youtube url
|
||||
func GetVideoInfoFromShortURL(u *url.URL) (*VideoInfo, error) {
|
||||
if len(u.Path) >= 1 {
|
||||
if path := u.Path[1:]; path != "" {
|
||||
return GetVideoInfoFromID(path)
|
||||
}
|
||||
}
|
||||
return nil, errors.New("Could not parse short URL")
|
||||
}
|
||||
|
||||
// GetVideoInfoFromID fetches video info from a youtube video id
|
||||
func GetVideoInfoFromID(id string) (*VideoInfo, error) {
|
||||
u, _ := url.ParseRequestURI(youtubeBaseURL)
|
||||
values := u.Query()
|
||||
values.Set("v", id)
|
||||
u.RawQuery = values.Encode()
|
||||
|
||||
resp, err := http.Get(u.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("Invalid status code: %d", resp.StatusCode)
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return getVideoInfoFromHTML(id, body)
|
||||
}
|
||||
|
||||
// GetDownloadURL gets the download url for a format
|
||||
func (info *VideoInfo) GetDownloadURL(format Format) (*url.URL, error) {
|
||||
return getDownloadURL(format, info.htmlPlayerFile)
|
||||
}
|
||||
|
||||
// GetThumbnailURL returns a url for the thumbnail image
|
||||
// with the given quality
|
||||
func (info *VideoInfo) GetThumbnailURL(quality ThumbnailQuality) *url.URL {
|
||||
u, _ := url.Parse(fmt.Sprintf("http://img.youtube.com/vi/%s/%s.jpg",
|
||||
info.ID, quality))
|
||||
return u
|
||||
}
|
||||
|
||||
// Download is a convenience method to download a format to an io.Writer
|
||||
func (info *VideoInfo) Download(format Format, dest io.Writer) error {
|
||||
u, err := info.GetDownloadURL(format)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := http.Get(u.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||
return fmt.Errorf("Invalid status code: %d", resp.StatusCode)
|
||||
}
|
||||
_, err = io.Copy(dest, resp.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
func getVideoInfoFromHTML(id string, html []byte) (*VideoInfo, error) {
|
||||
doc, err := goquery.NewDocumentFromReader(bytes.NewReader(html))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := &VideoInfo{}
|
||||
|
||||
// extract description and title
|
||||
info.Description = strings.TrimSpace(doc.Find("#eow-description").Text())
|
||||
info.Title = strings.TrimSpace(doc.Find("#eow-title").Text())
|
||||
info.ID = id
|
||||
dateStr, ok := doc.Find("meta[itemprop=\"datePublished\"]").Attr("content")
|
||||
if !ok {
|
||||
log.Debug("Unable to extract date published")
|
||||
} else {
|
||||
date, err := time.Parse(youtubeDateFormat, dateStr)
|
||||
if err == nil {
|
||||
info.DatePublished = date
|
||||
} else {
|
||||
log.Debug("Unable to parse date published", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// match json in javascript
|
||||
re := regexp.MustCompile("ytplayer.config = (.*?);ytplayer.load")
|
||||
matches := re.FindSubmatch(html)
|
||||
var jsonConfig map[string]interface{}
|
||||
if len(matches) > 1 {
|
||||
err = json.Unmarshal(matches[1], &jsonConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
log.Debug("Unable to extract json from default url, trying embedded url")
|
||||
var resp *http.Response
|
||||
resp, err = http.Get(youtubeEmbededBaseURL + id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("Embeded url request returned status code %d ", resp.StatusCode)
|
||||
}
|
||||
html, err = ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// re = regexp.MustCompile("\"sts\"\\s*:\\s*(\\d+)")
|
||||
re = regexp.MustCompile("yt.setConfig\\('PLAYER_CONFIG', (.*?)\\);</script>")
|
||||
|
||||
matches := re.FindSubmatch(html)
|
||||
if len(matches) < 2 {
|
||||
return nil, fmt.Errorf("Error extracting sts from embedded url response")
|
||||
}
|
||||
dec := json.NewDecoder(bytes.NewBuffer(matches[1]))
|
||||
err = dec.Decode(&jsonConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to extract json from embedded url: %s", err.Error())
|
||||
}
|
||||
query := url.Values{
|
||||
"sts": []string{strconv.Itoa(int(jsonConfig["sts"].(float64)))},
|
||||
"video_id": []string{id},
|
||||
"eurl": []string{youtubeVideoEURL + id},
|
||||
}
|
||||
|
||||
resp, err = http.Get(youtubeVideoInfoURL + "?" + query.Encode())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error fetching video info: %s", err.Error())
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("Video info response invalid status code")
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to read video info response body: %s", err.Error())
|
||||
}
|
||||
query, err = url.ParseQuery(string(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to parse video info data: %s", err.Error())
|
||||
}
|
||||
args := make(map[string]interface{})
|
||||
for k, v := range query {
|
||||
if len(v) > 0 {
|
||||
args[k] = v[0]
|
||||
}
|
||||
}
|
||||
jsonConfig["args"] = args
|
||||
}
|
||||
|
||||
inf := jsonConfig["args"].(map[string]interface{})
|
||||
if status, ok := inf["status"].(string); ok && status == "fail" {
|
||||
return nil, fmt.Errorf("Error %d:%s", inf["errorcode"], inf["reason"])
|
||||
}
|
||||
if a, ok := inf["author"].(string); ok {
|
||||
info.Author = a
|
||||
} else {
|
||||
log.Debug("Unable to extract author")
|
||||
}
|
||||
|
||||
if length, ok := inf["length_seconds"].(string); ok {
|
||||
if duration, err := strconv.ParseInt(length, 10, 64); err == nil {
|
||||
info.Duration = time.Second * time.Duration(duration)
|
||||
} else {
|
||||
log.Debug("Unable to parse duration string: ", length)
|
||||
}
|
||||
} else {
|
||||
log.Debug("Unable to extract duration")
|
||||
}
|
||||
|
||||
// For the future maybe
|
||||
parseKey := func(key string) []string {
|
||||
val, ok := inf[key].(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
vals := []string{}
|
||||
split := strings.Split(val, ",")
|
||||
for _, v := range split {
|
||||
if v != "" {
|
||||
vals = append(vals, v)
|
||||
}
|
||||
}
|
||||
return vals
|
||||
}
|
||||
info.Keywords = parseKey("keywords")
|
||||
info.htmlPlayerFile = jsonConfig["assets"].(map[string]interface{})["js"].(string)
|
||||
|
||||
/*
|
||||
fmtList := parseKey("fmt_list")
|
||||
fexp := parseKey("fexp")
|
||||
watermark := parseKey("watermark")
|
||||
|
||||
if len(fmtList) != 0 {
|
||||
vals := []string{}
|
||||
for _, v := range fmtList {
|
||||
vals = append(vals, strings.Split(v, "/")...)
|
||||
} else {
|
||||
info["fmt_list"] = []string{}
|
||||
}
|
||||
|
||||
videoVerticals := []string{}
|
||||
if videoVertsStr, ok := inf["video_verticals"].(string); ok {
|
||||
videoVertsStr = string([]byte(videoVertsStr)[1 : len(videoVertsStr)-2])
|
||||
videoVertsSplit := strings.Split(videoVertsStr, ", ")
|
||||
for _, v := range videoVertsSplit {
|
||||
if v != "" {
|
||||
videoVerticals = append(videoVerticals, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
var formatStrings []string
|
||||
if fmtStreamMap, ok := inf["url_encoded_fmt_stream_map"].(string); ok {
|
||||
formatStrings = append(formatStrings, strings.Split(fmtStreamMap, ",")...)
|
||||
}
|
||||
|
||||
if adaptiveFormats, ok := inf["adaptive_fmts"].(string); ok {
|
||||
formatStrings = append(formatStrings, strings.Split(adaptiveFormats, ",")...)
|
||||
}
|
||||
var formats FormatList
|
||||
for _, v := range formatStrings {
|
||||
query, err := url.ParseQuery(v)
|
||||
if err == nil {
|
||||
itag, _ := strconv.Atoi(query.Get("itag"))
|
||||
if format, ok := newFormat(itag); ok {
|
||||
if strings.HasPrefix(query.Get("conn"), "rtmp") {
|
||||
format.meta["rtmp"] = true
|
||||
}
|
||||
for k, v := range query {
|
||||
if len(v) == 1 {
|
||||
format.meta[k] = v[0]
|
||||
} else {
|
||||
format.meta[k] = v
|
||||
}
|
||||
}
|
||||
formats = append(formats, format)
|
||||
} else {
|
||||
log.Debug("No metadata found for itag: ", itag, ", skipping...")
|
||||
}
|
||||
} else {
|
||||
log.Debug("Unable to format string", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if dashManifestURL, ok := inf["dashmpd"].(string); ok {
|
||||
tokens, err := getSigTokens(info.htmlPlayerFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to extract signature tokens: %s", err.Error())
|
||||
}
|
||||
regex := regexp.MustCompile("\\/s\\/([a-fA-F0-9\\.]+)")
|
||||
regexSub := regexp.MustCompile("([a-fA-F0-9\\.]+)")
|
||||
dashManifestURL = regex.ReplaceAllStringFunc(dashManifestURL, func(str string) string {
|
||||
return "/signature/" + decipherTokens(tokens, regexSub.FindString(str))
|
||||
})
|
||||
dashFormats, err := getDashManifest(dashManifestURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to extract dash manifest: %s", err.Error())
|
||||
}
|
||||
|
||||
for _, dashFormat := range dashFormats {
|
||||
added := false
|
||||
for j, format := range formats {
|
||||
if dashFormat.Itag == format.Itag {
|
||||
formats[j] = dashFormat
|
||||
added = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !added {
|
||||
formats = append(formats, dashFormat)
|
||||
}
|
||||
}
|
||||
}
|
||||
info.Formats = formats
|
||||
return info, nil
|
||||
}
|
||||
|
||||
type representation struct {
|
||||
Itag int `xml:"id,attr"`
|
||||
Height int `xml:"height,attr"`
|
||||
URL string `xml:"BaseURL"`
|
||||
}
|
||||
|
||||
func getDashManifest(urlString string) (formats []Format, err error) {
|
||||
|
||||
resp, err := http.Get(urlString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("Invalid status code %d", resp.StatusCode)
|
||||
}
|
||||
dec := xml.NewDecoder(resp.Body)
|
||||
var token xml.Token
|
||||
for ; err == nil; token, err = dec.Token() {
|
||||
if el, ok := token.(xml.StartElement); ok && el.Name.Local == "Representation" {
|
||||
var rep representation
|
||||
err = dec.DecodeElement(&rep, &el)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if format, ok := newFormat(rep.Itag); ok {
|
||||
format.meta["url"] = rep.URL
|
||||
if rep.Height != 0 {
|
||||
format.Resolution = strconv.Itoa(rep.Height) + "p"
|
||||
} else {
|
||||
format.Resolution = ""
|
||||
}
|
||||
formats = append(formats, format)
|
||||
} else {
|
||||
log.Debug("No metadata found for itag: ", rep.Itag, ", skipping...")
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
return formats, nil
|
||||
}
|
Reference in New Issue
Block a user