207 lines
4.8 KiB
Go
207 lines
4.8 KiB
Go
package qq
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/iawia002/annie/downloader"
|
|
"github.com/iawia002/annie/extractors"
|
|
"github.com/iawia002/annie/request"
|
|
"github.com/iawia002/annie/utils"
|
|
)
|
|
|
|
type qqVideoInfo struct {
|
|
Fl struct {
|
|
Fi []struct {
|
|
ID int `json:"id"`
|
|
Name string `json:"name"`
|
|
Cname string `json:"cname"`
|
|
Fs int `json:"fs"`
|
|
} `json:"fi"`
|
|
} `json:"fl"`
|
|
Vl struct {
|
|
Vi []struct {
|
|
Fn string `json:"fn"`
|
|
Ti string `json:"ti"`
|
|
Fvkey string `json:"fvkey"`
|
|
Cl struct {
|
|
Fc int `json:"fc"`
|
|
Ci []struct {
|
|
Idx int `json:"idx"`
|
|
} `json:"ci"`
|
|
} `json:"cl"`
|
|
Ul struct {
|
|
UI []struct {
|
|
URL string `json:"url"`
|
|
} `json:"ui"`
|
|
} `json:"ul"`
|
|
} `json:"vi"`
|
|
} `json:"vl"`
|
|
Msg string `json:"msg"`
|
|
}
|
|
|
|
type qqKeyInfo struct {
|
|
Key string `json:"key"`
|
|
}
|
|
|
|
const qqPlayerVersion string = "3.2.19.333"
|
|
|
|
func genStreams(vid, cdn string, data qqVideoInfo) (map[string]downloader.Stream, error) {
|
|
streams := map[string]downloader.Stream{}
|
|
var vkey string
|
|
// number of fragments
|
|
clips := data.Vl.Vi[0].Cl.Fc
|
|
if clips == 0 {
|
|
clips = 1
|
|
}
|
|
|
|
for _, fi := range data.Fl.Fi {
|
|
var fmtIDPrefix string
|
|
fns := strings.Split(data.Vl.Vi[0].Fn, ".")
|
|
if fi.ID > 100000 {
|
|
fmtIDPrefix = "m"
|
|
} else if fi.ID > 10000 {
|
|
fmtIDPrefix = "p"
|
|
}
|
|
if fmtIDPrefix != "" {
|
|
fmtIDName := fmt.Sprintf("%s%d", fmtIDPrefix, fi.ID%10000)
|
|
if len(fns) < 3 {
|
|
// v0739eolv38.mp4 -> v0739eolv38.m701.mp4
|
|
fns = append(fns[:1], append([]string{fmtIDName}, fns[1:]...)...)
|
|
} else {
|
|
// n0687peq62x.p709.mp4 -> n0687peq62x.m709.mp4
|
|
fns[1] = fmtIDName
|
|
}
|
|
} else if len(fns) >= 3 {
|
|
// delete ID part
|
|
// e0765r4mwcr.2.mp4 -> e0765r4mwcr.mp4
|
|
fns = append(fns[:1], fns[2:]...)
|
|
}
|
|
|
|
var urls []downloader.URL
|
|
var totalSize int64
|
|
var filename string
|
|
for part := 1; part < clips+1; part++ {
|
|
// Multiple fragments per streams
|
|
if fmtIDPrefix == "p" {
|
|
if len(fns) < 4 {
|
|
// If the number of fragments > 0, the filename needs to add the number of fragments
|
|
// n0687peq62x.p709.mp4 -> n0687peq62x.p709.1.mp4
|
|
fns = append(fns[:2], append([]string{strconv.Itoa(part)}, fns[2:]...)...)
|
|
} else {
|
|
fns[2] = strconv.Itoa(part)
|
|
}
|
|
}
|
|
filename = strings.Join(fns, ".")
|
|
html, err := request.Get(
|
|
fmt.Sprintf(
|
|
"http://vv.video.qq.com/getkey?otype=json&platform=11&appver=%s&filename=%s&format=%d&vid=%s",
|
|
qqPlayerVersion, filename, fi.ID, vid,
|
|
), cdn, nil,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
jsonStrings := utils.MatchOneOf(html, `QZOutputJson=(.+);$`)
|
|
if jsonStrings == nil || len(jsonStrings) < 2 {
|
|
return nil, extractors.ErrURLParseFailed
|
|
}
|
|
jsonString := jsonStrings[1]
|
|
|
|
var keyData qqKeyInfo
|
|
if err = json.Unmarshal([]byte(jsonString), &keyData); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
vkey = keyData.Key
|
|
if vkey == "" {
|
|
vkey = data.Vl.Vi[0].Fvkey
|
|
}
|
|
realURL := fmt.Sprintf("%s%s?vkey=%s", cdn, filename, vkey)
|
|
size, err := request.Size(realURL, cdn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
urlData := downloader.URL{
|
|
URL: realURL,
|
|
Size: size,
|
|
Ext: "mp4",
|
|
}
|
|
urls = append(urls, urlData)
|
|
totalSize += size
|
|
}
|
|
streams[fi.Name] = downloader.Stream{
|
|
URLs: urls,
|
|
Size: totalSize,
|
|
Quality: fi.Cname,
|
|
}
|
|
}
|
|
return streams, nil
|
|
}
|
|
|
|
// Extract is the main function for extracting data
|
|
func Extract(url string) ([]downloader.Data, error) {
|
|
vids := utils.MatchOneOf(url, `vid=(\w+)`, `/(\w+)\.html`)
|
|
if vids == nil || len(vids) < 2 {
|
|
return nil, extractors.ErrURLParseFailed
|
|
}
|
|
vid := vids[1]
|
|
|
|
if len(vid) != 11 {
|
|
u, err := request.Get(url, url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
vids = utils.MatchOneOf(
|
|
u, `vid=(\w+)`, `vid:\s*["'](\w+)`, `vid\s*=\s*["']\s*(\w+)`,
|
|
)
|
|
if vids == nil || len(vids) < 2 {
|
|
return nil, extractors.ErrURLParseFailed
|
|
}
|
|
vid = vids[1]
|
|
}
|
|
html, err := request.Get(
|
|
fmt.Sprintf(
|
|
"http://vv.video.qq.com/getinfo?otype=json&platform=11&defnpayver=1&appver=%s&defn=shd&vid=%s",
|
|
qqPlayerVersion, vid,
|
|
), url, nil,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
jsonStrings := utils.MatchOneOf(html, `QZOutputJson=(.+);$`)
|
|
if jsonStrings == nil || len(jsonStrings) < 2 {
|
|
return nil, extractors.ErrURLParseFailed
|
|
}
|
|
jsonString := jsonStrings[1]
|
|
|
|
var data qqVideoInfo
|
|
if err = json.Unmarshal([]byte(jsonString), &data); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// API request error
|
|
if data.Msg != "" {
|
|
return nil, errors.New(data.Msg)
|
|
}
|
|
cdn := data.Vl.Vi[0].Ul.UI[len(data.Vl.Vi[0].Ul.UI)-1].URL
|
|
streams, err := genStreams(vid, cdn, data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return []downloader.Data{
|
|
{
|
|
Site: "腾讯视频 v.qq.com",
|
|
Title: data.Vl.Vi[0].Ti,
|
|
Type: "video",
|
|
Streams: streams,
|
|
URL: url,
|
|
},
|
|
}, nil
|
|
}
|