From 43af05cf910279de2fa82b16c071fde5ef29fc20 Mon Sep 17 00:00:00 2001 From: Erno Date: Sun, 19 Oct 2025 08:46:25 +0000 Subject: [PATCH] feat(api): add unified models, mappers, and prefixed routes (movie/tv/search) --- api/index.go | 9 +- main.go | 9 +- pkg/handlers/unified.go | 200 ++++++++++++++++++++++++++ pkg/handlers/unified_helpers.go | 23 +++ pkg/models/unified.go | 88 ++++++++++++ pkg/services/tmdb.go | 87 +---------- pkg/services/unified_mapper.go | 248 ++++++++++++++++++++++++++++++++ 7 files changed, 579 insertions(+), 85 deletions(-) create mode 100644 pkg/handlers/unified.go create mode 100644 pkg/handlers/unified_helpers.go create mode 100644 pkg/models/unified.go create mode 100644 pkg/services/unified_mapper.go diff --git a/api/index.go b/api/index.go index ccca41d..2207a27 100644 --- a/api/index.go +++ b/api/index.go @@ -67,7 +67,8 @@ func Handler(w http.ResponseWriter, r *http.Request) { tvHandler := handlersPkg.NewTVHandler(tvService) favoritesHandler := handlersPkg.NewFavoritesHandler(favoritesService, globalCfg) docsHandler := handlersPkg.NewDocsHandler() - searchHandler := handlersPkg.NewSearchHandler(tmdbService, kpService) + searchHandler := handlersPkg.NewSearchHandler(tmdbService, kpService) + unifiedHandler := handlersPkg.NewUnifiedHandler(tmdbService, kpService) categoriesHandler := handlersPkg.NewCategoriesHandler(tmdbService) playersHandler := handlersPkg.NewPlayersHandler(globalCfg) 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/upcoming", movieHandler.Upcoming).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}/similar", movieHandler.GetSimilar).Methods("GET") api.HandleFunc("/movies/{id}/external-ids", movieHandler.GetExternalIDs).Methods("GET") diff --git a/main.go b/main.go index 87ab5ff..4877d19 100644 --- a/main.go +++ b/main.go @@ -47,7 +47,8 @@ func main() { tvHandler := appHandlers.NewTVHandler(tvService) favoritesHandler := appHandlers.NewFavoritesHandler(favoritesService, cfg) docsHandler := appHandlers.NewDocsHandler() - searchHandler := appHandlers.NewSearchHandler(tmdbService, kpService) + searchHandler := appHandlers.NewSearchHandler(tmdbService, kpService) + unifiedHandler := appHandlers.NewUnifiedHandler(tmdbService, kpService) categoriesHandler := appHandlers.NewCategoriesHandler(tmdbService) playersHandler := appHandlers.NewPlayersHandler(cfg) torrentsHandler := appHandlers.NewTorrentsHandler(torrentService, tmdbService) @@ -100,7 +101,11 @@ func main() { api.HandleFunc("/movies/top-rated", movieHandler.TopRated).Methods("GET") api.HandleFunc("/movies/upcoming", movieHandler.Upcoming).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}/similar", movieHandler.GetSimilar).Methods("GET") api.HandleFunc("/movies/{id}/external-ids", movieHandler.GetExternalIDs).Methods("GET") diff --git a/pkg/handlers/unified.go b/pkg/handlers/unified.go new file mode 100644 index 0000000..8d391c2 --- /dev/null +++ b/pkg/handlers/unified.go @@ -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) +} diff --git a/pkg/handlers/unified_helpers.go b/pkg/handlers/unified_helpers.go new file mode 100644 index 0000000..bd7543b --- /dev/null +++ b/pkg/handlers/unified_helpers.go @@ -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"` +} diff --git a/pkg/models/unified.go b/pkg/models/unified.go new file mode 100644 index 0000000..2c274bd --- /dev/null +++ b/pkg/models/unified.go @@ -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"` +} diff --git a/pkg/services/tmdb.go b/pkg/services/tmdb.go index 040f368..8415964 100644 --- a/pkg/services/tmdb.go +++ b/pkg/services/tmdb.go @@ -1,14 +1,13 @@ package services import ( - "encoding/json" - "fmt" - "net/http" - "net/url" - "strconv" - "strings" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strconv" - "neomovies-api/pkg/models" + "neomovies-api/pkg/models" ) type TMDBService struct { @@ -180,80 +179,6 @@ func (s *TMDBService) GetTVShow(id int, language string) (*models.TVShow, error) 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) { params := url.Values{} diff --git a/pkg/services/unified_mapper.go b/pkg/services/unified_mapper.go new file mode 100644 index 0000000..3700beb --- /dev/null +++ b/pkg/services/unified_mapper.go @@ -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 +}