(msg: Api.Message, args: string[])
| 322 | if (!client || !reply || (!reply.document && !reply.video)) { |
| 323 | await msg.edit({ text: `❌ <b>使用错误</b>\n\n请回复一个视频消息后再使用此命令。\n\n请回复一个视频消息后再试。`, parseMode: "html" }); |
| 324 | return; |
| 325 | } |
| 326 | |
| 327 | const doc = reply.video || reply.document; |
| 328 | const fileNameAttr = doc?.attributes?.find((a: any) => a.fileName) as Api.DocumentAttributeFilename | undefined; |
| 329 | const originalFileName = fileNameAttr?.fileName || "video.mp4"; |
| 330 | |
| 331 | await msg.edit({ text: "📥 正在下载视频...", parseMode: "html" }); |
| 332 | |
| 333 | const timestamp = Date.now(); |
| 334 | const tempVideoPath = path.join(converter.tempDir, `${timestamp}_video`); |
| 335 | const tempAudioPath = path.join(converter.tempDir, `${timestamp}.mp3`); |
| 336 | let finalAudioPath = tempAudioPath; |
| 337 | let tempCoverPath: string | undefined; |
| 338 | |
| 339 | try { |
| 340 | await client.downloadMedia(reply, { outputFile: tempVideoPath }); |
| 341 | if (!fs.existsSync(tempVideoPath)) throw new Error("视频下载失败"); |
| 342 | |
| 343 | await msg.edit({ text: "🔄 正在转换为 MP3...", parseMode: "html" }); |
| 344 | if (!await converter.convertVideoToMp3(tempVideoPath, tempAudioPath)) { |
| 345 | throw new Error("视频转换失败,请检查 FFmpeg 是否已安装"); |
| 346 | } |
| 347 | |
| 348 | const useAi = (args[0] || "").toLowerCase() === 'u'; |
| 349 | const userQuery = useAi ? args.slice(1).join(' ') : args.join(' '); |
| 350 | |
| 351 | let audioFileName: string; |
| 352 | let songInfo: SongInfo = { title: "", artist: "", album: "" }; |
| 353 | |
| 354 | if (useAi && userQuery) { |
| 355 | const apiKey = GeminiConfigManager.get(GEMINI_API_KEY); |
| 356 | if (!apiKey) throw new Error(`Gemini API Key 未设置。\n请使用 <code>${mainPrefix}convert apikey <key></code> 命令设置。`); |
| 357 | |
| 358 | await msg.edit({ text: "🤖 AI 正在识别歌曲信息...", parseMode: "html" }); |
| 359 | const gemini = new GeminiClient(apiKey); |
| 360 | const aiResponse = await gemini.searchMusic(userQuery); |
| 361 | songInfo = extractSongInfo(aiResponse, userQuery); |
| 362 | |
| 363 | await msg.edit({ text: `🎵 AI 识别结果:\n<b>歌名:</b> ${htmlEscape(songInfo.title)}\n<b>歌手:</b> ${htmlEscape(songInfo.artist)}\n\n正在查找封面...` , parseMode: "html"}); |
| 364 | |
| 365 | tempCoverPath = path.join(converter.tempDir, `${timestamp}.jpg`); |
| 366 | const coverFound = await searchAndDownloadCover(`${songInfo.title} ${songInfo.artist}`, tempCoverPath); |
| 367 | if (!coverFound) tempCoverPath = undefined; |
| 368 | |
| 369 | await msg.edit({ text: "✍️ 正在写入元数据...", parseMode: "html" }); |
| 370 | const tempFinalAudioPath = path.join(converter.tempDir, `${timestamp}_final.mp3`); |
| 371 | if (await converter.addMetadataAndCover(tempAudioPath, tempFinalAudioPath, songInfo, tempCoverPath)) { |
| 372 | finalAudioPath = tempFinalAudioPath; |
| 373 | } |
| 374 | audioFileName = `${converter.safeFilename(songInfo.title)} - ${converter.safeFilename(songInfo.artist)}.mp3`; |
| 375 | |
| 376 | } else if (userQuery) { |
| 377 | audioFileName = `${converter.safeFilename(userQuery)}.mp3`; |
| 378 | songInfo.title = userQuery; |
| 379 | } else { |
| 380 | audioFileName = `${converter.safeFilename(originalFileName.replace(/\.[^.]+$/, ""))}.mp3`; |
| 381 | songInfo.title = originalFileName.replace(/\.[^.]+$/, ""); |
no test coverage detected