246 lines
5.9 KiB
Go
Raw Normal View History

2020-02-08 20:44:46 +08:00
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
}