Initial commit

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 13:57:10 +08:00
co-authored by Claude Opus 5
commit 8679200f41
1897 changed files with 257900 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
package mediaService
import "testing"
func TestShouldUpdateH265URL(t *testing.T) {
tests := []struct {
name string
current string
incoming string
want bool
}{
{
name: "empty incoming does not erase existing URL",
current: "https://cdn.example.com/existing.m3u8",
incoming: "",
want: false,
},
{
name: "whitespace incoming does not erase existing URL",
current: "https://cdn.example.com/existing.m3u8",
incoming: " ",
want: false,
},
{
name: "same URL does not update",
current: "https://cdn.example.com/hevc.m3u8",
incoming: " https://cdn.example.com/hevc.m3u8 ",
want: false,
},
{
name: "new non-empty URL updates",
current: "",
incoming: "https://cdn.example.com/hevc.m3u8",
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := shouldUpdateH265URL(tt.current, tt.incoming); got != tt.want {
t.Fatalf("shouldUpdateH265URL() = %v, want %v", got, tt.want)
}
})
}
}
+111
View File
@@ -0,0 +1,111 @@
package mediaService
import (
"91porn-server/common/log"
"91porn-server/common/mediaCenter"
"91porn-server/common/redis"
v10 "91porn-server/common/v10"
"91porn-server/models/v/mediamod"
"91porn-server/models/v/mediatagmod"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"time"
)
type mediaSync struct {
rd *redis.Client
Ms *mediaCenter.MediaCenterService
Worker *worker
}
func NewMediaSync(rd *redis.Client, ms *mediaCenter.MediaCenterService) *mediaSync {
return &mediaSync{
rd: rd,
Ms: ms,
Worker: w, //最多同时N个协程处理任务
}
}
func (m *mediaSync) SyncAll(admin string) {
// 判断当前是否有正在同步的任务,有的话就不执行
key := "MediaSyncAll"
has, err := m.rd.SetNX(key, 1, time.Minute*10)
if err != nil {
log.Error("SyncAll setNx fail", log.E(err))
return
}
if !has {
return
}
// 释放锁
defer m.rd.Del(key)
// 获取所有订阅未完结的动漫
filter := bson.M{
"mediaCenterId": bson.M{"$gt": 0},
"updateStatus": 1, //未完结的状态
"isDelete": false,
}
sort := bson.D{{"_id", -1}, {"createdAt", -1}}
var skip int64
var limit int64 = 500
for {
list, hasNext, err := mediamod.GetList(filter, skip, limit, sort)
if err != nil {
return
}
for _, v := range list {
media := v
m.Worker.Exec(func() {
err = m.SyncMediaData(media)
if err != nil {
log.Error("SyncMediaData fail", log.Any("mediaId", media.ID), log.E(err))
return
}
})
}
skip += limit
if !hasNext {
break
}
}
}
func (m *mediaSync) OnSyncTimeChange(mediaId primitive.ObjectID, syncTime int64) {
if mediaId.IsZero() || syncTime <= 0 {
return
}
// 修改下一次需要同步的时间点位
updataData := make(map[string]interface{})
updataData["syncTime"] = syncTime
updataData["updateTime"] = time.Now()
mediamod.UpdateByID(nil, mediaId, updataData)
}
// HandleTag 处理标签
func HandleTag(tagname, dimension string, dimensionType, kind int) (tagID primitive.ObjectID, suc bool) {
tMod := mediatagmod.MediaTag{}
tagname = v10.ExtractPureChar(tagname)
tagInfo, err := mediatagmod.FindOneTagByFilter(bson.M{
"name": tagname,
"kind": kind,
"dimension": dimension,
"type": dimensionType,
})
if err == nil && !tagInfo.ID.IsZero() {
return tagInfo.ID, true
}
tMod.Name = tagname
tMod.Dimension = dimension
tMod.Kind = kind
tMod.Type = dimensionType
tMod.Active = true
tMod.CreatedAt = time.Now()
tMod.UpdateTime = time.Now()
id, err := mediatagmod.Insert(nil, tMod)
if err != nil {
return primitive.NilObjectID, false
}
return id, true
}
+202
View File
@@ -0,0 +1,202 @@
package mediaService
import (
"91porn-server/common/log"
"91porn-server/common/mediaCenter"
"91porn-server/models/v/mediacontentmod"
"91porn-server/models/v/mediamod"
"fmt"
"strings"
"time"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// SyncMediaData 同步acg动漫
func (m *mediaSync) SyncMediaData(media *mediamod.Media) (err error) {
if media == nil {
return
}
// 加锁,防止多个同步出错
// 判断当前是否有正在同步的任务,有的话就不执行
key := fmt.Sprintf("MediaSync:%v", media.MediaCenterId)
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("mediaId", media.ID), log.Any("mediaCenterId", media.MediaCenterId))
// 同步数据
data, err := m.Ms.MediaSync(&mediaCenter.MediaSyncReq{
Id: media.MediaCenterId,
SyncTime: media.SyncTime,
})
if err != nil {
log.Error("SyncMediaData call mediaSync Fail", log.Any("id", media.MediaCenterId), log.E(err))
return err
}
if data.IsDelete {
// 数据已经删除,不需要同步
log.Info("【ACG同步任务】 数据在媒资库已经删除,不需要同步", log.Any("mediaId", media.ID), log.Any("mediaCenterId", media.MediaCenterId))
return
}
err = m.OnSyncMediaData(media.ID, data.Media)
if err != nil {
log.Error("SyncMediaData call OnSyncMediaData Fail", log.Any("id", media.MediaCenterId), log.E(err))
return err
}
newSyncTime, isAdd, err := m.OnSyncMediaContentData(media, data.MediaContentList)
if err != nil {
return
}
err = OnMediaContentChange(media.ID, media.MediaType, isAdd)
if err != nil {
log.Error("【ACG同步任务】 OnMediaContentChange Fail", log.Any("mediaId", media.ID), log.E(err))
return err
}
// 修改下一次需要同步的时间点位
m.OnSyncTimeChange(media.ID, newSyncTime)
log.Info("【ACG同步任务】完成", log.Any("mediaId", media.ID), log.Any("mediaCenterId", media.MediaCenterId))
return nil
}
func (m *mediaSync) OnSyncMediaContentData(media *mediamod.Media, mediaContentDataList []mediaCenter.MediaContent) (int64, bool, error) {
isAdd := false
syncTime := media.SyncTime
updatedIDs := make([]primitive.ObjectID, 0)
if len(mediaContentDataList) == 0 {
// 已经没有需要同步的数据了,直接返回
return syncTime, false, nil
}
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
}
// TODO 后续同步,只做追加,不对之前的子集数据进行更新,避免数据出错
if err == mediacontentmod.MediaContentNotFound {
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:转码失败, 视频资源使用
UpdatedAct: "系统自动同步", // 操作用户账号
CreatedAt: time.Now(), // 文档创建时间
UpdateTime: time.Now(), // 文档更新时间
IsActive: true, // 同步过来的默认上架
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{}
}
_, err = mediacontentmod.Insert(nil, mediaContent)
if err != nil {
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
}
} else if shouldUpdateH265URL(mediaContent.H265Url, h265URL) {
// 历史子集仍保持只追加策略,仅允许同步 H.265 地址并清理详情缓存。
_, err = mediacontentmod.UpdateByID(nil, mediaContent.ID, map[string]interface{}{
"h265Url": h265URL,
"updateTime": time.Now(),
})
if err != nil {
log.Error("【ACG同步任务】更新H.265地址失败",
log.Any("mediaId", media.ID),
log.Any("mediaContentId", mediaContent.ID),
log.Any("子集ID", mediaContentData.ID),
log.Any("err", err))
return 0, false, err
}
updatedIDs = append(updatedIDs, mediaContent.ID)
}
}
return syncTime, isAdd, nil
}
func normalizeH265URL(raw string) string {
return strings.TrimSpace(raw)
}
func shouldUpdateH265URL(current, incoming string) bool {
incoming = normalizeH265URL(incoming)
return incoming != "" && normalizeH265URL(current) != incoming
}
func (m *mediaSync) OnSyncMediaData(mediaId primitive.ObjectID, mediaData *mediaCenter.Media) (err error) {
//修改数据(不修改这个价格了,避免跟运营自己修改的价格冲突,只做部分内容的修改)
updateData := make(map[string]interface{})
//updateData["title"] = data.Media.Title
//updateData["horizontalCover"] = data.Media.HorizontalCover
//updateData["verticalCover"] = data.Media.VerticalCover
//updateData["summary"] = data.Media.Summary
// TODO 后续更新只 对集数跟连载状态进行修改
updateData["totalEpisode"] = int(mediaData.TotalEpisode)
updateData["updateStatus"] = mediaData.UpdateStatus
if mediaData.UpdateStatus == 0 {
// 直接是已完结
updateData["updateStatus"] = 2
}
//updateData["anchor"] = data.Media.Anchor
updateData["updateTime"] = time.Now()
_, err = mediamod.UpdateByID(nil, mediaId, updateData)
if err != nil {
return
}
return
}
+502
View File
@@ -0,0 +1,502 @@
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
}
+33
View File
@@ -0,0 +1,33 @@
package mediaService
import (
"testing"
"91porn-server/models/v/mediamod"
"go.mongodb.org/mongo-driver/bson/primitive"
)
func TestNormalizeImportedTagClassification(t *testing.T) {
dimension, dimensionType, kind := normalizeImportedTagClassification(5, "画面风格", 1, mediamod.RiFan)
if dimension != "剧场" || dimensionType != 5 || kind != mediamod.Theater {
t.Fatalf("drama classification = (%q, %d, %d), want (%q, %d, %d)", dimension, dimensionType, kind, "剧场", 5, mediamod.Theater)
}
dimension, dimensionType, kind = normalizeImportedTagClassification(1, "画面风格", 1, mediamod.RiFan)
if dimension != "画面风格" || dimensionType != 1 || kind != mediamod.RiFan {
t.Fatalf("animation classification changed to (%q, %d, %d)", dimension, dimensionType, kind)
}
}
func TestPrepareImportedTagIDs(t *testing.T) {
current := []primitive.ObjectID{primitive.NewObjectID()}
if got := prepareImportedTagIDs(5, current); len(got) != 0 {
t.Fatalf("drama retained %d existing tags, want 0", len(got))
}
got := prepareImportedTagIDs(1, current)
if len(got) != 1 || got[0] != current[0] {
t.Fatalf("animation tags = %v, want %v", got, current)
}
}
+59
View File
@@ -0,0 +1,59 @@
package mediaService
import (
"91porn-server/common/log"
"fmt"
"runtime"
"strings"
"sync"
)
type worker struct {
num int // 最大
ch chan struct{}
wg sync.WaitGroup
}
var w *worker
func init() {
num := 30 //最大同时执行的执程,防止同时开启的协程过多,把其他服务弄挂
w = &worker{
num: num,
ch: make(chan struct{}, num),
}
}
func (w *worker) Wait() {
w.wg.Wait()
}
func (w *worker) Exec(f func()) {
// 超出则阻塞
w.ch <- struct{}{}
w.wg.Add(1)
go func() {
defer func() {
<-w.ch
w.wg.Done()
if r := recover(); r != nil {
dep := 0
t := make([]string, 0, 10)
for i := 1; i < 10; i++ {
_, file, line, ok := runtime.Caller(i)
if !ok {
break
}
if strings.Contains(file, "/runtime/") || strings.Contains(file, "/reflect/") {
continue
}
t = append(t, fmt.Sprintf("%s∟%s:%d", strings.Repeat(" ", dep), file, line))
dep++
}
exception := strings.Join(t, "\n")
log.Error("worker exec Panic: ", log.Any("Exception", exception))
}
}()
f()
}()
}