mirror of
https://github.com/aykhans/movier.git
synced 2025-04-17 04:13:12 +00:00
302 lines
9.5 KiB
Go
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,
|
|
)
|
|
}
|