Select Git revision
api_views.go 7.76 KiB
package frontend
import (
"encoding/json"
// "fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"time"
"git.autistici.org/ale/djrandom/api"
"git.autistici.org/ale/djrandom/util"
db_client "git.autistici.org/ale/djrandom/services/database/client"
"github.com/gorilla/mux"
)
var (
numParallelQueries = 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 := util.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)
song, ok := r.Ctx.Db.GetSong(nil, songId)
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
}
rCh := util.RunAtOnce(numParallelQueries, fpReq.Fingerprints, func(fp string) interface{} {
if _, ok := r.Ctx.Db.GetAudioFile(s, fp); ok {
return &fpResponse{fp, true}
}
return &fpResponse{fp, false}
})
// Collector.
fpResp := api.FingerprintResponse{
Missing: make([]string, 0),
Dupes: make([]string, 0),
}
for data := range rCh {
fpr := (data).(*fpResponse)
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.
resp := api.SearchIdsResponse{
Results: make([]string, 0),
}
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).
songIdMap := r.Ctx.Index.Search(query)
songIds := make([]string, 0, len(songIdMap))
for songId, _ := range songIdMap {
songIds = append(songIds, songId)
}
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")
newKeyIds := make([]string, 0)
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)
}