286 lines
6.7 KiB
Go
286 lines
6.7 KiB
Go
package youtube
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"regexp"
|
|
"strconv"
|
|
|
|
"github.com/dop251/goja"
|
|
)
|
|
|
|
func (c *Client) decipherURL(ctx context.Context, videoID string, cipher string) (string, error) {
|
|
params, err := url.ParseQuery(cipher)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
uri, err := url.Parse(params.Get("url"))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
query := uri.Query()
|
|
|
|
config, err := c.getPlayerConfig(ctx, videoID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// decrypt s-parameter
|
|
bs, err := config.decrypt([]byte(params.Get("s")))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
query.Add(params.Get("sp"), string(bs))
|
|
|
|
query, err = c.decryptNParam(config, query)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
uri.RawQuery = query.Encode()
|
|
|
|
return uri.String(), nil
|
|
}
|
|
|
|
// see https://github.com/kkdai/youtube/pull/244
|
|
func (c *Client) unThrottle(ctx context.Context, videoID string, urlString string) (string, error) {
|
|
config, err := c.getPlayerConfig(ctx, videoID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
uri, err := url.Parse(urlString)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// for debugging
|
|
if artifactsFolder != "" {
|
|
writeArtifact("video-"+videoID+".url", []byte(uri.String()))
|
|
}
|
|
|
|
query, err := c.decryptNParam(config, uri.Query())
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
uri.RawQuery = query.Encode()
|
|
return uri.String(), nil
|
|
}
|
|
|
|
func (c *Client) decryptNParam(config playerConfig, query url.Values) (url.Values, error) {
|
|
// decrypt n-parameter
|
|
nSig := query.Get("v")
|
|
log := Logger.With("n", nSig)
|
|
|
|
if nSig != "" {
|
|
nDecoded, err := config.decodeNsig(nSig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to decode nSig: %w", err)
|
|
}
|
|
query.Set("v", nDecoded)
|
|
log = log.With("decoded", nDecoded)
|
|
}
|
|
|
|
log.Debug("nParam")
|
|
|
|
return query, nil
|
|
}
|
|
|
|
const (
|
|
jsvarStr = "[a-zA-Z_\\$][a-zA-Z_0-9]*"
|
|
reverseStr = ":function\\(a\\)\\{" +
|
|
"(?:return )?a\\.reverse\\(\\)" +
|
|
"\\}"
|
|
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 (
|
|
nFunctionNameRegexp = regexp.MustCompile("\\.get\\(\"n\"\\)\\)&&\\(b=([a-zA-Z0-9$]{0,3})\\[(\\d+)\\](.+)\\|\\|([a-zA-Z0-9]{0,3})")
|
|
actionsObjRegexp = regexp.MustCompile(fmt.Sprintf(
|
|
"var (%s)=\\{((?:(?:%s%s|%s%s|%s%s),?\\n?)+)\\};", jsvarStr, jsvarStr, swapStr, jsvarStr, spliceStr, jsvarStr, reverseStr))
|
|
|
|
actionsFuncRegexp = regexp.MustCompile(fmt.Sprintf(
|
|
"function(?: %s)?\\(a\\)\\{"+
|
|
"a=a\\.split\\(\"\"\\);\\s*"+
|
|
"((?:(?:a=)?%s\\.%s\\(a,\\d+\\);)+)"+
|
|
"return a\\.join\\(\"\"\\)"+
|
|
"\\}", jsvarStr, jsvarStr, jsvarStr))
|
|
|
|
reverseRegexp = regexp.MustCompile(fmt.Sprintf("(?m)(?:^|,)(%s)%s", jsvarStr, reverseStr))
|
|
spliceRegexp = regexp.MustCompile(fmt.Sprintf("(?m)(?:^|,)(%s)%s", jsvarStr, spliceStr))
|
|
swapRegexp = regexp.MustCompile(fmt.Sprintf("(?m)(?:^|,)(%s)%s", jsvarStr, swapStr))
|
|
)
|
|
|
|
func (config playerConfig) decodeNsig(encoded string) (string, error) {
|
|
fBody, err := config.getNFunction()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return evalJavascript(fBody, encoded)
|
|
}
|
|
|
|
func evalJavascript(jsFunction, arg string) (string, error) {
|
|
const myName = "myFunction"
|
|
|
|
vm := goja.New()
|
|
_, err := vm.RunString(myName + "=" + jsFunction)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var output func(string) string
|
|
err = vm.ExportTo(vm.Get(myName), &output)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return output(arg), nil
|
|
}
|
|
|
|
func (config playerConfig) getNFunction() (string, error) {
|
|
nameResult := nFunctionNameRegexp.FindSubmatch(config)
|
|
if len(nameResult) == 0 {
|
|
return "", errors.New("unable to extract n-function name")
|
|
}
|
|
|
|
var name string
|
|
if idx, _ := strconv.Atoi(string(nameResult[2])); idx == 0 {
|
|
name = string(nameResult[4])
|
|
} else {
|
|
name = string(nameResult[1])
|
|
}
|
|
|
|
return config.extraFunction(name)
|
|
|
|
}
|
|
|
|
func (config playerConfig) extraFunction(name string) (string, error) {
|
|
// find the beginning of the function
|
|
def := []byte(name + "=function(")
|
|
start := bytes.Index(config, def)
|
|
if start < 1 {
|
|
return "", fmt.Errorf("unable to extract n-function body: looking for '%s'", def)
|
|
}
|
|
|
|
// start after the first curly bracket
|
|
pos := start + bytes.IndexByte(config[start:], '{') + 1
|
|
|
|
var strChar byte
|
|
|
|
// find the bracket closing the function
|
|
for brackets := 1; brackets > 0; pos++ {
|
|
b := config[pos]
|
|
switch b {
|
|
case '{':
|
|
if strChar == 0 {
|
|
brackets++
|
|
}
|
|
case '}':
|
|
if strChar == 0 {
|
|
brackets--
|
|
}
|
|
case '`', '"', '\'':
|
|
if config[pos-1] == '\\' && config[pos-2] != '\\' {
|
|
continue
|
|
}
|
|
if strChar == 0 {
|
|
strChar = b
|
|
} else if strChar == b {
|
|
strChar = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
return string(config[start:pos]), nil
|
|
}
|
|
|
|
func (config playerConfig) decrypt(cyphertext []byte) ([]byte, error) {
|
|
operations, err := config.parseDecipherOps()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// apply operations
|
|
bs := []byte(cyphertext)
|
|
for _, op := range operations {
|
|
bs = op(bs)
|
|
}
|
|
|
|
return bs, nil
|
|
}
|
|
|
|
/*
|
|
parses decipher operations from https://youtube.com/s/player/4fbb4d5b/player_ias.vflset/en_US/base.js
|
|
|
|
var Mt={
|
|
splice:function(a,b){a.splice(0,b)},
|
|
reverse:function(a){a.reverse()},
|
|
EQ:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c}};
|
|
|
|
a=a.split("");
|
|
Mt.splice(a,3);
|
|
Mt.EQ(a,39);
|
|
Mt.splice(a,2);
|
|
Mt.EQ(a,1);
|
|
Mt.splice(a,1);
|
|
Mt.EQ(a,35);
|
|
Mt.EQ(a,51);
|
|
Mt.splice(a,2);
|
|
Mt.reverse(a,52);
|
|
return a.join("")
|
|
*/
|
|
func (config playerConfig) parseDecipherOps() (operations []DecipherOperation, err error) {
|
|
objResult := actionsObjRegexp.FindSubmatch(config)
|
|
funcResult := actionsFuncRegexp.FindSubmatch(config)
|
|
if len(objResult) < 3 || len(funcResult) < 2 {
|
|
return nil, fmt.Errorf("error parsing signature tokens (#obj=%d, #func=%d)", len(objResult), len(funcResult))
|
|
}
|
|
|
|
obj := objResult[1]
|
|
objBody := objResult[2]
|
|
funcBody := funcResult[1]
|
|
|
|
var reverseKey, spliceKey, swapKey string
|
|
|
|
if result := reverseRegexp.FindSubmatch(objBody); len(result) > 1 {
|
|
reverseKey = string(result[1])
|
|
}
|
|
if result := spliceRegexp.FindSubmatch(objBody); len(result) > 1 {
|
|
spliceKey = string(result[1])
|
|
}
|
|
if result := swapRegexp.FindSubmatch(objBody); len(result) > 1 {
|
|
swapKey = string(result[1])
|
|
}
|
|
|
|
regex, err := regexp.Compile(fmt.Sprintf("(?:a=)?%s\\.(%s|%s|%s)\\(a,(\\d+)\\)", regexp.QuoteMeta(string(obj)), regexp.QuoteMeta(reverseKey), regexp.QuoteMeta(spliceKey), regexp.QuoteMeta(swapKey)))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var ops []DecipherOperation
|
|
for _, s := range regex.FindAllSubmatch(funcBody, -1) {
|
|
switch string(s[1]) {
|
|
case reverseKey:
|
|
ops = append(ops, reverseFunc)
|
|
case swapKey:
|
|
arg, _ := strconv.Atoi(string(s[2]))
|
|
ops = append(ops, newSwapFunc(arg))
|
|
case spliceKey:
|
|
arg, _ := strconv.Atoi(string(s[2]))
|
|
ops = append(ops, newSpliceFunc(arg))
|
|
}
|
|
}
|
|
return ops, nil
|
|
}
|