From a28438fac6cc45195e89ea4ee5b72d200bb9f973 Mon Sep 17 00:00:00 2001 From: ShukeBta <272197458+ShukeBta@users.noreply.github.com> Date: Fri, 26 Jun 2026 20:00:42 +0800 Subject: [PATCH] refactor tmdb provider by responsibility --- internal/service/tmdb.go | 390 ------------------------------- internal/service/tmdb_details.go | 117 ++++++++++ internal/service/tmdb_episode.go | 68 ++++++ internal/service/tmdb_helpers.go | 46 ++++ internal/service/tmdb_match.go | 146 ++++++++++++ internal/service/tmdb_types.go | 41 ++++ 6 files changed, 418 insertions(+), 390 deletions(-) create mode 100644 internal/service/tmdb_details.go create mode 100644 internal/service/tmdb_episode.go create mode 100644 internal/service/tmdb_helpers.go create mode 100644 internal/service/tmdb_match.go create mode 100644 internal/service/tmdb_types.go diff --git a/internal/service/tmdb.go b/internal/service/tmdb.go index 87f95ec..9d3852e 100644 --- a/internal/service/tmdb.go +++ b/internal/service/tmdb.go @@ -20,8 +20,6 @@ import ( "encoding/json" "fmt" "net/http" - "net/url" - "strings" "time" "go.uber.org/zap" @@ -120,29 +118,6 @@ func (t *TMDbProvider) resolveBaseURL(ctx context.Context) string { return base } -// Match describes a successful metadata match. The same struct is reused -// across providers; provider-specific IDs sit side-by-side so the scraper -// orchestrator can write them all into a single update. -type Match struct { - TMDbID int `json:"tmdb_id"` - BangumiID int `json:"bangumi_id"` - DoubanID string `json:"douban_id,omitempty"` - TheTVDBID string `json:"thetvdb_id,omitempty"` - MediaType string `json:"media_type,omitempty"` - Title string `json:"title"` - OriginalName string `json:"original_name,omitempty"` - Overview string `json:"overview"` - PosterURL string `json:"poster_url"` - BackdropURL string `json:"backdrop_url"` - Year int `json:"year"` - Rating float32 `json:"rating"` - Languages []string `json:"languages,omitempty"` - Countries []string `json:"countries,omitempty"` - Genres []string `json:"genres,omitempty"` - NSFW bool `json:"nsfw,omitempty"` - SearchKeyword string `json:"-"` -} - func (t *TMDbProvider) getJSON(ctx context.Context, url string, out any) error { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { @@ -158,368 +133,3 @@ func (t *TMDbProvider) getJSON(ctx context.Context, url string, out any) error { } return json.NewDecoder(resp.Body).Decode(out) } - -func (t *TMDbProvider) GetMovieMatch(ctx context.Context, tmdbID int) (*Match, error) { - if tmdbID <= 0 { - return nil, nil - } - apiKey := t.resolveAPIKey(ctx) - if apiKey == "" { - return nil, nil - } - base := t.resolveBaseURL(ctx) - q := url.Values{} - q.Set("api_key", apiKey) - q.Set("language", "zh-CN") - u := base + "/movie/" + fmt.Sprint(tmdbID) + "?" + q.Encode() - var r struct { - ID int `json:"id"` - Title string `json:"title"` - OriginalTitle string `json:"original_title"` - OriginalLanguage string `json:"original_language"` - Overview string `json:"overview"` - PosterPath string `json:"poster_path"` - BackdropPath string `json:"backdrop_path"` - ReleaseDate string `json:"release_date"` - VoteAverage float32 `json:"vote_average"` - Genres []struct { - Name string `json:"name"` - } `json:"genres"` - ProductionCountries []struct { - Iso3166_1 string `json:"iso_3166_1"` - } `json:"production_countries"` - SpokenLanguages []struct { - Iso639_1 string `json:"iso_639_1"` - } `json:"spoken_languages"` - } - if err := t.getJSON(ctx, u, &r); err != nil { - return nil, err - } - m := &Match{ - TMDbID: r.ID, - MediaType: "movie", - Title: r.Title, - OriginalName: r.OriginalTitle, - Overview: r.Overview, - Rating: r.VoteAverage, - Languages: nonEmptyStrings(r.OriginalLanguage), - } - if m.Title == "" { - m.Title = r.OriginalTitle - } - if r.PosterPath != "" { - m.PosterURL = t.imgCDN + "/w500" + r.PosterPath - } - if r.BackdropPath != "" { - m.BackdropURL = t.imgCDN + "/w1280" + r.BackdropPath - } - if len(r.ReleaseDate) >= 4 { - _, _ = fmt.Sscanf(r.ReleaseDate[:4], "%d", &m.Year) - } - for _, g := range r.Genres { - m.Genres = append(m.Genres, g.Name) - } - for _, c := range r.ProductionCountries { - m.Countries = append(m.Countries, c.Iso3166_1) - } - for _, l := range r.SpokenLanguages { - m.Languages = append(m.Languages, l.Iso639_1) - } - m.Genres = deduplicate(m.Genres) - m.Countries = deduplicate(m.Countries) - m.Languages = deduplicate(m.Languages) - return m, nil -} - -func (t *TMDbProvider) GetTVMatch(ctx context.Context, tmdbID int) (*Match, error) { - if tmdbID <= 0 { - return nil, nil - } - apiKey := t.resolveAPIKey(ctx) - if apiKey == "" { - return nil, nil - } - base := t.resolveBaseURL(ctx) - q := url.Values{} - q.Set("api_key", apiKey) - q.Set("language", "zh-CN") - u := base + "/tv/" + fmt.Sprint(tmdbID) + "?" + q.Encode() - var r struct { - ID int `json:"id"` - Name string `json:"name"` - OriginalName string `json:"original_name"` - OriginalLanguage string `json:"original_language"` - OriginCountry []string `json:"origin_country"` - Overview string `json:"overview"` - PosterPath string `json:"poster_path"` - BackdropPath string `json:"backdrop_path"` - FirstAirDate string `json:"first_air_date"` - VoteAverage float32 `json:"vote_average"` - Genres []struct { - Name string `json:"name"` - } `json:"genres"` - SpokenLanguages []struct { - Iso639_1 string `json:"iso_639_1"` - } `json:"spoken_languages"` - } - if err := t.getJSON(ctx, u, &r); err != nil { - return nil, err - } - m := &Match{ - TMDbID: r.ID, - MediaType: "tv", - Title: r.Name, - OriginalName: r.OriginalName, - Overview: r.Overview, - Rating: r.VoteAverage, - Languages: nonEmptyStrings(r.OriginalLanguage), - Countries: deduplicate(r.OriginCountry), - } - if m.Title == "" { - m.Title = r.OriginalName - } - if r.PosterPath != "" { - m.PosterURL = t.imgCDN + "/w500" + r.PosterPath - } - if r.BackdropPath != "" { - m.BackdropURL = t.imgCDN + "/w1280" + r.BackdropPath - } - if len(r.FirstAirDate) >= 4 { - _, _ = fmt.Sscanf(r.FirstAirDate[:4], "%d", &m.Year) - } - for _, g := range r.Genres { - m.Genres = append(m.Genres, g.Name) - } - for _, l := range r.SpokenLanguages { - m.Languages = append(m.Languages, l.Iso639_1) - } - m.Genres = deduplicate(m.Genres) - m.Languages = deduplicate(m.Languages) - return m, nil -} - -// TMDbEpisodeDetails holds per-episode metadata from /tv/{id}/season/{season}/episode/{episode}. -type TMDbEpisodeDetails struct { - Name string - Overview string - StillURL string - AirYear int - Rating float32 - Runtime int -} - -func (t *TMDbProvider) GetTVEpisodeDetails(ctx context.Context, tmdbID, season, episode int) (*TMDbEpisodeDetails, error) { - if tmdbID <= 0 || episode <= 0 { - return nil, nil - } - apiKey := t.resolveAPIKey(ctx) - if apiKey == "" { - return nil, nil - } - base := t.resolveBaseURL(ctx) - q := url.Values{} - q.Set("api_key", apiKey) - q.Set("language", "zh-CN") - u := base + "/tv/" + fmt.Sprint(tmdbID) + "/season/" + fmt.Sprint(season) + "/episode/" + fmt.Sprint(episode) + "?" + q.Encode() - var r struct { - Name string `json:"name"` - Overview string `json:"overview"` - StillPath string `json:"still_path"` - AirDate string `json:"air_date"` - VoteAverage float32 `json:"vote_average"` - Runtime int `json:"runtime"` - } - if err := t.getJSON(ctx, u, &r); err != nil { - return nil, err - } - details := &TMDbEpisodeDetails{ - Name: r.Name, - Overview: r.Overview, - Rating: r.VoteAverage, - Runtime: r.Runtime, - } - if r.StillPath != "" { - details.StillURL = t.imgCDN + "/w500" + r.StillPath - } - if len(r.AirDate) >= 4 { - _, _ = fmt.Sscanf(r.AirDate[:4], "%d", &details.AirYear) - } - return details, nil -} - -// TMDbDetails holds extended metadata from the /movie/{id} or /tv/{id} endpoints. -type TMDbDetails struct { - Languages []string `json:"languages"` - Countries []string `json:"countries"` - Genres []string `json:"genres"` -} - -// GetDetails fetches extended metadata for a TMDb ID. -// It calls /movie/{id} or /tv/{id} with append_to_response=genres -// and extracts languages, production countries, and genres. -// mediaType should be "movie" or "tv". -func (t *TMDbProvider) GetDetails(ctx context.Context, tmdbID int, mediaType string) (*TMDbDetails, error) { - apiKey := t.resolveAPIKey(ctx) - if apiKey == "" { - return nil, fmt.Errorf("tmdb: no API key available") - } - base := t.resolveBaseURL(ctx) - - path := "/movie/" + fmt.Sprint(tmdbID) - if mediaType == "tv" { - path = "/tv/" + fmt.Sprint(tmdbID) - } - - q := url.Values{} - q.Set("api_key", apiKey) - q.Set("language", "zh-CN") - q.Set("append_to_response", "genres") - u := base + path + "?" + q.Encode() - - // Response structs for /movie/{id} and /tv/{id} - type genre struct { - Name string `json:"name"` - } - type movieResult struct { - OriginalLanguage string `json:"original_language"` - ProductionCountries []struct { - Iso3166_1 string `json:"iso_3166_1"` - } `json:"production_countries"` - SpokenLanguages []struct { - Iso639_1 string `json:"iso_639_1"` - } `json:"spoken_languages"` - Genres []genre `json:"genres"` - } - type tvResult struct { - OriginCountry []string `json:"origin_country"` - SpokenLanguages []struct { - Iso639_1 string `json:"iso_639_1"` - } `json:"spoken_languages"` - Genres []genre `json:"genres"` - } - - var ( - languages []string - countries []string - genres []string - ) - - if mediaType == "tv" { - var r tvResult - if err := t.getJSON(ctx, u, &r); err != nil { - return nil, err - } - // Spoken languages - for _, l := range r.SpokenLanguages { - languages = append(languages, l.Iso639_1) - } - // Origin countries - countries = append(countries, r.OriginCountry...) - // Genres - for _, g := range r.Genres { - genres = append(genres, g.Name) - } - } else { - var r movieResult - if err := t.getJSON(ctx, u, &r); err != nil { - return nil, err - } - // Original language - if r.OriginalLanguage != "" { - languages = append(languages, r.OriginalLanguage) - } - // Spoken languages - for _, l := range r.SpokenLanguages { - languages = append(languages, l.Iso639_1) - } - // Production countries - for _, c := range r.ProductionCountries { - countries = append(countries, c.Iso3166_1) - } - // Genres - for _, g := range r.Genres { - genres = append(genres, g.Name) - } - } - - // Deduplicate - languages = deduplicate(languages) - countries = deduplicate(countries) - genres = deduplicate(genres) - - t.log.Debug("tmdb: getDetails", - zap.Int("tmdb_id", tmdbID), - zap.String("type", mediaType), - zap.Strings("languages", languages), - zap.Strings("countries", countries), - zap.Strings("genres", genres), - ) - - return &TMDbDetails{ - Languages: languages, - Countries: countries, - Genres: genres, - }, nil -} - -func (t *TMDbProvider) GetTVEpisodeCount(ctx context.Context, tmdbID int) (int, error) { - if tmdbID <= 0 { - return 0, nil - } - apiKey := t.resolveAPIKey(ctx) - if apiKey == "" { - return 0, nil - } - base := t.resolveBaseURL(ctx) - q := url.Values{} - q.Set("api_key", apiKey) - q.Set("language", "zh-CN") - u := base + "/tv/" + fmt.Sprint(tmdbID) + "?" + q.Encode() - var r struct { - NumberOfEpisodes int `json:"number_of_episodes"` - } - if err := t.getJSON(ctx, u, &r); err != nil { - return 0, err - } - return r.NumberOfEpisodes, nil -} - -// deduplicate removes duplicates from a string slice. -func deduplicate(s []string) []string { - if len(s) == 0 { - return s - } - seen := make(map[string]bool, len(s)) - out := make([]string, 0, len(s)) - for _, v := range s { - if !seen[v] { - seen[v] = true - out = append(out, v) - } - } - return out -} - -func nonEmptyStrings(values ...string) []string { - out := make([]string, 0, len(values)) - for _, value := range values { - value = strings.TrimSpace(value) - if value != "" { - out = append(out, value) - } - } - return out -} - -func genreIDStrings(ids []int) []string { - if len(ids) == 0 { - return nil - } - out := make([]string, 0, len(ids)) - for _, id := range ids { - if id > 0 { - out = append(out, fmt.Sprint(id)) - } - } - return out -} diff --git a/internal/service/tmdb_details.go b/internal/service/tmdb_details.go new file mode 100644 index 0000000..f0c6aa3 --- /dev/null +++ b/internal/service/tmdb_details.go @@ -0,0 +1,117 @@ +package service + +import ( + "context" + "fmt" + "net/url" + + "go.uber.org/zap" +) + +// GetDetails fetches extended metadata for a TMDb ID. +// It calls /movie/{id} or /tv/{id} with append_to_response=genres +// and extracts languages, production countries, and genres. +// mediaType should be "movie" or "tv". +func (t *TMDbProvider) GetDetails(ctx context.Context, tmdbID int, mediaType string) (*TMDbDetails, error) { + apiKey := t.resolveAPIKey(ctx) + if apiKey == "" { + return nil, fmt.Errorf("tmdb: no API key available") + } + base := t.resolveBaseURL(ctx) + + path := "/movie/" + fmt.Sprint(tmdbID) + if mediaType == "tv" { + path = "/tv/" + fmt.Sprint(tmdbID) + } + + q := url.Values{} + q.Set("api_key", apiKey) + q.Set("language", "zh-CN") + q.Set("append_to_response", "genres") + u := base + path + "?" + q.Encode() + + // Response structs for /movie/{id} and /tv/{id} + type genre struct { + Name string `json:"name"` + } + type movieResult struct { + OriginalLanguage string `json:"original_language"` + ProductionCountries []struct { + Iso3166_1 string `json:"iso_3166_1"` + } `json:"production_countries"` + SpokenLanguages []struct { + Iso639_1 string `json:"iso_639_1"` + } `json:"spoken_languages"` + Genres []genre `json:"genres"` + } + type tvResult struct { + OriginCountry []string `json:"origin_country"` + SpokenLanguages []struct { + Iso639_1 string `json:"iso_639_1"` + } `json:"spoken_languages"` + Genres []genre `json:"genres"` + } + + var ( + languages []string + countries []string + genres []string + ) + + if mediaType == "tv" { + var r tvResult + if err := t.getJSON(ctx, u, &r); err != nil { + return nil, err + } + // Spoken languages + for _, l := range r.SpokenLanguages { + languages = append(languages, l.Iso639_1) + } + // Origin countries + countries = append(countries, r.OriginCountry...) + // Genres + for _, g := range r.Genres { + genres = append(genres, g.Name) + } + } else { + var r movieResult + if err := t.getJSON(ctx, u, &r); err != nil { + return nil, err + } + // Original language + if r.OriginalLanguage != "" { + languages = append(languages, r.OriginalLanguage) + } + // Spoken languages + for _, l := range r.SpokenLanguages { + languages = append(languages, l.Iso639_1) + } + // Production countries + for _, c := range r.ProductionCountries { + countries = append(countries, c.Iso3166_1) + } + // Genres + for _, g := range r.Genres { + genres = append(genres, g.Name) + } + } + + // Deduplicate + languages = deduplicate(languages) + countries = deduplicate(countries) + genres = deduplicate(genres) + + t.log.Debug("tmdb: getDetails", + zap.Int("tmdb_id", tmdbID), + zap.String("type", mediaType), + zap.Strings("languages", languages), + zap.Strings("countries", countries), + zap.Strings("genres", genres), + ) + + return &TMDbDetails{ + Languages: languages, + Countries: countries, + Genres: genres, + }, nil +} diff --git a/internal/service/tmdb_episode.go b/internal/service/tmdb_episode.go new file mode 100644 index 0000000..264a79e --- /dev/null +++ b/internal/service/tmdb_episode.go @@ -0,0 +1,68 @@ +package service + +import ( + "context" + "fmt" + "net/url" +) + +func (t *TMDbProvider) GetTVEpisodeDetails(ctx context.Context, tmdbID, season, episode int) (*TMDbEpisodeDetails, error) { + if tmdbID <= 0 || episode <= 0 { + return nil, nil + } + apiKey := t.resolveAPIKey(ctx) + if apiKey == "" { + return nil, nil + } + base := t.resolveBaseURL(ctx) + q := url.Values{} + q.Set("api_key", apiKey) + q.Set("language", "zh-CN") + u := base + "/tv/" + fmt.Sprint(tmdbID) + "/season/" + fmt.Sprint(season) + "/episode/" + fmt.Sprint(episode) + "?" + q.Encode() + var r struct { + Name string `json:"name"` + Overview string `json:"overview"` + StillPath string `json:"still_path"` + AirDate string `json:"air_date"` + VoteAverage float32 `json:"vote_average"` + Runtime int `json:"runtime"` + } + if err := t.getJSON(ctx, u, &r); err != nil { + return nil, err + } + details := &TMDbEpisodeDetails{ + Name: r.Name, + Overview: r.Overview, + Rating: r.VoteAverage, + Runtime: r.Runtime, + } + if r.StillPath != "" { + details.StillURL = t.imgCDN + "/w500" + r.StillPath + } + if len(r.AirDate) >= 4 { + _, _ = fmt.Sscanf(r.AirDate[:4], "%d", &details.AirYear) + } + return details, nil +} + +func (t *TMDbProvider) GetTVEpisodeCount(ctx context.Context, tmdbID int) (int, error) { + if tmdbID <= 0 { + return 0, nil + } + apiKey := t.resolveAPIKey(ctx) + if apiKey == "" { + return 0, nil + } + base := t.resolveBaseURL(ctx) + q := url.Values{} + q.Set("api_key", apiKey) + q.Set("language", "zh-CN") + u := base + "/tv/" + fmt.Sprint(tmdbID) + "?" + q.Encode() + var r struct { + NumberOfEpisodes int `json:"number_of_episodes"` + } + if err := t.getJSON(ctx, u, &r); err != nil { + return 0, err + } + return r.NumberOfEpisodes, nil +} diff --git a/internal/service/tmdb_helpers.go b/internal/service/tmdb_helpers.go new file mode 100644 index 0000000..e5bd9af --- /dev/null +++ b/internal/service/tmdb_helpers.go @@ -0,0 +1,46 @@ +package service + +import ( + "fmt" + "strings" +) + +// deduplicate removes duplicates from a string slice. +func deduplicate(s []string) []string { + if len(s) == 0 { + return s + } + seen := make(map[string]bool, len(s)) + out := make([]string, 0, len(s)) + for _, v := range s { + if !seen[v] { + seen[v] = true + out = append(out, v) + } + } + return out +} + +func nonEmptyStrings(values ...string) []string { + out := make([]string, 0, len(values)) + for _, value := range values { + value = strings.TrimSpace(value) + if value != "" { + out = append(out, value) + } + } + return out +} + +func genreIDStrings(ids []int) []string { + if len(ids) == 0 { + return nil + } + out := make([]string, 0, len(ids)) + for _, id := range ids { + if id > 0 { + out = append(out, fmt.Sprint(id)) + } + } + return out +} diff --git a/internal/service/tmdb_match.go b/internal/service/tmdb_match.go new file mode 100644 index 0000000..93778f3 --- /dev/null +++ b/internal/service/tmdb_match.go @@ -0,0 +1,146 @@ +package service + +import ( + "context" + "fmt" + "net/url" +) + +func (t *TMDbProvider) GetMovieMatch(ctx context.Context, tmdbID int) (*Match, error) { + if tmdbID <= 0 { + return nil, nil + } + apiKey := t.resolveAPIKey(ctx) + if apiKey == "" { + return nil, nil + } + base := t.resolveBaseURL(ctx) + q := url.Values{} + q.Set("api_key", apiKey) + q.Set("language", "zh-CN") + u := base + "/movie/" + fmt.Sprint(tmdbID) + "?" + q.Encode() + var r struct { + ID int `json:"id"` + Title string `json:"title"` + OriginalTitle string `json:"original_title"` + OriginalLanguage string `json:"original_language"` + Overview string `json:"overview"` + PosterPath string `json:"poster_path"` + BackdropPath string `json:"backdrop_path"` + ReleaseDate string `json:"release_date"` + VoteAverage float32 `json:"vote_average"` + Genres []struct { + Name string `json:"name"` + } `json:"genres"` + ProductionCountries []struct { + Iso3166_1 string `json:"iso_3166_1"` + } `json:"production_countries"` + SpokenLanguages []struct { + Iso639_1 string `json:"iso_639_1"` + } `json:"spoken_languages"` + } + if err := t.getJSON(ctx, u, &r); err != nil { + return nil, err + } + m := &Match{ + TMDbID: r.ID, + MediaType: "movie", + Title: r.Title, + OriginalName: r.OriginalTitle, + Overview: r.Overview, + Rating: r.VoteAverage, + Languages: nonEmptyStrings(r.OriginalLanguage), + } + if m.Title == "" { + m.Title = r.OriginalTitle + } + if r.PosterPath != "" { + m.PosterURL = t.imgCDN + "/w500" + r.PosterPath + } + if r.BackdropPath != "" { + m.BackdropURL = t.imgCDN + "/w1280" + r.BackdropPath + } + if len(r.ReleaseDate) >= 4 { + _, _ = fmt.Sscanf(r.ReleaseDate[:4], "%d", &m.Year) + } + for _, g := range r.Genres { + m.Genres = append(m.Genres, g.Name) + } + for _, c := range r.ProductionCountries { + m.Countries = append(m.Countries, c.Iso3166_1) + } + for _, l := range r.SpokenLanguages { + m.Languages = append(m.Languages, l.Iso639_1) + } + m.Genres = deduplicate(m.Genres) + m.Countries = deduplicate(m.Countries) + m.Languages = deduplicate(m.Languages) + return m, nil +} + +func (t *TMDbProvider) GetTVMatch(ctx context.Context, tmdbID int) (*Match, error) { + if tmdbID <= 0 { + return nil, nil + } + apiKey := t.resolveAPIKey(ctx) + if apiKey == "" { + return nil, nil + } + base := t.resolveBaseURL(ctx) + q := url.Values{} + q.Set("api_key", apiKey) + q.Set("language", "zh-CN") + u := base + "/tv/" + fmt.Sprint(tmdbID) + "?" + q.Encode() + var r struct { + ID int `json:"id"` + Name string `json:"name"` + OriginalName string `json:"original_name"` + OriginalLanguage string `json:"original_language"` + OriginCountry []string `json:"origin_country"` + Overview string `json:"overview"` + PosterPath string `json:"poster_path"` + BackdropPath string `json:"backdrop_path"` + FirstAirDate string `json:"first_air_date"` + VoteAverage float32 `json:"vote_average"` + Genres []struct { + Name string `json:"name"` + } `json:"genres"` + SpokenLanguages []struct { + Iso639_1 string `json:"iso_639_1"` + } `json:"spoken_languages"` + } + if err := t.getJSON(ctx, u, &r); err != nil { + return nil, err + } + m := &Match{ + TMDbID: r.ID, + MediaType: "tv", + Title: r.Name, + OriginalName: r.OriginalName, + Overview: r.Overview, + Rating: r.VoteAverage, + Languages: nonEmptyStrings(r.OriginalLanguage), + Countries: deduplicate(r.OriginCountry), + } + if m.Title == "" { + m.Title = r.OriginalName + } + if r.PosterPath != "" { + m.PosterURL = t.imgCDN + "/w500" + r.PosterPath + } + if r.BackdropPath != "" { + m.BackdropURL = t.imgCDN + "/w1280" + r.BackdropPath + } + if len(r.FirstAirDate) >= 4 { + _, _ = fmt.Sscanf(r.FirstAirDate[:4], "%d", &m.Year) + } + for _, g := range r.Genres { + m.Genres = append(m.Genres, g.Name) + } + for _, l := range r.SpokenLanguages { + m.Languages = append(m.Languages, l.Iso639_1) + } + m.Genres = deduplicate(m.Genres) + m.Languages = deduplicate(m.Languages) + return m, nil +} diff --git a/internal/service/tmdb_types.go b/internal/service/tmdb_types.go new file mode 100644 index 0000000..f8adc37 --- /dev/null +++ b/internal/service/tmdb_types.go @@ -0,0 +1,41 @@ +package service + +// Match describes a successful metadata match. The same struct is reused +// across providers; provider-specific IDs sit side-by-side so the scraper +// orchestrator can write them all into a single update. +type Match struct { + TMDbID int `json:"tmdb_id"` + BangumiID int `json:"bangumi_id"` + DoubanID string `json:"douban_id,omitempty"` + TheTVDBID string `json:"thetvdb_id,omitempty"` + MediaType string `json:"media_type,omitempty"` + Title string `json:"title"` + OriginalName string `json:"original_name,omitempty"` + Overview string `json:"overview"` + PosterURL string `json:"poster_url"` + BackdropURL string `json:"backdrop_url"` + Year int `json:"year"` + Rating float32 `json:"rating"` + Languages []string `json:"languages,omitempty"` + Countries []string `json:"countries,omitempty"` + Genres []string `json:"genres,omitempty"` + NSFW bool `json:"nsfw,omitempty"` + SearchKeyword string `json:"-"` +} + +// TMDbEpisodeDetails holds per-episode metadata from /tv/{id}/season/{season}/episode/{episode}. +type TMDbEpisodeDetails struct { + Name string + Overview string + StillURL string + AirYear int + Rating float32 + Runtime int +} + +// TMDbDetails holds extended metadata from the /movie/{id} or /tv/{id} endpoints. +type TMDbDetails struct { + Languages []string `json:"languages"` + Countries []string `json:"countries"` + Genres []string `json:"genres"` +}