Files
MeowMusicServer/api.go
2025-12-09 16:33:44 +08:00

246 lines
8.5 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"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
)
// APIHandler handles API requests.
func apiHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Server", "MeowMusicEmbeddedServer")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
queryParams := r.URL.Query()
fmt.Printf("[Web Access] Handling request for %s?%s\n", r.URL.Path, queryParams.Encode())
song := queryParams.Get("song")
singer := queryParams.Get("singer")
ip, err := IPhandler(r)
if err != nil {
ip = "0.0.0.0"
}
if song == "" {
musicItem := MusicItem{
FromCache: false,
IP: ip,
}
json.NewEncoder(w).Encode(musicItem)
return
}
// Attempt to retrieve music items from sources.json
sources := readSources()
var musicItem MusicItem
var found bool = false
// Build request scheme
var scheme string
if r.TLS == nil {
scheme = "http"
} else {
scheme = "https"
}
for _, source := range sources {
if source.Title == song {
if singer == "" || source.Artist == singer {
// Determine the protocol for each URL and build accordingly
var audioURL, audioFullURL, m3u8URL, lyricURL, coverURL string
if strings.HasPrefix(source.AudioURL, "http://") {
audioURL = scheme + "://" + r.Host + "/url/http/" + url.QueryEscape(strings.TrimPrefix(source.AudioURL, "http://"))
} else if strings.HasPrefix(source.AudioURL, "https://") {
audioURL = scheme + "://" + r.Host + "/url/https/" + url.QueryEscape(strings.TrimPrefix(source.AudioURL, "https://"))
} else {
audioURL = scheme + "://" + r.Host + "/" + url.QueryEscape(source.AudioURL)
}
if strings.HasPrefix(source.AudioFullURL, "http://") {
audioFullURL = scheme + "://" + r.Host + "/url/http/" + url.QueryEscape(strings.TrimPrefix(source.AudioFullURL, "http://"))
} else if strings.HasPrefix(source.AudioFullURL, "https://") {
audioFullURL = scheme + "://" + r.Host + "/url/https/" + url.QueryEscape(strings.TrimPrefix(source.AudioFullURL, "https://"))
} else {
audioFullURL = scheme + "://" + r.Host + "/" + url.QueryEscape(source.AudioFullURL)
}
if strings.HasPrefix(source.M3U8URL, "http://") {
m3u8URL = scheme + "://" + r.Host + "/url/http/" + url.QueryEscape(strings.TrimPrefix(source.M3U8URL, "http://"))
} else if strings.HasPrefix(source.M3U8URL, "https://") {
m3u8URL = scheme + "://" + r.Host + "/url/https/" + url.QueryEscape(strings.TrimPrefix(source.M3U8URL, "https://"))
} else {
m3u8URL = scheme + "://" + r.Host + "/" + url.QueryEscape(source.M3U8URL)
}
if strings.HasPrefix(source.LyricURL, "http://") {
lyricURL = scheme + "://" + r.Host + "/url/http/" + url.QueryEscape(strings.TrimPrefix(source.LyricURL, "http://"))
} else if strings.HasPrefix(source.LyricURL, "https://") {
lyricURL = scheme + "://" + r.Host + "/url/https/" + url.QueryEscape(strings.TrimPrefix(source.LyricURL, "https://"))
} else {
lyricURL = scheme + "://" + r.Host + "/" + url.QueryEscape(source.LyricURL)
}
if strings.HasPrefix(source.CoverURL, "http://") {
coverURL = scheme + "://" + r.Host + "/url/http/" + url.QueryEscape(strings.TrimPrefix(source.CoverURL, "http://"))
} else if strings.HasPrefix(source.CoverURL, "https://") {
coverURL = scheme + "://" + r.Host + "/url/https/" + url.QueryEscape(strings.TrimPrefix(source.CoverURL, "https://"))
} else {
coverURL = scheme + "://" + r.Host + "/" + url.QueryEscape(source.CoverURL)
}
musicItem = MusicItem{
Title: source.Title,
Artist: source.Artist,
AudioURL: audioURL,
AudioFullURL: audioFullURL,
M3U8URL: m3u8URL,
LyricURL: lyricURL,
CoverURL: coverURL,
Duration: source.Duration,
FromCache: false,
}
found = true
break
}
}
}
// If not found in sources.json, attempt to retrieve from local folder
if !found {
musicItem = getLocalMusicItem(song, singer)
musicItem.FromCache = false
if musicItem.Title != "" {
if musicItem.AudioURL != "" {
musicItem.AudioURL = scheme + "://" + r.Host + musicItem.AudioURL
}
if musicItem.AudioFullURL != "" {
musicItem.AudioFullURL = scheme + "://" + r.Host + musicItem.AudioFullURL
}
if musicItem.M3U8URL != "" {
musicItem.M3U8URL = scheme + "://" + r.Host + musicItem.M3U8URL
}
if musicItem.LyricURL != "" {
musicItem.LyricURL = scheme + "://" + r.Host + musicItem.LyricURL
}
if musicItem.CoverURL != "" {
musicItem.CoverURL = scheme + "://" + r.Host + musicItem.CoverURL
}
found = true
}
}
// If still not found, attempt to retrieve from cache file
if !found {
fmt.Println("[Info] Reading music from cache.")
// Fuzzy matching for singer and song
files, err := filepath.Glob("./cache/*.json")
if err != nil {
fmt.Println("[Error] Error reading cache directory:", err)
return
}
for _, file := range files {
if strings.Contains(filepath.Base(file), song) && (singer == "" || strings.Contains(filepath.Base(file), singer)) {
musicItem, found = readFromCache(file)
if found {
if musicItem.AudioURL != "" {
musicItem.AudioURL = scheme + "://" + r.Host + musicItem.AudioURL
}
if musicItem.AudioFullURL != "" {
musicItem.AudioFullURL = scheme + "://" + r.Host + musicItem.AudioFullURL
}
if musicItem.M3U8URL != "" {
musicItem.M3U8URL = scheme + "://" + r.Host + musicItem.M3U8URL
}
if musicItem.LyricURL != "" {
musicItem.LyricURL = scheme + "://" + r.Host + musicItem.LyricURL
}
if musicItem.CoverURL != "" {
musicItem.CoverURL = scheme + "://" + r.Host + musicItem.CoverURL
}
musicItem.FromCache = true
break
}
}
}
}
// If still not found, request and cache the music item in a separate goroutine
// 直接进行流式播放
if !found {
encodedSong := url.QueryEscape(song)
encodedSinger := url.QueryEscape(singer)
streamURL := scheme + "://" + r.Host + "/stream_live?song=" + encodedSong + "&singer=" + encodedSinger
fmt.Println("[Info] Updating music item cache from API request.")
musicItem = requestAndCacheMusic(song, singer)
fmt.Println("[Info] Music item cache updated.")
musicItem.FromCache = false
musicItem.AudioURL = streamURL
musicItem.AudioFullURL = streamURL
musicItem.M3U8URL = scheme + "://" + r.Host + musicItem.M3U8URL
musicItem.LyricURL = scheme + "://" + r.Host + musicItem.LyricURL
musicItem.CoverURL = scheme + "://" + r.Host + musicItem.CoverURL
found = true
}
// If still not found, return an empty MusicItem
if !found {
musicItem = MusicItem{
FromCache: false,
IP: ip,
}
} else {
musicItem.IP = ip
}
encoder := json.NewEncoder(w)
encoder.SetEscapeHTML(false)
encoder.Encode(musicItem)
}
// streamLiveHandler 实时流式转码接口 - 边下载边播放,无需等待!
func streamLiveHandler(w http.ResponseWriter, r *http.Request) {
// 设置 CORS 和音频相关头
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Accept-Ranges", "bytes")
queryParams := r.URL.Query()
song := queryParams.Get("song")
singer := queryParams.Get("singer")
fmt.Printf("[Stream Live] Request: song=%s, singer=%s\n", song, singer)
if song == "" {
http.Error(w, "Missing song parameter", http.StatusBadRequest)
return
}
// 1. 检查缓存是否存在
dirName := fmt.Sprintf("./files/cache/music/%s-%s", singer, song)
cachedFile := filepath.Join(dirName, "music.mp3")
if _, err := os.Stat(cachedFile); err == nil {
// 缓存存在,直接返回文件
fmt.Printf("[Stream Live] Serving from cache: %s\n", cachedFile)
w.Header().Set("Content-Type", "audio/mpeg")
http.ServeFile(w, r, cachedFile)
return
}
// 2. 缓存不存在获取远程URL并实时流式转码
fmt.Printf("[Stream Live] Cache miss, fetching from API...\n")
// 调用枫雨API获取远程音乐URL不下载只获取URL
remoteURL := getRemoteMusicURLOnly(song, singer)
if remoteURL == "" {
http.Error(w, "Failed to get remote music URL", http.StatusNotFound)
return
}
fmt.Printf("[Stream Live] Starting live stream from: %s\n", remoteURL)
// 4. 实时流式转码
if err := streamConvertToWriter(remoteURL, w); err != nil {
fmt.Printf("[Stream Live] Error: %v\n", err)
// 错误可能已经发送了部分数据,无法再发送错误响应
}
}