Flow optimization
This commit is contained in:
17
api.go
17
api.go
@@ -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
33
file.go
@@ -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
|
||||
}
|
||||
|
||||
27
helper.go
27
helper.go
@@ -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
|
||||
|
||||
8
index.go
8
index.go
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user