mirror of
https://github.com/aykhans/movier.git
synced 2025-07-20 00:54:03 +00:00
Rewritten in go and python
This commit is contained in:
301
server/pkg/handlers/imdb.go
Normal file
301
server/pkg/handlers/imdb.go
Normal file
@@ -0,0 +1,301 @@
|
||||
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,
|
||||
)
|
||||
}
|
18
server/pkg/handlers/middlewares.go
Normal file
18
server/pkg/handlers/middlewares.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package handlers
|
||||
|
||||
import "net/http"
|
||||
|
||||
func CORSMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
64
server/pkg/handlers/responses.go
Normal file
64
server/pkg/handlers/responses.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/aykhans/movier/server/pkg/config"
|
||||
)
|
||||
|
||||
type ErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func RespondWithServerError(w http.ResponseWriter) {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func RespondWithJSON(w http.ResponseWriter, data any, statusCode int) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(statusCode)
|
||||
if err := json.NewEncoder(w).Encode(data); err != nil {
|
||||
log.Printf("error encoding response: %v", err)
|
||||
RespondWithServerError(w)
|
||||
}
|
||||
}
|
||||
|
||||
func formatNumber(n uint) string {
|
||||
s := fmt.Sprintf("%d", n)
|
||||
var result strings.Builder
|
||||
length := len(s)
|
||||
|
||||
for i, digit := range s {
|
||||
if i > 0 && (length-i)%3 == 0 {
|
||||
result.WriteString(",")
|
||||
}
|
||||
result.WriteRune(digit)
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func RespondWithHTML(w http.ResponseWriter, templateName string, data any, statusCode int) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(statusCode)
|
||||
|
||||
funcMap := template.FuncMap{
|
||||
"formatNumber": formatNumber,
|
||||
}
|
||||
|
||||
t, err := template.New(templateName).Funcs(funcMap).ParseFiles(config.GetTemplatePath() + "/" + templateName)
|
||||
if err != nil {
|
||||
log.Printf("error parsing template: %v", err)
|
||||
RespondWithServerError(w)
|
||||
return
|
||||
}
|
||||
err = t.Execute(w, data)
|
||||
if err != nil {
|
||||
log.Printf("error executing template: %v", err)
|
||||
RespondWithServerError(w)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user