Files
huangguo_server/common/media/useMedia.go
T
rootandClaude Opus 5 8679200f41 Initial commit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 13:57:10 +08:00

503 lines
18 KiB
Go

package mediaService
import (
"91porn-server/common/log"
"91porn-server/common/mediaCenter"
sli "91porn-server/common/slice"
"91porn-server/models/v/mediacontentmod"
"91porn-server/models/v/mediamod"
"91porn-server/models/v/moduleconfmod"
"91porn-server/models/v/modulesectionmod"
"91porn-server/models/v/sensitivewordmod"
"fmt"
"strings"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// UseMediaData 同步acg动漫
func (m *mediaSync) UseMediaData(admin string, id uint, dimension string, dimensionType, kind int, sectionId primitive.ObjectID) (err error) {
// 加锁,防止多个同步出错
// 判断当前是否有正在同步的任务,有的话就不执行
key := fmt.Sprintf("MediaSync:%v", id)
has, err := m.rd.SetNX(key, 1, time.Minute*10)
if err != nil {
log.Error("SyncAll setNx fail", log.E(err))
return
}
if !has {
log.Info("【ACG订阅】该任务已经在进行", log.Any("task", key))
return
}
// 释放锁
defer m.rd.Del(key)
log.Info("【ACG订阅】开始",
log.Any("admin", admin),
log.Any("mediaCenterId", id),
log.Any("dimension", dimension),
log.Any("dimensionType", dimensionType),
log.Any("kind", kind),
log.Any("sectionId", sectionId),
)
var media mediamod.Media
var module moduleconfmod.ModuleConf
var section modulesectionmod.Section
// 获取专题
if !sectionId.IsZero() {
section, _ = modulesectionmod.GetBySectionID(sectionId)
module, _ = moduleconfmod.GetByID(section.SubModuleID)
}
// 同步数据
data, err := m.Ms.MediaSync(&mediaCenter.MediaSyncReq{
Id: id,
SyncTime: 0,
})
if err != nil {
log.Error("【ACG订阅】 call mediaSync Fail", log.Any("id", id), log.E(err))
return err
}
if data.IsDelete {
// 数据已经删除,不需要同步
log.Info("【ACG订阅】 数据在媒资库已经删除,不需要同步", log.Any("mediaId", media.ID), log.Any("mediaCenterId", media.MediaCenterId))
return
}
log.Info("【ACG订阅】获取到media数据",
log.Any("admin", admin),
log.Any("mediaCenterId", id),
log.Any("mediaData", data.Media),
)
media, err = m.OnUseMediaData(admin, module, section, dimension, dimensionType, kind, data.Media)
if err != nil {
log.Error("【ACG订阅】 OnUseMediaData Fail", log.Any("id", id), log.E(err))
return err
}
log.Info("【ACG订阅】处理media数据成功",
log.Any("admin", admin),
log.Any("mediaCenterId", id),
log.Any("media objId", media.ID),
)
if len(data.MediaContentList) == 0 {
// 已经没有需要同步的数据了,直接返回
return
}
newSyncTime, isAdd, err := m.OnUseMediaContentData(admin, media, data.MediaContentList)
if err != nil {
log.Error("【ACG订阅】 OnUseMediaContentData Fail", log.Any("id", id), log.E(err))
return err
}
err = OnMediaContentChange(media.ID, media.MediaType, isAdd)
if err != nil {
log.Error("【ACG订阅】 OnMediaContentChange Fail", log.Any("id", id), log.E(err))
return err
}
m.OnSyncTimeChange(media.ID, newSyncTime)
log.Info("【ACG订阅】完成",
log.Any("admin", admin),
log.Any("mediaCenterId", id),
log.Any("dimension", dimension),
log.Any("dimensionType", dimensionType),
log.Any("kind", kind),
log.Any("sectionId", sectionId),
)
return nil
}
func OnMediaContentChange(mediaId primitive.ObjectID, mediaType string, isAdd 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, // 默认展示的子集内容,冗余一个字段,减少一次查询
}
if isAdd {
now := time.Now()
updateData["contentUpdateTime"] = now
updateData["latestPublishedAt"] = now
}
//修改当前父级的记录
_, err = mediamod.UpdateByID(nil, mediaId, updateData)
if err != nil {
return err
}
return nil
}
func (m *mediaSync) OnUseMediaData(admin string, module moduleconfmod.ModuleConf, section modulesectionmod.Section, dimension string, dimensionType, kind int, mediaData *mediaCenter.Media) (media mediamod.Media, err error) {
if mediaData == nil {
return
}
dimension, dimensionType, kind = normalizeImportedTagClassification(mediaData.MediaType, dimension, dimensionType, kind)
// 查询是否存在
media, err = mediamod.GetInfoByMediaCenterId(mediaData.ID)
if err != nil && err != mediamod.MediaNotFound {
return
}
var exists bool
if err == nil {
exists = true
} else if err == mediamod.MediaNotFound && mediaData.DataVersion == 0 {
exists = false
} else if err == mediamod.MediaNotFound && mediaData.DataVersion > 0 {
// 如果不存在,则需要判断是否之前的存在
// 通过标题获取数据,看看是否存在
oldMedia, err := mediamod.GetInfoByTitle(mediaData.Title)
if err != nil && err != mediamod.MediaNotFound {
return media, err
}
if err == nil {
// 如果之前就存在
media = oldMedia
exists = true
}
} else {
// 数据库查询错误,直接返回
return
}
if !exists {
// 直接写入
media = mediamod.Media{
Title: mediaData.Title,
HorizontalCover: mediaData.HorizontalCover,
VerticalCover: mediaData.VerticalCover,
Summary: mediaData.Summary,
TotalEpisode: int(mediaData.TotalEpisode), // 总集数
UpdateStatus: mediaData.UpdateStatus, // 更新状态
PermissionIconHide: false, // 售卖类型标识隐藏
Permission: 0, // 收听权限 0:会员 1:金币购买 2:免费
Direction: 0, // 排版方向 0横 1竖(冗余字段)
FreeTime: 0, // 免费试看时长 单位 s
Anchor: mediaData.Anchor, // 作者
UpdatedAct: admin, // 操作用户账号
CreatedAt: time.Now(), // 文档创建时间
UpdateTime: time.Now(), // 文档更新时间
MediaCenterId: mediaData.ID, // 在媒资库中的唯一标识
Status: 1, // 默认上架
MID: module.ID, // 模块ID
ModuleName: module.ModuleName, // 模块名称
SID: section.ID,
SectionName: section.SectionName,
}
if kind > 0 {
media.Kind = kind
}
switch mediaData.MediaType {
case 1:
// 动漫
media.MediaType = "video"
case 2:
// 漫画
media.MediaType = "image"
case 3:
// 文本小说
media.MediaType = "text"
case 4:
// 有声小说
media.MediaType = "text"
media.MediaSubType = 1
case 5:
// 短剧
media.MediaType = mediamod.MediaTypeDrama
media.MediaSubType = 0
media.Kind = mediamod.Theater
media.Permission = 1
media.Price = 0
media.Status = 0
}
if media.UpdateStatus == 0 {
// 直接是已完结
media.UpdateStatus = 2
}
if strings.TrimSpace(mediaData.TagsText) != "" && kind > 0 && dimension != "" && dimensionType > 0 {
tagList := strings.Split(mediaData.TagsText, ",")
tagIds := make([]primitive.ObjectID, 0, len(tagList))
for _, tagName := range tagList {
tid, suc := HandleTag(tagName, dimension, dimensionType, kind)
if !suc {
continue
}
tagIds = append(tagIds, tid)
}
media.Tags = tagIds
}
// 敏感词命中:强制下架(Status=0),命中详情写 log,便于管理员从后台日志复查
// 此接口为异步 worker 写库,无法回传 handler,故只 log + 强制状态
terms := sensitivewordmod.LoadEnabledTerms()
titleHits := sensitivewordmod.MatchHits(media.Title, terms)
summaryHits := sensitivewordmod.MatchHits(media.Summary, terms)
if len(titleHits) > 0 || len(summaryHits) > 0 {
media.Status = 0
log.Warn("OnUseMediaData sensitive hit, forced offline",
log.Any("mediaCenterId", mediaData.ID),
log.Any("title", media.Title),
log.Any("detail", sensitivewordmod.FormatHitDetail(titleHits, summaryHits, nil)))
}
objId, err := mediamod.Insert(nil, media)
if err != nil {
return media, err
}
media.ID = objId
return media, nil
}
//修改数据(不修改这个价格了,避免跟运营自己修改的价格冲突,只做部分内容的修改)
updateData := make(map[string]interface{})
updateData["title"] = mediaData.Title
updateData["horizontalCover"] = mediaData.HorizontalCover
updateData["verticalCover"] = mediaData.VerticalCover
updateData["summary"] = mediaData.Summary
updateData["totalEpisode"] = int(mediaData.TotalEpisode)
updateData["updateStatus"] = mediaData.UpdateStatus
if media.UpdateStatus == 0 {
// 直接是已完结
updateData["updateStatus"] = 2
}
updateData["anchor"] = mediaData.Anchor
updateData["mediaCenterId"] = mediaData.ID
updateData["updateTime"] = time.Now()
if kind > 0 {
updateData["kind"] = kind
}
if strings.TrimSpace(mediaData.TagsText) != "" && kind > 0 && dimension != "" && dimensionType > 0 {
tagList := strings.Split(mediaData.TagsText, ",")
tagIds := prepareImportedTagIDs(mediaData.MediaType, media.Tags)
for _, tagName := range tagList {
tid, suc := HandleTag(tagName, dimension, dimensionType, kind)
if !suc {
continue
}
tagIds = append(tagIds, tid)
}
// 去重
updateData["tags"] = sli.RemoveRepObjectID(tagIds)
}
if !section.ID.IsZero() {
updateData["mId"] = module.ID
updateData["moduleName"] = module.ModuleName
updateData["sectionName"] = section.SectionName
updateData["sId"] = section.ID
}
_, err = mediamod.UpdateByID(nil, media.ID, updateData)
if err != nil {
return media, err
}
return
}
func normalizeImportedTagClassification(mediaType int, dimension string, dimensionType, kind int) (string, int, int) {
if mediaType == 5 {
return "剧场", 5, mediamod.Theater
}
return dimension, dimensionType, kind
}
func prepareImportedTagIDs(mediaType int, current []primitive.ObjectID) []primitive.ObjectID {
if mediaType == 5 {
return make([]primitive.ObjectID, 0, len(current))
}
return current
}
func (m *mediaSync) OnUseMediaContentData(admin string, media mediamod.Media, mediaContentDataList []mediaCenter.MediaContent) (int64, bool, error) {
syncTime := media.SyncTime
if len(mediaContentDataList) == 0 {
return syncTime, false, nil
}
isAdd := false
updatedIDs := make([]primitive.ObjectID, 0, len(mediaContentDataList))
log.Info("【ACG订阅】获取到需要同步的子集",
log.Any("mediaId", media.ID),
log.Any("mediaCenterId", media.MediaCenterId),
log.Any("子集数量", len(mediaContentDataList)))
for _, mediaContentData := range mediaContentDataList {
h265URL := normalizeH265URL(mediaContentData.H265Url)
// 更改下一次同步的时间点
if mediaContentData.UpdateTime > syncTime {
syncTime = mediaContentData.UpdateTime
}
log.Info("【ACG订阅】同步子集",
log.Any("mediaId", media.ID),
log.Any("mediaCenterId", media.MediaCenterId),
log.Any("子集ID", mediaContentData.ID))
// 判断是否已经存在该记录
mediaContent, err := mediacontentmod.GetInfoByHashId(media.ID, mediaContentData.HashId)
if err != nil && err != mediacontentmod.MediaContentNotFound {
log.Error("【ACG订阅】同步子集失败",
log.Any("mediaId", media.ID),
log.Any("mediaCenterId", media.MediaCenterId),
log.Any("子集ID", mediaContentData.ID),
log.Any("err", err))
return 0, false, err
}
var exists bool
if err == nil {
exists = true
} else if err == mediacontentmod.MediaContentNotFound && mediaContentData.DataVersion == 0 {
exists = false
} else if err == mediacontentmod.MediaContentNotFound && mediaContentData.DataVersion > 0 {
// 如果不存在,则需要判断是否之前的存在
// 通过标题获取数据,看看是否存在
oldMediaContent, err := mediacontentmod.GetInfoByMediaIdAndName(media.ID, mediaContentData.Title)
if err != nil && err != mediacontentmod.MediaContentNotFound {
log.Error("【ACG订阅】同步子集失败",
log.Any("mediaId", media.ID),
log.Any("mediaCenterId", media.MediaCenterId),
log.Any("子集ID", mediaContentData.ID),
log.Any("err", err))
return 0, false, err
}
if err == nil {
// 如果之前就存在
mediaContent = oldMediaContent
exists = true
}
} else {
// 数据库查询错误,直接返回
log.Error("【ACG订阅】同步子集失败",
log.Any("mediaId", media.ID),
log.Any("mediaCenterId", media.MediaCenterId),
log.Any("子集ID", mediaContentData.ID),
log.Any("err", err))
return 0, false, err
}
if !exists {
isAdd = true
// 不存在则入库
mediaContent = mediacontentmod.MediaContent{
MediaID: media.ID,
MediaType: media.MediaType,
EpisodeNumber: mediaContentData.EpisodeNumber,
ListenPermission: 0, // 收听权限 0:会员 1:金币购买 2:免费
Name: mediaContentData.Title, // 章节名
Text: mediaContentData.Text, // 内容(小说专用)
Author: media.Anchor, // 作者
Height: int(mediaContentData.Height), // 高
Weight: int(mediaContentData.Width), // 宽
MediaSize: int64(mediaContentData.MediaSize), // 资源大小
PlayTime: mediaContentData.PlayTime, // 影片长度
Status: 1, // 状态 0:转码中 1:转码成功 2:转码失败, 视频资源使用
IsActive: false, // 默认不上架
UpdatedAct: admin, // 操作用户账号
CreatedAt: time.Now(), // 文档创建时间
UpdateTime: time.Now(), // 文档更新时间
HashId: mediaContentData.HashId, // 在媒资库中的唯一标识
VideoUrl: mediaContentData.M3u8Url,
H265Url: h265URL,
AudioUrl: mediaContentData.AudioUrl,
}
if media.MediaType == mediamod.MediaTypeDrama {
mediaContent.Price = int64(mediaContentData.SaleCoin)
mediaContent.ListenPermission = 1
if mediaContentData.SaleCoin == 0 {
mediaContent.ListenPermission = 2
}
}
if strings.TrimSpace(mediaContentData.Urls) != "" {
mediaContent.UrlSet = strings.Split(mediaContentData.Urls, ",")
} else {
mediaContent.UrlSet = []string{}
}
insertedID, insertErr := mediacontentmod.Insert(nil, mediaContent)
err = insertErr
if err != nil {
log.Error("OnUseMediaContentData mediacontentmod.Insert fail", log.Any("mediaContent", mediaContent), log.E(err))
log.Error("【ACG订阅】同步子集失败",
log.Any("mediaId", media.ID),
log.Any("mediaCenterId", media.MediaCenterId),
log.Any("子集ID", mediaContentData.ID),
log.Any("err", err))
return 0, false, err
}
updatedIDs = append(updatedIDs, insertedID)
} else {
// 存在则修改
updateData := make(map[string]interface{})
updateData["episodeNumber"] = mediaContentData.EpisodeNumber
updateData["name"] = mediaContentData.Title
updateData["text"] = mediaContentData.Text
if strings.TrimSpace(mediaContentData.Urls) != "" {
updateData["urlSet"] = strings.Split(mediaContentData.Urls, ",")
} else {
updateData["urlSet"] = []string{}
}
updateData["weight"] = int(mediaContentData.Width)
updateData["height"] = int(mediaContentData.Height)
updateData["ratio"] = mediaContentData.Ratio
updateData["mediaSize"] = int64(mediaContentData.MediaSize)
updateData["playTime"] = mediaContentData.PlayTime
updateData["status"] = 1
updateData["hashId"] = mediaContentData.HashId
updateData["videoUrl"] = mediaContentData.M3u8Url
if shouldUpdateH265URL(mediaContent.H265Url, h265URL) {
updateData["h265Url"] = h265URL
}
// 其他的只有有声小说有这个地址
updateData["audioUrl"] = mediaContentData.AudioUrl
if media.MediaType == mediamod.MediaTypeDrama {
updateData["price"] = int64(mediaContentData.SaleCoin)
updateData["listenPermission"] = 1
if mediaContentData.SaleCoin == 0 {
updateData["listenPermission"] = 2
}
}
_, err = mediacontentmod.UpdateByID(nil, mediaContent.ID, updateData)
if err != nil {
log.Error("OnUseMediaContentData mediacontentmod.UpdateByID fail", log.Any("updateData", updateData), log.E(err))
log.Error("【ACG订阅】同步子集失败",
log.Any("mediaId", media.ID),
log.Any("mediaCenterId", media.MediaCenterId),
log.Any("子集ID", mediaContentData.ID),
log.Any("err", err))
return 0, false, err
}
updatedIDs = append(updatedIDs, mediaContent.ID)
}
}
return syncTime, isAdd, nil
}