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

33
file.go
View File

@@ -1,14 +1,12 @@
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
)
// 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
isEmptyMusic := (err == nil && len(fileContent) == 0 && strings.HasSuffix(filePath, "/music.mp3"))
if err != nil || isEmptyMusic {
// 智能等待
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)
}
// 没有/空的music.mp3文件直接返回404
NotFoundHandler(w, r)
return
}

View File

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

View File

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