diff --git a/.env.example b/.env.example
index 36a1dc0..dd91f6a 100644
--- a/.env.example
+++ b/.env.example
@@ -10,6 +10,7 @@ WEBSITE_FAVICON=/favicon.ico # Your website favicon
WEBSITE_BACKGROUND=/background.webp # Your website background image
WEBSITE_URL=http://127.0.0.1:2233 # Your website URL
+EMBEDDED_WEBSITE_URL=http://127.0.0.1:2233 # Your embedded website URL
PORT=2233 # Your website port
FONTAWESOME_CDN=https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css # Fontawesome CDN
diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index 8990409..6caac79 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -25,13 +25,4 @@ jobs:
run: go mod tidy
- name: Test
- run: go test -coverprofile=coverage.txt -v ./...
-
- - name: Upload coverage reports to Codecov
- uses: codecov/codecov-action@v5
- with:
- token: ${{ secrets.CODECOV_TOKEN }}
- files: ./coverage.txt
- flags: unittests
- name: codecov-umbrella
- fail_ci_if_error: true
+ run: go test -v ./...
\ No newline at end of file
diff --git a/README.md b/README.md
index 1bc9739..8a6994d 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,23 @@
# MeowEmbeddedMusicServer
-[](https://codecov.io/gh/IntelligentlyEverything/MeowEmbeddedMusicServer)
+[English](README.md) | [简体中文](README_zh-CN.md)
+Your Embedded Music Server for you.
-Your Embedded Music Server for you.
\ No newline at end of file
+## Features
+- Play music from your server
+- Music streaming for your embedded devices
+- Music library management
+- Music search and cache
+
+# Tutorial document
+Please refer to the [wiki](https://github.com/IntelligentlyEverything/MeowEmbeddedMusicServer/wiki).
+
+
+## Star History
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README_zh-CN.md b/README_zh-CN.md
new file mode 100644
index 0000000..a4355b9
--- /dev/null
+++ b/README_zh-CN.md
@@ -0,0 +1,25 @@
+# Meow 为嵌入式设备制作的音乐串流服务
+[English](README.md) | [简体中文](README_zh-CN.md)
+MeowEmbeddedMusicServer 是一个为嵌入式设备制作的音乐串流服务。
+它可以播放来自你的服务器的音乐,也可以为你的嵌入式设备提供音乐流媒体服务。
+它还可以管理音乐库,并且可以搜索和下载音乐。
+
+## 特性
+- 在线听音乐
+- 为嵌入式设备提供音乐串流服务
+- 管理音乐库
+- 搜索和缓存音乐
+
+# 教程文档
+请参阅 [维基](https://github.com/IntelligentlyEverything/MeowEmbeddedMusicServer/wiki).
+
+
+## Star 历史
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/api.go b/api.go
index f08096a..9390be7 100644
--- a/api.go
+++ b/api.go
@@ -4,7 +4,7 @@ import (
"encoding/json"
"fmt"
"net/http"
- "os"
+ "net/url"
"path/filepath"
"strings"
)
@@ -24,24 +24,79 @@ func apiHandler(w http.ResponseWriter, r *http.Request) {
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: source.AudioURL,
- M3U8URL: source.M3U8URL,
- LyricURL: source.LyricURL,
- CoverURL: source.CoverURL,
- Duration: source.Duration,
- FromCache: false,
+ Title: source.Title,
+ Artist: source.Artist,
+ AudioURL: audioURL,
+ AudioFullURL: audioFullURL,
+ M3U8URL: m3u8URL,
+ LyricURL: lyricURL,
+ CoverURL: coverURL,
+ Duration: source.Duration,
+ FromCache: false,
}
found = true
break
@@ -54,6 +109,21 @@ func apiHandler(w http.ResponseWriter, r *http.Request) {
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
}
}
@@ -71,6 +141,21 @@ func apiHandler(w http.ResponseWriter, r *http.Request) {
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
}
@@ -81,10 +166,15 @@ func apiHandler(w http.ResponseWriter, r *http.Request) {
// If still not found, request and cache the music item in a separate goroutine
if !found {
fmt.Println("[Info] Updating music item cache from API request.")
- go func() {
- requestAndCacheMusic(song, singer)
- fmt.Println("[Info] Music item cache updated.")
- }()
+ 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.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
@@ -99,155 +189,3 @@ func apiHandler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(musicItem)
}
-
-// Read sources.json file and return a list of SourceItem.
-func readSources() []MusicItem {
- data, err := os.ReadFile("./sources.json")
- fmt.Println("[Info] Reading local sources.json")
- if err != nil {
- fmt.Println("[Error] Failed to read sources.json:", err)
- return nil
- }
-
- var sources []MusicItem
- err = json.Unmarshal(data, &sources)
- if err != nil {
- fmt.Println("[Error] Failed to parse sources.json:", err)
- return nil
- }
-
- return sources
-}
-
-// Retrieve music items 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)
- if len(parts) != 2 {
- continue // Skip if the directory name doesn't contain exactly one "-"
- }
- artist := parts[0]
- title := parts[1]
- musicItem := MusicItem{
- Title: title,
- Artist: artist,
- AudioURL: "",
- AudioFullURL: "",
- M3U8URL: "",
- LyricURL: "",
- CoverURL: "",
- Duration: 0,
- }
-
- musicFilePath := filepath.Join(dirPath, "music.mp3")
- if _, err := os.Stat(musicFilePath); err == nil {
- musicItem.AudioURL = os.Getenv("WEBSITE_URL") + "/music/" + file.Name() + "/music.mp3"
- musicItem.Duration = getMusicDuration(musicFilePath)
- }
-
- for _, audioFormat := range []string{"music_full.mp3", "music_full.flac", "music_full.wav", "music_full.aac", "music_full.ogg"} {
- audioFilePath := filepath.Join(dirPath, audioFormat)
- if _, err := os.Stat(audioFilePath); err == nil {
- musicItem.AudioFullURL = os.Getenv("WEBSITE_URL") + "/music/" + file.Name() + "/" + audioFormat
- break
- }
- }
-
- m3u8FilePath := filepath.Join(dirPath, "music.m3u8")
- if _, err := os.Stat(m3u8FilePath); err == nil {
- musicItem.M3U8URL = os.Getenv("WEBSITE_URL") + "/music/" + file.Name() + "/music.m3u8"
- }
-
- lyricFilePath := filepath.Join(dirPath, "lyric.lrc")
- if _, err := os.Stat(lyricFilePath); err == nil {
- musicItem.LyricURL = os.Getenv("WEBSITE_URL") + "/music/" + file.Name() + "/lyric.lrc"
- }
-
- coverJpgFilePath := filepath.Join(dirPath, "cover.jpg")
- if _, err := os.Stat(coverJpgFilePath); err == nil {
- musicItem.CoverURL = os.Getenv("WEBSITE_URL") + "/music/" + file.Name() + "/cover.jpg"
- } else {
- coverPngFilePath := filepath.Join(dirPath, "cover.png")
- if _, err := os.Stat(coverPngFilePath); err == nil {
- musicItem.CoverURL = os.Getenv("WEBSITE_URL") + "/music/" + file.Name() + "/cover.png"
- }
- }
-
- return musicItem
- }
- } else {
- if strings.Contains(file.Name(), singer) && 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)
- if len(parts) != 2 {
- continue // Skip if the directory name doesn't contain exactly one "-"
- }
- artist := parts[0]
- title := parts[1]
- musicItem := MusicItem{
- Title: title,
- Artist: artist,
- AudioURL: "",
- AudioFullURL: "",
- M3U8URL: "",
- LyricURL: "",
- CoverURL: "",
- Duration: 0,
- }
-
- musicFilePath := filepath.Join(dirPath, "music.mp3")
- if _, err := os.Stat(musicFilePath); err == nil {
- musicItem.AudioURL = os.Getenv("WEBSITE_URL") + "/music/" + file.Name() + "/music.mp3"
- musicItem.Duration = getMusicDuration(musicFilePath)
- }
-
- for _, audioFormat := range []string{"music_full.mp3", "music_full.flac", "music_full.wav", "music_full.aac", "music_full.ogg"} {
- audioFilePath := filepath.Join(dirPath, audioFormat)
- if _, err := os.Stat(audioFilePath); err == nil {
- musicItem.AudioFullURL = os.Getenv("WEBSITE_URL") + "/music/" + file.Name() + "/" + audioFormat
- break
- }
- }
-
- m3u8FilePath := filepath.Join(dirPath, "music.m3u8")
- if _, err := os.Stat(m3u8FilePath); err == nil {
- musicItem.M3U8URL = os.Getenv("WEBSITE_URL") + "/music/" + file.Name() + "/music.m3u8"
- }
-
- lyricFilePath := filepath.Join(dirPath, "lyric.lrc")
- if _, err := os.Stat(lyricFilePath); err == nil {
- musicItem.LyricURL = os.Getenv("WEBSITE_URL") + "/music/" + file.Name() + "/lyric.lrc"
- }
-
- coverJpgFilePath := filepath.Join(dirPath, "cover.jpg")
- if _, err := os.Stat(coverJpgFilePath); err == nil {
- musicItem.CoverURL = os.Getenv("WEBSITE_URL") + "/music/" + file.Name() + "/cover.jpg"
- } else {
- coverPngFilePath := filepath.Join(dirPath, "cover.png")
- if _, err := os.Stat(coverPngFilePath); err == nil {
- musicItem.CoverURL = os.Getenv("WEBSITE_URL") + "/music/" + file.Name() + "/cover.png"
- }
- }
-
- return musicItem
- }
- }
- }
- }
-
- return MusicItem{} // If no matching folder is found, return an empty MusicItem
-}
diff --git a/file.go b/file.go
index e51099b..d363a19 100644
--- a/file.go
+++ b/file.go
@@ -1,9 +1,12 @@
package main
import (
+ "io"
"net/http"
+ "net/url"
"os"
"path/filepath"
+ "strings"
)
// ListFiles function: Traverse all files in the specified directory and return a slice of the file path
@@ -52,6 +55,96 @@ func fileHandler(w http.ResponseWriter, r *http.Request) {
// Obtain the path of the request
filePath := r.URL.Path
+ // Check if the request path starts with "/url/"
+ if strings.HasPrefix(filePath, "/url/") {
+ // Extract the URL after "/url/"
+ urlPath := filePath[len("/url/"):]
+ // Decode the URL path in case it's URL encoded
+ decodedURL, err := url.QueryUnescape(urlPath)
+ if err != nil {
+ NotFoundHandler(w, r)
+ return
+ }
+ // Determine the protocol based on the URL path
+ var protocol string
+ if strings.HasPrefix(decodedURL, "http/") {
+ protocol = "http://"
+ } else if strings.HasPrefix(decodedURL, "https/") {
+ protocol = "https://"
+ } else {
+ NotFoundHandler(w, r)
+ return
+ }
+ // Remove the protocol part from the decoded URL
+ decodedURL = strings.TrimPrefix(decodedURL, "http/")
+ decodedURL = strings.TrimPrefix(decodedURL, "https/")
+ // Correctly concatenate the protocol with the decoded URL
+ decodedURL = protocol + decodedURL
+ // Create a new HTTP request to the decoded URL, without copying headers
+ req, err := http.NewRequest("GET", decodedURL, nil)
+ if err != nil {
+ NotFoundHandler(w, r)
+ return
+ }
+ // Send the request and get the response
+ client := &http.Client{}
+ resp, err := client.Do(req)
+ if err != nil || resp.StatusCode != http.StatusOK {
+ NotFoundHandler(w, r)
+ return
+ }
+ defer resp.Body.Close()
+ // Read the response body into a byte slice
+ fileContent, err := io.ReadAll(resp.Body)
+ if err != nil {
+ 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")
+ }
+ // Write file content to response
+ w.Write(fileContent)
+ return
+ }
+
// Construct the complete file path
fullFilePath := filepath.Join("./files", filePath)
diff --git a/helper.go b/helper.go
index 249c8fb..f4cd2f3 100644
--- a/helper.go
+++ b/helper.go
@@ -6,6 +6,7 @@ import (
"io"
"mime"
"net/http"
+ "net/url"
"os"
"os/exec"
"path/filepath"
@@ -18,7 +19,7 @@ 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", "16000", outputFile)
+ cmd := exec.Command("ffmpeg", "-i", inputFile, "-ac", "1", "-ab", "32k", "-ar", "24000", outputFile)
err := cmd.Run()
if err != nil {
return err
@@ -92,7 +93,7 @@ func createM3U8Playlist(outputDir string) error {
if err != nil {
return err
}
- url := fmt.Sprintf("%s/cache/music/%s/%s/%s\n", os.Getenv("WEBSITE_URL"), filepath.Base(outputDir), "chunk", chunkFile)
+ url := fmt.Sprintf("%s/cache/music/%s/%s/%s\n", os.Getenv("EMBEDDED_WEBSITE_URL"), filepath.Base(outputDir), "chunk", chunkFile)
_, err = file.WriteString(url)
}
@@ -137,7 +138,7 @@ func getMusicDuration(filePath string) int {
return int(duration)
}
-// Function for identifying file formats
+// Helper function for identifying file formats
func getMusicFileExtension(url string) (string, error) {
resp, err := http.Head(url)
if err != nil {
@@ -183,14 +184,185 @@ func getMusicFileExtension(url string) (string, error) {
}
}
+// 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)
+ if len(parts) != 2 {
+ continue // Skip if the directory name doesn't contain exactly one "-"
+ }
+ artist := parts[0]
+ title := parts[1]
+ musicItem := MusicItem{
+ Title: title,
+ Artist: artist,
+ AudioURL: "",
+ AudioFullURL: "",
+ M3U8URL: "",
+ LyricURL: "",
+ CoverURL: "",
+ Duration: 0,
+ }
+
+ musicFilePath := filepath.Join(dirPath, "music.mp3")
+ if _, err := os.Stat(musicFilePath); err == nil {
+ musicItem.AudioURL = "/music/" + url.QueryEscape(file.Name()) + "/music.mp3"
+ musicItem.Duration = getMusicDuration(musicFilePath)
+ }
+
+ for _, audioFormat := range []string{"music_full.mp3", "music_full.flac", "music_full.wav", "music_full.aac", "music_full.ogg"} {
+ audioFilePath := filepath.Join(dirPath, audioFormat)
+ if _, err := os.Stat(audioFilePath); err == nil {
+ musicItem.AudioFullURL = "/music/" + url.QueryEscape(file.Name()) + "/" + audioFormat
+ break
+ }
+ }
+
+ m3u8FilePath := filepath.Join(dirPath, "music.m3u8")
+ if _, err := os.Stat(m3u8FilePath); err == nil {
+ musicItem.M3U8URL = "/music/" + url.QueryEscape(file.Name()) + "/music.m3u8"
+ }
+
+ lyricFilePath := filepath.Join(dirPath, "lyric.lrc")
+ if _, err := os.Stat(lyricFilePath); err == nil {
+ musicItem.LyricURL = "/music/" + url.QueryEscape(file.Name()) + "/lyric.lrc"
+ }
+
+ coverJpgFilePath := filepath.Join(dirPath, "cover.jpg")
+ if _, err := os.Stat(coverJpgFilePath); err == nil {
+ musicItem.CoverURL = "/music/" + url.QueryEscape(file.Name()) + "/cover.jpg"
+ } else {
+ coverPngFilePath := filepath.Join(dirPath, "cover.png")
+ if _, err := os.Stat(coverPngFilePath); err == nil {
+ musicItem.CoverURL = "/music/" + url.QueryEscape(file.Name()) + "/cover.png"
+ }
+ }
+
+ return musicItem
+ }
+ } else {
+ if strings.Contains(file.Name(), singer) && 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)
+ if len(parts) != 2 {
+ continue // Skip if the directory name doesn't contain exactly one "-"
+ }
+ artist := parts[0]
+ title := parts[1]
+ musicItem := MusicItem{
+ Title: title,
+ Artist: artist,
+ AudioURL: "",
+ AudioFullURL: "",
+ M3U8URL: "",
+ LyricURL: "",
+ CoverURL: "",
+ Duration: 0,
+ }
+
+ musicFilePath := filepath.Join(dirPath, "music.mp3")
+ if _, err := os.Stat(musicFilePath); err == nil {
+ musicItem.AudioURL = "/music/" + url.QueryEscape(file.Name()) + "/music.mp3"
+ musicItem.Duration = getMusicDuration(musicFilePath)
+ }
+
+ for _, audioFormat := range []string{"music_full.mp3", "music_full.flac", "music_full.wav", "music_full.aac", "music_full.ogg"} {
+ audioFilePath := filepath.Join(dirPath, audioFormat)
+ if _, err := os.Stat(audioFilePath); err == nil {
+ musicItem.AudioFullURL = "/music/" + url.QueryEscape(file.Name()) + "/" + audioFormat
+ break
+ }
+ }
+
+ m3u8FilePath := filepath.Join(dirPath, "music.m3u8")
+ if _, err := os.Stat(m3u8FilePath); err == nil {
+ musicItem.M3U8URL = "/music/" + url.QueryEscape(file.Name()) + "/music.m3u8"
+ }
+
+ lyricFilePath := filepath.Join(dirPath, "lyric.lrc")
+ if _, err := os.Stat(lyricFilePath); err == nil {
+ musicItem.LyricURL = "/music/" + url.QueryEscape(file.Name()) + "/lyric.lrc"
+ }
+
+ coverJpgFilePath := filepath.Join(dirPath, "cover.jpg")
+ if _, err := os.Stat(coverJpgFilePath); err == nil {
+ musicItem.CoverURL = "/music/" + url.QueryEscape(file.Name()) + "/cover.jpg"
+ } else {
+ coverPngFilePath := filepath.Join(dirPath, "cover.png")
+ if _, err := os.Stat(coverPngFilePath); err == nil {
+ musicItem.CoverURL = "/music/" + url.QueryEscape(file.Name()) + "/cover.png"
+ }
+ }
+
+ return musicItem
+ }
+ }
+ }
+ }
+
+ return MusicItem{} // If no matching folder is found, return an empty MusicItem
+}
+
+// Helper function to obtain IP address of the client
+func IPhandler(r *http.Request) (string, error) {
+ ip := r.Header.Get("X-Real-IP")
+ if ip != "" {
+ return ip, nil
+ }
+ ip = r.Header.Get("X-Forwarded-For")
+ if ip != "" {
+ ips := strings.Split(ip, ",")
+ return strings.TrimSpace(ips[0]), nil
+ }
+ ip = r.RemoteAddr
+ if ip != "" {
+ return strings.Split(ip, ":")[0], nil
+ }
+
+ return "", fmt.Errorf("unable to obtain IP address information")
+}
+
+// Helper function to read music sources from sources.json file
+func readSources() []MusicItem {
+ data, err := os.ReadFile("./sources.json")
+ fmt.Println("[Info] Reading local sources.json")
+ if err != nil {
+ fmt.Println("[Error] Failed to read sources.json:", err)
+ return nil
+ }
+
+ var sources []MusicItem
+ err = json.Unmarshal(data, &sources)
+ if err != nil {
+ fmt.Println("[Error] Failed to parse sources.json:", err)
+ return nil
+ }
+
+ return sources
+}
+
// Helper function to request and cache music from API sources
-func requestAndCacheMusic(song, singer string) {
+func requestAndCacheMusic(song, singer string) MusicItem {
fmt.Printf("[Info] Requesting and caching music for %s", song)
// Create cache directory if it doesn't exist
err := os.MkdirAll("./cache", 0755)
if err != nil {
fmt.Println("[Error] Error creating cache directory:", err)
- return
+ return MusicItem{}
}
// Get API_SOURCES and any subsequent environment variables (e.g. API_SOURCES_1, API_SOURCES_2, etc.)
@@ -223,7 +395,7 @@ func requestAndCacheMusic(song, singer string) {
// If no valid music item was found, return an empty MusicItem
if musicItem.Title == "" {
fmt.Printf("[Warning] No valid music item retrieved.\n")
- return
+ return MusicItem{}
}
// Create cache file path based on artist and title
@@ -233,15 +405,16 @@ func requestAndCacheMusic(song, singer string) {
cacheData, err := json.MarshalIndent(musicItem, "", " ")
if err != nil {
fmt.Println("[Error] Error marshalling cache data:", err)
- return
+ return MusicItem{}
}
err = os.WriteFile(cacheFile, cacheData, 0644)
if err != nil {
fmt.Println("[Error] Error writing cache file:", err)
- return
+ return MusicItem{}
}
fmt.Println("[Info] Music request and caching completed successfully.")
+ return musicItem
}
// Helper function to read music data from cache file
@@ -261,22 +434,3 @@ func readFromCache(filePath string) (MusicItem, bool) {
return musicItem, true
}
-
-// Helper function to obtain IP address of the client
-func IPhandler(r *http.Request) (string, error) {
- ip := r.Header.Get("X-Real-IP")
- if ip != "" {
- return ip, nil
- }
- ip = r.Header.Get("X-Forwarded-For")
- if ip != "" {
- ips := strings.Split(ip, ",")
- return strings.TrimSpace(ips[0]), nil
- }
- ip = r.RemoteAddr
- if ip != "" {
- return strings.Split(ip, ":")[0], nil
- }
-
- return "", fmt.Errorf("unable to obtain IP address information")
-}
diff --git a/httperr.go b/httperr.go
index 328e886..44514cd 100644
--- a/httperr.go
+++ b/httperr.go
@@ -13,4 +13,5 @@ func NotFoundHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, "
404 Music Lost!404 Music Lost!
We couldn't find the content you were looking for.
")
fmt.Fprintf(w, "", home_url)
+ fmt.Printf("[Web Access] Return 404 Not Found\n")
}
diff --git a/index.go b/index.go
index 3daed7c..a1b677f 100644
--- a/index.go
+++ b/index.go
@@ -25,6 +25,7 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
defaultIndexPage(w)
} else {
http.ServeFile(w, r, indexPath)
+ fmt.Printf("[Web Access] Return custom index pages\n")
}
}
@@ -372,4 +373,5 @@ func defaultIndexPage(w http.ResponseWriter) {
// Hide stream_pcm response
fmt.Fprintf(w, "hideStreamPcmBtn.addEventListener('click', function () {streamPcm.style.display = 'none';showStreamPcmBtn.style.display = 'block';hideStreamPcmBtn.style.display = 'none';});")
fmt.Fprintf(w, "