diff --git a/starbot/api/user.json b/starbot/api/user.json index ff4f49b..81b69f3 100644 --- a/starbot/api/user.json +++ b/starbot/api/user.json @@ -15,6 +15,17 @@ }, "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": { "url": "https://api.bilibili.com/x/relation/stat", "method": "GET", @@ -42,6 +53,17 @@ }, "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": { "url": "https://api.bilibili.com/x/space/arc/search", "method": "GET", diff --git a/starbot/core/bot.py b/starbot/core/bot.py index 54d1dbd..f866027 100644 --- a/starbot/core/bot.py +++ b/starbot/core/bot.py @@ -26,7 +26,7 @@ class StarBot: """ StarBot 类 """ - VERSION = "2.0.10" + VERSION = "2.0.14" STARBOT_ASCII_LOGO = "\n".join( ( r" _____ _ ____ _ ", @@ -35,7 +35,7 @@ class StarBot: 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"", r"", diff --git a/starbot/core/dynamic.py b/starbot/core/dynamic.py index 5467676..2d21056 100644 --- a/starbot/core/dynamic.py +++ b/starbot/core/dynamic.py @@ -31,6 +31,9 @@ async def dynamic_spider(datasource: DataSource): except ResponseCodeException as ex: if ex.code == -6: continue + if ex.code == 4100000: + logger.error("B 站登录凭据已失效, 无法继续抓取动态,请配置新登录凭据后重启服务") + continue logger.error(f"动态推送任务抓取最新动态异常, HTTP 错误码: {ex.code} ({ex.msg})") except NetworkException: continue diff --git a/starbot/core/live.py b/starbot/core/live.py index 71547f6..c2acd14 100644 --- a/starbot/core/live.py +++ b/starbot/core/live.py @@ -167,6 +167,48 @@ class LiveRoom: } 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): """ 获取自己在直播间的信息(粉丝勋章等级,直播用户等级等) diff --git a/starbot/core/room.py b/starbot/core/room.py index 0a918c6..91ce2f2 100644 --- a/starbot/core/room.py +++ b/starbot/core/room.py @@ -2,6 +2,7 @@ import asyncio import time import typing from asyncio import AbstractEventLoop +from datetime import datetime from typing import Optional, Any, Union, List from loguru import logger @@ -183,6 +184,8 @@ class Up(BaseModel): locked = False room_info = {} + fans_medal_info = {} + guards_info = {} # 是否为真正开播 if "live_time" in event["data"]: @@ -204,25 +207,34 @@ class Up(BaseModel): logger.opt(colors=True).info(f"[开播] {self.uname} ({self.room_id})") 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: if ex.code == 19002005: locked = True logger.warning(f"{self.uname} ({self.room_id}) 的直播间已加密") + else: + logger.error(f"{self.uname} ({self.room_id}) 的直播间信息获取失败, 错误信息: {ex.code} ({ex.msg})") 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) if not locked: - fans_count = room_info["anchor_info"]["relation_info"]["attention"] - if room_info["anchor_info"]["medal_info"] is None: - fans_medal_count = 0 - else: - fans_medal_count = room_info["anchor_info"]["medal_info"]["fansclub"] - guard_count = room_info["guard_info"]["count"] + fans_count = room_info["attention"] + fans_medal_count = fans_medal_info["fans_medal_light_count"] + guard_count = guards_info["info"]["num"] 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_guard_count(self.room_id, live_start_time, guard_count) @@ -231,12 +243,11 @@ class Up(BaseModel): # 推送开播消息 if not locked: - arg_base = room_info["room_info"] args = { "{uname}": self.uname, - "{title}": arg_base["title"], + "{title}": room_info["title"], "{url}": f"https://live.bilibili.com/{self.room_id}", - "{cover}": "".join(["{urlpic=", arg_base["cover"], "}"]) + "{cover}": "".join(["{urlpic=", room_info["user_cover"], "}"]) } else: args = { @@ -482,11 +493,15 @@ class Up(BaseModel): locked = False room_info = {} + fans_medal_info = {} + guards_info = {} # 基础数据变动 if self.__any_live_report_item_enabled(["fans_change", "fans_medal_change", "guard_change"]): 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: if ex.code == 19002005: locked = True @@ -506,21 +521,18 @@ class Up(BaseModel): else: guard_count = -1 - if room_info["anchor_info"]["medal_info"] is None: - fans_medal_count_after = 0 - else: - fans_medal_count_after = room_info["anchor_info"]["medal_info"]["fansclub"] + fans_medal_count_after = fans_medal_info["fans_medal_light_count"] live_report_param.update({ # 粉丝变动 "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_after": fans_medal_count_after, # 大航海变动 "guard_before": guard_count, - "guard_after": room_info["guard_info"]["count"] + "guard_after": guards_info["info"]["num"] }) # 直播数据 diff --git a/starbot/painter/LiveReportGenerator.py b/starbot/painter/LiveReportGenerator.py index 3bd02b0..81769d7 100644 --- a/starbot/painter/LiveReportGenerator.py +++ b/starbot/painter/LiveReportGenerator.py @@ -676,7 +676,7 @@ class LiveReportGenerator: end = abs_max + (-abs_max % 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) return cls.__get_line_diagram( indexs, profits, [], yticks, [], [], (-1, length), (start, end), width diff --git a/starbot/painter/RankingGenerator.py b/starbot/painter/RankingGenerator.py index 78468dc..a302927 100644 --- a/starbot/painter/RankingGenerator.py +++ b/starbot/painter/RankingGenerator.py @@ -158,7 +158,7 @@ class RankingGenerator: chart = PicGenerator(width, (face_size * count) + (row_space * (count - 1))) chart.set_row_space(row_space) 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 counts[i] > 0: bar = cls.__get_rank_bar_pic(bar_width, bar_height, start_color, end_color) diff --git a/starbot/utils/network.py b/starbot/utils/network.py index 4939de1..a029117 100644 --- a/starbot/utils/network.py +++ b/starbot/utils/network.py @@ -73,7 +73,7 @@ async def request(method: str, # 使用 Referer 和 UA 请求头以绕过反爬虫机制 default_headers = { "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 @@ -134,7 +134,7 @@ async def request(method: str, content_type = resp.headers.get("content-type") # 不是 application/json - if content_type.lower().index("application/json") == -1: + if content_type.lower().find("application/json") == -1: raise ResponseException("响应不是 application/json 类型") raw_data = await resp.text() @@ -156,7 +156,7 @@ async def request(method: str, if code != 0: # 4101131: 加载错误,请稍后再试, 22015: 您的账号异常,请稍后再试 - if code == 4101131 or code == 22015: + if code == 4101131 or code == 22015 or code == -352: await asyncio.sleep(10) continue diff --git a/starbot/utils/wbi.py b/starbot/utils/wbi.py new file mode 100644 index 0000000..03ddef4 --- /dev/null +++ b/starbot/utils/wbi.py @@ -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]