246 lines
5.9 KiB
Go
246 lines
5.9 KiB
Go
package youku
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/hmac"
|
|
"crypto/sha1"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math/rand"
|
|
netURL "net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/iawia002/annie/config"
|
|
"github.com/iawia002/annie/downloader"
|
|
"github.com/iawia002/annie/extractors"
|
|
"github.com/iawia002/annie/request"
|
|
"github.com/iawia002/annie/utils"
|
|
)
|
|
|
|
type errorData struct {
|
|
Note string `json:"note"`
|
|
Code int `json:"code"`
|
|
}
|
|
|
|
type segs struct {
|
|
Size int64 `json:"size"`
|
|
URL string `json:"cdn_url"`
|
|
}
|
|
|
|
type stream struct {
|
|
Size int64 `json:"size"`
|
|
Width int `json:"width"`
|
|
Height int `json:"height"`
|
|
Segs []segs `json:"segs"`
|
|
Type string `json:"stream_type"`
|
|
AudioLang string `json:"audio_lang"`
|
|
}
|
|
|
|
type youkuVideo struct {
|
|
Title string `json:"title"`
|
|
}
|
|
|
|
type youkuShow struct {
|
|
Title string `json:"title"`
|
|
}
|
|
|
|
type data struct {
|
|
Error errorData `json:"error"`
|
|
Stream []stream `json:"stream"`
|
|
Video youkuVideo `json:"video"`
|
|
Show youkuShow `json:"show"`
|
|
}
|
|
|
|
type youkuData struct {
|
|
Data data `json:"data"`
|
|
}
|
|
|
|
const youkuReferer = "https://v.youku.com"
|
|
|
|
func getAudioLang(lang string) string {
|
|
var youkuAudioLang = map[string]string{
|
|
"guoyu": "国语",
|
|
"ja": "日语",
|
|
"yue": "粤语",
|
|
}
|
|
translate, ok := youkuAudioLang[lang]
|
|
if !ok {
|
|
return lang
|
|
}
|
|
return translate
|
|
}
|
|
|
|
// https://g.alicdn.com/player/ykplayer/0.5.61/youku-player.min.js
|
|
// {"0505":"interior","050F":"interior","0501":"interior","0502":"interior","0503":"interior","0510":"adshow","0512":"BDskin","0590":"BDskin"}
|
|
|
|
// var ccodes = []string{"0510", "0502", "0507", "0508", "0512", "0513", "0514", "0503", "0590"}
|
|
|
|
func youkuUps(vid string) (*youkuData, error) {
|
|
var (
|
|
url string
|
|
utid string
|
|
utids []string
|
|
data youkuData
|
|
)
|
|
if strings.Contains(config.Cookie, "cna") {
|
|
utids = utils.MatchOneOf(config.Cookie, `cna=(.+?);`, `cna\s+(.+?)\s`, `cna\s+(.+?)$`)
|
|
|
|
} else {
|
|
headers, err := request.Headers("http://log.mmstat.com/eg.js", youkuReferer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
setCookie := headers.Get("Set-Cookie")
|
|
utids = utils.MatchOneOf(setCookie, `cna=(.+?);`)
|
|
}
|
|
if utids == nil || len(utids) < 2 {
|
|
return nil, extractors.ErrURLParseFailed
|
|
}
|
|
utid = utids[1]
|
|
|
|
// https://g.alicdn.com/player/ykplayer/0.5.61/youku-player.min.js
|
|
// grep -oE '"[0-9a-zA-Z+/=]{256}"' youku-player.min.js
|
|
for _, ccode := range []string{config.YoukuCcode} {
|
|
if ccode == "0103010102" {
|
|
utid = generateUtdid()
|
|
}
|
|
url = fmt.Sprintf(
|
|
"https://ups.youku.com/ups/get.json?vid=%s&ccode=%s&client_ip=192.168.1.1&client_ts=%d&utid=%s&ckey=%s",
|
|
vid, ccode, time.Now().Unix()/1000, netURL.QueryEscape(utid), netURL.QueryEscape(config.YoukuCkey),
|
|
)
|
|
if config.YoukuPassword != "" {
|
|
url = fmt.Sprintf("%s&password=%s", url, config.YoukuPassword)
|
|
}
|
|
html, err := request.GetByte(url, youkuReferer, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// data must be emptied before reassignment, otherwise it will contain the previous value(the 'error' data)
|
|
data = youkuData{}
|
|
if err = json.Unmarshal(html, &data); err != nil {
|
|
return nil, err
|
|
}
|
|
if data.Data.Error == (errorData{}) {
|
|
return &data, nil
|
|
}
|
|
}
|
|
return &data, nil
|
|
}
|
|
|
|
func getBytes(val int32) []byte {
|
|
var buff bytes.Buffer
|
|
binary.Write(&buff, binary.BigEndian, val)
|
|
return buff.Bytes()
|
|
}
|
|
|
|
func hashCode(s string) int32 {
|
|
var result int32
|
|
for _, c := range s {
|
|
result = result*0x1f + int32(c)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func hmacSha1(key []byte, msg []byte) []byte {
|
|
mac := hmac.New(sha1.New, key)
|
|
mac.Write(msg)
|
|
return mac.Sum(nil)
|
|
}
|
|
|
|
func generateUtdid() string {
|
|
timestamp := int32(time.Now().Unix())
|
|
var buffer bytes.Buffer
|
|
buffer.Write(getBytes(timestamp - 60*60*8))
|
|
buffer.Write(getBytes(rand.Int31()))
|
|
buffer.WriteByte(0x03)
|
|
buffer.WriteByte(0x00)
|
|
imei := fmt.Sprintf("%d", rand.Int31())
|
|
buffer.Write(getBytes(hashCode(imei)))
|
|
data := hmacSha1([]byte("d6fc3a4a06adbde89223bvefedc24fecde188aaa9161"), buffer.Bytes())
|
|
buffer.Write(getBytes(hashCode(base64.StdEncoding.EncodeToString(data))))
|
|
return base64.StdEncoding.EncodeToString(buffer.Bytes())
|
|
}
|
|
|
|
func genData(youkuData data) map[string]downloader.Stream {
|
|
var (
|
|
streamString string
|
|
quality string
|
|
)
|
|
streams := map[string]downloader.Stream{}
|
|
for _, stream := range youkuData.Stream {
|
|
if stream.AudioLang == "default" {
|
|
streamString = stream.Type
|
|
quality = fmt.Sprintf(
|
|
"%s %dx%d", stream.Type, stream.Width, stream.Height,
|
|
)
|
|
} else {
|
|
streamString = fmt.Sprintf("%s-%s", stream.Type, stream.AudioLang)
|
|
quality = fmt.Sprintf(
|
|
"%s %dx%d %s", stream.Type, stream.Width, stream.Height,
|
|
getAudioLang(stream.AudioLang),
|
|
)
|
|
}
|
|
|
|
ext := strings.Split(
|
|
strings.Split(stream.Segs[0].URL, "?")[0],
|
|
".",
|
|
)
|
|
urls := make([]downloader.URL, len(stream.Segs))
|
|
for index, data := range stream.Segs {
|
|
urls[index] = downloader.URL{
|
|
URL: data.URL,
|
|
Size: data.Size,
|
|
Ext: ext[len(ext)-1],
|
|
}
|
|
}
|
|
streams[streamString] = downloader.Stream{
|
|
URLs: urls,
|
|
Size: stream.Size,
|
|
Quality: quality,
|
|
}
|
|
}
|
|
return streams
|
|
}
|
|
|
|
// Extract is the main function for extracting data
|
|
func Extract(url string) ([]downloader.Data, error) {
|
|
vids := utils.MatchOneOf(
|
|
url, `id_(.+?)\.html`, `id_(.+)`,
|
|
)
|
|
if vids == nil || len(vids) < 2 {
|
|
return nil, extractors.ErrURLParseFailed
|
|
}
|
|
vid := vids[1]
|
|
|
|
youkuData, err := youkuUps(vid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if youkuData.Data.Error.Code != 0 {
|
|
return nil, errors.New(youkuData.Data.Error.Note)
|
|
}
|
|
streams := genData(youkuData.Data)
|
|
var title string
|
|
if youkuData.Data.Show.Title == "" || strings.Contains(
|
|
youkuData.Data.Video.Title, youkuData.Data.Show.Title,
|
|
) {
|
|
title = youkuData.Data.Video.Title
|
|
} else {
|
|
title = fmt.Sprintf("%s %s", youkuData.Data.Show.Title, youkuData.Data.Video.Title)
|
|
}
|
|
|
|
return []downloader.Data{
|
|
{
|
|
Site: "优酷 youku.com",
|
|
Title: title,
|
|
Type: "video",
|
|
Streams: streams,
|
|
URL: url,
|
|
},
|
|
}, nil
|
|
}
|