From ee785ae4a60d48db4fdb13fa90bee60894bc0c47 Mon Sep 17 00:00:00 2001 From: moecinnamo Date: Thu, 11 Sep 2025 17:15:39 +0800 Subject: [PATCH] improve functionality --- .env.example | 1 + .github/workflows/build-and-test.yml | 11 +- README.md | 23 ++- README_zh-CN.md | 25 +++ api.go | 268 ++++++++++----------------- file.go | 93 ++++++++++ helper.go | 208 ++++++++++++++++++--- httperr.go | 1 + index.go | 2 + sources.json.example | 2 + yuafengfreeapi.go | 24 +-- 11 files changed, 442 insertions(+), 216 deletions(-) create mode 100644 README_zh-CN.md 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 -[![codecov](https://codecov.io/gh/IntelligentlyEverything/MeowEmbeddedMusicServer/graph/badge.svg)](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 + + + + + + Star History Chart + + \ 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 历史 + + + + + + Star History Chart + + \ 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.

404
") 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, "") + fmt.Printf("[Web Access] Return default index pages\n") } diff --git a/sources.json.example b/sources.json.example index 7f359b1..8967e09 100644 --- a/sources.json.example +++ b/sources.json.example @@ -3,6 +3,7 @@ "title": "", "artist": "", "audio_url": "", + "audio_full_url": "", "m3u8_url": "", "lyric_url": "", "cover_url": "", @@ -12,6 +13,7 @@ "title": "", "artist": "", "audio_url": "", + "audio_full_url": "", "m3u8_url": "", "lyric_url": "", "cover_url": "", diff --git a/yuafengfreeapi.go b/yuafengfreeapi.go index 8de5efd..d65f509 100644 --- a/yuafengfreeapi.go +++ b/yuafengfreeapi.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net/http" + "net/url" "os" "path/filepath" "regexp" @@ -26,20 +27,20 @@ type YuafengAPIFreeResponse struct { // 枫雨API response handler. func YuafengAPIResponseHandler(sources, song, singer string) MusicItem { fmt.Printf("[Info] Fetching music data from 枫林 free API for %s by %s\n", song, singer) - var url string + var APIurl string switch sources { case "kuwo": - url = "https://api.yuafeng.cn/API/ly/kwmusic.php" + APIurl = "https://api.yuafeng.cn/API/ly/kwmusic.php" case "netease": - url = "https://api.yuafeng.cn/API/ly/wymusic.php" + APIurl = "https://api.yuafeng.cn/API/ly/wymusic.php" case "migu": - url = "https://api.yuafeng.cn/API/ly/mgmusic.php" + APIurl = "https://api.yuafeng.cn/API/ly/mgmusic.php" case "baidu": - url = "https://api.yuafeng.cn/API/ly/bdmusic.php" + APIurl = "https://api.yuafeng.cn/API/ly/bdmusic.php" default: return MusicItem{} } - resp, err := http.Get(url + "?msg=" + song + "&n=1") + resp, err := http.Get(APIurl + "?msg=" + song + "&n=1") if err != nil { fmt.Println("[Error] Error fetching the data from Yuafeng free API:", err) return MusicItem{} @@ -149,15 +150,14 @@ func YuafengAPIResponseHandler(sources, song, singer string) MusicItem { fmt.Println("[Error] Error creating m3u8 playlist:", err) } - websiteURL := os.Getenv("WEBSITE_URL") return MusicItem{ Title: response.Data.Song, Artist: response.Data.Singer, - CoverURL: websiteURL + "/cache/music/" + response.Data.Singer + "-" + response.Data.Song + "/cover" + ext, - LyricURL: websiteURL + "/cache/music/" + response.Data.Singer + "-" + response.Data.Song + "/lyric.lrc", - AudioFullURL: websiteURL + "/cache/music/" + response.Data.Singer + "-" + response.Data.Song + "/music_full" + musicExt, - AudioURL: websiteURL + "/cache/music/" + response.Data.Singer + "-" + response.Data.Song + "/music.mp3", - M3U8URL: websiteURL + "/cache/music/" + response.Data.Singer + "-" + response.Data.Song + "/music.m3u8", + CoverURL: "/cache/music/" + url.QueryEscape(response.Data.Singer+"-"+response.Data.Song) + "/cover" + ext, + LyricURL: "/cache/music/" + url.QueryEscape(response.Data.Singer+"-"+response.Data.Song) + "/lyric.lrc", + AudioFullURL: "/cache/music/" + url.QueryEscape(response.Data.Singer+"-"+response.Data.Song) + "/music_full" + musicExt, + AudioURL: "/cache/music/" + url.QueryEscape(response.Data.Singer+"-"+response.Data.Song) + "/music.mp3", + M3U8URL: "/cache/music/" + url.QueryEscape(response.Data.Singer+"-"+response.Data.Song) + "/music.m3u8", Duration: duration, } }