添加猜歌名功能,从API中提取歌曲前奏,然后猜歌名
This commit is contained in:
7
plugins/guess_song/__init__.py
Normal file
7
plugins/guess_song/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
# 从当前包的main模块导入GuessSongPlugin类
|
||||
from .main import GuessSongPlugin
|
||||
|
||||
# 提供get_plugin函数,返回插件实例
|
||||
def get_plugin():
|
||||
"""获取插件实例"""
|
||||
return GuessSongPlugin()
|
||||
8
plugins/guess_song/config.toml
Normal file
8
plugins/guess_song/config.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
enable = true
|
||||
command = ["猜歌名"]
|
||||
command-format = """
|
||||
🎵猜歌名指令:
|
||||
猜歌名 - 开始猜歌游戏
|
||||
猜歌名 歌手名 - 开始指定歌手的猜歌游戏
|
||||
猜歌名 歌名 - 提交你的答案
|
||||
"""
|
||||
371
plugins/guess_song/main.py
Normal file
371
plugins/guess_song/main.py
Normal file
@@ -0,0 +1,371 @@
|
||||
import base64
|
||||
import json
|
||||
import random
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from loguru import logger
|
||||
import requests
|
||||
from typing import Dict, Any, List, Optional, Tuple
|
||||
|
||||
from base.plugin_common.message_plugin_interface import MessagePluginInterface
|
||||
from base.plugin_common.plugin_interface import PluginStatus
|
||||
from utils.decorator.plugin_decorators import plugin_stats_decorator
|
||||
from utils.robot_cmd.robot_command import Feature, PermissionStatus, GroupBotManager
|
||||
from utils.decorator.points_decorator import plugin_points_cost, points_reward_decorator
|
||||
from wechat_ipad import WechatAPIClient
|
||||
from db.connection import DBConnectionManager
|
||||
|
||||
import aiohttp
|
||||
from pydub import AudioSegment
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
class GuessSongRedisDB:
|
||||
"""猜歌名游戏Redis相关操作"""
|
||||
|
||||
def __init__(self, db_manager: DBConnectionManager):
|
||||
self.db_manager = db_manager
|
||||
self.prefix = "group:guess_song:"
|
||||
|
||||
def get_redis_connection(self):
|
||||
"""获取Redis连接"""
|
||||
return self.db_manager.get_redis_connection()
|
||||
|
||||
def save_game_session(self, room_id: str, game_data: Dict[str, Any]) -> bool:
|
||||
"""保存游戏会话数据"""
|
||||
try:
|
||||
with self.get_redis_connection() as redis_client:
|
||||
# 设置过期时间为10分钟
|
||||
redis_client.setex(
|
||||
f'{self.prefix}{room_id}',
|
||||
600, # 10分钟过期
|
||||
json.dumps(game_data, ensure_ascii=False)
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"保存猜歌游戏数据出错: {e}")
|
||||
return False
|
||||
|
||||
def get_game_session(self, room_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""获取游戏会话数据"""
|
||||
try:
|
||||
with self.get_redis_connection() as redis_client:
|
||||
data = redis_client.get(f'{self.prefix}{room_id}')
|
||||
if data:
|
||||
if isinstance(data, bytes):
|
||||
data = data.decode('utf-8')
|
||||
return json.loads(data)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"获取猜歌游戏数据出错: {e}")
|
||||
return None
|
||||
|
||||
def delete_game_session(self, room_id: str) -> bool:
|
||||
"""删除游戏会话数据"""
|
||||
try:
|
||||
with self.get_redis_connection() as redis_client:
|
||||
redis_client.delete(f'{self.prefix}{room_id}')
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"删除猜歌游戏数据出错: {e}")
|
||||
return False
|
||||
|
||||
|
||||
class GuessSongPlugin(MessagePluginInterface):
|
||||
"""猜歌名游戏插件"""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "猜歌名游戏"
|
||||
|
||||
@property
|
||||
def version(self) -> str:
|
||||
return "1.0.0"
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return "提供猜歌名游戏功能,支持指定歌手和猜歌名"
|
||||
|
||||
@property
|
||||
def author(self) -> str:
|
||||
return "Trae AI"
|
||||
|
||||
@property
|
||||
def command_prefix(self) -> Optional[str]:
|
||||
return "" # 不需要前缀,直接匹配命令
|
||||
|
||||
@property
|
||||
def commands(self) -> List[str]:
|
||||
return self._commands
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.redis_db = None
|
||||
|
||||
def initialize(self, context: Dict[str, Any]) -> bool:
|
||||
"""初始化插件"""
|
||||
self.LOG = logger
|
||||
self.LOG.info(f"正在初始化 {self.name} 插件...")
|
||||
|
||||
# 保存上下文对象
|
||||
self.event_system = context.get("event_system")
|
||||
self.db_manager = context.get("db_manager")
|
||||
|
||||
# 初始化Redis操作类
|
||||
if self.db_manager:
|
||||
self.redis_db = GuessSongRedisDB(self.db_manager)
|
||||
|
||||
self._commands = self._config.get("GuessSong", {}).get("command", ["猜歌名"])
|
||||
self.command_format = self._config.get("GuessSong", {}).get("command-format", "猜歌名 [歌手名]/[歌名]")
|
||||
self.enable = self._config.get("GuessSong", {}).get("enable", True)
|
||||
|
||||
self.LOG.info(f"[{self.name}] 插件初始化完成,指令:{self._commands}")
|
||||
return True
|
||||
|
||||
def start(self) -> bool:
|
||||
"""启动插件"""
|
||||
self.LOG.info(f"[{self.name}] 插件已启动")
|
||||
self.status = PluginStatus.RUNNING
|
||||
return True
|
||||
|
||||
def stop(self) -> bool:
|
||||
"""停止插件"""
|
||||
self.LOG.info(f"[{self.name}] 插件已停止")
|
||||
self.status = PluginStatus.STOPPED
|
||||
return True
|
||||
|
||||
def can_process(self, message: Dict[str, Any]) -> bool:
|
||||
"""检查是否可以处理该消息"""
|
||||
if not self.enable:
|
||||
return False
|
||||
|
||||
content = str(message.get("content", "")).strip()
|
||||
command = content.split(" ")[0]
|
||||
|
||||
return command in self._commands
|
||||
|
||||
@plugin_stats_decorator(plugin_name="猜歌名游戏")
|
||||
async def process_message(self, message: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
|
||||
"""处理消息"""
|
||||
content = str(message.get("content", "")).strip()
|
||||
self.LOG.debug(f"插件执行: {self.name}:{content}")
|
||||
command = content.split(" ")[0]
|
||||
sender = message.get("sender")
|
||||
roomid = message.get("roomid", "")
|
||||
gbm: GroupBotManager = message.get("gbm")
|
||||
bot: WechatAPIClient = message.get("bot")
|
||||
|
||||
# 使用roomid或sender作为游戏会话ID
|
||||
session_id = roomid if roomid else sender
|
||||
|
||||
# 检查权限
|
||||
if roomid and gbm.get_group_permission(roomid, Feature.GUESS_MUSIC) == PermissionStatus.DISABLED:
|
||||
return False, "没有权限"
|
||||
|
||||
# 提取参数
|
||||
params = content[len(command):].strip()
|
||||
|
||||
# 获取当前游戏会话
|
||||
current_game = None
|
||||
if self.redis_db:
|
||||
current_game = self.redis_db.get_game_session(session_id)
|
||||
|
||||
# 如果没有参数,开始新游戏(随机歌手)
|
||||
if not params:
|
||||
return await self._start_new_game(bot, session_id, sender, None)
|
||||
|
||||
# 如果有游戏进行中,且参数不是歌手名,则视为猜歌名答案
|
||||
if current_game and current_game.get("status") == "playing":
|
||||
return await self._check_answer(bot, session_id, sender, params, current_game)
|
||||
|
||||
# 否则,视为指定歌手开始新游戏
|
||||
return await self._start_new_game(bot, session_id, sender, params)
|
||||
|
||||
async def _start_new_game(self, bot: WechatAPIClient, session_id: str, sender: str, singer_name: Optional[str]) -> \
|
||||
Tuple[bool, str]:
|
||||
"""开始新游戏"""
|
||||
try:
|
||||
# 搜索歌曲
|
||||
song_info = await self._get_random_song(singer_name)
|
||||
if not song_info or not song_info.get("play_url"):
|
||||
await bot.send_text_message(session_id, f"❌未找到{singer_name or '随机'}歌曲,请重试", sender)
|
||||
return False, "未找到歌曲"
|
||||
|
||||
# 保存游戏会话
|
||||
game_data = {
|
||||
"status": "playing",
|
||||
"song_name": song_info.get("song_name", ""),
|
||||
"singer_name": song_info.get("singer_name", ""),
|
||||
"start_time": time.time(),
|
||||
"hint_given": False
|
||||
}
|
||||
|
||||
if self.redis_db:
|
||||
self.redis_db.save_game_session(session_id, game_data)
|
||||
|
||||
# 发送游戏开始消息
|
||||
await bot.send_text_message(session_id,
|
||||
f"🎵 猜歌名游戏开始!\n请听10秒钟歌曲片段,然后回复[猜歌名 歌名]来猜测歌曲名称。\n歌手: {song_info.get('singer_name', '未知')}",
|
||||
sender)
|
||||
|
||||
# 发送10秒音频片段
|
||||
await self._send_song_clip(bot, song_info, session_id)
|
||||
|
||||
return True, "游戏开始"
|
||||
|
||||
except Exception as e:
|
||||
self.LOG.error(f"开始猜歌游戏出错: {e}")
|
||||
await bot.send_text_message(session_id, f"❌开始猜歌游戏出错,请稍后重试", sender)
|
||||
return False, f"处理出错: {e}"
|
||||
|
||||
@points_reward_decorator(5, "game", "猜歌名游戏", Feature.GUESS_MUSIC)
|
||||
async def _check_answer(self, bot: WechatAPIClient, session_id: str, sender: str, answer: str,
|
||||
game_data: Dict[str, Any]) -> Tuple[bool, str]:
|
||||
"""检查答案"""
|
||||
try:
|
||||
correct_answer = game_data.get("song_name", "")
|
||||
singer_name = game_data.get("singer_name", "")
|
||||
|
||||
# 检查答案是否正确(简单比较,可以改进为模糊匹配)
|
||||
if answer.lower() == correct_answer.lower():
|
||||
# 游戏结束,发送成功消息
|
||||
await bot.send_text_message(session_id, f"🎉 恭喜你猜对了!\n歌曲:{correct_answer}\n歌手:{singer_name}",
|
||||
sender)
|
||||
|
||||
# 删除游戏会话
|
||||
if self.redis_db:
|
||||
self.redis_db.delete_game_session(session_id)
|
||||
|
||||
return True, "猜对了"
|
||||
else:
|
||||
# 答案错误
|
||||
# 检查是否需要给提示(超过30秒且未给过提示)
|
||||
current_time = time.time()
|
||||
start_time = game_data.get("start_time", 0)
|
||||
hint_given = game_data.get("hint_given", False)
|
||||
|
||||
if not hint_given and (current_time - start_time) > 30:
|
||||
# 给出提示(显示歌名的第一个字)
|
||||
hint = correct_answer[0] + "*" * (len(correct_answer) - 1)
|
||||
await bot.send_text_message(session_id, f"💡 提示:歌名以 '{correct_answer[0]}' 开头")
|
||||
|
||||
# 更新游戏会话,标记已给出提示
|
||||
game_data["hint_given"] = True
|
||||
if self.redis_db:
|
||||
self.redis_db.save_game_session(session_id, game_data)
|
||||
|
||||
# 告知用户答案错误
|
||||
await bot.send_text_message(session_id, f"❌ 答案错误,请继续猜测!")
|
||||
return False, "答案错误"
|
||||
except Exception as e:
|
||||
self.LOG.error(f"检查答案出错: {e}")
|
||||
await bot.send_text_message(session_id, f"❌检查答案出错,请稍后重试", sender)
|
||||
return False, f"处理出错: {e}"
|
||||
|
||||
async def _get_random_song(self, singer_name: Optional[str]) -> Dict[str, Any]:
|
||||
"""获取随机歌曲信息"""
|
||||
try:
|
||||
# 构建API请求URL - 使用抖音音乐API
|
||||
if singer_name:
|
||||
# 如果指定了歌手,搜索该歌手的歌曲
|
||||
api_url = f"https://hhlqilongzhu.cn/api/dg_douyinmusic.php?msg={singer_name}&type=json"
|
||||
else:
|
||||
# 如果没有指定歌手,使用热门歌手列表
|
||||
popular_singers = ["周杰伦", "林俊杰", "薛之谦", "陈奕迅", "邓紫棋", "Taylor Swift", "李荣浩"]
|
||||
random_singer = random.choice(popular_singers)
|
||||
api_url = f"https://hhlqilongzhu.cn/api/dg_douyinmusic.php?msg={random_singer}&type=json"
|
||||
|
||||
# 发送请求获取歌曲列表
|
||||
response = requests.get(api_url)
|
||||
|
||||
if response.status_code != 200:
|
||||
self.LOG.error(f"API 请求失败,状态码: {response.status_code}")
|
||||
return {}
|
||||
|
||||
# 解析响应数据
|
||||
json_data = response.json()
|
||||
if not json_data.get("data") or json_data.get("code") != 200:
|
||||
self.LOG.error(f"API 返回数据格式错误: {json_data}")
|
||||
return {}
|
||||
|
||||
# 从歌曲列表中随机选择一首
|
||||
song_list = json_data.get("data")
|
||||
if not song_list or not isinstance(song_list, list) or len(song_list) == 0:
|
||||
self.LOG.error(f"未找到歌曲列表或列表为空")
|
||||
return {}
|
||||
|
||||
# 随机选择一首歌曲
|
||||
random_song = random.choice(song_list)
|
||||
|
||||
# 根据随机选择的歌曲信息,再次调用API获取完整信息(包含播放链接)
|
||||
song_name = random_song.get('title', '')
|
||||
singer_name = random_song.get('singer', '')
|
||||
search_query = f"{song_name} {singer_name}"
|
||||
|
||||
# 调用原有API获取播放链接
|
||||
detail_api = f"https://www.hhlqilongzhu.cn/api/joox/juhe_music.php?msg={search_query}&type=json&n=1"
|
||||
detail_response = requests.get(detail_api)
|
||||
|
||||
if detail_response.status_code != 200:
|
||||
self.LOG.error(f"获取歌曲详情失败,状态码: {detail_response.status_code}")
|
||||
return {}
|
||||
|
||||
detail_data = detail_response.json().get("data")
|
||||
if not detail_data:
|
||||
self.LOG.error(f"获取歌曲详情数据为空")
|
||||
return {}
|
||||
|
||||
# 返回完整的歌曲信息
|
||||
return {
|
||||
"song_name": song_name,
|
||||
"singer_name": singer_name,
|
||||
"play_url": detail_data.get('url', ''),
|
||||
"singer_pic": random_song.get('cover', '') or detail_data.get('cover', ''),
|
||||
"data_url": detail_data.get('link', '')
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.LOG.error(f"获取随机歌曲出错: {e}")
|
||||
return {}
|
||||
|
||||
async def _send_song_clip(self, bot: WechatAPIClient, song_info: Dict[str, Any], session_id: str) -> bool:
|
||||
"""发送歌曲片段"""
|
||||
try:
|
||||
play_url = song_info.get("play_url", "")
|
||||
if not play_url:
|
||||
return False
|
||||
|
||||
# 下载歌曲
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(play_url) as resp:
|
||||
if resp.status != 200:
|
||||
self.LOG.error(f"下载歌曲失败,状态码: {resp.status}")
|
||||
return False
|
||||
|
||||
# 读取音频数据
|
||||
audio_data = await resp.read()
|
||||
|
||||
try:
|
||||
# 使用pydub处理音频
|
||||
audio = AudioSegment.from_file(BytesIO(audio_data))
|
||||
|
||||
# 截取前10秒
|
||||
clip_duration = 10 * 1000 # 10秒,单位毫秒
|
||||
audio_clip = audio[:clip_duration]
|
||||
|
||||
# 将音频片段转换为字节
|
||||
output = BytesIO()
|
||||
audio_clip.export(output, format="mp3")
|
||||
clip_bytes = output.getvalue()
|
||||
|
||||
await bot.send_voice_message(session_id, clip_bytes, "mp3")
|
||||
return True
|
||||
|
||||
except Exception as audio_error:
|
||||
self.LOG.error(f"处理音频出错: {audio_error}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.LOG.error(f"发送歌曲片段出错: {e}")
|
||||
return False
|
||||
@@ -48,6 +48,7 @@ class Feature(Enum):
|
||||
WEATHER = 21, "天气查询[上海天气 天气上海]"
|
||||
JD_TOKEN = 22, "JD_京豆token设置[设置京东 pt_key=xxx;pt_pin=xxx; 备注名称]"
|
||||
AI_AUTO = 23, "仿真对话"
|
||||
GUESS_MUSIC = 23, "猜歌名游戏 [猜歌名 - 开始猜歌游戏 |猜歌名 歌手名 - 开始指定歌手的猜歌游戏| 猜歌名 歌名 - 提交你的答案]"
|
||||
|
||||
def __new__(cls, value, description):
|
||||
obj = object.__new__(cls)
|
||||
|
||||
Reference in New Issue
Block a user