Files
urldb/utils/douban_service.go
2025-09-09 16:27:07 +08:00

320 lines
8.8 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"
"log"
"strconv"
"strings"
"time"
"github.com/go-resty/resty/v2"
)
// top250
// api: https://m.douban.com/rexxar/api/v2/subject_collection/movie_top250/items?start=0&count=10&items_only=1&type_tag=&for_mobile=1
// 最近热门电影 https://movie.douban.com/explore
// api: https://m.douban.com/rexxar/api/v2/subject/recent_hot/movie?start=0&limit=20
// 最近热门剧集 https://movie.douban.com/tv/
// api: https://m.douban.com/rexxar/api/v2/subject/recent_hot/tv?start=20&limit=20
// 最近热门综艺
// api: https://m.douban.com/rexxar/api/v2/subject/recent_hot/tv?limit=50&category=show&type=show
// DoubanService 豆瓣服务
type DoubanService struct {
baseURL string
client *resty.Client
// 电影榜单配置 - 4个大类每个大类下有5个小类
MovieCategories map[string]map[string]map[string]string
// 剧集榜单配置 - 2个大类
TvCategories map[string]map[string]map[string]string
}
// DoubanItem 豆瓣项目
type DoubanItem struct {
ID string `json:"id"`
Title string `json:"title"`
CardSubtitle string `json:"card_subtitle"`
EpisodesInfo string `json:"episodes_info"`
IsNew bool `json:"is_new"`
Pic PicInfo `json:"pic"`
Rating Rating `json:"rating"`
Type string `json:"type"`
URI string `json:"uri"`
Year string `json:"year"`
Directors []string `json:"directors"`
Actors []string `json:"actors"`
Region string `json:"region"`
Genres []string `json:"genres"`
}
// PicInfo 图片信息
type PicInfo struct {
Large string `json:"large"`
Normal string `json:"normal"`
}
// Rating 评分
type Rating struct {
Value float64 `json:"value"`
Count int `json:"count"`
Max int `json:"max"`
StarCount float64 `json:"star_count"`
}
// DoubanCategory 豆瓣分类
type DoubanCategory struct {
Category string `json:"category"`
Selected bool `json:"selected"`
Type string `json:"type"`
Title string `json:"title"`
}
// DoubanResponse 豆瓣响应
type DoubanResponse struct {
Items []DoubanItem `json:"items"`
Categories []DoubanCategory `json:"categories"`
Total int `json:"total"`
IsMockData bool `json:"is_mock_data,omitempty"`
MockReason string `json:"mock_reason,omitempty"`
Notice string `json:"notice,omitempty"`
}
// DoubanResult 豆瓣结果
type DoubanResult struct {
Success bool `json:"success"`
Data *DoubanResponse `json:"data,omitempty"`
Message string `json:"message,omitempty"`
}
// NewDoubanService 创建新的豆瓣服务
func NewDoubanService() *DoubanService {
client := resty.New()
client.SetTimeout(30 * time.Second)
client.SetHeaders(map[string]string{
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1",
"Referer": "https://m.douban.com/",
"Accept": "application/json, text/plain, */*",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Accept-Encoding": "gzip, deflate",
"Connection": "keep-alive",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
"Cache-Control": "no-cache",
"Pragma": "no-cache",
"X-Requested-With": "XMLHttpRequest",
"Origin": "https://m.douban.com",
})
// 启用自动解压缩
client.SetDisableWarn(true)
client.SetRetryCount(3)
client.SetRetryWaitTime(1 * time.Second)
client.SetRetryMaxWaitTime(5 * time.Second)
return &DoubanService{
baseURL: "https://m.douban.com/rexxar/api/v2",
client: client,
}
}
// GetRecentHotMovies fetches recent hot movies
func (ds *DoubanService) GetRecentHotMovies() ([]DoubanItem, error) {
url := "https://m.douban.com/rexxar/api/v2/subject/recent_hot/movie"
params := map[string]string{
"start": "0",
"limit": "20",
}
items := []DoubanItem{}
for {
pageItems, total, err := ds.fetchPage(url, params)
if err != nil {
return nil, err
}
items = append(items, pageItems...)
if len(items) >= total {
break
}
start := len(items)
params["start"] = strconv.Itoa(start)
}
return items, nil
}
// GetRecentHotTVs fetches recent hot TV shows
func (ds *DoubanService) GetRecentHotTVs() ([]DoubanItem, error) {
url := "https://m.douban.com/rexxar/api/v2/subject/recent_hot/tv"
params := map[string]string{
"start": "0",
"limit": "300",
}
items := []DoubanItem{}
for {
pageItems, total, err := ds.fetchPage(url, params)
if err != nil {
return nil, err
}
items = append(items, pageItems...)
if len(items) >= total {
break
}
start := len(items)
params["start"] = strconv.Itoa(start)
}
return items, nil
}
// GetRecentHotShows fetches recent hot shows
func (ds *DoubanService) GetRecentHotShows() ([]DoubanItem, error) {
url := "https://m.douban.com/rexxar/api/v2/subject/recent_hot/tv"
params := map[string]string{
"limit": "300",
"category": "show",
"type": "show",
"start": "0",
}
items := []DoubanItem{}
for {
pageItems, total, err := ds.fetchPage(url, params)
if err != nil {
return nil, err
}
items = append(items, pageItems...)
if len(items) >= total {
break
}
start := len(items)
params["start"] = strconv.Itoa(start)
}
return items, nil
}
// GetTop250Movies fetches top 250 movies
func (ds *DoubanService) GetTop250Movies() ([]DoubanItem, error) {
url := "https://m.douban.com/rexxar/api/v2/subject_collection/movie_top250/items"
params := map[string]string{
"start": "0",
"count": "250",
"items_only": "1",
"type_tag": "",
"for_mobile": "1",
}
items, _, err := ds.fetchPage(url, params)
return items, err
}
// fetchPage fetches a page of items from a given URL and parameters
func (ds *DoubanService) fetchPage(url string, params map[string]string) ([]DoubanItem, int, error) {
var response *resty.Response
var err error
response, err = ds.client.R().
SetQueryParams(params).
Get(url)
if err != nil {
return nil, 0, err
}
var apiResponse map[string]interface{}
if err := json.Unmarshal(response.Body(), &apiResponse); err != nil {
return nil, 0, err
}
items := ds.extractItems(apiResponse)
total := ds.extractTotal(apiResponse)
return items, total, nil
}
// extractTotal extracts the total number of items from the API response
func (ds *DoubanService) extractTotal(response map[string]interface{}) int {
if totalData, ok := response["total"]; ok {
if totalFloat, ok := totalData.(float64); ok {
return int(totalFloat)
}
}
return 0
}
// extractItems 从API响应中提取项目列表
func (ds *DoubanService) extractItems(response map[string]interface{}) []DoubanItem {
var items []DoubanItem
// 根据实际接口返回格式,数据在 subject_collection_items 字段中
if itemsData, ok := response["subject_collection_items"]; ok {
if itemsBytes, err := json.Marshal(itemsData); err == nil {
if err := json.Unmarshal(itemsBytes, &items); err != nil {
log.Printf("解析subject_collection_items字段失败: %v", err)
}
}
} else if itemsData, ok := response["items"]; ok {
// 兼容旧的items字段
if itemsBytes, err := json.Marshal(itemsData); err == nil {
if err := json.Unmarshal(itemsBytes, &items); err != nil {
log.Printf("解析items字段失败: %v", err)
}
}
} else if subjectsData, ok := response["subjects"]; ok {
// 兼容subjects字段
if subjectsBytes, err := json.Marshal(subjectsData); err == nil {
if err := json.Unmarshal(subjectsBytes, &items); err != nil {
log.Printf("解析subjects字段失败: %v", err)
}
}
}
log.Printf("从API响应中提取到 %d 个项目", len(items))
// 解析每个项目的card_subtitle提取年份、地区、类型、导演、演员信息
for i := range items {
ds.parseCardSubtitle(&items[i])
}
return items
}
// parseCardSubtitle 解析card_subtitle字段
func (ds *DoubanService) parseCardSubtitle(item *DoubanItem) {
if item.CardSubtitle == "" {
return
}
// card_subtitle格式: "2025 / 中国大陆 / 剧情 爱情 / 丁梓光 / 杨紫 李现"
parts := strings.Split(item.CardSubtitle, " / ")
if len(parts) >= 4 {
// 年份
if len(parts) > 0 {
item.Year = strings.TrimSpace(parts[0])
}
// 地区
if len(parts) > 1 {
item.Region = strings.TrimSpace(parts[1])
}
// 类型(可能有多个,用空格分隔)
if len(parts) > 2 {
genresStr := strings.TrimSpace(parts[2])
item.Genres = strings.Fields(genresStr)
}
// 导演(可能有多个,用空格分隔)
if len(parts) > 3 {
directorsStr := strings.TrimSpace(parts[3])
item.Directors = strings.Fields(directorsStr)
}
// 演员(可能有多个,用空格分隔)
if len(parts) > 4 {
actorsStr := strings.TrimSpace(parts[4])
item.Actors = strings.Fields(actorsStr)
}
}
}