额外更新

This commit is contained in:
Jerry Yan 2021-01-30 19:54:40 +08:00
parent 81dad07fd5
commit 734a7204f8
9 changed files with 502 additions and 25 deletions

View File

@ -321,6 +321,9 @@ def loginBilibili(force=False):
class downloader(XiGuaLiveApi): class downloader(XiGuaLiveApi):
playlist = None playlist = None
def _checkUsernameIsMatched(self, compare=None):
return True
def updRoomInfo(self, force=False): def updRoomInfo(self, force=False):
global broadcaster global broadcaster
_prev_status = self.isLive _prev_status = self.isLive

40
Struct/Chat.py Normal file
View File

@ -0,0 +1,40 @@
from .User import User
from .Lottery import Lottery
from XiguaMessage_pb2 import ChatMessage
class Chat:
content = ""
user = None
filterString = ["", ]
isFiltered = False
def __init__(self, json=None, lottery: Lottery = None):
if lottery:
self.filterString.append(lottery.content)
if json:
if type(json) == bytes:
self.parsePb(json)
else:
self.parse(json)
def parsePb(self, raw):
_message = ChatMessage()
_message.ParseFromString(raw)
self.user = User(_message.user)
self.content = _message.content
if self.content in self.filterString:
self.isFiltered = True
def parse(self, json):
self.user = User(json)
if "extra" in json:
if "content" in json["extra"]:
self.content = json["extra"]['content']
if self.content in self.filterString:
self.isFiltered = True
def __str__(self):
return "{} : {}".format(self.user, self.content)
def __unicode__(self):
return self.__str__()

0
Struct/Digg.py Normal file
View File

72
Struct/Gift.py Normal file
View File

@ -0,0 +1,72 @@
import requests
from .User import User
from XiguaMessage_pb2 import GiftMessage
class Gift:
giftList = {}
def __init__(self, json=None):
self.ID = 0
self.count = 0
self.user = None
self.isFinished = False
self.backupName = None
if json:
if type(json) == bytes:
self.parsePb(json)
else:
self.parse(json)
def parsePb(self, raw):
_message = GiftMessage()
_message.ParseFromString(raw)
self.user = User(_message.user)
self.ID = _message.giftId
self.count = _message.repeated
self.isFinished = _message.isFinished
self.backupName = _message.commonInfo.displayText.params.gifts.gift.name
def parse(self, json):
self.user = User(json)
if "extra" in json and json["extra"] is not None:
if "present_info" in json["extra"] and json["extra"]['present_info'] is not None:
self.ID = int(json["extra"]['present_info']['id'])
self.count = json["extra"]['present_info']['repeat_count']
elif "present_end_info" in json["extra"] and json["extra"]['present_end_info'] is not None:
self.ID = int(json["extra"]['present_end_info']['id'])
self.count = json["extra"]['present_end_info']['count']
def isAnimate(self):
if self.ID != 0 and self.ID in self.giftList:
if 'combo' in self.giftList[self.ID]:
return self.giftList[self.ID]["combo"] == False
elif 'meta' in self.giftList[self.ID] and 'combo' in self.giftList[self.ID]['meta']:
return self.giftList[self.ID]['meta']["combo"] == False
elif 'type' in self.giftList[self.ID]:
return self.giftList[self.ID]["type"] == 2
return False
def _getGiftName(self):
if self.ID in self.giftList:
return self.giftList[self.ID]["name"]
elif self.backupName is not None:
return self.backupName
else:
return "未知礼物[{}]".format(self.ID)
def __str__(self):
return "{user} 送出的 {count}{name}".format(user=self.user, count=self.count, name=self._getGiftName())
def __unicode__(self):
return self.__str__()
def __repr__(self):
return "西瓜礼物【{}(ID:{})】".format(self._getGiftName(), self.ID)
@classmethod
def addGift(cls, _gift):
if 'id' not in _gift:
return
_id = int(_gift["id"])
cls.giftList[_id] = _gift

71
Struct/Lottery.py Normal file
View File

@ -0,0 +1,71 @@
# coding=utf-8
import requests
import time
from .LuckyUser import LuckyUser
class Lottery:
ID = 0
isActive = False
content = ""
isFinished = False
luckyUsers = []
joinedUserCount = 0
prizeName = ""
finish = 0
def __init__(self, json=None):
if json:
self.parse(json)
def parse(self, json):
if "lottery_info" in json and json["lottery_info"] is not None:
self.isActive = int(json["lottery_info"]["status"]) > 0
self.ID = json["lottery_info"]["lottery_id"]
for i in json["lottery_info"]['conditions']:
if i['type'] != 3:
continue
self.content = i["content"]
self.joinedUserCount = int(json["lottery_info"]["candidate_num"])
self.prizeName = json["lottery_info"]["prize_info"]["name"]
_delta = int(json["lottery_info"]["draw_time"]) - int(json["lottery_info"]["current_time"])
self.finish = time.time()+_delta+1
elif "extra" in json and json["extra"] is not None:
if "lottery_info" in json["extra"] and json["extra"]["lottery_info"] is not None:
return self.parse(json["extra"])
def update(self):
if self.isActive:
if not self.isFinished and self.finish > time.time():
self.checkFinished()
return True
return False
def checkFinished(self):
p = requests.get("https://i.snssdk.com/videolive/lottery/check_user_right?lottery_id={}"
"&version_code=730&device_platform=android".format(
self.ID
))
d = p.json()
if d["base_resp"]["status_code"] != 0:
self.isActive = False
self.isFinished = False
return
self.isActive = int(d["lottery_info"]["status"]) > 0
self.isFinished = int(d["lottery_info"]["status"]) == 2
self.joinedUserCount = int(d["lottery_info"]["candidate_num"])
if self.isFinished:
self.luckyUsers = [ LuckyUser(i) for i in d["lottery_info"]["lucky_users"] ]
def __str__(self):
if self.isFinished:
ret = "恭喜以下中奖用户:\n"
for i in self.luckyUsers:
ret += "> {} {}\n".format(i,self.prizeName)
ret += "> 参与人数:{}".format(self.joinedUserCount)
return ret
elif self.isActive:
return "正在抽奖中。。。\n" \
"> 参与人数:{}".format(self.joinedUserCount)
else:
return "抽奖已失效"

19
Struct/LuckyUser.py Normal file
View File

@ -0,0 +1,19 @@
from .User import User
class LuckyUser:
user = None
count = 0
def __init__(self, json=None):
if json:
self.parse(json)
def parse(self, json):
self.user = User()
self.user.ID = json['user_id']
self.user.name = json['user_name']
self.count = int(json["grant_count"])
def __str__(self):
return "用户 {} 获得了 {}".format(self.user,self.count)

36
Struct/MemberMsg.py Normal file
View File

@ -0,0 +1,36 @@
from .User import User
class MemberMsg:
type = 0
content = ""
user = None
def __init__(self, json=None):
if json:
self.parse(json)
def parse(self, json):
self.user = User(json)
if "extra" in json:
if "action" in json["extra"]:
self.type = json["extra"]['action']
elif "content" in json["extra"]:
self.content = json["extra"]['content']
def __str__(self):
if self.type == 3:
return "{} 被禁言了".format(self.user)
elif self.type == 4:
return "{} 被取消禁言了".format(self.user)
elif self.type == 5:
return "{} 被任命为房管".format(self.user)
elif self.type == 1:
return "{} 进入了房间".format(self.user)
else:
if self.content == "":
return "未知消息{} 关于用户 {}".format(self.type, self.user)
return self.content.format(self.user)
def __unicode__(self):
return self.__str__()

View File

@ -1,15 +1,33 @@
class User: from XiguaUser_pb2 import User as UserPb
ID = 0
name = ""
brand = ""
level = 0
type = 0
block = False
mute = False
class User:
def __init__(self, json=None): def __init__(self, json=None):
self.ID = 0
self.name = ""
self.brand = ""
self.level = 0
self.type = 0
self.block = False
self.mute = False
if json: if json:
self.parse(json) if type(json) == bytes:
self.parsePb(json)
elif type(json) == UserPb:
self.parseUserPb(json)
else:
self.parse(json)
def parseUserPb(self, _user):
self.ID = _user.id
self.name = _user.nickname
self.brand = _user.fansClub.fansClub.title
self.level = _user.fansClub.fansClub.level
def parsePb(self, raw):
_user = UserPb()
_user.ParseFromString(raw)
self.parseUserPb(_user)
def parse(self, json): def parse(self, json):
if "extra" in json: if "extra" in json:

250
api.py
View File

@ -1,9 +1,15 @@
# coding=utf-8 # coding=utf-8
import sys
from Struct.MemberMsg import MemberMsg
from Struct.User import User from Struct.User import User
from Struct.Gift import Gift
from Struct.Chat import Chat
import requests import requests
import time import time
from datetime import datetime, timedelta from datetime import datetime, timedelta
from Xigua_pb2 import XiguaLive
from XiguaMessage_pb2 import FansClubMessage, SocialMessage
DEBUG = False DEBUG = False
# 自己抓的自己设备的参数,建议开发者自己抓一个长期使用 # 自己抓的自己设备的参数,建议开发者自己抓一个长期使用
@ -13,7 +19,7 @@ CUSTOM_INFO = {
'device_id': "55714661189", 'device_id': "55714661189",
'cdid': "ed4295e8-5d9a-4cb9-b2a2-04009a3baa2d", 'cdid': "ed4295e8-5d9a-4cb9-b2a2-04009a3baa2d",
'openudid': "70d6668d41512c39", 'openudid': "70d6668d41512c39",
# 'aid': "32", # 是一个不变的值 # 'aid': "32", # 是一个不变的值
'channel': "xiaomi", 'channel': "xiaomi",
'device_brand': "Xiaomi", 'device_brand': "Xiaomi",
'device_type': "MI+8+SE", 'device_type': "MI+8+SE",
@ -23,16 +29,14 @@ CUSTOM_INFO = {
} }
VERSION_INFO = { VERSION_INFO = {
'app_name': "video_article", 'app_name': "video_article",
'version_code': "926", 'version_code': "942",
'version_code_full': "92609", 'version_code_full': "94214",
'version_name': "9.2.6", 'version_name': "9.4.2",
'ab_version': "941090,785218,668858,1046292,1073579,830454,956074,929436,797199,1135476,1179370,994679,959010," 'ab_version': "668852,668853,668858,668851,668859,668856,668855,2358970,"
"900042,1113833,668854,1193963,901277,1043330,1038721,994822,1002058,1230687,1189797,1143356,1143441," "668854,2393607,1477978,994679,2408463,2412359",
"1143501,1143698,1143713,1371009,1243997,1392586,1395695,1395486,1398858,668852,668856,668853," 'manifest_version_code': "542",
"1186421,668851,668859,999124,668855,1039075",
'manifest_version_code': "518",
'tma_jssdk_version': "1830001", 'tma_jssdk_version': "1830001",
# 'oaid': "a625f466e0975d42", # 一个定值,几个版本换设备都没变过 'oaid': "693ea85657ef38ca",
} }
COMMON_GET_PARAM = ( COMMON_GET_PARAM = (
"&iid={iid}&device_id={device_id}&channel={channel}&aid=32&app_name={app_name}&version_code={version_code}&" "&iid={iid}&device_id={device_id}&channel={channel}&aid=32&app_name={app_name}&version_code={version_code}&"
@ -40,7 +44,8 @@ COMMON_GET_PARAM = (
"device_brand={device_brand}&language=zh&os_api={os_api}&os_version={os_version}&openudid={openudid}&fp=a_fake_fp&" "device_brand={device_brand}&language=zh&os_api={os_api}&os_version={os_version}&openudid={openudid}&fp=a_fake_fp&"
"manifest_version_code={manifest_version_code}&update_version_code={version_code_full}&_rticket={{TIMESTAMP:.0f}}&" "manifest_version_code={manifest_version_code}&update_version_code={version_code_full}&_rticket={{TIMESTAMP:.0f}}&"
"_rticket={{TIMESTAMP:.0f}}&cdid_ts={{TIMESTAMP:.0f}}&tma_jssdk_version={tma_jssdk_version}&" "_rticket={{TIMESTAMP:.0f}}&cdid_ts={{TIMESTAMP:.0f}}&tma_jssdk_version={tma_jssdk_version}&"
"rom_version={rom_version}&cdid={cdid}&oaid=a625f466e0975d42").format_map({**VERSION_INFO, **CUSTOM_INFO}) "rom_version={rom_version}&cdid={cdid}&oaid={oaid}").format_map({**VERSION_INFO, **CUSTOM_INFO})
WEBCAST_GET_PARAMS = "webcast_sdk_version=1350&webcast_language=zh&webcast_locale=zh_CN"
SEARCH_USER_API = ( SEARCH_USER_API = (
"https://search-hl.ixigua.com/video/app/search/search_content/?format=json" "https://search-hl.ixigua.com/video/app/search/search_content/?format=json"
"&fss=search_subtab_switch&target_channel=video_search&keyword_type=search_subtab_switch&offset=0&count=10" "&fss=search_subtab_switch&target_channel=video_search&keyword_type=search_subtab_switch&offset=0&count=10"
@ -49,15 +54,18 @@ SEARCH_USER_API = (
'&ab_param={{"is_show_filter_feature": 1, "is_hit_new_ui": 1}}' '&ab_param={{"is_show_filter_feature": 1, "is_hit_new_ui": 1}}'
"&search_start_time={TIMESTAMP:.0f}&from=live&en_qc=1&pd=xigua_live&ssmix=a{COMMON}&keyword={keyword}") "&search_start_time={TIMESTAMP:.0f}&from=live&en_qc=1&pd=xigua_live&ssmix=a{COMMON}&keyword={keyword}")
USER_INFO_API = "https://api100-quic-c-hl.ixigua.com/video/app/user/home/v7/?to_user_id={userId}{COMMON}" USER_INFO_API = "https://api100-quic-c-hl.ixigua.com/video/app/user/home/v7/?to_user_id={userId}{COMMON}"
ROOM_INFO_API = ("https://webcast3-normal-c-hl.ixigua.com/webcast/room/enter/?room_id={roomId}&webcast_sdk_version=1350" ROOM_INFO_API = "https://webcast3-normal-c-hl.ixigua.com/webcast/room/enter/?room_id={roomId}&pack_level=4{COMMON}"
"&webcast_language=zh&webcast_locale=zh_CN&pack_level=4{COMMON}") DANMAKU_GET_API = "https://webcast3-normal-c-hl.ixigua.com/webcast/im/fetch/?{WEBCAST}{COMMON}"
GIFT_DATA_API = ("https://webcast3-normal-c-hl.ixigua.com/webcast/gift/list/?room_id={roomId}&to_room_id={roomId}&"
"gift_scene=1&fetch_giftlist_from=2&current_network_quality_info={{}}&"
"{WEBCAST}{COMMON}")
COMMON_HEADERS = { COMMON_HEADERS = {
"sdk-version": '2', "sdk-version": '2',
"passport-sdk-version": "19", "passport-sdk-version": "21",
"X-SS-DP": "32", "X-SS-DP": "32",
"x-vc-bdturing-sdk-version": "2.0.1",
"User-Agent": "Dalvik/2.1.0 (Linux; U; Android 10) VideoArticle/9.2.6 cronet/TTNetVersion:828f6f3c 2020-09-06 " "User-Agent": "Dalvik/2.1.0 (Linux; U; Android 10) VideoArticle/9.2.6 cronet/TTNetVersion:828f6f3c 2020-09-06 "
"QuicVersion:7aee791b 2020-06-05", "QuicVersion:7aee791b 2020-06-05",
# 最好别加brrequests库好像自带没法解析
"Accept-Encoding": "gzip, deflate" "Accept-Encoding": "gzip, deflate"
} }
@ -87,6 +95,7 @@ class XiGuaLiveApi:
self.isLive = False self.isLive = False
self._rawRoomInfo = {} self._rawRoomInfo = {}
self.roomID = 0 self.roomID = 0
self.roomPopularity = 0
self.s = requests.session() self.s = requests.session()
self.s.headers.update(COMMON_HEADERS) self.s.headers.update(COMMON_HEADERS)
self._updRoomAt = datetime.fromtimestamp(0) self._updRoomAt = datetime.fromtimestamp(0)
@ -94,6 +103,19 @@ class XiGuaLiveApi:
self._ext = "" self._ext = ""
self._cursor = "0" self._cursor = "0"
def _updateRoomPopularity(self, _data):
"""
更新房间人气的方法
Update Room Popularity
:param _data: Received Message
"""
if "extra" in _data:
if "member_count" in _data["extra"] and _data["extra"]["member_count"] > 0:
self.roomPopularity = _data["extra"]["member_count"]
if "data" in _data:
if "popularity" in _data["data"]:
self.roomPopularity = _data["data"]["popularity"]
def getJson(self, url, **kwargs): def getJson(self, url, **kwargs):
if "timeout" not in kwargs: if "timeout" not in kwargs:
kwargs["timeout"] = 10 kwargs["timeout"] = 10
@ -152,13 +174,93 @@ class XiGuaLiveApi:
if DEBUG: if DEBUG:
print(*args) print(*args)
def onPresent(self, gift: Gift):
"""
礼物连击中的消息
Message On Sending Presents
:param gift: Struct of Gift Message
"""
print("礼物连击 :", gift)
def onPresentEnd(self, gift: Gift):
"""
礼物送完了的提示信息
Message On Finished Send Present
:param gift: Struct of Gift Message
"""
print("感谢", gift)
def onAd(self, i):
"""
全局广播
All Channel Broadcasting Message( Just An Ad )
:param i: JSON DATA if you wanna using it
"""
# print(i)
pass
def onChat(self, chat: Chat):
"""
聊天信息
On Chatting
:param chat: Struct of Chat Message
"""
if not chat.isFiltered:
print(chat)
def onEnter(self, msg: MemberMsg):
"""
进入房间消息
On Entering Room
:param msg: Struct of Member Message
"""
print("提示 :", msg)
def onSubscribe(self, user: User):
"""
关注主播时的消息
On Subscribe
:param user: Struct of User Message
"""
print("消息 :", user, "关注了主播")
def onJoin(self, user: User):
"""
加入粉丝团消息
:param user:
"""
print("欢迎", user, "加入了粉丝团")
def onMessage(self, msg: str):
"""
系统消息
:param msg:
"""
print("消息 :", msg)
def onLike(self, user: User):
"""
点击喜欢的消息
On Like
:param user:
"""
print("用户", user, "点了喜欢")
def onLeave(self, json: any):
"""
下播消息
On Liver Leave
:param json:
"""
print("消息 :", "主播离开了")
self.updRoomInfo()
def _checkUsernameIsMatched(self, compare=None): def _checkUsernameIsMatched(self, compare=None):
""" """
验证主播名字是自己想要的那个 验证主播名字是自己想要的那个
Check name matched Check name matched
:return: bool: 是否匹配 :return: bool: 是否匹配
""" """
return True
if compare is None: if compare is None:
compare = self.broadcaster compare = self.broadcaster
if self.name is None or compare is None: if self.name is None or compare is None:
@ -258,6 +360,7 @@ class XiGuaLiveApi:
self._rawRoomInfo = d["data"] self._rawRoomInfo = d["data"]
self.isLive = d["data"]["status"] == 2 self.isLive = d["data"]["status"] == 2
self._updRoomAt = datetime.now() self._updRoomAt = datetime.now()
self._updateRoomPopularity(d)
return self.isLive return self.isLive
def updRoomInfo(self, force=False): def updRoomInfo(self, force=False):
@ -274,6 +377,121 @@ class XiGuaLiveApi:
else: else:
return self._getRoomInfo(force) return self._getRoomInfo(force)
def updGiftInfo(self):
self.updRoomInfo()
_formatData = {"TIMESTAMP": time.time() * 1000, "roomId": self.roomID}
_COMMON = COMMON_GET_PARAM.format_map(_formatData)
_formatData['COMMON'] = _COMMON
_formatData['WEBCAST'] = WEBCAST_GET_PARAMS
_url = GIFT_DATA_API.format_map(_formatData)
d = self.getJson(_url)
if d is None or d["status_code"] != 0:
return "异常"
elif 'pages' in d["data"]:
for _page in d["data"]['pages']:
if 'gifts' in _page:
for _gift in _page['gifts']:
Gift.addGift(_gift)
return len(Gift.giftList)
def getDanmaku(self):
"""
获取弹幕
"""
self.updRoomInfo()
_formatData = {"TIMESTAMP": time.time() * 1000, "roomId": self.roomID}
_COMMON = COMMON_GET_PARAM.format_map(_formatData)
_formatData['COMMON'] = _COMMON
_formatData['WEBCAST'] = WEBCAST_GET_PARAMS
_url = DANMAKU_GET_API.format_map(_formatData)
p = self.s.post(_url, data="room_id={roomId}&fetch_rule=0&cursor={cursor}&"
"resp_content_type=protobuf&live_id=3&user_id=0&identity=audience&"
"last_rtt=85&internal_ext={ext}"
.format_map({"roomId":self.roomID, "cursor": self._cursor, "ext": self._ext}),
headers={"Content-Type": "application/x-www-form-urlencoded"})
if p.status_code != 200:
return
data = XiguaLive()
data.ParseFromString(p.content)
self._cursor = data.cursor
self._ext = data.internal_ext
for _each in data.data:
if _each.method == "WebcastGiftMessage":
_gift = Gift(_each.raw)
if _gift.isAnimate() or _gift.isFinished:
self.onPresentEnd(_gift)
else:
self.onPresent(_gift)
elif _each.method == "WebcastChatMessage":
_chat = Chat(_each.raw)
self.onChat(_chat)
elif _each.method == "WebcastControlMessage":
# 下播的时候会有个这个
self.onLeave(None)
elif _each.method == "WebcastSocialMessage":
_socialMessage = SocialMessage()
_socialMessage.ParseFromString(_each.raw)
_user = User(_socialMessage.user)
self.onSubscribe(_user)
elif _each.method == "WebcastFansclubMessage":
_fansClubMessage = FansClubMessage()
_fansClubMessage.ParseFromString(_each.raw)
# 升级是1加入是2
if _fansClubMessage.type == 2:
_user = User(_fansClubMessage.user)
self.onJoin(_user)
else:
self.onMessage(_fansClubMessage.content)
else:
pass
@property @property
def updateAt(self): def updateAt(self):
return self._updRoomAt return self._updRoomAt
def public_hello():
print("西瓜直播弹幕助手 by JerryYan")
print("接口版本:{version_name}({version_code_full})".format_map(VERSION_INFO))
if __name__ == "__main__":
name = "永恒de草薙"
if len(sys.argv) > 2:
if sys.argv[-1] == "d":
DEBUG = True
name = sys.argv[1]
public_hello()
print("搜索【", name, "", end="\t", flush=True)
api = XiGuaLiveApi(name)
if not api.isValidUser:
input("用户不存在")
sys.exit()
print("OK")
print(api.broadcaster.__repr__())
print("更新房间信息,请稍后", end="\t", flush=True)
if api.updRoomInfo(True):
print("OK")
else:
print("FAIL")
print("更新房间礼物信息", end="\t", flush=True)
__res = api.updGiftInfo()
if __res < 0:
print("FAIL")
else:
print('OK\n礼物种数:', __res)
print("=" * 30)
while True:
if api.isLive:
try:
api.getDanmaku()
time.sleep(1)
except requests.exceptions.BaseHTTPError:
print("网络错误,请确认网络")
time.sleep(5)
# except Exception as e:
# print(e)
else:
print("主播未开播等待1分钟后重试")
time.sleep(60)
api.updRoomInfo(True)