feat(api): support prefixed IDs in routes and add unified mappers scaffolding

- Handlers parse IDs like kp_123 / tmdb_456 and set id_type accordingly
- Add KP->Unified and TMDB->Unified movie mappers (basic fields)
- Keep backward compatibility for numeric IDs
This commit is contained in:
Erno
2025-10-19 08:30:16 +00:00
parent 0fbf0f0f42
commit c7aa844f49
4 changed files with 220 additions and 10 deletions

View File

@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"github.com/gorilla/mux" "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) { func (h *MovieHandler) GetByID(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"]) 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 { if err != nil {
http.Error(w, "Invalid movie ID", http.StatusBadRequest) http.Error(w, "Invalid movie ID", http.StatusBadRequest)
return return
} }
id = parsed
}
language := GetLanguage(r) 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) movie, err := h.movieService.GetByID(id, language, idType)
if err != nil { if err != nil {

View File

@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"github.com/gorilla/mux" "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) { func (h *TVHandler) GetByID(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"]) 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 { if err != nil {
http.Error(w, "Invalid TV show ID", http.StatusBadRequest) http.Error(w, "Invalid TV show ID", http.StatusBadRequest)
return return
} }
id = parsed
}
language := GetLanguage(r) 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) tvShow, err := h.tvService.GetByID(id, language, idType)
if err != nil { if err != nil {

View File

@@ -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 { func MapKPSearchToTMDBResponse(kpSearch *KPSearchResponse) *models.TMDBResponse {
if kpSearch == nil { if kpSearch == nil {
return &models.TMDBResponse{ return &models.TMDBResponse{

View File

@@ -6,6 +6,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
"strings"
"neomovies-api/pkg/models" "neomovies-api/pkg/models"
) )
@@ -179,6 +180,80 @@ 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{}