From f388a4a7aac845e66710a5c2b53c6c0a3c1246a9 Mon Sep 17 00:00:00 2001 From: shavit Date: Sat, 6 May 2023 15:36:18 -0400 Subject: [PATCH 1/4] Add Rumble extractor --- app/register.go | 1 + extractors/rumble/rumble.go | 267 +++++++++++++++++++++++++++++++ extractors/rumble/rumble_test.go | 28 ++++ 3 files changed, 296 insertions(+) create mode 100644 extractors/rumble/rumble.go create mode 100644 extractors/rumble/rumble_test.go diff --git a/app/register.go b/app/register.go index b0392472d..2f63c9c8e 100644 --- a/app/register.go +++ b/app/register.go @@ -23,6 +23,7 @@ import ( _ "github.com/iawia002/lux/extractors/pornhub" _ "github.com/iawia002/lux/extractors/qq" _ "github.com/iawia002/lux/extractors/reddit" + _ "github.com/iawia002/lux/extractors/rumble" _ "github.com/iawia002/lux/extractors/streamtape" _ "github.com/iawia002/lux/extractors/tangdou" _ "github.com/iawia002/lux/extractors/tiktok" diff --git a/extractors/rumble/rumble.go b/extractors/rumble/rumble.go new file mode 100644 index 000000000..55a58defc --- /dev/null +++ b/extractors/rumble/rumble.go @@ -0,0 +1,267 @@ +package rumble + +import ( + "compress/flate" + "compress/gzip" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "path" + + "github.com/pkg/errors" + + "github.com/iawia002/lux/extractors" + "github.com/iawia002/lux/request" + "github.com/iawia002/lux/utils" +) + +func init() { + extractors.Register("rumble", New()) +} + +type extractor struct{} + +// New returns a rumble extractor. +func New() extractors.Extractor { + return &extractor{} +} + +type rumbleData struct { + Format string `json:"format"` + Name string `json:"name"` + EmbedURL string `json:"embedUrl"` + ThumbnailURL string `json:"thumbnailUrl"` + Type string `json:"@type"` + VideoURL string `json:"videoUrl"` + Quality string `json:"quality"` +} + +// Extract is the main function to extract the data. +func (e *extractor) Extract(url string, option extractors.Options) ([]*extractors.Data, error) { + res, err := request.Request(http.MethodGet, url, nil, nil) + if err != nil { + return nil, errors.WithStack(err) + } + + defer res.Body.Close() // nolint + + var reader io.ReadCloser + switch res.Header.Get("Content-Encoding") { + case "gzip": + reader, _ = gzip.NewReader(res.Body) + case "deflate": + reader = flate.NewReader(res.Body) + default: + reader = res.Body + } + defer reader.Close() // nolint + + b, err := io.ReadAll(reader) + if err != nil { + return nil, errors.WithStack(err) + } + + html := string(b) + + cookiesArr := make([]string, 0) + cookies := res.Cookies() + + for _, c := range cookies { + cookiesArr = append(cookiesArr, c.Name+"="+c.Value) + } + + var title string + matchTitle := utils.MatchOneOf(html, `(.+?)`) + if len(matchTitle) > 1 { + title = matchTitle[1] + } else { + title = "rumble video" + } + + payload, err := readPayload(html) + if err != nil { + return nil, errors.WithStack(err) + } + + videoID, err := getVideoID(payload.EmbedURL) + if err != nil { + return nil, errors.WithStack(err) + } + + streams, err := fetchVideoQuality(videoID) + if err != nil { + return nil, errors.WithStack(err) + } + + return []*extractors.Data{ + { + Site: "Rumble rumble.com", + Title: title, + Type: extractors.DataTypeVideo, + Streams: streams, + URL: url, + }, + }, nil +} + +// Read JSON object from the video webpage +func readPayload(html string) (*rumbleData, error) { + matchPayload := utils.MatchOneOf(html, `\(.+?)\<\/script>`) + if len(matchPayload) < 1 { + return nil, errors.WithStack(extractors.ErrURLQueryParamsParseFailed) + } + + rumbles := make([]rumbleData, 0) + if err := json.Unmarshal([]byte(matchPayload[1]), &rumbles); err != nil { + return nil, errors.WithStack(err) + } + + for _, it := range rumbles { + if it.Type == "VideoObject" { + return &it, nil + } + } + + return nil, errors.WithStack(extractors.ErrURLParseFailed) +} + +func getVideoID(embedURL string) (string, error) { + u, err := url.Parse(embedURL) + if err != nil { + return "", errors.WithStack(extractors.ErrURLParseFailed) + } + + return path.Base(u.Path), nil +} + +// Common video meta data +type rumbleStreamMeta struct { + URL string `json:"url"` + Meta struct { + Bitrate uint16 `json:"bitrate"` + Size int64 `json:"size"` + Width uint16 `json:"w"` + Height uint16 `json:"h"` + } `json:"meta"` +} + +// Rumble response contains the streams in `rumbleStreams` +type rumbleResponse struct { + Streams *json.RawMessage `json:"ua"` +} + +// Video payload for adaptive stream and different qualities +type rumbleStreams struct { + FormatMp4 struct { + Q240 struct{ rumbleStreamMeta } `json:"240"` + Q360 struct{ rumbleStreamMeta } `json:"360"` + Q480 struct{ rumbleStreamMeta } `json:"480"` + Q720 struct{ rumbleStreamMeta } `json:"720"` + Q1080 struct{ rumbleStreamMeta } `json:"1080"` + Q1440 struct{ rumbleStreamMeta } `json:"1440"` + Q2160 struct{ rumbleStreamMeta } `json:"2160"` + Q2161 struct{ rumbleStreamMeta } `json:"2161"` + } `json:"mp4"` + FormatWebm struct { + Q240 struct{ rumbleStreamMeta } `json:"240"` + Q360 struct{ rumbleStreamMeta } `json:"360"` + Q480 struct{ rumbleStreamMeta } `json:"480"` + Q720 struct{ rumbleStreamMeta } `json:"720"` + Q1080 struct{ rumbleStreamMeta } `json:"1080"` + Q1440 struct{ rumbleStreamMeta } `json:"1440"` + Q2160 struct{ rumbleStreamMeta } `json:"2160"` + Q2161 struct{ rumbleStreamMeta } `json:"2161"` + } `json:"webm"` + FormatHLS struct { + QAuto struct{ rumbleStreamMeta } `json:"auto"` + } `json:"hls"` +} + +// Unmarshall the video response +// Some properties like `mp4`, `webm` are either array or an object........ +func (r *rumbleStreams) UnmarshalJSON(b []byte) error { + var resp *rumbleResponse + if err := json.Unmarshal(b, &resp); err != nil { + return errors.WithStack(extractors.ErrURLParseFailed) + } + + // Get individual stream from the response + var obj map[string]*json.RawMessage + if err := json.Unmarshal(*resp.Streams, &obj); err != nil { + return errors.WithStack(extractors.ErrURLParseFailed) + } + + if v, ok := obj["mp4"]; ok { + _ = json.Unmarshal(*v, &r.FormatMp4) + } + if v, ok := obj["webm"]; ok { + _ = json.Unmarshal(*v, &r.FormatMp4) + } + if v, ok := obj["hls"]; ok { + _ = json.Unmarshal(*v, &r.FormatMp4) + } + + return nil +} + +// Request video formats and qualities +func fetchVideoQuality(videoID string) (map[string]*extractors.Stream, error) { + reqURL := fmt.Sprintf(`https://rumble.com/embedJS/u3/?request=video&ver=2&v=%s&ext={"ad_count":null}&ad_wt=0`, videoID) + + res, err := request.Request(http.MethodGet, reqURL, nil, nil) + if err != nil { + return nil, errors.WithStack(err) + } + defer res.Body.Close() // nolint + + var reader io.ReadCloser + switch res.Header.Get("Content-Encoding") { + case "gzip": + reader, _ = gzip.NewReader(res.Body) + case "deflate": + reader = flate.NewReader(res.Body) + default: + reader = res.Body + } + defer reader.Close() // nolint + + b, err := io.ReadAll(reader) + if err != nil { + return nil, errors.WithStack(err) + } + + var rStreams rumbleStreams + if err := json.Unmarshal(b, &rStreams); err != nil { + return nil, errors.WithStack(err) + } + + streams := make(map[string]*extractors.Stream, 9) + streams["hls"] = makeStreamMeta("auto", "hls", rStreams.FormatHLS.QAuto.URL, rStreams.FormatHLS.QAuto.Meta.Size) + streams["webm"] = makeStreamMeta("480", "webm", rStreams.FormatWebm.Q480.URL, rStreams.FormatWebm.Q480.Meta.Size) + streams["240"] = makeStreamMeta("240", "mp4", rStreams.FormatMp4.Q240.URL, rStreams.FormatMp4.Q240.Meta.Size) + streams["360"] = makeStreamMeta("360", "mp4", rStreams.FormatMp4.Q360.URL, rStreams.FormatMp4.Q360.Meta.Size) + streams["480"] = makeStreamMeta("480", "mp4", rStreams.FormatMp4.Q480.URL, rStreams.FormatMp4.Q480.Meta.Size) + streams["720"] = makeStreamMeta("720", "mp4", rStreams.FormatMp4.Q720.URL, rStreams.FormatMp4.Q720.Meta.Size) + streams["1080"] = makeStreamMeta("1080", "mp4", rStreams.FormatMp4.Q1080.URL, rStreams.FormatMp4.Q1080.Meta.Size) + streams["1440"] = makeStreamMeta("1440", "mp4", rStreams.FormatMp4.Q1440.URL, rStreams.FormatMp4.Q1440.Meta.Size) + streams["2160"] = makeStreamMeta("2160", "mp4", rStreams.FormatMp4.Q2160.URL, rStreams.FormatMp4.Q2160.Meta.Size) + streams["2161"] = makeStreamMeta("2161", "mp4", rStreams.FormatMp4.Q2161.URL, rStreams.FormatMp4.Q2160.Meta.Size) + + return streams, nil +} + +func makeStreamMeta(q, ext, url string, size int64) *extractors.Stream { + urlMeta := &extractors.Part{ + URL: url, + Size: size, + Ext: ext, + } + + return &extractors.Stream{ + Parts: []*extractors.Part{urlMeta}, + Size: size, + Quality: q, + } +} diff --git a/extractors/rumble/rumble_test.go b/extractors/rumble/rumble_test.go new file mode 100644 index 000000000..622d8e617 --- /dev/null +++ b/extractors/rumble/rumble_test.go @@ -0,0 +1,28 @@ +package rumble + +import ( + "testing" + + "github.com/iawia002/lux/extractors" + "github.com/iawia002/lux/test" +) + +func TestRumble(t *testing.T) { + tests := []struct { + name string + args test.Args + }{ + { + name: "normal test", + args: test.Args{ + URL: "https://rumble.com/v24swn0-just-say-yes-to-climate-lockdowns.html", + Title: "Just Say YES to Climate Lockdowns!", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + New().Extract(tt.args.URL, extractors.Options{}) + }) + } +} From 503626edc292b3efce10834aeec1bf2176545c9a Mon Sep 17 00:00:00 2001 From: shavit Date: Mon, 8 May 2023 07:52:07 -0400 Subject: [PATCH 2/4] Remove unused cookies --- extractors/rumble/rumble.go | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/extractors/rumble/rumble.go b/extractors/rumble/rumble.go index 55a58defc..ab0b8631f 100644 --- a/extractors/rumble/rumble.go +++ b/extractors/rumble/rumble.go @@ -64,14 +64,6 @@ func (e *extractor) Extract(url string, option extractors.Options) ([]*extractor } html := string(b) - - cookiesArr := make([]string, 0) - cookies := res.Cookies() - - for _, c := range cookies { - cookiesArr = append(cookiesArr, c.Name+"="+c.Value) - } - var title string matchTitle := utils.MatchOneOf(html, `(.+?)`) if len(matchTitle) > 1 { @@ -238,7 +230,7 @@ func fetchVideoQuality(videoID string) (map[string]*extractors.Stream, error) { } streams := make(map[string]*extractors.Stream, 9) - streams["hls"] = makeStreamMeta("auto", "hls", rStreams.FormatHLS.QAuto.URL, rStreams.FormatHLS.QAuto.Meta.Size) + streams["hls"] = makeStreamMeta("auto", "ts", rStreams.FormatHLS.QAuto.URL, rStreams.FormatHLS.QAuto.Meta.Size) streams["webm"] = makeStreamMeta("480", "webm", rStreams.FormatWebm.Q480.URL, rStreams.FormatWebm.Q480.Meta.Size) streams["240"] = makeStreamMeta("240", "mp4", rStreams.FormatMp4.Q240.URL, rStreams.FormatMp4.Q240.Meta.Size) streams["360"] = makeStreamMeta("360", "mp4", rStreams.FormatMp4.Q360.URL, rStreams.FormatMp4.Q360.Meta.Size) From 9d287d07bb6e04d36565b98f8593ee34c4b60580 Mon Sep 17 00:00:00 2001 From: shavit Date: Mon, 8 May 2023 07:54:08 -0400 Subject: [PATCH 3/4] Update Supported Sites --- .github/workflows/stream_rumble.yml | 31 +++++++++++++++++++++++++++++ README.md | 1 + 2 files changed, 32 insertions(+) create mode 100644 .github/workflows/stream_rumble.yml diff --git a/.github/workflows/stream_rumble.yml b/.github/workflows/stream_rumble.yml new file mode 100644 index 000000000..425aff8e7 --- /dev/null +++ b/.github/workflows/stream_rumble.yml @@ -0,0 +1,31 @@ +name: rumble + +on: + push: + paths: + - "extractors/rumble/*.go" + - ".github/workflows/stream_rumble.yml" + pull_request: + paths: + - "extractors/rumble/*.go" + - ".github/workflows/stream_rumble.yml" + schedule: + # run ci weekly + - cron: "0 0 * * 0" + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + go: ["1.20"] + os: [ubuntu-latest] + name: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go }} + + - name: Test + run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/rumble diff --git a/README.md b/README.md index 439184282..509296a4f 100644 --- a/README.md +++ b/README.md @@ -641,6 +641,7 @@ $ lux -j "https://www.bilibili.com/video/av20203945" | Reddit | | ✓ | ✓ | | | | [![reddit](https://github.com/iawia002/lux/actions/workflows/stream_reddit.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_reddit.yml) | | VKontakte | | ✓ | | | | | [![vk](https://github.com/iawia002/lux/actions/workflows/stream_vk.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_vk.yml/) | | 知乎 | | ✓ | | | | | [![zhihu](https://github.com/iawia002/lux/actions/workflows/stream_zhihu.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_zhihu.yml/) | +| Rumble | | ✓ | | | | | [![rumble](https://github.com/iawia002/lux/actions/workflows/stream_rumble.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_rumble.yml/) | ## Known issues From f7887f24394f839408baa6a2ac26868d283a4914 Mon Sep 17 00:00:00 2001 From: shavit Date: Mon, 8 May 2023 08:39:11 -0400 Subject: [PATCH 4/4] Fetch live playlist --- extractors/rumble/rumble.go | 149 +++++++++++++++++++++++++----------- 1 file changed, 105 insertions(+), 44 deletions(-) diff --git a/extractors/rumble/rumble.go b/extractors/rumble/rumble.go index ab0b8631f..d078fc2fc 100644 --- a/extractors/rumble/rumble.go +++ b/extractors/rumble/rumble.go @@ -9,6 +9,8 @@ import ( "net/http" "net/url" "path" + "regexp" + "strconv" "github.com/pkg/errors" @@ -128,8 +130,13 @@ func getVideoID(embedURL string) (string, error) { return path.Base(u.Path), nil } +// Rumble response contains the streams in `rumbleStreams` +type rumbleResponse struct { + Streams *json.RawMessage `json:"ua"` +} + // Common video meta data -type rumbleStreamMeta struct { +type streamInfo struct { URL string `json:"url"` Meta struct { Bitrate uint16 `json:"bitrate"` @@ -139,40 +146,33 @@ type rumbleStreamMeta struct { } `json:"meta"` } -// Rumble response contains the streams in `rumbleStreams` -type rumbleResponse struct { - Streams *json.RawMessage `json:"ua"` +// common video qualities for `mp4`, `webm` +type videoQualities struct { + Q240 struct{ streamInfo } `json:"240"` + Q360 struct{ streamInfo } `json:"360"` + Q480 struct{ streamInfo } `json:"480"` + Q720 struct{ streamInfo } `json:"720"` + Q1080 struct{ streamInfo } `json:"1080"` + Q1440 struct{ streamInfo } `json:"1440"` + Q2160 struct{ streamInfo } `json:"2160"` + Q2161 struct{ streamInfo } `json:"2161"` } // Video payload for adaptive stream and different qualities type rumbleStreams struct { - FormatMp4 struct { - Q240 struct{ rumbleStreamMeta } `json:"240"` - Q360 struct{ rumbleStreamMeta } `json:"360"` - Q480 struct{ rumbleStreamMeta } `json:"480"` - Q720 struct{ rumbleStreamMeta } `json:"720"` - Q1080 struct{ rumbleStreamMeta } `json:"1080"` - Q1440 struct{ rumbleStreamMeta } `json:"1440"` - Q2160 struct{ rumbleStreamMeta } `json:"2160"` - Q2161 struct{ rumbleStreamMeta } `json:"2161"` + FMp4 struct { + videoQualities } `json:"mp4"` - FormatWebm struct { - Q240 struct{ rumbleStreamMeta } `json:"240"` - Q360 struct{ rumbleStreamMeta } `json:"360"` - Q480 struct{ rumbleStreamMeta } `json:"480"` - Q720 struct{ rumbleStreamMeta } `json:"720"` - Q1080 struct{ rumbleStreamMeta } `json:"1080"` - Q1440 struct{ rumbleStreamMeta } `json:"1440"` - Q2160 struct{ rumbleStreamMeta } `json:"2160"` - Q2161 struct{ rumbleStreamMeta } `json:"2161"` + FWebm struct { + videoQualities } `json:"webm"` - FormatHLS struct { - QAuto struct{ rumbleStreamMeta } `json:"auto"` + FHLS struct { + QAuto struct{ streamInfo } `json:"auto"` } `json:"hls"` } // Unmarshall the video response -// Some properties like `mp4`, `webm` are either array or an object........ +// Some properties like `mp4`, `webm` are either array or an object func (r *rumbleStreams) UnmarshalJSON(b []byte) error { var resp *rumbleResponse if err := json.Unmarshal(b, &resp); err != nil { @@ -186,13 +186,82 @@ func (r *rumbleStreams) UnmarshalJSON(b []byte) error { } if v, ok := obj["mp4"]; ok { - _ = json.Unmarshal(*v, &r.FormatMp4) + _ = json.Unmarshal(*v, &r.FMp4) } if v, ok := obj["webm"]; ok { - _ = json.Unmarshal(*v, &r.FormatMp4) + _ = json.Unmarshal(*v, &r.FWebm) } if v, ok := obj["hls"]; ok { - _ = json.Unmarshal(*v, &r.FormatMp4) + _ = json.Unmarshal(*v, &r.FHLS) + } + + return nil +} + +// Use this to create all the streams for `mp4`, `webm` +func (rs *rumbleStreams) makeAllVODStreams(m map[string]*extractors.Stream) { + m["webm"] = makeStreamMeta("480", "webm", &rs.FWebm.Q480.streamInfo) + m["240"] = makeStreamMeta("240", "mp4", &rs.FMp4.Q240.streamInfo) + m["360"] = makeStreamMeta("360", "mp4", &rs.FMp4.Q360.streamInfo) + m["480"] = makeStreamMeta("480", "mp4", &rs.FMp4.Q480.streamInfo) + m["720"] = makeStreamMeta("720", "mp4", &rs.FMp4.Q720.streamInfo) + m["1080"] = makeStreamMeta("1080", "mp4", &rs.FMp4.Q1080.streamInfo) + m["1440"] = makeStreamMeta("1440", "mp4", &rs.FMp4.Q1440.streamInfo) + m["2160"] = makeStreamMeta("2160", "mp4", &rs.FMp4.Q2160.streamInfo) + m["2161"] = makeStreamMeta("2161", "mp4", &rs.FMp4.Q2161.streamInfo) +} + +var reResolution = regexp.MustCompile(`_(\d{3,4})p\/`) // ex. _720p/ + +// Use this to create all the streams for live videos +func (rs *rumbleStreams) makeAllLiveStreams(m map[string]*extractors.Stream) error { + playlists, err := utils.M3u8URLs(rs.FHLS.QAuto.URL) + if err != nil { + return errors.WithStack(err) + } + + if len(playlists) == 0 { + return errors.WithStack(extractors.ErrURLParseFailed) + } + + // Find the highest resolution + playlistURL := playlists[0] + maxRes := 0 + for _, x := range playlists { + matched := reResolution.FindStringSubmatch(x) + if len(matched) == 0 { + continue + } + res, err := strconv.Atoi(matched[1]) + if err != nil { + continue + } + + if maxRes < res { + maxRes = res + playlistURL = x + } + } + + tsURLs, err := utils.M3u8URLs(playlistURL) + if err != nil { + return errors.WithStack(err) + } + + var parts []*extractors.Part + for _, x := range tsURLs { + part := &extractors.Part{ + URL: x, + Size: rs.FHLS.QAuto.streamInfo.Meta.Size, + Ext: "ts", + } + parts = append(parts, part) + } + + m["hls"] = &extractors.Stream{ + Parts: parts, + Size: rs.FHLS.QAuto.streamInfo.Meta.Size, + Quality: strconv.Itoa(maxRes), } return nil @@ -224,36 +293,28 @@ func fetchVideoQuality(videoID string) (map[string]*extractors.Stream, error) { return nil, errors.WithStack(err) } - var rStreams rumbleStreams - if err := json.Unmarshal(b, &rStreams); err != nil { + var rs rumbleStreams + if err := json.Unmarshal(b, &rs); err != nil { return nil, errors.WithStack(err) } streams := make(map[string]*extractors.Stream, 9) - streams["hls"] = makeStreamMeta("auto", "ts", rStreams.FormatHLS.QAuto.URL, rStreams.FormatHLS.QAuto.Meta.Size) - streams["webm"] = makeStreamMeta("480", "webm", rStreams.FormatWebm.Q480.URL, rStreams.FormatWebm.Q480.Meta.Size) - streams["240"] = makeStreamMeta("240", "mp4", rStreams.FormatMp4.Q240.URL, rStreams.FormatMp4.Q240.Meta.Size) - streams["360"] = makeStreamMeta("360", "mp4", rStreams.FormatMp4.Q360.URL, rStreams.FormatMp4.Q360.Meta.Size) - streams["480"] = makeStreamMeta("480", "mp4", rStreams.FormatMp4.Q480.URL, rStreams.FormatMp4.Q480.Meta.Size) - streams["720"] = makeStreamMeta("720", "mp4", rStreams.FormatMp4.Q720.URL, rStreams.FormatMp4.Q720.Meta.Size) - streams["1080"] = makeStreamMeta("1080", "mp4", rStreams.FormatMp4.Q1080.URL, rStreams.FormatMp4.Q1080.Meta.Size) - streams["1440"] = makeStreamMeta("1440", "mp4", rStreams.FormatMp4.Q1440.URL, rStreams.FormatMp4.Q1440.Meta.Size) - streams["2160"] = makeStreamMeta("2160", "mp4", rStreams.FormatMp4.Q2160.URL, rStreams.FormatMp4.Q2160.Meta.Size) - streams["2161"] = makeStreamMeta("2161", "mp4", rStreams.FormatMp4.Q2161.URL, rStreams.FormatMp4.Q2160.Meta.Size) + rs.makeAllVODStreams(streams) + _ = rs.makeAllLiveStreams(streams) return streams, nil } -func makeStreamMeta(q, ext, url string, size int64) *extractors.Stream { +func makeStreamMeta(q, ext string, info *streamInfo) *extractors.Stream { urlMeta := &extractors.Part{ - URL: url, - Size: size, + URL: info.URL, + Size: info.Meta.Size, Ext: ext, } return &extractors.Stream{ Parts: []*extractors.Part{urlMeta}, - Size: size, + Size: info.Meta.Size, Quality: q, } }