Compare commits
10 Commits
532a34fb63
...
98365d7859
| Author | SHA1 | Date | |
|---|---|---|---|
| 98365d7859 | |||
| 161cbdfebf | |||
| 415172733f | |||
| 0e3e655939 | |||
| 6e1fd62247 | |||
| 5d54f8eb12 | |||
| 2cd8d0dc37 | |||
| d9abb0b18b | |||
| 0c097d63a6 | |||
| a8028bdc28 |
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
github: [MoeCinnamo]
|
||||
custom: ["https://afdian.com/a/MoeCinnamo"]
|
||||
57
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
57
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
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
36
.github/ISSUE_TEMPLATE/feature.yml
vendored
Normal 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)
|
||||
18
.github/workflows/test-and-build.yml
vendored
18
.github/workflows/test-and-build.yml
vendored
@@ -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
12
api.go
@@ -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
202
file.go
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
0
files/cache/music/.gitignore
vendored
0
files/cache/music/.gitignore
vendored
@@ -1 +1,6 @@
|
||||
{}
|
||||
{
|
||||
"favorite": {
|
||||
"name": "我喜欢",
|
||||
"songs": []
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user