Compare commits

17 Commits

Author SHA1 Message Date
7293845847 new: use local module 2025-02-21 02:45:46 -05:00
0ace306d49 Merge remote-tracking branch 'upstream/master' 2025-02-21 00:43:46 -05:00
LWR
ebf7343586 docs: Update version 2025-01-08 00:08:15 +08:00
LWR
4e70b4605d fix: Fixed None error caused by api risk control 2025-01-08 00:07:56 +08:00
LWR
79afbf8a96 refactor: Refactor wbi utils 2025-01-07 22:41:19 +08:00
方天宬
06334653da feat: Add wbi utils 2025-01-07 22:37:39 +08:00
LWR
354b1b661f docs: Update version 2024-12-04 00:27:40 +08:00
LWR
7965198c85 fix: Fixed error caused by api risk control 2024-12-04 00:25:59 +08:00
LWR
e683569d92 feat: Added a tip for expired cookie 2024-12-02 00:00:28 +08:00
LWR
f50b81e958 fix: Fixed exception when API returns non-JSON data 2024-12-01 23:54:51 +08:00
LWR
a7df9e58de fix: Fixed possible slice step is 0 when generate box profit diagram 2024-11-11 23:02:53 +08:00
474932c05d fix: fix the error when profits are zeros 2024-11-09 09:41:16 -05:00
8ac3078ea3 fix: fix numpy version 2024-11-07 02:10:12 +08:00
89532428d5 fix: make top_count nonzero 2024-11-06 13:06:16 -05:00
0e396728c6 fix: add dependences 2024-11-02 22:15:04 +08:00
c832e1da52 update: add fork info 2024-10-26 12:36:08 -04:00
221b2c8b0f set pipenv 2024-10-26 22:44:59 +08:00
21 changed files with 1776 additions and 27 deletions

11
Pipfile Normal file
View File

@@ -0,0 +1,11 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[requires]
python_version = "3.11"
[packages]
starbot-bilibili = { path = "./", editable = true }

1548
Pipfile.lock generated Normal file

File diff suppressed because it is too large Load Diff

4
setup.cfg Normal file
View File

@@ -0,0 +1,4 @@
[egg_info]
tag_build =
tag_date = 0

44
setup.py Normal file
View File

@@ -0,0 +1,44 @@
#!/usr/bin/env python
# coding: utf-8
from setuptools import setup, find_namespace_packages
from os import path
this_directory = path.abspath(path.dirname(__file__))
with open("README.md", "r", encoding="utf-8") as f:
long_description = f.read()
setup(
name='starbot-bilibili',
version='2.0.14',
license='GNU Affero General Public License v3.0',
description='一款极速,多功能的哔哩哔哩推送机器人',
author='LWR',
author_email='lwr1104@qq.com',
url='https://github.com/Starlwr/StarBot',
packages=find_namespace_packages(),
package_data={
'starbot.api': ['*.json'],
'starbot.resource': ['*.png', '*.ttf']
},
install_requires=[
'Brotli>=1.0.9',
'aiomysql>=0.1.1',
'redis>=4.5.5',
'emoji>=2.2.0',
'graia-broadcast==0.19.2',
'creart==0.2.2',
'creart-graia==0.1.5',
'graia-ariadne==0.9.8',
'graia-saya==0.0.16',
'jieba>=0.42.1',
'scipy>=1.10.0',
'Pillow==9.5.0',
'numpy==1.24.3',
'matplotlib==3.7.1',
'wordcloud>=1.8.2.2'
],
keywords='starbot',
long_description=long_description,
long_description_content_type='text/markdown',
python_requires=">=3.8"
)

View File

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

View File

@@ -117,6 +117,8 @@ async def room_data(app: Ariadne, source: Source, sender: Union[Friend, Group]):
pic.draw_text("")
pic.draw_text_right(25, "Designed By StarBot", Color.GRAY)
pic.draw_text_right(25, "https://github.com/Starlwr/StarBot", Color.LINK)
pic.draw_text_right(25, "本Bot由薛定谔的大喵维护", Color.LIGHTBLUE)
pic.draw_text_right(25, "https://gitea.phywyj.dynv6.net/wyj/starbot", Color.LINK)
pic.crop_and_paste_bottom()
await app.send_message(sender, MessageChain(Image(base64=pic.base64())))

View File

@@ -109,6 +109,8 @@ async def room_data_total(app: Ariadne, source: Source, sender: Union[Friend, Gr
pic.draw_text("")
pic.draw_text_right(25, "Designed By StarBot", Color.GRAY)
pic.draw_text_right(25, "https://github.com/Starlwr/StarBot", Color.LINK)
pic.draw_text_right(25, "本Bot由薛定谔的大喵维护", Color.LIGHTBLUE)
pic.draw_text_right(25, "https://gitea.phywyj.dynv6.net/wyj/starbot", Color.LINK)
pic.crop_and_paste_bottom()
await app.send_message(sender, MessageChain(Image(base64=pic.base64())))

View File

@@ -170,6 +170,8 @@ async def user_data(app: Ariadne, source: Source, sender: Union[Friend, Group],
pic.draw_text("")
pic.draw_text_right(25, "Designed By StarBot", Color.GRAY)
pic.draw_text_right(25, "https://github.com/Starlwr/StarBot", Color.LINK)
pic.draw_text_right(25, "本Bot由薛定谔的大喵维护", Color.LIGHTBLUE)
pic.draw_text_right(25, "https://gitea.phywyj.dynv6.net/wyj/starbot", Color.LINK)
pic.crop_and_paste_bottom()
await app.send_message(sender, MessageChain(Image(base64=pic.base64())))

View File

@@ -183,6 +183,8 @@ async def user_data_total(app: Ariadne, source: Source, sender: Union[Friend, Gr
pic.draw_text("")
pic.draw_text_right(25, "Designed By StarBot", Color.GRAY)
pic.draw_text_right(25, "https://github.com/Starlwr/StarBot", Color.LINK)
pic.draw_text_right(25, "本Bot由薛定谔的大喵维护", Color.LIGHTBLUE)
pic.draw_text_right(25, "https://gitea.phywyj.dynv6.net/wyj/starbot", Color.LINK)
pic.crop_and_paste_bottom()
await app.send_message(sender, MessageChain(Image(base64=pic.base64())))

View File

@@ -158,6 +158,8 @@ async def _help(app: Ariadne, sender: Union[Friend, Group]):
# 底部版权信息,请务必保留此处
pic.draw_text_right(25, "Designed By StarBot", Color.GRAY)
pic.draw_text_right(25, "https://github.com/Starlwr/StarBot", Color.LINK)
pic.draw_text_right(25, "本Bot由薛定谔的大喵维护", Color.LIGHTBLUE)
pic.draw_text_right(25, "https://gitea.phywyj.dynv6.net/wyj/starbot", Color.LINK)
pic.crop_and_paste_bottom()
await app.send_message(sender, MessageChain(Image(base64=pic.base64())))

View File

@@ -129,6 +129,8 @@ async def ranking(app: Ariadne,
pic.draw_text("")
pic.draw_text_right(25, "Designed By StarBot", Color.GRAY)
pic.draw_text_right(25, "https://github.com/Starlwr/StarBot", Color.LINK)
pic.draw_text_right(25, "本Bot由薛定谔的大喵维护", Color.LIGHTBLUE)
pic.draw_text_right(25, "https://gitea.phywyj.dynv6.net/wyj/starbot", Color.LINK)
pic.crop_and_paste_bottom()
await app.send_message(sender, MessageChain(Image(base64=pic.base64())))

View File

@@ -124,6 +124,8 @@ async def ranking_double(app: Ariadne,
pic.draw_text("")
pic.draw_text_right(25, "Designed By StarBot", Color.GRAY)
pic.draw_text_right(25, "https://github.com/Starlwr/StarBot", Color.LINK)
pic.draw_text_right(25, "本Bot由薛定谔的大喵维护", Color.LIGHTBLUE)
pic.draw_text_right(25, "https://gitea.phywyj.dynv6.net/wyj/starbot", Color.LINK)
pic.crop_and_paste_bottom()
await app.send_message(sender, MessageChain(Image(base64=pic.base64())))

View File

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

View File

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

View File

@@ -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):
"""
获取自己在直播间的信息(粉丝勋章等级,直播用户等级等)

View File

@@ -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"<magenta>[开播] {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"]
})
# 直播数据

View File

@@ -69,6 +69,8 @@ class DynamicPicGenerator:
pic.move_pos(0, 15)
pic.draw_text_right(25, "Designed By StarBot", Color.GRAY)
pic.draw_text_right(25, "https://github.com/Starlwr/StarBot", Color.LINK)
pic.draw_text_right(25, "本Bot由薛定谔的大喵维护", Color.LIGHTBLUE)
pic.draw_text_right(25, "https://gitea.phywyj.dynv6.net/wyj/starbot", Color.LINK)
pic.crop_and_paste_bottom()
return pic.base64()

View File

@@ -405,6 +405,8 @@ class LiveReportGenerator:
pic.set_row_space(10)
pic.draw_text_right(50, "Designed By StarBot", Color.GRAY, logo_limit)
pic.draw_text_right(50, "https://github.com/Starlwr/StarBot", Color.LINK, logo_limit)
pic.draw_text_right(50, "本Bot由薛定谔的大喵维护", Color.LIGHTBLUE, logo_limit)
pic.draw_text_right(50, "https://gitea.phywyj.dynv6.net/wyj/starbot", Color.LINK, logo_limit)
pic.crop_and_paste_bottom()
return pic.base64()
@@ -669,12 +671,12 @@ class LiveReportGenerator:
length = len(profits)
indexs = list(range(0, length))
abs_max = math.ceil(max(max(profits), abs(min(profits))))
abs_max = math.ceil(max(max(profits), abs(min(profits)), 0.01))
start = -abs_max - (-abs_max % 10)
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

View File

@@ -153,7 +153,7 @@ class RankingGenerator:
reverse_bar_x = face_x + offset
top_bar_width = (width - face_size) / 2 + offset
if top_count is None:
top_count = max(max(counts), abs(min(counts)))
top_count = max(max(counts), abs(min(counts)), 0.001)
chart = PicGenerator(width, (face_size * count) + (row_space * (count - 1)))
chart.set_row_space(row_space)

View File

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

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]