Flow optimization

This commit is contained in:
2025-12-02 20:27:05 +08:00
parent 0c097d63a6
commit d9abb0b18b
4 changed files with 28 additions and 57 deletions

17
api.go
View File

@@ -165,6 +165,7 @@ func apiHandler(w http.ResponseWriter, r *http.Request) {
} }
// If still not found, request and cache the music item in a separate goroutine // If still not found, request and cache the music item in a separate goroutine
// 直接进行流式播放
if !found { if !found {
fmt.Println("[Info] Updating music item cache from API request.") fmt.Println("[Info] Updating music item cache from API request.")
musicItem = requestAndCacheMusic(song, singer) musicItem = requestAndCacheMusic(song, singer)
@@ -196,18 +197,18 @@ func streamLiveHandler(w http.ResponseWriter, r *http.Request) {
// 设置 CORS 和音频相关头 // 设置 CORS 和音频相关头
w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Accept-Ranges", "bytes") w.Header().Set("Accept-Ranges", "bytes")
queryParams := r.URL.Query() queryParams := r.URL.Query()
song := queryParams.Get("song") song := queryParams.Get("song")
singer := queryParams.Get("singer") singer := queryParams.Get("singer")
fmt.Printf("[Stream Live] Request: song=%s, singer=%s\n", song, singer) fmt.Printf("[Stream Live] Request: song=%s, singer=%s\n", song, singer)
if song == "" { if song == "" {
http.Error(w, "Missing song parameter", http.StatusBadRequest) http.Error(w, "Missing song parameter", http.StatusBadRequest)
return return
} }
// 1. 检查缓存是否存在 // 1. 检查缓存是否存在
dirName := fmt.Sprintf("./files/cache/music/%s-%s", singer, song) dirName := fmt.Sprintf("./files/cache/music/%s-%s", singer, song)
cachedFile := filepath.Join(dirName, "music.mp3") cachedFile := filepath.Join(dirName, "music.mp3")
@@ -218,19 +219,19 @@ func streamLiveHandler(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, cachedFile) http.ServeFile(w, r, cachedFile)
return return
} }
// 2. 缓存不存在获取远程URL并实时流式转码 // 2. 缓存不存在获取远程URL并实时流式转码
fmt.Printf("[Stream Live] Cache miss, fetching from API...\n") fmt.Printf("[Stream Live] Cache miss, fetching from API...\n")
// 调用枫雨API获取远程音乐URL不下载只获取URL // 调用枫雨API获取远程音乐URL不下载只获取URL
remoteURL := getRemoteMusicURLOnly(song, singer) remoteURL := getRemoteMusicURLOnly(song, singer)
if remoteURL == "" { if remoteURL == "" {
http.Error(w, "Failed to get remote music URL", http.StatusNotFound) http.Error(w, "Failed to get remote music URL", http.StatusNotFound)
return return
} }
fmt.Printf("[Stream Live] Starting live stream from: %s\n", remoteURL) fmt.Printf("[Stream Live] Starting live stream from: %s\n", remoteURL)
// 4. 实时流式转码 // 4. 实时流式转码
if err := streamConvertToWriter(remoteURL, w); err != nil { if err := streamConvertToWriter(remoteURL, w); err != nil {
fmt.Printf("[Stream Live] Error: %v\n", err) fmt.Printf("[Stream Live] Error: %v\n", err)

33
file.go
View File

@@ -1,14 +1,12 @@
package main package main
import ( import (
"fmt"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
) )
// ListFiles function: Traverse all files in the specified directory and return a slice of the file path // ListFiles function: Traverse all files in the specified directory and return a slice of the file path
@@ -122,36 +120,7 @@ func fileHandler(w http.ResponseWriter, r *http.Request) {
// 特殊处理空music.mp3 // 特殊处理空music.mp3
isEmptyMusic := (err == nil && len(fileContent) == 0 && strings.HasSuffix(filePath, "/music.mp3")) isEmptyMusic := (err == nil && len(fileContent) == 0 && strings.HasSuffix(filePath, "/music.mp3"))
if err != nil || isEmptyMusic { if err != nil || isEmptyMusic {
// 智能等待 // 没有/空的music.mp3文件直接返回404
if strings.HasPrefix(filePath, "/cache/music/") {
maxWaitSec := 10
if strings.HasSuffix(filePath, "/music.mp3") {
maxWaitSec = 60
}
// 指数退避重试:快速响应文件生成
start := time.Now()
for i := 0; ; i++ {
elapsed := time.Since(start)
if elapsed.Seconds() > float64(maxWaitSec) {
break
}
// 重试间隔 = min(200ms * 2^i, 1s)
waitDuration := time.Duration(200*(1<<uint(i))) * time.Millisecond
if waitDuration > time.Second {
waitDuration = time.Second
}
time.Sleep(waitDuration)
// 只检查解码后的主路径(避免冗余检查)
if info, statErr := os.Stat(fullPath); statErr == nil && (!isEmptyMusic || info.Size() > 0) {
http.ServeFile(w, r, fullPath)
return
}
}
fmt.Printf("[FAST] Timeout after %.1fs waiting for: %s\n", time.Since(start).Seconds(), fullPath)
}
NotFoundHandler(w, r) NotFoundHandler(w, r)
return return
} }

View File

@@ -85,7 +85,7 @@ func readFromCache(path string) (MusicItem, bool) {
if err != nil || !info.IsDir() { if err != nil || !info.IsDir() {
return MusicItem{}, false return MusicItem{}, false
} }
dirName := filepath.Base(path) dirName := filepath.Base(path)
parts := strings.SplitN(dirName, "-", 2) parts := strings.SplitN(dirName, "-", 2)
var artist, title string var artist, title string
@@ -95,7 +95,7 @@ func readFromCache(path string) (MusicItem, bool) {
} else { } else {
title = dirName title = dirName
} }
return getLocalMusicItem(title, artist), true return getLocalMusicItem(title, artist), true
} }
@@ -115,10 +115,10 @@ func requestAndCacheMusic(song, singer string) MusicItem {
// 直接从远程URL流式转码边下载边转码超快 // 直接从远程URL流式转码边下载边转码超快
func streamConvertAudio(inputURL, outputFile string) error { func streamConvertAudio(inputURL, outputFile string) error {
fmt.Printf("[Info] Stream converting from URL (fast mode)\n") fmt.Printf("[Info] Stream converting from URL (fast mode)\n")
// 先写入临时文件,完成后再重命名(避免读取到不完整的文件) // 先写入临时文件,完成后再重命名(避免读取到不完整的文件)
tempFile := outputFile + ".tmp" tempFile := outputFile + ".tmp"
// ffmpeg 直接读取远程 URL 并转码 // ffmpeg 直接读取远程 URL 并转码
// -t 600: 只下载前10分钟减少80%下载量! // -t 600: 只下载前10分钟减少80%下载量!
// 移除 reconnect 参数,避免兼容性问题 // 移除 reconnect 参数,避免兼容性问题
@@ -130,14 +130,14 @@ func streamConvertAudio(inputURL, outputFile string) error {
"-ac", "1", "-ar", "24000", "-b:a", "32k", "-q:a", "9", "-ac", "1", "-ar", "24000", "-b:a", "32k", "-q:a", "9",
"-bufsize", "64k", "-bufsize", "64k",
tempFile) tempFile)
err := cmd.Run() err := cmd.Run()
if err != nil { if err != nil {
fmt.Printf("[Error] Stream convert failed: %v\n", err) fmt.Printf("[Error] Stream convert failed: %v\n", err)
os.Remove(tempFile) // 清理临时文件 os.Remove(tempFile) // 清理临时文件
return err return err
} }
// 检查生成的文件大小 // 检查生成的文件大小
fileInfo, err := os.Stat(tempFile) fileInfo, err := os.Stat(tempFile)
if err != nil || fileInfo.Size() < 1024 { if err != nil || fileInfo.Size() < 1024 {
@@ -152,7 +152,7 @@ func streamConvertAudio(inputURL, outputFile string) error {
fmt.Printf("[Error] Failed to rename temp file: %v\n", err) fmt.Printf("[Error] Failed to rename temp file: %v\n", err)
return err return err
} }
fmt.Printf("[Success] Stream convert completed: %s\n", outputFile) fmt.Printf("[Success] Stream convert completed: %s\n", outputFile)
return nil return nil
} }
@@ -160,30 +160,31 @@ func streamConvertAudio(inputURL, outputFile string) error {
// 实时流式转码到 HTTP Writer边下载边播放 // 实时流式转码到 HTTP Writer边下载边播放
func streamConvertToWriter(inputURL string, w http.ResponseWriter) error { func streamConvertToWriter(inputURL string, w http.ResponseWriter) error {
fmt.Printf("[Info] Live streaming from URL: %s\n", inputURL) fmt.Printf("[Info] Live streaming from URL: %s\n", inputURL)
// ffmpeg 边下载边转码,输出到 stdout // ffmpeg 边下载边转码,输出到 stdout
cmd := exec.Command("ffmpeg", cmd := exec.Command("ffmpeg",
"-i", inputURL, "-i", inputURL,
"-threads", "0", "-threads", "0",
"-ac", "1", "-ar", "24000", "-b:a", "32k", "-q:a", "9", "-ac", "1", "-ar", "24000", "-b:a", "32k", "-q:a", "9",
"-f", "mp3", "-f", "mp3",
"-map_metadata", "-1",
"pipe:1") // 输出到 stdout "pipe:1") // 输出到 stdout
// 获取 stdout pipe // 获取 stdout pipe
stdout, err := cmd.StdoutPipe() stdout, err := cmd.StdoutPipe()
if err != nil { if err != nil {
return fmt.Errorf("failed to get stdout pipe: %v", err) return fmt.Errorf("failed to get stdout pipe: %v", err)
} }
// 启动 ffmpeg // 启动 ffmpeg
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start ffmpeg: %v", err) return fmt.Errorf("failed to start ffmpeg: %v", err)
} }
// 设置响应头 // 设置响应头
w.Header().Set("Content-Type", "audio/mpeg") w.Header().Set("Content-Type", "audio/mpeg")
// 移除 Transfer-Encoding: chunked让 Go 自动处理 // 移除 Transfer-Encoding: chunked让 Go 自动处理
// 边读边写到 HTTP response // 边读边写到 HTTP response
buf := make([]byte, 8192) buf := make([]byte, 8192)
for { for {
@@ -198,7 +199,7 @@ func streamConvertToWriter(inputURL string, w http.ResponseWriter) error {
break break
} }
} }
cmd.Wait() cmd.Wait()
fmt.Printf("[Success] Live streaming completed\n") fmt.Printf("[Success] Live streaming completed\n")
return nil return nil

View File

@@ -11,7 +11,7 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Server", "MeowMusicEmbeddedServer") w.Header().Set("Server", "MeowMusicEmbeddedServer")
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Printf("[Web Access] Handling request for %s\n", r.URL.Path) fmt.Printf("[Web Access] Handling request for %s\n", r.URL.Path)
// Serve full music app for both / and /app // Serve full music app for both / and /app
if r.URL.Path == "/" || r.URL.Path == "/app" { if r.URL.Path == "/" || r.URL.Path == "/app" {
appPath := filepath.Join("theme", "full-app.html") appPath := filepath.Join("theme", "full-app.html")
@@ -21,7 +21,7 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
} }
// Test version available at /test // Test version available at /test
if r.URL.Path == "/test" { if r.URL.Path == "/test" {
testPath := filepath.Join("theme", "test-app.html") testPath := filepath.Join("theme", "test-app.html")
@@ -31,7 +31,7 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
} }
// Access classic interface via /classic // Access classic interface via /classic
if r.URL.Path == "/classic" { if r.URL.Path == "/classic" {
indexPath := filepath.Join("theme", "index.html") indexPath := filepath.Join("theme", "index.html")
@@ -43,7 +43,7 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
defaultIndexPage(w) defaultIndexPage(w)
return return
} }
if r.URL.Path != "/" { if r.URL.Path != "/" {
fileHandler(w, r) fileHandler(w, r)
return return