package mediacontentser import ( "91porn-server/common/log" "91porn-server/common/stderr" "91porn-server/models/v/mediamod" "91porn-server/models/v/vidmod" "91porn-server/web/service/updownloadser" "errors" "fmt" "path/filepath" "strings" "time" "go.mongodb.org/mongo-driver/mongo/options" "91porn-server/models/v/mediacontentmod" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" ) type WebListReq struct { ID *string `form:"id" bson:"id,omitempty"` // ID Name *string `form:"name" bson:"name,omitempty"` // 名称 Type *int `form:"type" bson:"type,omitempty"` // 类型 1:动漫,2:漫画 Status *int `form:"status" bson:"status,omitempty"` // 状态 IsActive *bool `form:"isActive" json:"isActive"` // 是否激活 MID *string `form:"mId" json:"mId" bson:"mId" binding:"required"` // 媒体资源ID PageNumber uint64 `form:"pageNumber" json:"pageNumber" binding:"required,min=1"` // 当前页 PageSize uint64 `form:"pageSize" json:"pageSize" binding:"required,min=1,max=100"` // 每页条数 } type WebListRes struct { Total int64 `json:"total"` List []*mediacontentmod.MediaContent `json:"list"` } func (q *WebListReq) Filter() bson.M { var query = bson.M{} if q.Name != nil { query["name"] = primitive.Regex{Pattern: *q.Name, Options: "i"} } if q.Status != nil { query["status"] = *q.Status } if q.MID != nil { mid, _ := primitive.ObjectIDFromHex(*q.MID) query["mediaId"] = mid } if q.Type != nil { query["type"] = *q.Type } if q.IsActive != nil { query["isActive"] = *q.IsActive } if q.ID != nil { id, _ := primitive.ObjectIDFromHex(*q.ID) query["_id"] = id } query["isDelete"] = false return query } func (q *WebListReq) Options() *options.FindOptions { return options.Find().SetSkip(int64((q.PageNumber - 1) * q.PageSize)).SetLimit(int64(q.PageSize)).SetSort(bson.M{"episodeNumber": 1}) } // GetList 获取列表 func (q *WebListReq) GetList() (res *WebListRes, err error) { var out = &WebListRes{} count, err := mediacontentmod.QueryAllCount(q.Filter()) if err != nil { return nil, err } if count == 0 { out.List = []*mediacontentmod.MediaContent{} out.Total = 0 return out, nil } out.Total = count out.List, err = mediacontentmod.QueryAllList(q.Filter(), q.Options()) if err != nil { log.Error(fmt.Sprintf("mediacontentmod queryAllList error:%+v:", err)) return nil, err } return out, nil } type WebCreateReq struct { MediaID primitive.ObjectID `json:"mediaId" form:"mediaId" binding:"required"` // 媒体资源ID MediaType string `json:"mediaType" form:"mediaType" binding:"required"` // 媒体类型 EpisodeNumber uint `json:"episodeNumber" form:"episodeNumber" ` // 第几集 ListenPermission int `json:"listenPermission" form:"listenPermission" ` // 收听权限 0:会员 1:金币购买 2:免费 Price int64 `json:"price" form:"price" ` // 购买价格 Cover string `json:"cover" form:"cover"` //封面 Name string `json:"name" form:"name" ` // 章节名 Text string `json:"text" form:"text" ` // 内容(小说专用) Md5 string `json:"md5" form:"md5" ` // 媒体md5 SourceID string `json:"sourceID" form:"sourceID"` // 视频唯一id,对应文件服 VideoUrl string `json:"videoUrl" form:"videoUrl" ` // 视频地址 AudioUrl string `json:"audioUrl" form:"audioUrl"` // 有声小说地址 UrlSet []string `json:"urlSet" form:"urlSet"` // 地址集(多个资源地址用 例如漫画) Filename string `json:"filename" form:"filename"` // 文件名称 IsActive bool `json:"isActive" form:"isActive" ` // 是否激活 PreviewEnabled bool `json:"previewEnabled" form:"previewEnabled"` // 是否允许试看 PreviewStart int `json:"previewStart" form:"previewStart"` // 试看起始时间(秒) PreviewSeconds int `json:"previewSeconds" form:"previewSeconds"` // 试看时长(秒) } const defaultDramaPreviewSeconds = 60 func normalizeDramaPreview(enabled bool, start, seconds int, playTime uint) (bool, int, int, error) { if !enabled { return false, 0, 0, nil } if start < 0 { return false, 0, 0, errors.New("试看起始时间不能小于0") } if seconds < 0 { return false, 0, 0, errors.New("试看时长不能小于0") } if seconds == 0 { seconds = defaultDramaPreviewSeconds } if playTime > 0 && uint64(start)+uint64(seconds) > uint64(playTime) { return false, 0, 0, errors.New("试看起始时间与时长之和不能超过视频总时长") } return true, start, seconds, nil } func validateDramaEpisodeState(episodeNumber int, listenPermission int, price int64, name, videoURL, h265URL string, active bool, status int) error { if episodeNumber < 1 { return errors.New("剧集编号必须大于0") } if strings.TrimSpace(name) == "" { return errors.New("剧集名称不能为空") } if listenPermission != 1 && listenPermission != 2 { return errors.New("短剧剧集listenPermission只支持1或2") } if listenPermission == 2 && price != 0 { return errors.New("免费剧集价格必须为0") } if listenPermission == 1 && price <= 0 { return errors.New("金币剧集价格必须大于0") } if active { if status != 1 { return errors.New("视频仍在转码,暂不能上架") } if strings.TrimSpace(videoURL) == "" && strings.TrimSpace(h265URL) == "" { return errors.New("剧集没有播放资源,暂不能上架") } } return nil } func ensureEpisodeNumberUnique(mediaID primitive.ObjectID, episodeNumber int, excludeID primitive.ObjectID) error { filter := bson.M{ "mediaId": mediaID, "episodeNumber": episodeNumber, "isDelete": false, } if !excludeID.IsZero() { filter["_id"] = bson.M{"$ne": excludeID} } count, err := mediacontentmod.QueryAllCount(filter) if err != nil { return err } if count > 0 { return errors.New("剧集编号已存在") } return nil } func fileStatusToEpisodeStatus(status string) int { switch status { case vidmod.ConvertCompleted, vidmod.Completed: return 1 case vidmod.FileUploadError, vidmod.MergeError, vidmod.ConvertError: return 2 default: return 0 } } func mediaMetadataFromFs(id string) (vidmod.AwsPullResp, int, error) { resp, err := updownloadser.PullFileInfo(id) if err != nil { return vidmod.AwsPullResp{}, 0, err } status := fileStatusToEpisodeStatus(resp.Data.Status) return resp.Data, status, nil } // Create 发布数据 func (p *WebCreateReq) Create(manager string) (err error, vid primitive.ObjectID) { now := time.Now() media, err := mediamod.QueryMediaByID(bson.M{"_id": p.MediaID}) if err != nil { return err, primitive.ObjectID{} } if media == nil || media.ID.IsZero() { return errors.New("media info is nll"), primitive.ObjectID{} } if media.IsDelete || p.MediaType != media.MediaType { return errors.New("剧集mediaType与父媒体不一致"), primitive.ObjectID{} } p.Name = strings.TrimSpace(p.Name) if p.MediaType == mediamod.MediaTypeVideo || p.MediaType == mediamod.MediaTypeDrama { vid, err = primitive.ObjectIDFromHex(p.SourceID) if err != nil || vid.IsZero() { return errors.New("sourceID不是有效的资源ID"), primitive.ObjectID{} } } else { vid = primitive.NewObjectID() } if err = ensureEpisodeNumberUnique(media.ID, int(p.EpisodeNumber), primitive.NilObjectID); err != nil { return err, primitive.ObjectID{} } if media.MediaType == mediamod.MediaTypeDrama { if err = validateDramaEpisodeState(int(p.EpisodeNumber), p.ListenPermission, p.Price, p.Name, p.VideoUrl, "", false, 0); err != nil { return err, primitive.ObjectID{} } if strings.TrimSpace(p.VideoUrl) == "" { return errors.New("短剧剧集必须提供videoUrl"), primitive.ObjectID{} } } previewEnabled, previewStart, previewSeconds := false, 0, 0 if media.MediaType == mediamod.MediaTypeDrama { previewEnabled, previewStart, previewSeconds, err = normalizeDramaPreview(p.PreviewEnabled, p.PreviewStart, p.PreviewSeconds, 0) if err != nil { return err, primitive.ObjectID{} } } mc := mediacontentmod.MediaContent{ ID: vid, MediaID: p.MediaID, MediaType: media.MediaType, Cover: p.Cover, EpisodeNumber: int(p.EpisodeNumber), ListenPermission: p.ListenPermission, Price: p.Price, Name: p.Name, IsActive: p.IsActive, IsDelete: false, UpdatedAct: manager, UpdateTime: now, CreatedAt: now, PreviewEnabled: previewEnabled, PreviewStart: previewStart, PreviewSeconds: previewSeconds, } // 视频和短剧都使用视频资源。 if media.MediaType == mediamod.MediaTypeVideo || media.MediaType == mediamod.MediaTypeDrama { mc.Md5 = p.Md5 mc.VideoUrl = strings.TrimSpace(p.VideoUrl) mc.Status = 0 if p.IsActive { metadata, status, metadataErr := mediaMetadataFromFs(vid.Hex()) if metadataErr != nil { return errors.New("文件服务连接失败,暂不能上架剧集"), primitive.ObjectID{} } mc.Status = status mc.PlayTime = metadata.PlayTime mc.Weight = metadata.Width mc.Height = metadata.Height mc.MediaSize = int64(metadata.Size) if metadata.CheckSum != "" { mc.Md5 = metadata.CheckSum } mc.Ratio = metadata.Ratio if mc.Ratio == 0 && mc.Height > 0 { mc.Ratio = float64(mc.Weight) / float64(mc.Height) } if media.MediaType == mediamod.MediaTypeDrama { mc.PreviewEnabled, mc.PreviewStart, mc.PreviewSeconds, err = normalizeDramaPreview(mc.PreviewEnabled, mc.PreviewStart, mc.PreviewSeconds, mc.PlayTime) if err != nil { return err, primitive.ObjectID{} } if err = validateDramaEpisodeState(mc.EpisodeNumber, mc.ListenPermission, mc.Price, mc.Name, mc.VideoUrl, mc.H265Url, true, mc.Status); err != nil { return err, primitive.ObjectID{} } } } } // 图集 if media.MediaType == mediamod.MediaTypeImage { mc.UrlSet = p.UrlSet } // 小说 if media.MediaType == mediamod.MediaTypeText { mc.AudioUrl = p.AudioUrl } // 创建数据 if _, err := mediacontentmod.Insert(nil, mc); err != nil { return err, primitive.ObjectID{} } err = OnMediaContentChange(media.ID, media.MediaType, true, mc.IsActive) if err != nil { return err, primitive.ObjectID{} } return nil, vid } type WebUpdateReq struct { ID primitive.ObjectID `json:"id" binding:"required"` 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"` // 地址集(多个资源地址用 例如漫画) IsActive *bool `json:"isActive"` // 是否激活 PreviewEnabled *bool `json:"previewEnabled"` // 是否允许试看 PreviewStart *int `json:"previewStart"` // 试看起始时间(秒) PreviewSeconds *int `json:"previewSeconds"` // 试看时长(秒) } // Update 更新数据 func (p *WebUpdateReq) Update(manager string) error { mediaContent, err := mediacontentmod.GetInfo(p.ID) if err != nil { return err } if mediaContent.ID.IsZero() { return errors.New("media content is null") } media, err := mediamod.GetInfo(mediaContent.MediaID) if err != nil { return err } next := mediaContent if p.EpisodeNumber != nil { next.EpisodeNumber = *p.EpisodeNumber } if p.ListenPermission != nil { next.ListenPermission = *p.ListenPermission } if p.Price != nil { next.Price = *p.Price } if p.Name != nil { next.Name = strings.TrimSpace(*p.Name) } if p.VideoUrl != nil { next.VideoUrl = strings.TrimSpace(*p.VideoUrl) } if p.H265Url != nil { next.H265Url = strings.TrimSpace(*p.H265Url) } if p.IsActive != nil { next.IsActive = *p.IsActive } if p.PreviewEnabled != nil { next.PreviewEnabled = *p.PreviewEnabled } if p.PreviewStart != nil { next.PreviewStart = *p.PreviewStart } if p.PreviewSeconds != nil { next.PreviewSeconds = *p.PreviewSeconds } var metadata *vidmod.AwsPullResp if next.IsActive && (media.MediaType == mediamod.MediaTypeVideo || media.MediaType == mediamod.MediaTypeDrama) && next.Status != 1 { loaded, status, metadataErr := mediaMetadataFromFs(mediaContent.ID.Hex()) if metadataErr != nil { return errors.New("文件服务连接失败,暂不能上架剧集") } metadata = &loaded next.Status = status next.PlayTime = loaded.PlayTime next.Weight = loaded.Width next.Height = loaded.Height next.MediaSize = int64(loaded.Size) next.Ratio = loaded.Ratio if next.Ratio == 0 && next.Height > 0 { next.Ratio = float64(next.Weight) / float64(next.Height) } } if media.MediaType == mediamod.MediaTypeDrama { next.PreviewEnabled, next.PreviewStart, next.PreviewSeconds, err = normalizeDramaPreview(next.PreviewEnabled, next.PreviewStart, next.PreviewSeconds, next.PlayTime) if err != nil { return err } if err = validateDramaEpisodeState(next.EpisodeNumber, next.ListenPermission, next.Price, next.Name, next.VideoUrl, next.H265Url, next.IsActive, next.Status); err != nil { return err } if err = ensureEpisodeNumberUnique(media.ID, next.EpisodeNumber, mediaContent.ID); err != nil { return err } } data := make(map[string]interface{}) if p.EpisodeNumber != nil { data["episodeNumber"] = *p.EpisodeNumber } if p.ListenPermission != nil { data["listenPermission"] = *p.ListenPermission } if p.Price != nil { data["price"] = *p.Price } if p.Name != nil { data["name"] = next.Name } if p.Cover != nil { data["cover"] = *p.Cover } if p.Text != nil { data["text"] = *p.Text } if p.Md5 != nil { data["md5"] = *p.Md5 } if p.VideoUrl != nil { data["videoUrl"] = next.VideoUrl } if p.H265Url != nil { data["h265Url"] = next.H265Url } if p.AudioUrl != nil { data["audioUrl"] = *p.AudioUrl } if p.UrlSet != nil { data["urlSet"] = *p.UrlSet } if p.IsActive != nil { data["isActive"] = *p.IsActive } if media.MediaType == mediamod.MediaTypeDrama && (p.PreviewEnabled != nil || p.PreviewStart != nil || p.PreviewSeconds != nil) { data["previewEnabled"] = next.PreviewEnabled data["previewStart"] = next.PreviewStart data["previewSeconds"] = next.PreviewSeconds } if metadata != nil { data["playTime"] = metadata.PlayTime data["weight"] = metadata.Width data["height"] = metadata.Height data["mediaSize"] = metadata.Size data["status"] = next.Status data["ratio"] = next.Ratio if metadata.CheckSum != "" { data["md5"] = metadata.CheckSum } } data["updatedAct"] = manager data["updateTime"] = time.Now() if _, err = mediacontentmod.UpdateByID(nil, p.ID, data); err != nil { return err } published := p.IsActive != nil && *p.IsActive && !mediaContent.IsActive err = OnMediaContentChange(media.ID, media.MediaType, false, published) if err != nil { return err } return nil } type WebBatchUpdateReq struct { IDS []string `json:"ids" binding:"required"` IsActive *bool `json:"isActive"` // 是否激活 Price *int64 `json:"price"` // 价格 Permission *int `json:"permission"` // 收听权限 0:会员 1:金币购买 2:免费 FreeEpisode *int `json:"freeEpisode"` // 前几集免费 } func (p *WebBatchUpdateReq) Filter() []primitive.ObjectID { ids, _ := p.ValidatedIDs() return ids } func (p *WebBatchUpdateReq) ValidatedIDs() ([]primitive.ObjectID, error) { if len(p.IDS) == 0 || len(p.IDS) > 100 { return nil, errors.New("ids数量必须在1到100之间") } if p.IsActive == nil && p.Price == nil && p.Permission == nil && p.FreeEpisode == nil { return nil, errors.New("至少需要一个批量更新字段") } if p.FreeEpisode != nil && *p.FreeEpisode < 0 { return nil, errors.New("freeEpisode不能小于0") } ids := make([]primitive.ObjectID, 0, len(p.IDS)) seen := make(map[primitive.ObjectID]struct{}, len(p.IDS)) for _, raw := range p.IDS { id, err := primitive.ObjectIDFromHex(raw) if err != nil || id.IsZero() { return nil, errors.New("ids包含无效的剧集ID") } if _, ok := seen[id]; ok { continue } seen[id] = struct{}{} ids = append(ids, id) } return ids, nil } func (p *WebBatchUpdateReq) applyContentFields(content mediacontentmod.MediaContent) mediacontentmod.MediaContent { if p.IsActive != nil { content.IsActive = *p.IsActive } if p.Price != nil { content.Price = *p.Price } if p.Permission != nil { content.ListenPermission = *p.Permission } return content } // Update 更新数据 func (p *WebBatchUpdateReq) Update(manager string) error { ids, err := p.ValidatedIDs() if err != nil { return err } contents, err := mediacontentmod.QueryAllList(bson.M{ "_id": bson.M{"$in": ids}, "isDelete": false, }, options.Find().SetLimit(int64(len(ids)))) if err != nil { return err } if len(contents) != len(ids) { return errors.New("部分剧集不存在或已删除") } mediaID := contents[0].MediaID mediaType := contents[0].MediaType published := false for _, content := range contents { if content.MediaID != mediaID { return errors.New("批量操作的剧集必须属于同一部短剧") } next := p.applyContentFields(*content) if p.IsActive != nil && *p.IsActive && !content.IsActive { published = true } validateState := p.Price != nil || p.Permission != nil || (p.IsActive != nil && *p.IsActive) if validateState && mediaType == mediamod.MediaTypeDrama { if err = validateDramaEpisodeState(next.EpisodeNumber, next.ListenPermission, next.Price, next.Name, next.VideoUrl, next.H265Url, next.IsActive, next.Status); err != nil { return fmt.Errorf("第%d集:%w", content.EpisodeNumber, err) } } } data := make(map[string]interface{}) if p.IsActive != nil { data["isActive"] = *p.IsActive } if p.Price != nil { data["price"] = *p.Price } if p.Permission != nil { data["listenPermission"] = *p.Permission } data["updatedAct"] = manager data["updateTime"] = time.Now() contentChanged := p.IsActive != nil || p.Price != nil || p.Permission != nil if contentChanged { if _, err := mediacontentmod.UpdateByIDS(nil, ids, data); err != nil { return err } if err = OnMediaContentChange(mediaID, mediaType, false, published); err != nil { return err } } if p.FreeEpisode != nil { if _, err = mediamod.UpdateByID(nil, mediaID, map[string]interface{}{ "freeEpisode": *p.FreeEpisode, "updatedAct": manager, "updateTime": time.Now(), }); err != nil { return err } } return nil } type WebDeleteReq struct { ID primitive.ObjectID `json:"id" binding:"required"` } // Delete 删除数据 func (p *WebDeleteReq) Delete() error { mediaContent, err := mediacontentmod.GetInfo(p.ID) if err != nil { return err } if err := mediacontentmod.DeleteByID(nil, p.ID); err != nil { return err } err = OnMediaContentChange(mediaContent.MediaID, mediaContent.MediaType, false, false) if err != nil { return err } return nil } func OnMediaContentChange(mediaId primitive.ObjectID, mediaType string, contentAdded, published bool) (err error) { // 获取当前最小的章节,重新update defaultContent := mediamod.DefaultContent{} if mediaType == mediamod.MediaTypeImage { contentList, _, _, err := mediacontentmod.GetList(bson.M{"mediaId": mediaId, "isActive": true, "isDelete": false}, 0, 1, bson.D{{Key: "episodeNumber", Value: 1}, {Key: "createdAt", Value: 1}}) if err != nil { return err } if len(contentList) > 0 { defaultContent.Id = contentList[0].ID defaultContent.Name = contentList[0].Name defaultContent.UrlSet = contentList[0].UrlSet defaultContent.Cover = contentList[0].Cover } } // 子集总数 count, err := mediacontentmod.QueryAllCount(bson.M{"mediaId": mediaId, "isActive": true, "isDelete": false}) if err != nil { return err } // 获取子集免费章节总数 freeCount, err := mediacontentmod.QueryAllCount(bson.M{"mediaId": mediaId, "listenPermission": 2, "isActive": true, "isDelete": false}) if err != nil { return err } contentsPrice, err := mediacontentmod.QueryContentsPrice(mediaId) if err != nil { return } updateData := bson.M{ "currentEpisode": count, "contentsPrice": contentsPrice, "freeEpisode": freeCount, "updateTime": time.Now(), "defaultContent": defaultContent, // 默认展示的子集内容,冗余一个字段,减少一次查询 } now := time.Now() if contentAdded { updateData["contentUpdateTime"] = now } if published { updateData["latestPublishedAt"] = now } //修改当前父级的记录 _, err = mediamod.UpdateByID(nil, mediaId, updateData) if err != nil { return err } return nil } // ViewMediaFromFs 查看视频转码状态 func ViewMediaFromFs(id string) (code stderr.Code, data interface{}) { resp, err := updownloadser.PullFileInfo(id) if err != nil { return stderr.ErrConnectToFs, nil } status := fileStatusToEpisodeStatus(resp.Data.Status) ratio := resp.Data.Ratio if ratio == 0 && resp.Data.Height > 0 { ratio = float64(resp.Data.Width) / float64(resp.Data.Height) } set := map[string]interface{}{ "status": status, "playTime": resp.Data.PlayTime, "weight": resp.Data.Width, "height": resp.Data.Height, "mediaSize": int64(resp.Data.Size), "ratio": ratio, "updateTime": time.Now(), } if resp.Data.CheckSum != "" { set["md5"] = resp.Data.CheckSum } objId, _ := primitive.ObjectIDFromHex(id) if _, err = mediacontentmod.UpdateByID(nil, objId, set); err != nil { return stderr.ErrDbUpdateError, nil } back := make(map[string]interface{}, 10) back["playTime"] = resp.Data.PlayTime back["size"] = resp.Data.Size back["width"] = resp.Data.Width back["height"] = resp.Data.Height back["status"] = resp.Data.Status back["ratio"] = ratio fileName := resp.Data.Filename fieldNameFs := resp.Data.FieldNameFs back["fileName"] = filepath.Join(filepath.Dir(fieldNameFs), fileName) switch resp.Data.Status { case vidmod.UploadLoadingToFs: back["statusDesc"] = "正在上传中" case vidmod.UploadCompleted: back["statusDesc"] = "上传完成" case vidmod.FileUploadError: back["statusDesc"] = "上传错误" case vidmod.Merging: back["statusDesc"] = "正在合并中" case vidmod.MergeCompleted: back["statusDesc"] = "合并完成" case vidmod.MergeError: back["statusDesc"] = "合并失败" case vidmod.Converting: back["statusDesc"] = "正在转码中" case vidmod.ConvertError: back["statusDesc"] = "转码失败" case vidmod.ConvertCompleted, vidmod.Completed: back["statusDesc"] = "完成状态" } return stderr.Success, back }