Flow enhancement
This commit is contained in:
4
api.go
4
api.go
@@ -171,8 +171,8 @@ func apiHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
musicItem = requestAndCacheMusic(song, singer)
|
musicItem = requestAndCacheMusic(song, singer)
|
||||||
fmt.Println("[Info] Music item cache updated.")
|
fmt.Println("[Info] Music item cache updated.")
|
||||||
musicItem.FromCache = false
|
musicItem.FromCache = false
|
||||||
musicItem.AudioURL = scheme + "://" + r.Host + musicItem.AudioURL
|
musicItem.AudioURL = scheme + "://" + r.Host + "/stream_live?song=" + song + "&singer=" + singer
|
||||||
musicItem.AudioFullURL = scheme + "://" + r.Host + musicItem.AudioFullURL
|
musicItem.AudioFullURL = scheme + "://" + r.Host + "/stream_live?song=" + song + "&singer=" + singer
|
||||||
musicItem.M3U8URL = scheme + "://" + r.Host + musicItem.M3U8URL
|
musicItem.M3U8URL = scheme + "://" + r.Host + musicItem.M3U8URL
|
||||||
musicItem.LyricURL = scheme + "://" + r.Host + musicItem.LyricURL
|
musicItem.LyricURL = scheme + "://" + r.Host + musicItem.LyricURL
|
||||||
musicItem.CoverURL = scheme + "://" + r.Host + musicItem.CoverURL
|
musicItem.CoverURL = scheme + "://" + r.Host + musicItem.CoverURL
|
||||||
|
|||||||
0
files/cache/music/.gitignore
vendored
0
files/cache/music/.gitignore
vendored
@@ -29,14 +29,14 @@ type YuafengAPIFreeResponse struct {
|
|||||||
// 枫雨API response handler with multiple API fallback
|
// 枫雨API response handler with multiple API fallback
|
||||||
func YuafengAPIResponseHandler(sources, song, singer string) MusicItem {
|
func YuafengAPIResponseHandler(sources, song, singer string) MusicItem {
|
||||||
fmt.Printf("[Info] Fetching music data for %s by %s\n", song, singer)
|
fmt.Printf("[Info] Fetching music data for %s by %s\n", song, singer)
|
||||||
|
|
||||||
// API hosts to try in order
|
// API hosts to try in order
|
||||||
apiHosts := []string{
|
apiHosts := []string{
|
||||||
"https://api.yuafeng.cn",
|
"https://api.yuafeng.cn",
|
||||||
"https://api-v2.yuafeng.cn",
|
"https://api-v2.yuafeng.cn",
|
||||||
"https://api.yaohud.cn",
|
"https://api.yaohud.cn",
|
||||||
}
|
}
|
||||||
|
|
||||||
var pathSuffix string
|
var pathSuffix string
|
||||||
switch sources {
|
switch sources {
|
||||||
case "kuwo":
|
case "kuwo":
|
||||||
@@ -50,9 +50,9 @@ func YuafengAPIResponseHandler(sources, song, singer string) MusicItem {
|
|||||||
default:
|
default:
|
||||||
return MusicItem{}
|
return MusicItem{}
|
||||||
}
|
}
|
||||||
|
|
||||||
var fallbackItem MusicItem // 保存第一个有音乐但没歌词的结果
|
var fallbackItem MusicItem // 保存第一个有音乐但没歌词的结果
|
||||||
|
|
||||||
// Try each API host - 尝试所有API直到找到歌词
|
// Try each API host - 尝试所有API直到找到歌词
|
||||||
for i, host := range apiHosts {
|
for i, host := range apiHosts {
|
||||||
fmt.Printf("[Info] Trying API %d/%d: %s\n", i+1, len(apiHosts), host)
|
fmt.Printf("[Info] Trying API %d/%d: %s\n", i+1, len(apiHosts), host)
|
||||||
@@ -74,13 +74,13 @@ func YuafengAPIResponseHandler(sources, song, singer string) MusicItem {
|
|||||||
fmt.Printf("[Warning] × API %s failed, trying next...\n", host)
|
fmt.Printf("[Warning] × API %s failed, trying next...\n", host)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 所有API都试完了
|
// 所有API都试完了
|
||||||
if fallbackItem.Title != "" {
|
if fallbackItem.Title != "" {
|
||||||
fmt.Println("[Info] ▶ All 3 APIs tried - no lyrics found, returning music without lyrics")
|
fmt.Println("[Info] ▶ All 3 APIs tried - no lyrics found, returning music without lyrics")
|
||||||
return fallbackItem
|
return fallbackItem
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("[Error] ✗ All 3 APIs failed completely")
|
fmt.Println("[Error] ✗ All 3 APIs failed completely")
|
||||||
return MusicItem{}
|
return MusicItem{}
|
||||||
}
|
}
|
||||||
@@ -98,17 +98,17 @@ func tryFetchFromAPI(APIurl, song, singer string) MusicItem {
|
|||||||
fmt.Println("[Error] Error reading the response body:", err)
|
fmt.Println("[Error] Error reading the response body:", err)
|
||||||
return MusicItem{}
|
return MusicItem{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if response is HTML (starts with < character)
|
// Check if response is HTML (starts with < character)
|
||||||
bodyStr := string(body)
|
bodyStr := string(body)
|
||||||
if len(bodyStr) > 0 && bodyStr[0] == '<' {
|
if len(bodyStr) > 0 && bodyStr[0] == '<' {
|
||||||
fmt.Println("[Warning] API returned HTML instead of JSON")
|
fmt.Println("[Warning] API returned HTML instead of JSON")
|
||||||
fmt.Printf("[Debug] Saving HTML response to debug.html for inspection\n")
|
fmt.Printf("[Debug] Saving HTML response to debug.html for inspection\n")
|
||||||
|
|
||||||
// Save HTML to file for debugging
|
// Save HTML to file for debugging
|
||||||
os.WriteFile("debug_api_response.html", body, 0644)
|
os.WriteFile("debug_api_response.html", body, 0644)
|
||||||
fmt.Println("[Info] HTML response saved to debug_api_response.html")
|
fmt.Println("[Info] HTML response saved to debug_api_response.html")
|
||||||
|
|
||||||
// Try to extract JSON from HTML if embedded
|
// Try to extract JSON from HTML if embedded
|
||||||
// Look for common patterns where JSON might be embedded
|
// Look for common patterns where JSON might be embedded
|
||||||
if strings.Contains(bodyStr, `"song"`) && strings.Contains(bodyStr, `"singer"`) {
|
if strings.Contains(bodyStr, `"song"`) && strings.Contains(bodyStr, `"singer"`) {
|
||||||
@@ -127,11 +127,11 @@ func tryFetchFromAPI(APIurl, song, singer string) MusicItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("[Error] Cannot parse HTML response - API may be unavailable")
|
fmt.Println("[Error] Cannot parse HTML response - API may be unavailable")
|
||||||
return MusicItem{}
|
return MusicItem{}
|
||||||
}
|
}
|
||||||
|
|
||||||
parseSuccess:
|
parseSuccess:
|
||||||
var response YuafengAPIFreeResponse
|
var response YuafengAPIFreeResponse
|
||||||
err = json.Unmarshal(body, &response)
|
err = json.Unmarshal(body, &response)
|
||||||
@@ -155,17 +155,17 @@ parseSuccess:
|
|||||||
|
|
||||||
// 获取封面扩展名
|
// 获取封面扩展名
|
||||||
ext := filepath.Ext(response.Data.Cover)
|
ext := filepath.Ext(response.Data.Cover)
|
||||||
|
|
||||||
// 保存远程 URL 到文件,供 file.go 流式转码使用
|
// 保存远程 URL 到文件,供 file.go 流式转码使用
|
||||||
remoteURLFile := filepath.Join(dirName, "remote_url.txt")
|
remoteURLFile := filepath.Join(dirName, "remote_url.txt")
|
||||||
os.WriteFile(remoteURLFile, []byte(response.Data.Music), 0644)
|
os.WriteFile(remoteURLFile, []byte(response.Data.Music), 0644)
|
||||||
|
|
||||||
// ========== 关键优化:先返回,后台异步处理 ==========
|
// ========== 关键优化:先返回,后台异步处理 ==========
|
||||||
// 把下载、转码等耗时操作放到 goroutine 异步执行
|
// 把下载、转码等耗时操作放到 goroutine 异步执行
|
||||||
go func() {
|
go func() {
|
||||||
fmt.Printf("[Async] Starting background processing for: %s - %s\n", response.Data.Singer, response.Data.Song)
|
fmt.Printf("[Async] Starting background processing for: %s - %s\n", response.Data.Singer, response.Data.Song)
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
// ========== 1. 歌词处理 ==========
|
// ========== 1. 歌词处理 ==========
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
@@ -199,14 +199,14 @@ parseSuccess:
|
|||||||
downloadFile(filepath.Join(dirName, "lyric.lrc"), lyricData)
|
downloadFile(filepath.Join(dirName, "lyric.lrc"), lyricData)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// ========== 2. 封面处理 ==========
|
// ========== 2. 封面处理 ==========
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
downloadFile(filepath.Join(dirName, "cover"+ext), response.Data.Cover)
|
downloadFile(filepath.Join(dirName, "cover"+ext), response.Data.Cover)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// ========== 3. 音频转码 ==========
|
// ========== 3. 音频转码 ==========
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
@@ -216,7 +216,7 @@ parseSuccess:
|
|||||||
fmt.Println("[Async Warning] Cannot identify music format, using default .mp3:", err)
|
fmt.Println("[Async Warning] Cannot identify music format, using default .mp3:", err)
|
||||||
musicExt = ".mp3"
|
musicExt = ".mp3"
|
||||||
}
|
}
|
||||||
|
|
||||||
outputMp3 := filepath.Join(dirName, "music.mp3")
|
outputMp3 := filepath.Join(dirName, "music.mp3")
|
||||||
err = streamConvertAudio(response.Data.Music, outputMp3)
|
err = streamConvertAudio(response.Data.Music, outputMp3)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -228,7 +228,7 @@ parseSuccess:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
wg.Wait() // 等待所有任务完成
|
wg.Wait() // 等待所有任务完成
|
||||||
fmt.Printf("[Async] Background processing completed for: %s - %s\n", response.Data.Singer, response.Data.Song)
|
fmt.Printf("[Async] Background processing completed for: %s - %s\n", response.Data.Singer, response.Data.Song)
|
||||||
}()
|
}()
|
||||||
@@ -236,7 +236,7 @@ parseSuccess:
|
|||||||
// ========== 立即返回 JSON,使用标准 .mp3 URL ==========
|
// ========== 立即返回 JSON,使用标准 .mp3 URL ==========
|
||||||
// 注意:返回标准的 .mp3 URL,file.go 会在文件不存在时自动触发流式转码
|
// 注意:返回标准的 .mp3 URL,file.go 会在文件不存在时自动触发流式转码
|
||||||
basePath := "/cache/music/" + url.QueryEscape(response.Data.Singer+"-"+response.Data.Song)
|
basePath := "/cache/music/" + url.QueryEscape(response.Data.Singer+"-"+response.Data.Song)
|
||||||
|
|
||||||
return MusicItem{
|
return MusicItem{
|
||||||
Title: response.Data.Song,
|
Title: response.Data.Song,
|
||||||
Artist: response.Data.Singer,
|
Artist: response.Data.Singer,
|
||||||
@@ -274,55 +274,55 @@ type YaohuLyricResponse struct {
|
|||||||
func fetchLyricFromYaohu(songName, artistName, dirPath string) bool {
|
func fetchLyricFromYaohu(songName, artistName, dirPath string) bool {
|
||||||
apiKey := "bXO9eq1pomwR1cyVhzX"
|
apiKey := "bXO9eq1pomwR1cyVhzX"
|
||||||
apiURL := "https://api.yaohud.cn/api/music/qq"
|
apiURL := "https://api.yaohud.cn/api/music/qq"
|
||||||
|
|
||||||
// 构建请求URL - QQ音乐VIP接口
|
// 构建请求URL - QQ音乐VIP接口
|
||||||
requestURL := fmt.Sprintf("%s?key=%s&msg=%s&n=1&size=hq",
|
requestURL := fmt.Sprintf("%s?key=%s&msg=%s&n=1&size=hq",
|
||||||
apiURL,
|
apiURL,
|
||||||
apiKey,
|
apiKey,
|
||||||
url.QueryEscape(songName))
|
url.QueryEscape(songName))
|
||||||
|
|
||||||
fmt.Printf("[Info] 🎵 Trying to fetch lyric from Yaohu QQ Music VIP API for: %s - %s\n", artistName, songName)
|
fmt.Printf("[Info] 🎵 Trying to fetch lyric from Yaohu QQ Music VIP API for: %s - %s\n", artistName, songName)
|
||||||
|
|
||||||
// 创建带超时的HTTP客户端
|
// 创建带超时的HTTP客户端
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Timeout: 10 * time.Second,
|
Timeout: 10 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := client.Get(requestURL)
|
resp, err := client.Get(requestURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("[Error] Yaohu QQ Music API request failed: %v\n", err)
|
fmt.Printf("[Error] Yaohu QQ Music API request failed: %v\n", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("[Error] Failed to read API response: %v\n", err)
|
fmt.Printf("[Error] Failed to read API response: %v\n", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
var qqResp YaohuQQMusicResponse
|
var qqResp YaohuQQMusicResponse
|
||||||
err = json.Unmarshal(body, &qqResp)
|
err = json.Unmarshal(body, &qqResp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("[Error] Failed to parse API response: %v\n", err)
|
fmt.Printf("[Error] Failed to parse API response: %v\n", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查响应状态
|
// 检查响应状态
|
||||||
if qqResp.Code != 200 {
|
if qqResp.Code != 200 {
|
||||||
fmt.Printf("[Warning] API returned error (code: %d, msg: %s)\n", qqResp.Code, qqResp.Msg)
|
fmt.Printf("[Warning] API returned error (code: %d, msg: %s)\n", qqResp.Code, qqResp.Msg)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查viplrc URL是否存在
|
// 检查viplrc URL是否存在
|
||||||
if qqResp.Data.Viplrc == "" {
|
if qqResp.Data.Viplrc == "" {
|
||||||
fmt.Printf("[Warning] No lyric URL available for: %s\n", songName)
|
fmt.Printf("[Warning] No lyric URL available for: %s\n", songName)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("[Info] 🔍 Found song: %s - %s\n", qqResp.Data.Songname, qqResp.Data.Name)
|
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)
|
fmt.Printf("[Info] 📝 Fetching lyric from: %s\n", qqResp.Data.Viplrc)
|
||||||
|
|
||||||
// Step 2: 获取实际歌词内容
|
// Step 2: 获取实际歌词内容
|
||||||
resp2, err := client.Get(qqResp.Data.Viplrc)
|
resp2, err := client.Get(qqResp.Data.Viplrc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -330,22 +330,22 @@ func fetchLyricFromYaohu(songName, artistName, dirPath string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
defer resp2.Body.Close()
|
defer resp2.Body.Close()
|
||||||
|
|
||||||
body2, err := io.ReadAll(resp2.Body)
|
body2, err := io.ReadAll(resp2.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("[Error] Failed to read lyric response: %v\n", err)
|
fmt.Printf("[Error] Failed to read lyric response: %v\n", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// viplrc URL直接返回LRC文本,不是JSON
|
// viplrc URL直接返回LRC文本,不是JSON
|
||||||
lyricText := string(body2)
|
lyricText := string(body2)
|
||||||
|
|
||||||
// 检查歌词内容
|
// 检查歌词内容
|
||||||
if lyricText == "" || len(lyricText) < 10 {
|
if lyricText == "" || len(lyricText) < 10 {
|
||||||
fmt.Printf("[Warning] No lyrics returned from viplrc URL\n")
|
fmt.Printf("[Warning] No lyrics returned from viplrc URL\n")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将歌词写入文件
|
// 将歌词写入文件
|
||||||
lyricFilePath := filepath.Join(dirPath, "lyric.lrc")
|
lyricFilePath := filepath.Join(dirPath, "lyric.lrc")
|
||||||
file, err := os.Create(lyricFilePath)
|
file, err := os.Create(lyricFilePath)
|
||||||
@@ -354,14 +354,14 @@ func fetchLyricFromYaohu(songName, artistName, dirPath string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
// 写入歌词内容(LRC文本格式)
|
// 写入歌词内容(LRC文本格式)
|
||||||
_, err = file.WriteString(lyricText)
|
_, err = file.WriteString(lyricText)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("[Error] Failed to write lyric content: %v\n", err)
|
fmt.Printf("[Error] Failed to write lyric content: %v\n", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("[Success] ✅ Lyric fetched from Yaohu QQ Music VIP API and saved to %s\n", lyricFilePath)
|
fmt.Printf("[Success] ✅ Lyric fetched from Yaohu QQ Music VIP API and saved to %s\n", lyricFilePath)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -369,50 +369,50 @@ func fetchLyricFromYaohu(songName, artistName, dirPath string) bool {
|
|||||||
// getRemoteMusicURLOnly 只获取远程音乐URL,不下载不处理(用于实时流式播放)
|
// getRemoteMusicURLOnly 只获取远程音乐URL,不下载不处理(用于实时流式播放)
|
||||||
func getRemoteMusicURLOnly(song, singer string) string {
|
func getRemoteMusicURLOnly(song, singer string) string {
|
||||||
fmt.Printf("[Info] Getting remote music URL for: %s - %s\n", singer, song)
|
fmt.Printf("[Info] Getting remote music URL for: %s - %s\n", singer, song)
|
||||||
|
|
||||||
// 尝试多个 API
|
// 尝试多个 API
|
||||||
apiHosts := []string{
|
apiHosts := []string{
|
||||||
"https://api.yuafeng.cn",
|
"https://api.yuafeng.cn",
|
||||||
"https://api-v2.yuafeng.cn",
|
"https://api-v2.yuafeng.cn",
|
||||||
}
|
}
|
||||||
|
|
||||||
sources := []string{"kuwo", "netease", "migu"}
|
sources := []string{"kuwo", "netease", "migu"}
|
||||||
pathMap := map[string]string{
|
pathMap := map[string]string{
|
||||||
"kuwo": "/API/ly/kwmusic.php",
|
"kuwo": "/API/ly/kwmusic.php",
|
||||||
"netease": "/API/ly/wymusic.php",
|
"netease": "/API/ly/wymusic.php",
|
||||||
"migu": "/API/ly/mgmusic.php",
|
"migu": "/API/ly/mgmusic.php",
|
||||||
}
|
}
|
||||||
|
|
||||||
client := &http.Client{Timeout: 15 * time.Second}
|
client := &http.Client{Timeout: 15 * time.Second}
|
||||||
|
|
||||||
for _, host := range apiHosts {
|
for _, host := range apiHosts {
|
||||||
for _, source := range sources {
|
for _, source := range sources {
|
||||||
path := pathMap[source]
|
path := pathMap[source]
|
||||||
apiURL := fmt.Sprintf("%s%s?song=%s&singer=%s", host, path, url.QueryEscape(song), url.QueryEscape(singer))
|
apiURL := fmt.Sprintf("%s%s?msg=%s-%s&n=1", host, path, url.QueryEscape(song), url.QueryEscape(singer))
|
||||||
|
|
||||||
resp, err := client.Get(apiURL)
|
resp, err := client.Get(apiURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var response YuafengAPIFreeResponse
|
var response YuafengAPIFreeResponse
|
||||||
if err := json.Unmarshal(body, &response); err != nil {
|
if err := json.Unmarshal(body, &response); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.Data.Music != "" {
|
if response.Data.Music != "" {
|
||||||
fmt.Printf("[Success] Got remote URL from %s: %s\n", source, response.Data.Music)
|
fmt.Printf("[Success] Got remote URL from %s: %s\n", source, response.Data.Music)
|
||||||
return response.Data.Music
|
return response.Data.Music
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("[Error] Failed to get remote music URL from all APIs")
|
fmt.Println("[Error] Failed to get remote music URL from all APIs")
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user