package frontend import ( "encoding/json" // "fmt" "io" "io/ioutil" "log" "net/http" "os" "strconv" "sync" "time" "git.autistici.org/ale/djrandom/api" db_client "git.autistici.org/ale/djrandom/services/database/client" "github.com/gorilla/mux" ) var ( defaultConcurrency = 20 ) func sendJsonResponse(w http.ResponseWriter, resp interface{}) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) } // Upload receives files from sync clients, saves them to permanent // storage and creates new Song objects in the db. func Upload(w http.ResponseWriter, r *djRequest) { // Create a local temporary file, and copy the request body to // it. As soon as this is done, return an 'ok' response to the // client (and process the file in the background). tmpf, err := ioutil.TempFile("", "djrandom_upload_") if err != nil { log.Printf("Upload(): Error creating temporary file: %s", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } defer r.Request.Body.Close() _, err = io.Copy(tmpf, r.Request.Body) if err != nil { log.Printf("Upload(): Error saving file to local storage: %s", err) os.Remove(tmpf.Name()) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } tmpf.Close() // Run further processing in the background. AnalyzeAndStore // will remove the file when it's done. go AnalyzeAndStore(r.Ctx, tmpf.Name()) sendJsonResponse(w, &api.UploadResponse{Ok: true}) } // GetSongInfo returns data on a specific song. func GetSongInfo(w http.ResponseWriter, r *djRequest) { vars := mux.Vars(r.Request) songId := vars["id"] log.Printf("GetSongInfo(%s)", songId) id, err := api.ParseSongID(songId) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } song, ok := r.Ctx.Db.GetSongWithoutDupes(nil, id) if !ok { http.Error(w, "Not Found", http.StatusNotFound) return } sendJsonResponse(w, song) } // GetManySongsInfo returns data on many songs. func GetManySongsInfo(w http.ResponseWriter, r *djRequest) { var songsReq api.GetManySongsRequest if err := json.NewDecoder(r.Request.Body).Decode(&songsReq); err != nil { log.Printf("GetManySongs(): Bad request: %s", err) http.Error(w, "Bad Request", http.StatusBadRequest) return } songs, err := db_client.ParallelFetchSongs(r.Ctx.Db, songsReq.SongIds) if err != nil { log.Printf("GetManySongs(): %s", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } resp := api.GetManySongsResponse{ Results: songs, } sendJsonResponse(w, &resp) } // GetAlbumArt returns art for an album. func GetAlbumArt(w http.ResponseWriter, r *djRequest) { args := r.Request.URL.Query() artist := args.Get("artist") album := args.Get("album") if artist == "" || album == "" { http.Error(w, "Not Found", http.StatusNotFound) return } // This will always return an image (maybe empty). img := r.Ctx.AlbumArt.GetAlbumArt(artist, album) w.Header().Set("Content-Type", "image/jpeg") w.Header().Set("Content-Length", strconv.Itoa(len(img))) expire := time.Now().Add(8760 * time.Hour) w.Header().Set("Expires", expire.Format(http.TimeFormat)) w.Write(img) } // CheckFingerprints verifies if songs are already in the db. func CheckFingerprints(w http.ResponseWriter, r *djRequest) { var fpReq api.FingerprintRequest if err := json.NewDecoder(r.Request.Body).Decode(&fpReq); err != nil { log.Printf("CheckFingerprints(): Bad request: %s", err) http.Error(w, "Bad Request", http.StatusBadRequest) return } // Run all the parallel queries within a single database session. s, err := r.Ctx.Db.NewSession() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } defer s.Close() // Type for replies. type fpResponse struct { fp string dupe bool } // Check services/database/client/database.go for a // description of the concurrency pattern. rch := make(chan fpResponse) go func() { var wg sync.WaitGroup ch := make(chan bool, defaultConcurrency) for _, fp := range fpReq.Fingerprints { wg.Add(1) ch <- true go func(fp string) { defer func() { <-ch }() defer wg.Done() if _, ok := r.Ctx.Db.GetAudioFile(s, fp); ok { rch <- fpResponse{fp, true} } else { rch <- fpResponse{fp, false} } }(fp) } go func() { wg.Wait() close(ch) close(rch) }() }() // Collector. var fpResp api.FingerprintResponse for fpr := range rch { if fpr.dupe { fpResp.Dupes = append(fpResp.Dupes, fpr.fp) } else { fpResp.Missing = append(fpResp.Missing, fpr.fp) } } log.Printf("CheckFingerprints(): ok, %d/%d", len(fpResp.Dupes), len(fpResp.Missing)) sendJsonResponse(w, &fpResp) } // SearchIds runs a search query and returns only song IDs. func SearchIds(w http.ResponseWriter, r *djRequest) { // Assemble the search query. query := r.Request.FormValue("q") if query == "" { http.Error(w, "Empty search query", http.StatusBadRequest) return } // Run search. var resp api.SearchIdsResponse for item, _ := range r.Ctx.Index.Search(query) { resp.Results = append(resp.Results, item) } sendJsonResponse(w, &resp) } // Search returns full search results. func Search(w http.ResponseWriter, r *djRequest) { // Assemble the search query. query := r.Request.FormValue("q") if query == "" { http.Error(w, "Empty search query", http.StatusBadRequest) return } // Run search (and create a list). results := r.Ctx.Index.Search(query) songIds := make([]api.SongID, 0, len(results)) for id, _ := range results { songIds = append(songIds, id) } songs, err := db_client.ParallelFetchSongs(r.Ctx.Db, songIds) if err != nil { log.Printf("Search(): %s", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } resp := api.SearchResponse{ Results: songs, } sendJsonResponse(w, &resp) } // ArtistAutocomplete returns artist autocompletion results. func ArtistAutocomplete(w http.ResponseWriter, r *djRequest) { prefix := r.Request.URL.Query().Get("prefix") if prefix == "" { http.Error(w, "Empty prefix", http.StatusBadRequest) return } authors, err := r.Ctx.Db.GetArtists(nil, prefix) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } resp := struct { Entries []string `json:"entries"` }{authors} sendJsonResponse(w, &resp) } // AddPlayLog adds an entry to the play log. func AddPlayLog(w http.ResponseWriter, r *djRequest) { var playLogReq api.AddPlayLogRequest if err := json.NewDecoder(r.Request.Body).Decode(&playLogReq); err != nil { log.Printf("AddPlayLog(): Bad request: %s", err) http.Error(w, "Bad Request", http.StatusBadRequest) return } entry := &api.PlayLogEntry{ User: r.AuthUser, Songs: playLogReq.Songs, Timestamp: time.Now().Unix(), } if err := r.Ctx.Db.AppendPlayLog(nil, entry); err != nil { log.Printf("AddPlayLog(): %s", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } success := true sendJsonResponse(w, &success) } // USER API func UserGetAuthKeys(w http.ResponseWriter, r *djRequest) { user, _ := r.Ctx.Db.GetUser(nil, r.AuthUser) sendJsonResponse(w, user.AuthKeyIds) } func UserCreateAuthKey(w http.ResponseWriter, r *djRequest) { s, err := r.Ctx.Db.NewSession() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } defer s.Close() user, _ := r.Ctx.Db.GetUser(s, r.AuthUser) authKey := user.NewAuthKey() err = r.Ctx.Db.PutAuthKey(s, authKey) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } sendJsonResponse(w, authKey) } func UserDeleteAuthKey(w http.ResponseWriter, r *djRequest) { s, err := r.Ctx.Db.NewSession() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } defer s.Close() user, _ := r.Ctx.Db.GetUser(s, r.AuthUser) keyId := r.Request.FormValue("auth_key_id") var newKeyIds []string for _, k := range user.AuthKeyIds { if k != keyId { newKeyIds = append(newKeyIds, k) } } user.AuthKeyIds = newKeyIds err = r.Ctx.Db.PutUser(s, user) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(200) }