From a2753fa6277628cba56869f5bb9ce3de728dcfac Mon Sep 17 00:00:00 2001 From: Gabriel Belinsky Date: Wed, 20 Sep 2023 14:45:01 -0700 Subject: [PATCH] Push demo code to share with interviewers --- cmd/demo.go | 22 ++++++ go.mod | 9 +++ internal/apiServer/apiServer.go | 106 +++++++++++++++++++++++++++ internal/audio/audioServer.go | 42 +++++++++++ internal/audio/audioStorage.go | 35 +++++++++ internal/metadata/metadataServer.go | 18 +++++ internal/metadata/metadataStorage.go | 47 ++++++++++++ 7 files changed, 279 insertions(+) create mode 100644 cmd/demo.go create mode 100644 go.mod create mode 100644 internal/apiServer/apiServer.go create mode 100644 internal/audio/audioServer.go create mode 100644 internal/audio/audioStorage.go create mode 100644 internal/metadata/metadataServer.go create mode 100644 internal/metadata/metadataStorage.go diff --git a/cmd/demo.go b/cmd/demo.go new file mode 100644 index 0000000..e691e82 --- /dev/null +++ b/cmd/demo.go @@ -0,0 +1,22 @@ +package main + +import ( + "fmt" + + "dread.land/deepgram-demo/internal/apiServer" + "dread.land/deepgram-demo/internal/audio" + "dread.land/deepgram-demo/internal/metadata" +) + +func main() { + fmt.Println("Hello, World!") + + // would be better off as config + audioStorage := audio.NewAudioMemoryStorage() + metadataStorage := metadata.NewMetadataMemoryStorage() + audioServer := &audio.AudioServer{Storage: audioStorage, Metadata: metadataStorage} + metadataServer := &metadata.MetadataServer{Storage: metadataStorage} + apiServer := &apiServer.ApiServer{Audio: audioServer, Metadata: metadataServer} + + apiServer.Serve() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..08a9aa8 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module dread.land/deepgram-demo + +go 1.19 + +require ( + github.com/go-audio/audio v1.0.0 // indirect + github.com/go-audio/riff v1.0.0 // indirect + github.com/go-audio/wav v1.1.0 +) diff --git a/internal/apiServer/apiServer.go b/internal/apiServer/apiServer.go new file mode 100644 index 0000000..32da09a --- /dev/null +++ b/internal/apiServer/apiServer.go @@ -0,0 +1,106 @@ +package apiServer + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "path" + "strconv" + + "dread.land/deepgram-demo/internal/audio" + "dread.land/deepgram-demo/internal/metadata" +) + +type ApiServer struct { + Audio *audio.AudioServer + Metadata *metadata.MetadataServer +} + +func (apiServer *ApiServer) filesHandler(w http.ResponseWriter, r *http.Request) { + + filename := path.Base(r.URL.Path) + switch r.Method { + case "GET": + // get file + contents, err := apiServer.Audio.Download(filename) + if err != nil { + http.Error(w, fmt.Sprint(err), http.StatusNotFound) + return + } + w.Write(contents) + return + case "POST": // save file + contents, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, fmt.Sprint(err), http.StatusBadRequest) + } + err = apiServer.Audio.Upload(filename, contents) + if err != nil { + http.Error(w, fmt.Sprint(err), http.StatusConflict) + } + return + default: + // error unsupported + http.Error(w, "unsupported", http.StatusMethodNotAllowed) + return + } +} + +func (apiServer *ApiServer) metadataHandler(w http.ResponseWriter, r *http.Request) { + filename := path.Base(r.URL.Path) + switch r.Method { + case "GET": + // get metadata + meta, err := apiServer.Metadata.Info(filename) + if err != nil { + http.Error(w, fmt.Sprint(err), http.StatusNotFound) + return + } + response, err := json.Marshal(meta) + if err != nil { + http.Error(w, fmt.Sprint(err), http.StatusInternalServerError) + return + } + fmt.Fprint(w, string(response)) + return + default: + http.Error(w, "unsupported", http.StatusMethodNotAllowed) + return + } +} + +func (apiServer *ApiServer) listHandler(w http.ResponseWriter, r *http.Request) { + maxdurationString := r.URL.Query().Get("maxduration") + maxduration := 0 + var err error + if maxdurationString != "" { + maxduration, err = strconv.Atoi(maxdurationString) + } + if err != nil { + http.Error(w, fmt.Sprint(err), http.StatusBadRequest) + return // error Bad Request + } + list := apiServer.Metadata.List(maxduration) + response, err := json.Marshal(list) + if err != nil { + http.Error(w, fmt.Sprint(err), http.StatusInternalServerError) + return + } + fmt.Fprint(w, string(response)) +} + +func (apiServer *ApiServer) Serve() { + + // POST and GET media + http.HandleFunc("/files/", apiServer.filesHandler) + // list media + http.HandleFunc("/files", apiServer.listHandler) + // list metadata (with filter) + http.HandleFunc("/metadata", apiServer.listHandler) + // GET metadata + http.HandleFunc("/metadata/", apiServer.metadataHandler) + + log.Fatal(http.ListenAndServe(":3030", nil)) +} diff --git a/internal/audio/audioServer.go b/internal/audio/audioServer.go new file mode 100644 index 0000000..3f43140 --- /dev/null +++ b/internal/audio/audioServer.go @@ -0,0 +1,42 @@ +package audio + +import ( + "bytes" + "errors" + "fmt" + + "github.com/go-audio/wav" + + "dread.land/deepgram-demo/internal/metadata" +) + +type AudioServer struct { + Storage *AudioMemoryStorage + Metadata *metadata.MetadataMemoryStorage +} + +func (a *AudioServer) Upload(filename string, contents []byte) error { + fmt.Println(a) + + // extract duration and save as metadata + r := bytes.NewReader(contents) + decoder := wav.NewDecoder(r) + if !decoder.IsValidFile() { + return errors.New("invalid wav file") + } + dur, err := decoder.Duration() + if err != nil { + return err + } + err = a.Metadata.Put(metadata.AudioMetadata{FileName: filename, Duration: int(dur.Milliseconds())}) + if err != nil { + return err + } + err = a.Storage.put(AudioFile{FileName: filename, Contents: contents}) + return err +} + +func (a *AudioServer) Download(filename string) ([]byte, error) { + file, err := a.Storage.get(filename) + return file.Contents, err +} diff --git a/internal/audio/audioStorage.go b/internal/audio/audioStorage.go new file mode 100644 index 0000000..ffeec7d --- /dev/null +++ b/internal/audio/audioStorage.go @@ -0,0 +1,35 @@ +package audio + +import ( + "errors" +) + +type AudioFile struct { + FileName string + Contents []byte +} + +type AudioMemoryStorage struct { + Data map[string]AudioFile +} + +func NewAudioMemoryStorage() *AudioMemoryStorage { + return &AudioMemoryStorage{Data: map[string]AudioFile{}} +} + +func (audio *AudioMemoryStorage) put(a AudioFile) (err error) { + _, ok := audio.Data[a.FileName] + if !ok { + audio.Data[a.FileName] = a + return nil + } + return errors.New("already exists") +} + +func (audio *AudioMemoryStorage) get(filename string) (a AudioFile, err error) { + val, ok := audio.Data[filename] + if ok { + return val, nil + } + return AudioFile{}, errors.New("not found") +} diff --git a/internal/metadata/metadataServer.go b/internal/metadata/metadataServer.go new file mode 100644 index 0000000..ae0ceea --- /dev/null +++ b/internal/metadata/metadataServer.go @@ -0,0 +1,18 @@ +package metadata + +type MetadataServer struct { + Storage *MetadataMemoryStorage +} + +func (m *MetadataServer) Info(filename string) (AudioMetadata, error) { + a, err := m.Storage.get(filename) + if err == nil { + return a, nil + } + return AudioMetadata{}, err +} + +func (m *MetadataServer) List(maxduration int) []string { + a := m.Storage.filterMaxDuration(maxduration) + return a +} diff --git a/internal/metadata/metadataStorage.go b/internal/metadata/metadataStorage.go new file mode 100644 index 0000000..1f1c198 --- /dev/null +++ b/internal/metadata/metadataStorage.go @@ -0,0 +1,47 @@ +package metadata + +import ( + "errors" + "fmt" +) + +type AudioMetadata struct { + FileName string `json:"filename"` + Duration int `json:"duration"` +} + +type MetadataMemoryStorage struct { + Metadata map[string]AudioMetadata +} + +func NewMetadataMemoryStorage() *MetadataMemoryStorage { + return &MetadataMemoryStorage{Metadata: map[string]AudioMetadata{}} +} + +func (metadata *MetadataMemoryStorage) Put(a AudioMetadata) (err error) { + fmt.Println(metadata, a) + _, ok := metadata.Metadata[a.FileName] + if !ok { + metadata.Metadata[a.FileName] = a + return nil + } + return errors.New("already exists") // 409 Conflict +} + +func (metadata *MetadataMemoryStorage) get(filename string) (a AudioMetadata, err error) { + val, ok := metadata.Metadata[filename] + if ok { + return val, nil + } + return AudioMetadata{}, errors.New("not found") // 404 Not Found +} + +func (metadata *MetadataMemoryStorage) filterMaxDuration(maxDuration int) (a []string) { + result := []string{} + for _, item := range metadata.Metadata { + if (item.Duration <= maxDuration) || (maxDuration <= 0) { + result = append(result, item.FileName) + } + } + return result +}