rebase
This commit is contained in:
351
helper.go
Executable file
351
helper.go
Executable 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{}
|
||||
}
|
||||
Reference in New Issue
Block a user