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 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
33
file.go
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
27
helper.go
27
helper.go
@@ -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
|
||||||
|
|||||||
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("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
|
||||||
|
|||||||
Reference in New Issue
Block a user