1
0
mirror of https://github.com/wbt5/real-url.git synced 2025-08-04 01:12:48 +08:00

Compare commits

...

9 Commits

Author SHA1 Message Date
wbt5
c4ab4dfb71 更新快手直播 2020-08-18 21:54:21 +08:00
wbt5
40eecd5bcf 新增2个平台 2020-08-18 21:53:10 +08:00
wbt5
47e8538164 新增2个平台 2020-08-18 21:52:56 +08:00
wbt5
ed4c68a1ec 每周一更 2020-08-18 21:52:17 +08:00
wbt5
0c7b9b7bcd 更新快手直播弹幕 2020-08-18 21:50:19 +08:00
wbt5
3cfbdcd5d0 新增acfun直播弹幕 2020-08-18 21:49:12 +08:00
wbt5
c75c75713f 新增艺气山直播弹幕 2020-08-18 21:48:16 +08:00
wbt5
27fa079145 新增艺气山直播弹幕 2020-08-18 21:48:04 +08:00
wbt5
313f33bb5c 新增艺气山直播弹幕 2020-08-18 21:47:53 +08:00
13 changed files with 7683 additions and 380 deletions

View File

@ -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
View 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
View 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

File diff suppressed because it is too large Load Diff

View File

@ -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
View 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
View 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=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

3650
danmu/danmaku/acfun_pb2.py Normal file

File diff suppressed because one or more lines are too long

View 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;
}

View File

@ -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

File diff suppressed because one or more lines are too long

View File

@ -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

View File

@ -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))