From cafe2ce4060171d0125eb13667c42b953ceeaa32 Mon Sep 17 00:00:00 2001 From: Kerwin Date: Tue, 9 Sep 2025 16:27:07 +0800 Subject: [PATCH] =?UTF-8?q?update=EF=BC=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db/converter/hot_drama_converter.go | 2 + db/dto/hot_drama.go | 2 + db/entity/hot_drama.go | 1 + db/repo/hot_drama_repository.go | 26 ++- handlers/hot_drama_handler.go | 94 ++++++++- main.go | 1 + scheduler/hot_drama.go | 223 +++++++++++--------- utils/douban_service.go | 309 +++++++++------------------- web/composables/useApi.ts | 8 +- web/pages/hot-dramas.vue | 300 +++++++++++++++++---------- 10 files changed, 548 insertions(+), 418 deletions(-) diff --git a/db/converter/hot_drama_converter.go b/db/converter/hot_drama_converter.go index f31e218..aae60e4 100644 --- a/db/converter/hot_drama_converter.go +++ b/db/converter/hot_drama_converter.go @@ -29,6 +29,7 @@ func HotDramaToResponse(drama *entity.HotDrama) *dto.HotDramaResponse { PosterURL: drama.PosterURL, Category: drama.Category, SubType: drama.SubType, + Rank: drama.Rank, Source: drama.Source, DoubanID: drama.DoubanID, DoubanURI: drama.DoubanURI, @@ -49,6 +50,7 @@ func RequestToHotDrama(req *dto.HotDramaRequest) *entity.HotDrama { Actors: req.Actors, Category: req.Category, SubType: req.SubType, + Rank: req.Rank, Source: req.Source, DoubanID: req.DoubanID, } diff --git a/db/dto/hot_drama.go b/db/dto/hot_drama.go index 14dc417..2686950 100644 --- a/db/dto/hot_drama.go +++ b/db/dto/hot_drama.go @@ -16,6 +16,7 @@ type HotDramaRequest struct { PosterURL string `json:"poster_url"` Category string `json:"category"` SubType string `json:"sub_type"` + Rank int `json:"rank"` Source string `json:"source"` DoubanID string `json:"douban_id"` DoubanURI string `json:"douban_uri"` @@ -41,6 +42,7 @@ type HotDramaResponse struct { PosterURL string `json:"poster_url"` Category string `json:"category"` SubType string `json:"sub_type"` + Rank int `json:"rank"` Source string `json:"source"` DoubanID string `json:"douban_id"` DoubanURI string `json:"douban_uri"` diff --git a/db/entity/hot_drama.go b/db/entity/hot_drama.go index cda1547..d492801 100644 --- a/db/entity/hot_drama.go +++ b/db/entity/hot_drama.go @@ -27,6 +27,7 @@ type HotDrama struct { // 分类信息 Category string `json:"category" gorm:"size:50"` // 分类(电影/电视剧) SubType string `json:"sub_type" gorm:"size:50"` // 子类型(华语/欧美/韩国/日本等) + Rank int `json:"rank" gorm:"default:0"` // 排序(豆瓣返回顺序) // 数据来源 Source string `json:"source" gorm:"size:50;default:'douban'"` // 数据来源 diff --git a/db/repo/hot_drama_repository.go b/db/repo/hot_drama_repository.go index bf3fcca..f491e89 100644 --- a/db/repo/hot_drama_repository.go +++ b/db/repo/hot_drama_repository.go @@ -12,6 +12,7 @@ type HotDramaRepository interface { FindByID(id uint) (*entity.HotDrama, error) FindAll(page, pageSize int) ([]entity.HotDrama, int64, error) FindByCategory(category string, page, pageSize int) ([]entity.HotDrama, int64, error) + FindByCategoryAndSubType(category, subType string, page, pageSize int) ([]entity.HotDrama, int64, error) FindByDoubanID(doubanID string) (*entity.HotDrama, error) Upsert(drama *entity.HotDrama) error Delete(id uint) error @@ -59,7 +60,7 @@ func (r *hotDramaRepository) FindAll(page, pageSize int) ([]entity.HotDrama, int } // 获取分页数据 - err := r.db.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&dramas).Error + err := r.db.Order("rank ASC").Offset(offset).Limit(pageSize).Find(&dramas).Error if err != nil { return nil, 0, err } @@ -80,7 +81,28 @@ func (r *hotDramaRepository) FindByCategory(category string, page, pageSize int) } // 获取分页数据 - err := r.db.Where("category = ?", category).Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&dramas).Error + err := r.db.Where("category = ?", category).Order("rank ASC").Offset(offset).Limit(pageSize).Find(&dramas).Error + if err != nil { + return nil, 0, err + } + + return dramas, total, nil +} + +// FindByCategoryAndSubType 根据分类和子类型查找热播剧(分页) +func (r *hotDramaRepository) FindByCategoryAndSubType(category, subType string, page, pageSize int) ([]entity.HotDrama, int64, error) { + var dramas []entity.HotDrama + var total int64 + + offset := (page - 1) * pageSize + + // 获取总数 + if err := r.db.Model(&entity.HotDrama{}).Where("category = ? AND sub_type = ?", category, subType).Count(&total).Error; err != nil { + return nil, 0, err + } + + // 获取分页数据 + err := r.db.Where("category = ? AND sub_type = ?", category, subType).Order("rank ASC").Offset(offset).Limit(pageSize).Find(&dramas).Error if err != nil { return nil, 0, err } diff --git a/handlers/hot_drama_handler.go b/handlers/hot_drama_handler.go index 8eacca4..7cc5663 100644 --- a/handlers/hot_drama_handler.go +++ b/handlers/hot_drama_handler.go @@ -1,13 +1,17 @@ package handlers import ( + "fmt" "net/http" "strconv" + "strings" + "time" "github.com/ctwj/urldb/db/converter" "github.com/ctwj/urldb/db/dto" "github.com/ctwj/urldb/db/entity" "github.com/ctwj/urldb/db/repo" + "github.com/go-resty/resty/v2" "github.com/gin-gonic/gin" ) @@ -94,6 +98,87 @@ func CreateHotDrama(c *gin.Context) { SuccessResponse(c, response) } +// GetPosterImage 获取海报图片代理 +func GetPosterImage(c *gin.Context) { + url := c.Query("url") + if url == "" { + ErrorResponse(c, "图片URL不能为空", http.StatusBadRequest) + return + } + + // 简单的URL验证 + if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") { + ErrorResponse(c, "无效的图片URL", http.StatusBadRequest) + return + } + + // 检查If-Modified-Since头,实现条件请求 + ifModifiedSince := c.GetHeader("If-Modified-Since") + if ifModifiedSince != "" { + // 如果存在,说明浏览器有缓存,检查是否过期 + ifLastModified, err := time.Parse("Mon, 02 Jan 2006 15:04:05 GMT", ifModifiedSince) + if err == nil && time.Since(ifLastModified) < 86400*time.Second { // 24小时内 + c.Status(http.StatusNotModified) + return + } + } + + // 检查ETag头 - 基于URL生成,保证相同URL有相同ETag + ifNoneMatch := c.GetHeader("If-None-Match") + if ifNoneMatch != "" { + etag := fmt.Sprintf(`"%x"`, len(url)) // 简单的基于URL长度的ETag + if ifNoneMatch == etag { + c.Status(http.StatusNotModified) + return + } + } + + client := resty.New(). + SetTimeout(30 * time.Second). + SetRetryCount(2). + SetRetryWaitTime(1 * time.Second) + + resp, err := client.R(). + SetHeaders(map[string]string{ + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", + "Referer": "https://m.douban.com/", + "Accept": "image/webp,image/apng,image/*,*/*;q=0.8", + "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", + }). + Get(url) + + if err != nil { + ErrorResponse(c, "获取图片失败: "+err.Error(), http.StatusInternalServerError) + return + } + + if resp.StatusCode() != 200 { + ErrorResponse(c, fmt.Sprintf("获取图片失败,状态码: %d", resp.StatusCode()), http.StatusInternalServerError) + return + } + + // 设置响应头 + contentType := resp.Header().Get("Content-Type") + if contentType == "" { + contentType = "image/jpeg" + } + c.Header("Content-Type", contentType) + + // 增强缓存策略 + c.Header("Cache-Control", "public, max-age=604800, s-maxage=86400") // 客户端7天,代理1天 + c.Header("Access-Control-Allow-Origin", "*") + c.Header("Access-Control-Allow-Methods", "GET, OPTIONS") + c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization") + + // 设置缓存验证头(基于URL长度生成的简单ETag) + etag := fmt.Sprintf(`"%x"`, len(url)) + c.Header("ETag", etag) + c.Header("Last-Modified", time.Now().Add(-86400*time.Second).Format("Mon, 02 Jan 2006 15:04:05 GMT")) // 设为1天前,避免立即过期 + + // 返回图片数据 + c.Data(resp.StatusCode(), contentType, resp.Body()) +} + // UpdateHotDrama 更新热播剧记录 func UpdateHotDrama(c *gin.Context) { idStr := c.Param("id") @@ -149,6 +234,7 @@ func GetHotDramaList(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) category := c.Query("category") + subType := c.Query("sub_type") var dramas []entity.HotDrama var total int64 @@ -156,13 +242,17 @@ func GetHotDramaList(c *gin.Context) { // 如果page_size很大(比如>=1000),则获取所有数据 if pageSize >= 1000 { - if category != "" { + if category != "" && subType != "" { + dramas, total, err = repoManager.HotDramaRepository.FindByCategoryAndSubType(category, subType, 1, 10000) + } else if category != "" { dramas, total, err = repoManager.HotDramaRepository.FindByCategory(category, 1, 10000) } else { dramas, total, err = repoManager.HotDramaRepository.FindAll(1, 10000) } } else { - if category != "" { + if category != "" && subType != "" { + dramas, total, err = repoManager.HotDramaRepository.FindByCategoryAndSubType(category, subType, page, pageSize) + } else if category != "" { dramas, total, err = repoManager.HotDramaRepository.FindByCategory(category, page, pageSize) } else { dramas, total, err = repoManager.HotDramaRepository.FindAll(page, pageSize) diff --git a/main.go b/main.go index afd1aab..970e64e 100644 --- a/main.go +++ b/main.go @@ -281,6 +281,7 @@ func main() { api.POST("/hot-dramas", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.CreateHotDrama) api.PUT("/hot-dramas/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.UpdateHotDrama) api.DELETE("/hot-dramas/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.DeleteHotDrama) + api.GET("/hot-dramas/poster", handlers.GetPosterImage) // 任务管理路由 api.POST("/tasks/transfer", middleware.AuthMiddleware(), middleware.AdminMiddleware(), taskHandler.CreateBatchTransferTask) diff --git a/scheduler/hot_drama.go b/scheduler/hot_drama.go index 0ff09c5..b5c3bd8 100644 --- a/scheduler/hot_drama.go +++ b/scheduler/hot_drama.go @@ -90,13 +90,26 @@ func (h *HotDramaScheduler) processHotDramaNames(dramaNames []string) { // 收集所有数据 var allDramas []*entity.HotDrama - // 获取电影数据 - movieDramas := h.processMovieData() - allDramas = append(allDramas, movieDramas...) + // 获取最近热门电影数据 + recentMovieDramas := h.processRecentMovies() + allDramas = append(allDramas, recentMovieDramas...) - // 获取电视剧数据 - tvDramas := h.processTvData() - allDramas = append(allDramas, tvDramas...) + // 获取最近热门剧集数据 + recentTVDramas := h.processRecentTVs() + allDramas = append(allDramas, recentTVDramas...) + + // 获取最近热门综艺数据 + recentShowDramas := h.processRecentShows() + allDramas = append(allDramas, recentShowDramas...) + + // 获取豆瓣电影Top250数据 + top250Dramas := h.processTop250Movies() + allDramas = append(allDramas, top250Dramas...) + + // 设置排名顺序(保持豆瓣返回的顺序) + for i, drama := range allDramas { + drama.Rank = i + } // 清空数据库 utils.Info("准备清空数据库,当前共有 %d 条数据", len(allDramas)) @@ -121,111 +134,127 @@ func (h *HotDramaScheduler) processHotDramaNames(dramaNames []string) { utils.Info("热播剧数据处理完成") } -// processMovieData 处理电影数据 -func (h *HotDramaScheduler) processMovieData() []*entity.HotDrama { - utils.Info("开始处理电影数据...") +// processRecentMovies 处理最近热门电影数据 +func (h *HotDramaScheduler) processRecentMovies() []*entity.HotDrama { + utils.Info("开始处理最近热门电影数据...") - var movieDramas []*entity.HotDrama + var recentMovies []*entity.HotDrama - // 使用GetTypePage方法获取电影数据 - movieResult, err := h.doubanService.GetTypePage("movie_top250", "全部") + items, err := h.doubanService.GetRecentHotMovies() if err != nil { - utils.Error(fmt.Sprintf("获取电影榜单失败: %v", err)) - return movieDramas + utils.Error(fmt.Sprintf("获取最近热门电影失败: %v", err)) + return recentMovies } - if movieResult.Success && movieResult.Data != nil { - utils.Info("电影获取到 %d 个数据", len(movieResult.Data.Items)) + utils.Info("最近热门电影获取到 %d 个数据", len(items)) - for _, item := range movieResult.Data.Items { - drama := &entity.HotDrama{ - Title: item.Title, - CardSubtitle: item.CardSubtitle, - EpisodesInfo: item.EpisodesInfo, - IsNew: item.IsNew, - Rating: item.Rating.Value, - RatingCount: item.Rating.Count, - Year: item.Year, - Region: item.Region, - Genres: strings.Join(item.Genres, ", "), - Directors: strings.Join(item.Directors, ", "), - Actors: strings.Join(item.Actors, ", "), - PosterURL: item.Pic.Normal, - Category: "电影", - SubType: "热门", - Source: "douban", - DoubanID: item.ID, - DoubanURI: item.URI, - } - - movieDramas = append(movieDramas, drama) - utils.Info("收集电影: %s (评分: %.1f, 年份: %s, 地区: %s)", - item.Title, item.Rating.Value, item.Year, item.Region) - } - } else { - utils.Warn("电影获取数据失败或为空") + for _, item := range items { + drama := h.convertDoubanItemToHotDrama(item, "电影", "热门") + recentMovies = append(recentMovies, drama) + utils.Info("收集最近热门电影: %s (评分: %.1f, 年份: %s, 地区: %s)", + item.Title, item.Rating.Value, item.Year, item.Region) } - utils.Info("电影数据处理完成,共收集 %d 条数据", len(movieDramas)) - return movieDramas + utils.Info("最近热门电影数据处理完成,共收集 %d 条数据", len(recentMovies)) + return recentMovies } -// processTvData 处理电视剧数据 -func (h *HotDramaScheduler) processTvData() []*entity.HotDrama { - utils.Info("开始处理电视剧数据...") +// processRecentTVs 处理最近热门剧集数据 +func (h *HotDramaScheduler) processRecentTVs() []*entity.HotDrama { + utils.Info("开始处理最近热门剧集数据...") - var tvDramas []*entity.HotDrama + var recentTVs []*entity.HotDrama - // 获取所有tv类型 - tvTypes := h.doubanService.GetAllTvTypes() - utils.Info("获取到 %d 个tv类型: %v", len(tvTypes), tvTypes) - - // 遍历每个type,分别请求数据 - for _, tvType := range tvTypes { - utils.Info("正在处理tv类型: %s", tvType) - - // 使用GetTypePage方法请求数据 - tvResult, err := h.doubanService.GetTypePage("tv", tvType) - if err != nil { - utils.Error(fmt.Sprintf("获取tv类型 %s 数据失败: %v", tvType, err)) - continue - } - - if tvResult.Success && tvResult.Data != nil { - utils.Info("tv类型 %s 获取到 %d 个数据", tvType, len(tvResult.Data.Items)) - - for _, item := range tvResult.Data.Items { - drama := &entity.HotDrama{ - Title: item.Title, - CardSubtitle: item.CardSubtitle, - EpisodesInfo: item.EpisodesInfo, - IsNew: item.IsNew, - Rating: item.Rating.Value, - RatingCount: item.Rating.Count, - Year: item.Year, - Region: item.Region, - Genres: strings.Join(item.Genres, ", "), - Directors: strings.Join(item.Directors, ", "), - Actors: strings.Join(item.Actors, ", "), - PosterURL: item.Pic.Normal, - Category: "电视剧", - SubType: tvType, // 使用具体的tv类型 - Source: "douban", - DoubanID: item.ID, - DoubanURI: item.URI, - } - - tvDramas = append(tvDramas, drama) - utils.Info("收集tv类型 %s: %s (评分: %.1f, 年份: %s, 地区: %s)", - tvType, item.Title, item.Rating.Value, item.Year, item.Region) - } - } else { - utils.Warn("tv类型 %s 获取数据失败或为空", tvType) - } + items, err := h.doubanService.GetRecentHotTVs() + if err != nil { + utils.Error(fmt.Sprintf("获取最近热门剧集失败: %v", err)) + return recentTVs } - utils.Info("电视剧数据处理完成,共收集 %d 条数据", len(tvDramas)) - return tvDramas + utils.Info("最近热门剧集获取到 %d 个数据", len(items)) + + for _, item := range items { + drama := h.convertDoubanItemToHotDrama(item, "电视剧", "热门") + recentTVs = append(recentTVs, drama) + utils.Info("收集最近热门剧集: %s (评分: %.1f, 年份: %s, 地区: %s)", + item.Title, item.Rating.Value, item.Year, item.Region) + } + + utils.Info("最近热门剧集数据处理完成,共收集 %d 条数据", len(recentTVs)) + return recentTVs +} + +// processRecentShows 处理最近热门综艺数据 +func (h *HotDramaScheduler) processRecentShows() []*entity.HotDrama { + utils.Info("开始处理最近热门综艺数据...") + + var recentShows []*entity.HotDrama + + items, err := h.doubanService.GetRecentHotShows() + if err != nil { + utils.Error(fmt.Sprintf("获取最近热门综艺失败: %v", err)) + return recentShows + } + + utils.Info("最近热门综艺获取到 %d 个数据", len(items)) + + for _, item := range items { + drama := h.convertDoubanItemToHotDrama(item, "综艺", "热门") + recentShows = append(recentShows, drama) + utils.Info("收集最近热门综艺: %s (评分: %.1f, 年份: %s, 地区: %s)", + item.Title, item.Rating.Value, item.Year, item.Region) + } + + utils.Info("最近热门综艺数据处理完成,共收集 %d 条数据", len(recentShows)) + return recentShows +} + +// processTop250Movies 处理豆瓣电影Top250数据 +func (h *HotDramaScheduler) processTop250Movies() []*entity.HotDrama { + utils.Info("开始处理豆瓣电影Top250数据...") + + var top250Movies []*entity.HotDrama + + items, err := h.doubanService.GetTop250Movies() + if err != nil { + utils.Error(fmt.Sprintf("获取豆瓣电影Top250失败: %v", err)) + return top250Movies + } + + utils.Info("豆瓣电影Top250获取到 %d 个数据", len(items)) + + for _, item := range items { + drama := h.convertDoubanItemToHotDrama(item, "电影", "Top250") + top250Movies = append(top250Movies, drama) + utils.Info("收集豆瓣Top250电影: %s (评分: %.1f, 年份: %s, 地区: %s)", + item.Title, item.Rating.Value, item.Year, item.Region) + } + + utils.Info("豆瓣电影Top250数据处理完成,共收集 %d 条数据", len(top250Movies)) + return top250Movies +} + +// convertDoubanItemToHotDrama 转换DoubanItem为HotDrama实体 +func (h *HotDramaScheduler) convertDoubanItemToHotDrama(item utils.DoubanItem, category, subType string) *entity.HotDrama { + return &entity.HotDrama{ + Title: item.Title, + CardSubtitle: item.CardSubtitle, + EpisodesInfo: item.EpisodesInfo, + IsNew: item.IsNew, + Rating: item.Rating.Value, + RatingCount: item.Rating.Count, + Year: item.Year, + Region: item.Region, + Genres: strings.Join(item.Genres, ", "), + Directors: strings.Join(item.Directors, ", "), + Actors: strings.Join(item.Actors, ", "), + PosterURL: item.Pic.Normal, + Category: category, + SubType: subType, + Source: "douban", + DoubanID: item.ID, + DoubanURI: item.URI, + } } // GetHotDramaNames 获取热播剧名称列表(公共方法) diff --git a/utils/douban_service.go b/utils/douban_service.go index 21b03e0..49e4e36 100644 --- a/utils/douban_service.go +++ b/utils/douban_service.go @@ -3,12 +3,16 @@ 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 @@ -113,229 +117,129 @@ func NewDoubanService() *DoubanService { client.SetRetryWaitTime(1 * time.Second) client.SetRetryMaxWaitTime(5 * time.Second) - // 初始化剧集榜单配置 - tvCategories := map[string]map[string]map[string]string{ - "最近热门剧集": { - // "国产剧": {"category": "tv", "type": "tv_domestic"}, - "欧美剧": {"category": "tv", "type": "tv_american"}, - "日剧": {"category": "tv", "type": "tv_japanese"}, - "韩剧": {"category": "tv", "type": "tv_korean"}, - "动画": {"category": "tv", "type": "tv_animation"}, - "纪录片": {"category": "tv", "type": "tv_documentary"}, - }, - "最近热门综艺": { - // "综合": {"category": "show", "type": "show"}, - "国内": {"category": "show", "type": "show_domestic"}, - "国外": {"category": "show", "type": "show_foreign"}, - }, - } - return &DoubanService{ - baseURL: "https://m.douban.com/rexxar/api/v2", - client: client, - TvCategories: tvCategories, + baseURL: "https://m.douban.com/rexxar/api/v2", + client: client, } } -// GetTypePage 获取指定类型的数据 -func (ds *DoubanService) GetTypePage(category, rankingType string) (*DoubanResult, error) { - // 构建请求参数 +// 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": "50", - "os": "window", - "_": "0", - "loc_id": "108288", + "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 +} - Debug("请求参数: %+v", params) - Debug("请求URL: %s/subject_collection/%s/items", ds.baseURL, rankingType) +// 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 - // 尝试调用豆瓣API - Debug("开始发送HTTP请求...") response, err = ds.client.R(). SetQueryParams(params). - Get(ds.baseURL + "/subject_collection/" + rankingType + "/items") + Get(url) if err != nil { - Error("=== 豆瓣API调用失败 ===") - Error("错误详情: %v", err) - return &DoubanResult{ - Success: false, - Message: "API调用失败: " + err.Error(), - }, nil + return nil, 0, err } - Debug("=== HTTP请求成功 ===") - Debug("响应状态码: %d", response.StatusCode()) - Debug("响应体长度: %d bytes", len(response.Body())) - - // 记录响应体的前500个字符用于调试 - responseBody := string(response.Body()) - Debug("响应体原始长度: %d 字符", len(responseBody)) - - if len(responseBody) > 500 { - Debug("响应体前500字符: %s...", responseBody[:500]) - } else { - Debug("完整响应体: %s", responseBody) - } - - // 检查响应体是否包含有效JSON - if len(responseBody) == 0 { - Warn("=== 响应体为空 ===") - return &DoubanResult{ - Success: false, - Message: "响应体为空", - }, nil - } - - // 尝试解析JSON var apiResponse map[string]interface{} if err := json.Unmarshal(response.Body(), &apiResponse); err != nil { - Error("=== 解析API响应失败 ===") - Error("JSON解析错误: %v", err) - Debug("响应体内容: %s", string(response.Body())) - - // 尝试检查是否是HTML错误页面 - if len(responseBody) > 100 && (strings.Contains(responseBody, "") || strings.Contains(responseBody, " limit { - log.Printf("限制返回数量从 %d 到 %d", len(items), limit) - items = items[:limit] - } - - // 获取总数,优先使用API返回的total字段 - total := len(items) - if totalData, ok := apiResponse["total"]; ok { +// 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 { - total = int(totalFloat) + return int(totalFloat) } } - - result := &DoubanResponse{ - Items: items, - Total: total, - Categories: categories, - IsMockData: false, - MockReason: "", - } - - log.Printf("=== 数据获取完成 ===") - log.Printf("最终返回数据数量: %d", len(items)) - - return &DoubanResult{ - Success: true, - Data: result, - }, nil -} - -// GetTvByType 获取指定type的全部剧集数据 -func (ds *DoubanService) GetTvByType(tvType string) ([]map[string]interface{}, error) { - url := ds.baseURL + "/subject_collection/" + tvType + "/items" - params := map[string]string{ - "start": "0", - "limit": "1000", // 假设不会超过1000条 - } - - resp, err := ds.client.R(). - SetQueryParams(params). - Get(url) - if err != nil { - return nil, err - } - - var result map[string]interface{} - if err := json.Unmarshal(resp.Body(), &result); err != nil { - return nil, err - } - - items, ok := result["subject_collection_items"].([]interface{}) - if !ok { - return nil, nil // 没有数据 - } - - // 转换为[]map[string]interface{} - var out []map[string]interface{} - for _, item := range items { - if m, ok := item.(map[string]interface{}); ok { - out = append(out, m) - } - } - return out, nil -} - -// GetAllTvTypes 获取所有tv类型(type列表) -func (ds *DoubanService) GetAllTvTypes() []string { - types := []string{} - for _, sub := range ds.TvCategories { - for _, v := range sub { - if t, ok := v["type"]; ok { - types = append(types, t) - } - } - } - return types + return 0 } // extractItems 从API响应中提取项目列表 @@ -413,16 +317,3 @@ func (ds *DoubanService) parseCardSubtitle(item *DoubanItem) { } } } - -// extractCategories 从API响应中提取分类列表 -func (ds *DoubanService) extractCategories(response map[string]interface{}) []DoubanCategory { - var categories []DoubanCategory - - if categoriesData, ok := response["categories"]; ok { - if categoriesBytes, err := json.Marshal(categoriesData); err == nil { - json.Unmarshal(categoriesBytes, &categories) - } - } - - return categories -} diff --git a/web/composables/useApi.ts b/web/composables/useApi.ts index dccf73a..10d4abe 100644 --- a/web/composables/useApi.ts +++ b/web/composables/useApi.ts @@ -181,7 +181,13 @@ export const useHotDramaApi = () => { const updateHotDrama = (id: number, data: any) => useApiFetch(`/hot-dramas/${id}`, { method: 'PUT', body: data }).then(parseApiResponse) const deleteHotDrama = (id: number) => useApiFetch(`/hot-dramas/${id}`, { method: 'DELETE' }).then(parseApiResponse) const fetchHotDramas = () => useApiFetch('/hot-dramas/fetch', { method: 'POST' }).then(parseApiResponse) - return { getHotDramas, createHotDrama, updateHotDrama, deleteHotDrama, fetchHotDramas } + + const getPosterUrl = (posterUrl: string): string => { + if (!posterUrl) return '' + return `/api/hot-dramas/poster?url=${encodeURIComponent(posterUrl)}` + } + + return { getHotDramas, createHotDrama, updateHotDrama, deleteHotDrama, fetchHotDramas, getPosterUrl } } export const useMonitorApi = () => { diff --git a/web/pages/hot-dramas.vue b/web/pages/hot-dramas.vue index 0ea049c..783175f 100644 --- a/web/pages/hot-dramas.vue +++ b/web/pages/hot-dramas.vue @@ -30,54 +30,6 @@ - -
-
-
-
- -
-
-

总数量

-

{{ total }}

-
-
-
-
-
-
- -
-
-

电影

-

{{ movieCount }}

-
-
-
-
-
-
- -
-
-

电视剧

-

{{ tvCount }}

-
-
-
-
-
-
- -
-
-

平均评分

-

{{ averageRating }}

-
-
-
-
-