mirror of
https://github.com/wbt5/real-url.git
synced 2025-08-04 01:12:48 +08:00
Compare commits
9 Commits
df183eee17
...
c4ab4dfb71
Author | SHA1 | Date | |
---|---|---|---|
|
c4ab4dfb71 | ||
|
40eecd5bcf | ||
|
47e8538164 | ||
|
ed4c68a1ec | ||
|
0c7b9b7bcd | ||
|
3cfbdcd5d0 | ||
|
c75c75713f | ||
|
27fa079145 | ||
|
313f33bb5c |
@ -25,7 +25,9 @@
|
||||
|
||||
## 更新
|
||||
|
||||
### 2020.08.08:新增:AcFun 直播、艺气山直播;更新:哔哩哔哩直播、虎牙直播;红人直播;优化:斗鱼直播。
|
||||
### 2020.08.18:更新快手直播源,现在播放链接需要带参数;更新快手直播弹幕,直接用 protobuf 序列化;新增 AcFun、艺气山两个平台的弹幕功能。
|
||||
|
||||
2020.08.08:新增 AcFun 直播、艺气山直播;更新:哔哩哔哩直播、虎牙直播、红人直播;优化:斗鱼直播。
|
||||
|
||||
2020.07.31:新增 19 个直播平台,详见上面说明;更新YY直播,现在可以获取最高画质;优化战旗直播、优酷直播代码;
|
||||
|
||||
|
139
danmu/danmaku/_173.proto
Normal file
139
danmu/danmaku/_173.proto
Normal file
@ -0,0 +1,139 @@
|
||||
syntax = "proto2";
|
||||
package YiQishanPack;
|
||||
|
||||
message CSHead {
|
||||
optional uint32 command = 1;
|
||||
optional uint32 subcmd = 2;
|
||||
optional uint32 seq = 3;
|
||||
optional bytes uuid = 4;
|
||||
optional uint32 clientType = 5;
|
||||
optional uint32 headFlag = 6;
|
||||
optional uint32 clientVer = 7;
|
||||
optional bytes signature = 8;
|
||||
optional uint32 routeKey = 9;
|
||||
}
|
||||
|
||||
message TCPAccessReq {
|
||||
optional bytes AccessToken = 1;
|
||||
optional bytes MachineCode = 2;
|
||||
}
|
||||
|
||||
message TcpHelloReq {
|
||||
optional string uuid = 1;
|
||||
}
|
||||
|
||||
message EnterRoomReq {
|
||||
optional bytes uuid = 1;
|
||||
optional bytes roomid = 2;
|
||||
optional uint32 neednum = 3;
|
||||
optional bool isfake = 4;
|
||||
optional bool needbroadcast = 5;
|
||||
optional bytes nick = 6;
|
||||
optional bytes clientip = 7;
|
||||
optional bytes subroomid = 8;
|
||||
optional uint32 gameid = 10;
|
||||
}
|
||||
|
||||
message RoomHelloReq {
|
||||
optional bytes uuid = 1;
|
||||
optional bytes roomid = 2;
|
||||
optional bytes roomsig = 3;
|
||||
optional uint32 connsvrip = 4;
|
||||
optional bool isinternal = 5;
|
||||
optional bytes subroomid = 6;
|
||||
}
|
||||
|
||||
message Token {
|
||||
optional string uuid = 1;
|
||||
optional bytes gtkey = 2;
|
||||
optional uint32 ip = 3;
|
||||
optional uint32 expiresstime = 4;
|
||||
optional uint32 gentime = 5;
|
||||
}
|
||||
|
||||
message PublicChatNotify {
|
||||
optional bytes roomid = 1;
|
||||
optional bytes uuid = 2;
|
||||
optional bytes nick = 3;
|
||||
optional ChatInfo info = 4;
|
||||
optional bytes touuid = 5;
|
||||
optional bytes tonick = 6;
|
||||
optional uint32 privilege = 7;
|
||||
optional uint32 rank = 8;
|
||||
optional uint32 fromgame = 9;
|
||||
optional bytes gameid = 10;
|
||||
repeated BadgeType badges = 11;
|
||||
optional RoomUserInfo userinfo = 12;
|
||||
optional bool isnoble = 13;
|
||||
optional uint32 noblelevelid = 14;
|
||||
optional string noblelevelname = 15;
|
||||
optional bool isnoblemessage = 16;
|
||||
}
|
||||
|
||||
enum BadgeType {
|
||||
NOBARRAGE = 0;
|
||||
FIRST_CHARGE_BADGE = 1;
|
||||
FIRST_CHARGE_COPPER = 2;
|
||||
FIRST_CHARGE_SLIVER = 3;
|
||||
FIRST_CHARGE_GOLD = 4;
|
||||
}
|
||||
|
||||
message ChatInfo {
|
||||
optional uint32 chattype = 1;
|
||||
optional bytes textmsg = 2;
|
||||
}
|
||||
|
||||
message RoomUserInfo {
|
||||
optional bytes uuid = 1;
|
||||
optional bytes nick = 2;
|
||||
optional uint32 weekartistconsume = 3;
|
||||
optional uint32 artisttotalconsume = 4;
|
||||
optional uint32 totalconsume = 5;
|
||||
optional uint32 guardendtime = 6;
|
||||
optional uint32 peerageid = 7;
|
||||
}
|
||||
|
||||
message GiftNotyInfo {
|
||||
optional bytes roomid = 1;
|
||||
optional bytes giftid = 2;
|
||||
optional uint32 giftcnt = 3;
|
||||
optional bytes fromuuid = 4;
|
||||
optional bytes fromnick = 5;
|
||||
optional bytes touuid = 6;
|
||||
optional bytes tonick = 7;
|
||||
optional uint32 consume = 8;
|
||||
optional bytes sessid = 9;
|
||||
optional uint32 hits = 10;
|
||||
optional uint32 hitsall = 11;
|
||||
optional uint32 flag = 12;
|
||||
optional uint32 fromviplevel = 13;
|
||||
optional uint32 fanslevel = 14;
|
||||
optional bool fromisnoble = 15;
|
||||
optional uint32 fromnoblelevelid = 16;
|
||||
}
|
||||
|
||||
message NotifyFreeGift {
|
||||
optional bytes uuid = 1;
|
||||
optional bytes fromnick = 2;
|
||||
optional bytes touuid = 3;
|
||||
optional bytes tonick = 4;
|
||||
optional bytes roomid = 5;
|
||||
optional uint32 giftid = 6;
|
||||
optional uint32 giftcnt = 7;
|
||||
optional uint32 fromviplevel = 8;
|
||||
optional uint32 fanslevel = 9;
|
||||
optional bool fromisnoble = 11;
|
||||
optional uint32 fromnoblelevelid = 12;
|
||||
}
|
||||
|
||||
message SendBroadcastPkg {
|
||||
optional bytes uuid = 1;
|
||||
repeated BroadcastMsg broadcastmsg = 2;
|
||||
|
||||
message BroadcastMsg {
|
||||
optional uint32 businesstype = 1;
|
||||
optional bytes title = 2;
|
||||
optional bytes content = 3;
|
||||
optional uint32 msgseq = 4;
|
||||
}
|
||||
}
|
137
danmu/danmaku/_173.py
Normal file
137
danmu/danmaku/_173.py
Normal file
@ -0,0 +1,137 @@
|
||||
import binascii
|
||||
import struct
|
||||
|
||||
import requests
|
||||
from Crypto.Cipher import DES
|
||||
from Crypto.Util.Padding import pad
|
||||
|
||||
from . import _173_pb2 as pb
|
||||
|
||||
|
||||
class YiQiShan:
|
||||
ws_url = 'wss://websocket.173.com/'
|
||||
|
||||
def __init__(self, rid):
|
||||
self.rid = str(rid)
|
||||
self.key = b'e#>&*m16'
|
||||
with requests.Session() as se:
|
||||
res = se.get('http://www.173.com/{}'.format(rid))
|
||||
try:
|
||||
self.uuid, _, token, _ = res.cookies.values()
|
||||
except ValueError:
|
||||
raise Exception('房间不存在')
|
||||
self.accesstoken = binascii.a2b_hex(token)
|
||||
s = YiQiShan.des_decode(self.accesstoken, self.key)
|
||||
p = pb.Token()
|
||||
p.ParseFromString(s)
|
||||
self.gtkey = p.gtkey[:8]
|
||||
|
||||
@staticmethod
|
||||
def des_encode(t, key):
|
||||
t = pad(t, DES.block_size)
|
||||
c = DES.new(key, DES.MODE_ECB)
|
||||
res = c.encrypt(t)
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def des_decode(t, key):
|
||||
c = DES.new(key, DES.MODE_ECB)
|
||||
res = c.decrypt(t)
|
||||
length = len(res)
|
||||
padding = res[length - 1]
|
||||
res = res[0:length - padding]
|
||||
return res
|
||||
|
||||
def startup(self):
|
||||
p = pb.TCPAccessReq()
|
||||
p.AccessToken = self.accesstoken
|
||||
return p.SerializeToString()
|
||||
|
||||
def tcphelloreq(self):
|
||||
p = pb.TcpHelloReq()
|
||||
p.uuid = self.uuid
|
||||
return p.SerializeToString()
|
||||
|
||||
def enterroomreq(self):
|
||||
p = pb.EnterRoomReq()
|
||||
p.uuid = self.uuid.encode()
|
||||
p.roomid = self.rid.encode()
|
||||
return p.SerializeToString()
|
||||
|
||||
def roomhelloreq(self):
|
||||
p = pb.RoomHelloReq()
|
||||
p.uuid = self.uuid.encode()
|
||||
p.roomid = self.rid.encode()
|
||||
return p.SerializeToString()
|
||||
|
||||
def pack(self, paylod_type):
|
||||
command = {
|
||||
'startup': 123,
|
||||
'tcphelloreq': 122,
|
||||
'enterroomreq': 601,
|
||||
'roomhelloreq': 600
|
||||
}
|
||||
subcmd = {
|
||||
'startup': 0,
|
||||
'tcphelloreq': 0,
|
||||
'enterroomreq': 1,
|
||||
'roomhelloreq': 1
|
||||
}
|
||||
p = pb.CSHead()
|
||||
p.command = command[paylod_type]
|
||||
p.subcmd = subcmd[paylod_type]
|
||||
p.uuid = self.uuid.encode()
|
||||
p.clientType = 4
|
||||
p.routeKey = int(self.rid)
|
||||
n = p.SerializeToString()
|
||||
|
||||
key = self.key if paylod_type == 'startup' else self.gtkey
|
||||
payload = getattr(self, paylod_type)()
|
||||
s = YiQiShan.des_encode(payload, key)
|
||||
|
||||
buf = struct.pack('!HcH', len(n) + len(s) + 8, b'W', len(n))
|
||||
buf += n
|
||||
buf += struct.pack('!H', len(s))
|
||||
buf += s + b'M'
|
||||
return buf
|
||||
|
||||
def unpack(self, data):
|
||||
msgs = [{'name': '', 'content': '', 'msg_type': 'other'}]
|
||||
|
||||
s, = struct.unpack_from('!h', data, 3)
|
||||
p, = struct.unpack_from('!h', data, 5 + s)
|
||||
u = data[7 + s:7 + s + p]
|
||||
|
||||
a = pb.CSHead()
|
||||
a.ParseFromString(data[5:5 + s])
|
||||
cmd = a.command
|
||||
key = self.key if cmd == 123 else self.gtkey
|
||||
t = u if cmd == 102 else YiQiShan.des_decode(u, key)
|
||||
|
||||
o = cmd
|
||||
# r = a.subcmd
|
||||
if o == 102:
|
||||
p = pb.SendBroadcastPkg()
|
||||
p.ParseFromString(t)
|
||||
for i in p.broadcastmsg:
|
||||
# PublicChatNotify = 1
|
||||
# BUSINESS_TYPE_FREE_GIFT = 2
|
||||
# BUSINESS_TYPE_PAY_GIFT = 3
|
||||
if i.businesstype == 1: # 发言
|
||||
q = pb.PublicChatNotify()
|
||||
q.ParseFromString(i.content)
|
||||
user = q.nick.decode()
|
||||
content = q.info.textmsg.decode()
|
||||
# elif i.businesstype == 2: # 免费礼物
|
||||
# print(i.businesstype)
|
||||
# q = pb.NotifyFreeGift()
|
||||
# q.ParseFromString(i.content)
|
||||
# elif i.businesstype == 3: # 收费礼物
|
||||
# print(i.businesstype)
|
||||
# q = pb.GiftNotyInfo()
|
||||
# q.ParseFromString(i.content)
|
||||
# else:
|
||||
# pass
|
||||
msg = {'name': user, 'content': content, 'msg_type': 'danmaku'}
|
||||
msgs.append(msg.copy())
|
||||
return msgs
|
1141
danmu/danmaku/_173_pb2.py
Normal file
1141
danmu/danmaku/_173_pb2.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,23 +1,26 @@
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
import aiohttp
|
||||
|
||||
from ._173 import YiQiShan
|
||||
from .acfun import AcFun
|
||||
from .bilibili import Bilibili
|
||||
from .cc import CC
|
||||
from .douyu import Douyu
|
||||
from .huya import Huya
|
||||
from .kuaishou import KuaiShou
|
||||
from .huomao import HuoMao
|
||||
from .egame import eGame
|
||||
from .huajiao import HuaJiao
|
||||
from .huomao import HuoMao
|
||||
from .huya import Huya
|
||||
from .inke import Inke
|
||||
from .cc import CC
|
||||
from .kuaishou import KuaiShou
|
||||
from .kugou import KuGou
|
||||
from .zhanqi import ZhanQi
|
||||
from .laifeng import LaiFeng
|
||||
from .longzhu import LongZhu
|
||||
from .look import Look
|
||||
from .pps import QiXiu
|
||||
from .qf import QF
|
||||
from .laifeng import LaiFeng
|
||||
from .look import Look
|
||||
from .zhanqi import ZhanQi
|
||||
|
||||
__all__ = ['DanmakuClient']
|
||||
|
||||
@ -50,14 +53,16 @@ class DanmakuClient:
|
||||
'pps.tv': QiXiu,
|
||||
'qf.56.com': QF,
|
||||
'laifeng.com': LaiFeng,
|
||||
'look.163.com': Look}.items():
|
||||
'look.163.com': Look,
|
||||
'acfun.cn': AcFun,
|
||||
'173.com': YiQiShan}.items():
|
||||
if re.match(r'^(?:http[s]?://)?.*?%s/(.+?)$' % u, url):
|
||||
self.__site = s
|
||||
self.__u = u
|
||||
break
|
||||
if self.__site is None:
|
||||
print('Invalid link!')
|
||||
exit
|
||||
exit()
|
||||
self.__hs = aiohttp.ClientSession()
|
||||
|
||||
async def init_ws(self):
|
||||
@ -108,14 +113,89 @@ class DanmakuClient:
|
||||
for m in ms:
|
||||
await self.__dm_queue.put(m)
|
||||
count += 1
|
||||
await self.heartbeats()
|
||||
|
||||
async def init_ws_acfun(self, s):
|
||||
self.__ws = await self.__hs.ws_connect(self.__site.ws_url)
|
||||
await self.__ws.send_bytes(s.encode_packet('register'))
|
||||
|
||||
async def ping_acfun(self, s):
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
await self.__ws.send_bytes(s.encode_packet('ping'))
|
||||
|
||||
async def keepalive_acfun(self, s):
|
||||
while True:
|
||||
await asyncio.sleep(50)
|
||||
await self.__ws.send_bytes(s.encode_packet('keepalive'))
|
||||
|
||||
async def heartbeat_acfun(self, s):
|
||||
while True:
|
||||
await asyncio.sleep(10)
|
||||
await self.__ws.send_bytes(s.encode_packet('ztlivecsheartbeat'))
|
||||
|
||||
async def fetch_danmaku_acfun(self, s):
|
||||
count = 0
|
||||
async for msg in self.__ws:
|
||||
self.__link_status = True
|
||||
ms = s.decode_packet(msg.data)
|
||||
if count == 0:
|
||||
await self.__ws.send_bytes(s.encode_packet('ztlivecsenterroom'))
|
||||
count += 1
|
||||
for m in ms:
|
||||
await self.__dm_queue.put(m)
|
||||
|
||||
async def init_ws_173(self, s):
|
||||
self.__ws = await self.__hs.ws_connect(self.__site.ws_url)
|
||||
await self.__ws.send_bytes(s.pack('startup'))
|
||||
await asyncio.sleep(1)
|
||||
await self.__ws.send_bytes(s.pack('enterroomreq'))
|
||||
|
||||
async def tcphelloreq_173(self, s):
|
||||
while True:
|
||||
await asyncio.sleep(10)
|
||||
await self.__ws.send_bytes(s.pack('tcphelloreq'))
|
||||
|
||||
async def roomhelloreq_173(self, s):
|
||||
while True:
|
||||
await asyncio.sleep(5)
|
||||
await self.__ws.send_bytes(s.pack('roomhelloreq'))
|
||||
|
||||
async def fetch_danmaku_173(self, s):
|
||||
async for msg in self.__ws:
|
||||
self.__link_status = True
|
||||
ms = s.unpack(msg.data)
|
||||
for m in ms:
|
||||
await self.__dm_queue.put(m)
|
||||
|
||||
async def start(self):
|
||||
if self.__u == 'huajiao.com':
|
||||
await self.init_ws_huajiao()
|
||||
elif self.__u == 'acfun.cn':
|
||||
rid = re.search(r'\d+', self.__url).group(0)
|
||||
s = self.__site(rid)
|
||||
await self.init_ws_acfun(s)
|
||||
await asyncio.gather(
|
||||
self.ping_acfun(s),
|
||||
self.fetch_danmaku_acfun(s),
|
||||
self.keepalive_acfun(s),
|
||||
self.heartbeat_acfun(s),
|
||||
)
|
||||
elif self.__u == '173.com':
|
||||
rid = self.__url.split('/')[-1]
|
||||
s = self.__site(rid)
|
||||
await self.init_ws_173(s)
|
||||
await asyncio.gather(
|
||||
self.fetch_danmaku_173(s),
|
||||
self.tcphelloreq_173(s),
|
||||
self.roomhelloreq_173(s),
|
||||
)
|
||||
else:
|
||||
await self.init_ws()
|
||||
await asyncio.gather(
|
||||
self.heartbeats(),
|
||||
self.fetch_danmaku(),
|
||||
)
|
||||
|
||||
async def stop(self):
|
||||
self.__stop = True
|
||||
await self.__hs.close()
|
||||
|
437
danmu/danmaku/acfun.proto
Normal file
437
danmu/danmaku/acfun.proto
Normal file
@ -0,0 +1,437 @@
|
||||
syntax = "proto2";
|
||||
package AcFunPack;
|
||||
|
||||
message RegisterRequest {
|
||||
optional AppInfo appInfo = 1;
|
||||
optional DeviceInfo deviceInfo = 2;
|
||||
optional EnvInfo envInfo = 3;
|
||||
optional PresenceStatus presenceStatus = 4;
|
||||
optional ActiveStatus appActiveStatus = 5;
|
||||
optional bytes appCustomStatus = 6;
|
||||
optional PushServiceToken pushServiceToken = 7;
|
||||
optional int64 instanceId = 8;
|
||||
repeated PushServiceToken pushServiceTokenList = 9;
|
||||
optional int32 keepaliveIntervalSec = 10;
|
||||
optional ZtCommonInfo ztCommonInfo = 11;
|
||||
|
||||
enum PresenceStatus {
|
||||
kPresenceOffline = 0;
|
||||
kPresenceOnline = 1;
|
||||
}
|
||||
|
||||
enum ActiveStatus {
|
||||
kInvalid = 0;
|
||||
kAppInForeground = 1;
|
||||
kAppInBackground = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message RegisterResponse {
|
||||
optional AccessPointsConfig accessPointsConfig = 1;
|
||||
optional bytes sessKey = 2;
|
||||
optional int64 instanceId = 3;
|
||||
optional SdkOption sdkOption = 4;
|
||||
optional AccessPointsConfig accessPointsConfigIpv6 = 5;
|
||||
}
|
||||
|
||||
message AccessPointsConfig {
|
||||
repeated AccessPoint optimalAps = 1;
|
||||
repeated AccessPoint backupAps = 2;
|
||||
repeated uint32 availablePorts = 3;
|
||||
optional AccessPoint forceLastConnectedAp = 4;
|
||||
}
|
||||
|
||||
message AccessPoint {
|
||||
optional AddressType addressType = 1;
|
||||
optional uint32 port = 2;
|
||||
optional fixed32 ipV4 = 3;
|
||||
optional bytes ipV6 = 4;
|
||||
optional string domain = 5;
|
||||
|
||||
enum AddressType {
|
||||
kIPV4 = 0;
|
||||
kIPV6 = 1;
|
||||
kDomain = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message SdkOption {
|
||||
optional int32 reportIntervalSeconds = 1;
|
||||
optional string reportSecurity = 2;
|
||||
optional int32 lz4CompressionThresholdBytes = 3;
|
||||
repeated string netCheckServers = 4;
|
||||
}
|
||||
|
||||
message ZtLiveCsEnterRoom {
|
||||
optional bool isAuthor = 1;
|
||||
optional uint32 reconnectCount = 2;
|
||||
optional uint32 lastErrorCode = 3;
|
||||
optional string enterRoomAttach = 4;
|
||||
optional string clientLiveSdkVersion = 5;
|
||||
}
|
||||
|
||||
message ZtLiveCsHeartbeat {
|
||||
optional uint64 clientTimestampMs = 1;
|
||||
optional bool sequence = 2;
|
||||
}
|
||||
|
||||
message CsCmd {
|
||||
optional string cmdType = 1;
|
||||
optional bytes payload = 2;
|
||||
optional string ticket = 3;
|
||||
optional string liveId = 4;
|
||||
}
|
||||
|
||||
message AppInfo {
|
||||
optional string appName = 1;
|
||||
optional string appVersion = 2;
|
||||
optional string appChannel = 3;
|
||||
optional string sdkVersion = 4;
|
||||
map < string,
|
||||
string > extensionInfo = 5;
|
||||
}
|
||||
|
||||
message DeviceInfo {
|
||||
optional PlatformType platformType = 1;
|
||||
optional string osVersion = 2;
|
||||
optional string deviceModel = 3;
|
||||
optional bytes imeiMd5 = 4;
|
||||
optional string deviceId = 5;
|
||||
optional string softDid = 6;
|
||||
optional string kwaiDid = 7;
|
||||
optional string manufacturer = 8;
|
||||
optional string deviceName = 9;
|
||||
|
||||
enum PlatformType {
|
||||
kInvalid = 0;
|
||||
kAndroid = 1;
|
||||
kiOS = 2;
|
||||
kWindows = 3;
|
||||
WECHAT_ANDROID = 4;
|
||||
WECHAT_IOS = 5;
|
||||
H5 = 6;
|
||||
H5_ANDROID = 7;
|
||||
H5_IOS = 8;
|
||||
H5_WINDOWS = 9;
|
||||
H5_MAC = 10;
|
||||
kPlatformNum = 11;
|
||||
}
|
||||
}
|
||||
|
||||
message EnvInfo {
|
||||
optional NetworkType networkType = 1;
|
||||
optional bytes apnName = 2;
|
||||
|
||||
enum NetworkType {
|
||||
kInvalid = 0;
|
||||
kWIFI = 1;
|
||||
kCellular = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message PushServiceToken {
|
||||
optional PushType pushType = 1;
|
||||
optional bytes token = 2;
|
||||
optional bool isPassThrough = 3;
|
||||
|
||||
enum PushType {
|
||||
kPushTypeInvalid = 0;
|
||||
kPushTypeAPNS = 1;
|
||||
kPushTypeXmPush = 2;
|
||||
kPushTypeJgPush = 3;
|
||||
kPushTypeGtPush = 4;
|
||||
kPushTypeOpPush = 5;
|
||||
kPushTypeVvPush = 6;
|
||||
kPushTypeHwPush = 7;
|
||||
kPushTypeFcm = 8;
|
||||
}
|
||||
}
|
||||
|
||||
message ZtCommonInfo {
|
||||
optional string kpn = 1;
|
||||
optional string kpf = 2;
|
||||
optional int64 uid = 4;
|
||||
optional string did = 5;
|
||||
}
|
||||
|
||||
message PingResponse {
|
||||
optional sfixed32 serverTimestamp = 1;
|
||||
optional fixed32 clientIp = 2;
|
||||
optional fixed32 redirectIp = 3;
|
||||
optional uint32 redirectPort = 4;
|
||||
}
|
||||
|
||||
message PingRequest {
|
||||
optional PingType pingType = 1;
|
||||
optional uint32 pingRound = 2;
|
||||
|
||||
enum PingType {
|
||||
kInvalid = 0;
|
||||
kPriorRegister = 1;
|
||||
kPostRegister = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message UpstreamPayload {
|
||||
optional string command = 1;
|
||||
optional int64 seqId = 2;
|
||||
optional uint32 retryCount = 3;
|
||||
optional bytes payloadData = 4;
|
||||
optional UserInstance userInstance = 5;
|
||||
optional int32 errorCode = 6;
|
||||
optional SettingInfo settingInfo = 7;
|
||||
optional RequsetBasicInfo requestBasicInfo = 8;
|
||||
optional string subBiz = 9;
|
||||
optional FrontendInfo frontendInfo = 10;
|
||||
optional string kpn = 11;
|
||||
optional bool anonymouseUser = 12;
|
||||
}
|
||||
|
||||
message DownstreamPayload {
|
||||
optional string command = 1;
|
||||
optional int64 seqId = 2;
|
||||
optional int32 errorCode = 3;
|
||||
optional bytes payloadData = 4;
|
||||
optional string errorMsg = 5;
|
||||
optional bytes errorData = 6;
|
||||
optional string subBiz = 7;
|
||||
}
|
||||
|
||||
message UserInstance {
|
||||
optional User user = 1;
|
||||
optional int64 instanceId = 2;
|
||||
}
|
||||
|
||||
message User {
|
||||
optional int32 appId = 1;
|
||||
optional int64 uid = 2;
|
||||
}
|
||||
|
||||
message SettingInfo {
|
||||
optional string locale = 1;
|
||||
optional sint32 timezone = 2;
|
||||
}
|
||||
|
||||
message RequsetBasicInfo {
|
||||
optional DeviceInfo.PlatformType clientType = 1;
|
||||
optional string deviceId = 2;
|
||||
optional string clientIp = 3;
|
||||
optional string appVersion = 4;
|
||||
optional string channel = 5;
|
||||
optional AppInfo appInfo = 6;
|
||||
optional DeviceInfo deviceInfo = 7;
|
||||
optional EnvInfo envInfo = 8;
|
||||
optional int32 clientPort = 9;
|
||||
optional string location = 10;
|
||||
optional string kpf = 11;
|
||||
}
|
||||
|
||||
message FrontendInfo {
|
||||
optional string ip = 1;
|
||||
optional int32 port = 2;
|
||||
}
|
||||
|
||||
message PacketHeader {
|
||||
optional int32 appId = 1;
|
||||
optional int64 uid = 2;
|
||||
optional int64 instanceId = 3;
|
||||
optional uint32 flags = 4;
|
||||
optional EncodingType encodingType = 6;
|
||||
optional uint32 decodedPayloadLen = 7;
|
||||
optional EncryptionMode encryptionMode = 8;
|
||||
optional TokenInfo tokenInfo = 9;
|
||||
optional int64 seqId = 10;
|
||||
repeated Feature features = 11;
|
||||
optional string kpn = 12;
|
||||
|
||||
enum Flags {
|
||||
option allow_alias = true;
|
||||
kDirUpstream = 0;
|
||||
kDirDownstream = 1;
|
||||
kDirMask = 1;
|
||||
}
|
||||
|
||||
enum EncodingType {
|
||||
kEncodingNone = 0;
|
||||
kEncodingLz4 = 1;
|
||||
}
|
||||
|
||||
enum EncryptionMode {
|
||||
kEncryptionNone = 0;
|
||||
kEncryptionServiceToken = 1;
|
||||
kEncryptionSessionKey = 2;
|
||||
}
|
||||
|
||||
enum Feature {
|
||||
kReserve = 0;
|
||||
kCompressLz4 = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message TokenInfo {
|
||||
optional TokenType tokenType = 1;
|
||||
optional bytes token = 2;
|
||||
|
||||
enum TokenType {
|
||||
kInvalid = 0;
|
||||
kServiceToken = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message KeepAliveRequest {
|
||||
optional RegisterRequest.PresenceStatus presenceStatus = 1;
|
||||
optional RegisterRequest.ActiveStatus appActiveStatus = 2;
|
||||
optional PushServiceToken pushServiceToken = 3;
|
||||
repeated PushServiceToken pushServiceTokenList = 4;
|
||||
repeated int32 keepaliveIntervalSec = 5;
|
||||
}
|
||||
|
||||
message ZtLiveScMessage {
|
||||
optional string messageType = 1;
|
||||
optional int32 compressionType = 2;
|
||||
optional bytes payload = 3;
|
||||
optional string liveId = 4;
|
||||
optional string ticket = 5;
|
||||
optional uint64 serverTimestampMs = 6;
|
||||
}
|
||||
|
||||
message ZtLiveScNotifySignal {
|
||||
repeated ZtLiveNotifySignalItem item = 1;
|
||||
|
||||
message ZtLiveNotifySignalItem {
|
||||
optional string signalType = 1;
|
||||
repeated bytes payload = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message ZtLiveScActionSignal {
|
||||
repeated ZtLiveActionSignalItem item = 1;
|
||||
|
||||
message ZtLiveActionSignalItem {
|
||||
optional string signalType = 1;
|
||||
repeated bytes payload = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message ZtLiveScStateSignal {
|
||||
repeated ZtLiveStateSignalItem item = 1;
|
||||
|
||||
message ZtLiveStateSignalItem {
|
||||
optional string signalType = 1;
|
||||
repeated bytes payload = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message ZtLiveScStatusChanged {
|
||||
optional int32 type = 1;
|
||||
optional uint64 maxRandomDelayMs = 2;
|
||||
optional BannedInfo bannedInfo = 3;
|
||||
|
||||
message BannedInfo {
|
||||
optional string banReason = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message CommonActionSignalComment {
|
||||
optional string content = 1;
|
||||
optional uint64 sendTimeMs = 2;
|
||||
optional ZtLiveUserInfo userInfo = 3;
|
||||
}
|
||||
|
||||
message CommonActionSignalLike {
|
||||
optional ZtLiveUserInfo userInfo = 1;
|
||||
optional uint64 sendTimeMs = 2;
|
||||
}
|
||||
|
||||
message CommonActionSignalGift {
|
||||
optional ZtLiveUserInfo userInfo = 1;
|
||||
optional uint64 sendTimeMs = 2;
|
||||
optional uint64 giftId = 3;
|
||||
optional uint32 batchSize = 4;
|
||||
optional uint32 comboCount = 5;
|
||||
optional uint64 rank = 6;
|
||||
optional string comboKey = 7;
|
||||
optional uint64 slotDisplayDurationMs = 8;
|
||||
optional uint64 expireDurationMs = 9;
|
||||
}
|
||||
|
||||
message CommonStateSignalDisplayInfo {
|
||||
optional string watchingCount = 1;
|
||||
optional string likeCount = 2;
|
||||
optional uint32 likeDelta = 3;
|
||||
}
|
||||
|
||||
message CommonStateSignalTopUsers {
|
||||
repeated TopUser topUser = 1;
|
||||
|
||||
message TopUser {
|
||||
optional ZtLiveUserInfo userInfo = 1;
|
||||
optional string customWatchingListData = 2;
|
||||
optional string displaySendAmount = 3;
|
||||
optional bool anonymousUser = 4;
|
||||
}
|
||||
}
|
||||
|
||||
message CommonActionSignalUserEnterRoom {
|
||||
optional ZtLiveUserInfo userInfo = 1;
|
||||
optional uint64 sendTimeMs = 2;
|
||||
}
|
||||
|
||||
message CommonActionSignalUserFollowAuthor {
|
||||
optional ZtLiveUserInfo userInfo = 1;
|
||||
optional uint64 sendTimeMs = 2;
|
||||
}
|
||||
|
||||
message CommonNotifySignalKickedOut {
|
||||
optional string reason = 1;
|
||||
}
|
||||
|
||||
message CommonNotifySignalViolationAlert {
|
||||
optional string violationContent = 1;
|
||||
}
|
||||
|
||||
message CommonStateSignalCurrentRedpackList {
|
||||
|
||||
}
|
||||
|
||||
message CommonStateSignalRecentComment {
|
||||
optional CommonActionSignalComment comment = 1;
|
||||
}
|
||||
|
||||
message CommonStateSignalChatReady {
|
||||
optional string chatId = 1;
|
||||
optional ZtLiveUserInfo guestUserInfo = 2;
|
||||
optional int32 mediaType = 3;
|
||||
}
|
||||
|
||||
message CommonStateSignalChatEnd {
|
||||
optional string chatId = 1;
|
||||
optional int32 endType = 2;
|
||||
}
|
||||
|
||||
message AcfunActionSignalThrowBanana {
|
||||
optional UserInfo visitor = 1;
|
||||
optional int32 count = 2;
|
||||
optional uint64 sendTimeMs = 3;
|
||||
}
|
||||
|
||||
message AcfunStateSignalDisplayInfo {
|
||||
optional string bananaCount = 1;
|
||||
}
|
||||
|
||||
message ZtLiveUserInfo {
|
||||
optional uint64 userId = 1;
|
||||
optional string nickname = 2;
|
||||
optional ImageCdnNode avatar = 3;
|
||||
}
|
||||
|
||||
message ImageCdnNode {
|
||||
optional string cdn = 1;
|
||||
optional string url = 2;
|
||||
optional string urlPattern = 3;
|
||||
}
|
||||
|
||||
message UserInfo {
|
||||
optional uint64 userId = 1;
|
||||
optional string name = 2;
|
||||
}
|
310
danmu/danmaku/acfun.py
Normal file
310
danmu/danmaku/acfun.py
Normal file
@ -0,0 +1,310 @@
|
||||
# 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=ssecurity,2时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
|
3650
danmu/danmaku/acfun_pb2.py
Normal file
3650
danmu/danmaku/acfun_pb2.py
Normal file
File diff suppressed because one or more lines are too long
204
danmu/danmaku/kuaishou.proto
Normal file
204
danmu/danmaku/kuaishou.proto
Normal file
@ -0,0 +1,204 @@
|
||||
syntax = "proto2";
|
||||
package KuaiShouPack;
|
||||
|
||||
message CSWebHeartbeat {
|
||||
optional uint64 timestamp = 1;
|
||||
}
|
||||
|
||||
message SocketMessage {
|
||||
optional PayloadType payloadType = 1;
|
||||
optional CompressionType compressionType = 2;
|
||||
optional bytes payload = 3;
|
||||
|
||||
enum CompressionType {
|
||||
UNKNOWN = 0;
|
||||
NONE = 1;
|
||||
GZIP = 2;
|
||||
AES = 3;
|
||||
}
|
||||
}
|
||||
|
||||
enum PayloadType {
|
||||
UNKNOWN = 0;
|
||||
CS_HEARTBEAT = 1;
|
||||
CS_ERROR = 3;
|
||||
CS_PING = 4;
|
||||
PS_HOST_INFO = 51;
|
||||
SC_HEARTBEAT_ACK = 101;
|
||||
SC_ECHO = 102;
|
||||
SC_ERROR = 103;
|
||||
SC_PING_ACK = 104;
|
||||
SC_INFO = 105;
|
||||
CS_ENTER_ROOM = 200;
|
||||
CS_USER_PAUSE = 201;
|
||||
CS_USER_EXIT = 202;
|
||||
CS_AUTHOR_PUSH_TRAFFIC_ZERO = 203;
|
||||
CS_HORSE_RACING = 204;
|
||||
CS_RACE_LOSE = 205;
|
||||
CS_VOIP_SIGNAL = 206;
|
||||
SC_ENTER_ROOM_ACK = 300;
|
||||
SC_AUTHOR_PAUSE = 301;
|
||||
SC_AUTHOR_RESUME = 302;
|
||||
SC_AUTHOR_PUSH_TRAFFIC_ZERO = 303;
|
||||
SC_AUTHOR_HEARTBEAT_MISS = 304;
|
||||
SC_PIP_STARTED = 305;
|
||||
SC_PIP_ENDED = 306;
|
||||
SC_HORSE_RACING_ACK = 307;
|
||||
SC_VOIP_SIGNAL = 308;
|
||||
SC_FEED_PUSH = 310;
|
||||
SC_ASSISTANT_STATUS = 311;
|
||||
SC_REFRESH_WALLET = 312;
|
||||
SC_LIVE_CHAT_CALL = 320;
|
||||
SC_LIVE_CHAT_CALL_ACCEPTED = 321;
|
||||
SC_LIVE_CHAT_CALL_REJECTED = 322;
|
||||
SC_LIVE_CHAT_READY = 323;
|
||||
SC_LIVE_CHAT_GUEST_END = 324;
|
||||
SC_LIVE_CHAT_ENDED = 325;
|
||||
SC_RENDERING_MAGIC_FACE_DISABLE = 326;
|
||||
SC_RENDERING_MAGIC_FACE_ENABLE = 327;
|
||||
SC_RED_PACK_FEED = 330;
|
||||
SC_LIVE_WATCHING_LIST = 340;
|
||||
SC_LIVE_QUIZ_QUESTION_ASKED = 350;
|
||||
SC_LIVE_QUIZ_QUESTION_REVIEWED = 351;
|
||||
SC_LIVE_QUIZ_SYNC = 352;
|
||||
SC_LIVE_QUIZ_ENDED = 353;
|
||||
SC_LIVE_QUIZ_WINNERS = 354;
|
||||
SC_SUSPECTED_VIOLATION = 355;
|
||||
SC_SHOP_OPENED = 360;
|
||||
SC_SHOP_CLOSED = 361;
|
||||
SC_GUESS_OPENED = 370;
|
||||
SC_GUESS_CLOSED = 371;
|
||||
SC_PK_INVITATION = 380;
|
||||
SC_PK_STATISTIC = 381;
|
||||
SC_RIDDLE_OPENED = 390;
|
||||
SC_RIDDLE_CLOESED = 391;
|
||||
SC_RIDE_CHANGED = 412;
|
||||
SC_BET_CHANGED = 441;
|
||||
SC_BET_CLOSED = 442;
|
||||
SC_LIVE_SPECIAL_ACCOUNT_CONFIG_STATE = 645;
|
||||
}
|
||||
|
||||
message CSWebEnterRoom {
|
||||
optional string token = 1;
|
||||
optional string liveStreamId = 2;
|
||||
optional uint32 reconnectCount = 3;
|
||||
optional uint32 lastErrorCode = 4;
|
||||
optional string expTag = 5;
|
||||
optional string attach = 6;
|
||||
optional string pageId = 7;
|
||||
}
|
||||
|
||||
message SCWebFeedPush {
|
||||
optional string displayWatchingCount = 1;
|
||||
optional string displayLikeCount = 2;
|
||||
optional uint64 pendingLikeCount = 3;
|
||||
optional uint64 pushInterval = 4;
|
||||
repeated WebCommentFeed commentFeeds = 5;
|
||||
optional string commentCursor = 6;
|
||||
repeated WebComboCommentFeed comboCommentFeed = 7;
|
||||
repeated WebLikeFeed likeFeeds = 8;
|
||||
repeated WebGiftFeed giftFeeds = 9;
|
||||
optional string giftCursor = 10;
|
||||
repeated WebSystemNoticeFeed systemNoticeFeeds = 11;
|
||||
repeated WebShareFeed shareFeeds = 12;
|
||||
|
||||
}
|
||||
|
||||
message WebCommentFeed {
|
||||
optional string id = 1;
|
||||
optional SimpleUserInfo user = 2;
|
||||
optional string content = 3;
|
||||
optional string deviceHash = 4;
|
||||
optional uint64 sortRank = 5;
|
||||
optional string color = 6;
|
||||
optional WebCommentFeedShowType showType = 7;
|
||||
}
|
||||
|
||||
message SimpleUserInfo {
|
||||
optional string principalId = 1;
|
||||
optional string userName = 2;
|
||||
optional string headUrl = 3;
|
||||
}
|
||||
|
||||
enum WebCommentFeedShowType {
|
||||
FEED_SHOW_UNKNOWN = 0;
|
||||
FEED_SHOW_NORMAL = 1;
|
||||
FEED_HIDDEN = 2;
|
||||
}
|
||||
|
||||
message WebComboCommentFeed {
|
||||
optional string id = 1;
|
||||
optional string content = 2;
|
||||
optional uint32 comboCount = 3;
|
||||
}
|
||||
|
||||
message WebLikeFeed {
|
||||
optional string id = 1;
|
||||
optional SimpleUserInfo user = 2;
|
||||
optional uint64 sortRank = 3;
|
||||
optional string deviceHash = 4;
|
||||
}
|
||||
|
||||
message WebGiftFeed {
|
||||
optional string id = 1;
|
||||
optional SimpleUserInfo user = 2;
|
||||
optional uint64 time = 3;
|
||||
optional uint32 giftId = 4;
|
||||
optional uint64 sortRank = 5;
|
||||
optional string mergeKey = 6;
|
||||
optional uint32 batchSize = 7;
|
||||
optional uint32 comboCount = 8;
|
||||
optional uint32 rank = 9;
|
||||
optional uint64 expireDuration = 10;
|
||||
optional uint64 clientTimestamp = 11;
|
||||
optional uint64 slotDisplayDuration = 12;
|
||||
optional uint32 starLevel = 13;
|
||||
optional StyleType styleType = 14;
|
||||
optional WebLiveAssistantType liveAssistantType = 15;
|
||||
optional string deviceHash = 16;
|
||||
optional bool danmakuDisplay = 17;
|
||||
|
||||
enum StyleType {
|
||||
UNKNOWN_STYLE = 0;
|
||||
BATCH_STAR_0 = 1;
|
||||
BATCH_STAR_1 = 2;
|
||||
BATCH_STAR_2 = 3;
|
||||
BATCH_STAR_3 = 4;
|
||||
BATCH_STAR_4 = 5;
|
||||
BATCH_STAR_5 = 6;
|
||||
BATCH_STAR_6 = 7;
|
||||
}
|
||||
}
|
||||
|
||||
enum WebLiveAssistantType {
|
||||
UNKNOWN_ASSISTANT_TYPE = 0;
|
||||
SUPER = 1;
|
||||
JUNIOR = 2;
|
||||
}
|
||||
|
||||
message WebSystemNoticeFeed {
|
||||
optional string id = 1;
|
||||
optional SimpleUserInfo user = 2;
|
||||
optional uint64 time = 3;
|
||||
optional string content = 4;
|
||||
optional uint64 displayDuration = 5;
|
||||
optional uint64 sortRank = 6;
|
||||
optional DisplayType displayType = 7;
|
||||
|
||||
enum DisplayType {
|
||||
UNKNOWN_DISPLAY_TYPE = 0;
|
||||
COMMENT = 1;
|
||||
ALERT = 2;
|
||||
TOAST = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message WebShareFeed {
|
||||
optional string id = 1;
|
||||
optional SimpleUserInfo user = 2;
|
||||
optional uint64 time = 3;
|
||||
optional uint32 thirdPartyPlatform = 4;
|
||||
optional uint64 sortRank = 5;
|
||||
optional WebLiveAssistantType liveAssistantType = 6;
|
||||
optional string deviceHash = 7;
|
||||
}
|
@ -1,14 +1,12 @@
|
||||
# 快手代码来源及思路:https://github.com/py-wuhao/ks_barrage
|
||||
|
||||
from . import kuaishou_pb2 as pb
|
||||
import aiohttp
|
||||
import random
|
||||
import time
|
||||
import json
|
||||
import re
|
||||
import json
|
||||
import time
|
||||
import random
|
||||
|
||||
|
||||
class KuaiShou:
|
||||
heartbeat = b'\x08\x01\x1A\x07\x08' # 发送心跳可固定
|
||||
heartbeatInterval = 20
|
||||
|
||||
@staticmethod
|
||||
@ -18,7 +16,7 @@ class KuaiShou:
|
||||
直播间完整地址
|
||||
Returns:
|
||||
webSocketUrls:wss地址
|
||||
reg_datas:第一次send数据
|
||||
data:第一次send数据
|
||||
liveStreamId:
|
||||
token:
|
||||
page_id:
|
||||
@ -29,27 +27,33 @@ class KuaiShou:
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, '
|
||||
'like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
|
||||
'Cookie': 'did=web_e8436e86a8ec476c801c1d534f56db0c'} # 请求失败则更换cookie中的did字段
|
||||
'Cookie': 'did=web_d563dca728d28b00336877723e0359ed'} # 请求失败则更换cookie中的did字段
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, headers=headers) as resp:
|
||||
wsFeedInfo = re.findall(r'wsFeedInfo":(.*),"liveExist', await resp.text())
|
||||
wsFeedInfo = json.loads(wsFeedInfo[0])
|
||||
liveStreamId = wsFeedInfo['liveStreamId']
|
||||
token = wsFeedInfo['token']
|
||||
webSocketUrls = wsFeedInfo['webSocketUrls'][0]
|
||||
res = await resp.text()
|
||||
|
||||
reg_datas = []
|
||||
part1 = b'\x08\xC8\x01\x1A\xC8\x01\x0A\x98\x01'
|
||||
part2 = token.encode()
|
||||
part3 = b'\x12\x0B'
|
||||
part4 = liveStreamId.encode()
|
||||
part5 = b'\x3A\x1E'
|
||||
page_id = KuaiShou.get_page_id()
|
||||
part6 = page_id.encode()
|
||||
s = part1 + part2 + part3 + part4 + part5 + part6
|
||||
reg_datas.append(s)
|
||||
wsfeedinfo = re.search(r'wsFeedInfo":(.*),"liveExist', res)
|
||||
if wsfeedinfo:
|
||||
wsfeedinfo = json.loads(wsfeedinfo.group(1))
|
||||
else:
|
||||
raise Exception('找不到 wsFeedInfo,可能链接错误或 Cookie 过期')
|
||||
|
||||
return webSocketUrls, reg_datas
|
||||
livestreamid, [websocketurls], token = wsfeedinfo.values()
|
||||
page_id = KuaiShou.get_page_id()
|
||||
|
||||
p, s = pb.SocketMessage(), pb.CSWebEnterRoom()
|
||||
s.liveStreamId, s.pageId, s.token = livestreamid, page_id, token
|
||||
p.payload = s.SerializeToString()
|
||||
p.payloadType = 200
|
||||
reg_data = p.SerializeToString()
|
||||
|
||||
t = pb.CSWebHeartbeat()
|
||||
t.timestamp = int(time.time() * 1000)
|
||||
p.payload = t.SerializeToString()
|
||||
p.payloadType = 1
|
||||
KuaiShou.heartbeat = p.SerializeToString() # 心跳可固定
|
||||
|
||||
return websocketurls, [reg_data]
|
||||
|
||||
@staticmethod
|
||||
def get_page_id():
|
||||
@ -62,324 +66,48 @@ class KuaiShou:
|
||||
return page_id
|
||||
|
||||
@staticmethod
|
||||
def decode_msg(data):
|
||||
msgs = []
|
||||
msg = {}
|
||||
s = MessageDecode(data)
|
||||
c = s.decode()
|
||||
if c.get('payloadType', 0) == 310: # SC_FEED_PUSH = 310 时有弹幕数据
|
||||
m = s.feed_decode(c['payload']) # 弹幕解码方法
|
||||
if m.get('comment'):
|
||||
for user in m.get('comment'):
|
||||
msg['name'] = user.get('user').get('userName').encode('utf-16', 'surrogatepass').decode('utf-16')
|
||||
msg['content'] = user.get('content').encode('utf-16', 'surrogatepass').decode('utf-16')
|
||||
msg['msg_type'] = 'danmaku'
|
||||
msgs.append(msg.copy())
|
||||
return msgs
|
||||
else:
|
||||
msg = {'name': '', 'content': '', 'msg_type': 'other'}
|
||||
else:
|
||||
msg = {'name': '', 'content': '', 'msg_type': 'other'}
|
||||
msgs.append(msg)
|
||||
def decode_msg(message):
|
||||
msgs = [{'name': '', 'content': '', 'msg_type': 'other'}]
|
||||
|
||||
p, s = pb.SocketMessage(), pb.SCWebFeedPush()
|
||||
p.ParseFromString(message)
|
||||
if p.payloadType == 310:
|
||||
s.ParseFromString(p.payload)
|
||||
|
||||
def f(*feeds):
|
||||
gift = {
|
||||
1: '荧光棒', 2: '棒棒糖', 3: '荧光棒', 4: 'PCL加油', 7: '么么哒', 9: '啤酒', 10: '甜甜圈',
|
||||
14: '钻戒', 16: '皇冠', 25: '凤冠', 33: '烟花', 41: '跑车', 56: '稳', 113: '火箭',
|
||||
114: '玫瑰', 132: '绷带', 133: '平底锅', 135: '红爸爸', 136: '蓝爸爸', 137: '铭文碎片',
|
||||
143: '太阳女神', 147: '赞', 149: '血瓶', 150: 'carry全场', 152: '大红灯笼', 156: '穿云箭',
|
||||
159: '膨胀了', 160: '秀你一脸', 161: 'MVP', 163: '加油', 164: '猫粮', 165: '小可爱',
|
||||
169: '男神', 172: '联盟金猪', 173: '有钱花', 193: '蛋糕', 197: '棒棒糖', 198: '瓜',
|
||||
199: '小可爱', 201: '赞', 207: '快手卡', 208: '灵狐姐', 216: 'LPL加油', 218: '烟花',
|
||||
219: '告白气球', 220: '大红灯笼', 221: '怦然心动', 222: '凤冠', 223: '火箭', 224: '跑车',
|
||||
225: '穿云箭', 226: '金话筒', 227: 'IG冲鸭', 228: 'GRF冲鸭', 229: 'FPX冲鸭', 230: 'FNC冲鸭',
|
||||
231: 'SKT冲鸭', 232: 'SPY冲鸭', 233: 'DWG冲鸭', 234: 'G2冲鸭', 235: '爆单', 236: '入团券',
|
||||
237: '陪着你540', 238: '支持牌', 239: '陪着你', 242: '金龙', 243: '豪车幻影', 244: '超级6',
|
||||
245: '水晶', 246: '金莲', 247: '福袋', 248: '铃铛', 249: '巧克力', 250: '感恩的心',
|
||||
254: '武汉加油', 256: '金龙', 257: '财神', 258: '金龙', 259: '天鹅湖', 260: '珍珠',
|
||||
261: '金莲', 262: '招财猫', 263: '铃铛', 264: '巧克力', 266: '幸运魔盒', 267: '吻你',
|
||||
268: '梦幻城堡', 269: '游乐园', 271: '萌宠', 272: '小雪豹', 275: '喜欢你', 276: '三级头',
|
||||
277: '喜欢你', 278: '财神', 279: '锦鲤', 281: '廉颇', 282: '开黑卡', 283: '付费直播门票(不下线)',
|
||||
285: '喜欢你呀', 286: '629', 287: '真爱大炮', 289: '玫瑰花园', 290: '珠峰', 292: '鹿角',
|
||||
296: '666', 297: '超跑车队', 298: '奥利给', 302: '互粉', 303: '冰棒', 304: '龙之谷',
|
||||
306: '浪漫游轮', 307: '壁咚', 308: '壁咚', 309: '鹿角', 310: '么么哒', 311: '私人飞机',
|
||||
312: '巅峰票', 313: '巅峰王者', 315: '莫吉托', 316: '地表最强', 318: '阳光海滩', 319: '12号唱片'
|
||||
}
|
||||
infos = [{'name': '', 'content': '', 'msg_type': 'other'}]
|
||||
for feed in feeds:
|
||||
if feed:
|
||||
for i in feed:
|
||||
name = i.user.userName
|
||||
content = i.content if hasattr(i, 'content') else '送 ' + gift.get(i.giftId, '') \
|
||||
if hasattr(i, 'giftId') else '点亮了 ❤'
|
||||
info = {'name': name, 'content': content, 'msg_type': 'danmaku'}
|
||||
infos.append(info.copy())
|
||||
return infos
|
||||
|
||||
msgs = f(s.commentFeeds, s.giftFeeds, s.likeFeeds)
|
||||
|
||||
return msgs
|
||||
|
||||
|
||||
class MessageDecode:
|
||||
"""
|
||||
返回的数据流解码
|
||||
"""
|
||||
|
||||
def __init__(self, buf):
|
||||
self.buf = buf
|
||||
self.pos = 0
|
||||
self.message = {}
|
||||
|
||||
def __len__(self):
|
||||
return len(self.buf)
|
||||
|
||||
def int_(self):
|
||||
res = 0
|
||||
i = 0
|
||||
while self.buf[self.pos] >= 128:
|
||||
res = res | (127 & self.buf[self.pos]) << 7 * i
|
||||
self.pos += 1
|
||||
i += 1
|
||||
res = res | self.buf[self.pos] << 7 * i
|
||||
self.pos += 1
|
||||
return res
|
||||
|
||||
def decode(self):
|
||||
"""
|
||||
服务器返回数据第一次解码
|
||||
Return:m
|
||||
payloadType:
|
||||
101: "SC_HEARTBEAT_ACK",
|
||||
103: "SC_ERROR",
|
||||
105: "SC_INFO",
|
||||
300: "SC_ENTER_ROOM_ACK",
|
||||
310: "SC_FEED_PUSH", # 310是弹幕信息
|
||||
330: "SC_RED_PACK_FEED",
|
||||
340: "SC_LIVE_WATCHING_LIST",
|
||||
370: "SC_GUESS_OPENED",
|
||||
371: "SC_GUESS_CLOSED",
|
||||
412: "SC_RIDE_CHANGED",
|
||||
441: "SC_BET_CHANGED",
|
||||
442: "SC_BET_CLOSED",
|
||||
645: "SC_LIVE_SPECIAL_ACCOUNT_CONFIG_STATE"
|
||||
"""
|
||||
m = {}
|
||||
self.pos = 0
|
||||
length = len(self)
|
||||
while self.pos < length:
|
||||
t = self.int_()
|
||||
tt = t >> 3
|
||||
if tt == 1:
|
||||
m['payloadType'] = self.int_()
|
||||
elif tt == 2:
|
||||
m['compressionType'] = self.int_()
|
||||
elif tt == 3:
|
||||
m['payload'] = self.bytes()
|
||||
else:
|
||||
self.skipType(t & 7)
|
||||
return m
|
||||
|
||||
def skipType(self, e):
|
||||
if e == 0:
|
||||
self.skip()
|
||||
elif e == 1:
|
||||
self.skip(8)
|
||||
elif e == 2:
|
||||
self.skip(self.int_())
|
||||
elif e == 3:
|
||||
while True:
|
||||
e = 7 & self.int_()
|
||||
if 4 != e:
|
||||
self.skipType(e)
|
||||
elif e == 5:
|
||||
self.skip(4)
|
||||
else:
|
||||
raise Exception('跳过类型错误')
|
||||
|
||||
def bytes(self):
|
||||
e = self.int_()
|
||||
if e + self.pos > len(self.buf):
|
||||
raise Exception('index out of range')
|
||||
res = self.buf[self.pos: (e + self.pos)]
|
||||
self.pos += e
|
||||
return res
|
||||
|
||||
def skip(self, e=None):
|
||||
"""跳过多少字节"""
|
||||
if e is None:
|
||||
while self.pos < len(self.buf):
|
||||
if 128 & self.buf[self.pos] == 0:
|
||||
self.pos += 1
|
||||
return
|
||||
self.pos += 1
|
||||
return
|
||||
self.pos += e
|
||||
if self.pos >= len(self.buf):
|
||||
self.pos -= 1
|
||||
|
||||
def feed_decode(self, payload):
|
||||
"""
|
||||
payload解码,即还原JS中的function SCWebFeedPush$decode(r, l)
|
||||
Args:
|
||||
decode函数返回的paylod
|
||||
Returns:
|
||||
m解码后的数据
|
||||
"""
|
||||
self.pos = 0
|
||||
self.buf = payload
|
||||
m = {}
|
||||
length = len(self.buf)
|
||||
while self.pos < length:
|
||||
t = self.int_()
|
||||
tt = t >> 3
|
||||
if tt == 1:
|
||||
m['displayWatchingCount'] = self.string()
|
||||
elif tt == 2:
|
||||
m['displayLikeCount'] = self.string()
|
||||
elif tt == 3:
|
||||
m['pendingLikeCount'] = self.int_()
|
||||
elif tt == 4:
|
||||
m['pushInterval'] = self.int_()
|
||||
elif tt == 5:
|
||||
if not m.get('comment'):
|
||||
m['comment'] = []
|
||||
m['comment'].append(self.comment_decode(self.buf, self.int_()))
|
||||
elif tt == 6:
|
||||
m['commentCursor'] = self.string()
|
||||
elif tt == 7:
|
||||
if not m.get('comboComment'):
|
||||
m['comboComment'] = []
|
||||
m['comboComment'].append(self.comboComment_decode(self.buf, self.int_()))
|
||||
elif tt == 8:
|
||||
if not m.get('like'):
|
||||
m['like'] = []
|
||||
m['like'].append(self.web_like_feed_decode(self.buf, self.int_()))
|
||||
elif tt == 9: # 礼物
|
||||
if not m.get('gift'):
|
||||
m['gift'] = []
|
||||
m['gift'].append(self.gift_decode(self.buf, self.int_()))
|
||||
elif tt == 10:
|
||||
m['giftCursor'] = self.string()
|
||||
elif tt == 11:
|
||||
if not m.get('systemNotice'):
|
||||
m['systemNotice'] = []
|
||||
m['systemNotice'].append(self.systemNotice_decode(self.buf, self.int_()))
|
||||
elif tt == 12:
|
||||
if not m.get('share'):
|
||||
m['share'] = []
|
||||
m['share'].append(self.share_decode(self.buf, self.int_()))
|
||||
else:
|
||||
self.skipType(t & 7)
|
||||
return m
|
||||
|
||||
def comment_decode(self, r, l):
|
||||
c = self.pos + l
|
||||
m = {}
|
||||
while self.pos < c:
|
||||
t = self.int_()
|
||||
tt = t >> 3
|
||||
if tt == 1:
|
||||
m['id'] = self.string()
|
||||
elif tt == 2:
|
||||
m['user'] = self.user_info_decode(self.buf, self.int_())
|
||||
elif tt == 3:
|
||||
m['content'] = self.string()
|
||||
elif tt == 4:
|
||||
m['deviceHash'] = self.string()
|
||||
elif tt == 5:
|
||||
m['sortRank'] = self.int_()
|
||||
elif tt == 6:
|
||||
m['color'] = self.string()
|
||||
elif tt == 7:
|
||||
m['showType'] = self.int_()
|
||||
else:
|
||||
self.skipType(t & 7)
|
||||
return m
|
||||
|
||||
def comboComment_decode(self, r, l):
|
||||
pass
|
||||
|
||||
def systemNotice_decode(self, r, l):
|
||||
pass
|
||||
|
||||
def share_decode(self, r, l):
|
||||
pass
|
||||
|
||||
def user_info_decode(self, r, l):
|
||||
c = self.pos + l
|
||||
m = {}
|
||||
while self.pos < c:
|
||||
t = self.int_()
|
||||
tt = t >> 3
|
||||
if tt == 1:
|
||||
m['principalId'] = self.string()
|
||||
elif tt == 2:
|
||||
m['userName'] = self.string()
|
||||
elif tt == 3:
|
||||
m['headUrl'] = self.string()
|
||||
else:
|
||||
self.skipType(t & 7)
|
||||
return m
|
||||
|
||||
def web_like_feed_decode(self, r, l):
|
||||
c = self.pos + l
|
||||
m = {}
|
||||
while self.pos < c:
|
||||
t = self.int_()
|
||||
tt = t >> 3
|
||||
if tt == 1:
|
||||
m['id'] = self.string()
|
||||
elif tt == 2:
|
||||
m['user'] = self.user_info_decode(self.buf, self.int_())
|
||||
elif tt == 3:
|
||||
m['sortRank'] = self.int_()
|
||||
elif tt == 4:
|
||||
m['deviceHash'] = self.string()
|
||||
else:
|
||||
self.skipType(t & 7)
|
||||
return m
|
||||
|
||||
def gift_decode(self, r, l):
|
||||
c = self.pos + l
|
||||
m = {}
|
||||
while self.pos < c:
|
||||
t = self.int_()
|
||||
tt = t >> 3
|
||||
if tt == 1:
|
||||
m['id'] = self.string()
|
||||
elif tt == 2:
|
||||
m['user'] = self.user_info_decode(self.buf, self.int_())
|
||||
elif tt == 3:
|
||||
m['time'] = self.int_()
|
||||
elif tt == 4:
|
||||
m['giftId'] = self.int_()
|
||||
elif tt == 5:
|
||||
m['sortRank'] = self.int_()
|
||||
elif tt == 6:
|
||||
m['mergeKey'] = self.string()
|
||||
elif tt == 7:
|
||||
m['batchSize'] = self.int_()
|
||||
elif tt == 8:
|
||||
m['comboCount'] = self.int_()
|
||||
elif tt == 9:
|
||||
m['rank'] = self.int_()
|
||||
elif tt == 10:
|
||||
m['expireDuration'] = self.int_()
|
||||
elif tt == 11:
|
||||
m['clientTimestamp'] = self.int_()
|
||||
elif tt == 12:
|
||||
m['slotDisplayDuration'] = self.int_()
|
||||
elif tt == 13:
|
||||
m['starLevel'] = self.int_()
|
||||
elif tt == 14:
|
||||
m['styleType'] = self.int_()
|
||||
elif tt == 15:
|
||||
m['liveAssistantType'] = self.int_()
|
||||
elif tt == 16:
|
||||
m['deviceHash'] = self.string()
|
||||
elif tt == 17:
|
||||
m['danmakuDisplay'] = self.int_()
|
||||
else:
|
||||
self.skipType(t & 7)
|
||||
return m
|
||||
|
||||
def string(self):
|
||||
e = self.bytes()
|
||||
n = len(e)
|
||||
if n < 1:
|
||||
return ""
|
||||
s = []
|
||||
t = 0
|
||||
while t < n:
|
||||
r = e[t]
|
||||
t += 1
|
||||
if r < 128:
|
||||
s.append(r)
|
||||
elif 191 < r < 224:
|
||||
s.append((31 & r) << 6 | 63 & e[t])
|
||||
t += 1
|
||||
elif 239 < r < 365:
|
||||
x = (7 & r) << 18 | (63 & e[t]) << 12
|
||||
t += 1
|
||||
y = (63 & e[t]) << 6
|
||||
t += 1
|
||||
z = 63 & e[t]
|
||||
t += 1
|
||||
r = (x | y | z) - 65536
|
||||
s.append(55296 + (r >> 10))
|
||||
s.append(56320 + (1023 & r))
|
||||
else:
|
||||
x = (15 & r) << 12
|
||||
y = (63 & e[t]) << 6
|
||||
t += 1
|
||||
z = 63 & e[t]
|
||||
t += 1
|
||||
s.append(x | y | z)
|
||||
string = ''
|
||||
for w in s:
|
||||
string += chr(w)
|
||||
return string
|
||||
|
1469
danmu/danmaku/kuaishou_pb2.py
Normal file
1469
danmu/danmaku/kuaishou_pb2.py
Normal file
File diff suppressed because one or more lines are too long
@ -23,19 +23,21 @@ async def main(url):
|
||||
a = input('请输入直播间地址:\n')
|
||||
asyncio.run(main(a))
|
||||
|
||||
# 虎牙:https://www.huya.com/11352915
|
||||
# 斗鱼:https://www.douyu.com/85894
|
||||
# B站:https://live.bilibili.com/70155
|
||||
# 快手:https://live.kuaishou.com/u/jjworld126
|
||||
# 火猫:
|
||||
# 虎牙直播:https://www.huya.com/11352915
|
||||
# 斗鱼直播:https://www.douyu.com/85894
|
||||
# B站直播:https://live.bilibili.com/70155
|
||||
# 快手直播:https://live.kuaishou.com/u/jjworld126
|
||||
# 火猫直播:
|
||||
# 企鹅电竞:https://egame.qq.com/383204988
|
||||
# 花椒直播:https://www.huajiao.com/l/303344861?qd=hu
|
||||
# 映客直播:https://www.inke.cn/liveroom/index.html?uid=87493223&id=1593906372018299
|
||||
# CC直播:https://cc.163.com/363936598/
|
||||
# 酷狗直播:https://fanxing.kugou.com/1676290
|
||||
# 战旗直播
|
||||
# 战旗直播:
|
||||
# 龙珠直播:http://star.longzhu.com/wsde135864219
|
||||
# PPS奇秀直播:https://x.pps.tv/room/208337
|
||||
# 搜狐千帆直播:https://qf.56.com/520208a
|
||||
# 来疯直播:https://v.laifeng.com/656428
|
||||
# LOOK直播:https://look.163.com/live?id=196257915
|
||||
# AcFun直播:https://live.acfun.cn/live/23682490
|
||||
# 艺气山直播:http://www.173.com/96
|
||||
|
40
kuaishou.py
40
kuaishou.py
@ -1,25 +1,29 @@
|
||||
# 获取快手直播的真实流媒体地址,默认输出最高画质
|
||||
|
||||
import requests
|
||||
import json
|
||||
import re
|
||||
|
||||
|
||||
def get_real_url(rid):
|
||||
try:
|
||||
room_url = 'https://m.gifshow.com/fw/live/' + str(rid)
|
||||
headers = {
|
||||
'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
|
||||
'cookie': 'did=web_'}
|
||||
response = requests.get(url=room_url, headers=headers).text
|
||||
m3u8_url = re.findall(r'type="application/x-mpegURL" src="([\s\S]*?)_sd1000(tp)?(/index)?.m3u8', response)[0]
|
||||
real_url = [m3u8_url[0] + i for i in ['.flv', '.m3u8']]
|
||||
except:
|
||||
real_url = '该直播间不存在或未开播'
|
||||
return real_url
|
||||
import requests
|
||||
|
||||
|
||||
rid = input('请输入快手直播间ID:\n')
|
||||
real_url = get_real_url(rid)
|
||||
print('该直播源地址为:')
|
||||
print(real_url)
|
||||
def kuaishou(rid):
|
||||
headers = {
|
||||
'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 '
|
||||
'(KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
|
||||
'cookie': 'did=web_'}
|
||||
with requests.Session() as s:
|
||||
res = s.get('https://m.gifshow.com/fw/live/{}'.format(rid), headers=headers)
|
||||
livestream = re.search(r'liveStream":(.*),"obfuseData', res.text)
|
||||
if livestream:
|
||||
livestream = json.loads(livestream.group(1))
|
||||
*_, hlsplayurls = livestream['multiResolutionHlsPlayUrls']
|
||||
urls, = hlsplayurls['urls']
|
||||
url = urls['url']
|
||||
return url
|
||||
else:
|
||||
raise Exception('直播间不存在或未开播')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
r = input('输入快手直播房间号:\n') # 例:jjworld126
|
||||
print(kuaishou(r))
|
||||
|
Loading…
x
Reference in New Issue
Block a user