diff --git a/plugins/bilibili_sub/data_source.py b/plugins/bilibili_sub/data_source.py index 4e8e7f94..d4032287 100644 --- a/plugins/bilibili_sub/data_source.py +++ b/plugins/bilibili_sub/data_source.py @@ -1,200 +1,379 @@ -from nonebot import on_command -from nonebot.typing import T_State -from nonebot.adapters.cqhttp import Bot, MessageEvent, GroupMessageEvent, Message -from .data_source import ( - add_live_sub, - delete_sub, - add_up_sub, - add_season_sub, - get_media_id, - get_sub_status, - SubManager, -) -from models.level_user import LevelUser -from configs.config import GROUP_BILIBILI_SUB_LEVEL -from utils.utils import get_message_text, is_number, scheduler, get_bot +from bilibili_api.exceptions.ResponseCodeException import ResponseCodeException +from asyncio.exceptions import TimeoutError from models.bilibili_sub import BilibiliSub +from bilibili_api.live import LiveRoom +from bilibili_api import bangumi +from utils.message_builder import image +from bilibili_api.user import User +from bilibili_api import user from typing import Optional +from pathlib import Path +from configs.path_config import IMAGE_PATH +from datetime import datetime +from utils.browser import get_browser +from services.db_context import db from services.log import logger -from nonebot import Driver -import nonebot - -__plugin_name__ = "B站订阅" - -__plugin_usage__ = """B站订阅帮助: - 添加订阅 [主播/UP/番剧] [id/链接/番名] - 删除订阅 [id] - 查看订阅""" - -add_sub = on_command("添加订阅", priority=5, block=True) -del_sub = on_command("删除订阅", priority=5, block=True) -show_sub_info = on_command('查看订阅', priority=5, block=True) - -driver: Driver = nonebot.get_driver() +import aiohttp +import random -sub_manager: Optional[SubManager] = None +bilibili_search_url = "https://api.bilibili.com/x/web-interface/search/all/v2" + +dynamic_path = Path(IMAGE_PATH) / "bilibili_sub" / "dynamic" +dynamic_path.mkdir(exist_ok=True, parents=True) -@driver.on_startup -async def _(): - global sub_manager - sub_manager = SubManager() - - -@add_sub.args_parser -async def _(bot: Bot, event: MessageEvent, state: T_State): - season_data = state["season_data"] - msg = get_message_text(event.json()) - if not is_number(msg) or int(msg) < 1 or int(msg) > len(season_data): - await add_sub.reject("Id必须为数字且在范围内!请重新输入...") - state["id"] = season_data[int(msg) - 1]["media_id"] - - -@add_sub.handle() -async def _(bot: Bot, event: MessageEvent, state: T_State): - msg = get_message_text(event.json()).split() - if len(msg) < 2: - await add_sub.finish("参数不完全,请查看订阅帮助...") - sub_type = msg[0] - id_ = "" - if isinstance(event, GroupMessageEvent): - if not await LevelUser.check_level( - event.user_id, event.group_id, GROUP_BILIBILI_SUB_LEVEL - ): - await add_sub.finish( - f"您的权限不足,群内订阅的需要 {GROUP_BILIBILI_SUB_LEVEL} 级权限..", at_sender=True - ) - sub_user = f"{event.user_id}:{event.group_id}" - else: - sub_user = f"{event.user_id}" - state["sub_type"] = sub_type - state["sub_user"] = sub_user - if len(msg) > 1: - if "http" in msg[1]: - msg[1] = msg[1].split("?")[0] - msg[1] = msg[1][:-1] if msg[1][-1] == "/" else msg[1] - msg[1] = msg[1].split("/")[-1] - id_ = msg[1][2:] if msg[1].startswith("md") else msg[1] - if not is_number(id_): - if sub_type in ["season", "动漫", "番剧"]: - rst = "*以为您找到以下番剧,请输入Id选择:*\n" - state["season_data"] = await get_media_id(id_) - print(state["season_data"]) - if len(state["season_data"]) == 0: - await add_sub.finish(f"未找到番剧:{msg}") - for i, x in enumerate(state["season_data"]): - rst += f'{i + 1}.{state["season_data"][x]["title"]}\n----------\n' - await add_sub.send("\n".join(rst.split("\n")[:-1])) - else: - await add_sub.finish("Id 必须为全数字!") - else: - state["id"] = int(id_) - - -@add_sub.got("id") -async def _(bot: Bot, event: MessageEvent, state: T_State): - sub_type = state["sub_type"] - sub_user = state["sub_user"] - id_ = state["id"] - if sub_type in ["主播", "直播"]: - await add_sub.send(await add_live_sub(id_, sub_user)) - elif sub_type.lower() in ["up", "用户"]: - await add_sub.send(await add_up_sub(id_, sub_user)) - elif sub_type in ["season", "动漫", "番剧"]: - await add_sub.send(await add_season_sub(id_, sub_user)) - else: - await add_sub.finish("参数错误,第一参数必须为:主播/up/番剧!") - sub_manager.reload_flag = True - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 添加订阅:{sub_type} -> {sub_user} -> {id_}" - ) - - -@del_sub.handle() -async def _(bot: Bot, event: MessageEvent, state: T_State): - msg = get_message_text(event.json()) - if not is_number(msg): - await del_sub.finish('Id必须为数字!', at_sender=True) - id_ = f'{event.user_id}:{event.group_id}' if isinstance(event, GroupMessageEvent) else f'{event.user_id}' - if await BilibiliSub.delete_bilibili_sub(int(msg), id_): - await del_sub.send(f'删除订阅id:{msg} 成功...') - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 删除订阅 {id_}" - ) - else: - await del_sub.send(f'删除订阅id:{msg} 失败...') - - -@show_sub_info.handle() -async def _(bot: Bot, event: MessageEvent, state: T_State): - id_ = f'{event.user_id}:{event.group_id}' if isinstance(event, GroupMessageEvent) else f'{event.user_id}' - data = await BilibiliSub.get_sub_data(id_) - live_rst = '' - up_rst = '' - season_rst = '' - for x in data: - if x.sub_type == 'live': - live_rst += f'\t直播间id:{x.sub_id}\n' \ - f'\t名称:{x.uname}\n' \ - f'------------------\n' - if x.sub_type == 'up': - up_rst += f'\tUP:{x.uname}\n' \ - f'\tuid:{x.uid}\n' \ - f'------------------\n' - if x.sub_type == 'season': - season_rst += f'\t番名:{x.season_name}\n' \ - f'\t当前集数:{x.season_current_episode}\n' \ - f'------------------\n' - live_rst = '当前订阅的直播:\n' + live_rst if live_rst else live_rst - up_rst = '当前订阅的UP:\n' + up_rst if up_rst else up_rst - season_rst = '当前订阅的番剧:\n' + season_rst if season_rst else season_rst - if not live_rst and not up_rst and not season_rst: - live_rst = '您目前没有任何订阅...' - await show_sub_info.send(live_rst + up_rst + season_rst) - - -# 推送 -@scheduler.scheduled_job( - "interval", - seconds=30, -) -async def _(): - bot = get_bot() - sub = None - if bot: - try: - await sub_manager.reload_sub_data() - sub = await sub_manager.random_sub_data() - if sub: - rst = await get_sub_status(sub.sub_id, sub.sub_type) - await send_sub_msg(rst, sub, bot) - if sub.sub_type == "live": - rst = await get_sub_status(sub.sub_id, "up") - await send_sub_msg(rst, sub, bot) - except Exception as e: - logger.error(f"B站订阅推送发生错误 sub_id:{sub.sub_id if sub else 0} {type(e)}:{e}") - - -async def send_sub_msg(rst: str, sub: BilibiliSub, bot: Bot): +async def add_live_sub(live_id: int, sub_user: str) -> str: """ - 推送信息 - :param rst: 回复 - :param sub: BilibiliSub - :param bot: Bot + 添加直播订阅 + :param live_id: 直播房间号 + :param sub_user: 订阅用户 id # 7384933:private or 7384933:2342344(group) + :return: """ - if rst: - for x in sub.sub_users.split(",")[:-1]: + try: + async with db.transaction(): try: - if ":" in x: - await bot.send_group_msg( - group_id=int(x.split(":")[1]), message=Message(rst) - ) - else: - await bot.send_private_msg(user_id=int(x), message=Message(rst)) - except Exception as e: - logger.error(f"B站订阅推送发生错误 sub_id:{sub.sub_id} {type(e)}:{e}") + live = LiveRoom(live_id) + live_info = (await live.get_room_info())["room_info"] + except ResponseCodeException: + return f"未找到房间号Id:{live_id} 的信息,请检查Id是否正确" + uid = live_info["uid"] + room_id = live_info["room_id"] + short_id = live_info["short_id"] + title = live_info["title"] + live_status = live_info["live_status"] + if await BilibiliSub.add_bilibili_sub( + room_id, + "live", + sub_user, + uid=uid, + live_short_id=short_id, + live_status=live_status, + ): + await _get_up_status(live_id) + uname = (await BilibiliSub.get_sub(live_id)).uname + return ( + "已成功订阅主播:\n" + f"\ttitle:{title}\n" + f"\tname: {uname}\n" + f"\tlive_id:{live_id}\n" + f"\tuid:{uid}" + ) + else: + return "添加订阅失败..." + except Exception as e: + logger.error(f"订阅主播live_id:{live_id} 发生了错误 {type(e)}:{e}") + return "添加订阅失败..." + + +async def add_up_sub(uid: int, sub_user: str) -> str: + """ + 添加订阅 UP + :param uid: UP uid + :param sub_user: 订阅用户 + """ + try: + async with db.transaction(): + try: + u = user.User(uid) + user_info = await u.get_user_info() + except ResponseCodeException: + return f"未找到UpId:{uid} 的信息,请检查Id是否正确" + uname = user_info["name"] + dynamic_info = await u.get_dynamics(0) + dynamic_upload_time = 0 + if dynamic_info.get("cards"): + dynamic_upload_time = dynamic_info["cards"][0]["desc"]["timestamp"] + video_info = await u.get_videos() + latest_video_created = 0 + if video_info["list"].get("vlist"): + latest_video_created = video_info["list"]["vlist"][0]["created"] + if await BilibiliSub.add_bilibili_sub( + uid, + "up", + sub_user, + uid=uid, + uname=uname, + dynamic_upload_time=dynamic_upload_time, + latest_video_created=latest_video_created, + ): + return "已成功订阅UP:\n" f"\tname: {uname}\n" f"\tuid:{uid}" + else: + return "添加订阅失败..." + except Exception as e: + logger.error(f"订阅Up uid:{uid} 发生了错误 {type(e)}:{e}") + return "添加订阅失败..." + + +async def add_season_sub(media_id: int, sub_user: str) -> str: + """ + 添加订阅 UP + :param media_id: 番剧 media_id + :param sub_user: 订阅用户 + """ + try: + async with db.transaction(): + try: + season_info = await bangumi.get_meta(media_id) + except ResponseCodeException: + return f"未找到media_id:{media_id} 的信息,请检查Id是否正确" + season_id = season_info["media"]["season_id"] + season_current_episode = season_info["media"]["new_ep"]["index"] + season_name = season_info["media"]["title"] + if await BilibiliSub.add_bilibili_sub( + media_id, + "season", + sub_user, + season_name=season_name, + season_id=season_id, + season_current_episode=season_current_episode, + ): + return ( + "已成功订阅番剧:\n" + f"\ttitle: {season_name}\n" + f"\tcurrent_episode: {season_current_episode}" + ) + else: + return "添加订阅失败..." + except Exception as e: + logger.error(f"订阅番剧 media_id:{media_id} 发生了错误 {type(e)}:{e}") + return "添加订阅失败..." + + +async def delete_sub(sub_id: str, sub_user: str) -> str: + """ + 删除订阅 + :param sub_id: 订阅 id + :param sub_user: 订阅用户 id # 7384933:private or 7384933:2342344(group) + """ + if await BilibiliSub.delete_bilibili_sub(sub_id, sub_user): + return f"已成功取消订阅:{sub_id}" + else: + return f"取消订阅:{sub_id} 失败,请检查是否订阅过该Id...." + + +async def get_media_id(keyword: str) -> dict: + """ + 获取番剧的 media_id + :param keyword: 番剧名称 + """ + params = {"keyword": keyword} + async with aiohttp.ClientSession() as session: + for _ in range(3): + try: + _season_data = {} + async with session.get( + bilibili_search_url, timeout=5, params=params + ) as response: + if response.status == 200: + data = await response.json() + if data.get("data"): + for item in data["data"]["result"]: + if item["result_type"] == "media_bangumi": + idx = 0 + for x in item["data"]: + _season_data[idx] = { + "media_id": x["media_id"], + "title": x["title"] + .replace('', "") + .replace("", ""), + } + idx += 1 + return _season_data + except TimeoutError: + pass + return {} + + +async def get_sub_status(id_: int, sub_type: str) -> Optional[str]: + """ + 获取订阅状态 + :param id_: 订阅 id + :param sub_type: 订阅类型 + :return: + """ + try: + if sub_type == "live": + return await _get_live_status(id_) + elif sub_type == "up": + return await _get_up_status(id_) + elif sub_type == "season": + return await _get_season_status(id_) + except ResponseCodeException: + return "获取信息失败...请检查订阅Id是否存在或稍后再试..." + # except Exception as e: + # logger.error(f"获取订阅状态发生预料之外的错误 id_:{id_} {type(e)}:{e}") + # return "发生了预料之外的错误..请稍后再试或联系管理员....." + + +async def _get_live_status(id_: int) -> Optional[str]: + """ + 获取直播订阅状态 + :param id_: 直播间 id + """ + live = LiveRoom(id_) + live_info = (await live.get_room_info())["room_info"] + title = live_info["title"] + room_id = live_info["room_id"] + live_status = live_info["live_status"] + cover = live_info["cover"] + sub = await BilibiliSub.get_sub(id_) + if sub.live_status != live_status: + await BilibiliSub.update_sub_info(id_, live_status=live_status) + if sub.live_status == 0 and live_status == 1: + return ( + f"{image(cover)}\n" + f"{sub.uname} 开播啦!\n" + f"标题:{title}\n" + f"直链:https://live.bilibili.com/{room_id}" + ) + return None + + +async def _get_up_status(id_: int) -> Optional[str]: + """ + 获取用户投稿状态 + :param id_: 用户 id + :return: + """ + _user = await BilibiliSub.get_sub(id_) + u = user.User(_user.uid) + user_info = await u.get_user_info() + uname = user_info["name"] + video_info = await u.get_videos() + latest_video_created = 0 + video = None + if _user.uname != uname: + await BilibiliSub.update_sub_info(id_, uname=uname) + dynamic_img, dynamic_upload_time = await get_user_dynamic(u, _user) + if video_info["list"].get("vlist"): + video = video_info["list"]["vlist"][0] + latest_video_created = video["created"] + rst = "" + if dynamic_img: + await BilibiliSub.update_sub_info(id_, dynamic_upload_time=dynamic_upload_time) + rst += f"{uname} 发布了动态!\n" f"{dynamic_img}\n" + if _user.latest_video_created != latest_video_created and video: + rst = rst + "-------------\n" if rst else rst + await BilibiliSub.update_sub_info( + id_, latest_video_created=latest_video_created + ) + rst += ( + f'{image(video["pic"])}\n' + f"{uname} 投稿了新视频啦\n" + f'标题:{video["title"]}\n' + f'Bvid:{video["bvid"]}\n' + f'直链:https://www.bilibili.com/video/{video["bvid"]}' + ) + rst = None if rst == "-------------\n" else rst + return rst + + +async def _get_season_status(id_) -> Optional[str]: + """ + 获取 番剧 更新状态 + :param id_: 番剧 id + """ + season_info = await bangumi.get_meta(id_) + title = season_info["media"]["title"] + _idx = (await BilibiliSub.get_sub(id_)).season_current_episode + new_ep = season_info["media"]["new_ep"]["index"] + if new_ep != _idx: + await BilibiliSub.update_sub_info(id_, season_current_episode=new_ep, season_update_time=datetime.now()) + return ( + f'{image(season_info["media"]["cover"])}\n' f"[{title}]更新啦\n" f"最新集数:{new_ep}" + ) + return None + + +async def get_user_dynamic( + u: User, local_user: BilibiliSub +) -> "Optional[MessageSegment], int": + """ + 获取用户动态 + :param u: 用户类 + :param local_user: 数据库存储的用户数据 + :return: 最新动态截图与时间 + """ + dynamic_info = await u.get_dynamics(0) + browser = await get_browser() + if dynamic_info.get("cards") and browser: + dynamic_upload_time = dynamic_info["cards"][0]["desc"]["timestamp"] + if local_user.dynamic_upload_time != dynamic_upload_time: + page = await browser.new_page() + await page.goto( + f"https://space.bilibili.com/{local_user.uid}/dynamic", + wait_until="networkidle", + timeout=10000, + ) + await page.set_viewport_size({"width": 2560, "height": 1080}) + # 删除置顶 + await page.evaluate( + """ + xs = document.getElementsByClassName('first-card-with-title'); + for (x of xs) { + x.remove(); + } + """ + ) + card = await page.query_selector(".card") + # 截图并保存 + await card.screenshot( + path=dynamic_path / f"{local_user.sub_id}_{dynamic_upload_time}.jpg", + timeout=100000, + ) + await page.close() + return ( + image( + f"{local_user.sub_id}_{dynamic_upload_time}.jpg", + "bilibili_sub/dynamic", + ), + dynamic_upload_time, + ) + return None, None + + +class SubManager: + def __init__(self): + self.reload_flag = True + self.live_data = [] + self.up_data = [] + self.season_data = [] + self.sub_list = [] + + async def reload_sub_data(self): + """ + 重载数据 + """ + if self.reload_flag or not self.sub_list: + ( + self.live_data, + self.up_data, + self.season_data, + ) = await BilibiliSub.get_all_sub_data() + for x, i, j in zip(self.live_data, self.up_data, self.season_data): + self.sub_list.append(x) + self.sub_list.append(i) + self.sub_list.append(j) + self.reload_flag = False + + def append(self, data: BilibiliSub): + """ + 增加新数据 + :param data: 数据 + """ + self.sub_list.append(data) + + async def random_sub_data(self) -> Optional[BilibiliSub]: + """ + 随机获取一条数据 + :return: + """ + if not self.sub_list: + await self.reload_sub_data() + if self.sub_list: + sub = random.choice(self.sub_list) + self.sub_list.remove(sub) + return sub + return None +