mirror of
https://gitlab.com/foxixus/neomovies-api.git
synced 2025-10-28 01:48:51 +05:00
feat(api): add unified models, mappers, and prefixed routes (movie/tv/search)
This commit is contained in:
200
pkg/handlers/unified.go
Normal file
200
pkg/handlers/unified.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"neomovies-api/pkg/models"
|
||||
"neomovies-api/pkg/services"
|
||||
)
|
||||
|
||||
type UnifiedHandler struct {
|
||||
tmdb *services.TMDBService
|
||||
kp *services.KinopoiskService
|
||||
}
|
||||
|
||||
func NewUnifiedHandler(tmdb *services.TMDBService, kp *services.KinopoiskService) *UnifiedHandler {
|
||||
return &UnifiedHandler{tmdb: tmdb, kp: kp}
|
||||
}
|
||||
|
||||
// Parse source ID of form "kp_123" or "tmdb_456"
|
||||
func parseSourceID(raw string) (source string, id int, err error) {
|
||||
parts := strings.SplitN(raw, "_", 2)
|
||||
if len(parts) != 2 {
|
||||
return "", 0, strconv.ErrSyntax
|
||||
}
|
||||
src := strings.ToLower(parts[0])
|
||||
if src != "kp" && src != "tmdb" {
|
||||
return "", 0, strconv.ErrSyntax
|
||||
}
|
||||
num, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
return src, num, nil
|
||||
}
|
||||
|
||||
func (h *UnifiedHandler) GetMovie(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
vars := muxVars(r)
|
||||
rawID := vars["id"]
|
||||
|
||||
source, id, err := parseSourceID(rawID)
|
||||
if err != nil {
|
||||
writeUnifiedError(w, http.StatusBadRequest, "invalid SOURCE_ID format", start, "")
|
||||
return
|
||||
}
|
||||
|
||||
language := GetLanguage(r)
|
||||
var data *models.UnifiedContent
|
||||
if source == "kp" {
|
||||
if h.kp == nil {
|
||||
writeUnifiedError(w, http.StatusBadGateway, "Kinopoisk service not configured", start, source)
|
||||
return
|
||||
}
|
||||
kpFilm, err := h.kp.GetFilmByKinopoiskId(id)
|
||||
if err != nil {
|
||||
writeUnifiedError(w, http.StatusBadGateway, err.Error(), start, source)
|
||||
return
|
||||
}
|
||||
data = services.MapKPToUnified(kpFilm)
|
||||
} else {
|
||||
// tmdb
|
||||
movie, err := h.tmdb.GetMovie(id, language)
|
||||
if err != nil {
|
||||
writeUnifiedError(w, http.StatusBadGateway, err.Error(), start, source)
|
||||
return
|
||||
}
|
||||
ext, _ := h.tmdb.GetMovieExternalIDs(id)
|
||||
data = services.MapTMDBToUnifiedMovie(movie, ext)
|
||||
}
|
||||
|
||||
writeUnifiedOK(w, data, start, source, "")
|
||||
}
|
||||
|
||||
func (h *UnifiedHandler) GetTV(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
vars := muxVars(r)
|
||||
rawID := vars["id"]
|
||||
|
||||
source, id, err := parseSourceID(rawID)
|
||||
if err != nil {
|
||||
writeUnifiedError(w, http.StatusBadRequest, "invalid SOURCE_ID format", start, "")
|
||||
return
|
||||
}
|
||||
|
||||
language := GetLanguage(r)
|
||||
var data *models.UnifiedContent
|
||||
if source == "kp" {
|
||||
if h.kp == nil {
|
||||
writeUnifiedError(w, http.StatusBadGateway, "Kinopoisk service not configured", start, source)
|
||||
return
|
||||
}
|
||||
kpFilm, err := h.kp.GetFilmByKinopoiskId(id)
|
||||
if err != nil {
|
||||
writeUnifiedError(w, http.StatusBadGateway, err.Error(), start, source)
|
||||
return
|
||||
}
|
||||
data = services.MapKPToUnified(kpFilm)
|
||||
} else {
|
||||
tv, err := h.tmdb.GetTVShow(id, language)
|
||||
if err != nil {
|
||||
writeUnifiedError(w, http.StatusBadGateway, err.Error(), start, source)
|
||||
return
|
||||
}
|
||||
ext, _ := h.tmdb.GetTVExternalIDs(id)
|
||||
data = services.MapTMDBTVToUnified(tv, ext)
|
||||
}
|
||||
|
||||
writeUnifiedOK(w, data, start, source, "")
|
||||
}
|
||||
|
||||
func (h *UnifiedHandler) Search(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
query := r.URL.Query().Get("query")
|
||||
if strings.TrimSpace(query) == "" {
|
||||
writeUnifiedError(w, http.StatusBadRequest, "query is required", start, "")
|
||||
return
|
||||
}
|
||||
source := strings.ToLower(r.URL.Query().Get("source")) // kp|tmdb
|
||||
page := getIntQuery(r, "page", 1)
|
||||
language := GetLanguage(r)
|
||||
|
||||
if source != "kp" && source != "tmdb" {
|
||||
writeUnifiedError(w, http.StatusBadRequest, "source must be 'kp' or 'tmdb'", start, "")
|
||||
return
|
||||
}
|
||||
|
||||
if source == "kp" {
|
||||
if h.kp == nil {
|
||||
writeUnifiedError(w, http.StatusBadGateway, "Kinopoisk service not configured", start, source)
|
||||
return
|
||||
}
|
||||
kpSearch, err := h.kp.SearchFilms(query, page)
|
||||
if err != nil {
|
||||
writeUnifiedError(w, http.StatusBadGateway, err.Error(), start, source)
|
||||
return
|
||||
}
|
||||
items := services.MapKPSearchToUnifiedItems(kpSearch)
|
||||
resp := models.UnifiedSearchResponse{
|
||||
Success: true,
|
||||
Data: items,
|
||||
Source: source,
|
||||
Pagination: models.UnifiedPagination{Page: page, TotalPages: kpSearch.PagesCount, TotalResults: kpSearch.SearchFilmsCountResult, PageSize: len(items)},
|
||||
Metadata: models.UnifiedMetadata{FetchedAt: time.Now(), APIVersion: "3.0", ResponseTime: time.Since(start).Milliseconds(), Query: query},
|
||||
}
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
return
|
||||
}
|
||||
|
||||
// TMDB multi search
|
||||
multi, err := h.tmdb.SearchMulti(query, page, language)
|
||||
if err != nil {
|
||||
writeUnifiedError(w, http.StatusBadGateway, err.Error(), start, source)
|
||||
return
|
||||
}
|
||||
items := services.MapTMDBMultiToUnifiedItems(multi)
|
||||
resp := models.UnifiedSearchResponse{
|
||||
Success: true,
|
||||
Data: items,
|
||||
Source: source,
|
||||
Pagination: models.UnifiedPagination{Page: multi.Page, TotalPages: multi.TotalPages, TotalResults: multi.TotalResults, PageSize: len(items)},
|
||||
Metadata: models.UnifiedMetadata{FetchedAt: time.Now(), APIVersion: "3.0", ResponseTime: time.Since(start).Milliseconds(), Query: query},
|
||||
}
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func writeUnifiedOK(w http.ResponseWriter, data *models.UnifiedContent, start time.Time, source string, query string) {
|
||||
resp := models.UnifiedAPIResponse{
|
||||
Success: true,
|
||||
Data: data,
|
||||
Source: source,
|
||||
Metadata: models.UnifiedMetadata{
|
||||
FetchedAt: time.Now(),
|
||||
APIVersion: "3.0",
|
||||
ResponseTime: time.Since(start).Milliseconds(),
|
||||
Query: query,
|
||||
},
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
func writeUnifiedError(w http.ResponseWriter, code int, message string, start time.Time, source string) {
|
||||
resp := models.UnifiedAPIResponse{
|
||||
Success: false,
|
||||
Error: message,
|
||||
Source: source,
|
||||
Metadata: models.UnifiedMetadata{
|
||||
FetchedAt: time.Now(),
|
||||
APIVersion: "3.0",
|
||||
ResponseTime: time.Since(start).Milliseconds(),
|
||||
},
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
23
pkg/handlers/unified_helpers.go
Normal file
23
pkg/handlers/unified_helpers.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func muxVars(r *http.Request) map[string]string { return mux.Vars(r) }
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
type metaEnvelope struct {
|
||||
FetchedAt time.Time `json:"fetchedAt"`
|
||||
APIVersion string `json:"apiVersion"`
|
||||
ResponseTime int64 `json:"responseTime"`
|
||||
}
|
||||
Reference in New Issue
Block a user