Compare commits

11 Commits

Author SHA1 Message Date
71b65e3707 add bug issue 2025-12-06 22:00:49 +08:00
98365d7859 add issue template 2025-12-06 19:49:06 +08:00
161cbdfebf add issue template 2025-12-06 19:48:28 +08:00
415172733f add funding 2025-12-03 17:12:20 +08:00
0e3e655939 add funding 2025-12-03 17:09:20 +08:00
6e1fd62247 Version comparison modification 2025-12-02 22:04:59 +08:00
5d54f8eb12 Resolve the issue of slow initial playback. 2025-12-02 21:56:51 +08:00
2cd8d0dc37 Flow enhancement 2025-12-02 21:41:21 +08:00
d9abb0b18b Flow optimization 2025-12-02 20:27:05 +08:00
0c097d63a6 Fixed issues such as slow/unable to play music through stream_pcm. 2025-12-02 19:46:41 +08:00
a8028bdc28 permissions 2025-12-02 18:53:05 +08:00
11 changed files with 252 additions and 232 deletions

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
github: [MoeCinnamo]
custom: ["https://afdian.com/a/MoeCinnamo"]

60
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View File

@@ -0,0 +1,60 @@
name: 🐞 Report error
description: Report an error (bug), please first check the FAQ and search the Issue list for the issue you want to raise.
title: "[Bug] "
body:
- type: checkboxes
id: check-answer
attributes:
label: Solution check
description: Please ensure that you have completed all of the following operations.
options:
- label: I have searched [Issues](https://github.com/OmniX-Space/MeowBox-Core/issues).However, no similar issues were found.
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: A clear and concise description of what is expected to happen
validations:
required: true
- type: textarea
id: actual-behavior
attributes:
label: Actual behavior
description: A clear and concise description of what actually happened.
validations:
required: true
- type: input
id: version
attributes:
label: MeowBox-Core version
description: What version of MeowBox-Core are you using?
placeholder: e.g. v0.0.2
validations:
required: true
- type: input
id: last-known-working-version
attributes:
label: The final normal version
description: If so, please fill in the final normal version here.
placeholder: e.g. v0.0.1
- type: input
id: operating-system-version
attributes:
label: Operating system version
description: |
What version of operating system are you using?
On macOS, click on "Apple Menu > About This Machine";
On Linux, execute the `lsc_release` or `uname - a` command;
On Windows, click the Start button > Settings > System > About.
placeholder: "e.g. macOS 11.2.3, Windows 10 20H2, Debian 12.1.0"
validations:
required: true
- type: textarea
id: additional-information
attributes:
label: Additional information
description: If your issue needs further explanation or you are facing a difficult-to-reproduce issue, please add more information here. (You can directly drag and drop images or videos into the text box)

36
.github/ISSUE_TEMPLATE/feature.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: ✨ Function request
description: To come up with an idea for this project, please first review the frequently asked questions and search the Issue list to see if there are any issues you want to raise.
title: "[Feature] "
body:
- type: checkboxes
id: check-answer
attributes:
label: Solution check
description: Please ensure that you have completed all of the following operations.
options:
- label: I have searched [Issues](https://github.com/OmniX-Space/MeowBox-Core/issues).However, no similar issues were found.
required: true
- type: textarea
id: problem-description
attributes:
label: Problem description
description: Please add a clear and concise description of the problem you want to solve.
validations:
required: true
- type: textarea
id: proposed-solution
attributes:
label: Describe the solution you want
description: Briefly and clearly describe what you are about to happen
validations:
required: true
- type: textarea
id: alternatives-considered
attributes:
label: Describe the alternative solutions you have considered
description: A concise and clear description of all alternative solutions or features you have considered
- type: textarea
id: additional-information
attributes:
label: Additional information
description: If your question requires further explanation or if you would like to express other content, please add more information here. (Simply drag the image/video to the editing box to add it)

View File

@@ -16,11 +16,16 @@ on:
jobs:
build:
runs-on: self-hosted
permissions:
contents: write
actions: write
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: v0.0.2-dev
fetch-depth: 0
- name: Test code
run: |
echo "Testing code"
@@ -70,6 +75,13 @@ jobs:
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w" -trimpath -o MeowEmbeddedMusicServer-Darwin-amd64 .
echo "Build darwin arm64 binary"
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "-s -w" -trimpath -o MeowEmbeddedMusicServer-Darwin-arm64 .
- name: Get latest tag
id: get_latest_tag
run: |
git fetch --tags
LATEST_TAG=$(git tag --sort=-refname | head -n 1)
echo "Latest tag found: $LATEST_TAG"
echo "previous_tag=$LATEST_TAG" >> $GITHUB_OUTPUT
- name: Upload binaries
uses: softprops/action-gh-release@v1
with:
@@ -90,7 +102,7 @@ jobs:
- macOS amd64
- macOS arm64
**Full Changelog**: https://github.com/OmniX-Space/MeowBox-Core/compare/v0.0.1...${{ github.sha }}
**Full Changelog**: https://github.com/OmniX-Space/MeowBox-Core/compare/${{ steps.get_latest_tag.outputs.previous_tag }}...${{ github.sha }}
files: |
MeowEmbeddedMusicServer-Linux-i386
MeowEmbeddedMusicServer-Linux-amd64
@@ -103,6 +115,6 @@ jobs:
prerelease: ${{ github.event.inputs.build_type == 'pre' }}
make_latest: ${{ github.event.inputs.build_type != 'pre' }}
env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.CUSTOM_GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# GITHUB_TOKEN: ${{ secrets.CUSTOM_GITHUB_TOKEN }}
continue-on-error: true

12
api.go
View File

@@ -165,13 +165,17 @@ func apiHandler(w http.ResponseWriter, r *http.Request) {
}
// 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 = scheme + "://" + r.Host + musicItem.AudioURL
musicItem.AudioFullURL = scheme + "://" + r.Host + musicItem.AudioFullURL
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
@@ -188,7 +192,9 @@ func apiHandler(w http.ResponseWriter, r *http.Request) {
musicItem.IP = ip
}
json.NewEncoder(w).Encode(musicItem)
encoder := json.NewEncoder(w)
encoder.SetEscapeHTML(false)
encoder.Encode(musicItem)
}
// streamLiveHandler 实时流式转码接口 - 边下载边播放,无需等待!

202
file.go
View File

@@ -1,14 +1,12 @@
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
)
// ListFiles function: Traverse all files in the specified directory and return a slice of the file path
@@ -54,10 +52,17 @@ func GetFileContent(filePath string) ([]byte, error) {
// fileHandler function: Handle file requests
func fileHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Server", "MeowMusicEmbeddedServer")
// Obtain the path of the request
filePath := r.URL.Path
// Check if the request path starts with "/url/"
// 提前URL解码
decodedPath, decodeErr := url.QueryUnescape(filePath)
if decodeErr == nil {
// 兼容历史数据:将+替换为空格(仅当解码成功时)
decodedPath = strings.ReplaceAll(decodedPath, "+", " ")
filePath = decodedPath // 后续统一使用解码后路径
}
// 处理 /url/ 远程请求(保持不变)
if strings.HasPrefix(filePath, "/url/") {
// Extract the URL after "/url/"
urlPath := filePath[len("/url/"):]
@@ -102,161 +107,54 @@ func fileHandler(w http.ResponseWriter, r *http.Request) {
NotFoundHandler(w, r)
return
}
// Set appropriate Content-Type based on file extension
ext := filepath.Ext(decodedURL)
switch ext {
case ".mp3":
w.Header().Set("Content-Type", "audio/mpeg")
case ".wav":
w.Header().Set("Content-Type", "audio/wav")
case ".flac":
w.Header().Set("Content-Type", "audio/flac")
case ".aac":
w.Header().Set("Content-Type", "audio/aac")
case ".ogg":
w.Header().Set("Content-Type", "audio/ogg")
case ".m4a":
w.Header().Set("Content-Type", "audio/mp4")
case ".amr":
w.Header().Set("Content-Type", "audio/amr")
case ".jpg", ".jpeg":
w.Header().Set("Content-Type", "image/jpeg")
case ".png":
w.Header().Set("Content-Type", "image/png")
case ".gif":
w.Header().Set("Content-Type", "image/gif")
case ".bmp":
w.Header().Set("Content-Type", "image/bmp")
case ".svg":
w.Header().Set("Content-Type", "image/svg+xml")
case ".webp":
w.Header().Set("Content-Type", "image/webp")
case ".txt":
w.Header().Set("Content-Type", "text/plain")
case ".lrc":
w.Header().Set("Content-Type", "text/plain")
case ".mrc":
w.Header().Set("Content-Type", "text/plain")
case ".json":
w.Header().Set("Content-Type", "application/json")
default:
w.Header().Set("Content-Type", "application/octet-stream")
}
setContentType(w, decodedURL)
// Write file content to response
w.Write(fileContent)
return
}
// Construct the complete file path
fullFilePath := filepath.Join("./files", filePath)
// 统一使用解码后路径
fullPath := filepath.Join("./files", filePath)
fileContent, err := GetFileContent(fullPath)
// Try replacing '+' with ' ' and check if the file exists
tempFilePath := strings.ReplaceAll(fullFilePath, "+", " ")
if _, err := os.Stat(tempFilePath); err == nil {
fullFilePath = tempFilePath
// 特殊处理空music.mp3
isEmptyMusic := (err == nil && len(fileContent) == 0 && strings.HasSuffix(filePath, "/music.mp3"))
if err != nil || isEmptyMusic {
// 没有/空的music.mp3文件直接返回404
NotFoundHandler(w, r)
return
}
// Get file content
fileContent, err := GetFileContent(fullFilePath)
// 检查是否为空文件(特别是 music.mp3如果是空的也视为不存在需要等待转码
if err == nil && len(fileContent) == 0 && strings.HasSuffix(filePath, "/music.mp3") {
err = fmt.Errorf("file is empty")
}
if err != nil {
// If file not found, try replacing ' ' with '+' and check again
tempFilePath = strings.ReplaceAll(fullFilePath, " ", "+")
fileContent, err = GetFileContent(tempFilePath)
// 同样检查带 + 的路径是否为空
if err == nil && len(fileContent) == 0 && strings.HasSuffix(filePath, "/music.mp3") {
err = fmt.Errorf("file is empty")
}
if err != nil {
// 特殊处理:如果请求的是缓存中的文件,等待后台处理完成
fmt.Printf("[Web Access] File not found, checking path prefix: %s\n", filePath)
if strings.HasPrefix(filePath, "/cache/music/") {
// music.mp3 等待最多 60 秒,歌词等待最多 10 秒
maxWait := 10
if strings.HasSuffix(filePath, "/music.mp3") {
maxWait = 60
}
// URL 解码路径(处理中文文件名)
decodedPath, _ := url.QueryUnescape(filePath)
decodedPath = strings.ReplaceAll(decodedPath, "+", " ") // + 转空格
decodedFullPath := filepath.Join("./files", decodedPath)
fmt.Printf("[Web Access] Waiting for file: %s (max %d seconds)\n", decodedFullPath, maxWait)
for i := 0; i < maxWait; i++ {
time.Sleep(1 * time.Second)
// 检查 URL 解码后的路径
if _, err := os.Stat(decodedFullPath); err == nil {
fmt.Printf("[Web Access] File ready after %d seconds: %s\n", i+1, decodedFullPath)
http.ServeFile(w, r, decodedFullPath)
return
}
// 检查原始路径
if _, err := os.Stat(fullFilePath); err == nil {
http.ServeFile(w, r, fullFilePath)
return
}
// 检查带 + 的路径
tempPath := strings.ReplaceAll(fullFilePath, " ", "+")
if _, err := os.Stat(tempPath); err == nil {
http.ServeFile(w, r, tempPath)
return
}
}
}
NotFoundHandler(w, r)
return
}
}
// Set appropriate Content-Type based on file extension
ext := filepath.Ext(filePath)
switch ext {
case ".mp3":
w.Header().Set("Content-Type", "audio/mpeg")
case ".wav":
w.Header().Set("Content-Type", "audio/wav")
case ".flac":
w.Header().Set("Content-Type", "audio/flac")
case ".aac":
w.Header().Set("Content-Type", "audio/aac")
case ".ogg":
w.Header().Set("Content-Type", "audio/ogg")
case ".m4a":
w.Header().Set("Content-Type", "audio/mp4")
case ".amr":
w.Header().Set("Content-Type", "audio/amr")
case ".jpg", ".jpeg":
w.Header().Set("Content-Type", "image/jpeg")
case ".png":
w.Header().Set("Content-Type", "image/png")
case ".gif":
w.Header().Set("Content-Type", "image/gif")
case ".bmp":
w.Header().Set("Content-Type", "image/bmp")
case ".svg":
w.Header().Set("Content-Type", "image/svg+xml")
case ".webp":
w.Header().Set("Content-Type", "image/webp")
case ".txt":
w.Header().Set("Content-Type", "text/plain")
case ".lrc":
w.Header().Set("Content-Type", "text/plain")
case ".mrc":
w.Header().Set("Content-Type", "text/plain")
case ".json":
w.Header().Set("Content-Type", "application/json")
default:
w.Header().Set("Content-Type", "application/octet-stream")
}
// Write file content to response
// 避免重复Content-Type设置
setContentType(w, filePath)
w.Write(fileContent)
}
func setContentType(w http.ResponseWriter, path string) {
ext := strings.ToLower(filepath.Ext(path))
contentTypes := map[string]string{
".mp3": "audio/mpeg",
".wav": "audio/wav",
".flac": "audio/flac",
".aac": "audio/aac",
".ogg": "audio/ogg",
".m4a": "audio/mp4",
".amr": "audio/amr",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".bmp": "image/bmp",
".svg": "image/svg+xml",
".webp": "image/webp",
".txt": "text/plain",
".lrc": "text/plain",
".mrc": "text/plain",
".json": "application/json",
}
if ct, ok := contentTypes[ext]; ok {
w.Header().Set("Content-Type", ct)
} else {
w.Header().Set("Content-Type", "application/octet-stream")
}
}

View File

View File

@@ -1 +1,6 @@
{}
{
"favorite": {
"name": "我喜欢",
"songs": []
}
}

View File

@@ -167,6 +167,7 @@ func streamConvertToWriter(inputURL string, w http.ResponseWriter) error {
"-threads", "0",
"-ac", "1", "-ar", "24000", "-b:a", "32k", "-q:a", "9",
"-f", "mp3",
"-map_metadata", "-1",
"pipe:1") // 输出到 stdout
// 获取 stdout pipe

View File

@@ -388,7 +388,7 @@ func getRemoteMusicURLOnly(song, singer string) string {
for _, host := range apiHosts {
for _, source := range sources {
path := pathMap[source]
apiURL := fmt.Sprintf("%s%s?song=%s&singer=%s", host, path, url.QueryEscape(song), url.QueryEscape(singer))
apiURL := fmt.Sprintf("%s%s?msg=%s-%s&n=1", host, path, url.QueryEscape(song), url.QueryEscape(singer))
resp, err := client.Get(apiURL)
if err != nil {