diff --git a/client.go b/client.go index 587bbb1..f80676d 100644 --- a/client.go +++ b/client.go @@ -12,7 +12,9 @@ import ( "net/http" "net/url" "strconv" + "strings" "sync/atomic" + "time" ) const ( @@ -23,6 +25,8 @@ const ( playerParams = "CgIQBg==" ) +const CONTENT_PLAYBACK_NONCE_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" + var ErrNoFormat = errors.New("no video format provided") // DefaultClient type to use. No reason to change but you could if you wanted to. @@ -157,6 +161,7 @@ type innertubeClient struct { TimeZone string `json:"timeZone"` UTCOffset int `json:"utcOffsetMinutes"` DeviceModel string `json:"deviceModel,omitempty"` + VisitorData string `json:"visitorData,omitempty"` } // client info for the innertube API @@ -232,6 +237,33 @@ func (c *Client) transcriptDataByInnertube(ctx context.Context, id string, lang return c.httpPostBodyBytes(ctx, "https://www.youtube.com/youtubei/v1/get_transcript?key="+c.client.key, data) } +func randString(alphabet string, sz int) string { + var buf strings.Builder + buf.Grow(sz) + for i := 0; i < sz; i++ { + buf.WriteByte(alphabet[rand.Intn(len(alphabet))]) + } + return buf.String() +} + +func randomVisitorData(countryCode string) string { + var pbE2 ProtoBuilder + + pbE2.String(2, "") + pbE2.Varint(4, int64(rand.Intn(255)+1)) + + var pbE ProtoBuilder + pbE.String(1, countryCode) + pbE.Bytes(2, pbE2.ToBytes()) + + var pb ProtoBuilder + pb.String(1, randString(CONTENT_PLAYBACK_NONCE_ALPHABET, 11)) + pb.Varint(5, time.Now().Unix()-int64(rand.Intn(600000))) + pb.Bytes(6, pbE.ToBytes()) + + return pb.ToUrlEncodedBase64() +} + func prepareInnertubeContext(clientInfo clientInfo) inntertubeContext { return inntertubeContext{ Client: innertubeClient{ @@ -243,6 +275,7 @@ func prepareInnertubeContext(clientInfo clientInfo) inntertubeContext { ClientVersion: clientInfo.version, AndroidSDKVersion: clientInfo.androidVersion, UserAgent: clientInfo.userAgent, + VisitorData: randomVisitorData("US"), }, } } diff --git a/protobuilder.go b/protobuilder.go new file mode 100644 index 0000000..155a051 --- /dev/null +++ b/protobuilder.go @@ -0,0 +1,73 @@ +package youtube + +import ( + "bytes" + "encoding/base64" + "net/url" +) + +type ProtoBuilder struct { + byteBuffer bytes.Buffer +} + +func (pb *ProtoBuilder) ToBytes() []byte { + return pb.byteBuffer.Bytes() +} + +func (pb *ProtoBuilder) ToUrlEncodedBase64() string { + b64 := base64.URLEncoding.EncodeToString(pb.ToBytes()) + return url.QueryEscape(b64) +} + +func (pb *ProtoBuilder) writeVarint(val int64) error { + if val == 0 { + _, err := pb.byteBuffer.Write([]byte{0}) + return err + } + for { + b := byte(val & 0x7F) + val >>= 7 + if val != 0 { + b |= 0x80 + } + _, err := pb.byteBuffer.Write([]byte{b}) + if err != nil { + return err + } + if val == 0 { + break + } + } + return nil +} + +func (pb *ProtoBuilder) field(field int, wireType byte) error { + val := int64(field<<3) | int64(wireType&0x07) + return pb.writeVarint(val) +} + +func (pb *ProtoBuilder) Varint(field int, val int64) error { + err := pb.field(field, 0) + if err != nil { + return err + } + return pb.writeVarint(val) +} + +func (pb *ProtoBuilder) String(field int, stringVal string) error { + strBts := []byte(stringVal) + return pb.Bytes(field, strBts) +} + +func (pb *ProtoBuilder) Bytes(field int, bytesVal []byte) error { + if err := pb.field(field, 2); err != nil { + return err + } + + if err := pb.writeVarint(int64(len(bytesVal))); err != nil { + return err + } + + _, err := pb.byteBuffer.Write(bytesVal) + return err +}