Skip to content
Snippets Groups Projects
Select Git revision
  • 8ec118963abacef13ffede11cd86465d411425da
  • master default protected
2 results

update.sh

Blame
  • 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)
    }