package playletser import ( "fmt" "net/url" "strconv" "strings" "videoweb/app/service/upstream" "videoweb/pub/common/log" "github.com/gin-gonic/gin" ) const ( chapterDetailPath = "/api/app/playlet/chapter/detail" chapterListPath = "/api/app/playlet/chapter/list" ) // PlayQuery 播放页参数。 type PlayQuery struct { PlayletID int64 PlayletSlug string ChapterID int64 } // ParsePlayQuery 解析播放页的路径参数。 func ParsePlayQuery(playletKey, chapterIDRaw string) PlayQuery { q := PlayQuery{PlayletSlug: strings.TrimSpace(playletKey)} if n, err := strconv.ParseInt(strings.TrimSpace(playletKey), 10, 64); err == nil && n > 0 { q.PlayletID = n q.PlayletSlug = "" } if n, err := strconv.ParseInt(strings.TrimSpace(chapterIDRaw), 10, 64); err == nil && n > 0 { q.ChapterID = n } return q } // FetchChapterDetail 查询剧集播放详情。 func FetchChapterDetail(c *gin.Context, chapterID, playletID int64, playletSlug string) (*PlayletChapterInfoResp, error) { if chapterID <= 0 { return nil, fmt.Errorf("chapter detail invalid chapterId") } params := url.Values{} params.Set("chapterId", strconv.FormatInt(chapterID, 10)) if playletSlug = strings.TrimSpace(playletSlug); playletSlug != "" { params.Set("playletSlug", playletSlug) } else if playletID > 0 { params.Set("playletId", strconv.FormatInt(playletID, 10)) } var env APIEnvelope[PlayletChapterInfoResp] if err := upstream.GetJSON(c, chapterDetailPath+"?"+params.Encode(), &env); err != nil { return nil, err } if env.Code != 200 { return nil, fmt.Errorf("chapter detail code=%d msg=%s tip=%s", env.Code, env.Msg, env.Tip) } env.Data.SeoInfo.Normalize() return &env.Data, nil } // FetchChapterDetailOrNil 失败返回 nil。 func FetchChapterDetailOrNil(c *gin.Context, chapterID, playletID int64, playletSlug string) *PlayletChapterInfoResp { data, err := FetchChapterDetail(c, chapterID, playletID, playletSlug) if err != nil { log.Error("fetch chapter detail failed", log.E(err)) return nil } if data == nil || data.ID <= 0 { return nil } return data } // FetchChapterList 根据短剧 slug 或 ID 查询选集列表,slug 优先。 func FetchChapterList(c *gin.Context, playletID int64, playletSlug string) (*PlayletChapterListResp, error) { params := url.Values{} if playletSlug = strings.TrimSpace(playletSlug); playletSlug != "" { params.Set("playletSlug", playletSlug) } else if playletID > 0 { params.Set("playletId", strconv.FormatInt(playletID, 10)) } else { return nil, fmt.Errorf("chapter list missing playletSlug or playletId") } var env APIEnvelope[PlayletChapterListResp] if err := upstream.GetJSON(c, chapterListPath+"?"+params.Encode(), &env); err != nil { return nil, err } if env.Code != 200 { return nil, fmt.Errorf("playlet chapter list code=%d msg=%s tip=%s", env.Code, env.Msg, env.Tip) } return &env.Data, nil } // ResolvePlayChapterID 无 chapterId 时取第 1 集(currentEpisode=1),否则取列表首条。 func ResolvePlayChapterID(playlet *PlayletInfoResp, chapterID int64) int64 { if chapterID > 0 { return chapterID } if playlet == nil { return 0 } var firstID int64 for _, ch := range playlet.PlayletChapterInfo { if ch.ID <= 0 { continue } if firstID == 0 { firstID = ch.ID } if ch.CurrentEpisode == 1 { return ch.ID } } return firstID } // FindChapter 在详情选集中按 chapterId 查找。 func FindChapter(playlet *PlayletInfoResp, chapterID int64) *PlayletChapterData { if playlet == nil || chapterID <= 0 { return nil } for i := range playlet.PlayletChapterInfo { if playlet.PlayletChapterInfo[i].ID == chapterID { return &playlet.PlayletChapterInfo[i] } } return nil } // NextChapterID 返回下一集 chapterId;没有则 0。 func NextChapterID(playlet *PlayletInfoResp, chapterID int64) int64 { if playlet == nil || chapterID <= 0 { return 0 } found := false for _, ch := range playlet.PlayletChapterInfo { if found && ch.ID > 0 { return ch.ID } if ch.ID == chapterID { found = true } } return 0 } // FirstUnlockCost 取第一个需金币解锁的单集价格。 func FirstUnlockCost(chapters []PlayletChapterData) int { for _, ch := range chapters { if ch.NeedCoinGate() { return ch.UnlockCost() } } return 0 } // ChapterFromList 将详情选集条目转成章节详情结构(chapter/detail 失败时的 UI 兜底)。 func ChapterFromList(ch PlayletChapterData) *PlayletChapterInfoResp { return &PlayletChapterInfoResp{ ID: ch.ID, PlayletID: ch.PlayletID, CurrentEpisode: ch.CurrentEpisode, Title: ch.Title, MD5: ch.MD5, Hash: ch.Hash, Height: ch.Height, Width: ch.Width, Ratio: ch.Ratio, VideoURL: ch.VideoURL, MediaSize: ch.MediaSize, MediaTime: ch.MediaTime, PayType: ch.PayType, SingleCoins: ch.SingleCoins, CostCoins: ch.CostCoins, Balance: ch.Balance, CanWatch: ch.CanWatch, IsUnlocked: ch.IsUnlocked, NeedUnlock: ch.NeedUnlock, NeedVip: ch.NeedVip, VipValid: ch.VipValid, } } // AllowWatch 是否可播放(优先 canWatch;兼容仅返回 videoUrl 的旧数据)。 func (c *PlayletChapterInfoResp) AllowWatch() bool { if c == nil { return false } if c.CanWatch { return true } if c.NeedUnlock || c.NeedVip { return false } return strings.TrimSpace(c.VideoURL) != "" && c.PayType == 0 } // UnlockCost 金币解锁消耗:优先 costCoins。 func (c *PlayletChapterInfoResp) UnlockCost() int64 { if c == nil { return 0 } if c.CostCoins > 0 { return c.CostCoins } if c.SingleCoins > 0 { return int64(c.SingleCoins) } return 0 } // AllowWatch 详情选集是否可直达播放(优先 canWatch)。 func (c PlayletChapterData) AllowWatch() bool { if c.CanWatch { return true } if c.NeedUnlock || c.NeedVip { return false } if c.IsUnlocked { return true } return c.PayType == 0 } // NeedCoinGate 是否弹出金币解锁。 func (c PlayletChapterData) NeedCoinGate() bool { if c.AllowWatch() { return false } if c.NeedUnlock { return true } return c.PayType == 1 } // NeedVipGate 是否弹出 VIP 开通。 func (c PlayletChapterData) NeedVipGate() bool { if c.AllowWatch() || c.NeedCoinGate() { return false } if c.NeedVip { return true } return c.PayType == 2 } // UnlockCost 金币解锁消耗:优先 costCoins。 func (c PlayletChapterData) UnlockCost() int { if c.CostCoins > 0 { return int(c.CostCoins) } return c.SingleCoins }