352 lines
9.3 KiB
Go
Executable File
352 lines
9.3 KiB
Go
Executable File
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{}
|
||
}
|