Initial commit: web_hghl_server
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
package srv_videoinfo
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
type VideoInfoReq struct {
|
||||
Id *int64 `json:"id"` //
|
||||
ActressIds *[]int64 `json:"actressIds" bson:"actressIds"` // 女优ID
|
||||
Title *string `json:"title"` // 标题
|
||||
Bango *string `json:"bango"` // 番号,
|
||||
CreatorId *int64 `json:"creatorId"` // 作者Id
|
||||
Status *pubdef.InformationStatus `json:"status"` // 帖子上架状态
|
||||
Tag *int64 `json:"tag" ` // 标签
|
||||
IsHot *bool `json:"isHot"` // 是否热门
|
||||
IsRecommend *bool `json:"isRecommend"` // 是否推荐 true-推荐 false-不推荐
|
||||
MediaType *videoinformationmod.MediaType `json:"mediaType"` // 资源类型
|
||||
CreatorName *string `json:"creatorName"` // 作者名称
|
||||
Keyworld *string `json:"keyworld"` // 关键字
|
||||
CategoryId *int64 `json:"categoryId"` // 分类ID
|
||||
VideoTaskId *string `json:"videoTaskId"` // 视频任务id
|
||||
IsTop *bool `json:"isTop" bson:"isTop"` // 是否置顶
|
||||
VideoFileId *string `json:"videoFileId"` // 切片id(切片列表的ID)
|
||||
Sort int64 `json:"sort"` // 排序方式 0=创建时间 1=上架时间 2=更新时间 3=排序值 4=播放次数。按分类查询且排序方式为排序值时,使用该分类的 categoryRanks 排序
|
||||
pubdef.Page
|
||||
StartTime *string `json:"startTime"` // 创建时间-开始
|
||||
EndTime *string `json:"endTime"` // 创建时间-截止
|
||||
OptionAccount *string `json:"optionAccount"` // 最后操作人
|
||||
}
|
||||
|
||||
type VideoInfoResp struct {
|
||||
Total int64 `json:"total"`
|
||||
List videoinformationmod.VideoInformationList `json:"list"`
|
||||
}
|
||||
|
||||
type AddVideoReq struct {
|
||||
FileIds []string `json:"fileId"` // 资源id
|
||||
}
|
||||
|
||||
type SyncMediaFileReq struct {
|
||||
FileIds []string `json:"fileId"` // 媒资库影片 ID
|
||||
SourceVersion string `json:"sourceVersion,omitempty"` // v1 / v2
|
||||
FirstTagMap map[string]string `json:"firstTagMap,omitempty"` // 覆盖一级标签
|
||||
MinVideoSeconds int32 `json:"minVideoSeconds,omitempty"` // 最短时长,0 表示不限制
|
||||
VideoList []SyncMediaFileItem `json:"videoList,omitempty"` // 媒资库搜索结果,优先使用
|
||||
}
|
||||
|
||||
type SyncMediaFileItem struct {
|
||||
Id string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
FieldNameFs string `json:"fieldNameFs"`
|
||||
FirstTag string `json:"firstTag"`
|
||||
SecondTags []string `json:"secondTags"`
|
||||
VideoTime int32 `json:"videoTime"`
|
||||
CoverImg string `json:"coverImg"`
|
||||
Publish string `json:"publish"`
|
||||
Bango string `json:"bango"`
|
||||
TaskID string `json:"taskId"`
|
||||
Keyworld []string `json:"keyworld"`
|
||||
Width int64 `json:"width"`
|
||||
Height int64 `json:"height"`
|
||||
}
|
||||
|
||||
type SyncMediaFileResp struct {
|
||||
Synced int `json:"synced"`
|
||||
Failed int `json:"failed"`
|
||||
}
|
||||
type VideoInfoUpdate struct {
|
||||
Title *string `json:"title,omitempty" bson:"title,omitempty"` // 帖子标题
|
||||
Slug *string `json:"slug,omitempty" bson:"slug,omitempty"` // slug
|
||||
ActressIds *[]int64 `json:"actressIds" bson:"actressIds,omitempty"` // 女优ID
|
||||
AddedTime *time.Time `json:"addedTime,omitempty" bson:"addedTime,omitempty"`
|
||||
CreatorId *int64 `json:"creatorId,omitempty" bson:"creatorId,omitempty"` // 作者Id
|
||||
Keyworld *[]string `bson:"keyworld,omitempty" json:"keyworld,omitempty"` //
|
||||
MediaType *videoinformationmod.MediaType `json:"mediaType,omitempty" bson:"mediaType,omitempty"` // 资源类型
|
||||
FirstTag *string `bson:"firstTag,omitempty" json:"firstTag,omitempty"` // 一级标签
|
||||
SecondTags *[]string `bson:"secondTags,omitempty" json:"secondTags,omitempty"` // 二级标签标签
|
||||
CoverImg *string `bson:"coverImg,omitempty" json:"coverImg,omitempty"` // 封面
|
||||
UnencryptedImage *string `bson:"unencryptedImage,omitempty" json:"unencryptedImage"` // 不加密封面
|
||||
Desc *string `bson:"desc,omitempty" json:"desc,omitempty"` // 详细描述
|
||||
Remark *string `bson:"remark,omitempty" json:"remark,omitempty"` // 备注
|
||||
Nodes *[]videoinformationmod.Node `json:"nodes,omitempty" bson:"nodes,omitempty"` // 节点
|
||||
Status *pubdef.InformationStatus `json:"status,omitempty" bson:"status,omitempty"` // 帖子上架状态
|
||||
Sort *int64 `json:"sort,omitempty" bson:"sort,omitempty"` // 排序
|
||||
CategoryId *int64 `json:"categoryId" bson:"categoryId,omitempty"` // 分类ID
|
||||
CategoryRanks *[]videoinformationmod.VideoCategoryRank `json:"categoryRanks" bson:"categoryRanks,omitempty"` // 各分类下的排序,传空数组表示清空
|
||||
|
||||
Cover *string `json:"cover,omitempty" bson:"cover,omitempty"` // 视频封面
|
||||
Height *int64 `json:"height,omitempty" bson:"height,omitempty"` // 分辨率高(视频才会存在)
|
||||
Width *int64 `json:"width,omitempty" bson:"width,omitempty"` // 分辨率宽(视频才会存在)
|
||||
VideoTime *int32 `bson:"videoTime,omitempty" json:"videoTime,omitempty"` // 影片长度
|
||||
|
||||
Tags *[]int64 `json:"tags,omitempty" bson:"tags,omitempty"` // 标签
|
||||
IsHot *bool `json:"isHot,omitempty" bson:"isHot,omitempty"` // 是否热门
|
||||
IsRecommend *bool `json:"isRecommend,omitempty" bson:"isRecommend,omitempty"` // 是否推荐 true-推荐 false-不推荐
|
||||
UpdatedAt *time.Time `json:"updatedAt,omitempty" bson:"updatedAt,omitempty"` // 更新时间
|
||||
|
||||
CreatorName *string `bson:"creatorName,omitempty"` // 作者名
|
||||
IsTop *bool `json:"isTop,omitempty" bson:"isTop,omitempty"` // 是否置顶
|
||||
Video *videoinformationmod.VideoInfo `bson:"video,omitempty"` // 视频
|
||||
IsReIndexNow *bool `json:"isReIndexNow" bson:"isReIndexNow"` // 是否重新提交索引
|
||||
IndexNow *int64 `bson:"indexNow,omitempty"`
|
||||
IssuedAt *time.Time `json:"issuedAt" bson:"issuedAt"` // 发行时间
|
||||
IssuedBrand *string `json:"issuedBrand" bson:"issuedBrand"` // 发行商
|
||||
Bango *string `bson:"bango,omitempty" json:"bango"` // 番号,唯一
|
||||
SecondUpdatedAt *time.Time `json:"secondUpdatedAt,omitempty" bson:"secondUpdatedAt,omitempty"` // 二次修改时间
|
||||
}
|
||||
type VideoInfoUpdateReq struct {
|
||||
Id int64 `json:"id" binding:"required"`
|
||||
Param VideoInfoUpdate `json:"param" bson:"param"` // appId,唯一id
|
||||
}
|
||||
|
||||
type VideoInfoDelReq struct {
|
||||
Id int64 `json:"id" binding:"required"`
|
||||
}
|
||||
type AddVideoCollectionReq struct {
|
||||
Title string `json:"title,omitempty" bson:"title,omitempty"` // 帖子标题
|
||||
Keyworld []string `bson:"keyworld,omitempty" json:"keyworld,omitempty"` // 关键字
|
||||
FirstTag string `bson:"firstTag,omitempty" json:"firstTag,omitempty"` // 一级标签
|
||||
SecondTags []string `bson:"secondTags,omitempty" json:"secondTags,omitempty"` // 二级标签标签
|
||||
CoverImg string `bson:"coverImg,omitempty" json:"coverImg,omitempty"` // 封面
|
||||
Desc string `bson:"desc,omitempty" json:"desc,omitempty"` // 详细描述
|
||||
Remark string `bson:"remark,omitempty" json:"remark,omitempty"` // 备注
|
||||
Tags []int64 `json:"tags,omitempty" bson:"tags,omitempty"` // 标签
|
||||
}
|
||||
|
||||
type VideoAddTagsReq struct {
|
||||
VideoIds []int64 `json:"videoIds" binding:"required"` // 视频ID集合
|
||||
Tags []int64 `json:"tags" binding:"required"` // 统一标签ID集合
|
||||
}
|
||||
|
||||
type VideoSetTagsReq struct {
|
||||
Ids []int64 `json:"ids" binding:"required"` // 视频ID集合,传1个表示单条设置,传多个表示批量设置
|
||||
TagIds *[]int64 `json:"tagIds"` // 标签ID集合,可设置多个标签,传空数组表示清空标签,不传表示参数错误
|
||||
}
|
||||
|
||||
type SetFakeLikesReq struct {
|
||||
Ids []int64 `json:"ids"` // 视频ID集合,支持单个或批量
|
||||
LikesMin int64 `json:"likesMin"` // 假点赞数随机最小值,包含该值
|
||||
LikesMax int64 `json:"likesMax"` // 假点赞数随机最大值,包含该值
|
||||
}
|
||||
|
||||
type SetFakeWatchTimesReq struct {
|
||||
Ids []int64 `json:"ids"` // 视频ID集合,支持单个或批量
|
||||
WatchTimesMin int64 `json:"watchTimesMin"` // 假观看次数随机最小值,包含该值
|
||||
WatchTimesMax int64 `json:"watchTimesMax"` // 假观看次数随机最大值,包含该值
|
||||
}
|
||||
|
||||
// SetVideoHeatReq 单/批量设置视频热度
|
||||
type SetVideoHeatReq struct {
|
||||
Ids []int64 `json:"ids"` // 视频ID集合
|
||||
HeatMin int64 `json:"heatMin"` // 热度最小值
|
||||
HeatMax int64 `json:"heatMax"` // 热度最大值
|
||||
}
|
||||
|
||||
type SetRecommendReq struct {
|
||||
Ids []int64 `json:"ids"` // 视频ID集合,传1个表示单条设置,传多个表示批量设置
|
||||
IsRecommend bool `json:"isRecommend"` // 是否推荐 true-推荐 false-不推荐
|
||||
}
|
||||
|
||||
type SetFakeCountResp struct {
|
||||
MatchedCount int64 `json:"matchedCount"` // 匹配到的视频数量
|
||||
ModifiedCount int64 `json:"modifiedCount"` // 实际修改的视频数量
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package srv_videoinfo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/contenttagmod"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
type ExternalVideoListReq struct {
|
||||
pubdef.Page
|
||||
Date string `form:"date"` // 日期筛选,格式:2006-01-02
|
||||
Publisher string `form:"publisher"` // 发布人
|
||||
}
|
||||
|
||||
type ExternalVideoListItem struct {
|
||||
Date string `json:"date"`
|
||||
Publisher string `json:"publisher"`
|
||||
SiteName string `json:"siteName"`
|
||||
Title string `json:"title"`
|
||||
Keyworld []string `json:"keyworld"`
|
||||
Tags []string `json:"tags"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Url string `json:"url"`
|
||||
Content []string `json:"content"`
|
||||
UploadUserName string `json:"uploadUserName"`
|
||||
}
|
||||
|
||||
type ExternalVideoListResp struct {
|
||||
Total int64 `json:"total"`
|
||||
List []ExternalVideoListItem `json:"list"`
|
||||
}
|
||||
|
||||
// SearchExternalVideoList 按日期查询已上架视频,供外部数据接口使用。
|
||||
func SearchExternalVideoList(ctx context.Context, req *ExternalVideoListReq) (*ExternalVideoListResp, *srvdef.SrvErr) {
|
||||
cst := time.FixedZone("CST", 8*3600)
|
||||
|
||||
var dayStart, dayEnd time.Time
|
||||
if req.Date != "" {
|
||||
t, err := time.ParseInLocation("2006-01-02", req.Date, cst)
|
||||
if err == nil {
|
||||
dayStart = t
|
||||
} else {
|
||||
dayStart = time.Now().In(cst).Truncate(24 * time.Hour).Add(-24 * time.Hour)
|
||||
}
|
||||
} else {
|
||||
now := time.Now().In(cst)
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, cst)
|
||||
dayStart = today.Add(-24 * time.Hour)
|
||||
}
|
||||
dayEnd = dayStart.Add(24 * time.Hour)
|
||||
|
||||
filter := bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
|
||||
"$or": bson.A{
|
||||
bson.M{
|
||||
videoinformationmod.FN.VideoInformation.AddedTime: bson.M{
|
||||
"$gte": dayStart.UTC(),
|
||||
"$lt": dayEnd.UTC(),
|
||||
},
|
||||
},
|
||||
bson.M{
|
||||
videoinformationmod.FN.VideoInformation.SecondUpdatedAt: bson.M{
|
||||
"$gte": dayStart.UTC(),
|
||||
"$lt": dayEnd.UTC(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if req.Publisher != "" {
|
||||
filter[videoinformationmod.FN.VideoInformation.UploadUserName] = req.Publisher
|
||||
}
|
||||
|
||||
total, serr := videoinformationmod.CountVideoInformations(ctx, filter)
|
||||
if serr != nil {
|
||||
log.Warn("SearchExternalVideoList CountVideoInformations failed", log.E(serr))
|
||||
return nil, serr
|
||||
}
|
||||
|
||||
pageSize := req.Page.PageSize
|
||||
if pageSize <= 0 {
|
||||
pageSize = 1000
|
||||
}
|
||||
pageNum := req.Page.PageNum
|
||||
if pageNum <= 0 {
|
||||
pageNum = 1
|
||||
}
|
||||
opts := &options.FindOptions{}
|
||||
opts.SetLimit(pageSize)
|
||||
opts.SetSkip((pageNum - 1) * pageSize)
|
||||
opts.SetSort(bson.D{{Key: videoinformationmod.FN.VideoInformation.AddedTime, Value: -1}})
|
||||
list, serr := videoinformationmod.FindManyVideoInformations(ctx, filter, opts)
|
||||
if serr != nil {
|
||||
log.Warn("SearchExternalVideoList FindManyVideoInformations failed", log.E(serr))
|
||||
return nil, serr
|
||||
}
|
||||
|
||||
tagIdSet := make(map[int64]struct{})
|
||||
for _, v := range list {
|
||||
for _, tid := range v.Tags {
|
||||
tagIdSet[tid] = struct{}{}
|
||||
}
|
||||
}
|
||||
tagTitleMap := make(map[int64]string)
|
||||
if len(tagIdSet) > 0 {
|
||||
tagIds := make([]int64, 0, len(tagIdSet))
|
||||
for tid := range tagIdSet {
|
||||
tagIds = append(tagIds, tid)
|
||||
}
|
||||
tags, _ := contenttagmod.GetMapTags(ctx, tagIds)
|
||||
for _, t := range tags {
|
||||
tagTitleMap[t.Id] = t.Name
|
||||
}
|
||||
}
|
||||
|
||||
items := make([]ExternalVideoListItem, 0, len(list))
|
||||
for _, v := range list {
|
||||
tagTitles := make([]string, 0, len(v.Tags))
|
||||
for _, tid := range v.Tags {
|
||||
if title, ok := tagTitleMap[tid]; ok {
|
||||
tagTitles = append(tagTitles, title)
|
||||
}
|
||||
}
|
||||
var content []string
|
||||
for _, node := range v.Nodes {
|
||||
content = append(content, node.Text)
|
||||
}
|
||||
items = append(items, ExternalVideoListItem{
|
||||
Date: utils.GetDateString(v.AddedTime),
|
||||
CreatedAt: utils.GetTimeString(v.CreatedAt),
|
||||
UpdatedAt: utils.GetTimeString(v.SecondUpdatedAt),
|
||||
Publisher: v.OptionAccount,
|
||||
SiteName: pubdef.AppName,
|
||||
Title: v.Title,
|
||||
Keyworld: v.Keyworld,
|
||||
Tags: tagTitles,
|
||||
Url: fmt.Sprintf("/video/detail?id=%d", v.Id),
|
||||
Content: content,
|
||||
UploadUserName: v.UploadUserName,
|
||||
})
|
||||
}
|
||||
return &ExternalVideoListResp{Total: total, List: items}, nil
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package srv_videoinfo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"videoweb/pub/common/laosiji"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
"videoweb/web/webg"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
// TranscodeVideo 手动提交主视频 H265 转码。
|
||||
func TranscodeVideo(ctx context.Context, videoId int64) *srvdef.SrvErr {
|
||||
info, serr := videoinformationmod.FindOneVideoInformation(ctx, bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Id: videoId,
|
||||
})
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
src := strings.TrimSpace(info.Video.Url)
|
||||
if src == "" {
|
||||
return srvdef.New(srvdef.Err, "", "该视频没有可转码的源地址")
|
||||
}
|
||||
if laosiji.LaosijiClient == nil || laosiji.LaosijiDomains == nil {
|
||||
return srvdef.New(srvdef.Err, "", "转码服务未就绪")
|
||||
}
|
||||
if info.HevcStatus == videoinformationmod.HevcStatusPending {
|
||||
return srvdef.New(srvdef.Err, "", "转码请求已提交,正在转码中,请勿重复提交")
|
||||
}
|
||||
if err := SubmitTranscodeVideo(ctx, videoId, src, info.Video.FileId); err != nil {
|
||||
return srvdef.New(srvdef.Err, "", "提交转码失败:"+err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildTranscodeSourceURL(videoUrl string) string {
|
||||
name := strings.TrimSpace(videoUrl)
|
||||
if name == "" {
|
||||
return ""
|
||||
}
|
||||
if !strings.Contains(name, ".m3u8") {
|
||||
name += ".m3u8"
|
||||
}
|
||||
host := strings.TrimSpace(webg.Conf.WebM3u8Host)
|
||||
if host == "" {
|
||||
return ""
|
||||
}
|
||||
return utils.JoinUrlPath(host, "/api/web/m3u8/"+name)
|
||||
}
|
||||
|
||||
// SubmitTranscodeVideo 提交主视频转码任务。
|
||||
func SubmitTranscodeVideo(ctx context.Context, videoId int64, src, fileId string) error {
|
||||
fileURL := buildTranscodeSourceURL(src)
|
||||
if fileURL == "" {
|
||||
return fmt.Errorf("empty transcode source url, videoUrl:%s host:%s", src, webg.Conf.WebM3u8Host)
|
||||
}
|
||||
sd, err := laosiji.LaosijiDomains.Get(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, err := laosiji.LaosijiClient.SubmitTranscoding(ctx, fileURL, fileId, *sd, videoId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return applyTranscodeResult(ctx, videoId, result)
|
||||
}
|
||||
|
||||
// RefreshTranscodeVideo 查询转码任务状态并回写。
|
||||
func RefreshTranscodeVideo(ctx context.Context, videoId int64, src, fileId string) error {
|
||||
fileURL := buildTranscodeSourceURL(src)
|
||||
if fileURL == "" {
|
||||
return fmt.Errorf("empty transcode source url, videoUrl:%s host:%s", src, webg.Conf.WebM3u8Host)
|
||||
}
|
||||
sd, err := laosiji.LaosijiDomains.Get(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, found, err := laosiji.LaosijiClient.QueryTranscoding(ctx, fileURL, fileId, *sd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
return applyTranscodeResult(ctx, videoId, result)
|
||||
}
|
||||
|
||||
func applyTranscodeResult(ctx context.Context, videoId int64, result laosiji.TranscodeResult) error {
|
||||
log.Infof("==applyTranscodeResult===> result", log.Any("videoId", videoId), log.Any("result", result))
|
||||
var update bson.M
|
||||
switch {
|
||||
case result.Done:
|
||||
meta := toVideoMetaInfo(result.HevcMeta)
|
||||
if meta == nil {
|
||||
meta = &videoinformationmod.VideoMetaInfo{}
|
||||
}
|
||||
h265Url := normalizeH265UrlForSave(result.HevcURL)
|
||||
meta.M3u8URL = h265Url
|
||||
set := bson.M{
|
||||
videoinformationmod.FN.VideoInformation.HevcStatus: videoinformationmod.HevcStatusSuccess,
|
||||
videoinformationmod.FN.VideoInformation.HevcMetaInfo: meta,
|
||||
videoinformationmod.FN.VideoInformation.Video.H265Url: h265Url,
|
||||
}
|
||||
if result.PreviewVtt != "" {
|
||||
set[videoinformationmod.FN.VideoInformation.PreviewVtt] = result.PreviewVtt
|
||||
}
|
||||
if result.AudioFile != "" {
|
||||
set[videoinformationmod.FN.VideoInformation.AudioFile] = result.AudioFile
|
||||
}
|
||||
if result.SmartCover != "" {
|
||||
set[videoinformationmod.FN.VideoInformation.SmartCover] = result.SmartCover
|
||||
}
|
||||
update = bson.M{"$set": set}
|
||||
case result.Failed:
|
||||
update = bson.M{
|
||||
"$set": bson.M{videoinformationmod.FN.VideoInformation.HevcStatus: videoinformationmod.HevcStatusFailed},
|
||||
"$inc": bson.M{videoinformationmod.FN.VideoInformation.HevcFailCount: 1},
|
||||
}
|
||||
default:
|
||||
update = bson.M{"$set": bson.M{videoinformationmod.FN.VideoInformation.HevcStatus: videoinformationmod.HevcStatusPending}}
|
||||
}
|
||||
if serr := videoinformationmod.UpdateOneVideoInformation(ctx, bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Id: videoId,
|
||||
}, update); serr != nil {
|
||||
return fmt.Errorf("update video transcode status: %v", serr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeH265UrlForSave(url string) string {
|
||||
url = strings.TrimSpace(url)
|
||||
return strings.TrimPrefix(url, "/")
|
||||
}
|
||||
|
||||
func toVideoMetaInfo(m *laosiji.MetaInfo) *videoinformationmod.VideoMetaInfo {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
return &videoinformationmod.VideoMetaInfo{
|
||||
Codec: m.Codec,
|
||||
Profile: m.Profile,
|
||||
Width: m.Width,
|
||||
Height: m.Height,
|
||||
Fps: m.Fps,
|
||||
VideoBitrate: m.VideoBitrate,
|
||||
PixFmt: m.PixFmt,
|
||||
AudioCodec: m.AudioCodec,
|
||||
AudioBitrate: m.AudioBitrate,
|
||||
AudioSampleRate: m.AudioSampleRate,
|
||||
AudioChannels: m.AudioChannels,
|
||||
Bitrate: m.Bitrate,
|
||||
Size: m.Size,
|
||||
Duration: m.Duration,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
package srv_videoinfo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/playletcache"
|
||||
"videoweb/pub/common/slugx"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/playletchaptermod"
|
||||
"videoweb/pub/models/videocreatermod"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
"videoweb/pub/mongos"
|
||||
"videoweb/pub/storage/proxy"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func videoSyncKey(media proxy.MediaResp) string {
|
||||
if !media.MediaSourceId.IsZero() {
|
||||
return media.MediaSourceId.Hex()
|
||||
}
|
||||
return media.Id.Hex()
|
||||
}
|
||||
|
||||
func buildVideoSyncFilter(media proxy.MediaResp) bson.M {
|
||||
keys := []string{media.Id.Hex()}
|
||||
if !media.MediaSourceId.IsZero() {
|
||||
msid := media.MediaSourceId.Hex()
|
||||
if msid != keys[0] {
|
||||
keys = append(keys, msid)
|
||||
}
|
||||
}
|
||||
or := make([]bson.M, 0, len(keys)*2)
|
||||
for _, key := range keys {
|
||||
or = append(or,
|
||||
bson.M{videoinformationmod.FN.VideoInformation.Video.FileId: key},
|
||||
bson.M{videoinformationmod.FN.VideoInformation.SyncId: key},
|
||||
)
|
||||
}
|
||||
return bson.M{"$or": or}
|
||||
}
|
||||
|
||||
func IsPlayletMedia(ctx context.Context, media proxy.MediaResp) bool {
|
||||
return playletchaptermod.IsLinkedMedia(ctx, media.IsPlaylet, media.PlayletKeys()...)
|
||||
}
|
||||
|
||||
func SyncOneVideo(ctx context.Context, media proxy.MediaResp) *srvdef.SrvErr {
|
||||
if IsPlayletMedia(ctx, media) {
|
||||
log.Info("VideoSync skip playlet media", log.Any("mediaId", media.Id.Hex()), log.Any("mediaSourceId", media.MediaSourceId.Hex()))
|
||||
return nil
|
||||
}
|
||||
// 同步时不再自动写 video_taginfo,标签由后台统一标签绑定维护
|
||||
syncKey := videoSyncKey(media)
|
||||
filter := buildVideoSyncFilter(media)
|
||||
videoInfo, serr := videoinformationmod.FindOneVideoInformation(ctx, filter)
|
||||
if serr != nil {
|
||||
// 新加入的视频
|
||||
if serr.Code == srvdef.ErrNoData {
|
||||
videoInfo = media.ToVideoInfo()
|
||||
if videoInfo == nil {
|
||||
log.Error("VideoSync FindOneVideoInformation", log.Any("MediaId", media.Id))
|
||||
return serr
|
||||
}
|
||||
if len(media.Publish) > 0 {
|
||||
creater, serr := videocreatermod.FindOneVideoCreaterInfo(ctx, bson.M{
|
||||
videocreatermod.FN.VideoCreaterInfo.Name: media.Publish,
|
||||
})
|
||||
if serr != nil && serr.Code != srvdef.ErrNoData {
|
||||
log.Error("VideoSync.FindOneVideoCreaterInfo", log.Any("media", media), log.E(serr))
|
||||
return serr
|
||||
}
|
||||
if creater == nil {
|
||||
createrId := mongos.Lid.VideoCreator.Get()
|
||||
creater = &videocreatermod.VideoCreaterInfo{
|
||||
Id: createrId,
|
||||
Name: media.Publish,
|
||||
MediaTypes: []int64{1},
|
||||
Status: true,
|
||||
Sort: 0,
|
||||
UpdatedAt: time.Now(),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
createrExists := func(s string) bool {
|
||||
doc, _ := videocreatermod.FindOneVideoCreaterInfo(ctx, slugx.Filter(
|
||||
videocreatermod.FN.VideoCreaterInfo.Slug, videocreatermod.FN.VideoCreaterInfo.Id, s, 0))
|
||||
return doc != nil
|
||||
}
|
||||
if serr = slugx.Ensure(&creater.Slug, utils.SlugSourceFromKeywordsOrTitle(nil, creater.Name), createrExists); serr != nil {
|
||||
return serr
|
||||
}
|
||||
serr = videocreatermod.InsertOneVideoCreaterInfo(ctx, creater)
|
||||
if serr != nil {
|
||||
log.Error("VideoSync.InsertOneVideoCreaterInfo", log.Any("media", media), log.E(serr))
|
||||
return serr
|
||||
}
|
||||
} else {
|
||||
isFind := false
|
||||
for _, v := range creater.MediaTypes {
|
||||
if v == 1 {
|
||||
isFind = true
|
||||
}
|
||||
}
|
||||
if !isFind {
|
||||
mediaTypes := append(creater.MediaTypes, 1)
|
||||
_ = videocreatermod.UpdateOneVideoCreaterInfo(ctx, bson.M{videocreatermod.FN.VideoCreaterInfo.Id: creater.Id}, bson.M{
|
||||
"$set": bson.M{videocreatermod.FN.VideoCreaterInfo.MediaTypes: mediaTypes},
|
||||
})
|
||||
}
|
||||
}
|
||||
videoInfo.CreatorId = creater.Id
|
||||
videoInfo.CreatorName = creater.Name
|
||||
}
|
||||
videoInfo.IndexNow = 0
|
||||
applyDefaultFakeFlow(&videoInfo.Fake, rand.New(rand.NewSource(time.Now().UnixNano())))
|
||||
exists := func(s string) bool {
|
||||
doc, _ := videoinformationmod.FindOneVideoInformation(ctx, slugx.Filter(
|
||||
videoinformationmod.FN.VideoInformation.Slug, videoinformationmod.FN.VideoInformation.Id, s, 0))
|
||||
return doc != nil
|
||||
}
|
||||
if serr = slugx.Ensure(&videoInfo.Slug, utils.SlugSourceFromKeywordsOrTitle(videoInfo.Keyworld, videoInfo.Title), exists); serr != nil {
|
||||
return serr
|
||||
}
|
||||
serr = videoinformationmod.InsertOneVideoInformation(ctx, videoInfo)
|
||||
if serr != nil {
|
||||
log.Error("VideoSync InsertOneVideoInformation", log.Any("videoInfo", videoInfo), log.E(serr))
|
||||
return serr
|
||||
}
|
||||
} else {
|
||||
log.Error("VideoSync FindOneVideoInformation", log.Any("filter", filter), log.E(serr))
|
||||
return serr
|
||||
}
|
||||
} else {
|
||||
// 更新资源
|
||||
nodes := make([]videoinformationmod.Node, 0)
|
||||
if videoInfo != nil {
|
||||
mediaNodeMap := make(map[string]videoinformationmod.Node, len(media.Nodes))
|
||||
for _, node := range media.Nodes {
|
||||
if node.NodeId != "" {
|
||||
mediaNodeMap[node.NodeId] = node
|
||||
}
|
||||
}
|
||||
// 更新节点里面图片水印 以本地视频列表的节点为主
|
||||
for _, node := range videoInfo.Nodes {
|
||||
if mediaNode, ok := mediaNodeMap[node.NodeId]; ok {
|
||||
node.Images = mediaNode.Images
|
||||
}
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
} else {
|
||||
nodes = media.Nodes
|
||||
}
|
||||
setDoc := bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Nodes: nodes,
|
||||
videoinformationmod.FN.VideoInformation.Keyworld: media.Keyworld,
|
||||
videoinformationmod.FN.VideoInformation.FirstTag: media.FirstTag,
|
||||
videoinformationmod.FN.VideoInformation.SecondTags: media.SecondTags,
|
||||
videoinformationmod.FN.VideoInformation.CoverImg: media.CoverImg,
|
||||
videoinformationmod.FN.VideoInformation.IssuedBrand: media.IssuedBrand,
|
||||
videoinformationmod.FN.VideoInformation.IssuedAt: media.IssuedAt,
|
||||
}
|
||||
for k, v := range defaultFakeFlowSetDoc(rand.New(rand.NewSource(time.Now().UnixNano()))) {
|
||||
setDoc[k] = v
|
||||
}
|
||||
setDoc[videoinformationmod.FN.VideoInformation.SyncId] = syncKey
|
||||
if len(media.FieldNameFs) > 0 {
|
||||
video := videoinformationmod.VideoInfo{
|
||||
TaskId: media.TaskID,
|
||||
FileId: syncKey,
|
||||
Url: media.FieldNameFs,
|
||||
Cover: media.CoverImg,
|
||||
Height: media.Height,
|
||||
Width: media.Width,
|
||||
VideoTime: media.VideoTime,
|
||||
}
|
||||
if !strings.HasSuffix(video.Url, ".m3u8") {
|
||||
video.Url += ".m3u8"
|
||||
}
|
||||
setDoc[videoinformationmod.FN.VideoInformation.Video.TaskId] = video.TaskId
|
||||
setDoc[videoinformationmod.FN.VideoInformation.Video.FileId] = video.FileId
|
||||
setDoc[videoinformationmod.FN.VideoInformation.Video.Url] = video.Url
|
||||
setDoc[videoinformationmod.FN.VideoInformation.Video.Cover] = video.Cover
|
||||
setDoc[videoinformationmod.FN.VideoInformation.Video.Height] = video.Height
|
||||
setDoc[videoinformationmod.FN.VideoInformation.Video.Width] = video.Width
|
||||
setDoc[videoinformationmod.FN.VideoInformation.Video.VideoTime] = video.VideoTime
|
||||
}
|
||||
if preMp4 := strings.TrimSpace(media.PreMp4FileNameFs); preMp4 != "" {
|
||||
setDoc[videoinformationmod.FN.VideoInformation.Video.PreMp4FileNameFs] = preMp4
|
||||
}
|
||||
if unencryptedPreMp4 := strings.TrimSpace(media.UnencryptedPreMp4); unencryptedPreMp4 != "" {
|
||||
setDoc[videoinformationmod.FN.VideoInformation.Video.UnencryptedPreMp4] = unencryptedPreMp4
|
||||
}
|
||||
if videoInfo.Slug == "" {
|
||||
exists := func(s string) bool {
|
||||
doc, _ := videoinformationmod.FindOneVideoInformation(ctx, slugx.Filter(
|
||||
videoinformationmod.FN.VideoInformation.Slug, videoinformationmod.FN.VideoInformation.Id, s, videoInfo.Id))
|
||||
return doc != nil
|
||||
}
|
||||
slug := ""
|
||||
if serr = slugx.Ensure(&slug, utils.SlugSourceFromKeywordsOrTitle(media.Keyworld, videoInfo.Title), exists); serr != nil {
|
||||
return serr
|
||||
}
|
||||
setDoc[videoinformationmod.FN.VideoInformation.Slug] = slug
|
||||
}
|
||||
serr = videoinformationmod.UpdateOneVideoInformation(ctx, bson.M{videoinformationmod.FN.VideoInformation.Id: videoInfo.Id}, bson.M{"$set": setDoc})
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
}
|
||||
playletcache.ClearHomeCache()
|
||||
_ = syncNodeVideo(ctx, media)
|
||||
return nil
|
||||
}
|
||||
|
||||
// syncNodeVideo 同步节点里的视频
|
||||
func syncNodeVideo(ctx context.Context, media proxy.MediaResp) *srvdef.SrvErr {
|
||||
mediaID := videoSyncKey(media)
|
||||
nodeIDs := []string{mediaID}
|
||||
if !media.MediaSourceId.IsZero() {
|
||||
msid := media.MediaSourceId.Hex()
|
||||
if msid != mediaID {
|
||||
nodeIDs = append(nodeIDs, msid)
|
||||
}
|
||||
}
|
||||
if media.Id.Hex() != mediaID && media.Id.Hex() != "" {
|
||||
nodeIDs = append(nodeIDs, media.Id.Hex())
|
||||
}
|
||||
filter := bson.M{"nodes.nodeId": bson.M{"$in": nodeIDs}}
|
||||
opts := &options.FindOptions{}
|
||||
opts.SetLimit(100)
|
||||
videoInfo, serr := videoinformationmod.FindManyVideoInformations(ctx, filter, opts)
|
||||
if serr != nil {
|
||||
log.Warn("syncNodeVideo", log.Any("filter", filter), log.E(serr))
|
||||
return serr
|
||||
}
|
||||
if !strings.HasSuffix(media.FieldNameFs, ".m3u8") {
|
||||
media.FieldNameFs += ".m3u8"
|
||||
}
|
||||
if len(videoInfo) > 0 {
|
||||
writeModels := make([]mongo.WriteModel, 0, len(videoInfo))
|
||||
now := time.Now()
|
||||
// 更新视频路径
|
||||
for _, video := range videoInfo {
|
||||
if video.Nodes != nil && len(video.Nodes) > 0 {
|
||||
isFind := false
|
||||
for i, node := range video.Nodes {
|
||||
if node.Videos != nil && len(node.Videos) > 0 {
|
||||
for j, v := range node.Videos {
|
||||
if v.FileId == mediaID || containsString(nodeIDs, v.FileId) {
|
||||
node.Videos[j].Url = media.FieldNameFs
|
||||
isFind = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if isFind {
|
||||
video.Nodes[i] = node
|
||||
break
|
||||
}
|
||||
}
|
||||
if isFind {
|
||||
writeModels = append(writeModels, mongo.NewUpdateOneModel().
|
||||
SetFilter(bson.M{videoinformationmod.FN.VideoInformation.Id: video.Id}).
|
||||
SetUpdate(bson.M{"$set": bson.M{
|
||||
videoinformationmod.FN.VideoInformation.UpdatedAt: now,
|
||||
videoinformationmod.FN.VideoInformation.Nodes: video.Nodes,
|
||||
}}))
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(writeModels) > 0 {
|
||||
_, serr = videoinformationmod.BulkWriteVideoInformation(ctx, writeModels, options.BulkWrite().SetOrdered(false))
|
||||
if serr != nil {
|
||||
log.Warn("syncNodeVideo BulkWriteVideoInformation failed", log.Any("filter", filter), log.E(serr))
|
||||
return serr
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func containsString(list []string, target string) bool {
|
||||
for _, item := range list {
|
||||
if item == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
package srv_videoinfo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// UnmarshalJSON 兼容 id 使用数字字符串,以及 param 里的表单写法。
|
||||
func (r *VideoInfoUpdateReq) UnmarshalJSON(data []byte) error {
|
||||
data = bytes.TrimSpace(data)
|
||||
if len(data) == 0 || bytes.Equal(data, []byte("null")) {
|
||||
return nil
|
||||
}
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
if value, ok := raw["id"]; ok {
|
||||
next, keep, err := normalizeIntRaw(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("id %w", err)
|
||||
}
|
||||
if keep && next != nil && !bytes.Equal(bytes.TrimSpace(next), []byte("null")) {
|
||||
if err := json.Unmarshal(next, &r.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if value, ok := raw["param"]; ok && !bytes.Equal(bytes.TrimSpace(value), []byte("null")) {
|
||||
if err := json.Unmarshal(value, &r.Param); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON 兼容后台表单里的空字符串、数字字符串,以及分类排序的多种写法。
|
||||
func (v *VideoInfoUpdate) UnmarshalJSON(data []byte) error {
|
||||
data = bytes.TrimSpace(data)
|
||||
if len(data) == 0 || bytes.Equal(data, []byte("null")) {
|
||||
return nil
|
||||
}
|
||||
normalized, err := normalizeVideoUpdateJSON(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
type alias VideoInfoUpdate
|
||||
var out alias
|
||||
if err := json.Unmarshal(normalized, &out); err != nil {
|
||||
return err
|
||||
}
|
||||
*v = VideoInfoUpdate(out)
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeVideoUpdateJSON(data []byte) ([]byte, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, key := range []string{"addedTime", "updatedAt", "issuedAt", "secondUpdatedAt"} {
|
||||
value, ok := raw[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
next, err := normalizeTimeRaw(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if next == nil {
|
||||
delete(raw, key)
|
||||
continue
|
||||
}
|
||||
raw[key] = next
|
||||
}
|
||||
for _, key := range []string{"categoryId", "sort", "creatorId", "height", "width", "videoTime", "indexNow", "mediaType", "status"} {
|
||||
value, ok := raw[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
next, keep, err := normalizeIntRaw(value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s %w", key, err)
|
||||
}
|
||||
if !keep {
|
||||
continue
|
||||
}
|
||||
if next == nil {
|
||||
delete(raw, key)
|
||||
continue
|
||||
}
|
||||
raw[key] = next
|
||||
}
|
||||
for _, key := range []string{"actressIds", "tags"} {
|
||||
value, ok := raw[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
next, keep, err := normalizeIntSliceRaw(value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s %w", key, err)
|
||||
}
|
||||
if !keep {
|
||||
continue
|
||||
}
|
||||
if next == nil {
|
||||
delete(raw, key)
|
||||
continue
|
||||
}
|
||||
raw[key] = next
|
||||
}
|
||||
if value, ok := raw["categoryRanks"]; ok {
|
||||
next, keep, err := normalizeCategoryRanksRaw(value, raw["categoryId"])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !keep {
|
||||
delete(raw, "categoryRanks")
|
||||
} else {
|
||||
raw["categoryRanks"] = next
|
||||
}
|
||||
}
|
||||
return json.Marshal(raw)
|
||||
}
|
||||
|
||||
func normalizeTimeRaw(raw json.RawMessage) (json.RawMessage, error) {
|
||||
raw = bytes.TrimSpace(raw)
|
||||
if len(raw) == 0 || bytes.Equal(raw, []byte("null")) {
|
||||
return raw, nil
|
||||
}
|
||||
var text string
|
||||
if err := json.Unmarshal(raw, &text); err != nil {
|
||||
return nil, fmt.Errorf("时间格式错误")
|
||||
}
|
||||
text = strings.TrimSpace(text)
|
||||
if text == "" {
|
||||
return nil, nil
|
||||
}
|
||||
parsed, err := parseFlexTime(text)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(parsed)
|
||||
}
|
||||
|
||||
func parseFlexTime(text string) (time.Time, error) {
|
||||
if parsed, err := time.Parse(time.RFC3339Nano, text); err == nil {
|
||||
return parsed, nil
|
||||
}
|
||||
if parsed, err := time.Parse(time.RFC3339, text); err == nil {
|
||||
return parsed, nil
|
||||
}
|
||||
for _, layout := range []string{"2006-01-02 15:04:05", "2006-01-02T15:04:05", "2006-01-02"} {
|
||||
if parsed, err := time.ParseInLocation(layout, text, time.Local); err == nil {
|
||||
return parsed, nil
|
||||
}
|
||||
}
|
||||
return time.Time{}, fmt.Errorf("时间格式错误: %s", text)
|
||||
}
|
||||
|
||||
// normalizeIntRaw 把数字或数字字符串整理成 JSON 数字。空字符串表示删除该字段。
|
||||
func normalizeIntRaw(raw json.RawMessage) (json.RawMessage, bool, error) {
|
||||
raw = bytes.TrimSpace(raw)
|
||||
if len(raw) == 0 {
|
||||
return nil, false, nil
|
||||
}
|
||||
if bytes.Equal(raw, []byte("null")) {
|
||||
return raw, true, nil
|
||||
}
|
||||
if raw[0] != '"' {
|
||||
var number json.Number
|
||||
if err := json.Unmarshal(raw, &number); err != nil {
|
||||
return nil, false, fmt.Errorf("数字格式错误")
|
||||
}
|
||||
if _, err := number.Int64(); err != nil {
|
||||
return nil, false, fmt.Errorf("数字格式错误")
|
||||
}
|
||||
return raw, true, nil
|
||||
}
|
||||
var text string
|
||||
if err := json.Unmarshal(raw, &text); err != nil {
|
||||
return nil, false, fmt.Errorf("数字格式错误")
|
||||
}
|
||||
text = strings.TrimSpace(text)
|
||||
if text == "" {
|
||||
return nil, true, nil
|
||||
}
|
||||
value, err := strconv.ParseInt(text, 10, 64)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("数字格式错误: %s", text)
|
||||
}
|
||||
encoded, err := json.Marshal(value)
|
||||
return encoded, true, err
|
||||
}
|
||||
|
||||
func normalizeIntSliceRaw(raw json.RawMessage) (json.RawMessage, bool, error) {
|
||||
raw = bytes.TrimSpace(raw)
|
||||
if len(raw) == 0 || bytes.Equal(raw, []byte("null")) {
|
||||
return raw, true, nil
|
||||
}
|
||||
if bytes.Equal(raw, []byte(`""`)) {
|
||||
return nil, true, nil
|
||||
}
|
||||
if raw[0] != '[' {
|
||||
return nil, false, fmt.Errorf("数组格式错误")
|
||||
}
|
||||
var items []json.RawMessage
|
||||
if err := json.Unmarshal(raw, &items); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
values := make([]int64, 0, len(items))
|
||||
for _, item := range items {
|
||||
next, keep, err := normalizeIntRaw(item)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if !keep || next == nil || bytes.Equal(bytes.TrimSpace(next), []byte("null")) {
|
||||
return nil, false, fmt.Errorf("数字格式错误")
|
||||
}
|
||||
var value int64
|
||||
if err := json.Unmarshal(next, &value); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
values = append(values, value)
|
||||
}
|
||||
encoded, err := json.Marshal(values)
|
||||
return encoded, true, err
|
||||
}
|
||||
|
||||
func normalizeCategoryRanksRaw(raw, categoryIDRaw json.RawMessage) (json.RawMessage, bool, error) {
|
||||
raw = bytes.TrimSpace(raw)
|
||||
if len(raw) == 0 || bytes.Equal(raw, []byte("null")) || bytes.Equal(raw, []byte(`""`)) {
|
||||
return nil, false, nil
|
||||
}
|
||||
fallbackID := int64FromRaw(categoryIDRaw)
|
||||
if raw[0] != '[' {
|
||||
item, ok, err := normalizeCategoryRankItem(raw, fallbackID)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if !ok {
|
||||
return nil, false, nil
|
||||
}
|
||||
encoded, err := json.Marshal([]categoryRankJSON{item})
|
||||
return encoded, true, err
|
||||
}
|
||||
var items []json.RawMessage
|
||||
if err := json.Unmarshal(raw, &items); err != nil {
|
||||
return nil, false, fmt.Errorf("分类排序格式错误")
|
||||
}
|
||||
if len(items) == 0 {
|
||||
encoded, err := json.Marshal([]categoryRankJSON{})
|
||||
return encoded, true, err
|
||||
}
|
||||
cleaned := make([]categoryRankJSON, 0, len(items))
|
||||
for _, item := range items {
|
||||
rank, ok, err := normalizeCategoryRankItem(item, fallbackID)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
cleaned = append(cleaned, rank)
|
||||
}
|
||||
if len(cleaned) == 0 {
|
||||
return nil, false, nil
|
||||
}
|
||||
encoded, err := json.Marshal(cleaned)
|
||||
return encoded, true, err
|
||||
}
|
||||
|
||||
type categoryRankJSON struct {
|
||||
Id int64 `json:"id"`
|
||||
Rank int64 `json:"rank"`
|
||||
}
|
||||
|
||||
func normalizeCategoryRankItem(raw json.RawMessage, fallbackID int64) (categoryRankJSON, bool, error) {
|
||||
raw = bytes.TrimSpace(raw)
|
||||
if len(raw) == 0 || bytes.Equal(raw, []byte("null")) {
|
||||
return categoryRankJSON{}, false, nil
|
||||
}
|
||||
if raw[0] != '{' {
|
||||
if bytes.Equal(raw, []byte("null")) || bytes.Equal(raw, []byte(`""`)) || rawIsZero(raw) {
|
||||
return categoryRankJSON{}, false, nil
|
||||
}
|
||||
rank := int64FromRaw(raw)
|
||||
if rank == 0 {
|
||||
return categoryRankJSON{}, false, fmt.Errorf("分类排序格式错误")
|
||||
}
|
||||
if fallbackID <= 0 {
|
||||
return categoryRankJSON{}, false, fmt.Errorf("分类排序缺少分类ID")
|
||||
}
|
||||
return categoryRankJSON{Id: fallbackID, Rank: rank}, true, nil
|
||||
}
|
||||
var fields map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &fields); err != nil {
|
||||
return categoryRankJSON{}, false, fmt.Errorf("分类排序格式错误")
|
||||
}
|
||||
id := int64FromRaw(fields["id"])
|
||||
if id <= 0 {
|
||||
id = int64FromRaw(fields["categoryId"])
|
||||
}
|
||||
rank := int64FromRaw(fields["rank"])
|
||||
if id <= 0 && !rawHasValue(fields["rank"]) {
|
||||
return categoryRankJSON{}, false, nil
|
||||
}
|
||||
if id <= 0 {
|
||||
id = fallbackID
|
||||
}
|
||||
if id <= 0 {
|
||||
return categoryRankJSON{}, false, fmt.Errorf("分类排序缺少分类ID")
|
||||
}
|
||||
return categoryRankJSON{Id: id, Rank: rank}, true, nil
|
||||
}
|
||||
|
||||
func rawHasValue(raw json.RawMessage) bool {
|
||||
raw = bytes.TrimSpace(raw)
|
||||
return len(raw) > 0 && !bytes.Equal(raw, []byte("null")) && !bytes.Equal(raw, []byte(`""`))
|
||||
}
|
||||
|
||||
func int64FromRaw(raw json.RawMessage) int64 {
|
||||
next, keep, err := normalizeIntRaw(raw)
|
||||
if err != nil || !keep || next == nil || bytes.Equal(bytes.TrimSpace(next), []byte("null")) {
|
||||
return 0
|
||||
}
|
||||
var value int64
|
||||
if err := json.Unmarshal(next, &value); err != nil {
|
||||
return 0
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func rawIsZero(raw json.RawMessage) bool {
|
||||
next, keep, err := normalizeIntRaw(raw)
|
||||
return err == nil && keep && bytes.Equal(bytes.TrimSpace(next), []byte("0"))
|
||||
}
|
||||
@@ -0,0 +1,992 @@
|
||||
package srv_videoinfo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"time"
|
||||
"videoweb/pub/common/godef"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/playletcache"
|
||||
"videoweb/pub/common/slugx"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/actress"
|
||||
"videoweb/pub/models/contenttagmod"
|
||||
"videoweb/pub/models/videocategorymod"
|
||||
"videoweb/pub/models/videocollectionmediamod"
|
||||
"videoweb/pub/models/videocreatermod"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
"videoweb/pub/mongos"
|
||||
"videoweb/pub/pubdef"
|
||||
"videoweb/pub/storage"
|
||||
"videoweb/pub/storage/proxy"
|
||||
"videoweb/web/service/srv_contenttag"
|
||||
"videoweb/web/service/srv_report"
|
||||
"videoweb/web/service/srv_seopush"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// GetVideoInfos 按照参数搜索APP
|
||||
func GetVideoInfos(ctx context.Context, param *VideoInfoReq) (*VideoInfoResp, *srvdef.SrvErr) {
|
||||
filter := bson.M{}
|
||||
if param.MediaType != nil {
|
||||
filter[videoinformationmod.FN.VideoInformation.MediaType] = *param.MediaType
|
||||
}
|
||||
if param.Id != nil && *param.Id > 0 {
|
||||
filter[videoinformationmod.FN.VideoInformation.Id] = *param.Id
|
||||
}
|
||||
if param.Title != nil && len(*param.Title) > 0 {
|
||||
filter[videoinformationmod.FN.VideoInformation.Title] = bson.M{"$regex": *param.Title}
|
||||
}
|
||||
if param.Bango != nil && *param.Bango != "" {
|
||||
filter[videoinformationmod.FN.VideoInformation.Bango] = bson.M{"$regex": *param.Bango}
|
||||
}
|
||||
if param.CreatorId != nil && *param.CreatorId > 0 {
|
||||
filter[videoinformationmod.FN.VideoInformation.CreatorId] = *param.CreatorId
|
||||
}
|
||||
if param.Status != nil {
|
||||
filter[videoinformationmod.FN.VideoInformation.Status] = *param.Status
|
||||
}
|
||||
if param.Tag != nil && *param.Tag > 0 {
|
||||
filter[videoinformationmod.FN.VideoInformation.Tags] = bson.M{"$in": []int64{*param.Tag}}
|
||||
}
|
||||
if param.IsHot != nil {
|
||||
filter[videoinformationmod.FN.VideoInformation.IsHot] = *param.IsHot
|
||||
}
|
||||
if param.IsRecommend != nil {
|
||||
filter[videoinformationmod.FN.VideoInformation.IsRecommend] = *param.IsRecommend
|
||||
}
|
||||
if param.CreatorName != nil && len(*param.CreatorName) > 0 {
|
||||
filter[videoinformationmod.FN.VideoInformation.CreatorName] = bson.M{"$regex": *param.CreatorName}
|
||||
}
|
||||
if param.Keyworld != nil && len(*param.Keyworld) > 0 {
|
||||
filter[videoinformationmod.FN.VideoInformation.Keyworld] = bson.M{"$in": []string{*param.Keyworld}}
|
||||
}
|
||||
if param.VideoTaskId != nil && len(*param.VideoTaskId) > 0 {
|
||||
filter[videoinformationmod.FN.VideoInformation.Video.TaskId] = *param.VideoTaskId
|
||||
}
|
||||
if param.VideoFileId != nil && len(*param.VideoFileId) > 0 {
|
||||
filter[videoinformationmod.FN.VideoInformation.Video.FileId] = *param.VideoFileId
|
||||
}
|
||||
if param.CategoryId != nil && *param.CategoryId > 0 {
|
||||
filter[videoinformationmod.FN.VideoInformation.CategoryId] = *param.CategoryId
|
||||
}
|
||||
if param.IsTop != nil {
|
||||
filter[videoinformationmod.FN.VideoInformation.IsTop] = *param.IsTop
|
||||
}
|
||||
if param.ActressIds != nil && len(*param.ActressIds) > 0 {
|
||||
filter[videoinformationmod.FN.VideoInformation.ActressIds] = bson.M{"$in": *param.ActressIds}
|
||||
}
|
||||
if param.StartTime != nil && param.EndTime != nil {
|
||||
start := utils.GetDayBeginByTime(*param.StartTime)
|
||||
end := utils.GetDayEndByTime(*param.EndTime)
|
||||
if end.Unix() > start.Unix() {
|
||||
filter[videoinformationmod.FN.VideoInformation.CreatedAt] = bson.M{
|
||||
"$gte": start, "$lte": end,
|
||||
}
|
||||
}
|
||||
}
|
||||
if param.OptionAccount != nil {
|
||||
filter[videoinformationmod.FN.VideoInformation.OptionAccount] = *param.OptionAccount
|
||||
}
|
||||
opt := &options.FindOptions{}
|
||||
resp := &VideoInfoResp{
|
||||
Total: 0,
|
||||
List: make(videoinformationmod.VideoInformationList, 0),
|
||||
}
|
||||
|
||||
opt.SetSort(bson.M{videoinformationmod.FN.VideoInformation.Id: -1})
|
||||
switch param.Sort {
|
||||
case 0:
|
||||
opt.SetSort(bson.M{videoinformationmod.FN.VideoInformation.CreatedAt: -1})
|
||||
case 1:
|
||||
opt.SetSort(bson.M{videoinformationmod.FN.VideoInformation.AddedTime: -1})
|
||||
case 2:
|
||||
opt.SetSort(bson.M{videoinformationmod.FN.VideoInformation.UpdatedAt: -1})
|
||||
case 3:
|
||||
opt.SetSort(bson.D{
|
||||
{videoinformationmod.FN.VideoInformation.Sort, -1},
|
||||
{videoinformationmod.FN.VideoInformation.AddedTime, -1},
|
||||
})
|
||||
case 4:
|
||||
opt.SetSort(bson.D{
|
||||
{videoinformationmod.FN.VideoInformation.Real.WatchTimes, -1},
|
||||
{videoinformationmod.FN.VideoInformation.AddedTime, -1},
|
||||
})
|
||||
}
|
||||
opt.SetSkip((param.PageNum - 1) * param.PageSize).SetLimit(param.PageSize)
|
||||
|
||||
total, serr := videoinformationmod.CountVideoInformations(ctx, filter)
|
||||
if serr != nil {
|
||||
return resp, serr
|
||||
}
|
||||
resp.Total = total
|
||||
if total == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
var ads videoinformationmod.VideoInformationList
|
||||
if param.Sort == 3 && param.CategoryId != nil && *param.CategoryId > 0 {
|
||||
ads, serr = videoinformationmod.FindManyVideoByCategoryRank(ctx, filter, *param.CategoryId, (param.PageNum-1)*param.PageSize, param.PageSize, nil)
|
||||
} else {
|
||||
ads, serr = videoinformationmod.FindManyVideoInformations(ctx, filter, opt)
|
||||
}
|
||||
if serr != nil {
|
||||
return resp, serr
|
||||
}
|
||||
resp.List = ads
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// SetVideoRecommend 单/批量设置视频是否推荐
|
||||
func SetVideoRecommend(ctx context.Context, req *SetRecommendReq) *srvdef.SrvErr {
|
||||
if req == nil || len(req.Ids) == 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "ids不能为空")
|
||||
}
|
||||
ids, err := normalizeFakeFlowIds(req.Ids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = videoinformationmod.UpdateManyVideoInformation(ctx, bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Id: bson.M{"$in": ids},
|
||||
}, bson.M{
|
||||
"$set": bson.M{
|
||||
videoinformationmod.FN.VideoInformation.IsRecommend: req.IsRecommend,
|
||||
videoinformationmod.FN.VideoInformation.UpdatedAt: time.Now(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
playletcache.ClearHomeCache()
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetVideoFakeLikes 单/批量随机设置视频假点赞数
|
||||
func SetVideoFakeLikes(ctx context.Context, req *SetFakeLikesReq) (*SetFakeCountResp, *srvdef.SrvErr) {
|
||||
if req == nil || len(req.Ids) == 0 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "ids不能为空")
|
||||
}
|
||||
if req.LikesMin < 0 || req.LikesMax < req.LikesMin {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "点赞数区间无效")
|
||||
}
|
||||
ids, serr := normalizeFakeFlowIds(req.Ids)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
return setVideoFakeField(ctx, ids, videoinformationmod.FN.VideoInformation.Fake.Likes, req.LikesMin, req.LikesMax)
|
||||
}
|
||||
|
||||
// SetVideoFakeWatchTimes 单/批量随机设置视频假观看次数
|
||||
func SetVideoFakeWatchTimes(ctx context.Context, req *SetFakeWatchTimesReq) (*SetFakeCountResp, *srvdef.SrvErr) {
|
||||
if req == nil || len(req.Ids) == 0 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "ids不能为空")
|
||||
}
|
||||
if req.WatchTimesMin < 0 || req.WatchTimesMax < req.WatchTimesMin {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "观看次数区间无效")
|
||||
}
|
||||
ids, err := normalizeFakeFlowIds(req.Ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return setVideoFakeField(ctx, ids, videoinformationmod.FN.VideoInformation.Fake.WatchTimes, req.WatchTimesMin, req.WatchTimesMax)
|
||||
}
|
||||
|
||||
// SetVideoHeat 单/批量随机设置视频热度
|
||||
func SetVideoHeat(ctx context.Context, req *SetVideoHeatReq) (*SetFakeCountResp, *srvdef.SrvErr) {
|
||||
if req == nil || len(req.Ids) == 0 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "ids不能为空")
|
||||
}
|
||||
if req.HeatMin < 0 || req.HeatMax < req.HeatMin {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "热度区间无效")
|
||||
}
|
||||
ids, err := normalizeFakeFlowIds(req.Ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return setVideoFakeField(ctx, ids, videoinformationmod.FN.VideoInformation.HeatValue, req.HeatMin, req.HeatMax)
|
||||
}
|
||||
|
||||
func setVideoFakeField(ctx context.Context, ids []int64, field string, min, max int64) (*SetFakeCountResp, *srvdef.SrvErr) {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
docs := make([]mongo.WriteModel, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
docs = append(docs, mongo.NewUpdateOneModel().
|
||||
SetFilter(bson.M{videoinformationmod.FN.VideoInformation.Id: id}).
|
||||
SetUpdate(bson.M{"$set": bson.M{field: randomFakeFlowValue(r, min, max)}}))
|
||||
}
|
||||
result, err := videoinformationmod.BulkWriteVideoInformation(ctx, docs, options.BulkWrite().SetOrdered(false))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.MatchedCount == 0 {
|
||||
return nil, srvdef.New(srvdef.ErrNoData, "", "")
|
||||
}
|
||||
playletcache.ClearHomeCache()
|
||||
return &SetFakeCountResp{MatchedCount: result.MatchedCount, ModifiedCount: result.ModifiedCount}, nil
|
||||
}
|
||||
|
||||
func normalizeFakeFlowIds(rawIds []int64) ([]int64, *srvdef.SrvErr) {
|
||||
ids := make([]int64, 0, len(rawIds))
|
||||
seen := make(map[int64]struct{}, len(rawIds))
|
||||
for _, id := range rawIds {
|
||||
if id <= 0 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "ids包含无效id")
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "ids不能为空")
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
const (
|
||||
defaultFakeFlowMin = int64(1000)
|
||||
defaultFakeFlowMax = int64(10000)
|
||||
)
|
||||
|
||||
func randomDefaultFakeFlowPair(r *rand.Rand) (watchTimes, likes int64) {
|
||||
if r == nil {
|
||||
r = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
}
|
||||
likes = randomFakeFlowValue(r, defaultFakeFlowMin, defaultFakeFlowMax-1)
|
||||
watchTimes = randomFakeFlowValue(r, likes+1, defaultFakeFlowMax)
|
||||
return watchTimes, likes
|
||||
}
|
||||
|
||||
func applyDefaultFakeFlow(fake *pubdef.FakeFlow, r *rand.Rand) {
|
||||
if fake == nil {
|
||||
return
|
||||
}
|
||||
fake.WatchTimes, fake.Likes = randomDefaultFakeFlowPair(r)
|
||||
}
|
||||
|
||||
func defaultFakeFlowSetDoc(r *rand.Rand) bson.M {
|
||||
watchTimes, likes := randomDefaultFakeFlowPair(r)
|
||||
return bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Fake.WatchTimes: watchTimes,
|
||||
videoinformationmod.FN.VideoInformation.Fake.Likes: likes,
|
||||
}
|
||||
}
|
||||
|
||||
const fillZeroFakeFlowBatchSize = 500
|
||||
|
||||
func zeroFakeFlowFilter() bson.M {
|
||||
return bson.M{
|
||||
"$or": []bson.M{
|
||||
{
|
||||
"$expr": bson.M{
|
||||
"$lt": []interface{}{
|
||||
bson.M{"$add": []interface{}{"$real.watchTimes", "$fake.watchTimes"}},
|
||||
defaultFakeFlowMin,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"$expr": bson.M{
|
||||
"$lt": []interface{}{
|
||||
bson.M{"$add": []interface{}{"$real.likes", "$fake.likes"}},
|
||||
defaultFakeFlowMin,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// FillZeroVideoFakeFlow 为播放量或收藏总量小于 1000 的视频随机设置 fake 播放量和收藏数(1000-10000,播放量大于收藏量)。
|
||||
func FillZeroVideoFakeFlow(ctx context.Context) (*SetFakeCountResp, *srvdef.SrvErr) {
|
||||
filter := zeroFakeFlowFilter()
|
||||
opt := options.Find().
|
||||
SetProjection(bson.M{videoinformationmod.FN.VideoInformation.Id: 1}).
|
||||
SetBatchSize(fillZeroFakeFlowBatchSize).
|
||||
SetLimit(fillZeroFakeFlowBatchSize)
|
||||
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
resp := &SetFakeCountResp{}
|
||||
var lastID int64
|
||||
|
||||
for {
|
||||
pageFilter := filter
|
||||
if lastID > 0 {
|
||||
pageFilter = bson.M{
|
||||
"$and": []bson.M{
|
||||
filter,
|
||||
{videoinformationmod.FN.VideoInformation.Id: bson.M{"$gt": lastID}},
|
||||
},
|
||||
}
|
||||
}
|
||||
findOpt := opt.SetSort(bson.M{videoinformationmod.FN.VideoInformation.Id: 1})
|
||||
list, serr := videoinformationmod.FindManyVideoInformations(ctx, pageFilter, findOpt)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
if len(list) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
docs := make([]mongo.WriteModel, 0, len(list))
|
||||
for _, item := range list {
|
||||
if item == nil || item.Id <= 0 {
|
||||
continue
|
||||
}
|
||||
lastID = item.Id
|
||||
resp.MatchedCount++
|
||||
docs = append(docs, mongo.NewUpdateOneModel().
|
||||
SetFilter(bson.M{videoinformationmod.FN.VideoInformation.Id: item.Id}).
|
||||
SetUpdate(bson.M{"$set": defaultFakeFlowSetDoc(r)}))
|
||||
}
|
||||
if len(docs) == 0 {
|
||||
break
|
||||
}
|
||||
result, err := videoinformationmod.BulkWriteVideoInformation(ctx, docs, options.BulkWrite().SetOrdered(false))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.ModifiedCount += result.ModifiedCount
|
||||
if len(list) < fillZeroFakeFlowBatchSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
if resp.ModifiedCount > 0 {
|
||||
playletcache.ClearHomeCache()
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func randomFakeFlowValue(r *rand.Rand, min, max int64) int64 {
|
||||
if min == max {
|
||||
return min
|
||||
}
|
||||
return min + r.Int63n(max-min+1)
|
||||
}
|
||||
|
||||
// SyncVideoInfo 同步视频
|
||||
func SyncVideoInfo(ctx context.Context,
|
||||
req *AddVideoReq) *srvdef.SrvErr {
|
||||
|
||||
ids := make([]string, 0)
|
||||
for _, id := range req.FileIds {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
queryMediaReq := &proxy.MediaQueryesReq{
|
||||
Ids: ids,
|
||||
}
|
||||
resp, err := storage.QueryMedias(ctx, queryMediaReq, "")
|
||||
if err != nil || resp.Code != http.StatusOK {
|
||||
if resp != nil {
|
||||
log.Warn("AddVideoInfo.QueryMedias failed", log.Any("filter", req.FileIds), log.E(err), log.Any("respCode", resp.Code))
|
||||
} else {
|
||||
log.Warn("AddVideoInfo.QueryMedias failed", log.Any("filter", req.FileIds), log.E(err))
|
||||
}
|
||||
return srvdef.New(srvdef.ErrMediaNotExsit, err.Error(), err.Error())
|
||||
}
|
||||
if len(resp.Data) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, media := range resp.Data {
|
||||
se := SyncOneVideo(ctx, media)
|
||||
if se != nil {
|
||||
log.Warn("SyncVideoInfo SYncOneVideo failed", log.Any("filter", req.FileIds), log.E(se))
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mediaFileDomain(sourceVersion string) string {
|
||||
if sourceVersion == "v1" {
|
||||
return proxy.BucketVideoAddressV1
|
||||
}
|
||||
return proxy.BucketVideoAddressV2
|
||||
}
|
||||
|
||||
func buildMediaRespFromSyncItem(item SyncMediaFileItem) (proxy.MediaResp, error) {
|
||||
oid, err := primitive.ObjectIDFromHex(item.Id)
|
||||
if err != nil {
|
||||
return proxy.MediaResp{}, err
|
||||
}
|
||||
return proxy.MediaResp{
|
||||
Id: oid,
|
||||
MediaSourceId: oid,
|
||||
MediaType: 2,
|
||||
Title: item.Title,
|
||||
FieldNameFs: item.FieldNameFs,
|
||||
FirstTag: item.FirstTag,
|
||||
SecondTags: item.SecondTags,
|
||||
Keyworld: item.Keyworld,
|
||||
VideoTime: item.VideoTime,
|
||||
CoverImg: item.CoverImg,
|
||||
Publish: item.Publish,
|
||||
Bango: item.Bango,
|
||||
TaskID: item.TaskID,
|
||||
Width: item.Width,
|
||||
Height: item.Height,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SyncVideoFromMediaFile 从媒资库直同步到视频列表(不经过切片任务)。
|
||||
func SyncVideoFromMediaFile(ctx context.Context, req *SyncMediaFileReq) (*SyncMediaFileResp, *srvdef.SrvErr) {
|
||||
result := &SyncMediaFileResp{}
|
||||
if req == nil {
|
||||
return result, nil
|
||||
}
|
||||
minVideoSeconds := req.MinVideoSeconds
|
||||
|
||||
syncOne := func(id string, media proxy.MediaResp) {
|
||||
if media.Id.IsZero() {
|
||||
result.Failed++
|
||||
return
|
||||
}
|
||||
if minVideoSeconds > 0 && media.VideoTime > 0 && media.VideoTime <= minVideoSeconds {
|
||||
log.Info("SyncVideoFromMediaFile skip short video",
|
||||
log.Any("id", id),
|
||||
log.Any("videoTime", media.VideoTime))
|
||||
result.Failed++
|
||||
return
|
||||
}
|
||||
if len(media.FieldNameFs) == 0 {
|
||||
log.Warn("SyncVideoFromMediaFile empty fieldNameFs", log.Any("id", id))
|
||||
result.Failed++
|
||||
return
|
||||
}
|
||||
media.MediaSourceId = media.Id
|
||||
media.MediaType = 2
|
||||
if req.FirstTagMap != nil {
|
||||
if firstTag, ok := req.FirstTagMap[id]; ok && len(firstTag) > 0 {
|
||||
media.FirstTag = firstTag
|
||||
}
|
||||
}
|
||||
if se := SyncOneVideo(ctx, media); se != nil {
|
||||
log.Warn("SyncVideoFromMediaFile SyncOneVideo failed", log.Any("id", id), log.E(se))
|
||||
result.Failed++
|
||||
return
|
||||
}
|
||||
result.Synced++
|
||||
}
|
||||
|
||||
if len(req.VideoList) > 0 {
|
||||
for _, item := range req.VideoList {
|
||||
media, err := buildMediaRespFromSyncItem(item)
|
||||
if err != nil {
|
||||
log.Warn("SyncVideoFromMediaFile invalid item", log.Any("id", item.Id), log.E(err))
|
||||
result.Failed++
|
||||
continue
|
||||
}
|
||||
syncOne(item.Id, media)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
domain := mediaFileDomain(req.SourceVersion)
|
||||
for _, id := range req.FileIds {
|
||||
if len(id) == 0 {
|
||||
continue
|
||||
}
|
||||
resp, err := storage.QueryMedia(ctx, &proxy.MediaQueryeReq{Id: id}, domain)
|
||||
if err != nil || resp == nil || resp.Code != proxy.Success {
|
||||
if resp != nil {
|
||||
log.Warn("SyncVideoFromMediaFile QueryMedia failed", log.Any("id", id), log.E(err), log.Any("respCode", resp.Code))
|
||||
} else {
|
||||
log.Warn("SyncVideoFromMediaFile QueryMedia failed", log.Any("id", id), log.E(err))
|
||||
}
|
||||
result.Failed++
|
||||
continue
|
||||
}
|
||||
media := resp.Data
|
||||
if media.Id.IsZero() {
|
||||
oid, parseErr := primitive.ObjectIDFromHex(id)
|
||||
if parseErr != nil {
|
||||
log.Warn("SyncVideoFromMediaFile invalid id", log.Any("id", id), log.E(parseErr))
|
||||
result.Failed++
|
||||
continue
|
||||
}
|
||||
media.Id = oid
|
||||
}
|
||||
syncOne(id, media)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DelVideoInfo 删除指定APP
|
||||
func DelVideoInfo(ctx context.Context, id int64) *srvdef.SrvErr {
|
||||
info, err1 := videoinformationmod.FindOneVideoInformation(ctx, bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Id: id,
|
||||
}, options.FindOne().SetProjection(bson.D{
|
||||
{Key: videoinformationmod.FN.VideoInformation.Id, Value: 1},
|
||||
{Key: videoinformationmod.FN.VideoInformation.Tags, Value: 1},
|
||||
}))
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
adFilter := bson.M{videoinformationmod.FN.VideoInformation.Id: id}
|
||||
//删除
|
||||
count, serr := videoinformationmod.DelOneVideoInformation(ctx, adFilter)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
if count == 0 {
|
||||
return srvdef.New(srvdef.ErrNoData, "", "")
|
||||
}
|
||||
playletcache.ClearHomeCache()
|
||||
// 删除后检查一下合集数据
|
||||
collectionInfos, _ := videocollectionmediamod.FindManyVideoCollectionMediaModels(ctx, bson.M{
|
||||
videocollectionmediamod.FN.VideoCollectionMediaModel.VideoId: id,
|
||||
})
|
||||
if len(collectionInfos) > 0 {
|
||||
collectionIds := make([]int64, 0)
|
||||
for _, collectionInfo := range collectionInfos {
|
||||
collectionIds = append(collectionIds, collectionInfo.CollectionID)
|
||||
}
|
||||
if len(collectionIds) > 0 {
|
||||
_ = videoinformationmod.UpdateManyVideoInformation(ctx, bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Id: bson.M{"$in": collectionInfos},
|
||||
}, bson.M{"$inc": bson.M{
|
||||
videoinformationmod.FN.VideoInformation.VideoCount: -1,
|
||||
}})
|
||||
}
|
||||
}
|
||||
_, _ = videocollectionmediamod.DelManyVideoCollectionMediaModel(ctx, bson.M{
|
||||
videocollectionmediamod.FN.VideoCollectionMediaModel.VideoId: id,
|
||||
})
|
||||
if len(info.Tags) > 0 {
|
||||
oldTagIds := append([]int64{}, info.Tags...)
|
||||
godef.Go(func() {
|
||||
_ = srv_contenttag.SyncContentTagBindCount(context.Background(), oldTagIds, nil, contenttagmod.ContentTagTypeVideo)
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateVideoInfo 更新指定APP
|
||||
func UpdateVideoInfo(ctx context.Context, id, userId int64, param VideoInfoUpdate) *srvdef.SrvErr {
|
||||
now := time.Now()
|
||||
info, serr := videoinformationmod.FindOneVideoInformation(ctx, bson.M{videoinformationmod.FN.VideoInformation.Id: id})
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
param.UpdatedAt = &now
|
||||
param.SecondUpdatedAt = &now
|
||||
if param.CreatorId != nil {
|
||||
name := ""
|
||||
if *param.CreatorId == 0 {
|
||||
param.CreatorName = &name
|
||||
} else {
|
||||
// 查询作者
|
||||
c, serr := videocreatermod.FindOneVideoCreaterInfo(ctx, bson.M{
|
||||
videocreatermod.FN.VideoCreaterInfo.Id: *param.CreatorId,
|
||||
})
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
name = c.Name
|
||||
param.CreatorName = &name
|
||||
}
|
||||
}
|
||||
if param.Tags != nil {
|
||||
tagIds, err1 := srv_contenttag.NormalizeContentTagIds(*param.Tags)
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
if err1 := srv_contenttag.ValidateContentTags(ctx, tagIds, contenttagmod.ContentTagTypeVideo); err1 != nil {
|
||||
return err1
|
||||
}
|
||||
param.Tags = &tagIds
|
||||
}
|
||||
if param.CategoryRanks != nil {
|
||||
seen := make(map[int64]bool, len(*param.CategoryRanks))
|
||||
for _, rank := range *param.CategoryRanks {
|
||||
if rank.Id <= 0 || seen[rank.Id] {
|
||||
return srvdef.New(srvdef.ErrParamError, "分类ID无效或重复", "分类ID无效或重复")
|
||||
}
|
||||
seen[rank.Id] = true
|
||||
if _, err := videocategorymod.FindOneVideoCategory(ctx, bson.M{videocategorymod.FN.VideoCategory.Id: rank.Id}); err != nil {
|
||||
if err.Code == srvdef.ErrNoData {
|
||||
return srvdef.New(srvdef.ErrParamError, "视频分类不存在", "视频分类不存在")
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
isChangeActress := checkActressChange(*info, param)
|
||||
isChangeTag := checkTagChange(*info, param)
|
||||
if param.ActressIds != nil && len(*param.ActressIds) > 0 {
|
||||
acts, serr := actress.FindManyActresss(ctx, bson.M{
|
||||
actress.FN.Actress.Id: bson.M{"$in": *param.ActressIds},
|
||||
})
|
||||
if serr != nil {
|
||||
log.Warn("UpdateVideoInfo FindManyActresss failed", log.Any("filter", param.ActressIds), log.E(serr))
|
||||
return serr
|
||||
}
|
||||
acIds := make([]int64, 0)
|
||||
for _, a := range acts {
|
||||
acIds = append(acIds, a.Id)
|
||||
}
|
||||
param.ActressIds = &acIds
|
||||
}
|
||||
|
||||
if param.Nodes != nil && len(*param.Nodes) > 0 {
|
||||
nodes := *param.Nodes
|
||||
for i, node := range nodes {
|
||||
if len(node.NodeId) == 0 {
|
||||
node.NodeId = primitive.NewObjectID().Hex()
|
||||
nodes[i] = node
|
||||
}
|
||||
}
|
||||
param.Nodes = &nodes
|
||||
}
|
||||
if param.Cover != nil && len(*param.Cover) > 0 {
|
||||
if len(info.Video.Url) > 0 {
|
||||
info.Video.Cover = *param.Cover
|
||||
param.Video = &info.Video
|
||||
}
|
||||
}
|
||||
if param.Status != nil && *param.Status != info.Status {
|
||||
isChangeActress = true
|
||||
tempActressIds := make([]int64, 0)
|
||||
if param.ActressIds != nil {
|
||||
tempActressIds = *param.ActressIds
|
||||
}
|
||||
go updateActressWorks(info.ActressIds, tempActressIds)
|
||||
}
|
||||
title := info.Title
|
||||
if param.Title != nil {
|
||||
title = *param.Title
|
||||
}
|
||||
keyworld := info.Keyworld
|
||||
if param.Keyworld != nil {
|
||||
keyworld = *param.Keyworld
|
||||
}
|
||||
exists := func(s string) bool {
|
||||
doc, _ := videoinformationmod.FindOneVideoInformation(ctx, slugx.Filter(
|
||||
videoinformationmod.FN.VideoInformation.Slug, videoinformationmod.FN.VideoInformation.Id, s, id))
|
||||
return doc != nil
|
||||
}
|
||||
slug, changed, slugErr := slugx.PrepareOnUpdate(info.Slug, param.Slug,
|
||||
utils.SlugSourceFromKeywordsOrTitle(keyworld, title), exists)
|
||||
if slugErr != nil {
|
||||
return slugErr
|
||||
}
|
||||
if changed {
|
||||
param.Slug = &slug
|
||||
} else {
|
||||
param.Slug = nil
|
||||
}
|
||||
adFilter := bson.M{videoinformationmod.FN.VideoInformation.Id: id}
|
||||
param.IndexNow = nil
|
||||
isPublished := param.Status != nil &&
|
||||
info.Status != *param.Status &&
|
||||
*param.Status == pubdef.InformationStatusVerifyOK
|
||||
if isPublished || (param.IsReIndexNow != nil && *param.IsReIndexNow) {
|
||||
indexNow := int64(0)
|
||||
param.IndexNow = &indexNow
|
||||
}
|
||||
serr = videoinformationmod.UpdateOneVideoInformation(ctx, adFilter, bson.M{"$set": param})
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
playletcache.ClearHomeCache()
|
||||
if isChangeActress {
|
||||
tempActressIds := make([]int64, 0)
|
||||
if param.ActressIds != nil {
|
||||
tempActressIds = *param.ActressIds
|
||||
}
|
||||
go updateActressWorks(info.ActressIds, tempActressIds)
|
||||
}
|
||||
if isChangeTag {
|
||||
tempIds := make([]int64, 0)
|
||||
if param.Tags != nil {
|
||||
tempIds = *param.Tags
|
||||
}
|
||||
oldTagIds := append([]int64{}, info.Tags...)
|
||||
newTagIds := append([]int64{}, tempIds...)
|
||||
godef.Go(func() {
|
||||
_ = srv_contenttag.SyncContentTagBindCount(context.Background(), oldTagIds, newTagIds, contenttagmod.ContentTagTypeVideo)
|
||||
})
|
||||
}
|
||||
if param.Status != nil && info.Status != *param.Status {
|
||||
if flag := srv_report.VideoStatusFlag(*param.Status); flag != 0 {
|
||||
mediaCopy := *info
|
||||
mediaCopy.Status = *param.Status
|
||||
godef.Go(func() {
|
||||
srv_report.ReportVideoStatusChangeEvent(&mediaCopy, flag, userId)
|
||||
})
|
||||
}
|
||||
}
|
||||
newStatus := info.Status
|
||||
if param.Status != nil {
|
||||
newStatus = *param.Status
|
||||
}
|
||||
srv_seopush.NotifyInfoAfterWrite(ctx, srv_seopush.KindVideo, id, info.Status, newStatus)
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkActressChange(info videoinformationmod.VideoInformation, updateParam VideoInfoUpdate) bool {
|
||||
if updateParam.ActressIds != nil {
|
||||
if len(info.ActressIds) != len(*updateParam.ActressIds) {
|
||||
return true
|
||||
}
|
||||
actressIds := *updateParam.ActressIds
|
||||
for i, v := range info.ActressIds {
|
||||
if v != actressIds[i] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
func checkTagChange(info videoinformationmod.VideoInformation, updateParam VideoInfoUpdate) bool {
|
||||
if updateParam.Tags != nil {
|
||||
if len(info.Tags) != len(*updateParam.Tags) {
|
||||
return true
|
||||
}
|
||||
tags := *updateParam.Tags
|
||||
for i, v := range info.Tags {
|
||||
if v != tags[i] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func updateActressWorks(oldIds []int64, newIds []int64) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Error("updateActressWorks failed", log.Any("err", err))
|
||||
}
|
||||
}()
|
||||
mapIds := make(map[int64]struct{})
|
||||
if oldIds == nil {
|
||||
oldIds = make([]int64, 0)
|
||||
}
|
||||
if newIds == nil {
|
||||
newIds = make([]int64, 0)
|
||||
}
|
||||
for _, id := range newIds {
|
||||
mapIds[id] = struct{}{}
|
||||
}
|
||||
for _, id := range oldIds {
|
||||
mapIds[id] = struct{}{}
|
||||
}
|
||||
ids := make([]int64, 0)
|
||||
for id := range mapIds {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
ctx := context.Background()
|
||||
for _, id := range ids {
|
||||
count, err := videoinformationmod.CountVideoInformations(ctx, bson.M{videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK, videoinformationmod.FN.VideoInformation.ActressIds: bson.M{"$in": []int64{id}}})
|
||||
if err == nil {
|
||||
_ = actress.UpdateOneActress(ctx, bson.M{
|
||||
actress.FN.Actress.Id: id,
|
||||
}, bson.M{
|
||||
"$set": bson.M{
|
||||
actress.FN.Actress.Works: count,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func AddVideoCollection(ctx context.Context, param *AddVideoCollectionReq) *srvdef.SrvErr {
|
||||
tagIds, err1 := srv_contenttag.NormalizeContentTagIds(param.Tags)
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
if err1 := srv_contenttag.ValidateContentTags(ctx, tagIds, contenttagmod.ContentTagTypeVideo); err1 != nil {
|
||||
return err1
|
||||
}
|
||||
info := videoinformationmod.VideoInformation{
|
||||
Id: mongos.Lid.VideoInfo.Get(),
|
||||
Title: param.Title,
|
||||
MediaType: videoinformationmod.MediaType_Collection,
|
||||
AddedTime: time.Now(),
|
||||
Keyworld: param.Keyworld,
|
||||
FirstTag: param.FirstTag,
|
||||
SecondTags: param.SecondTags,
|
||||
CoverImg: param.CoverImg,
|
||||
Desc: param.Desc,
|
||||
Remark: param.Remark,
|
||||
Nodes: make([]videoinformationmod.Node, 0),
|
||||
Status: pubdef.InformationStatusWaiting,
|
||||
Video: videoinformationmod.VideoInfo{},
|
||||
Tags: tagIds,
|
||||
Real: pubdef.Flow{},
|
||||
VideoCount: 0,
|
||||
UpdatedAt: time.Now(),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if err1 := videoinformationmod.InsertOneVideoInformation(ctx, &info); err1 != nil {
|
||||
return err1
|
||||
}
|
||||
return srv_contenttag.SyncContentTagBindCount(ctx, nil, tagIds, contenttagmod.ContentTagTypeVideo)
|
||||
}
|
||||
|
||||
// AddVideoTags 批量追加视频统一标签
|
||||
func AddVideoTags(ctx context.Context, req *VideoAddTagsReq) *srvdef.SrvErr {
|
||||
if req == nil || len(req.VideoIds) == 0 || len(req.Tags) == 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "视频ID和标签ID不能为空", "")
|
||||
}
|
||||
videoIds, err := normalizeFakeFlowIds(req.VideoIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tagIds, err := srv_contenttag.NormalizeContentTagIds(req.Tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(tagIds) == 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "标签ID不能为空", "")
|
||||
}
|
||||
if err := srv_contenttag.ValidateContentTags(ctx, tagIds, contenttagmod.ContentTagTypeVideo); err != nil {
|
||||
return err
|
||||
}
|
||||
list, err := videoinformationmod.FindManyVideoInformations(ctx, bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Id: bson.M{"$in": videoIds},
|
||||
}, options.Find().SetProjection(bson.D{
|
||||
{Key: videoinformationmod.FN.VideoInformation.Id, Value: 1},
|
||||
{Key: videoinformationmod.FN.VideoInformation.Tags, Value: 1},
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return srvdef.New(srvdef.ErrNoData, "", "")
|
||||
}
|
||||
now := time.Now()
|
||||
docs := make([]mongo.WriteModel, 0, len(list))
|
||||
changes := make([]srv_contenttag.ContentTagBindChange, 0, len(list))
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
newTagIds, changed := appendMissingVideoTagIds(item.Tags, tagIds)
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
docs = append(docs, mongo.NewUpdateOneModel().
|
||||
SetFilter(bson.M{videoinformationmod.FN.VideoInformation.Id: item.Id}).
|
||||
SetUpdate(bson.M{"$set": bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Tags: newTagIds,
|
||||
videoinformationmod.FN.VideoInformation.UpdatedAt: now,
|
||||
}}))
|
||||
changes = append(changes, srv_contenttag.ContentTagBindChange{
|
||||
OldTagIds: append([]int64{}, item.Tags...),
|
||||
NewTagIds: newTagIds,
|
||||
})
|
||||
}
|
||||
if len(docs) == 0 {
|
||||
return nil
|
||||
}
|
||||
if _, err := videoinformationmod.BulkWriteVideoInformation(ctx, docs, options.BulkWrite().SetOrdered(false)); err != nil {
|
||||
return err
|
||||
}
|
||||
playletcache.ClearHomeCache()
|
||||
return srv_contenttag.SyncContentTagBindCountBatch(ctx, changes, contenttagmod.ContentTagTypeVideo)
|
||||
}
|
||||
|
||||
// SetVideoTags 单/批量覆盖设置视频统一标签,并同步新标签表视频绑定数量
|
||||
func SetVideoTags(ctx context.Context, req *VideoSetTagsReq) *srvdef.SrvErr {
|
||||
if req == nil || len(req.Ids) == 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "视频ID不能为空", "")
|
||||
}
|
||||
if req.TagIds == nil {
|
||||
return srvdef.New(srvdef.ErrParamError, "标签ID不能为空", "")
|
||||
}
|
||||
videoIds, err := normalizeFakeFlowIds(req.Ids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tagIds, err := srv_contenttag.NormalizeContentTagIds(*req.TagIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := srv_contenttag.ValidateContentTags(ctx, tagIds, contenttagmod.ContentTagTypeVideo); err != nil {
|
||||
return err
|
||||
}
|
||||
list, err := videoinformationmod.FindManyVideoInformations(ctx, bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Id: bson.M{"$in": videoIds},
|
||||
}, options.Find().SetProjection(bson.D{
|
||||
{Key: videoinformationmod.FN.VideoInformation.Id, Value: 1},
|
||||
{Key: videoinformationmod.FN.VideoInformation.Tags, Value: 1},
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return srvdef.New(srvdef.ErrNoData, "", "")
|
||||
}
|
||||
now := time.Now()
|
||||
docs := make([]mongo.WriteModel, 0, len(list))
|
||||
changes := make([]srv_contenttag.ContentTagBindChange, 0, len(list))
|
||||
for _, item := range list {
|
||||
if item == nil || sameVideoTagIds(item.Tags, tagIds) {
|
||||
continue
|
||||
}
|
||||
docs = append(docs, mongo.NewUpdateOneModel().
|
||||
SetFilter(bson.M{videoinformationmod.FN.VideoInformation.Id: item.Id}).
|
||||
SetUpdate(bson.M{"$set": bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Tags: tagIds,
|
||||
videoinformationmod.FN.VideoInformation.UpdatedAt: now,
|
||||
}}))
|
||||
changes = append(changes, srv_contenttag.ContentTagBindChange{
|
||||
OldTagIds: append([]int64{}, item.Tags...),
|
||||
NewTagIds: append([]int64{}, tagIds...),
|
||||
})
|
||||
}
|
||||
if len(docs) == 0 {
|
||||
return nil
|
||||
}
|
||||
if _, err := videoinformationmod.BulkWriteVideoInformation(ctx, docs, options.BulkWrite().SetOrdered(false)); err != nil {
|
||||
return err
|
||||
}
|
||||
playletcache.ClearHomeCache()
|
||||
return srv_contenttag.SyncContentTagBindCountBatch(ctx, changes, contenttagmod.ContentTagTypeVideo)
|
||||
}
|
||||
|
||||
func sameVideoTagIds(current, target []int64) bool {
|
||||
if len(current) != len(target) {
|
||||
return false
|
||||
}
|
||||
for i := range current {
|
||||
if current[i] != target[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func appendMissingVideoTagIds(current, addIds []int64) ([]int64, bool) {
|
||||
result := append([]int64{}, current...)
|
||||
tagMap := make(map[int64]struct{}, len(current)+len(addIds))
|
||||
for _, tagId := range current {
|
||||
if tagId > 0 {
|
||||
tagMap[tagId] = struct{}{}
|
||||
}
|
||||
}
|
||||
changed := false
|
||||
for _, tagId := range addIds {
|
||||
if _, ok := tagMap[tagId]; ok {
|
||||
continue
|
||||
}
|
||||
tagMap[tagId] = struct{}{}
|
||||
result = append(result, tagId)
|
||||
changed = true
|
||||
}
|
||||
return result, changed
|
||||
}
|
||||
Reference in New Issue
Block a user