Files
MeowMusicServer/yuafengfreeapi.go
2025-12-09 16:33:44 +08:00

419 lines
13 KiB
Go
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"time"
)
type YuafengAPIFreeResponse struct {
Data struct {
Song string `json:"song"`
Singer string `json:"singer"`
Cover string `json:"cover"`
AlbumName string `json:"album_name"`
Music string `json:"music"`
Lyric string `json:"lyric"`
} `json:"data"`
}
// 枫雨API response handler with multiple API fallback
func YuafengAPIResponseHandler(sources, song, singer string) MusicItem {
fmt.Printf("[Info] Fetching music data for %s by %s\n", song, singer)
// API hosts to try in order
apiHosts := []string{
"https://api.yuafeng.cn",
"https://api-v2.yuafeng.cn",
"https://api.yaohud.cn",
}
var pathSuffix string
switch sources {
case "kuwo":
pathSuffix = "/API/ly/kwmusic.php"
case "netease":
pathSuffix = "/API/ly/wymusic.php"
case "migu":
pathSuffix = "/API/ly/mgmusic.php"
case "baidu":
pathSuffix = "/API/ly/bdmusic.php"
default:
return MusicItem{}
}
var fallbackItem MusicItem // 保存第一个有音乐但没歌词的结果
// Try each API host - 尝试所有API直到找到歌词
for i, host := range apiHosts {
fmt.Printf("[Info] Trying API %d/%d: %s\n", i+1, len(apiHosts), host)
item := tryFetchFromAPI(host+pathSuffix, song, singer)
if item.Title != "" {
// 如果有歌词,立即返回
if item.LyricURL != "" {
fmt.Printf("[Success] ✓ Found music WITH lyrics from %s\n", host)
return item
}
// 如果没有歌词但有音乐保存作为fallback并继续尝试
if fallbackItem.Title == "" {
fallbackItem = item
fmt.Printf("[Info] ○ Got music WITHOUT lyrics from %s, saved as fallback, continuing...\n", host)
} else {
fmt.Printf("[Info] ○ Got music WITHOUT lyrics from %s, trying next API...\n", host)
}
} else {
fmt.Printf("[Warning] × API %s failed, trying next...\n", host)
}
}
// 所有API都试完了
if fallbackItem.Title != "" {
fmt.Println("[Info] ▶ All 3 APIs tried - no lyrics found, returning music without lyrics")
return fallbackItem
}
fmt.Println("[Error] ✗ All 3 APIs failed completely")
return MusicItem{}
}
// tryFetchFromAPI attempts to fetch music data from a single API endpoint
func tryFetchFromAPI(APIurl, song, singer string) MusicItem {
resp, err := http.Get(APIurl + "?msg=" + song + "&n=1")
if err != nil {
fmt.Println("[Error] Error fetching the data from API:", err)
return MusicItem{}
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("[Error] Error reading the response body:", err)
return MusicItem{}
}
// Check if response is HTML (starts with < character)
bodyStr := string(body)
if len(bodyStr) > 0 && bodyStr[0] == '<' {
fmt.Println("[Warning] API returned HTML instead of JSON")
fmt.Printf("[Debug] Saving HTML response to debug.html for inspection\n")
// Save HTML to file for debugging
os.WriteFile("debug_api_response.html", body, 0644)
fmt.Println("[Info] HTML response saved to debug_api_response.html")
// Try to extract JSON from HTML if embedded
// Look for common patterns where JSON might be embedded
if strings.Contains(bodyStr, `"song"`) && strings.Contains(bodyStr, `"singer"`) {
fmt.Println("[Info] Attempting to extract JSON from HTML...")
// Try to find JSON block in HTML
jsonStart := strings.Index(bodyStr, "{")
jsonEnd := strings.LastIndex(bodyStr, "}")
if jsonStart != -1 && jsonEnd != -1 && jsonEnd > jsonStart {
jsonStr := bodyStr[jsonStart : jsonEnd+1]
var response YuafengAPIFreeResponse
err = json.Unmarshal([]byte(jsonStr), &response)
if err == nil {
fmt.Println("[Success] Extracted JSON from HTML successfully")
body = []byte(jsonStr)
goto parseSuccess
}
}
}
fmt.Println("[Error] Cannot parse HTML response - API may be unavailable")
return MusicItem{}
}
parseSuccess:
var response YuafengAPIFreeResponse
err = json.Unmarshal(body, &response)
if err != nil {
fmt.Println("[Error] Error parsing API response:", err)
return MusicItem{}
}
// Create directory
dirName := fmt.Sprintf("./files/cache/music/%s-%s", response.Data.Singer, response.Data.Song)
err = os.MkdirAll(dirName, 0755)
if err != nil {
fmt.Println("[Error] Error creating directory:", err)
return MusicItem{}
}
if response.Data.Music == "" {
fmt.Println("[Warning] Music URL is empty")
return MusicItem{}
}
// 获取封面扩展名
ext := filepath.Ext(response.Data.Cover)
// 保存远程 URL 到文件,供 file.go 流式转码使用
remoteURLFile := filepath.Join(dirName, "remote_url.txt")
os.WriteFile(remoteURLFile, []byte(response.Data.Music), 0644)
// ========== 关键优化:先返回,后台异步处理 ==========
// 把下载、转码等耗时操作放到 goroutine 异步执行
go func() {
fmt.Printf("[Async] Starting background processing for: %s - %s\n", response.Data.Singer, response.Data.Song)
var wg sync.WaitGroup
// ========== 1. 歌词处理 ==========
wg.Add(1)
go func() {
defer wg.Done()
lyricData := response.Data.Lyric
if lyricData == "获取歌词失败" {
fetchLyricFromYaohu(response.Data.Song, response.Data.Singer, dirName)
} else if !strings.HasPrefix(lyricData, "http://") && !strings.HasPrefix(lyricData, "https://") {
// 直接写入歌词内容
lines := strings.Split(lyricData, "\n")
lyricFilePath := filepath.Join(dirName, "lyric.lrc")
file, err := os.Create(lyricFilePath)
if err == nil {
timeTagRegex := regexp.MustCompile(`^\[(\d+(?:\.\d+)?)\]`)
for _, line := range lines {
match := timeTagRegex.FindStringSubmatch(line)
if match != nil {
timeInSeconds, _ := strconv.ParseFloat(match[1], 64)
minutes := int(timeInSeconds / 60)
seconds := int(timeInSeconds) % 60
milliseconds := int((timeInSeconds-float64(seconds))*1000) / 100 % 100
formattedTimeTag := fmt.Sprintf("[%02d:%02d.%02d]", minutes, seconds, milliseconds)
line = timeTagRegex.ReplaceAllString(line, formattedTimeTag)
}
file.WriteString(line + "\r\n")
}
file.Close()
fmt.Printf("[Async] Lyrics saved for: %s - %s\n", response.Data.Singer, response.Data.Song)
}
} else {
downloadFile(filepath.Join(dirName, "lyric.lrc"), lyricData)
}
}()
// ========== 2. 封面处理 ==========
wg.Add(1)
go func() {
defer wg.Done()
downloadFile(filepath.Join(dirName, "cover"+ext), response.Data.Cover)
}()
// ========== 3. 音频转码 ==========
wg.Add(1)
go func() {
defer wg.Done()
musicExt, err := getMusicFileExtension(response.Data.Music)
if err != nil {
fmt.Println("[Async Warning] Cannot identify music format, using default .mp3:", err)
musicExt = ".mp3"
}
outputMp3 := filepath.Join(dirName, "music.mp3")
err = streamConvertAudio(response.Data.Music, outputMp3)
if err != nil {
fmt.Println("[Async Error] Error stream converting audio:", err)
// 备用方案
err = downloadFile(filepath.Join(dirName, "music_full"+musicExt), response.Data.Music)
if err == nil {
compressAndSegmentAudio(filepath.Join(dirName, "music_full"+musicExt), dirName)
}
}
}()
wg.Wait() // 等待所有任务完成
fmt.Printf("[Async] Background processing completed for: %s - %s\n", response.Data.Singer, response.Data.Song)
}()
// ========== 立即返回 JSON使用标准 .mp3 URL ==========
// 注意:返回标准的 .mp3 URLfile.go 会在文件不存在时自动触发流式转码
basePath := "/cache/music/" + url.QueryEscape(response.Data.Singer+"-"+response.Data.Song)
return MusicItem{
Title: response.Data.Song,
Artist: response.Data.Singer,
CoverURL: basePath + "/cover" + ext,
LyricURL: basePath + "/lyric.lrc",
AudioFullURL: basePath + "/music.mp3", // 标准 .mp3 URL
AudioURL: basePath + "/music.mp3",
M3U8URL: basePath + "/music.m3u8",
Duration: 0, // 时长后台获取先返回0
}
}
// YaohuQQMusicResponse 妖狐QQ音乐API响应结构
type YaohuQQMusicResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data struct {
Songname string `json:"songname"`
Name string `json:"name"`
Picture string `json:"picture"`
Musicurl string `json:"musicurl"`
Viplrc string `json:"viplrc"` // VIP歌词URL链接
} `json:"data"`
}
// YaohuLyricResponse 妖狐歌词API响应结构
type YaohuLyricResponse struct {
Code int `json:"code"`
Data struct {
Lyric string `json:"lyric"`
} `json:"data"`
}
// fetchLyricFromYaohu 从妖狐数据QQ音乐VIP API获取歌词备用方案
func fetchLyricFromYaohu(songName, artistName, dirPath string) bool {
apiKey := "bXO9eq1pomwR1cyVhzX"
apiURL := "https://api.yaohud.cn/api/music/qq"
// 构建请求URL - QQ音乐VIP接口
requestURL := fmt.Sprintf("%s?key=%s&msg=%s&n=1&size=hq",
apiURL,
apiKey,
url.QueryEscape(songName))
fmt.Printf("[Info] 🎵 Trying to fetch lyric from Yaohu QQ Music VIP API for: %s - %s\n", artistName, songName)
// 创建带超时的HTTP客户端
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Get(requestURL)
if err != nil {
fmt.Printf("[Error] Yaohu QQ Music API request failed: %v\n", err)
return false
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("[Error] Failed to read API response: %v\n", err)
return false
}
var qqResp YaohuQQMusicResponse
err = json.Unmarshal(body, &qqResp)
if err != nil {
fmt.Printf("[Error] Failed to parse API response: %v\n", err)
return false
}
// 检查响应状态
if qqResp.Code != 200 {
fmt.Printf("[Warning] API returned error (code: %d, msg: %s)\n", qqResp.Code, qqResp.Msg)
return false
}
// 检查viplrc URL是否存在
if qqResp.Data.Viplrc == "" {
fmt.Printf("[Warning] No lyric URL available for: %s\n", songName)
return false
}
fmt.Printf("[Info] 🔍 Found song: %s - %s\n", qqResp.Data.Songname, qqResp.Data.Name)
fmt.Printf("[Info] 📝 Fetching lyric from: %s\n", qqResp.Data.Viplrc)
// Step 2: 获取实际歌词内容
resp2, err := client.Get(qqResp.Data.Viplrc)
if err != nil {
fmt.Printf("[Error] Failed to fetch lyric from viplrc URL: %v\n", err)
return false
}
defer resp2.Body.Close()
body2, err := io.ReadAll(resp2.Body)
if err != nil {
fmt.Printf("[Error] Failed to read lyric response: %v\n", err)
return false
}
// viplrc URL直接返回LRC文本不是JSON
lyricText := string(body2)
// 检查歌词内容
if lyricText == "" || len(lyricText) < 10 {
fmt.Printf("[Warning] No lyrics returned from viplrc URL\n")
return false
}
// 将歌词写入文件
lyricFilePath := filepath.Join(dirPath, "lyric.lrc")
file, err := os.Create(lyricFilePath)
if err != nil {
fmt.Printf("[Error] Failed to create lyric file: %v\n", err)
return false
}
defer file.Close()
// 写入歌词内容LRC文本格式
_, err = file.WriteString(lyricText)
if err != nil {
fmt.Printf("[Error] Failed to write lyric content: %v\n", err)
return false
}
fmt.Printf("[Success] ✅ Lyric fetched from Yaohu QQ Music VIP API and saved to %s\n", lyricFilePath)
return true
}
// getRemoteMusicURLOnly 只获取远程音乐URL不下载不处理用于实时流式播放
func getRemoteMusicURLOnly(song, singer string) string {
fmt.Printf("[Info] Getting remote music URL for: %s - %s\n", singer, song)
// 尝试多个 API
apiHosts := []string{
"https://api.yuafeng.cn",
"https://api-v2.yuafeng.cn",
}
sources := []string{"kuwo", "netease", "migu"}
pathMap := map[string]string{
"kuwo": "/API/ly/kwmusic.php",
"netease": "/API/ly/wymusic.php",
"migu": "/API/ly/mgmusic.php",
}
client := &http.Client{Timeout: 15 * time.Second}
for _, host := range apiHosts {
for _, source := range sources {
path := pathMap[source]
apiURL := fmt.Sprintf("%s%s?msg=%s-%s&n=1", host, path, url.QueryEscape(song), url.QueryEscape(singer))
resp, err := client.Get(apiURL)
if err != nil {
continue
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
continue
}
var response YuafengAPIFreeResponse
if err := json.Unmarshal(body, &response); err != nil {
continue
}
if response.Data.Music != "" {
fmt.Printf("[Success] Got remote URL from %s: %s\n", source, response.Data.Music)
return response.Data.Music
}
}
}
fmt.Println("[Error] Failed to get remote music URL from all APIs")
return ""
}