302 lines
9.5 KiB
Go

package handlers
import (
"fmt"
"log"
"net/http"
"strconv"
"strings"
"github.com/aykhans/movier/server/pkg/dto"
"github.com/aykhans/movier/server/pkg/proto"
"github.com/aykhans/movier/server/pkg/storage/postgresql/repository"
"github.com/aykhans/movier/server/pkg/utils"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type IMDbHandler struct {
imdbRepo repository.IMDbRepository
grpcRecommenderService *grpc.ClientConn
baseURL string
}
func NewIMDbHandler(imdbRepo repository.IMDbRepository, grpcRecommenderService *grpc.ClientConn, baseURL string) *IMDbHandler {
return &IMDbHandler{
imdbRepo: imdbRepo,
grpcRecommenderService: grpcRecommenderService,
baseURL: baseURL,
}
}
func (h *IMDbHandler) HandlerGetRecommendations(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
tconstsQ := query["tconst"]
tconstsLen := len(tconstsQ)
if tconstsLen < 1 || tconstsLen > 5 {
RespondWithJSON(w, ErrorResponse{Error: "tconsts should be between 1 and 5"}, http.StatusBadRequest)
return
}
uniqueTconsts := make(map[string]struct{})
for _, str := range tconstsQ {
uniqueTconsts[str] = struct{}{}
}
invalidTconsts := []string{}
tconsts := []string{}
for tconst := range uniqueTconsts {
tconstLength := len(tconst)
if 9 > tconstLength || tconstLength > 12 || !strings.HasPrefix(tconst, "tt") {
invalidTconsts = append(invalidTconsts, tconst)
}
tconsts = append(tconsts, tconst)
}
if len(invalidTconsts) > 0 {
RespondWithJSON(
w,
ErrorResponse{
Error: fmt.Sprintf("Invalid tconsts: %s", strings.Join(invalidTconsts, ", ")),
},
http.StatusBadRequest,
)
return
}
n := 5
nQuery := query.Get("n")
if nQuery != "" {
nInt, err := strconv.Atoi(nQuery)
if err != nil {
RespondWithJSON(w, ErrorResponse{Error: "n should be an integer"}, http.StatusBadRequest)
return
}
if nInt < 1 || nInt > 20 {
RespondWithJSON(w, ErrorResponse{Error: "n should be greater than 0 and less than 21"}, http.StatusBadRequest)
return
}
n = nInt
}
filter := &proto.Filter{}
minVotesQ := query.Get("min_votes")
if minVotesQ != "" {
minVotesInt, err := strconv.Atoi(minVotesQ)
if err != nil {
RespondWithJSON(w, ErrorResponse{Error: "min_votes should be an integer"}, http.StatusBadRequest)
return
}
if !utils.IsUint32(minVotesInt) {
RespondWithJSON(w, ErrorResponse{Error: "min_votes should be greater than or equal to 0 and less than or equal to 4294967295"}, http.StatusBadRequest)
return
}
filter.MinVotesOneof = &proto.Filter_MinVotes{MinVotes: uint32(minVotesInt)}
}
maxVotesQ := query.Get("max_votes")
if maxVotesQ != "" {
maxVotesInt, err := strconv.Atoi(maxVotesQ)
if err != nil {
RespondWithJSON(w, ErrorResponse{Error: "max_votes should be an integer"}, http.StatusBadRequest)
return
}
if !utils.IsUint32(maxVotesInt) {
RespondWithJSON(w, ErrorResponse{Error: "max_votes should be greater than 0 or equal to and less than or equal to 4294967295"}, http.StatusBadRequest)
return
}
if uint32(maxVotesInt) < filter.GetMinVotes() {
RespondWithJSON(w, ErrorResponse{Error: "max_votes should be greater than min_votes"}, http.StatusBadRequest)
return
}
filter.MaxVotesOneof = &proto.Filter_MaxVotes{MaxVotes: uint32(maxVotesInt)}
}
minRatingQ := query.Get("min_rating")
if minRatingQ != "" {
minRatingFloat, err := strconv.ParseFloat(minRatingQ, 32)
if err != nil {
RespondWithJSON(w, ErrorResponse{Error: "min_rating should be a float"}, http.StatusBadRequest)
return
}
if minRatingFloat < 0 || minRatingFloat > 10 {
RespondWithJSON(w, ErrorResponse{Error: "min_rating should be greater than or equal to 0.0 and less than equal to 10.0"}, http.StatusBadRequest)
return
}
filter.MinRatingOneof = &proto.Filter_MinRating{MinRating: float32(minRatingFloat)}
}
maxRatingQ := query.Get("max_rating")
if maxRatingQ != "" {
maxRatingFloat, err := strconv.ParseFloat(maxRatingQ, 32)
if err != nil {
RespondWithJSON(w, ErrorResponse{Error: "max_rating should be a float"}, http.StatusBadRequest)
return
}
if maxRatingFloat < 0 || maxRatingFloat > 10 {
RespondWithJSON(w, ErrorResponse{Error: "max_rating should be greater than or equal to 0.0 and less than or equal to 10.0"}, http.StatusBadRequest)
return
}
if float32(maxRatingFloat) < filter.GetMinRating() {
RespondWithJSON(w, ErrorResponse{Error: "max_rating should be greater than min_rating"}, http.StatusBadRequest)
return
}
filter.MaxRatingOneof = &proto.Filter_MaxRating{MaxRating: float32(maxRatingFloat)}
}
minYearQ := query.Get("min_year")
if minYearQ != "" {
minYearInt, err := strconv.Atoi(minYearQ)
if err != nil {
RespondWithJSON(w, ErrorResponse{Error: "min_year should be an integer"}, http.StatusBadRequest)
return
}
if !utils.IsUint32(minYearInt) {
RespondWithJSON(w, ErrorResponse{Error: "min_year should be greater than or equal to 0 and less than or equal to 4294967295"}, http.StatusBadRequest)
return
}
filter.MinYearOneof = &proto.Filter_MinYear{MinYear: uint32(minYearInt)}
}
maxYearQ := query.Get("max_year")
if maxYearQ != "" {
maxYearInt, err := strconv.Atoi(maxYearQ)
if err != nil {
RespondWithJSON(w, ErrorResponse{Error: "max_year should be an integer"}, http.StatusBadRequest)
return
}
if !utils.IsUint32(maxYearInt) {
RespondWithJSON(w, ErrorResponse{Error: "max_year should be greater than or equal to 0 and less than or equal to 4294967295"}, http.StatusBadRequest)
return
}
if uint32(maxYearInt) < filter.GetMinYear() {
RespondWithJSON(w, ErrorResponse{Error: "max_year should be greater than min_year"}, http.StatusBadRequest)
return
}
filter.MaxYearOneof = &proto.Filter_MaxYear{MaxYear: uint32(maxYearInt)}
}
yearWeightQ := query.Get("year_weight")
ratingWeightQ := query.Get("rating_weight")
genresWeightQ := query.Get("genres_weight")
nconstsWeightQ := query.Get("nconsts_weight")
weight := &proto.Weight{}
features := []string{}
totalSum := 0
if yearWeightQ != "" {
yearWeight, err := strconv.Atoi(yearWeightQ)
if err != nil {
RespondWithJSON(w, ErrorResponse{Error: "year_weight should be an integer"}, http.StatusBadRequest)
return
}
if yearWeight < 0 || yearWeight > 400 {
RespondWithJSON(w, ErrorResponse{Error: "year_weight should be greater than or equal to 0 and less than or equal to 400"}, http.StatusBadRequest)
return
}
weight.Year = uint32(yearWeight)
totalSum += yearWeight
features = append(features, "year")
}
if ratingWeightQ != "" {
ratingWeight, err := strconv.Atoi(ratingWeightQ)
if err != nil {
RespondWithJSON(w, ErrorResponse{Error: "rating_weight should be an integer"}, http.StatusBadRequest)
return
}
if ratingWeight < 0 || ratingWeight > 400 {
RespondWithJSON(w, ErrorResponse{Error: "rating_weight should be greater than or equal to 0 and less than or equal to 400"}, http.StatusBadRequest)
return
}
weight.Rating = uint32(ratingWeight)
totalSum += ratingWeight
features = append(features, "rating")
}
if genresWeightQ != "" {
genresWeight, err := strconv.Atoi(genresWeightQ)
if err != nil {
RespondWithJSON(w, ErrorResponse{Error: "genres_weight should be an integer"}, http.StatusBadRequest)
return
}
if genresWeight < 0 || genresWeight > 400 {
RespondWithJSON(w, ErrorResponse{Error: "genres_weight should be greater than or equal to 0 and less than or equal to 400"}, http.StatusBadRequest)
return
}
weight.Genres = uint32(genresWeight)
totalSum += genresWeight
features = append(features, "genres")
}
if nconstsWeightQ != "" {
nconstsWeight, err := strconv.Atoi(nconstsWeightQ)
if err != nil {
RespondWithJSON(w, ErrorResponse{Error: "nconsts_weight should be an integer"}, http.StatusBadRequest)
return
}
if nconstsWeight < 0 || nconstsWeight > 400 {
RespondWithJSON(w, ErrorResponse{Error: "nconsts_weight should be greater than or equal to 0 and less than or equal to 400"}, http.StatusBadRequest)
return
}
weight.Nconsts = uint32(nconstsWeight)
totalSum += nconstsWeight
features = append(features, "nconsts")
}
featuresLen := len(features)
if featuresLen < 1 {
RespondWithJSON(w, ErrorResponse{Error: "At least one feature should be selected"}, http.StatusBadRequest)
return
}
if featuresLen*100 != totalSum {
RespondWithJSON(w, ErrorResponse{Error: fmt.Sprintf("Sum of the %d features should be equal to %d", featuresLen, featuresLen*100)}, http.StatusBadRequest)
return
}
client := proto.NewRecommenderClient(h.grpcRecommenderService)
response, err := client.GetRecommendations(r.Context(), &proto.Request{
Tconsts: tconsts,
N: uint32(n),
Filter: filter,
Weight: weight,
})
if err != nil {
if st, ok := status.FromError(err); ok {
switch st.Code() {
case codes.InvalidArgument:
RespondWithJSON(w, ErrorResponse{Error: st.Message()}, http.StatusBadRequest)
case codes.NotFound:
RespondWithJSON(w, ErrorResponse{Error: st.Message()}, http.StatusNotFound)
case codes.Internal:
RespondWithServerError(w)
default:
fmt.Println(err)
RespondWithServerError(w)
}
return
}
RespondWithServerError(w)
return
}
RespondWithJSON(w, response.Movies, http.StatusOK)
}
func (h *IMDbHandler) HandlerHome(w http.ResponseWriter, r *http.Request) {
minMax, err := h.imdbRepo.GetMinMax()
if err != nil {
log.Printf("error getting min max: %v", err)
RespondWithServerError(w)
return
}
RespondWithHTML(
w, "index.html",
struct {
MinMax dto.MinMax
BaseURL string
}{*minMax, h.baseURL},
http.StatusOK,
)
}