Merge remote-tracking branch 'upstream/master'

This commit is contained in:
2025-02-21 00:43:46 -05:00
9 changed files with 150 additions and 26 deletions

View File

@@ -15,6 +15,17 @@
}, },
"comment": "用户基本信息" "comment": "用户基本信息"
}, },
"info_wbi": {
"url": "https://api.bilibili.com/x/space/wbi/acc/info",
"method": "GET",
"verify": false,
"params": {
"mid": "int: uid",
"w_rid": "str: Wbi签名",
"wts": "int: 当前时间戳"
},
"comment": "用户基本信息WBI版本"
},
"relation": { "relation": {
"url": "https://api.bilibili.com/x/relation/stat", "url": "https://api.bilibili.com/x/relation/stat",
"method": "GET", "method": "GET",
@@ -42,6 +53,17 @@
}, },
"comment": "直播间基本信息" "comment": "直播间基本信息"
}, },
"live_wbi": {
"url": "https://api.bilibili.com/x/space/wbi/acc/info",
"method": "GET",
"verify": false,
"params": {
"mid": "int: uid",
"w_rid": "str: Wbi签名",
"wts": "int: 当前时间戳"
},
"comment": "直播间基本信息WBI版本"
},
"video": { "video": {
"url": "https://api.bilibili.com/x/space/arc/search", "url": "https://api.bilibili.com/x/space/arc/search",
"method": "GET", "method": "GET",

View File

@@ -26,7 +26,7 @@ class StarBot:
""" """
StarBot 类 StarBot 类
""" """
VERSION = "2.0.10" VERSION = "2.0.14"
STARBOT_ASCII_LOGO = "\n".join( STARBOT_ASCII_LOGO = "\n".join(
( (
r" _____ _ ____ _ ", r" _____ _ ____ _ ",
@@ -35,7 +35,7 @@ class StarBot:
r" \___ \| __/ _` | '__| _ < / _ \| __|", r" \___ \| __/ _` | '__| _ < / _ \| __|",
r" ____) | || (_| | | | |_) | (_) | |_ ", r" ____) | || (_| | | | |_) | (_) | |_ ",
r" |_____/ \__\__,_|_| |____/ \___/ \__|", r" |_____/ \__\__,_|_| |____/ \___/ \__|",
f" StarBot - (v{VERSION}) 2024-06-30", f" StarBot - (v{VERSION}) 2025-01-08",
r" Github: https://github.com/Starlwr/StarBot", r" Github: https://github.com/Starlwr/StarBot",
r"", r"",
r"", r"",

View File

@@ -31,6 +31,9 @@ async def dynamic_spider(datasource: DataSource):
except ResponseCodeException as ex: except ResponseCodeException as ex:
if ex.code == -6: if ex.code == -6:
continue continue
if ex.code == 4100000:
logger.error("B 站登录凭据已失效, 无法继续抓取动态,请配置新登录凭据后重启服务")
continue
logger.error(f"动态推送任务抓取最新动态异常, HTTP 错误码: {ex.code} ({ex.msg})") logger.error(f"动态推送任务抓取最新动态异常, HTTP 错误码: {ex.code} ({ex.msg})")
except NetworkException: except NetworkException:
continue continue

View File

@@ -167,6 +167,48 @@ class LiveRoom:
} }
return await request(api['method'], api["url"], params, credential=self.credential) return await request(api['method'], api["url"], params, credential=self.credential)
async def get_room_info_v2(self):
"""
获取直播间信息(标题,简介等)
"""
api = "https://api.live.bilibili.com/room/v1/Room/get_info"
params = {
"room_id": self.room_display_id
}
return await request("GET", api, params, credential=self.credential)
async def get_fans_medal_info(self, uid: int):
"""
获取粉丝勋章信息
Args:
uid: 用户 UID
"""
api = "https://api.live.bilibili.com/xlive/app-ucenter/v1/fansMedal/fans_medal_info"
params = {
"target_id": uid,
"room_id": self.room_display_id
}
return await request("GET", api, params, credential=self.credential)
async def get_guards_info(self, uid: int):
"""
获取大航海信息
Args:
uid: 用户 UID
"""
api = "https://api.live.bilibili.com/xlive/app-room/v2/guardTab/topListNew"
params = {
"roomid": self.room_display_id,
"page": 1,
"ruid": uid,
"page_size": 20,
"typ": 5,
"platform": "web"
}
return await request("GET", api, params, credential=self.credential)
async def get_user_info_in_room(self): async def get_user_info_in_room(self):
""" """
获取自己在直播间的信息(粉丝勋章等级,直播用户等级等) 获取自己在直播间的信息(粉丝勋章等级,直播用户等级等)

View File

@@ -2,6 +2,7 @@ import asyncio
import time import time
import typing import typing
from asyncio import AbstractEventLoop from asyncio import AbstractEventLoop
from datetime import datetime
from typing import Optional, Any, Union, List from typing import Optional, Any, Union, List
from loguru import logger from loguru import logger
@@ -183,6 +184,8 @@ class Up(BaseModel):
locked = False locked = False
room_info = {} room_info = {}
fans_medal_info = {}
guards_info = {}
# 是否为真正开播 # 是否为真正开播
if "live_time" in event["data"]: if "live_time" in event["data"]:
@@ -204,25 +207,34 @@ class Up(BaseModel):
logger.opt(colors=True).info(f"<magenta>[开播] {self.uname} ({self.room_id})</>") logger.opt(colors=True).info(f"<magenta>[开播] {self.uname} ({self.room_id})</>")
try: try:
room_info = await self.__live_room.get_room_info() room_info = await self.__live_room.get_room_info_v2()
fans_medal_info = await self.__live_room.get_fans_medal_info(self.uid)
guards_info = await self.__live_room.get_guards_info(self.uid)
except ResponseCodeException as ex: except ResponseCodeException as ex:
if ex.code == 19002005: if ex.code == 19002005:
locked = True locked = True
logger.warning(f"{self.uname} ({self.room_id}) 的直播间已加密") logger.warning(f"{self.uname} ({self.room_id}) 的直播间已加密")
else:
logger.error(f"{self.uname} ({self.room_id}) 的直播间信息获取失败, 错误信息: {ex.code} ({ex.msg})")
if not locked: if not locked:
self.uname = room_info["anchor_info"]["base_info"]["uname"] # 此处若有合适 API 需更新一下最新昵称
pass
live_start_time = room_info["room_info"]["live_start_time"] if not locked else int(time.time()) if locked:
live_start_time = int(time.time())
else:
if room_info["live_time"] != "0000-00-00 00:00:00":
time_format = "%Y-%m-%d %H:%M:%S"
live_start_time = int(datetime.strptime(room_info["live_time"], time_format).timestamp())
else:
live_start_time = int(time.time())
await redis.set_live_start_time(self.room_id, live_start_time) await redis.set_live_start_time(self.room_id, live_start_time)
if not locked: if not locked:
fans_count = room_info["anchor_info"]["relation_info"]["attention"] fans_count = room_info["attention"]
if room_info["anchor_info"]["medal_info"] is None: fans_medal_count = fans_medal_info["fans_medal_light_count"]
fans_medal_count = 0 guard_count = guards_info["info"]["num"]
else:
fans_medal_count = room_info["anchor_info"]["medal_info"]["fansclub"]
guard_count = room_info["guard_info"]["count"]
await redis.set_fans_count(self.room_id, live_start_time, fans_count) await redis.set_fans_count(self.room_id, live_start_time, fans_count)
await redis.set_fans_medal_count(self.room_id, live_start_time, fans_medal_count) await redis.set_fans_medal_count(self.room_id, live_start_time, fans_medal_count)
await redis.set_guard_count(self.room_id, live_start_time, guard_count) await redis.set_guard_count(self.room_id, live_start_time, guard_count)
@@ -231,12 +243,11 @@ class Up(BaseModel):
# 推送开播消息 # 推送开播消息
if not locked: if not locked:
arg_base = room_info["room_info"]
args = { args = {
"{uname}": self.uname, "{uname}": self.uname,
"{title}": arg_base["title"], "{title}": room_info["title"],
"{url}": f"https://live.bilibili.com/{self.room_id}", "{url}": f"https://live.bilibili.com/{self.room_id}",
"{cover}": "".join(["{urlpic=", arg_base["cover"], "}"]) "{cover}": "".join(["{urlpic=", room_info["user_cover"], "}"])
} }
else: else:
args = { args = {
@@ -482,11 +493,15 @@ class Up(BaseModel):
locked = False locked = False
room_info = {} room_info = {}
fans_medal_info = {}
guards_info = {}
# 基础数据变动 # 基础数据变动
if self.__any_live_report_item_enabled(["fans_change", "fans_medal_change", "guard_change"]): if self.__any_live_report_item_enabled(["fans_change", "fans_medal_change", "guard_change"]):
try: try:
room_info = await self.__live_room.get_room_info() room_info = await self.__live_room.get_room_info_v2()
fans_medal_info = await self.__live_room.get_fans_medal_info(self.uid)
guards_info = await self.__live_room.get_guards_info(self.uid)
except ResponseCodeException as ex: except ResponseCodeException as ex:
if ex.code == 19002005: if ex.code == 19002005:
locked = True locked = True
@@ -506,21 +521,18 @@ class Up(BaseModel):
else: else:
guard_count = -1 guard_count = -1
if room_info["anchor_info"]["medal_info"] is None: fans_medal_count_after = fans_medal_info["fans_medal_light_count"]
fans_medal_count_after = 0
else:
fans_medal_count_after = room_info["anchor_info"]["medal_info"]["fansclub"]
live_report_param.update({ live_report_param.update({
# 粉丝变动 # 粉丝变动
"fans_before": fans_count, "fans_before": fans_count,
"fans_after": room_info["anchor_info"]["relation_info"]["attention"], "fans_after": room_info["attention"],
# 粉丝团(粉丝勋章数)变动 # 粉丝团(粉丝勋章数)变动
"fans_medal_before": fans_medal_count, "fans_medal_before": fans_medal_count,
"fans_medal_after": fans_medal_count_after, "fans_medal_after": fans_medal_count_after,
# 大航海变动 # 大航海变动
"guard_before": guard_count, "guard_before": guard_count,
"guard_after": room_info["guard_info"]["count"] "guard_after": guards_info["info"]["num"]
}) })
# 直播数据 # 直播数据

View File

@@ -676,7 +676,7 @@ class LiveReportGenerator:
end = abs_max + (-abs_max % 10) end = abs_max + (-abs_max % 10)
step = int((end - start) / 10) step = int((end - start) / 10)
yticks = list(range(start, end)[::step]) yticks = list(range(start, end)[::step]) if step != 0 else [0]
yticks.append(end) yticks.append(end)
return cls.__get_line_diagram( return cls.__get_line_diagram(
indexs, profits, [], yticks, [], [], (-1, length), (start, end), width indexs, profits, [], yticks, [], [], (-1, length), (start, end), width

View File

@@ -158,7 +158,7 @@ class RankingGenerator:
chart = PicGenerator(width, (face_size * count) + (row_space * (count - 1))) chart = PicGenerator(width, (face_size * count) + (row_space * (count - 1)))
chart.set_row_space(row_space) chart.set_row_space(row_space)
for i in range(count): for i in range(count):
bar_width = int(abs(counts[i]) / top_count * top_bar_width) bar_width = int(abs(counts[i]) / top_count * top_bar_width) if top_count != 0 else 0.1
if bar_width != 0: if bar_width != 0:
if counts[i] > 0: if counts[i] > 0:
bar = cls.__get_rank_bar_pic(bar_width, bar_height, start_color, end_color) bar = cls.__get_rank_bar_pic(bar_width, bar_height, start_color, end_color)

View File

@@ -73,7 +73,7 @@ async def request(method: str,
# 使用 Referer 和 UA 请求头以绕过反爬虫机制 # 使用 Referer 和 UA 请求头以绕过反爬虫机制
default_headers = { default_headers = {
"Referer": "https://www.bilibili.com", "Referer": "https://www.bilibili.com",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36 Core/1.94.218.400 QQBrowser/12.1.5496.400" "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.97 Safari/537.36 Core/1.116.462.400 QQBrowser/13.3.6197.400"
} }
headers = default_headers headers = default_headers
@@ -134,7 +134,7 @@ async def request(method: str,
content_type = resp.headers.get("content-type") content_type = resp.headers.get("content-type")
# 不是 application/json # 不是 application/json
if content_type.lower().index("application/json") == -1: if content_type.lower().find("application/json") == -1:
raise ResponseException("响应不是 application/json 类型") raise ResponseException("响应不是 application/json 类型")
raw_data = await resp.text() raw_data = await resp.text()
@@ -156,7 +156,7 @@ async def request(method: str,
if code != 0: if code != 0:
# 4101131: 加载错误,请稍后再试, 22015: 您的账号异常,请稍后再试 # 4101131: 加载错误,请稍后再试, 22015: 您的账号异常,请稍后再试
if code == 4101131 or code == 22015: if code == 4101131 or code == 22015 or code == -352:
await asyncio.sleep(10) await asyncio.sleep(10)
continue continue

45
starbot/utils/wbi.py Normal file
View File

@@ -0,0 +1,45 @@
# https://socialsisteryi.github.io/bilibili-API-collect/docs/misc/sign/wbi.html#python
from functools import reduce
from hashlib import md5
import urllib.parse
import time
from starbot.utils.network import request
from starbot.utils.utils import get_credential
mixinKeyEncTab = [
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40,
61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11,
36, 20, 34, 44, 52
]
def get_mixin_key(orig: str):
return reduce(lambda s, i: s + orig[i], mixinKeyEncTab, '')[:32]
def enc_wbi(params: dict, img_key: str, sub_key: str):
mixin_key = get_mixin_key(img_key + sub_key)
curr_time = round(time.time())
params['wts'] = curr_time # 添加 wts 字段
params = dict(sorted(params.items())) # 按照 key 重排参数
# 过滤 value 中的 "!'()*" 字符
params = {
k: ''.join(filter(lambda c: c not in "!'()*", str(v)))
for k, v
in params.items()
}
query = urllib.parse.urlencode(params) # 序列化参数
wbi_sign = md5((query + mixin_key).encode()).hexdigest() # 计算 w_rid
params['w_rid'] = wbi_sign
return params
async def get_wbi_keys() -> tuple[str, str]:
"""获取最新的 img_key 和 sub_key"""
result = await request("GET", "https://api.bilibili.com/x/web-interface/nav", credential=get_credential())
img_url = result['wbi_img']['img_url']
sub_url = result['wbi_img']['sub_url']
return img_url.rsplit('/', 1)[1].split('.')[0], sub_url.rsplit('/', 1)[1].split('.')[0]