package mediacontentser import ( "91porn-server/app/appg" "91porn-server/common" "91porn-server/common/log" topser "91porn-server/common/top" topasist "91porn-server/common/top/asistant" "91porn-server/models/v/media_buy_record_mod" "91porn-server/models/v/mediamod" "91porn-server/models/v/usermod" "91porn-server/models/v/walletmod" "fmt" "strings" "time" "91porn-server/models/cache/mediacontentdata" "91porn-server/models/v/mediacontentmod" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" ) // MediaContentInfo 移动端返回内容 type MediaContentInfo struct { ID primitive.ObjectID `json:"id"` // 文档id MediaID primitive.ObjectID `json:"mediaId"` // 媒体资源ID MediaType string `json:"mediaType"` // 媒体类型 EpisodeNumber int `json:"episodeNumber"` // 第几集 ListenPermission int `json:"listenPermission"` // 收听权限 0:会员 1:金币购买 2:免费 Price int64 `json:"price"` // 购买价格 Name string `json:"name"` // 章节名 Cover string `json:"cover"` // 封面 Text string `json:"text"` // 内容(小说专用) Md5 string `json:"md5"` // 媒体md5 VideoUrl string `json:"videoUrl"` // 视频地址 H265Url string `json:"h265Url"` // H.265 视频地址 AudioUrl string `json:"audioUrl"` // 音频地址 UrlSet []string `json:"urlSet"` // 地址集(多个资源地址用 例如漫画) Height int `json:"height"` // 高 Weight int `json:"weight"` // 宽 MediaSize int64 `json:"mediaSize"` // 资源大小 PlayTime uint `json:"playTime"` // 影片长度 IsActive bool `json:"isActive"` // 是否激活 Ratio float64 `json:"ratio"` // 宽高比 CreatedAt time.Time `json:"createdAt"` // 文档创建时间 UpdateTime time.Time `json:"updateTime"` // 文档更新时间 HasBuy bool `json:"hasBuy"` // 是否已购买 IsFree bool `json:"isFree"` HasDramaCard bool `json:"hasDramaCard"` CanPlay bool `json:"canPlay"` AccessType string `json:"accessType"` PreviewEnabled bool `json:"previewEnabled"` PreviewStart int `json:"previewStart"` PreviewSeconds int `json:"previewSeconds"` PreviewVideoUrl string `json:"previewVideoUrl"` PreviewH265Url string `json:"previewH265Url"` Prev *OtherContent `json:"prev"` // 上一集 Next *OtherContent `json:"next"` // 下一集 Paywall *PaywallInfo `json:"paywall"` } type OtherContent struct { Id primitive.ObjectID `json:"id"` EpisodeNumber int `json:"episodeNumber"` Name string `json:"name"` Cover string `json:"cover"` } type PaywallInfo struct { CheckoutContextID string `json:"checkoutContextId"` Title string `json:"title"` CoinBalance int64 `json:"coinBalance"` UnlockCoin int64 `json:"unlockCoin"` CoinButtonText string `json:"coinButtonText"` CardButtonText string `json:"cardButtonText"` } type AppQueryListReq struct { PageNumber uint64 `form:"pageNumber" json:"pageNumber" binding:"required,min=1"` PageSize uint64 `form:"pageSize" json:"pageSize" binding:"required,min=1,max=200"` MediaId string `json:"mediaId" form:"mediaId" binding:"required"` SortType int `json:"sortType" form:"sortType" binding:"omitempty,oneof=0 1"` // 排序类型 0-正序 1-倒序 } func (p *AppQueryListReq) Skip() uint64 { return (p.PageNumber - 1) * p.PageSize } func (p *AppQueryListReq) Limit() uint64 { return p.PageSize } type AppListRes struct { Total int64 `json:"total"` HasNext bool `json:"hasNext"` List []AppMediaContent `json:"list"` } type AppMediaContent struct { ID primitive.ObjectID `json:"id"` // 文档id MediaID primitive.ObjectID `json:"mediaId"` // 媒体资源ID MediaType string `json:"mediaType"` // 媒体类型 EpisodeNumber int `json:"episodeNumber"` // 第几集 ListenPermission int `json:"listenPermission"` // 收听权限 0:会员 1:金币购买 2:免费 VideoUrl string `json:"videoUrl"` // 视频地址 H265Url string `json:"h265Url"` // H.265 视频地址 AudioUrl string `json:"audioUrl"` // 音频地址 PlayTime uint `json:"playTime"` // 播放时长 Price int64 `json:"price"` // 购买价格 Name string `json:"name"` // 章节名 Cover string `json:"cover"` // 封面 HasBuy bool `json:"hasBuy"` // 是否已购买 IsFree bool `json:"isFree"` HasDramaCard bool `json:"hasDramaCard"` CanPlay bool `json:"canPlay"` AccessType string `json:"accessType"` PreviewEnabled bool `json:"previewEnabled"` PreviewStart int `json:"previewStart"` PreviewSeconds int `json:"previewSeconds"` PreviewVideoUrl string `json:"previewVideoUrl"` PreviewH265Url string `json:"previewH265Url"` Ratio float64 `json:"ratio"` CreatedAt time.Time `json:"createdAt"` // 创建时间 UpdateTime time.Time `json:"updateTime"` } const ( dramaAccessFree = "free" dramaAccessBought = "bought" dramaAccessCard = "card" dramaAccessCoin = "coin" ) func ActiveDramaCard(uid uint64) bool { ok, err := ActiveDramaCardStatus(uid) return err == nil && ok } func ActiveDramaCardStatus(uid uint64) (bool, error) { user, err := usermod.FindUserByUID(uid) if err != nil { return false, err } return user.DramaExpire.After(time.Now()), nil } func ResolveDramaAccess( item mediacontentmod.MediaContent, media mediamod.Media, hasBuy, hasCard bool, ) (isFree, canPlay bool, accessType string) { isFree = item.ListenPermission == 2 || item.Price <= 0 || (media.FreeEpisode > 0 && item.EpisodeNumber <= media.FreeEpisode) switch { case isFree: return true, true, dramaAccessFree case hasBuy: return false, true, dramaAccessBought case hasCard: return false, true, dramaAccessCard default: return false, false, dramaAccessCoin } } func newPaywall(uid uint64, price int64) *PaywallInfo { balance := int64(0) if wallet, err := walletmod.GetWallet(uid); err == nil && wallet != nil { balance = wallet.Amount + wallet.Income } return &PaywallInfo{ CheckoutContextID: "drama-checkout-" + primitive.NewObjectID().Hex(), Title: "更多精彩解锁即享", CoinBalance: balance, UnlockCoin: price, CoinButtonText: fmt.Sprintf("%d金币解锁", price), CardButtonText: "开通短剧卡免费看", } } // GetList 获取列表 func (p *AppQueryListReq) GetList(uid uint64) (res AppListRes, err error) { mediaId, err := primitive.ObjectIDFromHex(p.MediaId) if err != nil { return res, err } media, err := mediamod.GetInfo(mediaId) if err != nil { return res, err } filter := bson.M{"mediaId": mediaId, "isActive": true, "isDelete": false} if media.MediaType == mediamod.MediaTypeDrama { filter["mediaType"] = mediamod.MediaTypeDrama if media.Status != 1 || media.IsDelete { return res, fmt.Errorf("drama is inactive") } } sort := bson.D{{Key: "episodeNumber", Value: 1}} if p.SortType == 1 { sort = bson.D{{Key: "episodeNumber", Value: -1}} } // 获取列表 var data []mediacontentmod.MediaContent data, res.Total, res.HasNext, err = mediacontentdata.GetListFromCache(filter, int64(p.Skip()), int64(p.Limit()), sort) if err != nil { log.Error("AppQueryListReq.GetList mediacontentdata.GetListFromCache fail", log.Any("req", p), log.E(err)) return res, err } contentIds := []primitive.ObjectID{} // 获取是否已经购买 for _, item := range data { contentIds = append(contentIds, item.ID) } buyMap, isWholeBuy, err := media_buy_record_mod.IsBuyBatch(uid, mediaId, contentIds) if err != nil { log.Error("AppQueryListReq.GetList media_buy_record_mod.IsBuyBatch fail", log.Any("req", p), log.E(err)) return res, err } hasCard := false if media.MediaType == mediamod.MediaTypeDrama { hasCard, err = ActiveDramaCardStatus(uid) if err != nil { return res, err } } // 组装返回数据 for _, item := range data { tmp := AppMediaContent{ ID: item.ID, MediaID: item.MediaID, MediaType: item.MediaType, EpisodeNumber: item.EpisodeNumber, ListenPermission: item.ListenPermission, Price: item.Price, Name: item.Name, PlayTime: item.PlayTime, Cover: item.Cover, AudioUrl: item.AudioUrl, VideoUrl: item.VideoUrl, H265Url: appg.H265URLForApp(item.H265Url), HasBuy: buyMap[item.ID], Ratio: item.Ratio, CreatedAt: item.CreatedAt, UpdateTime: item.UpdateTime, } if strings.TrimSpace(tmp.Name) == "" { tmp.Name = fmt.Sprintf("第%v集", item.EpisodeNumber) } // 是否整本购买 if isWholeBuy { tmp.HasBuy = true } if item.MediaType == mediamod.MediaTypeDrama { tmp.HasDramaCard = hasCard tmp.IsFree, tmp.CanPlay, tmp.AccessType = ResolveDramaAccess(item, media, tmp.HasBuy, hasCard) tmp.PreviewEnabled = item.PreviewEnabled tmp.PreviewStart = item.PreviewStart tmp.PreviewSeconds = item.PreviewSeconds if !tmp.CanPlay { if tmp.PreviewEnabled { tmp.PreviewVideoUrl = tmp.VideoUrl tmp.PreviewH265Url = tmp.H265Url } tmp.AudioUrl = "" tmp.VideoUrl = "" tmp.H265Url = "" } } if tmp.Cover == "" && len(item.UrlSet) > 0 { tmp.Cover = item.UrlSet[0] } res.List = append(res.List, tmp) } return res, nil } type AppQueryInfoReq struct { ID string `json:"id" form:"id"` // id } // GetInfo 获取详情 func (p *AppQueryInfoReq) GetInfo(uid uint64) (res MediaContentInfo, err error) { oid, err := primitive.ObjectIDFromHex(p.ID) if err != nil { return res, err } var item mediacontentmod.MediaContent item, err = mediacontentdata.GetInfoFromCache(oid) if err != nil { log.Error("AppQueryInfoReq.GetInfo mediacontentdata.GetInfoFromCache fail", log.Any("mediaContentId", p.ID), log.E(err)) return } var media mediamod.Media if item.MediaType == mediamod.MediaTypeDrama { media, err = mediamod.GetInfo(item.MediaID) if err != nil { return res, err } if media.MediaType != mediamod.MediaTypeDrama || media.Status != 1 || media.IsDelete || !item.IsActive || item.IsDelete { return res, fmt.Errorf("drama episode is inactive") } } // 判断是否已经购买 hasBuy, err := media_buy_record_mod.IsBuy(uid, item.MediaID, item.ID) if err != nil { log.Error("AppQueryInfoReq.GetInfo media_buy_record_mod.IsBuy fail", log.Any("mediaContentId", p.ID), log.E(err)) return } res = MediaContentInfo{ ID: item.ID, MediaID: item.MediaID, MediaType: item.MediaType, EpisodeNumber: item.EpisodeNumber, ListenPermission: item.ListenPermission, Price: item.Price, Name: item.Name, Cover: item.Cover, Text: item.Text, Md5: item.Md5, VideoUrl: item.VideoUrl, H265Url: appg.H265URLForApp(item.H265Url), AudioUrl: item.AudioUrl, UrlSet: item.UrlSet, Height: item.Height, Weight: item.Weight, MediaSize: item.MediaSize, PlayTime: item.PlayTime, IsActive: item.IsActive, //Ratio: item.Ratio, Ratio: 1, CreatedAt: item.CreatedAt, UpdateTime: item.UpdateTime, HasBuy: hasBuy, PreviewEnabled: item.PreviewEnabled, PreviewStart: item.PreviewStart, PreviewSeconds: item.PreviewSeconds, } if item.MediaType == mediamod.MediaTypeDrama { res.HasDramaCard, err = ActiveDramaCardStatus(uid) if err != nil { return res, err } res.IsFree, res.CanPlay, res.AccessType = ResolveDramaAccess(item, media, hasBuy, res.HasDramaCard) res.Ratio = item.Ratio if !res.CanPlay { if res.PreviewEnabled { res.PreviewVideoUrl = res.VideoUrl res.PreviewH265Url = res.H265Url } res.VideoUrl = "" res.H265Url = "" res.AudioUrl = "" res.UrlSet = []string{} res.Text = "" res.Md5 = "" res.Paywall = newPaywall(uid, item.Price) } } if strings.TrimSpace(res.Name) == "" { res.Name = fmt.Sprintf("第%v集", item.EpisodeNumber) } if res.Cover == "" && len(item.UrlSet) > 0 { res.Cover = item.UrlSet[0] } EpisodeNumbers := []int{} if item.EpisodeNumber-1 > 0 { EpisodeNumbers = append(EpisodeNumbers, item.EpisodeNumber-1) } EpisodeNumbers = append(EpisodeNumbers, item.EpisodeNumber+1) // 获取上一集下一集 otherFilter := bson.M{"mediaId": item.MediaID, "episodeNumber": bson.M{"$in": EpisodeNumbers}, "isActive": true, "isDelete": false} if item.MediaType == mediamod.MediaTypeDrama { otherFilter["mediaType"] = mediamod.MediaTypeDrama } otherContents, _, _, err := mediacontentdata.GetListFromCache(otherFilter, 0, 2, bson.D{}) if err != nil { log.Error("AppQueryInfoReq.GetInfo mediacontentdata.GetListData fail", log.Any("mediaContentId", p.ID), log.E(err)) return } for _, v := range otherContents { if v.Cover == "" && len(v.UrlSet) > 0 { v.Cover = v.UrlSet[0] } if v.EpisodeNumber == item.EpisodeNumber-1 { // 上一集 res.Prev = &OtherContent{ Id: v.ID, EpisodeNumber: v.EpisodeNumber, Name: v.Name, Cover: v.Cover, } } if v.EpisodeNumber == item.EpisodeNumber+1 { // 下一集 res.Next = &OtherContent{ Id: v.ID, EpisodeNumber: v.EpisodeNumber, Name: v.Name, Cover: v.Cover, } } } // 累加浏览数 common.Go(func() { // 增加父级的浏览数量 mediamod.IncBrowseCount(item.MediaID, 1) topasist.Incr(topser.TypeMedia(item.MediaType), item.ID.Hex(), 1) }) return }