├── __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 | 
26 |
27 | 
28 |
29 | 
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 |
--------------------------------------------------------------------------------