1
0
mirror of https://github.com/wbt5/real-url.git synced 2025-08-01 14:48:01 +08:00
2020-08-18 21:49:12 +08:00

311 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# AcFun直播现在属于快手旗下了其JS源码中也可以看到很多名为'kuaishou'的变量所以和快手直播弹幕的获取方法有点类似都使用了protobuf压缩数据。
# 在mplayer-live.xxx.js中可以看到所有websocket过程。奇怪的是其序列化和反序列化时代码并不统一可能有啥"特殊意义"我没看懂。
# ws请求过程Register--EnterRoom--Heartbeat,其中Register后返回的第一条数据里有sessionkey和instanceid。
# 在Chrome调试时发现客户端一直会向服务器发送每1秒一次ping每10秒一次keepalive模拟实现时不发送也正常。
import base64
import gzip
import struct
import time
import requests
from Crypto import Random
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from . import acfun_pb2 as pb
class AcFun:
ws_url = 'wss://link.xiatou.com/'
def __init__(self, rid):
self.encryptionmode = 0 # 加密模式0不加密1时key=ssecurity2时key=sessionkey
self.seqId = 0 # 客户端发送请求时都会加1
self.payload_len = 0
self.sessionkey = b''
self.instanceid = 0 # 初始值0
self.gift = { # 有些直播间的礼物编号不一样实际打开直播间页面会有一次XHR请求
1: '香蕉',
17: '快乐水',
9: '告白',
35: '氧气瓶',
4: '牛啤',
33: '情书',
8: '星蕉雨',
31: '金坷垃',
34: '狗粮',
2: '吃瓜',
12: '打Call',
32: '变身腰带',
15: 'AC机娘',
16: '猴岛',
10: '666',
11: '菜鸡',
30: '鸽鸽',
13: '立FLAG',
6: '魔法棒',
7: '好人卡',
14: '窜天猴',
21: '生日快乐',
5: '手柄',
29: '大触'
}
# 下面获取一些加入房间所需注册参数
headers = {
'content-type': 'application/x-www-form-urlencoded',
'cookie': '_did=H5_',
'referer': 'https://m.acfun.cn/'
}
url = 'https://id.app.acfun.cn/rest/app/visitor/login'
form_data = 'sid=acfun.api.visitor'
with requests.Session() as s:
res = s.post(url, data=form_data, headers=headers).json()
status, *d = res.values()
if status == 0:
# 未登陆访客获取的3个参数
acsecurity, self.uid, visitor_st = d
self.ssecurity = base64.b64decode(acsecurity)
self.token = visitor_st.encode()
else:
raise Exception('token 获取错误')
url = 'https://api.kuaishouzt.com/rest/zt/live/web/startPlay'
params = {
'subBiz': 'mainApp',
'kpn': 'ACFUN_APP',
'kpf': 'OUTSIDE_IOS_H5',
'userId': self.uid,
'did': 'H5_',
'acfun.api.visitor_st': visitor_st
}
data = 'authorId={}&pullStreamType=SINGLE_HLS'.format(rid)
res = s.post(url, params=params, data=data, headers=headers).json()
if res['result'] == 1:
data = res['data']
# 获取直播间的几个参数
self.availabletickets, *_ = data['availableTickets']
self.enterroomattach = data['enterRoomAttach']
self.liveid = data['liveId']
else:
raise Exception('直播已关闭')
@staticmethod
# aes加密iv随机key值取决去encryptionmode
def aes_encode(t, key):
t = pad(t, AES.block_size)
iv = Random.new().read(AES.block_size)
mode = AES.MODE_CBC
c = AES.new(key, mode, iv)
res = c.encrypt(t)
return iv + res # 把iv加到头部
@staticmethod
# 这里传入的待解密数据不用补全一直都是16的整数倍。key值取决去encryptionmode。pkcs7模式去掉最后的填充padding。
def aes_decode(t, key):
iv = t[:16]
n = t[16:]
# n = pad(n, AES.block_size)
mode = AES.MODE_CBC
c = AES.new(key, mode, iv)
res = c.decrypt(n)
length = len(res)
padding = res[length - 1]
res = res[0:length - padding]
return res
def register(self):
# 注册
p = pb.RegisterRequest()
p.appActiveStatus = 1
p.appInfo.appName = 'link-sdk'
p.appInfo.sdkVersion = '1.2.1'
p.deviceInfo.deviceModel = 'h5'
p.deviceInfo.platformType = 6
p.instanceId = 0
p.presenceStatus = 1
p.ztCommonInfo.kpn = 'ACFUN_APP'
p.ztCommonInfo.kpf = 'OUTSIDE_IOS_H5'
p.ztCommonInfo.uid = self.uid
register_data = p.SerializeToString()
self.encryptionmode = 1
self.seqId = 1
return register_data
def keepalive(self):
# keepalive
p = pb.KeepAliveRequest()
p.appActiveStatus = 1
p.presenceStatus = 1
keepalive_data = p.SerializeToString()
self.encryptionmode = 2
self.seqId += 1
return keepalive_data
def ping(self):
# ping
p = pb.PingRequest()
# p.pingType = 2
ping_data = p.SerializeToString()
self.seqId += 1
self.encryptionmode = 2
return ping_data
def ztlivecsenterroom(self):
return self.cscmd('ZtLiveCsEnterRoom')
def ztlivecsheartbeat(self):
# 心跳数据
return self.cscmd('ZtLiveCsHeartbeat')
def cscmd(self, payload_type):
p = getattr(pb, payload_type)()
if payload_type == 'ZtLiveCsEnterRoom':
p.isAuthor = False
p.reconnectCount = 0
p.enterRoomAttach = self.enterroomattach
p.clientLiveSdkVersion = 'kwai-acfun-live-link'
elif payload_type == 'ZtLiveCsHeartbeat':
p.sequence = self.seqId
p.clientTimestampMs = int(time.time() * 1000)
payload = p.SerializeToString()
p = pb.CsCmd()
p.cmdType = payload_type
p.ticket = self.availabletickets
p.payload = payload
p.liveId = self.liveid
cscmd_data = p.SerializeToString()
self.seqId += 1
self.encryptionmode = 2
return cscmd_data
def encode_payload(self, payload_type):
# 要发送的原始数据都是先序列化再拼接
c = {
'keepalive': 'Basic.KeepAlive',
'register': 'Basic.Register',
'ping': 'Basic.Ping',
'ztlivecsenterroom': 'Global.ZtLiveInteractive.CsCmd',
'ztlivecsheartbeat': 'Global.ZtLiveInteractive.CsCmd'
}
p = pb.UpstreamPayload()
p.command = c[payload_type]
p.retryCount = 1
p.payloadData = getattr(self, payload_type)()
p.seqId = self.seqId
p.subBiz = 'mainApp'
e = p.SerializeToString()
key = self.ssecurity if self.encryptionmode == 1 else self.sessionkey
payload_data = AcFun.aes_encode(e, key) if self.encryptionmode != 0 else e
self.payload_len = len(e)
return payload_data
def encode_head(self):
# 头部数据,里面有原始数据长度
p = pb.PacketHeader()
p.appId = 13
p.decodedPayloadLen = self.payload_len
p.encryptionMode = self.encryptionmode
p.instanceId = self.instanceid
p.kpn = 'ACFUN_APP'
p.seqId = self.seqId
if self.encryptionmode == 1:
p.tokenInfo.tokenType = 1
p.tokenInfo.token = self.token
p.uid = self.uid
head = p.SerializeToString()
return head
def encode_packet(self, payload_type):
# 数据组成:固定数据 + 头部长度 + 原始数据长度 + 头部 + 原始数据
body = self.encode_payload(payload_type)
head = self.encode_head()
data = struct.pack('!HHII', 43981, 1, len(head), len(body))
data += head + body
return data
def decode_packet(self, data):
# 数据解包
msgs = [{'name': '', 'content': '', 'msg_type': 'other'}]
head_length, body_length = struct.unpack('!II', data[4:12])
if 12 + head_length + body_length != len(data):
raise Exception('downstream message size is not correct')
# 头部解包
e = data[12:head_length + 12]
c = pb.PacketHeader()
c.ParseFromString(e)
encryptionmode = c.encryptionMode # 根据加密模式确定解密的key
# body解包
h = data[head_length + 12:]
key = self.ssecurity if encryptionmode == 1 else self.sessionkey
n = AcFun.aes_decode(h, key) if encryptionmode != 0 else h
u = pb.DownstreamPayload()
u.ParseFromString(n)
# header = c
# body = u
payload = u.payloadData
command = u.command # 根据command确定返回数据类型
# print(command)
if command == 'Basic.Register':
# websocket第一次返回数据,确定sessKey和instanceId
p = pb.RegisterResponse()
p.ParseFromString(payload)
self.sessionkey = p.sessKey
self.instanceid = p.instanceId
elif command == 'Push.ZtLiveInteractive.Message': # 'Push.ZtLiveInteractive.Message'
a = pb.ZtLiveScMessage()
a.ParseFromString(payload)
o = a.messageType
s = a.payload
if a.compressionType == 2:
s = gzip.decompress(s)
if o == 'ZtLiveScTicketInvalid':
raise Exception('ZtLiveScTicketInvalid')
# o可为'ZtLiveScNotifySignal' 'ZtLiveScActionSignal' 等,弹幕礼物入场点赞等在ZtLiveScActionSignal中
elif o == 'ZtLiveScActionSignal':
p = getattr(pb, o)()
p.ParseFromString(s)
for i in p.item:
p = getattr(pb, i.signalType)()
# signalType:
# CommonActionSignalComment 评论
# CommonActionSignalGift 礼物
# CommonActionSignalUserEnterRoom 入场
# CommonActionSignalLike 点赞
# CommonActionSignalUserFollowAuthor 关注主播
# 等等等
u = {
'CommonActionSignalUserEnterRoom': '进入直播间',
'CommonActionSignalUserFollowAuthor': '关注了主播',
'CommonActionSignalComment': '',
'CommonActionSignalLike': '点赞了❤',
'CommonActionSignalGift': ''
}
for a_payload in i.payload: # i.payload 是 repeated 类型
p.ParseFromString(a_payload)
if i.signalType in u.keys():
user = p.userInfo.nickname
content = u[i.signalType]
if i.signalType == 'CommonActionSignalComment':
content = p.content
elif i.signalType == 'CommonActionSignalGift':
content = '送出 ' + self.gift.get(p.giftId, '')
msg = {'name': user, 'content': content, 'msg_type': 'danmaku'}
msgs.append(msg.copy())
return msgs