Files
GoComicMosaic/gobackend/internal/utils/tmdb_multi_search.go
2025-07-16 15:31:27 +08:00

286 lines
9.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package utils
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"strings"
"time"
)
// TMDBMultiSearchResult 表示Multi Search API的单个结果项
type TMDBMultiSearchResult struct {
Adult bool `json:"adult"`
BackdropPath string `json:"backdrop_path"`
ID int `json:"id"`
Title string `json:"title,omitempty"` // 电影标题
OriginalTitle string `json:"original_title,omitempty"` // 电影原始标题
Name string `json:"name,omitempty"` // 电视剧标题
OriginalName string `json:"original_name,omitempty"` // 电视剧原始标题
Overview string `json:"overview"`
PosterPath string `json:"poster_path"`
MediaType string `json:"media_type"` // movie 或 tv
OriginalLanguage string `json:"original_language"`
GenreIDs []int `json:"genre_ids"`
Popularity float64 `json:"popularity"`
ReleaseDate string `json:"release_date,omitempty"` // 电影发行日期
FirstAirDate string `json:"first_air_date,omitempty"` // 电视剧首播日期
Video bool `json:"video,omitempty"` // 仅电影有此字段
VoteAverage float64 `json:"vote_average"`
VoteCount int `json:"vote_count"`
OriginCountry []string `json:"origin_country,omitempty"` // 仅电视剧有此字段
}
// TMDBMultiSearchResponse 表示Multi Search API的响应
type TMDBMultiSearchResponse struct {
Page int `json:"page"`
Results []TMDBMultiSearchResult `json:"results"`
TotalPages int `json:"total_pages"`
TotalResults int `json:"total_results"`
}
// MultiSearch 执行多类型搜索调用TMDB API获取结果
// 参数:
// - query: 搜索关键词
// - page: 页码默认为1
// 返回:
// - *TMDBMultiSearchResponse: 搜索结果
// - error: 错误信息
func MultiSearch(query string, page ...int) (*TMDBMultiSearchResponse, error) {
// URL编码查询参数
encodedQuery := url.QueryEscape(query)
// 处理页码参数
pageNum := 1
if len(page) > 0 && page[0] > 0 {
pageNum = page[0]
}
// 构建URL使用GetTMDBAPIKey()获取API密钥
requestURL := fmt.Sprintf("%s/search/multi?api_key=%s&query=%s&language=zh-CN&page=%d",
BASE_URL, GetTMDBAPIKey(), encodedQuery, pageNum)
// 创建HTTP客户端并设置超时
client := &http.Client{
Timeout: 5 * time.Second,
}
// 发送请求
log.Printf("发送TMDB Multi Search请求: %s", requestURL)
resp, err := client.Get(requestURL)
if err != nil {
return nil, fmt.Errorf("TMDB API请求失败: %w", err)
}
defer resp.Body.Close()
// 检查响应状态码
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("TMDB API返回错误状态码: %d", resp.StatusCode)
}
// 解析响应
var searchResp TMDBMultiSearchResponse
err = json.NewDecoder(resp.Body).Decode(&searchResp)
if err != nil {
return nil, fmt.Errorf("解析TMDB API响应失败: %w", err)
}
// 过滤结果,只保留电影和电视剧
var filteredResults []TMDBMultiSearchResult
for _, result := range searchResp.Results {
if result.MediaType == "movie" || result.MediaType == "tv" {
filteredResults = append(filteredResults, result)
}
}
searchResp.Results = filteredResults
log.Printf("TMDB Multi Search成功找到 %d 个结果,当前页 %d总页数 %d",
len(searchResp.Results), searchResp.Page, searchResp.TotalPages)
return &searchResp, nil
}
// TMDBMediaDetails 表示媒体详情响应
type TMDBMediaDetails struct {
ID int `json:"id"`
Images TMDBMediaImages `json:"images"`
Genres []TMDBGenre `json:"genres"`
// 其他字段根据需要添加
}
// TMDBMediaImages 表示媒体图片集合
type TMDBMediaImages struct {
Backdrops []TMDBImage `json:"backdrops"`
Posters []TMDBImage `json:"posters"`
}
// GetMediaDetails 获取媒体详情
// 参数:
// - mediaType: 媒体类型 (movie 或 tv)
// - mediaID: 媒体ID
// 返回:
// - map[string]interface{}: 详情数据
// - error: 错误信息
func GetMediaDetails(mediaType string, mediaID int) (map[string]interface{}, error) {
// 构建URL使用正确的API接口路径
requestURL := fmt.Sprintf("%s/%s/%d?api_key=%s&append_to_response=images",
BASE_URL, mediaType, mediaID, GetTMDBAPIKey())
// 创建HTTP客户端并设置超时
client := &http.Client{
Timeout: 10 * time.Second,
}
// 发送请求
log.Printf("发送TMDB %s详情请求: %s", mediaType, requestURL)
resp, err := client.Get(requestURL)
if err != nil {
return nil, fmt.Errorf("TMDB API请求失败: %w", err)
}
defer resp.Body.Close()
// 检查响应状态码
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("TMDB API返回错误状态码: %d", resp.StatusCode)
}
// 解析响应
var details TMDBMediaDetails
err = json.NewDecoder(resp.Body).Decode(&details)
if err != nil {
return nil, fmt.Errorf("解析TMDB API响应失败: %w", err)
}
// 处理图片数据
var imageURLs []string
if len(details.Images.Backdrops) > 0 {
// 按照投票数排序
for i := 0; i < len(details.Images.Backdrops); i++ {
for j := i + 1; j < len(details.Images.Backdrops); j++ {
if details.Images.Backdrops[i].VoteCount < details.Images.Backdrops[j].VoteCount {
details.Images.Backdrops[i], details.Images.Backdrops[j] = details.Images.Backdrops[j], details.Images.Backdrops[i]
}
}
}
// 最多取10张图片
count := min(10, len(details.Images.Backdrops))
for i := 0; i < count; i++ {
imageURL := fmt.Sprintf("https://image.tmdb.org/t/p/w1280%s", details.Images.Backdrops[i].FilePath)
imageURLs = append(imageURLs, imageURL)
}
}
// 获取海报URL
posterURL := ""
if len(details.Images.Posters) > 0 {
posterURL = fmt.Sprintf("https://image.tmdb.org/t/p/w500%s", details.Images.Posters[0].FilePath)
}
// 处理类型,转换为中文分类
var genres []string
// 创建中文化的genres数组替换原有的英文genres
var chineseGenres []map[string]interface{}
for _, genre := range details.Genres {
// 对于resource_type字段使用中文名称拼接
if genreName, ok := GENRES[genre.ID]; ok {
genres = append(genres, genreName)
// 创建一个新的genre对象但使用中文名称
chineseGenre := map[string]interface{}{
"id": genre.ID,
"name": genreName,
}
chineseGenres = append(chineseGenres, chineseGenre)
} else {
// 如果在映射表中找不到对应的中文名称,保留原始英文名称
chineseGenres = append(chineseGenres, map[string]interface{}{
"id": genre.ID,
"name": genre.Name,
})
}
}
// 构建返回结果
result := map[string]interface{}{
"id": details.ID,
"genres": chineseGenres, // 使用中文化的genres数组
"images": imageURLs,
"poster_path": posterURL,
"media_type": mediaType,
"resource_type": strings.Join(genres, ","), // 添加中文分类字段
}
// 根据媒体类型添加不同的字段
if mediaType == "movie" {
// 再次发起一个multi_search请求获取中文标题和简介
searchResp, err := MultiSearch(fmt.Sprintf("id:%d", mediaID), 1)
if err == nil && len(searchResp.Results) > 0 {
for _, item := range searchResp.Results {
if item.MediaType == "movie" && item.ID == mediaID {
result["title"] = item.Title
result["original_title"] = item.OriginalTitle
result["overview"] = item.Overview
break
}
}
} else {
// 如果无法通过multi_search获取直接从详情API解析
var movieDetails struct {
Title string `json:"title"`
OriginalTitle string `json:"original_title"`
Overview string `json:"overview"`
}
resp, err := client.Get(requestURL)
if err == nil {
defer resp.Body.Close()
json.NewDecoder(resp.Body).Decode(&movieDetails)
result["title"] = movieDetails.Title
result["original_title"] = movieDetails.OriginalTitle
result["overview"] = movieDetails.Overview
}
}
} else if mediaType == "tv" {
// 再次发起一个multi_search请求获取中文标题和简介
searchResp, err := MultiSearch(fmt.Sprintf("id:%d", mediaID), 1)
if err == nil && len(searchResp.Results) > 0 {
for _, item := range searchResp.Results {
if item.MediaType == "tv" && item.ID == mediaID {
result["name"] = item.Name
result["original_name"] = item.OriginalName
result["overview"] = item.Overview
break
}
}
} else {
// 如果无法通过multi_search获取直接从详情API解析
var tvDetails struct {
Name string `json:"name"`
OriginalName string `json:"original_name"`
Overview string `json:"overview"`
Seasons []interface{} `json:"seasons"`
}
resp, err := client.Get(requestURL)
if err == nil {
defer resp.Body.Close()
json.NewDecoder(resp.Body).Decode(&tvDetails)
result["name"] = tvDetails.Name
result["original_name"] = tvDetails.OriginalName
result["overview"] = tvDetails.Overview
result["seasons"] = tvDetails.Seasons
}
}
}
return result, nil
}
// min 返回两个整数中的较小值
func min(a, b int) int {
if a < b {
return a
}
return b
}