From 0c667d8ba37831553a6822cb21871686465eb27f Mon Sep 17 00:00:00 2001 From: liuwei Date: Fri, 30 Jan 2026 14:57:53 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=97=E9=B1=BC=E9=B1=BC=E5=90=A7=E8=AE=A2?= =?UTF-8?q?=E9=98=85=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/douyu/main.py | 165 +++++++++++++++++++++++++++++++++++++++++- test/douyu_news.py | 91 +++++++++++++++++++++++ 2 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 test/douyu_news.py diff --git a/plugins/douyu/main.py b/plugins/douyu/main.py index 018b7eb..3d11a51 100644 --- a/plugins/douyu/main.py +++ b/plugins/douyu/main.py @@ -53,6 +53,51 @@ class DouyuRedisManager: res.append(gid) return res + # --- 鱼吧相关方法 --- + def add_group_yuba(self, group_id: str, hash_id: str) -> bool: + key = f"{self.prefix}group:{group_id}:yubas" + return self.redis.sadd(key, hash_id) >= 0 + + def remove_group_yuba(self, group_id: str, hash_id: str) -> bool: + key = f"{self.prefix}group:{group_id}:yubas" + return self.redis.srem(key, hash_id) >= 0 + + def list_group_yubas(self, group_id: str) -> List[str]: + key = f"{self.prefix}group:{group_id}:yubas" + yubas = self.redis.smembers(key) or set() + result = [] + for y in yubas: + result.append(y.decode("utf-8") if isinstance(y, bytes) else y) + return sorted(result) + + def all_subscribed_yubas(self) -> Set[str]: + groups = GroupBotManager.get_group_list() + yubas: Set[str] = set() + for gid in groups: + for y in self.list_group_yubas(gid): + yubas.add(y) + return yubas + + def groups_for_yuba(self, hash_id: str) -> List[str]: + groups = GroupBotManager.get_group_list() + res = [] + for gid in groups: + if hash_id in set(self.list_group_yubas(gid)): + res.append(gid) + return res + + def get_yuba_last_id(self, hash_id: str) -> Optional[str]: + key = f"{self.prefix}yuba_last_id:{hash_id}" + data = self.redis.get(key) + if not data: + return None + return data.decode("utf-8") if isinstance(data, bytes) else data + + def set_yuba_last_id(self, hash_id: str, feed_id: str) -> bool: + key = f"{self.prefix}yuba_last_id:{hash_id}" + return self.redis.set(key, feed_id) + + # --- 提醒名单方法 --- def add_group_subscriber(self, group_id: str, user_id: str) -> bool: key = f"{self.prefix}group:{group_id}:subscribers" return self.redis.sadd(key, user_id) >= 0 @@ -127,11 +172,18 @@ class DouyuPlugin(MessagePluginInterface): self.bot: WechatAPIClient = None self.feature = self.register_feature() self.redis_manager: Optional[DouyuRedisManager] = None - self._commands = ["斗鱼订阅", "取消斗鱼订阅", "斗鱼订阅列表", "斗鱼订阅提醒", "取消斗鱼订阅提醒"] + self._commands = ["斗鱼订阅", "取消斗鱼订阅", "斗鱼订阅列表", "斗鱼订阅提醒", "取消斗鱼订阅提醒", + "订阅鱼吧", "取消订阅鱼吧", "鱼吧订阅列表"] self._api_template = "https://www.douyu.com/betard/{room_id}" + self._yuba_api = "https://yuba.douyu.com/wgapi/yubanc/api/feed/getUserFeedList" self._user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" self._check_interval = 5 - async_job.every_minutes(self._check_interval)(self._scheduled_check_job) + async_job.every_minutes(self._check_interval)(self._scheduled_unified_check_job) + + async def _scheduled_unified_check_job(self): + """统一检查直播和鱼吧动态""" + await self._scheduled_check_job() + await self._scheduled_yuba_check_job() def initialize(self, context: Dict[str, Any]) -> bool: try: @@ -220,6 +272,36 @@ class DouyuPlugin(MessagePluginInterface): ok = self.redis_manager.remove_group_room(roomid or sender, room_id) await self.bot.send_text_message(roomid or sender, f"✅ 已取消订阅斗鱼房间 {room_id}", sender) return True, "取消成功" if ok else "取消失败" + + if content == "鱼吧订阅列表": + yubas = self.redis_manager.list_group_yubas(roomid or sender) + if not yubas: + await self.bot.send_text_message(roomid or sender, "暂无鱼吧订阅", sender) + return True, "暂无鱼吧订阅" + text = "当前订阅的斗鱼鱼吧:\n" + "\n".join(yubas) + await self.bot.send_text_message(roomid or sender, text, sender) + return True, "列表已发送" + + if first_token == "订阅鱼吧": + parts = content.split() + if len(parts) < 2: + await self.bot.send_text_message(roomid or sender, "请提供鱼吧 hash_id,例如:订阅鱼吧 PDAP2zEk3nwx", sender) + return True, "命令格式错误" + hash_id = parts[1].strip() + ok = self.redis_manager.add_group_yuba(roomid or sender, hash_id) + await self.bot.send_text_message(roomid or sender, f"✅ 已订阅斗鱼鱼吧 {hash_id}", sender) + return True, "订阅成功" if ok else "订阅失败" + + if first_token == "取消订阅鱼吧": + parts = content.split() + if len(parts) < 2: + await self.bot.send_text_message(roomid or sender, "请提供鱼吧 hash_id,例如:取消订阅鱼吧 PDAP2zEk3nwx", sender) + return True, "命令格式错误" + hash_id = parts[1].strip() + ok = self.redis_manager.remove_group_yuba(roomid or sender, hash_id) + await self.bot.send_text_message(roomid or sender, f"✅ 已取消订阅斗鱼鱼吧 {hash_id}", sender) + return True, "取消成功" if ok else "取消失败" + return False, None async def _scheduled_check_job(self): @@ -305,3 +387,82 @@ class DouyuPlugin(MessagePluginInterface): except Exception as e: logger.error(f"发送斗鱼下播提醒失败: {e}") continue + + async def _scheduled_yuba_check_job(self): + try: + yubas = self.redis_manager.all_subscribed_yubas() + if not yubas: + return + async with aiohttp.ClientSession() as session: + for hash_id in yubas: + try: + params = { + "filter_type": 1, + "hash_id": hash_id, + "limit": 10, + "offset": 0 + } + headers = { + "User-Agent": self._user_agent, + "Referer": f"https://yuba.douyu.com/member/{hash_id}/main/news", + } + async with session.get(self._yuba_api, headers=headers, params=params, timeout=aiohttp.ClientTimeout(total=10)) as resp: + data = await resp.json(content_type=None) + + if data.get("error") != 0: + logger.error(f"斗鱼鱼吧 API 错误 ({hash_id}): {data.get('msg')}") + continue + + feed_list = data.get("data", {}).get("feed_list", []) + # 查找第一条【非置顶】动态 + target_feed = None + for feed in feed_list: + if feed.get("home_feed_top") == 1: + continue + target_feed = feed + break + + if not target_feed: + continue + + feed_id = str(target_feed.get("feed_id")) + last_id = self.redis_manager.get_yuba_last_id(hash_id) + + if last_id and feed_id == last_id: + continue + + # 发现新动态 + nickname = target_feed.get("publisher", {}).get("nickname", "未知主播") + content = target_feed.get("text", "") + # 限制内容长度 + if len(content) > 200: + content = content[:200] + "..." + + full_url = f"https://yuba.douyu.com/feed/{feed_id}" + + await self._notify_groups_yuba(hash_id, nickname, content, full_url) + + # 保存标记 + self.redis_manager.set_yuba_last_id(hash_id, feed_id) + + await asyncio.sleep(0.5) + except Exception as e: + logger.error(f"检查斗鱼鱼吧 ({hash_id}) 失败: {e}") + continue + except Exception as e: + logger.error(f"斗鱼鱼吧定时任务异常: {e}") + + async def _notify_groups_yuba(self, hash_id: str, nickname: str, content: str, url: str): + groups = self.redis_manager.groups_for_yuba(hash_id) + text = f"🌟 斗鱼鱼吧动态提醒 \n👤 主播:{nickname}\n📝 内容:{content}\n🔗 链接:{url}" + for gid in groups: + if GroupBotManager.get_group_permission(gid, self.feature) == PermissionStatus.ENABLED: + try: + subs = self.redis_manager.list_group_subscribers(gid) + if subs: + await self.bot.send_at_message(gid, text, subs) + else: + await self.bot.send_text_message(gid, text) + except Exception as e: + logger.error(f"发送斗鱼鱼吧动态提醒失败: {e}") + continue diff --git a/test/douyu_news.py b/test/douyu_news.py new file mode 100644 index 0000000..f846a67 --- /dev/null +++ b/test/douyu_news.py @@ -0,0 +1,91 @@ +import requests +import os +from datetime import datetime + + +class DouyuYubaMonitor: + def __init__(self, hash_id): + self.hash_id = hash_id + self.api_url = "https://yuba.douyu.com/wgapi/yubanc/api/feed/getUserFeedList" + self.record_file = f"last_processed_{hash_id}.txt" + self.headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Referer": f"https://yuba.douyu.com/member/{hash_id}/main/news", + } + + def get_last_id(self): + if os.path.exists(self.record_file): + with open(self.record_file, "r") as f: + return f.read().strip() + return None + + def save_last_id(self, feed_id): + with open(self.record_file, "w") as f: + f.write(str(feed_id)) + + def fetch_and_process(self): + params = { + "filter_type": 1, + "hash_id": self.hash_id, + "limit": 10, + "offset": 0 + } + + try: + response = requests.get(self.api_url, headers=self.headers, params=params, timeout=10) + data = response.json() + + if data.get("error") != 0: + print(f"API Error: {data.get('msg')}") + return + + feed_list = data.get("data", {}).get("feed_list", []) + + # 查找第一条【非置顶】动态 + target_feed = None + for feed in feed_list: + if feed.get("home_feed_top") == 1: + continue + target_feed = feed + break + + if not target_feed: + print("未发现有效动态(可能全是置顶)。") + return + + feed_id = str(target_feed.get("feed_id")) + last_id = self.get_last_id() + + # 标记检查 + if feed_id == last_id: + print(f"[{datetime.now().strftime('%H:%M:%S')}] 监控中,暂无最新非置顶消息。") + return + + # 提取参数拼接精准链接 + # 根据你提供的样本:origin 固定为 9, scode 可以从 publisher 逻辑或固定获取(通常 scode 是动态的,这里采用 feed 中的核心参数) + group_id = target_feed.get("group", {}).get("group_id", "0") + + # 斗鱼 Web 端scode通常在分享链接中生成,这里优先匹配你给出的格式 + # 如果接口没直接给 scode,可以根据业务需求固定或留空,这里保留你给出的示例参数 + + full_url = f"https://yuba.douyu.com/feed/{feed_id}" + + # 输出结果 + print("\n" + "★" * 50) + print(f"【提取到最新动态】") + print(f"发布时间: {datetime.fromtimestamp(int(target_feed.get('ctime'))).strftime('%Y-%m-%d %H:%M:%S')}") + print(f"正文内容: {target_feed.get('text', '')[:200]}") + print(f"精准链接: {full_url}") + print("★" * 50 + "\n") + + # 保存标记 + self.save_last_id(feed_id) + + except Exception as e: + print(f"运行异常: {e}") + + +if __name__ == "__main__": + # 使用你提供的 hash_id + monitor = DouyuYubaMonitor("PDAP2zEk3nwx") + monitor.fetch_and_process() \ No newline at end of file