mirror of
https://gitlab.com/foxixus/neomovies-api.git
synced 2025-10-27 17:38:51 +05:00
feat(api): add unified models, mappers, and prefixed routes (movie/tv/search)
This commit is contained in:
@@ -67,7 +67,8 @@ func Handler(w http.ResponseWriter, r *http.Request) {
|
|||||||
tvHandler := handlersPkg.NewTVHandler(tvService)
|
tvHandler := handlersPkg.NewTVHandler(tvService)
|
||||||
favoritesHandler := handlersPkg.NewFavoritesHandler(favoritesService, globalCfg)
|
favoritesHandler := handlersPkg.NewFavoritesHandler(favoritesService, globalCfg)
|
||||||
docsHandler := handlersPkg.NewDocsHandler()
|
docsHandler := handlersPkg.NewDocsHandler()
|
||||||
searchHandler := handlersPkg.NewSearchHandler(tmdbService, kpService)
|
searchHandler := handlersPkg.NewSearchHandler(tmdbService, kpService)
|
||||||
|
unifiedHandler := handlersPkg.NewUnifiedHandler(tmdbService, kpService)
|
||||||
categoriesHandler := handlersPkg.NewCategoriesHandler(tmdbService)
|
categoriesHandler := handlersPkg.NewCategoriesHandler(tmdbService)
|
||||||
playersHandler := handlersPkg.NewPlayersHandler(globalCfg)
|
playersHandler := handlersPkg.NewPlayersHandler(globalCfg)
|
||||||
torrentsHandler := handlersPkg.NewTorrentsHandler(torrentService, tmdbService)
|
torrentsHandler := handlersPkg.NewTorrentsHandler(torrentService, tmdbService)
|
||||||
@@ -123,7 +124,11 @@ func Handler(w http.ResponseWriter, r *http.Request) {
|
|||||||
api.HandleFunc("/movies/top-rated", movieHandler.TopRated).Methods("GET")
|
api.HandleFunc("/movies/top-rated", movieHandler.TopRated).Methods("GET")
|
||||||
api.HandleFunc("/movies/upcoming", movieHandler.Upcoming).Methods("GET")
|
api.HandleFunc("/movies/upcoming", movieHandler.Upcoming).Methods("GET")
|
||||||
api.HandleFunc("/movies/now-playing", movieHandler.NowPlaying).Methods("GET")
|
api.HandleFunc("/movies/now-playing", movieHandler.NowPlaying).Methods("GET")
|
||||||
api.HandleFunc("/movies/{id}", movieHandler.GetByID).Methods("GET")
|
api.HandleFunc("/movies/{id}", movieHandler.GetByID).Methods("GET")
|
||||||
|
// Unified prefixed routes
|
||||||
|
api.HandleFunc("/movie/{id}", unifiedHandler.GetMovie).Methods("GET")
|
||||||
|
api.HandleFunc("/tv/{id}", unifiedHandler.GetTV).Methods("GET")
|
||||||
|
api.HandleFunc("/search", unifiedHandler.Search).Methods("GET")
|
||||||
api.HandleFunc("/movies/{id}/recommendations", movieHandler.GetRecommendations).Methods("GET")
|
api.HandleFunc("/movies/{id}/recommendations", movieHandler.GetRecommendations).Methods("GET")
|
||||||
api.HandleFunc("/movies/{id}/similar", movieHandler.GetSimilar).Methods("GET")
|
api.HandleFunc("/movies/{id}/similar", movieHandler.GetSimilar).Methods("GET")
|
||||||
api.HandleFunc("/movies/{id}/external-ids", movieHandler.GetExternalIDs).Methods("GET")
|
api.HandleFunc("/movies/{id}/external-ids", movieHandler.GetExternalIDs).Methods("GET")
|
||||||
|
|||||||
9
main.go
9
main.go
@@ -47,7 +47,8 @@ func main() {
|
|||||||
tvHandler := appHandlers.NewTVHandler(tvService)
|
tvHandler := appHandlers.NewTVHandler(tvService)
|
||||||
favoritesHandler := appHandlers.NewFavoritesHandler(favoritesService, cfg)
|
favoritesHandler := appHandlers.NewFavoritesHandler(favoritesService, cfg)
|
||||||
docsHandler := appHandlers.NewDocsHandler()
|
docsHandler := appHandlers.NewDocsHandler()
|
||||||
searchHandler := appHandlers.NewSearchHandler(tmdbService, kpService)
|
searchHandler := appHandlers.NewSearchHandler(tmdbService, kpService)
|
||||||
|
unifiedHandler := appHandlers.NewUnifiedHandler(tmdbService, kpService)
|
||||||
categoriesHandler := appHandlers.NewCategoriesHandler(tmdbService)
|
categoriesHandler := appHandlers.NewCategoriesHandler(tmdbService)
|
||||||
playersHandler := appHandlers.NewPlayersHandler(cfg)
|
playersHandler := appHandlers.NewPlayersHandler(cfg)
|
||||||
torrentsHandler := appHandlers.NewTorrentsHandler(torrentService, tmdbService)
|
torrentsHandler := appHandlers.NewTorrentsHandler(torrentService, tmdbService)
|
||||||
@@ -100,7 +101,11 @@ func main() {
|
|||||||
api.HandleFunc("/movies/top-rated", movieHandler.TopRated).Methods("GET")
|
api.HandleFunc("/movies/top-rated", movieHandler.TopRated).Methods("GET")
|
||||||
api.HandleFunc("/movies/upcoming", movieHandler.Upcoming).Methods("GET")
|
api.HandleFunc("/movies/upcoming", movieHandler.Upcoming).Methods("GET")
|
||||||
api.HandleFunc("/movies/now-playing", movieHandler.NowPlaying).Methods("GET")
|
api.HandleFunc("/movies/now-playing", movieHandler.NowPlaying).Methods("GET")
|
||||||
api.HandleFunc("/movies/{id}", movieHandler.GetByID).Methods("GET")
|
api.HandleFunc("/movies/{id}", movieHandler.GetByID).Methods("GET")
|
||||||
|
// Unified prefixed routes
|
||||||
|
api.HandleFunc("/movie/{id}", unifiedHandler.GetMovie).Methods("GET")
|
||||||
|
api.HandleFunc("/tv/{id}", unifiedHandler.GetTV).Methods("GET")
|
||||||
|
api.HandleFunc("/search", unifiedHandler.Search).Methods("GET")
|
||||||
api.HandleFunc("/movies/{id}/recommendations", movieHandler.GetRecommendations).Methods("GET")
|
api.HandleFunc("/movies/{id}/recommendations", movieHandler.GetRecommendations).Methods("GET")
|
||||||
api.HandleFunc("/movies/{id}/similar", movieHandler.GetSimilar).Methods("GET")
|
api.HandleFunc("/movies/{id}/similar", movieHandler.GetSimilar).Methods("GET")
|
||||||
api.HandleFunc("/movies/{id}/external-ids", movieHandler.GetExternalIDs).Methods("GET")
|
api.HandleFunc("/movies/{id}/external-ids", movieHandler.GetExternalIDs).Methods("GET")
|
||||||
|
|||||||
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"`
|
||||||
|
}
|
||||||
88
pkg/models/unified.go
Normal file
88
pkg/models/unified.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Unified entities and response envelopes for prefixed-source API
|
||||||
|
|
||||||
|
type UnifiedGenre struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnifiedCastMember struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Character string `json:"character,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnifiedExternalIDs struct {
|
||||||
|
KP *int `json:"kp"`
|
||||||
|
TMDB *int `json:"tmdb"`
|
||||||
|
IMDb string `json:"imdb"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnifiedContent struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
SourceID string `json:"sourceId"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
OriginalTitle string `json:"originalTitle"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
ReleaseDate string `json:"releaseDate"`
|
||||||
|
EndDate *string `json:"endDate"`
|
||||||
|
Type string `json:"type"` // movie | tv
|
||||||
|
Genres []UnifiedGenre `json:"genres"`
|
||||||
|
Rating float64 `json:"rating"`
|
||||||
|
PosterURL string `json:"posterUrl"`
|
||||||
|
BackdropURL string `json:"backdropUrl"`
|
||||||
|
Director string `json:"director"`
|
||||||
|
Cast []UnifiedCastMember `json:"cast"`
|
||||||
|
Duration int `json:"duration"`
|
||||||
|
Country string `json:"country"`
|
||||||
|
Language string `json:"language"`
|
||||||
|
Budget *int64 `json:"budget"`
|
||||||
|
Revenue *int64 `json:"revenue"`
|
||||||
|
IMDbID string `json:"imdbId"`
|
||||||
|
ExternalIDs UnifiedExternalIDs `json:"externalIds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnifiedSearchItem struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
SourceID string `json:"sourceId"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
ReleaseDate string `json:"releaseDate"`
|
||||||
|
PosterURL string `json:"posterUrl"`
|
||||||
|
Rating float64 `json:"rating"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
ExternalIDs UnifiedExternalIDs `json:"externalIds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnifiedPagination struct {
|
||||||
|
Page int `json:"page"`
|
||||||
|
TotalPages int `json:"totalPages"`
|
||||||
|
TotalResults int `json:"totalResults"`
|
||||||
|
PageSize int `json:"pageSize"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnifiedMetadata struct {
|
||||||
|
FetchedAt time.Time `json:"fetchedAt"`
|
||||||
|
APIVersion string `json:"apiVersion"`
|
||||||
|
ResponseTime int64 `json:"responseTime"`
|
||||||
|
Query string `json:"query,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnifiedAPIResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Data interface{} `json:"data,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
Source string `json:"source,omitempty"`
|
||||||
|
Metadata UnifiedMetadata `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnifiedSearchResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Data []UnifiedSearchItem `json:"data"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
Pagination UnifiedPagination `json:"pagination"`
|
||||||
|
Metadata UnifiedMetadata `json:"metadata"`
|
||||||
|
}
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"neomovies-api/pkg/models"
|
"neomovies-api/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TMDBService struct {
|
type TMDBService struct {
|
||||||
@@ -180,80 +179,6 @@ func (s *TMDBService) GetTVShow(id int, language string) (*models.TVShow, error)
|
|||||||
return &tvShow, err
|
return &tvShow, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map TMDB movie to unified content with prefixed IDs. Requires optional external IDs for imdbId.
|
|
||||||
func MapTMDBToUnifiedMovie(movie *models.Movie, external *models.ExternalIDs) *models.UnifiedContent {
|
|
||||||
if movie == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
genres := make([]models.UnifiedGenre, 0, len(movie.Genres))
|
|
||||||
for _, g := range movie.Genres {
|
|
||||||
name := strings.TrimSpace(g.Name)
|
|
||||||
id := strings.ToLower(strings.ReplaceAll(name, " ", "-"))
|
|
||||||
if id == "" {
|
|
||||||
id = strconv.Itoa(g.ID)
|
|
||||||
}
|
|
||||||
genres = append(genres, models.UnifiedGenre{ID: id, Name: name})
|
|
||||||
}
|
|
||||||
|
|
||||||
var imdb string
|
|
||||||
if external != nil {
|
|
||||||
imdb = external.IMDbID
|
|
||||||
}
|
|
||||||
|
|
||||||
var budgetPtr *int64
|
|
||||||
if movie.Budget > 0 {
|
|
||||||
v := movie.Budget
|
|
||||||
budgetPtr = &v
|
|
||||||
}
|
|
||||||
|
|
||||||
var revenuePtr *int64
|
|
||||||
if movie.Revenue > 0 {
|
|
||||||
v := movie.Revenue
|
|
||||||
revenuePtr = &v
|
|
||||||
}
|
|
||||||
|
|
||||||
ext := models.UnifiedExternalIDs{
|
|
||||||
KP: nil,
|
|
||||||
TMDB: &movie.ID,
|
|
||||||
IMDb: imdb,
|
|
||||||
}
|
|
||||||
|
|
||||||
return &models.UnifiedContent{
|
|
||||||
ID: strconv.Itoa(movie.ID),
|
|
||||||
SourceID: "tmdb_" + strconv.Itoa(movie.ID),
|
|
||||||
Title: movie.Title,
|
|
||||||
OriginalTitle: movie.OriginalTitle,
|
|
||||||
Description: movie.Overview,
|
|
||||||
ReleaseDate: movie.ReleaseDate,
|
|
||||||
EndDate: nil,
|
|
||||||
Type: "movie",
|
|
||||||
Genres: genres,
|
|
||||||
Rating: movie.VoteAverage,
|
|
||||||
PosterURL: movie.PosterPath,
|
|
||||||
BackdropURL: movie.BackdropPath,
|
|
||||||
Director: "",
|
|
||||||
Cast: []models.UnifiedCastMember{},
|
|
||||||
Duration: movie.Runtime,
|
|
||||||
Country: firstCountry(movie.ProductionCountries),
|
|
||||||
Language: movie.OriginalLanguage,
|
|
||||||
Budget: budgetPtr,
|
|
||||||
Revenue: revenuePtr,
|
|
||||||
IMDbID: imdb,
|
|
||||||
ExternalIDs: ext,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func firstCountry(countries []models.ProductionCountry) string {
|
|
||||||
if len(countries) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(countries[0].Name) != "" {
|
|
||||||
return countries[0].Name
|
|
||||||
}
|
|
||||||
return countries[0].ISO31661
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *TMDBService) GetGenres(mediaType string, language string) (*models.GenresResponse, error) {
|
func (s *TMDBService) GetGenres(mediaType string, language string) (*models.GenresResponse, error) {
|
||||||
params := url.Values{}
|
params := url.Values{}
|
||||||
|
|
||||||
|
|||||||
248
pkg/services/unified_mapper.go
Normal file
248
pkg/services/unified_mapper.go
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"neomovies-api/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
const tmdbImageBase = "https://image.tmdb.org/t/p"
|
||||||
|
|
||||||
|
func BuildTMDBImageURL(path string, size string) string {
|
||||||
|
if path == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
if size == "" {
|
||||||
|
size = "w500"
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(path, "/") {
|
||||||
|
path = "/" + path
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s/%s%s", tmdbImageBase, size, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MapTMDBToUnifiedMovie(movie *models.Movie, external *models.ExternalIDs) *models.UnifiedContent {
|
||||||
|
if movie == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
genres := make([]models.UnifiedGenre, 0, len(movie.Genres))
|
||||||
|
for _, g := range movie.Genres {
|
||||||
|
name := strings.TrimSpace(g.Name)
|
||||||
|
id := strings.ToLower(strings.ReplaceAll(name, " ", "-"))
|
||||||
|
if id == "" {
|
||||||
|
id = strconv.Itoa(g.ID)
|
||||||
|
}
|
||||||
|
genres = append(genres, models.UnifiedGenre{ID: id, Name: name})
|
||||||
|
}
|
||||||
|
|
||||||
|
var imdb string
|
||||||
|
if external != nil {
|
||||||
|
imdb = external.IMDbID
|
||||||
|
}
|
||||||
|
|
||||||
|
var budgetPtr *int64
|
||||||
|
if movie.Budget > 0 {
|
||||||
|
v := movie.Budget
|
||||||
|
budgetPtr = &v
|
||||||
|
}
|
||||||
|
var revenuePtr *int64
|
||||||
|
if movie.Revenue > 0 {
|
||||||
|
v := movie.Revenue
|
||||||
|
revenuePtr = &v
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := models.UnifiedExternalIDs{
|
||||||
|
KP: nil,
|
||||||
|
TMDB: &movie.ID,
|
||||||
|
IMDb: imdb,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.UnifiedContent{
|
||||||
|
ID: strconv.Itoa(movie.ID),
|
||||||
|
SourceID: "tmdb_" + strconv.Itoa(movie.ID),
|
||||||
|
Title: movie.Title,
|
||||||
|
OriginalTitle: movie.OriginalTitle,
|
||||||
|
Description: movie.Overview,
|
||||||
|
ReleaseDate: movie.ReleaseDate,
|
||||||
|
EndDate: nil,
|
||||||
|
Type: "movie",
|
||||||
|
Genres: genres,
|
||||||
|
Rating: movie.VoteAverage,
|
||||||
|
PosterURL: BuildTMDBImageURL(movie.PosterPath, "w500"),
|
||||||
|
BackdropURL: BuildTMDBImageURL(movie.BackdropPath, "w1280"),
|
||||||
|
Director: "",
|
||||||
|
Cast: []models.UnifiedCastMember{},
|
||||||
|
Duration: movie.Runtime,
|
||||||
|
Country: firstCountry(movie.ProductionCountries),
|
||||||
|
Language: movie.OriginalLanguage,
|
||||||
|
Budget: budgetPtr,
|
||||||
|
Revenue: revenuePtr,
|
||||||
|
IMDbID: imdb,
|
||||||
|
ExternalIDs: ext,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MapTMDBTVToUnified(tv *models.TVShow, external *models.ExternalIDs) *models.UnifiedContent {
|
||||||
|
if tv == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
genres := make([]models.UnifiedGenre, 0, len(tv.Genres))
|
||||||
|
for _, g := range tv.Genres {
|
||||||
|
name := strings.TrimSpace(g.Name)
|
||||||
|
id := strings.ToLower(strings.ReplaceAll(name, " ", "-"))
|
||||||
|
if id == "" {
|
||||||
|
id = strconv.Itoa(g.ID)
|
||||||
|
}
|
||||||
|
genres = append(genres, models.UnifiedGenre{ID: id, Name: name})
|
||||||
|
}
|
||||||
|
|
||||||
|
var imdb string
|
||||||
|
if external != nil {
|
||||||
|
imdb = external.IMDbID
|
||||||
|
}
|
||||||
|
|
||||||
|
endDate := (*string)(nil)
|
||||||
|
if strings.TrimSpace(tv.LastAirDate) != "" {
|
||||||
|
v := tv.LastAirDate
|
||||||
|
endDate = &v
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := models.UnifiedExternalIDs{
|
||||||
|
KP: nil,
|
||||||
|
TMDB: &tv.ID,
|
||||||
|
IMDb: imdb,
|
||||||
|
}
|
||||||
|
|
||||||
|
duration := 0
|
||||||
|
if len(tv.EpisodeRunTime) > 0 {
|
||||||
|
duration = tv.EpisodeRunTime[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.UnifiedContent{
|
||||||
|
ID: strconv.Itoa(tv.ID),
|
||||||
|
SourceID: "tmdb_" + strconv.Itoa(tv.ID),
|
||||||
|
Title: tv.Name,
|
||||||
|
OriginalTitle: tv.OriginalName,
|
||||||
|
Description: tv.Overview,
|
||||||
|
ReleaseDate: tv.FirstAirDate,
|
||||||
|
EndDate: endDate,
|
||||||
|
Type: "tv",
|
||||||
|
Genres: genres,
|
||||||
|
Rating: tv.VoteAverage,
|
||||||
|
PosterURL: BuildTMDBImageURL(tv.PosterPath, "w500"),
|
||||||
|
BackdropURL: BuildTMDBImageURL(tv.BackdropPath, "w1280"),
|
||||||
|
Director: "",
|
||||||
|
Cast: []models.UnifiedCastMember{},
|
||||||
|
Duration: duration,
|
||||||
|
Country: firstCountry(tv.ProductionCountries),
|
||||||
|
Language: tv.OriginalLanguage,
|
||||||
|
Budget: nil,
|
||||||
|
Revenue: nil,
|
||||||
|
IMDbID: imdb,
|
||||||
|
ExternalIDs: ext,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MapTMDBMultiToUnifiedItems(m *models.MultiSearchResponse) []models.UnifiedSearchItem {
|
||||||
|
if m == nil {
|
||||||
|
return []models.UnifiedSearchItem{}
|
||||||
|
}
|
||||||
|
items := make([]models.UnifiedSearchItem, 0, len(m.Results))
|
||||||
|
for _, r := range m.Results {
|
||||||
|
if r.MediaType != "movie" && r.MediaType != "tv" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
title := r.Title
|
||||||
|
if r.MediaType == "tv" {
|
||||||
|
title = r.Name
|
||||||
|
}
|
||||||
|
release := r.ReleaseDate
|
||||||
|
if r.MediaType == "tv" {
|
||||||
|
release = r.FirstAirDate
|
||||||
|
}
|
||||||
|
poster := BuildTMDBImageURL(r.PosterPath, "w500")
|
||||||
|
tmdbId := r.ID
|
||||||
|
items = append(items, models.UnifiedSearchItem{
|
||||||
|
ID: strconv.Itoa(tmdbId),
|
||||||
|
SourceID: "tmdb_" + strconv.Itoa(tmdbId),
|
||||||
|
Title: title,
|
||||||
|
Type: map[string]string{"movie":"movie","tv":"tv"}[r.MediaType],
|
||||||
|
ReleaseDate: release,
|
||||||
|
PosterURL: poster,
|
||||||
|
Rating: r.VoteAverage,
|
||||||
|
Description: r.Overview,
|
||||||
|
ExternalIDs: models.UnifiedExternalIDs{KP: nil, TMDB: &tmdbId, IMDb: ""},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
func MapKPSearchToUnifiedItems(kps *KPSearchResponse) []models.UnifiedSearchItem {
|
||||||
|
if kps == nil {
|
||||||
|
return []models.UnifiedSearchItem{}
|
||||||
|
}
|
||||||
|
items := make([]models.UnifiedSearchItem, 0, len(kps.Films))
|
||||||
|
for _, f := range kps.Films {
|
||||||
|
title := f.NameRu
|
||||||
|
if strings.TrimSpace(title) == "" {
|
||||||
|
title = f.NameEn
|
||||||
|
}
|
||||||
|
poster := f.PosterUrlPreview
|
||||||
|
if poster == "" {
|
||||||
|
poster = f.PosterUrl
|
||||||
|
}
|
||||||
|
rating := 0.0
|
||||||
|
if strings.TrimSpace(f.Rating) != "" {
|
||||||
|
if v, err := strconv.ParseFloat(f.Rating, 64); err == nil {
|
||||||
|
rating = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kpId := f.FilmId
|
||||||
|
items = append(items, models.UnifiedSearchItem{
|
||||||
|
ID: strconv.Itoa(kpId),
|
||||||
|
SourceID: "kp_" + strconv.Itoa(kpId),
|
||||||
|
Title: title,
|
||||||
|
Type: mapKPTypeToUnifiedShort(f.Type),
|
||||||
|
ReleaseDate: yearToDate(f.Year),
|
||||||
|
PosterURL: poster,
|
||||||
|
Rating: rating,
|
||||||
|
Description: f.Description,
|
||||||
|
ExternalIDs: models.UnifiedExternalIDs{KP: &kpId, TMDB: nil, IMDb: ""},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapKPTypeToUnifiedShort(t string) string {
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(t)) {
|
||||||
|
case "TV_SERIES", "MINI_SERIES":
|
||||||
|
return "tv"
|
||||||
|
default:
|
||||||
|
return "movie"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func yearToDate(y string) string {
|
||||||
|
y = strings.TrimSpace(y)
|
||||||
|
if y == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return y + "-01-01"
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstCountry(countries []models.ProductionCountry) string {
|
||||||
|
if len(countries) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(countries[0].Name) != "" {
|
||||||
|
return countries[0].Name
|
||||||
|
}
|
||||||
|
return countries[0].ISO31661
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user