commit 811a8bf5faf2c4318e4597d4e50ebae35653b4a1 Author: milarin Date: Tue Feb 14 21:59:56 2023 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6eab256 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env +music-library \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c6a67c1 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module git.milar.in/milarin/music-library + +go 1.20 + +require ( + 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 new file mode 100644 index 0000000..d57bc8a --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +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= +git.milar.in/milarin/slices v0.0.6/go.mod h1:NOr53AOeur/qscu/FBj3lsFR262PNYBccLYSTCAXRk4= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= diff --git a/library.go b/library.go new file mode 100644 index 0000000..7d74ee4 --- /dev/null +++ b/library.go @@ -0,0 +1,55 @@ +package main + +import ( + "os" + "path/filepath" + + "git.milar.in/milarin/slices" +) + +type Playlist struct { + Name string `json:"name"` + Songs []string `json:"songs"` +} + +func GetAll() ([]Playlist, error) { + playlistNames, err := GetPlaylists() + if err != nil { + return nil, err + } + + playlists := make([]Playlist, 0, len(playlistNames)) + for _, playlistName := range playlistNames { + songs, err := GetSongs(playlistName) + if err != nil { + return nil, err + } + + playlists = append(playlists, Playlist{ + Name: playlistName, + Songs: songs, + }) + } + + return playlists, nil +} + +func GetPlaylists() ([]string, error) { + entries, err := os.ReadDir(LibraryPath) + if err != nil { + return nil, err + } + + directories := slices.Filter(entries, And(IsDir, Not(IsHidden))) + return slices.Map(directories, FsEntry2Name), nil +} + +func GetSongs(playlist string) ([]string, 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 +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..129be78 --- /dev/null +++ b/main.go @@ -0,0 +1,107 @@ +package main + +import ( + "bytes" + "crypto/sha512" + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + "path/filepath" + + "git.milar.in/milarin/envvars/v2" + "github.com/gorilla/mux" +) + +var ( + HttpIntf = envvars.String("HTTP_INTERFACE", "") + HttpPort = envvars.Uint16("HTTP_PORT", 80) + LibraryPath = envvars.String("LIBRARY_PATH", "/music") +) + +func main() { + 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/{playlist}/", GetPlaylistHandler).Methods("GET") + r.HandleFunc("/playlist/{playlist}/song/{song}/", GetSongHandler).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 { + panic(err) + } +} + +func GetAllHashHandler(w http.ResponseWriter, r *http.Request) { + playlists, err := GetAll() + if err != nil { + InternalServerError(w, err) + return + } + + b := &bytes.Buffer{} + if err := json.NewEncoder(b).Encode(playlists); err != nil { + InternalServerError(w, err) + return + } + + hash := sha512.Sum512(b.Bytes()) + if _, err := fmt.Fprint(w, hex.EncodeToString(hash[:])); err != nil { + InternalServerError(w, err) + return + } +} + +func GetAllHandler(w http.ResponseWriter, r *http.Request) { + playlists, err := GetAll() + if err != nil { + InternalServerError(w, err) + return + } + + w.Header().Add("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(playlists); err != nil { + InternalServerError(w, err) + return + } +} + +func GetSongHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + playlist := vars["playlist"] + song := vars["song"] + + http.ServeFile(w, r, filepath.Join(LibraryPath, playlist, song)) +} + +func GetPlaylistHandler(w http.ResponseWriter, r *http.Request) { + playlist := mux.Vars(r)["playlist"] + + songs, err := GetSongs(playlist) + if err != nil { + InternalServerError(w, err) + return + } + + w.Header().Add("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(songs); err != nil { + InternalServerError(w, err) + return + } +} + +func GetPlaylistsHandler(w http.ResponseWriter, r *http.Request) { + playlists, err := GetPlaylists() + if err != nil { + InternalServerError(w, err) + return + } + + w.Header().Add("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(playlists); err != nil { + InternalServerError(w, err) + return + } +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..d3dc645 --- /dev/null +++ b/utils.go @@ -0,0 +1,50 @@ +package main + +import ( + "log" + "net/http" + "os" + "strings" +) + +func MethodNotAllowed(w http.ResponseWriter, allowedMethods ...string) { + for _, method := range allowedMethods { + w.Header().Add("Allow", strings.ToUpper(method)) + } + w.WriteHeader(http.StatusMethodNotAllowed) +} + +func InternalServerError(w http.ResponseWriter, err error) { + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) +} + +func And[T any](f1, f2 func(T) bool) func(T) bool { + return func(v T) bool { + return f1(v) && f2(v) + } +} + +func Or[T any](f1, f2 func(T) bool) func(T) bool { + return func(v T) bool { + return f1(v) || f2(v) + } +} + +func Not[T any](f func(T) bool) func(T) bool { + return func(v T) bool { + return !f(v) + } +} + +func IsDir(entry os.DirEntry) bool { + return entry.IsDir() +} + +func IsHidden(entry os.DirEntry) bool { + return strings.HasPrefix(entry.Name(), ".") +} + +func FsEntry2Name(entry os.DirEntry) string { + return entry.Name() +}