diff --git a/pkg/handlers/movie.go b/pkg/handlers/movie.go index 53249b2..f7afb72 100644 --- a/pkg/handlers/movie.go +++ b/pkg/handlers/movie.go @@ -4,6 +4,7 @@ import ( "encoding/json" "net/http" "strconv" + "strings" "github.com/gorilla/mux" @@ -48,14 +49,39 @@ func (h *MovieHandler) Search(w http.ResponseWriter, r *http.Request) { func (h *MovieHandler) GetByID(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) - id, err := strconv.Atoi(vars["id"]) - if err != nil { - http.Error(w, "Invalid movie ID", http.StatusBadRequest) - return + rawID := vars["id"] + + // Support formats: "123" (old), "kp_123", "tmdb_123" + source := "" + var id int + if strings.Contains(rawID, "_") { + parts := strings.SplitN(rawID, "_", 2) + if len(parts) != 2 { + http.Error(w, "Invalid ID format", http.StatusBadRequest) + return + } + source = parts[0] + parsed, err := strconv.Atoi(parts[1]) + if err != nil { + http.Error(w, "Invalid numeric ID", http.StatusBadRequest) + return + } + id = parsed + } else { + // Backward compatibility + parsed, err := strconv.Atoi(rawID) + if err != nil { + http.Error(w, "Invalid movie ID", http.StatusBadRequest) + return + } + id = parsed } language := GetLanguage(r) - idType := r.URL.Query().Get("id_type") // kp or tmdb + idType := r.URL.Query().Get("id_type") + if source == "kp" || source == "tmdb" { + idType = source + } movie, err := h.movieService.GetByID(id, language, idType) if err != nil { diff --git a/pkg/handlers/tv.go b/pkg/handlers/tv.go index 14a4ada..468f2d7 100644 --- a/pkg/handlers/tv.go +++ b/pkg/handlers/tv.go @@ -4,6 +4,7 @@ import ( "encoding/json" "net/http" "strconv" + "strings" "github.com/gorilla/mux" @@ -47,14 +48,39 @@ func (h *TVHandler) Search(w http.ResponseWriter, r *http.Request) { func (h *TVHandler) GetByID(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) - id, err := strconv.Atoi(vars["id"]) - if err != nil { - http.Error(w, "Invalid TV show ID", http.StatusBadRequest) - return + rawID := vars["id"] + + // Support formats: "123" (old), "kp_123", "tmdb_123" + source := "" + var id int + if strings.Contains(rawID, "_") { + parts := strings.SplitN(rawID, "_", 2) + if len(parts) != 2 { + http.Error(w, "Invalid ID format", http.StatusBadRequest) + return + } + source = parts[0] + parsed, err := strconv.Atoi(parts[1]) + if err != nil { + http.Error(w, "Invalid numeric ID", http.StatusBadRequest) + return + } + id = parsed + } else { + // Backward compatibility + parsed, err := strconv.Atoi(rawID) + if err != nil { + http.Error(w, "Invalid TV show ID", http.StatusBadRequest) + return + } + id = parsed } language := GetLanguage(r) - idType := r.URL.Query().Get("id_type") // kp or tmdb + idType := r.URL.Query().Get("id_type") + if source == "kp" || source == "tmdb" { + idType = source + } tvShow, err := h.tvService.GetByID(id, language, idType) if err != nil { diff --git a/pkg/services/kp_mapper.go b/pkg/services/kp_mapper.go index ee7b916..982ad3f 100644 --- a/pkg/services/kp_mapper.go +++ b/pkg/services/kp_mapper.go @@ -167,6 +167,89 @@ func MapKPFilmToTVShow(kpFilm *KPFilm) *models.TVShow { } } +// Unified mappers with prefixed IDs +func MapKPToUnified(kpFilm *KPFilm) *models.UnifiedContent { + if kpFilm == nil { + return nil + } + + releaseDate := FormatKPDate(kpFilm.Year) + endDate := (*string)(nil) + if kpFilm.EndYear > 0 { + v := FormatKPDate(kpFilm.EndYear) + endDate = &v + } + + genres := make([]models.UnifiedGenre, 0) + for _, g := range kpFilm.Genres { + genres = append(genres, models.UnifiedGenre{ID: strings.ToLower(g.Genre), Name: g.Genre}) + } + + poster := kpFilm.PosterUrlPreview + if poster == "" { + poster = kpFilm.PosterUrl + } + + country := "" + if len(kpFilm.Countries) > 0 { + country = kpFilm.Countries[0].Country + } + + title := kpFilm.NameRu + if title == "" { + title = kpFilm.NameEn + } + originalTitle := kpFilm.NameOriginal + if originalTitle == "" { + originalTitle = kpFilm.NameEn + } + + var budgetPtr *int64 + var revenuePtr *int64 + + external := models.UnifiedExternalIDs{KP: &kpFilm.KinopoiskId, TMDB: nil, IMDb: kpFilm.ImdbId} + + return &models.UnifiedContent{ + ID: strconv.Itoa(kpFilm.KinopoiskId), + SourceID: "kp_" + strconv.Itoa(kpFilm.KinopoiskId), + Title: title, + OriginalTitle: originalTitle, + Description: firstNonEmpty(kpFilm.Description, kpFilm.ShortDescription), + ReleaseDate: releaseDate, + EndDate: endDate, + Type: mapKPTypeToUnified(kpFilm), + Genres: genres, + Rating: kpFilm.RatingKinopoisk, + PosterURL: poster, + BackdropURL: kpFilm.CoverUrl, + Director: "", + Cast: []models.UnifiedCastMember{}, + Duration: kpFilm.FilmLength, + Country: country, + Language: detectLanguage(kpFilm), + Budget: budgetPtr, + Revenue: revenuePtr, + IMDbID: kpFilm.ImdbId, + ExternalIDs: external, + } +} + +func mapKPTypeToUnified(kp *KPFilm) string { + if kp.Serial || kp.Type == "TV_SERIES" || kp.Type == "MINI_SERIES" { + return "tv" + } + return "movie" +} + +func firstNonEmpty(values ...string) string { + for _, v := range values { + if strings.TrimSpace(v) != "" { + return v + } + } + return "" +} + func MapKPSearchToTMDBResponse(kpSearch *KPSearchResponse) *models.TMDBResponse { if kpSearch == nil { return &models.TMDBResponse{ diff --git a/pkg/services/tmdb.go b/pkg/services/tmdb.go index dc37abc..040f368 100644 --- a/pkg/services/tmdb.go +++ b/pkg/services/tmdb.go @@ -6,6 +6,7 @@ import ( "net/http" "net/url" "strconv" + "strings" "neomovies-api/pkg/models" ) @@ -179,6 +180,80 @@ 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{}