├── __init__.py ├── LICENSE ├── README.md └── SearchMusic.py /__init__.py: -------------------------------------------------------------------------------- 1 | from .SearchMusic import * -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Lingyuzhou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SearchMusic 2 | 3 | ## 基本信息 4 | 插件名称:SearchMusic 5 | 作者:Lingyuzhou 6 | 版本:3.0 7 | 8 | 感谢好朋友“標”提供的代码支持(微信号:tkekingcom) 9 | 10 | ## 插件更新日志 11 | # v3.0 12 | - 🎴 新增音乐分享卡片功能,支持显示歌曲封面 13 | - 🔄 替换了失效的神秘音乐API,改为使用汽水音乐API 14 | - 🎵 新增随机点歌/听歌功能,支持音乐卡片和语音消息 15 | 16 | # v2.0 17 | - 🎵 新增直接播放功能,支持在聊天中播放音乐 18 | - 💾 新增了临时文件管理(通过gewechat_channel.py实现) 19 | 20 | # v1.0 21 | - 🎵 支持三大音乐平台:酷狗、网易、神秘音乐 22 | - 🔍 支持音乐搜索和播放链接获取 23 | 24 | ## 使用示例 25 | ![image](https://github.com/user-attachments/assets/a0395607-325a-4516-9c79-13449447b41b) 26 | 27 | ![image](https://github.com/user-attachments/assets/358998e0-eb65-4456-af5f-5ed3c8e1d23c) 28 | 29 | ![image](https://github.com/user-attachments/assets/a71ef877-f776-4289-956e-787a77312156) 30 | 31 | 32 | ## 使用方法 33 | 34 | ### 1. 酷狗音乐 35 | - 搜索歌曲:发送 酷狗点歌 歌曲名称 36 | - 音乐卡片:发送 酷狗点歌 歌曲名称 序号(返回音乐卡片) 37 | - 语音播放:发送 酷狗听歌 歌曲名称 序号(返回语音消息) 38 | 39 | ### 2. 网易音乐 40 | - 搜索歌曲:发送 网易点歌 歌曲名称 41 | - 音乐卡片:发送 网易点歌 歌曲名称 序号(返回音乐卡片) 42 | - 语音播放:发送 网易听歌 歌曲名称 序号(返回语音消息) 43 | 44 | ### 3. 汽水音乐 45 | - 搜索歌曲:发送 汽水点歌 歌曲名称 46 | - 音乐卡片:发送 汽水点歌 歌曲名称 序号(返回音乐卡片) 47 | (由于第三方接口的域名限制,汽水点歌音乐卡片暂不支持歌曲封面显示) 48 | - 语音播放:发送 汽水听歌 歌曲名称 序号(返回语音消息) 49 | 50 | ### 4. 随机歌单 51 | - 音乐卡片:发送 随机点歌(返回音乐卡片) 52 | - 语音播放:发送 随机听歌(返回语音消息) 53 | 54 | ## 安装和使用教程 55 | 该插件需要在最新版dify-on-wechat基础之上修改gewechat代码,详细教程请查看文档 56 | https://rq4rfacax27.feishu.cn/wiki/L4zFwQmbKiZezlkQ26jckBkcnod?fromScene=spaceOverview 57 | -------------------------------------------------------------------------------- /SearchMusic.py: -------------------------------------------------------------------------------- 1 | # encoding:utf-8 2 | import json 3 | import requests 4 | import re 5 | import os 6 | import time 7 | import plugins 8 | from bridge.context import ContextType 9 | from bridge.reply import Reply, ReplyType 10 | from common.log import logger 11 | from common.tmp_dir import TmpDir 12 | from plugins import * 13 | import random 14 | import urllib.parse 15 | 16 | @plugins.register( 17 | name="SearchMusic", 18 | desire_priority=100, 19 | desc="输入关键词'点歌 歌曲名称'即可获取对应歌曲详情和播放链接", 20 | version="3.0", 21 | author="Lingyuzhou", 22 | ) 23 | class SearchMusic(Plugin): 24 | def __init__(self): 25 | super().__init__() 26 | self.handlers[Event.ON_HANDLE_CONTEXT] = self.on_handle_context 27 | logger.info("[SearchMusic] inited.") 28 | 29 | def construct_music_appmsg(self, title, singer, url, thumb_url="", platform=""): 30 | """ 31 | 构造音乐分享卡片的appmsg XML 32 | :param title: 音乐标题 33 | :param singer: 歌手名 34 | :param url: 音乐播放链接 35 | :param thumb_url: 封面图片URL(可选) 36 | :param platform: 音乐平台(酷狗/网易/抖音) 37 | :return: appmsg XML字符串 38 | """ 39 | # 处理封面URL 40 | if thumb_url: 41 | # 不再移除抖音图片URL的后缀 42 | # 只确保URL是以http或https开头的 43 | if not thumb_url.startswith(("http://", "https://")): 44 | thumb_url = "https://" + thumb_url.lstrip("/") 45 | 46 | # 确保URL没有特殊字符 47 | thumb_url = thumb_url.replace("&", "&") 48 | 49 | # 根据平台在标题中添加前缀 50 | if platform.lower() == "kugou": 51 | display_title = f"[酷狗] {title}" 52 | source_display_name = "酷狗音乐" 53 | elif platform.lower() == "netease": 54 | display_title = f"[网易] {title}" 55 | source_display_name = "网易云音乐" 56 | elif platform.lower() == "qishui": 57 | display_title = f"[汽水] {title}" 58 | source_display_name = "汽水音乐" 59 | elif platform.lower() == "kuwo": 60 | display_title = f"[酷我] {title}" 61 | source_display_name = "酷我音乐" 62 | else: 63 | display_title = title 64 | source_display_name = "音乐分享" 65 | 66 | # 确保URL没有特殊字符 67 | url = url.replace("&", "&") 68 | 69 | # 使用更简化的XML结构,但保留关键标签 70 | xml = f""" 71 | {display_title} 72 | {singer} 73 | view 74 | 3 75 | 0 76 | 0 77 | 音乐 78 | 79 | 80 | 0 81 | {url} 82 | {url} 83 | {url} 84 | {url} 85 | 86 | 0 87 | 88 | 89 | 90 | {thumb_url} 91 | 92 | 93 | 94 | 95 | 96 | {source_display_name} 97 | {thumb_url} 98 | {thumb_url} 99 | 100 | """ 101 | 102 | # 记录生成的XML,便于调试 103 | logger.debug(f"[SearchMusic] 生成的音乐卡片XML: {xml}") 104 | 105 | return xml 106 | 107 | def get_music_cover(self, platform, detail_url, song_name="", singer=""): 108 | """ 109 | 尝试获取歌曲封面图片URL 110 | :param platform: 平台名称(kugou, netease, qishui, kuwo等) 111 | :param detail_url: 详情页URL(可选) 112 | :param song_name: 歌曲名称(可选,用于备用搜索) 113 | :param singer: 歌手名称(可选,用于备用搜索) 114 | :return: 封面图片URL 115 | """ 116 | default_cover = "https://p2.music.126.net/tGHU62DTszbFQ37W9qPHcw==/2002210674180197.jpg" 117 | 118 | try: 119 | # 根据平台选择不同的获取方式 120 | if platform == "kugou": 121 | # 尝试从酷狗音乐详情页获取封面 122 | try: 123 | if detail_url: 124 | response = requests.get(detail_url, timeout=10) 125 | # 使用正则表达式提取封面URL 126 | cover_pattern = r'' 127 | match = re.search(cover_pattern, response.text) 128 | if match: 129 | cover_url = match.group(1) 130 | logger.info(f"[SearchMusic] 从酷狗音乐详情页提取到封面: {cover_url}") 131 | return cover_url 132 | except Exception as e: 133 | logger.error(f"[SearchMusic] 从酷狗音乐详情页获取封面时出错: {e}") 134 | 135 | # 如果从详情页获取失败,尝试使用备用方法 136 | if song_name and singer: 137 | try: 138 | # 使用备用API 139 | backup_url = f"https://mobilecdn.kugou.com/api/v3/search/song?keyword={song_name}%20{singer}&page=1&pagesize=1" 140 | response = requests.get(backup_url, timeout=10) 141 | data = response.json() 142 | if data["status"] == 1 and data["data"]["total"] > 0: 143 | song_info = data["data"]["info"][0] 144 | hash_value = song_info["hash"] 145 | album_id = song_info.get("album_id", "") 146 | if album_id: 147 | cover_url = f"https://imge.kugou.com/stdmusic/{album_id}.jpg" 148 | logger.info(f"[SearchMusic] 使用酷狗音乐API获取到封面: {cover_url}") 149 | return cover_url 150 | except Exception as e: 151 | logger.error(f"[SearchMusic] 使用酷狗音乐API获取封面时出错: {e}") 152 | 153 | elif platform == "qishui": 154 | # 汽水音乐封面已在API响应中提供,这里不需要额外处理 155 | # 如果需要备用方法,可以在这里添加 156 | pass 157 | 158 | elif platform == "kuwo": 159 | # 尝试从酷我音乐API获取封面 160 | try: 161 | if song_name and singer: 162 | # 使用酷我音乐API搜索歌曲 163 | search_url = f"https://api.suyanw.cn/api/kw.php?msg={song_name}" 164 | response = requests.get(search_url, timeout=10) 165 | data = json.loads(response.text) 166 | if "data" in data and isinstance(data["data"], list) and len(data["data"]) > 0: 167 | # 查找匹配的歌曲 168 | for song in data["data"]: 169 | if "singer" in song and singer.lower() in song["singer"].lower(): 170 | if "pic" in song and song["pic"]: 171 | cover_url = song["pic"] 172 | logger.info(f"[SearchMusic] 使用酷我音乐API获取到封面: {cover_url}") 173 | return cover_url 174 | break 175 | except Exception as e: 176 | logger.error(f"[SearchMusic] 使用酷我音乐API获取封面时出错: {e}") 177 | 178 | elif platform == "netease": 179 | # 尝试从网易云音乐API获取封面 180 | try: 181 | if song_name and singer: 182 | # 使用网易云音乐API搜索歌曲 183 | search_url = f"https://music.163.com/api/search/get/web?csrf_token=hlpretag=&hlposttag=&s={song_name}&type=1&offset=0&total=true&limit=1" 184 | response = requests.get(search_url, timeout=10) 185 | data = response.json() 186 | if "result" in data and "songs" in data["result"] and len(data["result"]["songs"]) > 0: 187 | song_info = data["result"]["songs"][0] 188 | if "al" in song_info and "picUrl" in song_info["al"]: 189 | cover_url = song_info["al"]["picUrl"] 190 | logger.info(f"[SearchMusic] 使用网易云音乐API获取到封面: {cover_url}") 191 | return cover_url 192 | except Exception as e: 193 | logger.error(f"[SearchMusic] 使用网易云音乐API获取封面时出错: {e}") 194 | 195 | # 对于其他平台,尝试使用歌曲名称和歌手名称搜索封面 196 | if song_name and singer: 197 | # 尝试使用QQ音乐搜索API获取封面 198 | try: 199 | search_url = f"https://c.y.qq.com/soso/fcgi-bin/client_search_cp?w={urllib.parse.quote(f'{song_name} {singer}')}&format=json&p=1&n=1" 200 | response = requests.get(search_url, timeout=10) 201 | if response.status_code == 200: 202 | data = json.loads(response.text) 203 | if "data" in data and "song" in data["data"] and "list" in data["data"]["song"] and data["data"]["song"]["list"]: 204 | song_info = data["data"]["song"]["list"][0] 205 | if "albummid" in song_info: 206 | albummid = song_info["albummid"] 207 | cover_url = f"https://y.gtimg.cn/music/photo_new/T002R300x300M000{albummid}.jpg" 208 | logger.info(f"[SearchMusic] 使用QQ音乐API获取到封面: {cover_url}") 209 | return cover_url 210 | except Exception as e: 211 | logger.error(f"[SearchMusic] 使用QQ音乐API获取封面时出错: {e}") 212 | 213 | logger.warning(f"[SearchMusic] 无法获取封面图片,使用默认封面: {song_name} - {singer}") 214 | return default_cover 215 | 216 | except Exception as e: 217 | logger.error(f"[SearchMusic] 获取封面图片时出错: {e}") 218 | return default_cover 219 | 220 | def extract_cover_from_response(self, response_text): 221 | """ 222 | 从API返回的内容中提取封面图片URL 223 | :param response_text: API返回的文本内容 224 | :return: 封面图片URL或None 225 | """ 226 | try: 227 | # 尝试解析为JSON格式(汽水音乐API) 228 | try: 229 | data = json.loads(response_text) 230 | if "cover" in data and data["cover"]: 231 | cover_url = data["cover"] 232 | # 检查是否是抖音域名的图片 233 | if "douyinpic.com" in cover_url or "douyincdn.com" in cover_url: 234 | logger.warning(f"[SearchMusic] 检测到抖音域名图片,可能无法在微信中正常显示: {cover_url}") 235 | # 不再使用备用图片 236 | logger.info(f"[SearchMusic] 从JSON中提取到封面URL: {cover_url}") 237 | return cover_url 238 | except json.JSONDecodeError: 239 | # 不是JSON格式,继续使用文本解析方法 240 | pass 241 | 242 | # 查找 ±img=URL± 格式的封面图片(抖音API格式) 243 | img_pattern = r'±img=(https?://[^±]+)±' 244 | match = re.search(img_pattern, response_text) 245 | if match: 246 | cover_url = match.group(1) 247 | # 检查是否是抖音域名的图片 248 | if "douyinpic.com" in cover_url or "douyincdn.com" in cover_url: 249 | logger.warning(f"[SearchMusic] 检测到抖音域名图片,可能无法在微信中正常显示: {cover_url}") 250 | # 不再移除后缀,保留完整的URL 251 | logger.info(f"[SearchMusic] 从API响应中提取到封面图片: {cover_url}") 252 | return cover_url 253 | return None 254 | except Exception as e: 255 | logger.error(f"[SearchMusic] 提取封面图片时出错: {e}") 256 | return None 257 | 258 | def get_video_url(self, url): 259 | """ 260 | 验证视频URL是否有效并返回可用的视频链接 261 | :param url: 视频URL 262 | :return: 有效的视频URL或None 263 | """ 264 | try: 265 | response = requests.get(url) 266 | response.raise_for_status() 267 | content_type = response.headers.get('Content-Type') 268 | if 'video' in content_type: 269 | logger.debug("[SearchMusic] 视频内容已检测") 270 | return response.url 271 | return None 272 | except requests.exceptions.RequestException as e: 273 | logger.error(f"[SearchMusic] 请求视频URL失败: {e}") 274 | return None 275 | 276 | def download_music(self, music_url, platform): 277 | """ 278 | 下载音乐文件并返回文件路径 279 | :param music_url: 音乐文件URL 280 | :param platform: 平台名称(用于文件名) 281 | :return: 音乐文件保存路径或None(如果下载失败) 282 | """ 283 | try: 284 | # 检查URL是否有效 285 | if not music_url or not music_url.startswith('http'): 286 | logger.error(f"[SearchMusic] 无效的音乐URL: {music_url}") 287 | return None 288 | 289 | # 发送GET请求下载文件,添加超时和重试机制 290 | headers = { 291 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' 292 | } 293 | for retry in range(3): # 最多重试3次 294 | try: 295 | response = requests.get(music_url, stream=True, headers=headers, timeout=30) 296 | response.raise_for_status() # 检查响应状态 297 | break 298 | except requests.RequestException as e: 299 | if retry == 2: # 最后一次重试 300 | logger.error(f"[SearchMusic] 下载音乐文件失败,重试次数已用完: {e}") 301 | return None 302 | logger.warning(f"[SearchMusic] 下载重试 {retry + 1}/3: {e}") 303 | time.sleep(1) # 等待1秒后重试 304 | 305 | # 使用TmpDir().path()获取正确的临时目录 306 | tmp_dir = TmpDir().path() 307 | 308 | # 生成唯一的文件名,包含时间戳和随机字符串 309 | timestamp = int(time.time()) 310 | random_str = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz', k=6)) 311 | music_name = f"{platform}_music_{timestamp}_{random_str}.mp3" 312 | music_path = os.path.join(tmp_dir, music_name) 313 | 314 | # 保存文件,使用块写入以节省内存 315 | total_size = 0 316 | with open(music_path, "wb") as file: 317 | for chunk in response.iter_content(chunk_size=8192): 318 | if chunk: 319 | file.write(chunk) 320 | total_size += len(chunk) 321 | 322 | # 验证文件大小 323 | if total_size == 0: 324 | logger.error("[SearchMusic] 下载的文件大小为0") 325 | os.remove(music_path) # 删除空文件 326 | return None 327 | 328 | logger.info(f"[SearchMusic] 音乐下载完成: {music_path}, 大小: {total_size/1024:.2f}KB") 329 | return music_path 330 | 331 | except Exception as e: 332 | logger.error(f"[SearchMusic] 下载音乐文件时出错: {e}") 333 | # 如果文件已创建,清理它 334 | if 'music_path' in locals() and os.path.exists(music_path): 335 | try: 336 | os.remove(music_path) 337 | except Exception as clean_error: 338 | logger.error(f"[SearchMusic] 清理失败的下载文件时出错: {clean_error}") 339 | return None 340 | 341 | def on_handle_context(self, e_context: EventContext): 342 | if e_context["context"].type != ContextType.TEXT: 343 | return 344 | 345 | content = e_context["context"].content 346 | reply = Reply() 347 | reply.type = ReplyType.TEXT 348 | 349 | # 处理随机点歌命令 350 | if content.strip() == "随机点歌": 351 | url = "https://hhlqilongzhu.cn/api/wangyi_hot_review.php" 352 | try: 353 | response = requests.get(url, timeout=10) 354 | if response.status_code == 200: 355 | try: 356 | data = json.loads(response.text) 357 | if "code" in data and data["code"] == 200: 358 | # 提取歌曲信息 359 | title = data.get("song", "未知歌曲") 360 | singer = data.get("singer", "未知歌手") 361 | music_url = data.get("url", "") 362 | thumb_url = data.get("img", "") 363 | link = data.get("link", "") 364 | 365 | # 记录获取到的随机歌曲信息 366 | logger.info(f"[SearchMusic] 随机点歌获取成功: {title} - {singer}") 367 | 368 | # 构造音乐分享卡片 369 | appmsg = self.construct_music_appmsg(title, singer, music_url, thumb_url, "netease") 370 | 371 | # 返回APP消息类型 372 | reply.type = ReplyType.APP 373 | reply.content = appmsg 374 | else: 375 | reply.content = "随机点歌失败,请稍后重试" 376 | except json.JSONDecodeError: 377 | logger.error(f"[SearchMusic] 随机点歌API返回的不是有效的JSON: {response.text[:100]}...") 378 | reply.content = "随机点歌失败,请稍后重试" 379 | else: 380 | reply.content = "随机点歌失败,请稍后重试" 381 | except Exception as e: 382 | logger.error(f"[SearchMusic] 随机点歌错误: {e}") 383 | reply.content = "随机点歌失败,请稍后重试" 384 | 385 | # 处理随机听歌命令 386 | elif content.strip() == "随机听歌": 387 | url = "https://hhlqilongzhu.cn/api/wangyi_hot_review.php" 388 | try: 389 | response = requests.get(url, timeout=10) 390 | if response.status_code == 200: 391 | try: 392 | data = json.loads(response.text) 393 | if "code" in data and data["code"] == 200: 394 | # 提取歌曲信息 395 | title = data.get("song", "未知歌曲") 396 | singer = data.get("singer", "未知歌手") 397 | music_url = data.get("url", "") 398 | 399 | # 记录获取到的随机歌曲信息 400 | logger.info(f"[SearchMusic] 随机听歌获取成功: {title} - {singer}") 401 | 402 | # 下载音乐文件 403 | music_path = self.download_music(music_url, "netease") 404 | 405 | if music_path: 406 | # 返回语音消息 407 | reply.type = ReplyType.VOICE 408 | reply.content = music_path 409 | else: 410 | reply.type = ReplyType.TEXT 411 | reply.content = "音乐文件下载失败,请稍后重试" 412 | else: 413 | reply.content = "随机听歌失败,请稍后重试" 414 | except json.JSONDecodeError: 415 | logger.error(f"[SearchMusic] 随机听歌API返回的不是有效的JSON: {response.text[:100]}...") 416 | reply.content = "随机听歌失败,请稍后重试" 417 | else: 418 | reply.content = "随机听歌失败,请稍后重试" 419 | except Exception as e: 420 | logger.error(f"[SearchMusic] 随机听歌错误: {e}") 421 | reply.content = "随机听歌失败,请稍后重试" 422 | 423 | # 处理酷狗点歌命令(搜索歌曲列表) 424 | elif content.startswith("酷狗点歌 "): 425 | song_name = content[5:].strip() # 去除多余空格 426 | if not song_name: 427 | reply.content = "请输入要搜索的歌曲名称" 428 | e_context["reply"] = reply 429 | e_context.action = EventAction.BREAK_PASS 430 | return 431 | 432 | # 检查是否包含序号(新增的详情获取功能) 433 | params = song_name.split() 434 | if len(params) == 2 and params[1].isdigit(): 435 | song_name, song_number = params 436 | url = f"https://www.hhlqilongzhu.cn/api/dg_kgmusic.php?gm={song_name}&n={song_number}" 437 | try: 438 | response = requests.get(url, timeout=10) 439 | content = response.text 440 | song_info = content.split('\n') 441 | 442 | if len(song_info) >= 4: # 确保有足够的信息行 443 | # 提取歌曲信息 444 | title = song_info[1].replace("歌名:", "").strip() 445 | singer = song_info[2].replace("歌手:", "").strip() 446 | detail_url = song_info[3].replace("歌曲详情页:", "").strip() 447 | music_url = song_info[4].replace("播放链接:", "").strip() 448 | 449 | # 尝试从响应中提取封面图片URL 450 | thumb_url = self.extract_cover_from_response(content) 451 | 452 | # 如果从响应中没有提取到封面,尝试从详情页获取 453 | if not thumb_url: 454 | thumb_url = self.get_music_cover("kugou", detail_url, title, singer) 455 | 456 | # 构造音乐分享卡片 457 | appmsg = self.construct_music_appmsg(title, singer, music_url, thumb_url, "kugou") 458 | 459 | # 返回APP消息类型 460 | reply.type = ReplyType.APP 461 | reply.content = appmsg 462 | else: 463 | reply.content = "未找到该歌曲,请确认歌名和序号是否正确" 464 | except Exception as e: 465 | logger.error(f"[SearchMusic] 酷狗点歌详情错误: {e}") 466 | reply.content = "获取失败,请稍后重试" 467 | else: 468 | # 原有的搜索歌曲列表功能 469 | url = f"https://www.hhlqilongzhu.cn/api/dg_kgmusic.php?gm={song_name}&n=" 470 | try: 471 | response = requests.get(url, timeout=10) 472 | songs = response.text.strip().split('\n') 473 | if songs and len(songs) > 1: # 确保有搜索结果 474 | reply_content = " 为你在酷狗音乐库中找到以下歌曲:\n\n" 475 | for song in songs: 476 | if song.strip(): # 确保不是空行 477 | reply_content += f"{song}\n" 478 | reply_content += f"\n请发送「酷狗点歌 {song_name} 序号」获取歌曲详情\n或发送「酷狗听歌 {song_name} 序号」来播放对应歌曲" 479 | else: 480 | reply_content = "未找到相关歌曲,请换个关键词试试" 481 | reply.content = reply_content 482 | except Exception as e: 483 | logger.error(f"[SearchMusic] 酷狗点歌错误: {e}") 484 | reply.content = "搜索失败,请稍后重试" 485 | 486 | # 处理网易点歌命令(搜索歌曲列表) 487 | elif content.startswith("网易点歌 "): 488 | song_name = content[5:].strip() 489 | if not song_name: 490 | reply.content = "请输入要搜索的歌曲名称" 491 | e_context["reply"] = reply 492 | e_context.action = EventAction.BREAK_PASS 493 | return 494 | 495 | # 检查是否包含序号(新增的详情获取功能) 496 | params = song_name.split() 497 | if len(params) == 2 and params[1].isdigit(): 498 | song_name, song_number = params 499 | url = f"https://www.hhlqilongzhu.cn/api/dg_wyymusic.php?gm={song_name}&n={song_number}" 500 | try: 501 | response = requests.get(url, timeout=10) 502 | content = response.text 503 | song_info = content.split('\n') 504 | 505 | if len(song_info) >= 4: # 确保有足够的信息行 506 | # 提取歌曲信息 507 | title = song_info[1].replace("歌名:", "").strip() 508 | singer = song_info[2].replace("歌手:", "").strip() 509 | detail_url = song_info[3].replace("歌曲详情页:", "").strip() 510 | music_url = song_info[4].replace("播放链接:", "").strip() 511 | 512 | # 尝试从响应中提取封面图片URL 513 | thumb_url = self.extract_cover_from_response(content) 514 | 515 | # 如果从响应中没有提取到封面,尝试从详情页获取 516 | if not thumb_url: 517 | thumb_url = self.get_music_cover("netease", detail_url, title, singer) 518 | 519 | # 构造音乐分享卡片 520 | appmsg = self.construct_music_appmsg(title, singer, music_url, thumb_url, "netease") 521 | 522 | # 返回APP消息类型 523 | reply.type = ReplyType.APP 524 | reply.content = appmsg 525 | else: 526 | reply.content = "未找到该歌曲,请确认歌名和序号是否正确" 527 | except Exception as e: 528 | logger.error(f"[SearchMusic] 网易点歌详情错误: {e}") 529 | reply.content = "获取失败,请稍后重试" 530 | else: 531 | # 原有的搜索歌曲列表功能 532 | url = f"https://www.hhlqilongzhu.cn/api/dg_wyymusic.php?gm={song_name}&n=&num=20" 533 | try: 534 | response = requests.get(url, timeout=10) 535 | songs = response.text.strip().split('\n') 536 | if songs and len(songs) > 1: # 确保有搜索结果 537 | reply_content = " 为你在网易音乐库中找到以下歌曲:\n\n" 538 | for song in songs: 539 | if song.strip(): # 确保不是空行 540 | reply_content += f"{song}\n" 541 | reply_content += f"\n请发送「网易点歌 {song_name} 序号」获取歌曲详情\n或发送「网易听歌 {song_name} 序号」来播放对应歌曲" 542 | else: 543 | reply_content = "未找到相关歌曲,请换个关键词试试" 544 | reply.content = reply_content 545 | except Exception as e: 546 | logger.error(f"[SearchMusic] 网易点歌错误: {e}") 547 | reply.content = "搜索失败,请稍后重试" 548 | 549 | # 处理汽水点歌命令 550 | elif content.startswith("汽水点歌 "): 551 | song_name = content[5:].strip() 552 | 553 | if not song_name: 554 | reply.content = "请输入要搜索的歌曲名称" 555 | e_context["reply"] = reply 556 | e_context.action = EventAction.BREAK_PASS 557 | return 558 | 559 | # 检查是否包含序号(详情获取功能) 560 | params = song_name.split() 561 | if len(params) == 2 and params[1].isdigit(): 562 | song_name, song_number = params 563 | url = f"https://hhlqilongzhu.cn/api/dg_qishuimusic.php?msg={song_name}&n={song_number}" 564 | try: 565 | response = requests.get(url, timeout=10) 566 | content = response.text 567 | 568 | # 尝试解析JSON响应 569 | try: 570 | data = json.loads(content) 571 | if "title" in data and "singer" in data and "music" in data: 572 | title = data["title"] 573 | singer = data["singer"] 574 | music_url = data["music"] 575 | 576 | # 提取封面图片URL 577 | thumb_url = "" 578 | if "cover" in data and data["cover"]: 579 | thumb_url = data["cover"] 580 | # 检查是否是抖音域名的图片 581 | if "douyinpic.com" in thumb_url or "douyincdn.com" in thumb_url: 582 | logger.warning(f"[SearchMusic] 汽水点歌检测到抖音域名图片,可能无法在微信中正常显示: {thumb_url}") 583 | # 不再使用备用图片 584 | thumb_url = thumb_url 585 | 586 | # 如果没有提取到封面,尝试从详情页获取 587 | if not thumb_url: 588 | thumb_url = self.get_music_cover("qishui", "", title, singer) 589 | 590 | # 记录封面URL信息,便于调试 591 | logger.info(f"[SearchMusic] 汽水点歌封面URL: {thumb_url}") 592 | 593 | # 构造音乐分享卡片 594 | appmsg = self.construct_music_appmsg(title, singer, music_url, thumb_url, "qishui") 595 | 596 | # 返回APP消息类型 597 | reply.type = ReplyType.APP 598 | reply.content = appmsg 599 | else: 600 | reply.content = "未找到该歌曲,请确认歌名和序号是否正确" 601 | except json.JSONDecodeError: 602 | logger.error(f"[SearchMusic] 汽水音乐API返回的不是有效的JSON: {content[:100]}...") 603 | reply.content = "获取失败,请稍后重试" 604 | 605 | except Exception as e: 606 | logger.error(f"[SearchMusic] 汽水点歌详情错误: {e}") 607 | reply.content = "获取失败,请稍后重试" 608 | else: 609 | # 搜索歌曲列表功能 610 | url = f"https://hhlqilongzhu.cn/api/dg_qishuimusic.php?msg={song_name}" 611 | try: 612 | response = requests.get(url, timeout=10) 613 | content = response.text.strip() 614 | 615 | # 尝试解析JSON响应 616 | try: 617 | data = json.loads(content) 618 | # 检查是否返回了歌曲列表 619 | if "data" in data and isinstance(data["data"], list) and len(data["data"]) > 0: 620 | # 新格式:包含完整歌曲列表的JSON 621 | reply_content = " 为你在汽水音乐库中找到以下歌曲:\n\n" 622 | for song in data["data"]: 623 | if "n" in song and "title" in song and "singer" in song: 624 | reply_content += f"{song['n']}. {song['title']} - {song['singer']}\n" 625 | 626 | reply_content += f"\n请发送「汽水点歌 {song_name} 序号」获取歌曲详情\n或发送「汽水听歌 {song_name} 序号」来播放对应歌曲" 627 | elif "title" in data and "singer" in data: 628 | # 旧格式:只返回单个歌曲的JSON 629 | reply_content = " 为你在汽水音乐库中找到以下歌曲:\n\n" 630 | reply_content += f"1. {data['title']} - {data['singer']}\n" 631 | reply_content += f"\n请发送「汽水点歌 {song_name} 1」获取歌曲详情\n或发送「汽水听歌 {song_name} 1」来播放对应歌曲" 632 | else: 633 | reply_content = "未找到相关歌曲,请换个关键词试试" 634 | except json.JSONDecodeError: 635 | # 如果不是JSON,尝试使用正则表达式解析文本格式的结果 636 | pattern = r"(\d+)\.\s+(.*?)\s+-\s+(.*?)$" 637 | matches = re.findall(pattern, content, re.MULTILINE) 638 | 639 | if matches: 640 | reply_content = " 为你在汽水音乐库中找到以下歌曲:\n\n" 641 | for match in matches: 642 | number, title, singer = match 643 | reply_content += f"{number}. {title} - {singer}\n" 644 | 645 | reply_content += f"\n请发送「汽水点歌 {song_name} 序号」获取歌曲详情\n或发送「汽水听歌 {song_name} 序号」来播放对应歌曲" 646 | else: 647 | logger.error(f"[SearchMusic] 汽水音乐API返回格式无法解析: {content[:100]}...") 648 | reply_content = "搜索结果解析失败,请稍后重试" 649 | 650 | reply.content = reply_content 651 | except Exception as e: 652 | logger.error(f"[SearchMusic] 汽水点歌错误: {e}") 653 | reply.content = "搜索失败,请稍后重试" 654 | 655 | 656 | # 处理酷狗听歌命令 657 | elif content.startswith("酷狗听歌 "): 658 | params = content[5:].strip().split() 659 | if len(params) != 2: 660 | reply.content = "请输入正确的格式:酷狗听歌 歌曲名称 序号" 661 | e_context["reply"] = reply 662 | e_context.action = EventAction.BREAK_PASS 663 | return 664 | 665 | song_name, song_number = params 666 | if not song_number.isdigit(): 667 | reply.content = "请输入正确的歌曲序号(纯数字)" 668 | e_context["reply"] = reply 669 | e_context.action = EventAction.BREAK_PASS 670 | return 671 | 672 | url = f"https://www.hhlqilongzhu.cn/api/dg_kgmusic.php?gm={song_name}&n={song_number}" 673 | 674 | try: 675 | response = requests.get(url, timeout=10) 676 | content = response.text 677 | song_info = content.split('\n') 678 | 679 | if len(song_info) >= 4: # 确保有足够的信息行 680 | # 获取音乐文件URL(在第4行),并去除可能的"播放链接:"前缀 681 | music_url = song_info[4].strip() 682 | if "播放链接:" in music_url: 683 | music_url = music_url.split("播放链接:")[1].strip() 684 | 685 | # 下载音乐文件 686 | music_path = self.download_music(music_url, "kugou") 687 | 688 | if music_path: 689 | # 返回语音消息 690 | reply.type = ReplyType.VOICE 691 | reply.content = music_path 692 | else: 693 | reply.type = ReplyType.TEXT 694 | reply.content = "音乐文件下载失败,请稍后重试" 695 | else: 696 | reply.content = "未找到该歌曲,请确认歌名和序号是否正确" 697 | 698 | except Exception as e: 699 | logger.error(f"[SearchMusic] 酷狗听歌错误: {e}") 700 | reply.content = "获取失败,请稍后重试" 701 | 702 | # 处理网易听歌命令 703 | elif content.startswith("网易听歌 "): 704 | params = content[5:].strip().split() 705 | if len(params) != 2: 706 | reply.content = "请输入正确的格式:网易听歌 歌曲名称 序号" 707 | e_context["reply"] = reply 708 | e_context.action = EventAction.BREAK_PASS 709 | return 710 | 711 | song_name, song_number = params 712 | if not song_number.isdigit(): 713 | reply.content = "请输入正确的歌曲序号(纯数字)" 714 | e_context["reply"] = reply 715 | e_context.action = EventAction.BREAK_PASS 716 | return 717 | 718 | url = f"https://www.hhlqilongzhu.cn/api/dg_wyymusic.php?gm={song_name}&n={song_number}" 719 | 720 | try: 721 | response = requests.get(url, timeout=10) 722 | content = response.text 723 | 724 | # 解析返回内容 725 | song_info = content.split('\n') 726 | 727 | if len(song_info) >= 4: # 确保有足够的信息行 728 | # 获取音乐文件URL(在第4行),并去除可能的"播放链接:"前缀 729 | music_url = song_info[4].strip() 730 | if "播放链接:" in music_url: 731 | music_url = music_url.split("播放链接:")[1].strip() 732 | 733 | # 下载音乐文件 734 | music_path = self.download_music(music_url, "netease") 735 | 736 | if music_path: 737 | # 返回语音消息 738 | reply.type = ReplyType.VOICE 739 | reply.content = music_path 740 | else: 741 | reply.type = ReplyType.TEXT 742 | reply.content = "音乐文件下载失败,请稍后重试" 743 | else: 744 | reply.content = "未找到该歌曲,请确认歌名和序号是否正确" 745 | 746 | except Exception as e: 747 | logger.error(f"[SearchMusic] 网易听歌错误: {e}") 748 | reply.content = "获取失败,请稍后重试" 749 | 750 | # 处理酷我点歌命令 751 | elif content.startswith("酷我点歌 "): 752 | song_name = content[5:].strip() 753 | 754 | if not song_name: 755 | reply.content = "请输入要搜索的歌曲名称" 756 | e_context["reply"] = reply 757 | e_context.action = EventAction.BREAK_PASS 758 | return 759 | 760 | # 检查是否包含序号(详情获取功能) 761 | params = song_name.split() 762 | if len(params) == 2 and params[1].isdigit(): 763 | song_name, song_number = params 764 | url = f"https://hhlqilongzhu.cn/api/dg_kuwomusic.php?msg={song_name}&n={song_number}" 765 | try: 766 | response = requests.get(url, timeout=10) 767 | content = response.text 768 | 769 | # 解析文本格式的响应 770 | song_info = content.split('\n') 771 | 772 | if len(song_info) >= 4: # 确保有足够的信息行 773 | # 提取歌曲信息 774 | thumb_url = "" 775 | title = "" 776 | singer = "" 777 | music_url = "" 778 | 779 | # 解析每一行信息 780 | for line in song_info: 781 | line = line.strip() 782 | if line.startswith("±img="): 783 | thumb_url = line.replace("±img=", "").replace("±", "").strip() 784 | elif line.startswith("歌名:"): 785 | title = line.replace("歌名:", "").strip() 786 | elif line.startswith("歌手:"): 787 | singer = line.replace("歌手:", "").strip() 788 | elif line.startswith("播放链接:"): 789 | music_url = line.replace("播放链接:", "").strip() 790 | 791 | if title and singer and music_url: 792 | # 记录歌曲信息,便于调试 793 | logger.info(f"[SearchMusic] 酷我点歌信息: {title} - {singer}, 封面: {thumb_url}, URL: {music_url}") 794 | 795 | # 构造音乐分享卡片 796 | appmsg = self.construct_music_appmsg(title, singer, music_url, thumb_url, "kuwo") 797 | 798 | # 返回APP消息类型 799 | reply.type = ReplyType.APP 800 | reply.content = appmsg 801 | else: 802 | reply.content = "解析歌曲信息失败,请稍后重试" 803 | else: 804 | reply.content = "未找到该歌曲,请确认歌名和序号是否正确" 805 | except Exception as e: 806 | logger.error(f"[SearchMusic] 酷我点歌详情错误: {e}") 807 | reply.content = "获取失败,请稍后重试" 808 | else: 809 | # 搜索歌曲列表功能 810 | url = f"https://hhlqilongzhu.cn/api/dg_kuwomusic.php?msg={song_name}" 811 | try: 812 | response = requests.get(url, timeout=10) 813 | content = response.text.strip() 814 | 815 | # 解析返回的歌曲列表 816 | songs = content.strip().split('\n') 817 | if songs and len(songs) > 0: 818 | reply_content = " 为你在酷我音乐库中找到以下歌曲:\n\n" 819 | for song in songs: 820 | if song.strip(): 821 | reply_content += f"{song}\n" 822 | 823 | reply_content += f"\n请发送「酷我点歌 {song_name} 序号」获取歌曲详情\n或发送「酷我听歌 {song_name} 序号」来播放对应歌曲" 824 | else: 825 | reply_content = "未找到相关歌曲,请换个关键词试试" 826 | 827 | reply.content = reply_content 828 | except Exception as e: 829 | logger.error(f"[SearchMusic] 酷我点歌错误: {e}") 830 | reply.content = "搜索失败,请稍后重试" 831 | 832 | # 处理汽水听歌命令 833 | elif content.startswith("汽水听歌 "): 834 | params = content[5:].strip().split() 835 | if len(params) != 2: 836 | reply.content = "请输入正确的格式:汽水听歌 歌曲名称 序号" 837 | e_context["reply"] = reply 838 | e_context.action = EventAction.BREAK_PASS 839 | return 840 | 841 | song_name, song_number = params 842 | if not song_number.isdigit(): 843 | reply.content = "请输入正确的歌曲序号(纯数字)" 844 | e_context["reply"] = reply 845 | e_context.action = EventAction.BREAK_PASS 846 | return 847 | 848 | url = f"https://hhlqilongzhu.cn/api/dg_qishuimusic.php?msg={song_name}&n={song_number}" 849 | 850 | try: 851 | response = requests.get(url, timeout=10) 852 | content = response.text 853 | 854 | # 尝试解析JSON响应 855 | try: 856 | data = json.loads(content) 857 | if "music" in data and data["music"]: 858 | music_url = data["music"] 859 | 860 | # 下载音乐文件 861 | music_path = self.download_music(music_url, "qishui") 862 | 863 | if music_path: 864 | # 返回语音消息 865 | reply.type = ReplyType.VOICE 866 | reply.content = music_path 867 | else: 868 | reply.type = ReplyType.TEXT 869 | reply.content = "音乐文件下载失败,请稍后重试" 870 | else: 871 | reply.content = "未找到该歌曲的播放链接,请确认歌名和序号是否正确" 872 | except json.JSONDecodeError: 873 | logger.error(f"[SearchMusic] 汽水音乐API返回的不是有效的JSON: {content[:100]}...") 874 | reply.content = "获取失败,请稍后重试" 875 | 876 | except Exception as e: 877 | logger.error(f"[SearchMusic] 汽水听歌错误: {e}") 878 | reply.content = "获取失败,请稍后重试" 879 | 880 | # 处理酷我听歌命令 881 | elif content.startswith("酷我听歌 "): 882 | params = content[5:].strip().split() 883 | if len(params) != 2: 884 | reply.content = "请输入正确的格式:酷我听歌 歌曲名称 序号" 885 | e_context["reply"] = reply 886 | e_context.action = EventAction.BREAK_PASS 887 | return 888 | 889 | song_name, song_number = params 890 | if not song_number.isdigit(): 891 | reply.content = "请输入正确的歌曲序号(纯数字)" 892 | e_context["reply"] = reply 893 | e_context.action = EventAction.BREAK_PASS 894 | return 895 | 896 | url = f"https://hhlqilongzhu.cn/api/dg_kuwomusic.php?msg={song_name}&n={song_number}" 897 | 898 | try: 899 | response = requests.get(url, timeout=10) 900 | content = response.text 901 | 902 | # 尝试解析JSON响应 903 | try: 904 | data = json.loads(content) 905 | if "url" in data and data["url"]: 906 | music_url = data["url"] 907 | 908 | # 下载音乐文件 909 | music_path = self.download_music(music_url, "kuwo") 910 | 911 | if music_path: 912 | # 返回语音消息 913 | reply.type = ReplyType.VOICE 914 | reply.content = music_path 915 | else: 916 | reply.type = ReplyType.TEXT 917 | reply.content = "音乐文件下载失败,请稍后重试" 918 | else: 919 | reply.content = "未找到该歌曲的播放链接,请确认歌名和序号是否正确" 920 | except json.JSONDecodeError: 921 | # 如果不是JSON,尝试解析文本格式 922 | logger.info(f"[SearchMusic] 酷我音乐API返回文本格式响应,尝试解析: {content[:100]}...") 923 | 924 | # 解析文本格式的响应 925 | song_info = content.split('\n') 926 | music_url = "" 927 | 928 | for line in song_info: 929 | line = line.strip() 930 | if line.startswith("播放链接:") or "播放链接:" in line: 931 | # 提取播放链接,可能包含在标签中 932 | if " 0: 1021 | # 包含完整MV列表的JSON 1022 | reply_content = " 为你在酷狗MV库中找到以下视频:\n\n" 1023 | 1024 | # 为每个MV添加序号 1025 | for i, mv in enumerate(data["data"], 1): 1026 | if "name" in mv and "singer" in mv: 1027 | reply_content += f"{i}. {mv['name']} - {mv['singer']}\n" 1028 | 1029 | reply_content += f"\n请发送「酷狗MV {song_name} 序号」获取对应MV视频" 1030 | else: 1031 | reply_content = "未找到相关MV,请换个关键词试试" 1032 | except json.JSONDecodeError: 1033 | logger.error(f"[SearchMusic] 酷狗MV API返回的不是有效的JSON: {content[:100]}...") 1034 | reply_content = "搜索结果解析失败,请稍后重试" 1035 | 1036 | reply.content = reply_content 1037 | except Exception as e: 1038 | logger.error(f"[SearchMusic] 酷狗MV搜索错误: {e}") 1039 | reply.content = "搜索失败,请稍后重试" 1040 | 1041 | else: 1042 | return 1043 | 1044 | e_context["reply"] = reply 1045 | e_context.action = EventAction.BREAK_PASS 1046 | 1047 | def get_help_text(self, **kwargs): 1048 | return ( 1049 | " 音乐搜索和播放功能:\n\n" 1050 | "1. 酷狗音乐:\n" 1051 | " - 搜索歌单:发送「酷狗点歌 歌曲名称」\n" 1052 | " - 音乐卡片:发送「酷狗点歌 歌曲名称 序号」\n" 1053 | " - 视频播放:发送「酷狗MV 歌曲名称」搜索MV,发送「酷狗MV 歌曲名称 序号」获取MV详情\n" 1054 | " - 语音播放:发送「酷狗听歌 歌曲名称 序号」\n" 1055 | "2. 网易音乐:\n" 1056 | " - 搜索歌单:发送「网易点歌 歌曲名称」\n" 1057 | " - 音乐卡片:发送「网易点歌 歌曲名称 序号」\n" 1058 | " - 语音播放:发送「网易听歌 歌曲名称 序号」\n" 1059 | "3. 汽水音乐:\n" 1060 | " - 搜索歌单:发送「汽水点歌 歌曲名称」\n" 1061 | " - 音乐卡片:发送「汽水点歌 歌曲名称 序号」\n" 1062 | " - 语音播放:发送「汽水听歌 歌曲名称 序号」\n" 1063 | "4. 酷我音乐:\n" 1064 | " - 搜索歌单:发送「酷我点歌 歌曲名称」\n" 1065 | " - 音乐卡片:发送「酷我点歌 歌曲名称 序号」\n" 1066 | " - 语音播放:发送「酷我听歌 歌曲名称 序号」\n" 1067 | "5. 随机点歌:发送「随机点歌」获取随机音乐卡片\n" 1068 | "6. 随机听歌:发送「随机听歌」获取随机语音播放\n" 1069 | "注:序号在搜索结果中获取" 1070 | ) 1071 | --------------------------------------------------------------------------------