Files
MeowBox-Core/helper.go
2025-12-02 20:27:05 +08:00

352 lines
9.3 KiB
Go
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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{}
}