This commit is contained in:
2025-12-09 16:33:44 +08:00
parent 345af5e2a3
commit 1d42f5ea50
49 changed files with 12015 additions and 1 deletions

351
helper.go Executable file
View File

@@ -0,0 +1,351 @@
package main
import (
"encoding/json"
"fmt"
"io"
"mime"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
)
// Source is an alias for MusicItem (used in sources.json)
type Source = MusicItem
// Download file from URL
func downloadFile(filepath string, url string) error {
// Create the file
out, err := os.Create(filepath)
if err != nil {
return err
}
defer out.Close()
// Get the data
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
// Check server response
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("bad status: %s", resp.Status)
}
// Write the body to file
_, err = io.Copy(out, resp.Body)
if err != nil {
return err
}
return nil
}
// Get IP address from request
func IPhandler(r *http.Request) (string, error) {
ip := r.Header.Get("X-Real-IP")
if ip == "" {
ip = r.Header.Get("X-Forwarded-For")
}
if ip == "" {
ip, _, _ = net.SplitHostPort(r.RemoteAddr)
}
return ip, nil
}
// Read sources from sources.json
func readSources() []Source {
file, err := os.Open("sources.json")
if err != nil {
return []Source{}
}
defer file.Close()
var sources []Source
decoder := json.NewDecoder(file)
err = decoder.Decode(&sources)
if err != nil {
return []Source{}
}
return sources
}
// Read music from cache
func readFromCache(path string) (MusicItem, bool) {
// Logic to read music item from a cached folder path
// This assumes path is like "files/cache/music/Artist-Song"
info, err := os.Stat(path)
if err != nil || !info.IsDir() {
return MusicItem{}, false
}
dirName := filepath.Base(path)
parts := strings.SplitN(dirName, "-", 2)
var artist, title string
if len(parts) == 2 {
artist = parts[0]
title = parts[1]
} else {
title = dirName
}
return getLocalMusicItem(title, artist), true
}
// Request and cache music from API
func requestAndCacheMusic(song, singer string) MusicItem {
// Try different sources in priority order
sources := []string{"kuwo", "netease", "migu", "baidu"}
for _, source := range sources {
item := YuafengAPIResponseHandler(source, song, singer)
if item.Title != "" {
return item
}
}
return 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 参数,避免兼容性问题
// 添加 -bufsize 以提高稳定性
cmd := exec.Command("ffmpeg", "-y",
"-t", "600",
"-i", inputURL,
"-threads", "0",
"-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 {
fmt.Printf("[Error] Stream converted file is too small or empty\n")
os.Remove(tempFile)
return fmt.Errorf("converted file is too small")
}
// 转码完成后重命名为最终文件
err = os.Rename(tempFile, outputFile)
if err != nil {
fmt.Printf("[Error] Failed to rename temp file: %v\n", err)
return err
}
fmt.Printf("[Success] Stream convert completed: %s\n", outputFile)
return nil
}
// 实时流式转码到 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 {
n, err := stdout.Read(buf)
if n > 0 {
w.Write(buf[:n])
if f, ok := w.(http.Flusher); ok {
f.Flush() // 立即发送给客户端
}
}
if err != nil {
break
}
}
cmd.Wait()
fmt.Printf("[Success] Live streaming completed\n")
return nil
}
// Helper function for identifying file formats
func getMusicFileExtension(url string) (string, error) {
resp, err := http.Head(url)
if err != nil {
return "", err
}
// Get file format from Content-Type header
contentType := resp.Header.Get("Content-Type")
ext, _, err := mime.ParseMediaType(contentType)
if err != nil {
return "", err
}
// Identify file extension based on file format
switch ext {
case "audio/mpeg":
return ".mp3", nil
case "audio/flac":
return ".flac", nil
case "audio/x-flac":
return ".flac", nil
case "audio/wav":
return ".wav", nil
case "audio/aac":
return ".aac", nil
case "audio/ogg":
return ".ogg", nil
case "application/octet-stream":
// Try to guess file format from URL or other information
if strings.Contains(url, ".mp3") {
return ".mp3", nil
} else if strings.Contains(url, ".flac") {
return ".flac", nil
} else if strings.Contains(url, ".wav") {
return ".wav", nil
} else if strings.Contains(url, ".aac") {
return ".aac", nil
} else if strings.Contains(url, ".ogg") {
return ".ogg", nil
} else {
return "", fmt.Errorf("unknown file format from Content-Type and URL: %s", contentType)
}
default:
return "", fmt.Errorf("unknown file format: %s", ext)
}
}
// Helper function for identifying file formats
func GetDuration(filePath string) int {
fmt.Printf("[Info] Get duration of obtaining music file %s\n", filePath)
// Use ffprobe to get audio duration
output, err := exec.Command("ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", filePath).Output()
if err != nil {
fmt.Println("[Error] Error getting audio duration:", err)
return 0
}
duration, err := strconv.ParseFloat(strings.TrimSpace(string(output)), 64)
if err != nil {
fmt.Println("[Error] Error converting duration to float:", err)
return 0
}
return int(duration)
}
// Helper function to compress and segment audio file
func compressAndSegmentAudio(inputFile, outputDir string) error {
fmt.Printf("[Info] Compress and segment audio file %s\n", inputFile)
// Compress music files
outputFile := filepath.Join(outputDir, "music.mp3")
cmd := exec.Command("ffmpeg", "-i", inputFile, "-ac", "1", "-ab", "32k", "-ar", "24000", outputFile)
err := cmd.Run()
if err != nil {
return err
}
return nil
}
// Helper function to obtain music data from local folder
func getLocalMusicItem(song, singer string) MusicItem {
musicDir := "./files/music"
fmt.Println("[Info] Reading local folder music.")
files, err := os.ReadDir(musicDir)
if err != nil {
fmt.Println("[Error] Failed to read local music directory:", err)
return MusicItem{}
}
for _, file := range files {
if file.IsDir() {
if singer == "" {
if strings.Contains(file.Name(), song) {
dirPath := filepath.Join(musicDir, file.Name())
// Extract artist and title from the directory name
parts := strings.SplitN(file.Name(), "-", 2)
var artist, title string
if len(parts) == 2 {
artist = parts[0]
title = parts[1]
} else {
title = file.Name()
}
basePath := "/cache/music/" + url.QueryEscape(file.Name())
return MusicItem{
Title: title,
Artist: artist,
CoverURL: basePath + "/cover.jpg",
LyricURL: basePath + "/lyric.lrc",
AudioFullURL: basePath + "/music.mp3",
AudioURL: basePath + "/music.mp3",
M3U8URL: basePath + "/music.m3u8",
Duration: GetDuration(filepath.Join(dirPath, "music.mp3")),
}
}
} else {
if strings.Contains(file.Name(), song) && strings.Contains(file.Name(), singer) {
dirPath := filepath.Join(musicDir, file.Name())
// Extract artist and title from the directory name
parts := strings.SplitN(file.Name(), "-", 2)
var artist, title string
if len(parts) == 2 {
artist = parts[0]
title = parts[1]
} else {
title = file.Name()
}
basePath := "/cache/music/" + url.QueryEscape(file.Name())
return MusicItem{
Title: title,
Artist: artist,
CoverURL: basePath + "/cover.jpg",
LyricURL: basePath + "/lyric.lrc",
AudioFullURL: basePath + "/music.mp3",
AudioURL: basePath + "/music.mp3",
M3U8URL: basePath + "/music.m3u8",
Duration: GetDuration(filepath.Join(dirPath, "music.mp3")),
}
}
}
}
}
return MusicItem{}
}