diff --git a/go.mod b/go.mod index c6a67c1..6b1074b 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module git.milar.in/milarin/music-library go 1.20 require ( + git.milar.in/milarin/adverr v1.1.0 git.milar.in/milarin/envvars/v2 v2.0.0 git.milar.in/milarin/slices v0.0.6 github.com/gorilla/mux v1.8.0 diff --git a/go.sum b/go.sum index d57bc8a..6c5b70a 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +git.milar.in/milarin/adverr v1.1.0 h1:jD9WnOvs40lfMhvqQ7cllOaRJNBMWr1f07/s9jAadp0= +git.milar.in/milarin/adverr v1.1.0/go.mod h1:joU9sBb7ySyNv4SpTXB0Z4o1mjXsArBw4N27wjgzj9E= git.milar.in/milarin/envvars/v2 v2.0.0 h1:DWRQCWaHqzDD8NGpSgv5tYLuF9A/dVFPAtTvz3oiIqE= git.milar.in/milarin/envvars/v2 v2.0.0/go.mod h1:HkdEi+gG2lJSmVq547bTlQV4qQ0hO333bE8IrE0B9yY= git.milar.in/milarin/slices v0.0.6 h1:AQoSarZ58WHYol9c6woWJSe8wFpPC2RC4cvIlZpfg9s= diff --git a/library.go b/library.go index 7d74ee4..f16d272 100644 --- a/library.go +++ b/library.go @@ -4,23 +4,29 @@ import ( "os" "path/filepath" + "git.milar.in/milarin/adverr" "git.milar.in/milarin/slices" ) type Playlist struct { - Name string `json:"name"` - Songs []string `json:"songs"` + Name string `json:"name"` + Songs []Song `json:"songs"` } -func GetAll() ([]Playlist, error) { - playlistNames, err := GetPlaylists() +type Song struct { + Name string `json:"name"` + File string `json:"file"` +} + +func GetAllPlaylists() ([]Playlist, error) { + playlistNames, err := GetAllPlaylistNames() if err != nil { return nil, err } playlists := make([]Playlist, 0, len(playlistNames)) for _, playlistName := range playlistNames { - songs, err := GetSongs(playlistName) + songs, err := GetSongsByPlaylist(playlistName) if err != nil { return nil, err } @@ -34,7 +40,7 @@ func GetAll() ([]Playlist, error) { return playlists, nil } -func GetPlaylists() ([]string, error) { +func GetAllPlaylistNames() ([]string, error) { entries, err := os.ReadDir(LibraryPath) if err != nil { return nil, err @@ -44,12 +50,17 @@ func GetPlaylists() ([]string, error) { return slices.Map(directories, FsEntry2Name), nil } -func GetSongs(playlist string) ([]string, error) { +func GetSongsByPlaylist(playlist string) ([]Song, error) { entries, err := os.ReadDir(filepath.Join(LibraryPath, playlist)) if err != nil { return nil, err } - directories := slices.Filter(entries, Not(IsDir)) - return slices.Map(directories, FsEntry2Name), nil + symlinks := slices.Filter(entries, IsSymlink) + return slices.Map(symlinks, func(entry os.DirEntry) Song { + return Song{ + Name: FsEntry2Name(entry), + File: filepath.Base(adverr.Must(os.Readlink(filepath.Join(LibraryPath, playlist, entry.Name())))), + } + }), nil } diff --git a/main.go b/main.go index 129be78..2ae3f25 100644 --- a/main.go +++ b/main.go @@ -6,10 +6,14 @@ import ( "encoding/hex" "encoding/json" "fmt" + "math/rand" "net/http" + "os" + "os/exec" "path/filepath" "git.milar.in/milarin/envvars/v2" + "git.milar.in/milarin/slices" "github.com/gorilla/mux" ) @@ -20,13 +24,19 @@ var ( ) func main() { + if _, err := exec.LookPath("ffmpeg"); err != nil { + panic("ffmpeg not found in PATH") + } + r := mux.NewRouter() r.HandleFunc("/all/", GetAllHandler).Methods("GET") r.HandleFunc("/all/hash/", GetAllHashHandler).Methods("GET") - r.HandleFunc("/playlist/", GetPlaylistsHandler).Methods("GET") + r.HandleFunc("/playlist-names/", GetPlaylistNamesHandler).Methods("GET") r.HandleFunc("/playlist/{playlist}/", GetPlaylistHandler).Methods("GET") - r.HandleFunc("/playlist/{playlist}/song/{song}/", GetSongHandler).Methods("GET") + r.HandleFunc("/file/{file}/", GetFileHandler).Methods("GET") + r.HandleFunc("/file/{file}/{format}/", GetEncodeFileHandler).Methods("GET") + r.HandleFunc("/file/", GetAllFilesHandler).Methods("GET") fmt.Printf("Starting music server on port %d\n", HttpPort) if err := http.ListenAndServe(fmt.Sprintf("%s:%d", HttpIntf, HttpPort), r); err != nil { @@ -35,7 +45,7 @@ func main() { } func GetAllHashHandler(w http.ResponseWriter, r *http.Request) { - playlists, err := GetAll() + playlists, err := GetAllPlaylists() if err != nil { InternalServerError(w, err) return @@ -55,7 +65,7 @@ func GetAllHashHandler(w http.ResponseWriter, r *http.Request) { } func GetAllHandler(w http.ResponseWriter, r *http.Request) { - playlists, err := GetAll() + playlists, err := GetAllPlaylists() if err != nil { InternalServerError(w, err) return @@ -68,40 +78,86 @@ func GetAllHandler(w http.ResponseWriter, r *http.Request) { } } -func GetSongHandler(w http.ResponseWriter, r *http.Request) { +func GetFileHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) - playlist := vars["playlist"] - song := vars["song"] + file := vars["file"] - http.ServeFile(w, r, filepath.Join(LibraryPath, playlist, song)) + http.ServeFile(w, r, filepath.Join(LibraryPath, ".songs", file)) } func GetPlaylistHandler(w http.ResponseWriter, r *http.Request) { - playlist := mux.Vars(r)["playlist"] + playlistName := mux.Vars(r)["playlist"] - songs, err := GetSongs(playlist) + songs, err := GetSongsByPlaylist(playlistName) + if err != nil { + InternalServerError(w, err) + return + } + + playlist := Playlist{ + Name: playlistName, + Songs: songs, + } + + w.Header().Add("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(playlist); err != nil { + InternalServerError(w, err) + return + } +} + +func GetPlaylistNamesHandler(w http.ResponseWriter, r *http.Request) { + playlistNames, err := GetAllPlaylistNames() if err != nil { InternalServerError(w, err) return } w.Header().Add("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(songs); err != nil { + if err := json.NewEncoder(w).Encode(playlistNames); err != nil { InternalServerError(w, err) return } } -func GetPlaylistsHandler(w http.ResponseWriter, r *http.Request) { - playlists, err := GetPlaylists() +func GetAllFilesHandler(w http.ResponseWriter, r *http.Request) { + fileDirectory := filepath.Join(LibraryPath, ".songs") + + entries, err := os.ReadDir(fileDirectory) if err != nil { InternalServerError(w, err) return } + files := slices.Filter(entries, IsRegular) + fileNames := slices.Map(files, FsEntry2Name) + w.Header().Add("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(playlists); err != nil { + if err := json.NewEncoder(w).Encode(fileNames); err != nil { InternalServerError(w, err) return } } + +func GetEncodeFileHandler(w http.ResponseWriter, r *http.Request) { + fileName := mux.Vars(r)["file"] + format := mux.Vars(r)["format"] + + inputFilePath := filepath.Join(LibraryPath, ".songs", fileName) + outputFilePath := filepath.Join(os.TempDir(), fmt.Sprintf("%d.%s", rand.Int(), format)) + defer os.Remove(outputFilePath) + + fmt.Printf("encode file to %s: '%s' -> '%s'\n", format, inputFilePath, outputFilePath) + + cmd := exec.Command("ffmpeg", "-i", inputFilePath, "-vn", outputFilePath) + if err := cmd.Start(); err != nil { + InternalServerError(w, err) + return + } + if err := cmd.Wait(); err != nil { + InternalServerError(w, err) + return + } + + http.ServeFile(w, r, outputFilePath) +} diff --git a/utils.go b/utils.go index d3dc645..2339512 100644 --- a/utils.go +++ b/utils.go @@ -37,6 +37,14 @@ func Not[T any](f func(T) bool) func(T) bool { } } +func IsSymlink(entry os.DirEntry) bool { + return entry.Type()&os.ModeSymlink == os.ModeSymlink +} + +func IsRegular(entry os.DirEntry) bool { + return entry.Type().IsRegular() +} + func IsDir(entry os.DirEntry) bool { return entry.IsDir() }