This commit is contained in:
mubai
2023-12-23 22:32:52 +08:00
parent d85dbe915c
commit b48e53a637
151 changed files with 12451 additions and 1382 deletions

View File

@@ -27,7 +27,11 @@ const (
// ImgCacheFlag 是否开启将主站影片图片放入本地进行存储
ImgCacheFlag = false
ImageDir = "./resource/static/images"
//ImageDir = "./resource/static/images"
FilmPictureUploadDir = "./static/upload/gallery"
FilmPictureUrlPath = "/upload/pic/poster/"
FilmPictureAccess = "/api/upload/pic/poster/"
)
// -------------------------redis key-----------------------------------
@@ -41,7 +45,7 @@ const (
// MovieDetailKey movie detail影视详情信息 可以
MovieDetailKey = "MovieDetail:Cid%d:Id%d"
// MovieBasicInfoKey 影片基本信息, 简略版本
MovieBasicInfoKey = "MovieBasicInfoKey:Cid%d:Id%d"
MovieBasicInfoKey = "MovieBasicInfo:Cid%d:Id%d"
// MultipleSiteDetail 多站点影片信息存储key
MultipleSiteDetail = "MultipleSource:%s"
@@ -49,8 +53,15 @@ const (
// SearchInfoTemp redis暂存检索数据信息
SearchInfoTemp = "Search:SearchInfoTemp"
// SearchTitle 影片分类标题key
SearchTitle = "Search:Pid%d:Title"
SearchTag = "Search:Pid%d:%s"
// SearchTag 影片剧情标签key
SearchTag = "Search:Pid%d:%s"
// VirtualPictureKey 待同步图片临时存储 key
VirtualPictureKey = "VirtualPicture"
// MaxScanCount redis Scan 操作每次扫描的数据量, 每次最多扫描300条数据
MaxScanCount = 300
// SearchCount Search scan 识别范围
SearchCount = 3000
@@ -62,6 +73,27 @@ const (
SearchHeatListKey = "Search:SearchHeatList"
)
const (
AuthUserClaims = "UserClaims"
)
// -------------------------manage 管理后台相关key----------------------------------
const (
// FilmSourceListKey 采集 API 信息列表key
FilmSourceListKey = "Config:Collect:FilmSource"
// ManageConfigExpired 管理配置key 长期有效, 暂定10年
ManageConfigExpired = time.Hour * 24 * 365 * 10
// SiteConfigBasic 网站参数配置
SiteConfigBasic = "SystemConfig:SiteConfig:Basic"
// FilmCrontabKey 定时任务列表信息
FilmCrontabKey = "Cron:Task:Film"
// DefaultUpdateSpec 每20分钟执行一次
DefaultUpdateSpec = "0 */20 * * * ?"
// DefaultUpdateTime 每次采集最近 3 小时内更新的影片
DefaultUpdateTime = 3
)
// -------------------------Web API相关redis key-----------------------------------
const (
// IndexCacheKey , 首页数据缓存
@@ -71,7 +103,10 @@ const (
// -------------------------Database Connection Params-----------------------------------
const (
// SearchTableName 存放检索信息的数据表名
SearchTableName = "search"
SearchTableName = "search"
UserTableName = "users"
UserIdInitialVal = 10000
PictureTableName = "picture"
//mysql服务配置信息 root:root 设置mysql账户的用户名和密码

View File

@@ -0,0 +1,19 @@
package config
import "time"
/*
对外开放API相关配置
*/
const (
// ResourceExpired API所需要的资源有效期
ResourceExpired = time.Hour * 24 * 90
// OriginalFilmDetailKey 采集时原始数据存储key
OriginalFilmDetailKey = "OriginalResource:FilmDetail:Id%d"
FilmClassKey = "OriginalResource:FilmClass"
PlayForm = "gfm3u8"
PlayFormCloud = "gofilm"
PlayFormAll = "gofilm$$$gfmu38"
RssVersion = "5.1"
)

View File

@@ -0,0 +1,24 @@
package config
const PrivateKey = `-----BEGIN RSA PRIVATE KEY-----
MIIBPAIBAAJBANNnshoUaT2gFNrihmFdmC1cBCs1XLFc5Fn3MfNOR3aOGDO0ohXl
bku6Ir/qITN/yeH5pY34WEcETet3YhESpE8CAwEAAQJBAI7Ekdfg/u26RTtJDd2F
WrcPVFVl1TKGfERxl08sB0D9HLvUSBfAEg/UpfWSQ57aSJ9b0gVKmDhgF8FymuUV
v2kCIQDzXXSZ/oeKmqObwad0Fa82IFof3LeZdpbrjyz3w45JDQIhAN5hdmuW+y2w
UgSy0o4zGFsEG/RBZsvVnSSfkdR47dPLAiEA2XbPNLQu5fnc7NeVDLQ7xsAOCJ6w
KR/BKGjeI9/JCxkCIQCjMkU0ec2FXxMhzZXFs2uZR6+4FdL5nZ9ABDaCBekK9wIg
XEfd11qabi9jPrbsOVNZCTk51B7Ug0ZwGyn0BA8Jlo0=
-----END RSA PRIVATE KEY-----
`
const PublicKey = `-----BEGIN RSA PUBLIC KEY-----
MEgCQQDTZ7IaFGk9oBTa4oZhXZgtXAQrNVyxXORZ9zHzTkd2jhgztKIV5W5LuiK/
6iEzf8nh+aWN+FhHBE3rd2IREqRPAgMBAAE=
-----END RSA PUBLIC KEY-----
`
const (
Issuer = "GoFilm"
AuthTokenExpires = 10 * 24 // 单位 h
UserTokenKey = "User:Token:%d"
)

View File

@@ -0,0 +1,217 @@
package controller
import (
"errors"
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"server/logic"
"server/model/system"
"server/plugin/spider"
"strings"
)
// ------------------------------------------------------ 定时任务管理 ------------------------------------------------------
// FilmCronTaskList 获取所有的定时任务信息
func FilmCronTaskList(c *gin.Context) {
tl := logic.CL.GetFilmCrontab()
if len(tl) <= 0 {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "暂无任务定时任务信息",
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": StatusOk,
"data": tl,
})
}
// GetFilmCronTask 通过Id获取对应的定时任务信息
func GetFilmCronTask(c *gin.Context) {
id := c.DefaultQuery("id", "")
if id == "" {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "定时任务信息获取失败,任务Id不能为空",
})
return
}
task, err := logic.CL.GetFilmCrontabById(id)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": fmt.Sprint("定时任务信息获取失败", err.Error()),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": StatusOk,
"data": task,
})
}
// FilmCronAdd 添加定时任务
func FilmCronAdd(c *gin.Context) {
var vo = system.FilmCronVo{}
// 获取请求参数
if err := c.ShouldBindJSON(&vo); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "请求参数异常",
})
return
}
// 校验请求参数
if err := validTaskAddVo(vo); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": err.Error(),
})
return
}
// 去除cron表达式左右空格
vo.Spec = strings.TrimSpace(vo.Spec)
// 执行 定时任务信息保存逻辑
if err := logic.CL.AddFilmCrontab(vo); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": fmt.Sprint("定时任务添加失败: ", err.Error()),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": StatusOk,
"message": "定时任务添加成功",
})
}
// FilmCronUpdate 更新定时任务信息
func FilmCronUpdate(c *gin.Context) {
var t = system.FilmCollectTask{}
// 获取请求参数
if err := c.ShouldBindJSON(&t); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "请求参数异常",
})
return
}
// 校验必要参数
if err := validTaskInfo(t); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": err.Error(),
})
return
}
// 获取未更新的task信息
task, err := logic.CL.GetFilmCrontabById(t.Id)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": fmt.Sprint("更新失败: ", err.Error()),
})
return
}
// 将task的可变更属性进行变更
task.Ids = t.Ids
task.Time = t.Time
task.State = t.State
task.Remark = t.Remark
// 将变更后的task更新到系统中
logic.CL.UpdateFilmCron(task)
c.JSON(http.StatusOK, gin.H{
"status": StatusOk,
"message": fmt.Sprintf("定时任务[%s]更新成功", task.Id),
})
}
// ChangeTaskState 开启 | 关闭Id 对应的定时任务
func ChangeTaskState(c *gin.Context) {
var t = system.FilmCollectTask{}
// 获取请求参数
if err := c.ShouldBindJSON(&t); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "请求参数异常",
})
return
}
// 获取未更新的task信息
task, err := logic.CL.GetFilmCrontabById(t.Id)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": fmt.Sprint("更新失败: ", err.Error()),
})
return
}
// 修改task的状态
task.State = t.State
// 将变更后的task更新到系统中
logic.CL.UpdateFilmCron(task)
c.JSON(http.StatusOK, gin.H{
"status": StatusOk,
"message": fmt.Sprintf("定时任务[%s]更新成功", task.Id),
})
}
// DelFilmCron 删除定时任务
func DelFilmCron(c *gin.Context) {
id := c.DefaultQuery("id", "")
if id == "" {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "删除失败,任务Id不能为空",
})
return
}
// 如果Id不为空则执行删除逻辑
if err := logic.CL.DelFilmCrontab(id); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": StatusOk,
"message": fmt.Sprintf("定时任务[%s]已删除", id),
})
}
// -------------------------------------------------- 参数校验 --------------------------------------------------
// 定时任务必要属性校验
func validTaskInfo(t system.FilmCollectTask) error {
if len(t.Id) <= 0 {
return errors.New("参数校验失败, 任务Id信息不能为空")
}
if t.Time == 0 {
return errors.New("参数校验失败, 采集时长不能为零值")
}
return nil
}
// 任务添加参数校验
func validTaskAddVo(vo system.FilmCronVo) error {
if vo.Model != 0 && vo.Model != 1 {
return errors.New("参数校验失败, 未定义的任务类型")
}
if vo.Time == 0 {
return errors.New("参数校验失败, 采集时长不能为零值")
}
if err := spider.ValidSpec(vo.Spec); err != nil {
return errors.New(fmt.Sprint("参数校验失败 cron表达式校验失败: ", err.Error()))
}
if vo.Model == 1 && (vo.Ids == nil || len(vo.Ids) <= 0) {
return errors.New("参数校验失败, 自定义更新未绑定任何资源站点")
}
return nil
}

View File

@@ -0,0 +1,59 @@
package controller
import (
"fmt"
"github.com/gin-gonic/gin"
"path/filepath"
"server/config"
"server/logic"
"server/model/system"
"server/plugin/common/util"
)
// SingleUpload 单文件上传, 暂定为图片上传
func SingleUpload(c *gin.Context) {
// 获取执行操作的用户信息
v, ok := c.Get(config.AuthUserClaims)
if !ok {
system.Failed("上传失败, 当前用户信息异常", c)
return
}
// 结合搜文件内容
file, err := c.FormFile("file")
if err != nil {
system.Failed(err.Error(), c)
return
}
// 创建文件保存路径, 如果不存在则创建
//if _, err = os.Stat(config.ImageDir); os.IsNotExist(err) {
// err = os.MkdirAll(config.ImageDir, os.ModePerm)
// if err != nil {
// return
// }
//}
// 生成文件名, 保存文件到服务器
fileName := fmt.Sprintf("%s/%s%s", config.FilmPictureUploadDir, util.RandomString(8), filepath.Ext(file.Filename))
err = c.SaveUploadedFile(file, fileName)
if err != nil {
system.Failed(err.Error(), c)
return
}
uc := v.(*system.UserClaims)
// 记录图片信息到系统表中, 并获取返回的图片访问路径
link := logic.FileL.SingleFileUpload(fileName, int(uc.UserID))
// 返回图片访问地址以及成功的响应
system.Success(link, "上传成功", c)
}
// PhotoWall 照片墙数据
func PhotoWall(c *gin.Context) {
// 获取系统保存的文件的图片分页数据
page := system.Page{PageSize: 10, Current: 1}
// 获取分页数据
pl := logic.FileL.GetPhotoPage(&page)
system.Success(pl, "图片分页数据获取成功", c)
}

View File

@@ -0,0 +1,176 @@
package controller
import (
"fmt"
"github.com/gin-gonic/gin"
"server/logic"
"server/model/system"
"strconv"
"time"
)
// FilmSearchPage 获取影视分页数据
func FilmSearchPage(c *gin.Context) {
var s = system.SearchVo{Paging: &system.Page{}}
var err error
// 检索参数
s.Name = c.DefaultQuery("name", "")
s.Pid, err = strconv.ParseInt(c.DefaultQuery("pid", "0"), 10, 64)
if err != nil {
system.Failed("影片分页数据获取失败, 请求参数异常", c)
return
}
s.Cid, err = strconv.ParseInt(c.DefaultQuery("cid", "0"), 10, 64)
if err != nil {
system.Failed("影片分页数据获取失败, 请求参数异常", c)
return
}
s.Plot = c.DefaultQuery("plot", "")
s.Area = c.DefaultQuery("area", "")
s.Language = c.DefaultQuery("language", "")
year := c.DefaultQuery("year", "")
if year == "" {
s.Year = 0
} else {
s.Year, err = strconv.ParseInt(year, 10, 64)
if err != nil {
system.Failed("影片分页数据获取失败, 请求参数异常", c)
return
}
}
s.Remarks = c.DefaultQuery("remarks", "")
// 处理时间参数
begin := c.DefaultQuery("beginTime", "")
if begin == "" {
s.BeginTime = 0
} else {
beginTime, e := time.ParseInLocation(time.DateTime, begin, time.Local)
if e != nil {
system.Failed("影片分页数据获取失败, 请求参数异常", c)
return
}
s.BeginTime = beginTime.Unix()
}
end := c.DefaultQuery("endTime", "")
if end == "" {
s.EndTime = 0
} else {
endTime, e := time.ParseInLocation(time.DateTime, end, time.Local)
if e != nil {
system.Failed("影片分页数据获取失败, 请求参数异常", c)
return
}
s.EndTime = endTime.Unix()
}
// 分页参数
s.Paging.Current, err = strconv.Atoi(c.DefaultQuery("current", "1"))
s.Paging.PageSize, err = strconv.Atoi(c.DefaultQuery("pageSize", "10"))
// 如果分页数据超出指定范围则设置为默认值
if s.Paging.PageSize <= 0 || s.Paging.PageSize > 500 {
s.Paging.PageSize = 10
}
if err != nil {
system.Failed("影片分页数据获取失败, 请求参数异常", c)
return
}
// 提供检索tag options
options := logic.FL.GetSearchOptions()
// 检索条件
sl := logic.FL.GetFilmPage(s)
system.Success(gin.H{
"params": s,
"list": sl,
"options": options,
}, "影片分页信息获取成功", c)
}
// FilmAdd 手动添加影片
func FilmAdd(c *gin.Context) {
// 获取请求参数
var fd = system.FilmDetailVo{}
if err := c.ShouldBindJSON(&fd); err != nil {
system.Failed("影片添加失败, 影片参数提交异常", c)
return
}
// 如果绑定成功则执行影片信息处理保存逻辑
if err := logic.FL.SaveFilmDetail(fd); err != nil {
system.Failed(fmt.Sprint("影片添加失败, 影片信息保存错误: ", err.Error()), c)
return
}
system.SuccessOnlyMsg("影片信息添加成功", c)
}
//----------------------------------------------------影片分类处理----------------------------------------------------
// FilmClassTree 影片分类树数据
func FilmClassTree(c *gin.Context) {
// 获取影片分类树信息
tree := logic.FL.GetFilmClassTree()
system.Success(tree, "影片分类信息获取成功", c)
return
}
// FindFilmClass 获取指定ID对应的影片分类信息
func FindFilmClass(c *gin.Context) {
idStr := c.DefaultQuery("id", "")
if idStr == "" {
system.Failed("影片分类信息获取失败, 分类Id不能为空", c)
return
}
// 转化id类型为int
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
system.Failed("影片分类信息获取失败, 参数分类Id格式异常", c)
return
}
// 通过Id返回对应的分类信息
class := logic.FL.GetFilmClassById(id)
if class == nil {
system.Failed("影片分类信息获取失败, 分类信息不存在", c)
return
}
system.Success(class, "分类信息查找成功", c)
}
func UpdateFilmClass(c *gin.Context) {
// 获取修改后的分类信息
var class = system.CategoryTree{}
if err := c.ShouldBindJSON(&class); err != nil {
system.Failed("更新失败, 请求参数异常", c)
return
}
if class.Id == 0 {
system.Failed("更新失败, 分类Id缺失", c)
return
}
// 修改分类信息
if err := logic.FL.UpdateClass(class); err != nil {
system.Failed(err.Error(), c)
return
}
system.SuccessOnlyMsg("影片分类信息更新成功", c)
}
// DelFilmClass 删除指定ID对应的影片分类
func DelFilmClass(c *gin.Context) {
idStr := c.DefaultQuery("id", "")
if idStr == "" {
system.Failed("影片分类信息获取失败, 分类Id不能为空", c)
return
}
// 转化id类型为int
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
system.Failed("影片分类信息获取失败, 参数分类Id格式异常", c)
return
}
// 通过ID删除对应分类信息
if err = logic.FL.DelClass(id); err != nil {
system.Failed(err.Error(), c)
return
}
system.SuccessOnlyMsg("当前分类已删除成功", c)
}

View File

@@ -4,7 +4,7 @@ import (
"github.com/gin-gonic/gin"
"net/http"
"server/logic"
"server/model"
"server/model/system"
"strconv"
"strings"
)
@@ -26,7 +26,8 @@ func Index(c *gin.Context) {
// CategoriesInfo 分类信息获取
func CategoriesInfo(c *gin.Context) {
data := logic.IL.GetCategoryInfo()
//data := logic.IL.GetCategoryInfo()
data := logic.IL.GetNavCategory()
if data == nil {
c.JSON(http.StatusOK, gin.H{
@@ -55,7 +56,7 @@ func FilmDetail(c *gin.Context) {
// 获取影片详情信息
detail := logic.IL.GetFilmDetail(id)
// 获取相关推荐影片数据
page := model.Page{Current: 0, PageSize: 14}
page := system.Page{Current: 0, PageSize: 14}
relateMovie := logic.IL.RelateMovie(detail, &page)
c.JSON(http.StatusOK, gin.H{
"status": StatusOk,
@@ -82,7 +83,7 @@ func FilmPlayInfo(c *gin.Context) {
// 获取影片详情信息
detail := logic.IL.GetFilmDetail(id)
// 推荐影片信息
page := model.Page{Current: 0, PageSize: 14}
page := system.Page{Current: 0, PageSize: 14}
relateMovie := logic.IL.RelateMovie(detail, &page)
c.JSON(http.StatusOK, gin.H{
"status": StatusOk,
@@ -101,7 +102,7 @@ func SearchFilm(c *gin.Context) {
keyword := c.DefaultQuery("keyword", "")
currStr := c.DefaultQuery("current", "1")
current, _ := strconv.Atoi(currStr)
page := model.Page{PageSize: 10, Current: current}
page := system.Page{PageSize: 10, Current: current}
bl := logic.IL.SearchFilmInfo(strings.TrimSpace(keyword), &page)
c.JSON(http.StatusOK, gin.H{
@@ -115,7 +116,7 @@ func SearchFilm(c *gin.Context) {
// FilmTagSearch 通过tag获取满足条件的对应影片
func FilmTagSearch(c *gin.Context) {
params := model.SearchTagsVO{}
params := system.SearchTagsVO{}
pidStr := c.DefaultQuery("Pid", "")
cidStr := c.DefaultQuery("Category", "")
yStr := c.DefaultQuery("Year", "")
@@ -137,7 +138,7 @@ func FilmTagSearch(c *gin.Context) {
// 设置分页信息
currentStr := c.DefaultQuery("current", "1")
current, _ := strconv.Atoi(currentStr)
page := model.Page{PageSize: 49, Current: current}
page := system.Page{PageSize: 49, Current: current}
logic.IL.GetFilmsByTags(params, &page)
// 获取当前分类Title
// 返回对应信息
@@ -175,7 +176,7 @@ func FilmClassify(c *gin.Context) {
pid, _ := strconv.ParseInt(pidStr, 10, 64)
title := logic.IL.GetPidCategory(pid)
// 2. 设置分页信息
page := model.Page{PageSize: 21, Current: 1}
page := system.Page{PageSize: 21, Current: 1}
// 3. 获取当前分类下的 最新上映, 排行榜, 最近更新 影片信息
c.JSON(http.StatusOK, gin.H{
"status": StatusOk,
@@ -207,7 +208,7 @@ func FilmCategory(c *gin.Context) {
// 2 设置分页信息
currentStr := c.DefaultQuery("current", "1")
current, _ := strconv.Atoi(currentStr)
page := model.Page{PageSize: 49, Current: current}
page := system.Page{PageSize: 49, Current: current}
// 2.1 如果不存在cid则根据Pid进行查询
if cidStr == "" {
// 2.2 如果存在pid则根据pid进行查找

View File

@@ -0,0 +1,374 @@
package controller
import (
"errors"
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"server/logic"
"server/model/system"
"server/plugin/SystemInit"
"server/plugin/common/util"
"server/plugin/spider"
)
func ManageIndex(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": StatusOk,
"message": "hahah",
})
return
}
// ------------------------------------------------------ 影视采集 ------------------------------------------------------
// FilmSourceList 采集站点信息
func FilmSourceList(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": StatusOk,
"data": logic.ML.GetFilmSourceList(),
})
return
}
// FindFilmSource 通过ID返回对应的资源站数据
func FindFilmSource(c *gin.Context) {
id := c.Query("id")
if id == "" {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "参数异常, 资源站标识不能为空",
})
return
}
fs := logic.ML.GetFilmSource(id)
if fs == nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "数据异常,资源站信息不存在",
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": StatusOk,
"data": fs,
})
}
func FilmSourceAdd(c *gin.Context) {
var s = system.FilmSource{}
// 获取请求参数
if err := c.ShouldBindJSON(&s); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "请求参数异常",
})
return
}
// 校验必要参数
if err := validFilmSource(s); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": err.Error(),
})
return
}
// 如果采集站开启图片同步, 且采集站为附属站点则返回错误提示
if s.SyncPictures && (s.Grade == system.SlaveCollect) {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "附属站点无法开启图片同步功能",
})
return
}
// 执行 spider
if err := spider.CollectApiTest(s); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "资源接口测试失败, 请确认接口有效再添加",
})
return
}
// 测试通过后将资源站信息添加到list
if err := logic.ML.SaveFilmSource(s); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": fmt.Sprint("资源站添加失败: ", err.Error()),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": StatusOk,
"message": "添加成功",
})
}
func FilmSourceUpdate(c *gin.Context) {
var s = system.FilmSource{}
// 获取请求参数
if err := c.ShouldBindJSON(&s); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "请求参数异常",
})
return
}
// 校验必要参数
if err := validFilmSource(s); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": err.Error(),
})
return
}
// 如果采集站开启图片同步, 且采集站为附属站点则返回错误提示
if s.SyncPictures && (s.Grade == system.SlaveCollect) {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "附属站点无法开启图片同步功能",
})
return
}
// 校验Id信息是否为空
if s.Id == "" {
c.JSON(http.StatusOK, gin.H{"status": StatusFailed, "message": "参数异常, 资源站标识不能为空"})
return
}
fs := logic.ML.GetFilmSource(s.Id)
if fs == nil {
c.JSON(http.StatusOK, gin.H{"status": StatusFailed, "message": "数据异常,资源站信息不存在"})
return
}
// 如果 uri发生变更则执行spider测试
if fs.Uri != s.Uri {
// 执行 spider
if err := spider.CollectApiTest(s); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "资源接口测试失败, 请确认更新的数据接口是否有效",
})
return
}
}
// 更新资源站信息
if err := logic.ML.UpdateFilmSource(s); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": fmt.Sprint("资源站更新失败: ", err.Error()),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": StatusOk,
"message": "更新成功",
})
}
func FilmSourceChange(c *gin.Context) {
var s = system.FilmSource{}
// 获取请求参数
if err := c.ShouldBindJSON(&s); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "请求参数异常",
})
return
}
if s.Id == "" {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "参数异常, 资源站标识不能为空",
})
return
}
// 查找对应的资源站点信息
fs := logic.ML.GetFilmSource(s.Id)
if fs == nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "数据异常,资源站信息不存在",
})
return
}
// 如果采集站开启图片同步, 且采集站为附属站点则返回错误提示
if s.SyncPictures && (fs.Grade == system.SlaveCollect) {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "附属站点无法开启图片同步功能",
})
return
}
if s.State != fs.State || s.SyncPictures != fs.SyncPictures {
// 执行更新操作
s := system.FilmSource{Id: fs.Id, Name: fs.Name, Uri: fs.Uri, ResultModel: fs.ResultModel,
Grade: fs.Grade, SyncPictures: s.SyncPictures, CollectType: fs.CollectType, State: s.State}
// 更新资源站信息
if err := logic.ML.UpdateFilmSource(s); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": fmt.Sprint("资源站更新失败: ", err.Error()),
})
return
}
}
c.JSON(http.StatusOK, gin.H{
"status": StatusOk,
"message": "更新成功",
})
}
func FilmSourceDel(c *gin.Context) {
id := c.Query("id")
if len(id) <= 0 {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "资源站ID信息不能为空",
})
return
}
if err := logic.ML.DelFilmSource(id); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": fmt.Sprint("删除资源站失败: ", err.Error()),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": StatusOk,
"message": "删除成功:",
})
}
// FilmSourceTest 测试影视站点数据是否可用
func FilmSourceTest(c *gin.Context) {
var s = system.FilmSource{}
// 获取请求参数
if err := c.ShouldBindJSON(&s); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "请求参数异常",
})
return
}
// 校验必要参数
if err := validFilmSource(s); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": err.Error(),
})
return
}
// 执行 spider
if err := spider.CollectApiTest(s); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": StatusOk,
"message": "测试成功!!!",
})
}
// GetNormalFilmSource 获取状态为启用的采集站信息
func GetNormalFilmSource(c *gin.Context) {
// 获取所有的采集站信息
var l []system.FilmTaskOptions
for _, v := range logic.ML.GetFilmSourceList() {
if v.State {
l = append(l, system.FilmTaskOptions{Id: v.Id, Name: v.Name})
}
}
c.JSON(http.StatusOK, gin.H{
"status": StatusOk,
"data": l,
})
}
// ------------------------------------------------------ 站点基本配置 ------------------------------------------------------
// SiteBasicConfig 网站基本配置
func SiteBasicConfig(c *gin.Context) {
system.Success(logic.ML.GetSiteBasicConfig(), "网站基本信息获取成功", c)
}
// UpdateSiteBasic 更新网站配置信息
func UpdateSiteBasic(c *gin.Context) {
// 获取请求参数 && 校验关键配置项
bc := system.BasicConfig{}
if err := c.ShouldBindJSON(&bc); err == nil {
// 对参数进行校验
if !util.ValidDomain(bc.Domain) && !util.ValidIPHost(bc.Domain) {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "域名格式校验失败: ",
})
return
}
if len(bc.SiteName) <= 0 {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "网站名称不能为空: ",
})
return
}
} else {
c.JSON(http.StatusBadRequest, gin.H{"status": StatusOk, "message": fmt.Sprint("参数提交失败: ", err)})
return
}
// 保存更新后的配置信息
if err := logic.ML.UpdateSiteBasic(bc); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": fmt.Sprint("网站配置更新失败: ", err),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": StatusOk,
"message": "更新成功: ",
})
return
}
// ResetSiteBasic 重置网站配置信息为初始化状态
func ResetSiteBasic(c *gin.Context) {
// 执行配置初始化方法直接覆盖当前基本配置信息
SystemInit.BasicConfigInit()
c.JSON(http.StatusOK, gin.H{
"status": StatusOk,
"message": "重置成功: ",
})
}
// ------------------------------------------------------ 参数校验 ------------------------------------------------------
func validFilmSource(fs system.FilmSource) error {
// 资源名称不能为空 且长度不能超过20
if len(fs.Name) <= 0 || len(fs.Name) > 20 {
return errors.New("资源名称不能为空且长度不能超过20")
}
// Uri 采集链接测试格式
if !util.ValidURL(fs.Uri) {
return errors.New("资源链接格式异常, 请输入规范的URL链接")
}
// 校验接口类型是否是 JSON || XML
if fs.ResultModel != system.JsonResult && fs.ResultModel != system.XmlResult {
return errors.New("接口类型异常, 请提交正确的接口类型")
}
// 校验采集类型是否符合规范
switch fs.CollectType {
case system.CollectVideo, system.CollectArticle, system.CollectActor, system.CollectRole, system.CollectWebSite:
return nil
default:
return errors.New("资源类型异常, 未知的资源类型")
}
}
func apiValidityCheck() {
}

View File

@@ -0,0 +1,67 @@
package controller
import (
"github.com/gin-gonic/gin"
"net/http"
"server/logic"
"server/model/system"
"strconv"
)
// 提供用于第三方站点采集的API
// HandleProvide 返回视频列表信息
func HandleProvide(c *gin.Context) {
// 将请求参数封装为一个map
var params = map[string]string{
"t": c.DefaultQuery("t", ""),
//"pg": c.DefaultQuery("pg", ""),
"wd": c.DefaultQuery("wd", ""),
"h": c.DefaultQuery("h", ""),
"ids": c.DefaultQuery("ids", ""),
}
// 设置分页信息
currentStr := c.DefaultQuery("pg", "1")
pageSizeStr := c.DefaultQuery("limit", "20")
current, _ := strconv.Atoi(currentStr)
pageSize, _ := strconv.Atoi(pageSizeStr)
page := system.Page{PageSize: pageSize, Current: current}
// ac-请求类型 t-类别ID pg-页码 wd-搜索关键字 h=几小时内的数据 ids-数据ID 多个ID逗好分割
var ac string = c.DefaultQuery("ac", "")
switch ac {
case "list":
c.JSON(http.StatusOK, logic.PL.GetFilmListPage(params, &page))
case "detail", "videolist":
c.JSON(http.StatusOK, logic.PL.GetFilmDetailPage(params, &page))
default:
c.JSON(http.StatusOK, logic.PL.GetFilmListPage(params, &page))
}
}
// HandleProvideXml 处理返回xml格式的数据
func HandleProvideXml(c *gin.Context) {
// 将请求参数封装为一个map
var params = map[string]string{
"t": c.DefaultQuery("t", ""),
//"pg": c.DefaultQuery("pg", ""),
"wd": c.DefaultQuery("wd", ""),
"h": c.DefaultQuery("h", ""),
"ids": c.DefaultQuery("ids", ""),
}
// 设置分页信息
currentStr := c.DefaultQuery("pg", "1")
pageSizeStr := c.DefaultQuery("limit", "20")
current, _ := strconv.Atoi(currentStr)
pageSize, _ := strconv.Atoi(pageSizeStr)
page := system.Page{PageSize: pageSize, Current: current}
// ac-请求类型 t-类别ID pg-页码 wd-搜索关键字 h=几小时内的数据 ids-数据ID 多个ID逗好分割
var ac string = c.DefaultQuery("ac", "")
switch ac {
case "list":
c.XML(http.StatusOK, logic.PL.GetFilmListXmlPage(params, &page))
case "detail", "videolist":
c.XML(http.StatusOK, logic.PL.GetFilmDetailXmlPage(params, &page))
default:
c.XML(http.StatusOK, logic.PL.GetFilmListXmlPage(params, &page))
}
}

View File

@@ -1,65 +1,135 @@
package controller
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"server/config"
"server/logic"
"server/model/system"
"strconv"
)
// SpiderRe 数据清零重开
func SpiderRe(c *gin.Context) {
// 获取指令参数
cip := c.Query("cipher")
if cip != config.SpiderCipher {
// CollectFilm 开启ID对应的资源站的采集任务
func CollectFilm(c *gin.Context) {
id := c.DefaultQuery("id", "")
hourStr := c.DefaultQuery("h", "0")
if id == "" || hourStr == "0" {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "指令错误无法进行此操作",
"message": "采集任务开启失败, 缺乏必要参数",
})
return
}
go logic.SL.ReZero()
c.JSON(http.StatusOK, gin.H{
"status": StatusOk,
"message": "ReZero 任务执行已成功开启",
})
}
// FixFilmDetail 修复因网络异常造成的影片详情数据丢失
func FixFilmDetail(c *gin.Context) {
// 获取指令参数
cip := c.Query("cipher")
if cip != config.SpiderCipher {
h, err := strconv.Atoi(hourStr)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "指令错误无法进行此操作",
"message": "采集任务开启失败, hour(时长)参数不符合规范",
})
return
}
// 如果指令正确,则执行详情数据获取
go logic.SL.FixDetail()
c.JSON(http.StatusOK, gin.H{
"status": StatusOk,
"message": "FixDetail 任务执行已成功开启",
})
}
// RefreshSitePlay 清空附属站点影片数据并重新获取
func RefreshSitePlay(c *gin.Context) {
// 获取指令参数
cip := c.Query("cipher")
if cip != config.SpiderCipher {
// 执行采集逻处理逻辑
if err = logic.SL.StartCollect(id, h); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "指令错误无法进行此操作",
"message": fmt.Sprint("采集任务开启失败: ", err.Error()),
})
return
}
// 执行多站点播放数据重置
go logic.SL.SpiderMtPlayRe()
c.JSON(http.StatusOK, gin.H{
"status": StatusOk,
"message": "SpiderMtPlayRe 任务执行已成功开启",
"message": "采集任务已成功开启!!!",
})
}
// StarSpider 开启并执行采集任务
func StarSpider(c *gin.Context) {
var cp system.CollectParams
// 获取请求参数
if err := c.ShouldBindJSON(&cp); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "请求参数异常",
})
return
}
if cp.Time == 0 {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "采集开启失败,采集时长不能为0",
})
return
}
// 根据 Batch 执行对应的逻辑
if cp.Batch {
// 执行批量采集
if cp.Ids == nil || len(cp.Ids) <= 0 {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "批量采集开启失败, 关联的资源站信息为空",
})
return
}
// 执行批量采集
logic.SL.BatchCollect(cp.Time, cp.Ids)
} else {
if len(cp.Id) <= 0 {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "批量采集开启失败, 资源站Id获取失败",
})
return
}
if err := logic.SL.StartCollect(cp.Id, cp.Time); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": fmt.Sprint("采集任务开启失败: ", err.Error()),
})
return
}
}
// 返回成功执行的信息
c.JSON(http.StatusOK, gin.H{
"status": StatusOk,
"message": "采集任务已成功开启!!!",
})
}
// SpiderReset 重置影视数据, 清空库存, 从零开始
func SpiderReset(c *gin.Context) {
var cp system.CollectParams
// 获取请求参数
if err := c.ShouldBindJSON(&cp); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "请求参数异常",
})
return
}
if cp.Time == 0 {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "采集开启失败,采集时长不能为0",
})
return
}
// 后期加入一些前置验证
if len(cp.Id) <= 0 {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "SpiderReset Failed, 资源站Id获取失败",
})
return
}
logic.SL.ZeroCollect(cp.Time)
}
// CoverFilmClass 重置覆盖影片分类信息
func CoverFilmClass(c *gin.Context) {
// 执行分类采集, 覆盖当前分类信息
if err := logic.SL.FilmClassCollect(); err != nil {
system.Failed(err.Error(), c)
return
}
system.SuccessOnlyMsg("影视分类信息重置成功, 请稍等片刻后刷新页面", c)
}

View File

@@ -0,0 +1,144 @@
package controller
import (
"fmt"
"github.com/gin-gonic/gin"
"log"
"net/http"
"server/config"
"server/logic"
"server/model/system"
"server/plugin/common/util"
)
// Login 管理员登录接口
func Login(c *gin.Context) {
var u system.User
if err := c.ShouldBindJSON(&u); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "数据格式异常!!!",
})
return
}
if len(u.UserName) <= 0 || len(u.Password) <= 0 {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "用户名和密码信息不能为空!!!",
})
return
}
token, err := logic.UL.UserLogin(u.UserName, u.Password)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": err.Error(),
})
return
}
c.Header("new-token", token)
c.JSON(http.StatusOK, gin.H{
"status": StatusOk,
"message": "登录成功!!!",
})
return
}
// Logout 退出登录
func Logout(c *gin.Context) {
// 获取已登录的用户信息
v, ok := c.Get(config.AuthUserClaims)
if !ok {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "登录信息异常!!!",
})
return
}
// 清除redis中存储的对应token
uc, ok := v.(*system.UserClaims)
if !ok {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "登录信息异常!!!",
})
return
}
err := system.ClearUserToken(uc.UserID)
if err != nil {
log.Println("user logOut err: ", err)
}
c.JSON(http.StatusOK, gin.H{
"status": StatusOk,
"message": "logout success!!!",
})
}
// UserPasswordChange 修改用户密码
func UserPasswordChange(c *gin.Context) {
// 接收密码修改参数
var params map[string]string
if err := c.ShouldBindJSON(&params); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "数据格式异常!!!",
})
return
}
// 校验参数是否存在空值
if params["password"] == "" || params["newPassword"] == "" {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "原密码和新密码不能为空!!!",
})
return
}
// 校验新密码是否符合规范
if err := util.ValidPwd(params["newPassword"]); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": fmt.Sprint("密码格式校验失败: ", err.Error()),
})
return
}
// 获取已登录的用户信息
v, ok := c.Get(config.AuthUserClaims)
if !ok {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": "登录信息异常!!!",
})
return
}
// 从context中获取用户的登录信息
uc := v.(*system.UserClaims)
if err := logic.UL.ChangePassword(uc.UserName, params["password"], params["newPassword"]); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": StatusFailed,
"message": fmt.Sprint("密码修改失败: ", err.Error()),
})
return
}
// 密码修改成功后不主动使token失效, 以免影响体验
c.JSON(http.StatusOK, gin.H{
"status": StatusOk,
"message": "密码修改成功",
})
}
func UserInfo(c *gin.Context) {
// 从context中获取用户的相关信息
v, ok := c.Get(config.AuthUserClaims)
if !ok {
system.Failed("用户信息获取失败, 未获取到用户授权信息", c)
return
}
uc, ok := v.(*system.UserClaims)
if !ok {
system.Failed("用户信息获取失败, 户授权信息异常", c)
return
}
// 通过用户ID获取用户基本信息
info := logic.UL.GetUserInfo(uc.UserID)
system.Success(info, "成功获取用户信息", c)
}

View File

@@ -5,12 +5,14 @@ go 1.20
require (
github.com/gin-gonic/gin v1.9.0
github.com/gocolly/colly/v2 v2.1.0
github.com/golang-jwt/jwt/v5 v5.1.0
github.com/redis/go-redis/v9 v9.0.2
github.com/robfig/cron/v3 v3.0.0
gorm.io/driver/mysql v1.4.7
gorm.io/gorm v1.24.6
)
require (
github.com/PuerkitoBio/goquery v1.5.1 // indirect
github.com/andybalholm/cascadia v1.2.0 // indirect

View File

@@ -46,11 +46,14 @@ github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA=
github.com/gocolly/colly/v2 v2.1.0 h1:k0DuZkDoCsx51bKpRJNEmcxcp+W5N8ziuwGaSDuFoGs=
github.com/gocolly/colly/v2 v2.1.0/go.mod h1:I2MuhsLjQ+Ex+IzK3afNS8/1qP3AedHOusRPcRdC5o0=
github.com/golang-jwt/jwt/v5 v5.1.0 h1:UGKbA/IPjtS6zLcdB7i5TyACMgSbOTiR8qzXgw8HWQU=
github.com/golang-jwt/jwt/v5 v5.1.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=

99
server/logic/CronLogic.go Normal file
View File

@@ -0,0 +1,99 @@
package logic
import (
"errors"
"fmt"
"server/model/system"
"server/plugin/common/util"
"server/plugin/spider"
"time"
)
type CronLogic struct {
}
var CL *CronLogic
// AddFilmCrontab 添加影片更新任务
func (cl *CronLogic) AddFilmCrontab(cv system.FilmCronVo) error {
// 如果 spec 表达式校验失败则直接返回错误信息并终止
if err := spider.ValidSpec(cv.Spec); err != nil {
return err
}
// 生成任务信息 生成一个唯一ID 作为Task唯一标识
task := system.FilmCollectTask{Id: util.GenerateSalt(), Ids: cv.Ids, Time: cv.Time, Spec: cv.Spec, Model: cv.Model, State: cv.State, Remark: cv.Remark}
// 添加一条定时任务
switch task.Model {
case 0:
cid, err := spider.AddAutoUpdateCron(task.Id, task.Spec)
// 如果任务添加失败则直接返回错误信息
if err != nil {
return errors.New(fmt.Sprint("影视自动更新任务添加失败: ", err.Error()))
}
// 将定时任务Id记录到Task中
task.Cid = cid
case 1:
cid, err := spider.AddFilmUpdateCron(task.Id, task.Spec)
// 如果任务添加失败则直接返回错误信息
if err != nil {
return errors.New(fmt.Sprint("影视更新定时任务添加失败: ", err.Error()))
}
// 将定时任务Id记录到Task中
task.Cid = cid
}
// 如果没有异常则将当前定时任务信息记录到redis中
system.SaveFilmTask(task)
return nil
}
// GetFilmCrontab 获取所有定时任务信息
func (cl *CronLogic) GetFilmCrontab() []system.CronTaskVo {
var l []system.CronTaskVo
tl := system.GetAllFilmTask()
for _, t := range tl {
e := spider.GetEntryById(t.Cid)
taskVo := system.CronTaskVo{FilmCollectTask: t, PreV: e.Prev.Format(time.DateTime), Next: e.Next.Format(time.DateTime)}
l = append(l, taskVo)
}
return l
}
// GetFilmCrontabById 通过ID获取对应的定时任务信息
func (cl *CronLogic) GetFilmCrontabById(id string) (system.FilmCollectTask, error) {
t, err := system.GetFilmTaskById(id)
//e := spider.GetEntryById(t.Cid)
//taskVo := system.CronTaskVo{FilmCollectTask: t, PreV: e.Prev.Format(time.DateTime), Next: e.Next.Format(time.DateTime)}
return t, err
}
// ChangeFilmCrontab 改变定时任务的状态 开启 | 停止
func (cl *CronLogic) ChangeFilmCrontab(id string, state bool) error {
// 通过定时任务信息的唯一标识获取对应的定时任务信息
ft, err := system.GetFilmTaskById(id)
if err != nil {
return errors.New(fmt.Sprintf("定时任务停止失败: %s", err.Error()))
}
// 修改当前定时任务的状态为 false, 则在定时执行方法时不会执行具体逻辑
ft.State = state
system.UpdateFilmTask(ft)
return err
}
// UpdateFilmCron 更新定时任务的状态信息
func (cl *CronLogic) UpdateFilmCron(t system.FilmCollectTask) {
system.UpdateFilmTask(t)
}
// DelFilmCrontab 删除定时任务
func (cl *CronLogic) DelFilmCrontab(id string) error {
// 通过定时任务信息的唯一Id标识获取对应的定时任务信息
ft, err := system.GetFilmTaskById(id)
if err != nil {
return errors.New(fmt.Sprintf("定时任务删除失败: %s", err.Error()))
}
// 通过定时任务EntryID移出对应的定时任务
spider.RemoveCron(ft.Cid)
// 将定时任务相关信息删除
system.DelFilmTask(id)
return nil
}

29
server/logic/FileLogic.go Normal file
View File

@@ -0,0 +1,29 @@
package logic
import (
"fmt"
"path/filepath"
"server/config"
"server/model/system"
"strings"
)
type FileLogic struct {
}
var FileL FileLogic
func (fl *FileLogic) SingleFileUpload(fileName string, uid int) string {
// 生成图片信息
var p = system.Picture{Link: fmt.Sprint(config.FilmPictureAccess, filepath.Base(fileName)), Uid: uid, PicType: 0}
p.PicUid = strings.TrimSuffix(filepath.Base(fileName), filepath.Ext(fileName))
// 记录图片信息到系统表中
system.SaveGallery(p)
return p.Link
}
// GetPhotoPage 获取系统内的图片分页信息
func (fl *FileLogic) GetPhotoPage(page *system.Page) []system.Picture {
return system.GetPicturePage(page)
}

161
server/logic/FilmLogic.go Normal file
View File

@@ -0,0 +1,161 @@
package logic
import (
"errors"
"fmt"
"server/model/system"
"server/plugin/common/conver"
"time"
)
/*
处理影片管理相关业务
*/
type FilmLogic struct {
}
var FL *FilmLogic
//----------------------------------------------------影片管理业务逻辑----------------------------------------------------
func (fl *FilmLogic) GetFilmPage(s system.SearchVo) []system.SearchInfo {
// 获取影片检索信息分页数据
sl := system.GetSearchPage(s)
//
return sl
}
// GetSearchOptions 获取影片检索的select的选项options
func (fl *FilmLogic) GetSearchOptions() map[string]any {
var options = make(map[string]any)
// 获取分类 options
tree := system.GetCategoryTree()
tree.Name = "全部分类"
options["class"] = conver.ConvertCategoryList(tree)
options["remarks"] = []map[string]string{{"Name": `全部`, "Value": ``}, {"Name": `完结`, "Value": `完结`}, {"Name": `未完结`, "Value": `未完结`}}
// 获取 剧情,地区,语言, 年份 组信息 (每个分类对应的检索信息并不相同)
var tagGroup = make(map[int64]map[string]any)
// 遍历一级分类获取对应的标签组信息
for _, t := range tree.Children {
option := system.GetSearchOptions(t.Id)
if len(option) > 0 {
tagGroup[t.Id] = system.GetSearchOptions(t.Id)
// 如果年份信息不存在则独立一份年份信息
if _, ok := options["year"]; !ok {
options["year"] = tagGroup[t.Id]["Year"]
}
}
}
options["tags"] = tagGroup
return options
}
// SaveFilmDetail 自定义上传保存影片信息
func (fl *FilmLogic) SaveFilmDetail(fd system.FilmDetailVo) error {
// 补全影片信息
now := time.Now()
fd.UpdateTime = now.Format(time.DateTime)
fd.AddTime = fd.UpdateTime
// 生成ID, 由于是自定义上传的影片, 避免和采集站点的影片冲突, 以当前时间时间戳作为ID
fd.Id = now.Unix()
// 生成影片详情信息
detail, err := conver.CovertFilmDetailVo(fd)
if err != nil || detail.PlayList == nil {
return errors.New("影片参数格式异常或缺少关键信息")
}
// 保存影片信息
return system.SaveDetail(detail)
}
//----------------------------------------------------影片分类业务逻辑----------------------------------------------------
// GetFilmClassTree 获取影片分类信息
func (fl *FilmLogic) GetFilmClassTree() system.CategoryTree {
// 获取原本的影片分类信息
return system.GetCategoryTree()
}
func (fl *FilmLogic) GetFilmClassById(id int64) *system.CategoryTree {
tree := system.GetCategoryTree()
for _, c := range tree.Children {
// 如果是一级分类, 则相等时直接返回
if c.Id == id {
return c
}
// 如果当前分类含有子分类, 则继续遍历匹配
if c.Children != nil {
for _, subC := range c.Children {
if subC.Id == id {
return subC
}
}
}
}
return nil
}
// UpdateClass 更新分类信息
func (fl *FilmLogic) UpdateClass(class system.CategoryTree) error {
// 遍历影片分类信息
tree := system.GetCategoryTree()
for _, c := range tree.Children {
// 如果是一级分类, 则相等时直接修改对应的name和show属性
if c.Id == class.Id {
if class.Name != "" {
c.Name = class.Name
}
c.Show = class.Show
if err := system.SaveCategoryTree(&tree); err != nil {
return fmt.Errorf("影片分类信息更新失败: %s", err.Error())
}
return nil
}
// 如果当前分类含有子分类, 则继续遍历匹配
if c.Children != nil {
for _, subC := range c.Children {
if subC.Id == class.Id {
if class.Name != "" {
subC.Name = class.Name
}
subC.Show = class.Show
if err := system.SaveCategoryTree(&tree); err != nil {
return fmt.Errorf("影片分类信息更新失败: %s", err.Error())
}
return nil
}
}
}
}
return errors.New("需要更新的分类信息不存在")
}
// DelClass 删除分类信息
func (fl *FilmLogic) DelClass(id int64) error {
tree := system.GetCategoryTree()
for i, c := range tree.Children {
// 如果是一级分类, 则相等时直接返回
if c.Id == id {
tree.Children = append(tree.Children[:i], tree.Children[i+1:]...)
if err := system.SaveCategoryTree(&tree); err != nil {
return fmt.Errorf("影片分类信息删除失败: %s", err.Error())
}
return nil
}
// 如果当前分类含有子分类, 则继续遍历匹配
if c.Children != nil {
for j, subC := range c.Children {
if subC.Id == id {
c.Children = append(c.Children[:j], c.Children[j+1:]...)
if err := system.SaveCategoryTree(&tree); err != nil {
return fmt.Errorf("影片分类信息删除失败: %s", err.Error())
}
return nil
}
}
}
}
return errors.New("需要删除的分类信息不存在")
}

View File

@@ -5,9 +5,8 @@ import (
"github.com/gin-gonic/gin"
"regexp"
"server/config"
"server/model"
"server/model/system"
"server/plugin/db"
"server/plugin/spider"
"strings"
)
@@ -27,45 +26,53 @@ func (i *IndexLogic) IndexPage() map[string]interface{} {
//Info := make(map[string]interface{})
// 首页请求时长较高, 采用redis进行缓存, 在定时任务更新影片时清除对应缓存
// 判断是否存在缓存数据, 存在则直接将数据返回
Info := model.GetCacheData(config.IndexCacheKey)
Info := system.GetCacheData(config.IndexCacheKey)
if Info != nil {
return Info
}
Info = make(map[string]interface{})
// 1. 首页分类数据处理 导航分类数据处理, 只提供 电影 电视剧 综艺 动漫 四大顶级分类和其子分类
tree := model.CategoryTree{Category: &model.Category{Id: 0, Name: "分类信息"}}
sysTree := model.GetCategoryTree()
tree := system.CategoryTree{Category: &system.Category{Id: 0, Name: "分类信息"}}
sysTree := system.GetCategoryTree()
// 由于采集源数据格式不一,因此采用名称匹配
//for _, c := range sysTree.Children {
// switch c.Category.Name {
// case "电影", "电影片", "连续剧", "电视剧", "综艺", "综艺片", "动漫", "动漫片":
// tree.Children = append(tree.Children, c)
// }
//}
for _, c := range sysTree.Children {
switch c.Category.Name {
case "电影", "电影片", "连续剧", "电视剧", "综艺", "综艺片", "动漫", "动漫片":
// 只针对一级分类进行处理
if c.Show {
tree.Children = append(tree.Children, c)
}
}
Info["category"] = tree
// 2. 提供用于首页展示的顶级分类影片信息, 每分类 14条数据
var list []map[string]interface{}
for _, c := range tree.Children {
page := model.Page{PageSize: 14, Current: 1}
movies := model.GetMovieListByPid(c.Id, &page)
page := system.Page{PageSize: 14, Current: 1}
movies := system.GetMovieListByPid(c.Id, &page)
// 获取当前分类的本月热门影片
HotMovies := model.GetHotMovieByPid(c.Id, &page)
HotMovies := system.GetHotMovieByPid(c.Id, &page)
item := map[string]interface{}{"nav": c, "movies": movies, "hot": HotMovies}
list = append(list, item)
}
Info["content"] = list
// 不存在首页数据缓存时将查询数据缓存到redis中
model.DataCache(config.IndexCacheKey, Info)
//system.DataCache(config.IndexCacheKey, Info)
return Info
}
// GetFilmDetail 影片详情信息页面处理
func (i *IndexLogic) GetFilmDetail(id int) model.MovieDetail {
func (i *IndexLogic) GetFilmDetail(id int) system.MovieDetail {
// 通过Id 获取影片search信息
search := model.SearchInfo{}
search := system.SearchInfo{}
db.Mdb.Where("mid", id).First(&search)
// 获取redis中的完整影视信息 MovieDetail:Cid11:Id24676
movieDetail := model.GetDetailByKey(fmt.Sprintf(config.MovieDetailKey, search.Cid, search.Mid))
movieDetail := system.GetDetailByKey(fmt.Sprintf(config.MovieDetailKey, search.Cid, search.Mid))
//查找其他站点是否存在影片对应的播放源
multipleSource(&movieDetail)
return movieDetail
@@ -76,7 +83,7 @@ func (i *IndexLogic) GetCategoryInfo() gin.H {
// 组装nav导航所需的信息
nav := gin.H{}
// 1.获取所有分类信息
tree := model.GetCategoryTree()
tree := system.GetCategoryTree()
// 2. 过滤出主页四大分类的tree信息
for _, t := range tree.Children {
switch t.Category.Name {
@@ -94,34 +101,48 @@ func (i *IndexLogic) GetCategoryInfo() gin.H {
return nav
}
func (i *IndexLogic) GetNavCategory() []*system.Category {
// 1.获取所有分类信息
tree := system.GetCategoryTree()
// 遍历一级分类返回可展示的分类数据
var cl []*system.Category
for _, c := range tree.Children {
if c.Show {
cl = append(cl, c.Category)
}
}
// 返回一级分类列表数据
return cl
}
// SearchFilmInfo 获取关键字匹配的影片信息
func (i *IndexLogic) SearchFilmInfo(key string, page *model.Page) []model.MovieBasicInfo {
func (i *IndexLogic) SearchFilmInfo(key string, page *system.Page) []system.MovieBasicInfo {
// 1. 从mysql中获取满足条件的数据, 每页10条
sl := model.SearchFilmKeyword(key, page)
sl := system.SearchFilmKeyword(key, page)
// 2. 获取redis中的basicMovieInfo信息
var bl []model.MovieBasicInfo
var bl []system.MovieBasicInfo
for _, s := range sl {
bl = append(bl, model.GetBasicInfoByKey(fmt.Sprintf(config.MovieBasicInfoKey, s.Cid, s.Mid)))
bl = append(bl, system.GetBasicInfoByKey(fmt.Sprintf(config.MovieBasicInfoKey, s.Cid, s.Mid)))
}
return bl
}
// GetFilmCategory 根据Pid或Cid获取指定的分页数据
func (i *IndexLogic) GetFilmCategory(id int64, idType string, page *model.Page) []model.MovieBasicInfo {
func (i *IndexLogic) GetFilmCategory(id int64, idType string, page *system.Page) []system.MovieBasicInfo {
// 1. 根据不同类型进不同的查找
var basicList []model.MovieBasicInfo
var basicList []system.MovieBasicInfo
switch idType {
case "pid":
basicList = model.GetMovieListByPid(id, page)
basicList = system.GetMovieListByPid(id, page)
case "cid":
basicList = model.GetMovieListByCid(id, page)
basicList = system.GetMovieListByCid(id, page)
}
return basicList
}
// GetPidCategory 获取pid对应的分类信息
func (i *IndexLogic) GetPidCategory(pid int64) *model.CategoryTree {
tree := model.GetCategoryTree()
func (i *IndexLogic) GetPidCategory(pid int64) *system.CategoryTree {
tree := system.GetCategoryTree()
for _, t := range tree.Children {
if t.Id == pid {
return t
@@ -131,7 +152,7 @@ func (i *IndexLogic) GetPidCategory(pid int64) *model.CategoryTree {
}
// RelateMovie 根据当前影片信息匹配相关的影片
func (i *IndexLogic) RelateMovie(detail model.MovieDetail, page *model.Page) []model.MovieBasicInfo {
func (i *IndexLogic) RelateMovie(detail system.MovieDetail, page *system.Page) []system.MovieBasicInfo {
/*
根据当前影片信息匹配相关的影片
1. 分类Cid,
@@ -140,20 +161,20 @@ func (i *IndexLogic) RelateMovie(detail model.MovieDetail, page *model.Page) []m
4. 地区 area
5. 语言 Language
*/
search := model.SearchInfo{
search := system.SearchInfo{
Cid: detail.Cid,
Name: detail.Name,
ClassTag: detail.ClassTag,
Area: detail.Area,
Language: detail.Language,
}
return model.GetRelateMovieBasicInfo(search, page)
return system.GetRelateMovieBasicInfo(search, page)
}
// SearchTags 整合对应分类的搜索tag
func (i *IndexLogic) SearchTags(pid int64) map[string]interface{} {
// 通过pid 获取对应分类的 tags
return model.GetSearchTag(pid)
return system.GetSearchTag(pid)
}
/*
@@ -162,33 +183,34 @@ func (i *IndexLogic) SearchTags(pid int64) map[string]interface{} {
2. 仅对主站点影片name进行映射关系处理并将结果添加到map中
例如: xxx第一季 xxx
*/
func multipleSource(detail *model.MovieDetail) {
func multipleSource(detail *system.MovieDetail) {
// 整合多播放源, 初始化存储key map
names := make(map[string]int)
// 1. 判断detail的dbId是否存在, 存在则添加到names中作为匹配条件
if detail.DbId > 0 {
names[model.GenerateHashKey(detail.DbId)] = 0
names[system.GenerateHashKey(detail.DbId)] = 0
}
// 2. 对name进行去除特殊格式处理
names[model.GenerateHashKey(detail.Name)] = 0
names[system.GenerateHashKey(detail.Name)] = 0
// 3. 对包含第一季的name进行处理
names[model.GenerateHashKey(regexp.MustCompile(`第一季$`).ReplaceAllString(detail.Name, ""))] = 0
names[system.GenerateHashKey(regexp.MustCompile(`第一季$`).ReplaceAllString(detail.Name, ""))] = 0
// 4. 将subtitle进行切分,放入names中
if len(detail.SubTitle) > 0 && strings.Contains(detail.SubTitle, ",") {
for _, v := range strings.Split(detail.SubTitle, ",") {
names[model.GenerateHashKey(v)] = 0
names[system.GenerateHashKey(v)] = 0
}
}
if len(detail.SubTitle) > 0 && strings.Contains(detail.SubTitle, "/") {
for _, v := range strings.Split(detail.SubTitle, "/") {
names[model.GenerateHashKey(v)] = 0
names[system.GenerateHashKey(v)] = 0
}
}
// 遍历站点列表
for _, s := range spider.SiteList {
sc := system.GetCollectSourceListByGrade(system.SlaveCollect)
for _, s := range sc {
for k, _ := range names {
pl := model.GetMultiplePlay(s.Name, k)
pl := system.GetMultiplePlay(s.Name, k)
if len(pl) > 0 {
// 如果当前站点已经匹配到数据则直接退出当前循环
detail.PlayList = append(detail.PlayList, pl)
@@ -196,26 +218,25 @@ func multipleSource(detail *model.MovieDetail) {
}
}
}
}
// GetFilmsByTags 通过searchTag 返回满足条件的分页影片信息
func (i *IndexLogic) GetFilmsByTags(st model.SearchTagsVO, page *model.Page) []model.MovieBasicInfo {
func (i *IndexLogic) GetFilmsByTags(st system.SearchTagsVO, page *system.Page) []system.MovieBasicInfo {
// 获取满足条件的影片id 列表
sl := model.GetSearchInfosByTags(st, page)
sl := system.GetSearchInfosByTags(st, page)
// 通过key 获取对应影片的基本信息
return model.GetBasicInfoBySearchInfos(sl...)
return system.GetBasicInfoBySearchInfos(sl...)
}
// GetFilmClassify 通过Pid返回当前所属分类下的首页展示数据
func (i *IndexLogic) GetFilmClassify(pid int64, page *model.Page) map[string]interface{} {
func (i *IndexLogic) GetFilmClassify(pid int64, page *system.Page) map[string]interface{} {
res := make(map[string]interface{})
// 最新上映 (上映时间)
res["news"] = model.GetMovieListBySort(0, pid, page)
res["news"] = system.GetMovieListBySort(0, pid, page)
// 排行榜 (暂定为热度排行)
res["top"] = model.GetMovieListBySort(1, pid, page)
res["top"] = system.GetMovieListBySort(1, pid, page)
// 最近更新 (更新时间)
res["recent"] = model.GetMovieListBySort(2, pid, page)
res["recent"] = system.GetMovieListBySort(2, pid, page)
return res

View File

@@ -0,0 +1,53 @@
package logic
import (
"errors"
"server/model/system"
)
type ManageLogic struct {
}
var ML *ManageLogic
// GetFilmSourceList 获取采集站列表数据
func (ml *ManageLogic) GetFilmSourceList() []system.FilmSource {
// 返回当前已添加的采集站列表信息
return system.GetCollectSourceList()
}
func (ml *ManageLogic) GetFilmSource(id string) *system.FilmSource {
return system.FindCollectSourceById(id)
}
func (ml *ManageLogic) UpdateFilmSource(s system.FilmSource) error {
return system.UpdateCollectSource(s)
}
func (ml *ManageLogic) SaveFilmSource(s system.FilmSource) error {
return system.AddCollectSource(s)
}
func (ml *ManageLogic) DelFilmSource(id string) error {
// 先查找是否存在对应ID的站点信息
s := system.FindCollectSourceById(id)
if s == nil {
return errors.New("当前资源站信息不存在, 请勿重复操作")
}
// 如果是主站点则返回提示禁止直接删除
if s.Grade == system.MasterCollect {
return errors.New("主站点无法直接删除, 请先降级为附属站点再进行删除")
}
system.DelCollectResource(id)
return nil
}
// GetSiteBasicConfig 获取网站基本配置信息
func (ml *ManageLogic) GetSiteBasicConfig() system.BasicConfig {
return system.GetSiteBasic()
}
// UpdateSiteBasic 更新网站配置信息
func (ml *ManageLogic) UpdateSiteBasic(c system.BasicConfig) error {
return system.SaveSiteBasic(c)
}

View File

@@ -0,0 +1,114 @@
package logic
import (
"fmt"
"log"
"server/config"
"server/model/collect"
"server/model/system"
"server/plugin/common/conver"
"strconv"
"strings"
)
type ProvideLogic struct {
}
var PL *ProvideLogic
// GetFilmDetailPage 处理请求参数, 返回filmDetail数据
func (pl *ProvideLogic) GetFilmDetailPage(params map[string]string, page *system.Page) collect.FilmDetailLPage {
return filmDetailPage(params, page)
}
// GetFilmListPage 处理请求参数, 返回filmList数据
func (pl *ProvideLogic) GetFilmListPage(params map[string]string, page *system.Page) collect.FilmListPage {
dp := filmDetailPage(params, page)
var p collect.FilmListPage = collect.FilmListPage{
Code: dp.Code,
Msg: dp.Msg,
Page: dp.Page,
PageCount: dp.PageCount,
Limit: dp.Limit,
Total: dp.Total,
List: conver.DetailCovertList(dp.List),
Class: collect.GetFilmClass(),
}
return p
}
func (pl *ProvideLogic) GetFilmDetailXmlPage(params map[string]string, page *system.Page) collect.RssD {
dp := filmDetailPage(params, page)
var dxp = collect.RssD{
Version: config.RssVersion,
List: collect.FilmDetailPageX{
Page: fmt.Sprint(dp.Page),
PageCount: dp.PageCount,
PageSize: fmt.Sprint(dp.Limit),
RecordCount: len(dp.List),
Videos: conver.DetailCovertXml(dp.List),
},
}
return dxp
}
func (pl *ProvideLogic) GetFilmListXmlPage(params map[string]string, page *system.Page) collect.RssL {
dp := filmDetailPage(params, page)
cl := collect.GetFilmClass()
var dxp = collect.RssL{
Version: config.RssVersion,
List: collect.FilmListPageX{
Page: dp.Page,
PageCount: dp.PageCount,
PageSize: dp.Limit,
RecordCount: len(dp.List),
Videos: conver.DetailCovertListXml(dp.List),
},
ClassXL: conver.ClassListCovertXml(cl),
}
return dxp
}
func filmDetailPage(params map[string]string, page *system.Page) collect.FilmDetailLPage {
var p collect.FilmDetailLPage = collect.FilmDetailLPage{
Code: 1,
Msg: "数据列表",
Page: fmt.Sprint(page.Current),
}
// 如果params中的ids不为空, 则直接返回ids对应的数据
if len(params["ids"]) > 0 {
var ids []int64
for _, idStr := range strings.Split(params["ids"], ",") {
if id, err := strconv.ParseInt(idStr, 10, 64); err == nil {
ids = append(ids, id)
}
}
page.Total = len(ids)
page.PageCount = int((len(ids) + page.PageSize - 1) / page.PageSize)
// 获取id对应的数据
for i := 0; i >= (page.Current-1)*page.PageSize && i < page.Total && i < page.Current*page.PageSize; i++ {
if fd, err := collect.GetOriginalDetailById(ids[i]); err == nil {
p.List = append(p.List, conver.FilterFilmDetail(fd, 0))
}
}
p.PageCount = page.PageCount
p.Limit = fmt.Sprint(page.PageSize)
p.Total = page.Total
return p
}
// 如果请求参数中不包含 ids, 则通过条件进行对应查找
l, err := system.FindFilmIds(params, page)
if err != nil {
log.Println(err)
}
for _, id := range l {
if fd, e := collect.GetOriginalDetailById(id); e == nil {
p.List = append(p.List, conver.FilterFilmDetail(fd, 0))
}
}
p.PageCount = page.PageCount
p.Limit = fmt.Sprint(page.PageSize)
p.Total = page.Total
return p
}

View File

@@ -1,10 +1,9 @@
package logic
import (
"fmt"
"errors"
"log"
"server/config"
"server/model"
"server/model/system"
"server/plugin/spider"
)
@@ -13,32 +12,49 @@ type SpiderLogic struct {
var SL *SpiderLogic
// ReZero 清空所有数据从零开始拉取
func (sl *SpiderLogic) ReZero() {
// 如果指令正确,则执行重制
spider.StartSpiderRe()
// BatchCollect 批量采集
func (sl *SpiderLogic) BatchCollect(time int, ids []string) {
go spider.BatchCollect(time, ids...)
}
// FixDetail 重新获取主站点数据信息
func (sl *SpiderLogic) FixDetail() {
spider.MainSiteSpider()
log.Println("FilmDetail 重制完成!!!")
// 先截断表中的数据
model.TunCateSearchTable()
// 重新扫描完整的信息到mysql中
spider.SearchInfoToMdb()
log.Println("SearchInfo 重制完成!!!")
}
// SpiderMtPlayRe 多站点播放数据清空重新获取
func (sl *SpiderLogic) SpiderMtPlayRe() {
// 先清空有所附加播放源
var keys []string
for _, site := range spider.SiteList {
keys = append(keys, fmt.Sprintf(config.MultipleSiteDetail, site.Name))
// StartCollect 执行对指定站点的采集任务
func (sl *SpiderLogic) StartCollect(id string, h int) error {
// 先判断采集站是否存在于系统数据中
if fs := system.FindCollectSourceById(id); fs == nil {
return errors.New("采集任务开启失败采集站信息不存在")
}
model.DelMtPlay(keys)
// 如果指令正确,则执行详情数据获取
spider.MtSiteSpider()
log.Println("MtSiteSpider 重制完成!!!")
// 存在则开启协程执行采集方法
go func() {
err := spider.HandleCollect(id, h)
if err != nil {
log.Printf("资源站[%s]采集任务执行失败: %s", id, err)
}
}()
return nil
}
// AutoCollect 自动采集
func (sl *SpiderLogic) AutoCollect(time int) {
go spider.AutoCollect(time)
}
// ZeroCollect 数据清除从零开始采集
func (sl *SpiderLogic) ZeroCollect(time int) {
go spider.StarZero(time)
}
// FilmClassCollect 影视分类采集, 直接覆盖当前分类数据
func (sl *SpiderLogic) FilmClassCollect() error {
l := system.GetCollectSourceListByGrade(system.MasterCollect)
if l == nil {
return errors.New("未获取到主采集站信息")
}
// 获取主站点信息, 只取第一条有效
for _, fs := range l {
if fs.State {
go spider.CollectCategory(&fs)
return nil
}
}
return errors.New("未获取到已启用的主采集站信息")
}

66
server/logic/UserLogic.go Normal file
View File

@@ -0,0 +1,66 @@
package logic
import (
"errors"
"server/model/system"
"server/plugin/common/util"
)
type UserLogic struct {
}
var UL *UserLogic
// UserLogin 用户登录
func (ul *UserLogic) UserLogin(account, password string) (token string, err error) {
// 根据 username 或 email 查询用户信息
var u *system.User = system.GetUserByNameOrEmail(account)
// 用户信息不存在则返回提示信息
if u == nil {
return "", errors.New(" 用户信息不存在!!!")
}
// 校验用户信息, 执行账号密码校验逻辑
if util.PasswordEncrypt(password, u.Salt) != u.Password {
return "", errors.New("用户名或密码错误")
}
// 密码校验成功后下发token
token, err = system.GenToken(u.ID, u.UserName)
err = system.SaveUserToken(token, u.ID)
return
}
// UserLogout 用户退出登录 注销
func (ul *UserLogic) UserLogout() {
// 通过用户ID清除Redis中的token信息
}
// ChangePassword 修改密码
func (ul *UserLogic) ChangePassword(account, password, newPassword string) error {
// 根据 username 或 email 查询用户信息
var u *system.User = system.GetUserByNameOrEmail(account)
// 用户信息不存在则返回提示信息
if u == nil {
return errors.New(" 用户信息不存在!!!")
}
// 首先校验用户的旧密码是否正确
if util.PasswordEncrypt(password, u.Salt) != u.Password {
return errors.New("原密码校验失败")
}
// 密码校验正确则生成新的用户信息
newUser := system.User{}
newUser.ID = u.ID
// 将新密码进行加密
newUser.Password = util.PasswordEncrypt(newPassword, u.Salt)
// 更新用户信息
system.UpdateUserInfo(newUser)
return nil
}
func (ul *UserLogic) GetUserInfo(id uint) system.UserInfoVo {
// 通过用户ID查询对应的用户信息
u := system.GetUserById(id)
// 去除user信息中的不必要信息
var vo = system.UserInfoVo{Id: u.ID, UserName: u.UserName, Email: u.Email, Gender: u.Gender, NickName: u.NickName, Avatar: u.Avatar, Status: u.Status}
return vo
}

View File

@@ -3,7 +3,8 @@ package main
import (
"fmt"
"server/config"
"server/model"
"server/model/system"
"server/plugin/SystemInit"
"server/plugin/db"
"server/plugin/spider"
"server/router"
@@ -24,26 +25,39 @@ func init() {
panic(err)
}
}
func main() {
start()
}
func start() {
// 开启前先判断是否需要执行Spider
ExecSpider()
//ExecSpider()
// web服务启动后开启定时任务, 用于定期更新资源
spider.RegularUpdateMovie()
//spider.RegularUpdateMovie()
// 启动前先执行数据库内容的初始化工作
DefaultDataInit()
// 开启路由监听
r := router.SetupRouter()
_ = r.Run(fmt.Sprintf(":%s", config.ListenerPort))
}
func ExecSpider() {
// 判断分类信息是否存在
isStart := model.ExistsCategoryTree()
isStart := system.ExistsCategoryTree()
// 如果分类信息不存在则进行一次完整爬取
if !isStart {
DefaultDataInit()
spider.StartSpider()
}
}
func DefaultDataInit() {
// 初始化影视来源列表信息
SystemInit.SpiderInit()
// 初始化数据库相关数据
SystemInit.TableInIt()
// 初始化网站基本配置信息
SystemInit.BasicConfigInit()
}

View File

@@ -1,11 +0,0 @@
package model
type SearchTagsVO struct {
Pid int64
Cid int64
Plot string
Area string
Language string
Year int64
Sort string
}

View File

@@ -0,0 +1,201 @@
package collect
import (
"encoding/json"
"encoding/xml"
"fmt"
"log"
"server/config"
"server/plugin/db"
)
/*
视频详情接口序列化 struct
*/
//-------------------------------------------------Json 格式-------------------------------------------------
// FilmDetailLPage 视频详情分页数据
type FilmDetailLPage struct {
Code int `json:"code"` // 响应状态码
Msg string `json:"msg"` // 数据类型
Page any `json:"page"` // 页码
PageCount int `json:"pagecount"` // 总页数
Limit any `json:"limit"` // 每页数据量
Total int `json:"total"` // 总数据量
List []FilmDetail `json:"list"` // 影片详情数据List集合
}
// FilmDetail 视频详情列表
type FilmDetail struct {
VodID int64 `json:"vod_id"` // 影片ID
TypeID int64 `json:"type_id"` // 分类ID
TypeID1 int64 `json:"type_id_1"` // 一级分类ID
GroupID int `json:"group_id"` // 用户组ID
VodName string `json:"vod_name"` // 影片名称
VodSub string `json:"vod_sub"` // 影片别名
VodEn string `json:"vod_en"` // 影片名中文拼音
VodStatus int64 `json:"vod_status"` // 影片状态
VodLetter string `json:"vod_letter"` // 影片名首字母(大写)
VodColor string `json:"vod_color"` // UI展示颜色
VodTag string `json:"vod_tag"` // 索引标签
VodClass string `json:"vod_class"` // 剧情分类标签
VodPic string `json:"vod_pic"` // 影片封面图
VodPicThumb string `json:"vod_pic_thumb"` // 缩略图
VodPicSlide string `json:"vod_pic_slide"` // 幻灯图片
VodPicScreenshot string `json:"vod_pic_screenshot"` // ?截图
VodActor string `json:"vod_actor"` // 演员名
VodDirector string `json:"vod_director"` // 导演
VodWriter string `json:"vod_writer"` // 作者
VodBehind string `json:"vod_behind"` // 幕后
VodBlurb string `json:"vod_blurb"` // 内容简介
VodRemarks string `json:"vod_remarks"` // 更新状态 ( 完结 || 更新值 xx集)
VodPubDate string `json:"vod_pubdate"` // 上映日期
VodTotal int64 `json:"vod_total"` // 总集数
VodSerial string `json:"vod_serial"` // 连载数
VodTv string `json:"vod_tv"` // 上映电视台
VodWeekday string `json:"vod_weekday"` // 节目周期
VodArea string `json:"vod_area"` // 地区
VodLang string `json:"vod_lang"` // 语言
VodYear string `json:"vod_year"` // 年代
VodVersion string `json:"vod_version"` // 画质版本 DVD || HD || 720P
VodState string `json:"vod_state"` // 影片类别 正片 || 花絮 || 预告
VodAuthor string `json:"vod_author"` // 编辑人员
VodJumpUrl string `json:"vod_jumpurl"` // 跳转url
VodTpl string `json:"vod_tpl"` // 独立模板
VodTplPlay string `json:"vod_tpl_play"` // 独立播放页模板
VodTplDown string `json:"vod_tpl_down"` // 独立下载页模板
VodIsEnd int64 `json:"vod_isend"` // 是否完结
VodLock int64 `json:"vod_lock"` // 锁定
VodLevel int64 `json:"vod_level"` // 推荐级别
VodCopyright int64 `json:"vod_copyright"` // 版权
VodPoints int64 `json:"vod_points"` // 积分
VodPointsPlay int64 `json:"vod_points_play"` // 点播付费
VodPointsDown int64 `json:"vod_points_down"` // 下载付费
VodHits int64 `json:"vod_hits"` // 总点击量
VodHitsDay int64 `json:"vod_hits_day"` // 日点击量
VodHitsWeek int64 `json:"vod_hits_week"` // 周点击量
VodHitsMonth int64 `json:"vod_hits_month"` // 月点击量
VodDuration string `json:"vod_duration"` // 时长
VodUp int64 `json:"vod_up"` // 顶数
VodDown int64 `json:"vod_down"` // 踩数
VodScore string `json:"vod_score"` // 平均分
VodScoreAll int64 `json:"vod_score_all"` // 总评分
VodScoreNum int64 `json:"vod_score_num"` // 评分次数
VodTime string `json:"vod_time"` // 更新时间
VodTimeAdd int64 `json:"vod_time_add"` // 添加时间
VodTimeHits int64 `json:"vod_time_hits"` // 点击时间
VodTimeMake int64 `json:"vod_time_make"` // 生成时间
VodTrySee int64 `json:"vod_trysee"` // 试看时长
VodDouBanID int64 `json:"vod_douban_id"` // 豆瓣ID
VodDouBanScore string `json:"vod_douban_score"` // 豆瓣评分
VodReRrl string `json:"vod_reurl"` // 来源地址
VodRelVod string `json:"vod_rel_vod"` // 关联视频ids
VodRelArt string `json:"vod_rel_art"` // 关联文章 ids
VodPwd string `json:"vod_pwd"` // 访问内容密码
VodPwdURL string `json:"vod_pwd_url"` // 访问密码连接
VodPwdPlay string `json:"vod_pwd_play"` // 访问播放页密码
VodPwdPlayURL string `json:"vod_pwd_play_url"` // 获取访问密码连接
VodPwdDown string `json:"vod_pwd_down"` // 访问下载页密码
VodPwdDownURL string `json:"vod_pwd_down_url"` // 获取下载密码连接
VodContent string `json:"vod_content"` // 详细介绍
VodPlayFrom string `json:"vod_play_from"` // 播放组
VodPlayServer string `json:"vod_play_server"` // 播放组服务器
VodPlayNote string `json:"vod_play_note"` // 播放组备注 (分隔符)
VodPlayURL string `json:"vod_play_url"` // 播放地址
VodDownFrom string `json:"vod_down_from"` // 下载组
VodDownServer string `json:"vod_down_server"` // 瞎子服务器组
VodDownNote string `json:"vod_down_note"` // 下载备注 (分隔符)
VodDownURL string `json:"vod_down_url"` // 下载地址
VodPlot int64 `json:"vod_plot"` // 是否包含分级剧情
VodPlotName string `json:"vod_plot_name"` // 分类剧情名称
VodPlotDetail string `json:"vod_plot_detail"` // 分集剧情详情
TypeName string `json:"type_name"` // 分类名称
}
//-------------------------------------------------Xml 格式-------------------------------------------------
type RssD struct {
XMLName xml.Name `xml:"rss"`
Version string `xml:"version,attr"`
List FilmDetailPageX `xml:"list"`
}
type CDATA struct {
Text string `xml:",cdata"`
}
type FilmDetailPageX struct {
XMLName xml.Name `xml:"list"`
Page string `xml:"page,attr"`
PageCount int `xml:"pagecount,attr"`
PageSize string `xml:"pagesize,attr"`
RecordCount int `xml:"recordcount,attr"`
Videos []VideoDetail `xml:"video"`
}
type VideoDetail struct {
XMLName xml.Name `xml:"video"`
Last string `xml:"last"`
ID int64 `xml:"id"`
Tid int64 `xml:"tid"`
Name CDATA `xml:"name"`
Type string `xml:"type"`
Pic string `xml:"pic"`
Lang string `xml:"lang"`
Area string `xml:"area"`
Year string `xml:"year"`
State string `xml:"state"`
Note CDATA `xml:"note"`
Actor CDATA `xml:"actor"`
Director CDATA `xml:"director"`
DL DL `xml:"dl"`
Des CDATA `xml:"des"`
}
type DL struct {
XMLName xml.Name `xml:"dl"`
DD []DD `xml:"dd"`
}
type DD struct {
XMLName xml.Name `xml:"dd"`
Flag string `xml:"flag,attr"`
Value string `xml:",cdata"`
}
//-------------------------------------------------Json 格式-------------------------------------------------
// BatchSaveOriginalDetail 批量保存原始影片详情数据
func BatchSaveOriginalDetail(dl []FilmDetail) {
for _, d := range dl {
SaveOriginalDetail(d)
}
}
// SaveOriginalDetail 保存未处理的完整影片详情信息到redis
func SaveOriginalDetail(fd FilmDetail) {
data, err := json.Marshal(fd)
if err != nil {
log.Println("Json Marshal FilmDetail Error: ", err)
}
if err = db.Rdb.Set(db.Cxt, fmt.Sprintf(config.OriginalFilmDetailKey, fd.VodID), data, config.ResourceExpired).Err(); err != nil {
log.Println("Save Original FilmDetail Error: ", err)
}
}
// GetOriginalDetailById 获取原始的影片详情数据
func GetOriginalDetailById(id int64) (FilmDetail, error) {
data, err := db.Rdb.Get(db.Cxt, fmt.Sprintf(config.OriginalFilmDetailKey, id)).Result()
if err != nil {
log.Println("Get OriginalDetail Fail: ", err)
}
var fd = FilmDetail{}
err = json.Unmarshal([]byte(data), &fd)
if err != nil {
log.Println("json.Unmarshal OriginalDetail Fail: ", err)
return fd, err
}
return fd, nil
}

View File

@@ -0,0 +1,109 @@
package collect
import (
"encoding/json"
"encoding/xml"
"server/config"
"server/plugin/db"
)
/*
视频列表接口序列化 struct
*/
//-------------------------------------------------Json 格式-------------------------------------------------
// CommonPage 影视列表接口分页数据结构体
type CommonPage struct {
Code int `json:"code"` // 响应状态码
Msg string `json:"msg"` // 数据类型
Page any `json:"page"` // 页码
PageCount int `json:"pagecount"` // 总页数
Limit any `json:"limit"` // 每页数据量
Total int `json:"total"` // 总数据量
}
// FilmListPage 影视列表接口分页数据结构体
type FilmListPage struct {
Code int `json:"code"` // 响应状态码
Msg string `json:"msg"` // 数据类型
Page any `json:"page"` // 页码
PageCount int `json:"pagecount"` // 总页数
Limit any `json:"limit"` // 每页数据量
Total int `json:"total"` // 总数据量
List []FilmList `json:"list"` // 影片列表数据List集合
Class []FilmClass `json:"class"` // 影片分类信息
}
// FilmList 影视列表单部影片信息结构体
type FilmList struct {
VodID int64 `json:"vod_id"` // 影片ID
VodName string `json:"vod_name"` // 影片名称
TypeID int64 `json:"type_id"` // 分类ID
TypeName string `json:"type_name"` // 分类名称
VodEn string `json:"vod_en"` // 影片名中文拼音
VodTime string `json:"vod_time"` // 更新时间
VodRemarks string `json:"vod_remarks"` // 更新状态
VodPlayFrom string `json:"vod_play_from"` // 播放来源
}
// FilmClass 影视分类信息结构体
type FilmClass struct {
TypeID int64 `json:"type_id"` // 分类ID
TypePid int64 `json:"type_pid"` // 父级ID
TypeName string `json:"type_name"` // 类型名称
}
//-------------------------------------------------Xml 格式-------------------------------------------------
type RssL struct {
XMLName xml.Name `xml:"rss"`
Version string `xml:"version,attr"`
List FilmListPageX `xml:"list"`
ClassXL ClassXL `xml:"class"`
}
type FilmListPageX struct {
XMLName xml.Name `xml:"list"`
Page any `xml:"page,attr"`
PageCount int `xml:"pagecount,attr"`
PageSize any `xml:"pagesize,attr"`
RecordCount int `xml:"recordcount,attr"`
Videos []VideoList `xml:"video"`
}
type VideoList struct {
Last string `xml:"last"`
ID int64 `xml:"id"`
Tid int64 `xml:"tid"`
Name CDATA `xml:"name"`
Type string `xml:"type"`
Dt string `xml:"dt"`
Note CDATA `xml:"note"`
}
type ClassXL struct {
XMLName xml.Name `xml:"class"`
ClassX []ClassX `xml:"ty"`
}
type ClassX struct {
XMLName xml.Name `xml:"ty"`
ID int64 `xml:"id,attr"`
Value string `xml:",chardata"`
}
//-------------------------------------------------redis Func-------------------------------------------------
// SaveFilmClass 保存影片分类列表信息
func SaveFilmClass(list []FilmClass) error {
data, _ := json.Marshal(list)
return db.Rdb.Set(db.Cxt, config.FilmClassKey, data, config.ResourceExpired).Err()
}
// GetFilmClass 获取分类列表信息
func GetFilmClass() []FilmClass {
var l []FilmClass
data := db.Rdb.Get(db.Cxt, config.FilmClassKey).Val()
_ = json.Unmarshal([]byte(data), &l)
return l
}

View File

@@ -1,4 +1,4 @@
package model
package system
import (
"encoding/json"
@@ -12,6 +12,7 @@ type Category struct {
Id int64 `json:"id"` // 分类ID
Pid int64 `json:"pid"` // 父级分类ID
Name string `json:"name"` // 分类名称
Show bool `json:"show"` // 是否展示
}
// CategoryTree 分类信息树形结构
@@ -20,9 +21,12 @@ type CategoryTree struct {
Children []*CategoryTree `json:"children"` // 子分类信息
}
// 影视分类展示树形结构
// SaveCategoryTree 保存影片分类信息
func SaveCategoryTree(tree string) error {
return db.Rdb.Set(db.Cxt, config.CategoryTreeKey, tree, config.CategoryTreeExpired).Err()
func SaveCategoryTree(tree *CategoryTree) error {
data, _ := json.Marshal(tree)
return db.Rdb.Set(db.Cxt, config.CategoryTreeKey, data, config.CategoryTreeExpired).Err()
}
// GetCategoryTree 获取影片分类信息

View File

@@ -0,0 +1,176 @@
package system
import (
"encoding/json"
"errors"
"fmt"
"github.com/redis/go-redis/v9"
"log"
"server/config"
"server/plugin/common/util"
"server/plugin/db"
)
/*
影视采集站点信息
*/
type SourceGrade int
const (
MasterCollect SourceGrade = iota
SlaveCollect
)
type CollectResultModel int
const (
JsonResult CollectResultModel = iota
XmlResult
)
type ResourceType int
func (rt ResourceType) GetActionType() string {
var ac string = ""
switch rt {
case CollectVideo:
ac = "detail"
case CollectArticle:
ac = "article"
case CollectActor:
ac = "actor"
case CollectRole:
ac = "role"
case CollectWebSite:
ac = "web"
default:
ac = "detail"
}
return ac
}
const (
CollectVideo = iota
CollectArticle
CollectActor
CollectRole
CollectWebSite
)
// FilmSource 影视站点信息保存结构体
type FilmSource struct {
Id string `json:"id"` // 唯一ID
Name string `json:"name"` // 采集站点备注名
Uri string `json:"uri"` // 采集链接
ResultModel CollectResultModel `json:"resultModel"` // 接口返回类型, json || xml
Grade SourceGrade `json:"grade"` // 采集站等级 主站点 || 附属站
SyncPictures bool `json:"syncPictures"` // 是否同步图片到服务器
CollectType ResourceType `json:"collectType"` // 采集资源类型
State bool `json:"state"` // 是否启用
}
// SaveCollectSourceList 保存采集站Api列表
func SaveCollectSourceList(list []FilmSource) error {
var zl []redis.Z
for _, v := range list {
m, _ := json.Marshal(v)
zl = append(zl, redis.Z{Score: float64(v.Grade), Member: m})
}
return db.Rdb.ZAdd(db.Cxt, config.FilmSourceListKey, zl...).Err()
}
// GetCollectSourceList 获取采集站API列表
func GetCollectSourceList() []FilmSource {
l, err := db.Rdb.ZRange(db.Cxt, config.FilmSourceListKey, 0, -1).Result()
if err != nil {
log.Println(err)
return nil
}
return getCollectSource(l)
}
// GetCollectSourceListByGrade 返回指定类型的采集Api信息 Master | Slave
func GetCollectSourceListByGrade(grade SourceGrade) []FilmSource {
s := fmt.Sprintf("%d", grade)
zl, err := db.Rdb.ZRangeByScore(db.Cxt, config.FilmSourceListKey, &redis.ZRangeBy{Max: s, Min: s}).Result()
if err != nil {
log.Println(err)
return nil
}
return getCollectSource(zl)
}
// FindCollectSourceById 通过Id标识获取对应的资源站信息
func FindCollectSourceById(id string) *FilmSource {
for _, v := range GetCollectSourceList() {
if v.Id == id {
return &v
}
}
return nil
}
// 将 []string 转化为 []FilmSourceApi
func getCollectSource(sl []string) []FilmSource {
var l []FilmSource
for _, s := range sl {
f := FilmSource{}
_ = json.Unmarshal([]byte(s), &f)
l = append(l, f)
}
return l
}
// DelCollectResource 通过Id删除对应的采集站点信息
func DelCollectResource(id string) {
for _, v := range GetCollectSourceList() {
if v.Id == id {
data, _ := json.Marshal(v)
db.Rdb.ZRem(db.Cxt, config.FilmSourceListKey, data)
}
}
}
// AddCollectSource 添加采集站信息
func AddCollectSource(s FilmSource) error {
for _, v := range GetCollectSourceList() {
if v.Uri == s.Uri {
return errors.New("当前采集站点信息已存在, 请勿重复添加")
}
}
// 生成一个短uuid
s.Id = util.GenerateSalt()
data, _ := json.Marshal(s)
return db.Rdb.ZAddNX(db.Cxt, config.FilmSourceListKey, redis.Z{Score: float64(s.Grade), Member: data}).Err()
}
// UpdateCollectSource 更新采集站信息
func UpdateCollectSource(s FilmSource) error {
for _, v := range GetCollectSourceList() {
if v.Id != s.Id && v.Uri == s.Uri {
return errors.New("当前采集站链接已存在其他站点中, 请勿重复添加")
} else if v.Id == s.Id {
// 删除当前旧的采集信息
DelCollectResource(s.Id)
// 将新的采集信息存入list中
data, _ := json.Marshal(s)
db.Rdb.ZAdd(db.Cxt, config.FilmSourceListKey, redis.Z{Score: float64(s.Grade), Member: data})
}
}
return nil
}
// ClearAllCollectSource 删除所有采集站信息
func ClearAllCollectSource() {
db.Rdb.Del(db.Cxt, config.FilmSourceListKey)
}
// ExistCollectSourceList 查询是否已经存在站点list相关数据
func ExistCollectSourceList() bool {
if db.Rdb.Exists(db.Cxt, config.FilmSourceListKey).Val() == 0 {
return false
}
return true
}

View File

@@ -0,0 +1,70 @@
package system
import (
"encoding/json"
"errors"
"github.com/robfig/cron/v3"
"server/config"
"server/plugin/db"
)
/*
定时任务持久化
*/
// FilmCollectTask 影视采集任务
type FilmCollectTask struct {
Id string `json:"id"` // 唯一标识uid
Ids []string `json:"ids"` // 采集站id列表
Cid cron.EntryID `json:"cid"` // 定时任务Id
Time int `json:"time"` // 采集时长, 最新x小时更新的内容
Spec string `json:"spec"` // 执行周期 cron表达式
Model int `json:"model"` // 任务类型, 0 - 自动更新已启用站点 || 1 - 更新Ids中的资源站数据
State bool `json:"state"` // 状态 开启 | 禁用
Remark string `json:"remark"` // 任务备注信息
}
// SaveFilmTask 保存影视采集任务信息 {EntryId:FilmCollectTask}
func SaveFilmTask(t FilmCollectTask) {
data, _ := json.Marshal(t)
db.Rdb.HSet(db.Cxt, config.FilmCrontabKey, t.Id, data)
}
// GetAllFilmTask 获取所有的任务信息
func GetAllFilmTask() []FilmCollectTask {
var tl []FilmCollectTask
tMap := db.Rdb.HGetAll(db.Cxt, config.FilmCrontabKey).Val()
for _, v := range tMap {
var t = FilmCollectTask{}
_ = json.Unmarshal([]byte(v), &t)
tl = append(tl, t)
}
return tl
}
// GetFilmTaskById 通过Id获取当前任务信息
func GetFilmTaskById(id string) (FilmCollectTask, error) {
var ft = FilmCollectTask{}
// 如果Id对应的task不存在则返回错误信息
if !db.Rdb.HExists(db.Cxt, config.FilmCrontabKey, id).Val() {
return ft, errors.New(" The task does not exist ")
}
data := db.Rdb.HGet(db.Cxt, config.FilmCrontabKey, id).Val()
err := json.Unmarshal([]byte(data), &ft)
return ft, err
}
// UpdateFilmTask 更新定时任务信息(直接覆盖Id对应的定时任务信息) -- 后续待调整
func UpdateFilmTask(t FilmCollectTask) {
SaveFilmTask(t)
}
// DelFilmTask 通过Id删除对应的定时任务信息
func DelFilmTask(id string) {
db.Rdb.HDel(db.Cxt, config.FilmCrontabKey, id)
}
// ExistTask 是否存在定时任务相关信息
func ExistTask() bool {
return db.Rdb.Exists(db.Cxt, config.FilmCrontabKey).Val() == 1
}

View File

@@ -0,0 +1,160 @@
package system
import (
"encoding/json"
"fmt"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
"log"
"regexp"
"server/config"
"server/plugin/common/util"
"server/plugin/db"
)
// Picture 图片信息对象
type Picture struct {
gorm.Model
Link string `json:"link"` // 图片链接
Uid int `json:"uid"` // 上传人ID
RelevanceId int64 `json:"relevanceId"` // 关联资源ID
PicType int `json:"picType"` // 图片类型 (0 影片封面, 1 用户头像)
PicUid string `json:"picUid"` // 图片唯一标识, 通常为文件名
//Size int `json:"size"` // 图片大小
}
// VirtualPicture 采集入站,待同步的图片信息
type VirtualPicture struct {
Id int64 `json:"id"`
Link string `json:"link"`
}
//------------------------------------------------本地图库------------------------------------------------
// TableName 设置图片存储表的表名
func (p *Picture) TableName() string {
return config.PictureTableName
}
// CreatePictureTable 创建图片关联信息存储表
func CreatePictureTable() {
// 如果不存在则创建表 并设置自增ID初始值为10000
if !ExistPictureTable() {
err := db.Mdb.AutoMigrate(&Picture{})
if err != nil {
log.Println("Create Table Picture Failed: ", err)
}
}
}
// ExistPictureTable 是否存在Picture表
func ExistPictureTable() bool {
// 1. 判断表中是否存在当前表
return db.Mdb.Migrator().HasTable(&Picture{})
}
// SaveGallery 保存图片关联信息
func SaveGallery(p Picture) {
db.Mdb.Create(&p)
}
// ExistPictureByRid 查找图片信息是否存在
func ExistPictureByRid(rid int64) bool {
var count int64
db.Mdb.Model(&Picture{}).Where("relevance_id = ?", rid).Count(&count)
return count > 0
}
// GetPictureByRid 通过关联的资源id获取对应的图片信息
func GetPictureByRid(rid int64) Picture {
var p Picture
db.Mdb.Where("relevance_id = ?", rid).First(&p)
return p
}
func GetPicturePage(page *Page) []Picture {
var pl []Picture
query := db.Mdb.Model(&Picture{})
// 获取分页相关参数
GetPage(query, page)
// 获取分页数据
if err := query.Limit(page.PageSize).Offset((page.Current - 1) * page.PageSize).Find(&pl).Error; err != nil {
log.Println(err)
return nil
}
return pl
}
//------------------------------------------------图片同步------------------------------------------------
// SaveVirtualPic 保存待同步的图片信息
func SaveVirtualPic(pl []VirtualPicture) error {
// 保存对应的
var zl []redis.Z
for _, p := range pl {
// 首先查询 Gallery 表中是否存在当前ID对应的图片信息, 如果不存在则保存
if !ExistPictureByRid(p.Id) {
m, _ := json.Marshal(p)
zl = append(zl, redis.Z{Score: float64(p.Id), Member: m})
}
}
return db.Rdb.ZAdd(db.Cxt, config.VirtualPictureKey, zl...).Err()
}
// SyncFilmPicture 同步新采集入栈还未同步的图片
func SyncFilmPicture() {
// 扫描待同步图片的信息, 每次扫描count条
sl, cursor := db.Rdb.ZScan(db.Cxt, config.VirtualPictureKey, 0, "*", config.MaxScanCount).Val()
if len(sl) <= 0 {
return
}
// 获取 VirtualPicture
for i, s := range sl {
if i%2 == 0 {
// 获取图片信息
vp := VirtualPicture{}
_ = json.Unmarshal([]byte(s), &vp)
// 删除已经取出的数据
db.Rdb.ZRem(db.Cxt, config.VirtualPictureKey, []byte(s))
// 将图片同步到服务器
fileName, err := util.SaveOnlineFile(vp.Link, config.FilmPictureUploadDir)
if err != nil {
continue
}
// 完成同步后将图片信息保存到 Gallery 中
SaveGallery(Picture{
Link: fmt.Sprint(config.FilmPictureAccess, fileName),
Uid: config.UserIdInitialVal,
RelevanceId: vp.Id,
PicType: 0,
PicUid: regexp.MustCompile(`\.[^.]+$`).ReplaceAllString(fileName, ""),
})
}
}
// 如果 cursor != 0 则继续递归执行
if cursor > 0 {
SyncFilmPicture()
}
}
// ReplaceDetailPic 将影片详情中的图片地址替换为自己的
func ReplaceDetailPic(d *MovieDetail) {
// 查询影片对应的本地图片信息
if ExistPictureByRid(d.Id) {
// 如果存在关联的本地图片, 则查询对应的图片信息
p := GetPictureByRid(d.Id)
// 替换采集站的图片链接为本地链接
d.Picture = p.Link
}
}
// ReplaceBasicDetailPic 替换影片基本数据中的封面图为本地图片
func ReplaceBasicDetailPic(d *MovieBasicInfo) {
// 查询影片对应的本地图片信息
if ExistPictureByRid(d.Id) {
// 如果存在关联的本地图片, 则查询对应的图片信息
p := GetPictureByRid(d.Id)
// 替换采集站的图片链接为本地链接
d.Picture = p.Link
}
}

View File

@@ -0,0 +1,89 @@
package system
import (
"errors"
"fmt"
"github.com/golang-jwt/jwt/v5"
"log"
"server/config"
"server/plugin/common/util"
"server/plugin/db"
"time"
)
type UserClaims struct {
UserID uint `json:"userID"`
UserName string `json:"userName"`
jwt.RegisteredClaims
}
// GenToken 生成token
func GenToken(userId uint, userName string) (string, error) {
uc := UserClaims{
UserID: userId,
UserName: userName,
RegisteredClaims: jwt.RegisteredClaims{
Issuer: config.Issuer,
Subject: userName,
Audience: jwt.ClaimStrings{"Auth_All"},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(config.AuthTokenExpires * time.Hour)),
NotBefore: jwt.NewNumericDate(time.Now().Add(-10 * time.Second)),
IssuedAt: jwt.NewNumericDate(time.Now()),
ID: util.GenerateSalt(),
},
}
priKey, err := util.ParsePriKeyBytes([]byte(config.PrivateKey))
token, err := jwt.NewWithClaims(jwt.SigningMethodRS256, uc).SignedString(priKey)
return token, err
}
// ParseToken 解析token
func ParseToken(tokenStr string) (*UserClaims, error) {
token, err := jwt.ParseWithClaims(tokenStr, &UserClaims{}, func(token *jwt.Token) (interface{}, error) {
pub, err := util.ParsePubKeyBytes([]byte(config.PublicKey))
if err != nil {
return nil, err
}
return pub, nil
})
if err != nil {
if errors.Is(err, jwt.ErrTokenExpired) {
claims, _ := token.Claims.(*UserClaims)
return claims, err
}
return nil, err
}
// 验证token是否有效
if !token.Valid {
return nil, errors.New("token is invalid")
}
// 解析token中的claims内容
claims, ok := token.Claims.(*UserClaims)
if !ok {
return nil, errors.New("invalid claim type error")
}
return claims, err
}
// RefreshToken 刷新 token
// SaveUserToken 将用户登录成功后的token字符串存放到redis中
func SaveUserToken(token string, userId uint) error {
// 设置redis中token的过期时间为 token过期时间后的7天
return db.Rdb.Set(db.Cxt, fmt.Sprintf(config.UserTokenKey, userId), token, (config.AuthTokenExpires+7*24)*time.Hour).Err()
}
// GetUserTokenById 从redis中获取指定userId对应的token
func GetUserTokenById(userId uint) string {
token, err := db.Rdb.Get(db.Cxt, fmt.Sprintf(config.UserTokenKey, userId)).Result()
if err != nil {
log.Println(err)
return ""
}
return token
}
// ClearUserToken 清楚指定id的用户的登录信息
func ClearUserToken(userId uint) error {
return db.Rdb.Del(db.Cxt, fmt.Sprintf(config.UserTokenKey, userId)).Err()
}

View File

@@ -0,0 +1,36 @@
package system
import (
"encoding/json"
"log"
"server/config"
"server/plugin/db"
)
type BasicConfig struct {
SiteName string `json:"siteName"` // 网站名称
Domain string `json:"domain"` // 网站域名
Logo string `json:"logo"` // 网站logo
Keyword string `json:"keyword"` // seo关键字
Describe string `json:"describe"` // 网站描述信息
State bool `json:"state"` // 网站状态 开启 || 关闭
Hint string `json:"hint"` // 网站关闭提示
}
// ------------------------------------------------------ Redis ------------------------------------------------------
// SaveSiteBasic 保存网站基本配置信息
func SaveSiteBasic(c BasicConfig) error {
data, _ := json.Marshal(c)
return db.Rdb.Set(db.Cxt, config.SiteConfigBasic, data, config.ManageConfigExpired).Err()
}
// GetSiteBasic 获取网站基本配置信息
func GetSiteBasic() BasicConfig {
c := BasicConfig{}
data := db.Rdb.Get(db.Cxt, config.SiteConfigBasic).Val()
if err := json.Unmarshal([]byte(data), &c); err != nil {
log.Println("GetSiteBasic Err", err)
}
return c
}

View File

@@ -1,14 +1,12 @@
package model
package system
import (
"encoding/json"
"fmt"
"github.com/redis/go-redis/v9"
"hash/fnv"
"path/filepath"
"regexp"
"server/config"
"server/plugin/common/util"
"server/plugin/db"
"strconv"
"strings"
@@ -93,23 +91,6 @@ type MovieDetail struct {
// ===================================Redis数据交互========================================================
// SaveMoviePic 保存影片图片到服务器
func SaveMoviePic(details ...*MovieDetail) {
for _, d := range details {
// 判断 detail 在redis中是否已经存在
if db.Rdb.Exists(db.Cxt, fmt.Sprintf(config.MovieDetailKey, d.Cid, d.Id)).Val() == 1 {
// 如果已经存在则直接continue
continue
}
// 将影片信息中的pic图片下载保存到resource/images 文件夹下
err := util.SaveOnlineFile(d.Picture, config.ImageDir)
// 如果没有异常则将detail的图片路径替换为本地的保存路径
if err == nil {
d.Picture = fmt.Sprintf("http://127.0.0.1:%s/static/image/%s", config.ListenerPort, filepath.Base(d.Picture))
}
}
}
// SaveDetails 保存影片详情信息到redis中 格式: MovieDetail:Cid?:Id?
func SaveDetails(list []MovieDetail) (err error) {
// 遍历list中的信息
@@ -135,6 +116,27 @@ func SaveDetails(list []MovieDetail) (err error) {
return err
}
// SaveDetail 保存单部影片信息
func SaveDetail(detail MovieDetail) (err error) {
// 序列化影片详情信息
data, _ := json.Marshal(detail)
// 保存影片信息到Redis
err = db.Rdb.Set(db.Cxt, fmt.Sprintf(config.MovieDetailKey, detail.Cid, detail.Id), data, config.CategoryTreeExpired).Err()
if err != nil {
return err
}
// 2. 同步保存简略信息到redis中
SaveMovieBasicInfo(detail)
// 转换 detail信息
searchInfo := ConvertSearchInfo(detail)
// 3. 保存 Search tag redis中
// 只存储用于检索对应影片的关键字信息
SaveSearchTag(searchInfo)
// 保存影片检索信息到searchTable
err = SaveSearchInfo(searchInfo)
return err
}
// SaveMovieBasicInfo 摘取影片的详情部分信息转存为影视基本信息
func SaveMovieBasicInfo(detail MovieDetail) {
basicInfo := MovieBasicInfo{
@@ -194,12 +196,6 @@ func BatchSaveSearchInfo(list []MovieDetail) {
}
// 将检索信息存入redis中做一次转存
RdbSaveSearchInfo(infoList)
// 废弃方案, 频繁大量入库容易引起主键冲突, 事务影响速率
// 批量插入时应对已存在数据进行检测, 使用mysql事务进行锁表
//BatchSave(infoList)
// 使用批量添加or更新
//BatchSaveOrUpdate(infoList)
}
// ConvertSearchInfo 将detail信息处理成 searchInfo
@@ -239,6 +235,8 @@ func GetBasicInfoByKey(key string) MovieBasicInfo {
data := []byte(db.Rdb.Get(db.Cxt, key).Val())
basic := MovieBasicInfo{}
_ = json.Unmarshal(data, &basic)
// 执行本地图片匹配
ReplaceBasicDetailPic(&basic)
return basic
}
@@ -248,6 +246,9 @@ func GetDetailByKey(key string) MovieDetail {
data := []byte(db.Rdb.Get(db.Cxt, key).Val())
detail := MovieDetail{}
_ = json.Unmarshal(data, &detail)
// 执行本地图片匹配
ReplaceDetailPic(&detail)
return detail
}
@@ -258,6 +259,9 @@ func GetBasicInfoBySearchInfos(infos ...SearchInfo) []MovieBasicInfo {
data := []byte(db.Rdb.Get(db.Cxt, fmt.Sprintf(config.MovieBasicInfoKey, s.Cid, s.Mid)).Val())
basic := MovieBasicInfo{}
_ = json.Unmarshal(data, &basic)
// 执行本地图片匹配
ReplaceBasicDetailPic(&basic)
list = append(list, basic)
}
return list

View File

@@ -0,0 +1,76 @@
package system
import (
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"net/http"
)
/*
对 http response 做简单的封装
*/
const (
SUCCESS = 0
FAILED = -1
)
// Response http返回数据结构体
type Response struct {
Code int `json:"code"` // 状态 ok | failed
Data any `json:"data"` // 数据
Msg string `json:"msg"` // 提示信息
//Count int `json:"count"` // 内容长度
}
// PagingData 分页基本数据通用格式
type PagingData struct {
List []any `json:"list"`
Paging Page `json:"paging"`
}
// Page 分页信息结构体
type Page struct {
PageSize int `json:"pageSize"` // 每页大小
Current int `json:"current"` // 当前页
PageCount int `json:"pageCount"` // 总页数
Total int `json:"total"` // 总记录数
//List []interface{} `json:"list"` // 数据
}
// Result 构建response返回数据结构
func Result(code int, data any, msg string, c *gin.Context) {
c.JSON(http.StatusOK, Response{
Code: code,
Data: data,
Msg: msg,
})
}
// Success 成功响应 数据 + 成功提示
func Success(data any, message string, c *gin.Context) {
Result(SUCCESS, data, message, c)
}
// SuccessOnlyMsg 成功响应, 只返回成功信息
func SuccessOnlyMsg(message string, c *gin.Context) {
Result(SUCCESS, nil, message, c)
}
// Failed 响应失败 只返回错误信息
func Failed(message string, c *gin.Context) {
Result(FAILED, nil, message, c)
}
// FailedWithData 返回错误信息以及必要数据
func FailedWithData(data any, message string, c *gin.Context) {
Result(FAILED, data, message, c)
}
// GetPage 获取分页相关数据
func GetPage(db *gorm.DB, page *Page) {
var count int64
db.Count(&count)
page.Total = int(count)
page.PageCount = int((page.Total + page.PageSize - 1) / page.PageSize)
}

View File

@@ -1,4 +1,4 @@
package model
package system
/*
量子资源JSON解析

View File

@@ -1,4 +1,4 @@
package model
package system
import (
"encoding/json"
@@ -12,6 +12,7 @@ import (
"server/config"
"server/plugin/common/param"
"server/plugin/db"
"strconv"
"strings"
"time"
)
@@ -24,7 +25,7 @@ type SearchInfo struct {
Pid int64 `json:"pid"` //上级分类ID
Name string `json:"name"` // 片名
SubTitle string `json:"subTitle"` // 影片子标题
CName string `json:"CName"` // 分类名称
CName string `json:"cName"` // 分类名称
ClassTag string `json:"classTag"` //类型标签
Area string `json:"area"` // 地区
Language string `json:"language"` // 语言
@@ -38,15 +39,6 @@ type SearchInfo struct {
ReleaseStamp int64 `json:"releaseStamp"` //上映时间 时间戳
}
// Page 分页信息结构体
type Page struct {
PageSize int `json:"pageSize"` // 每页大小
Current int `json:"current"` // 当前页
PageCount int `json:"pageCount"` // 总页数
Total int `json:"total"` // 总记录数
//List []interface{} `json:"list"` // 数据
}
// Tag 影片分类标签结构体
type Tag struct {
Name string `json:"name"`
@@ -71,31 +63,31 @@ func RdbSaveSearchInfo(list []SearchInfo) {
db.Rdb.ZAdd(db.Cxt, config.SearchInfoTemp, members...)
}
// ScanSearchInfo 批量扫描处理详情检索信息, 返回检索信息列表和下次开始的游标
func ScanSearchInfo(cursor uint64, count int64) ([]SearchInfo, uint64) {
// 1.从redis中批量扫描详情信息
list, nextCursor := db.Rdb.ZScan(db.Cxt, config.SearchInfoTemp, cursor, "*", count).Val()
// 2. 处理数据
var resList []SearchInfo
for i, s := range list {
// 3. 判断当前是否是元素
if i%2 == 0 {
info := SearchInfo{}
_ = json.Unmarshal([]byte(s), &info)
info.Model = gorm.Model{}
resList = append(resList, info)
}
}
return resList, nextCursor
}
// RemoveAll 删除所有库存数据
func RemoveAll() {
// FilmZero 删除所有库存数据
func FilmZero() {
// 删除redis中当前库存储的所有数据
db.Rdb.FlushDB(db.Cxt)
//db.Rdb.FlushDB(db.Cxt)
db.Rdb.Del(db.Cxt, db.Rdb.Keys(db.Cxt, "MovieBasicInfoKey*").Val()...)
db.Rdb.Del(db.Cxt, db.Rdb.Keys(db.Cxt, "MovieDetail*").Val()...)
db.Rdb.Del(db.Cxt, db.Rdb.Keys(db.Cxt, "MultipleSource*").Val()...)
db.Rdb.Del(db.Cxt, db.Rdb.Keys(db.Cxt, "OriginalResource*").Val()...)
db.Rdb.Del(db.Cxt, db.Rdb.Keys(db.Cxt, "Search*").Val()...)
// 删除mysql中留存的检索表
var s *SearchInfo
//db.Mdb.Exec(fmt.Sprintf(`drop table if exists %s`, s.TableName()))
// 截断数据表 truncate table users
if ExistSearchTable() {
db.Mdb.Exec(fmt.Sprintf(`TRUNCATE table %s`, s.TableName()))
}
}
// ResetSearchTable 重置Search表
func ResetSearchTable() {
// 删除 Search 表
var s *SearchInfo
db.Mdb.Exec(fmt.Sprintf(`drop table if exists %s`, s.TableName()))
// 重新创建 Search 表
CreateSearchTable()
}
// DelMtPlay 清空附加播放源信息
@@ -207,7 +199,10 @@ func HandleSearchTags(preTags string, k string) {
f("、")
default:
// 获取 tag对应的score
if len(preTags) == 0 || preTags == "其它" {
if len(preTags) == 0 {
// 如果没有 tag信息则不进行缓存
//db.Rdb.ZAdd(db.Cxt, k, redis.Z{Score: 0, Member: fmt.Sprintf("%v:%v", "未知", "未知")})
} else if preTags == "其它" {
db.Rdb.ZAdd(db.Cxt, k, redis.Z{Score: 0, Member: fmt.Sprintf("%v:%v", preTags, preTags)})
} else {
score := db.Rdb.ZScore(db.Cxt, k, fmt.Sprintf("%v:%v", preTags, preTags)).Val()
@@ -226,10 +221,8 @@ func BatchHandleSearchTag(infos ...SearchInfo) {
// CreateSearchTable 创建存储检索信息的数据表
func CreateSearchTable() {
// 1. 判断表中是否存在当前表
isExist := db.Mdb.Migrator().HasTable(&SearchInfo{})
// 如果不存在则创建表
if !isExist {
if !ExistSearchTable() {
err := db.Mdb.AutoMigrate(&SearchInfo{})
if err != nil {
log.Println("Create Table SearchInfo Failed: ", err)
@@ -237,6 +230,11 @@ func CreateSearchTable() {
}
}
func ExistSearchTable() bool {
// 1. 判断表中是否存在当前表
return db.Mdb.Migrator().HasTable(&SearchInfo{})
}
// AddSearchIndex search表中数据保存完毕后 将常用字段添加索引提高查询效率
func AddSearchIndex() {
var s *SearchInfo
@@ -298,22 +296,38 @@ func BatchSaveOrUpdate(list []SearchInfo) {
tx.Commit()
}
// SaveSearchData 添加影片检索信息
func SaveSearchData(s SearchInfo) {
// SaveSearchInfo 添加影片检索信息
func SaveSearchInfo(s SearchInfo) error {
// 先查询数据库中是否存在对应记录
isExist := SearchMovieInfo(s.Mid)
// 如果不存在对应记录则 保存当前记录
if !isExist {
db.Mdb.Create(&s)
tx := db.Mdb.Begin()
if !ExistSearchInfo(s.Mid) {
// 执行插入操作
if err := tx.Create(&s).Error; err != nil {
tx.Rollback()
return err
}
// 执行添加操作时保存一份tag信息
BatchHandleSearchTag(s)
} else {
// 如果已经存在当前记录则将当前记录进行更新
err := tx.Model(&SearchInfo{}).Where("mid", s.Mid).Updates(SearchInfo{UpdateStamp: s.UpdateStamp, Hits: s.Hits, State: s.State,
Remarks: s.Remarks, Score: s.Score, ReleaseStamp: s.ReleaseStamp}).Error
if err != nil {
tx.Rollback()
return err
}
}
// 提交事务
tx.Commit()
return nil
}
// SearchMovieInfo 通过Mid查询符合条件的数据
func SearchMovieInfo(mid int64) bool {
search := SearchInfo{}
db.Mdb.Where("mid", mid).First(&search)
// reflect.DeepEqual(a, A{})
return !reflect.DeepEqual(search, SearchInfo{})
// ExistSearchInfo 通过Mid查询是否存在影片的检索信息
func ExistSearchInfo(mid int64) bool {
var count int64
db.Mdb.Model(&SearchInfo{}).Where("mid", mid).Count(&count)
return count > 0
}
// TunCateSearchTable 截断SearchInfo数据表
@@ -325,12 +339,56 @@ func TunCateSearchTable() {
}
}
// GetPage 获取分页相关数据
func GetPage(db *gorm.DB, page *Page) {
var count int64
db.Count(&count)
page.Total = int(count)
page.PageCount = int((page.Total + page.PageSize - 1) / page.PageSize)
// SyncSearchInfo 同步影片检索信息
func SyncSearchInfo(model int) {
switch model {
case 0:
// 重置Search表, (恢复为初始状态, 未添加索引)
ResetSearchTable()
// 批量添加 SearchInfo
SearchInfoToMdb(model)
// 保存完所有 SearchInfo 后添加字段索引
AddSearchIndex()
case 1:
// 批量更新或添加
SearchInfoToMdb(model)
}
}
// SearchInfoToMdb 扫描redis中的检索信息, 并批量存入mysql (model 执行模式 0-清空并保存 || 1-更新)
func SearchInfoToMdb(model int) {
// 1.从redis中批量扫描详情信息
list, cursor := db.Rdb.ZScan(db.Cxt, config.SearchInfoTemp, 0, "*", config.MaxScanCount).Val()
// 如果扫描到的信息为空则直接退出
if len(list) <= 0 {
return
}
// 2. 处理数据
var sl []SearchInfo
for i, s := range list {
// 3. 判断当前是否是元素
if i%2 == 0 {
info := SearchInfo{}
_ = json.Unmarshal([]byte(s), &info)
info.Model = gorm.Model{}
// 获取完则删除元素, 避免重复保存
db.Rdb.ZRem(db.Cxt, config.SearchInfoTemp, []byte(s))
sl = append(sl, info)
}
}
//
switch model {
case 0:
// 批量添加 SearchInfo
BatchSave(sl)
case 1:
// 批量更新或添加
BatchSaveOrUpdate(sl)
}
// 如果 SearchInfoTemp 依然存在数据, 则递归执行
if cursor > 0 {
SearchInfoToMdb(model)
}
}
// ================================= API 数据接口信息处理 =================================
@@ -597,7 +655,7 @@ func GetSearchInfosByTags(st SearchTagsVO, page *Page) []SearchInfo {
// 返回分页参数
GetPage(qw, page)
//
// 查询具体的searchInfo 分页数据
var sl []SearchInfo
if err := qw.Limit(page.PageSize).Offset((page.Current - 1) * page.PageSize).Find(&sl).Error; err != nil {
log.Println(err)
@@ -631,6 +689,76 @@ func GetMovieListBySort(t int, pid int64, page *Page) []MovieBasicInfo {
}
// ================================= Manage 管理后台 =================================
func GetSearchPage(s SearchVo) []SearchInfo {
// 构建 query查询条件
query := db.Mdb.Model(&SearchInfo{})
// 如果参数不为空则追加对应查询条件
if s.Name != "" {
query = query.Where("name LIKE ?", fmt.Sprintf("%%%s%%", s.Name))
}
// 分类ID为负数则默认不追加该条件
if s.Cid > 0 {
query = query.Where("cid = ?", s.Cid)
} else if s.Pid > 0 {
query = query.Where("pid = ?", s.Pid)
}
if s.Plot != "" {
query = query.Where("class_tag LIKE ?", fmt.Sprintf("%%%s%%", s.Plot))
}
if s.Area != "" {
query = query.Where("area = ?", s.Area)
}
if s.Language != "" {
query = query.Where("language = ?", s.Language)
}
if int(s.Year) > time.Now().Year()-12 {
query = query.Where("year = ?", s.Year)
}
switch s.Remarks {
case "完结":
query = query.Where("remarks IN ?", []string{"完结", "HD"})
case "":
default:
query = query.Not(map[string]interface{}{"remarks": []string{"完结", "HD"}})
}
if s.BeginTime > 0 {
query = query.Where("update_stamp >= ? ", s.BeginTime)
}
if s.EndTime > 0 {
query = query.Where("update_stamp <= ? ", s.EndTime)
}
// 返回分页参数
GetPage(query, s.Paging)
// 查询具体的数据
var sl []SearchInfo
if err := query.Limit(s.Paging.PageSize).Offset((s.Paging.Current - 1) * s.Paging.PageSize).Find(&sl).Error; err != nil {
log.Println(err)
return nil
}
return sl
}
// GetSearchOptions 获取全部影片的检索标签信息
func GetSearchOptions(pid int64) map[string]interface{} {
// 整合searchTag相关内容
titles := db.Rdb.HGetAll(db.Cxt, fmt.Sprintf(config.SearchTitle, pid)).Val()
// 处理单一分类的数据格式
tagMap := make(map[string]interface{})
for t, _ := range titles {
switch t {
// 只获取对应几个类型的标签
case "Plot", "Area", "Language", "Year":
tagMap[t] = HandleTagStr(t, GetTagsByTitle(pid, t)...)
default:
}
}
return tagMap
}
// ================================= 接口数据缓存 =================================
// DataCache API请求 数据缓存
@@ -654,3 +782,36 @@ func GetCacheData(key string) map[string]interface{} {
func RemoveCache(key string) {
db.Rdb.Del(db.Cxt, key)
}
// ================================= OpenApi请求处理 =================================
func FindFilmIds(params map[string]string, page *Page) ([]int64, error) {
var ids []int64
query := db.Mdb.Model(&SearchInfo{}).Select("mid")
for k, v := range params {
// 如果 v 为空则直接 continue
if len(v) <= 0 {
continue
}
switch k {
case "t":
if cid, err := strconv.ParseInt(v, 10, 64); err == nil {
query = query.Where("cid = ?", cid)
}
case "wd":
query = query.Where("name like ?", fmt.Sprintf("%%%s%%", v))
case "h":
if h, err := strconv.ParseInt(v, 10, 64); err == nil {
query = query.Where("update_stamp >= ?", time.Now().Unix()-h*3600)
}
}
}
// 返回分页参数
var count int64
query.Count(&count)
page.Total = int(count)
page.PageCount = int(page.Total+page.PageSize-1) / page.PageSize
// 返回满足条件的ids
err := query.Limit(page.PageSize).Offset(page.Current - 1).Order("update_stamp DESC").Find(&ids).Error
return ids, err
}

View File

@@ -0,0 +1,94 @@
package system
import (
"fmt"
"gorm.io/gorm"
"log"
"server/config"
"server/plugin/common/util"
"server/plugin/db"
)
type User struct {
gorm.Model
UserName string `json:"userName"` // 用户名
Password string `json:"password"` // 密码
Salt string `json:"salt"` // 盐值
Email string `json:"email"` // 邮箱
Gender int `json:"gender"` // 性别
NickName string `json:"nickName"` // 昵称
Avatar string `json:"avatar"` // 头像
Status int `json:"status"` // 状态
Reserve1 string `json:"reserve1"` // 预留字段 3
Reserve2 string `json:"reserve2"` // 预留字段 2
Reserve3 string `json:"reserve3"` // 预留字段 1
//LastLongTime time.Time `json:"LastLongTime"` // 最后登录时间
}
// TableName 设置user表的表名
func (u *User) TableName() string {
return config.UserTableName
}
// CreateUserTable 创建存储检索信息的数据表
func CreateUserTable() {
var u = &User{}
// 如果不存在则创建表 并设置自增ID初始值为10000
if !ExistUserTable() {
err := db.Mdb.AutoMigrate(u)
db.Mdb.Exec(fmt.Sprintf("alter table %s auto_Increment=%d", u.TableName(), config.UserIdInitialVal))
if err != nil {
log.Println("Create Table SearchInfo Failed: ", err)
}
}
}
// ExistUserTable 判断表中是否存在User表
func ExistUserTable() bool {
return db.Mdb.Migrator().HasTable(&User{})
}
// InitAdminAccount 初始化admin用户密码
func InitAdminAccount() {
// 先查询是否已经存在admin用户信息, 存在则直接退出
user := GetUserByNameOrEmail("admin")
if user != nil {
return
}
// 不存在管理员用户则进行初始化创建
u := &User{
UserName: "admin",
Password: "admin",
Salt: util.GenerateSalt(),
Email: "administrator@gmail.com",
Gender: 2,
NickName: "Zero",
Avatar: "empty",
Status: 0,
}
u.Password = util.PasswordEncrypt(u.Password, u.Salt)
db.Mdb.Create(u)
}
// GetUserByNameOrEmail 查询 username || email 对应的账户信息
func GetUserByNameOrEmail(userName string) *User {
var u *User
if err := db.Mdb.Where("user_name = ? OR email = ?", userName, userName).First(&u).Error; err != nil {
log.Println(err)
return nil
}
return u
}
func GetUserById(id uint) User {
var user = User{Model: gorm.Model{ID: id}}
db.Mdb.First(&user)
return user
}
// UpdateUserInfo 更新用户信息
func UpdateUserInfo(u User) {
// 值更新允许修改的部分字段, 零值会在更新时被自动忽略
db.Mdb.Model(&u).Updates(User{Password: u.Password, Email: u.Email, NickName: u.NickName, Status: u.Status})
}

View File

@@ -0,0 +1,102 @@
package system
type SearchTagsVO struct {
Pid int64 `json:"pid"`
Cid int64 `json:"cid"`
Plot string `json:"plot"`
Area string `json:"area"`
Language string `json:"language"`
Year int64 `json:"year"`
Sort string `json:"sort"`
}
// FilmCronVo 影视更新任务请求参数
type FilmCronVo struct {
Ids []string `json:"ids"` // 定时任务关联的资源站Id
Time int `json:"time"` // 更新最近几小时内更新的影片
Spec string `json:"spec"` // cron表达式
Model int `json:"model"` // 任务类型, 0 - 自动更新已启用站点 || 1 - 更新Ids中的资源站数据
State bool `json:"state"` // 任务状态 开启 | 关闭
Remark string `json:"remark"` // 备注信息
}
// CronTaskVo 定时任务数据response
type CronTaskVo struct {
FilmCollectTask
PreV string `json:"preV"` // 上次执行时间
Next string `json:"next"` // 下次执行时间
}
// FilmTaskOptions 影视采集任务添加时需要的options
type FilmTaskOptions struct {
Id string `json:"id"`
Name string `json:"name"`
}
// CollectParams 数据采集所需要的参数
type CollectParams struct {
Id string `json:"id"` // 资源站id
Ids []string `json:"ids"` // 资源站id列表
Time int `json:"time"` // 采集时长
Batch bool `json:"batch"` // 是否批量执行
}
// SearchVo 影片信息搜索参数
type SearchVo struct {
Name string `json:"name"` // 影片名
Pid int64 `json:"pid"` // 一级分类ID
Cid int64 `json:"cid"` // 二级分类ID
Plot string `json:"plot"` // 剧情
Area string `json:"area"` // 地区
Language string `json:"language"` // 语言
Year int64 `json:"year"` // 年份
//Score int64 `json:"score"` // 评分
Remarks string `json:"remarks"` // 完结 | 未完结
BeginTime int64 `json:"beginTime"` // 更新时间戳起始值
EndTime int64 `json:"endTime"` // 更新时间戳结束值
Paging *Page `json:"paging"` // 分页参数
}
// FilmDetailVo 添加影片对象
type FilmDetailVo struct {
Id int64 `json:"id"` // 影片id
Cid int64 `json:"cid"` //分类ID
Pid int64 `json:"pid"` //一级分类ID
Name string `json:"name"` //片名
Picture string `json:"picture"` //简介图片
PlayFrom []string `json:"playFrom"` // 播放来源
DownFrom string `json:"DownFrom"` //下载来源 例: http
PlayLink string `json:"playLink"` //播放地址url
DownloadLink string `json:"downloadLink"` // 下载url地址
SubTitle string `json:"subTitle"` //子标题
CName string `json:"cName"` //分类名称
EnName string `json:"enName"` //英文名
Initial string `json:"initial"` //首字母
ClassTag string `json:"classTag"` //分类标签
Actor string `json:"actor"` //主演
Director string `json:"director"` //导演
Writer string `json:"writer"` //作者
Remarks string `json:"remarks"` // 更新情况
ReleaseDate string `json:"releaseDate"` //上映时间
Area string `json:"area"` // 地区
Language string `json:"language"` //语言
Year string `json:"year"` //年份
State string `json:"state"` //影片状态 正片|预告...
UpdateTime string `json:"updateTime"` //更新时间
AddTime string `json:"addTime"` //资源添加时间戳
DbId int64 `json:"dbId"` //豆瓣id
DbScore string `json:"dbScore"` // 豆瓣评分
Hits int64 `json:"hits"` //影片热度
Content string `json:"content"` //内容简介
}
// UserInfoVo 用户信息返回对象
type UserInfoVo struct {
Id uint `json:"id"`
UserName string `json:"userName"` // 用户名
Email string `json:"email"` // 邮箱
Gender int `json:"gender"` // 性别
NickName string `json:"nickName"` // 昵称
Avatar string `json:"avatar"` // 头像
Status int `json:"status"` // 状态
}

View File

@@ -0,0 +1,15 @@
package SystemInit
import "server/model/system"
// TableInIt 初始化 mysql 数据库相关数据
func TableInIt() {
// 创建 User Table
system.CreateUserTable()
// 初始化管理员账户
system.InitAdminAccount()
// 创建 Search Table
system.CreateSearchTable()
// 创建图片信息管理表
system.CreatePictureTable()
}

View File

@@ -0,0 +1,89 @@
package SystemInit
import (
"log"
"server/config"
"server/model/system"
"server/plugin/common/util"
"server/plugin/spider"
)
// SpiderInit 数据采集相关信息初始化
func SpiderInit() {
FilmSourceInit()
CollectCrontabInit()
}
// FilmSourceInit 初始化预存站点信息 提供一些预存采集连Api链接
func FilmSourceInit() {
// 首先获取filmSourceList 数据, 如果存在则直接返回
if system.ExistCollectSourceList() {
return
}
var l []system.FilmSource = []system.FilmSource{
{Id: util.GenerateSalt(), Name: "HD(lzBk)", Uri: `https://cj.lzcaiji.com/api.php/provide/vod/`, ResultModel: system.JsonResult, Grade: system.SlaveCollect, SyncPictures: false, CollectType: system.CollectVideo, State: false},
{Id: util.GenerateSalt(), Name: "HD(bf)", Uri: `https://bfzyapi.com/api.php/provide/vod/`, ResultModel: system.JsonResult, Grade: system.SlaveCollect, SyncPictures: false, CollectType: system.CollectVideo, State: true},
{Id: util.GenerateSalt(), Name: "HD(ff)", Uri: `http://cj.ffzyapi.com/api.php/provide/vod/`, ResultModel: system.JsonResult, Grade: system.SlaveCollect, SyncPictures: false, CollectType: system.CollectVideo, State: true},
{Id: util.GenerateSalt(), Name: "HD(kk)", Uri: `https://kuaikan-api.com/api.php/provide/vod/from/kuaikan/`, ResultModel: system.JsonResult, Grade: system.SlaveCollect, SyncPictures: false, CollectType: system.CollectVideo, State: true},
{Id: util.GenerateSalt(), Name: "HD(sn)", Uri: `https://suoniapi.com/api.php/provide/vod/from/snm3u8/`, ResultModel: system.JsonResult, Grade: system.SlaveCollect, SyncPictures: false, CollectType: system.CollectVideo, State: true},
//{Id: util.GenerateSalt(), Name: "HD(lz)", Uri: `https://cj.lziapi.com/api.php/provide/vod/`, ResultModel: system.JsonResult, Grade: system.SlaveCollect, SyncPictures: false, CollectType: system.CollectVideo, State: true},
//{Id: util.GenerateSalt(), Name: "HD(fs)", Uri: `https://www.feisuzyapi.com/api.php/provide/vod/`, ResultModel: system.JsonResult, Grade: system.SlaveCollect, SyncPictures: false, CollectType: system.CollectVideo, State: true},
//{Id: util.GenerateSalt(), Name: "HD(bfApp)", Uri: `http://app.bfzyapi.com/api.php/provide/vod/`, ResultModel: system.JsonResult, Grade: system.SlaveCollect, SyncPictures: false, CollectType: system.CollectVideo, State: true},
//Id: util.GenerateSalt(), {Name: "HD(bfBk)", Uri: `http://by.bfzyapi.com/api.php/provide/vod/`, ResultModel: system.JsonResult, Grade: system.SlaveCollect, SyncPictures: false,CollectType:system.CollectVideo, State: false},
}
err := system.SaveCollectSourceList(l)
if err != nil {
log.Println("SaveSourceApiList Error: ", err)
}
}
// CollectCrontabInit 初始化系统预定义的定时任务
func CollectCrontabInit() {
// 如果系统已经存在Task定时任务信息,则直接返回
if system.ExistTask() {
// 将系统中的定时任务重新设置到 CollectCron中
for _, task := range system.GetAllFilmTask() {
switch task.Model {
case 0:
cid, err := spider.AddAutoUpdateCron(task.Id, task.Spec)
// 如果任务添加失败则直接返回错误信息
if err != nil {
log.Println("影视自动更新任务添加失败: ", err.Error())
continue
}
// 将新的定时任务Id记录到Task中
task.Cid = cid
case 1:
cid, err := spider.AddFilmUpdateCron(task.Id, task.Spec)
// 如果任务添加失败则直接返回错误信息
if err != nil {
log.Println("影视更新定时任务添加失败: ", err.Error())
continue
}
// 将定时任务Id记录到Task中
task.Cid = cid
}
system.UpdateFilmTask(task)
}
} else {
// 如果系统中不存在任何定时任务信息, 则添加默认的定时任务
// 1. 添加一条默认任务, 定时更新所有已启用站点的影片信息
// 生成任务信息
task := system.FilmCollectTask{Id: util.GenerateSalt(), Time: config.DefaultUpdateTime, Spec: config.DefaultUpdateSpec,
Model: 0, State: false, Remark: "每20分钟执行一次已启用站点数据的自动更新"}
// 添加一条定时任务
cid, err := spider.AddAutoUpdateCron(task.Id, task.Spec)
// 如果任务添加失败则直接返回错误信息
if err != nil {
log.Println("影视更新定时任务添加失败: ", err.Error())
return
}
// 将定时任务Id记录到Task中
task.Cid = cid
// 如果没有异常则将当前定时任务信息记录到redis中
system.SaveFilmTask(task)
}
// 完成初始化后启动 Cron
spider.CronCollect.Start()
}

View File

@@ -0,0 +1,22 @@
package SystemInit
import "server/model/system"
// SiteConfigInit 网站配置初始化
func SiteConfigInit() {
}
// BasicConfigInit 初始化网站基本配置信息
func BasicConfigInit() {
var bc = system.BasicConfig{
SiteName: "GoFilm",
Domain: "http://127.0.0.1:3600",
Logo: "https://s2.loli.net/2023/12/05/O2SEiUcMx5aWlv4.jpg",
Keyword: "在线视频, 免费观影",
Describe: "自动采集, 多播放源集成,在线观影网站",
State: true,
Hint: "网站升级中, 暂时无法访问 !!!",
}
_ = system.SaveSiteBasic(bc)
}

View File

@@ -0,0 +1,283 @@
package conver
import (
"encoding/xml"
"log"
"server/config"
"server/model/collect"
"server/model/system"
"strings"
)
/*
处理 不同结构体数据之间的转化
统一转化为内部结构体
*/
// GenCategoryTree 解析处理 filmListPage数据 生成分类树形数据
func GenCategoryTree(list []collect.FilmClass) *system.CategoryTree {
// 遍历所有分类进行树形结构组装
tree := &system.CategoryTree{Category: &system.Category{Id: 0, Pid: -1, Name: "分类信息", Show: true}}
temp := make(map[int64]*system.CategoryTree)
temp[tree.Id] = tree
for _, c := range list {
// 判断当前节点ID是否存在于 temp中
category, ok := temp[c.TypeID]
if ok {
// 将当前节点信息保存
category.Category = &system.Category{Id: c.TypeID, Pid: c.TypePid, Name: c.TypeName, Show: true}
} else {
// 如果不存在则将当前分类存放到 temp中
category = &system.CategoryTree{Category: &system.Category{Id: c.TypeID, Pid: c.TypePid, Name: c.TypeName, Show: true}}
temp[c.TypeID] = category
}
// 根据 pid获取父节点信息
parent, ok := temp[category.Pid]
if !ok {
// 如果不存在父节点存在, 则将父节点存放到temp中
temp[c.TypePid] = parent
}
// 将当前节点存放到父节点的Children中
parent.Children = append(parent.Children, category)
}
return tree
}
// ConvertCategoryList 将分类树形数据转化为list类型
func ConvertCategoryList(tree system.CategoryTree) []system.Category {
var cl = []system.Category{system.Category{Id: tree.Id, Pid: tree.Pid, Name: tree.Name, Show: tree.Show}}
for _, c := range tree.Children {
cl = append(cl, system.Category{Id: c.Id, Pid: c.Pid, Name: c.Name, Show: c.Show})
if c.Children != nil && len(c.Children) > 0 {
for _, subC := range c.Children {
cl = append(cl, system.Category{Id: subC.Id, Pid: subC.Pid, Name: subC.Name, Show: subC.Show})
}
}
}
return cl
}
// ConvertFilmDetails 批量处理影片详情信息
func ConvertFilmDetails(details []collect.FilmDetail) []system.MovieDetail {
var dl []system.MovieDetail
for _, d := range details {
dl = append(dl, ConvertFilmDetail(d))
}
return dl
}
// ConvertFilmDetail 将影片详情数据处理转化为 system.MovieDetail
func ConvertFilmDetail(detail collect.FilmDetail) system.MovieDetail {
md := system.MovieDetail{
Id: detail.VodID,
Cid: detail.TypeID,
Pid: detail.TypeID1,
Name: detail.VodName,
Picture: detail.VodPic,
DownFrom: detail.VodDownFrom,
MovieDescriptor: system.MovieDescriptor{
SubTitle: detail.VodSub,
CName: detail.TypeName,
EnName: detail.VodEn,
Initial: detail.VodLetter,
ClassTag: detail.VodClass,
Actor: detail.VodActor,
Director: detail.VodDirector,
Writer: detail.VodWriter,
Blurb: detail.VodBlurb,
Remarks: detail.VodRemarks,
ReleaseDate: detail.VodPubDate,
Area: detail.VodArea,
Language: detail.VodLang,
Year: detail.VodYear,
State: detail.VodState,
UpdateTime: detail.VodTime,
AddTime: detail.VodTimeAdd,
DbId: detail.VodDouBanID,
DbScore: detail.VodDouBanScore,
Hits: detail.VodHits,
Content: detail.VodContent,
},
}
// 通过分割符切分播放源信息 PlaySeparator $$$
md.PlayFrom = strings.Split(detail.VodPlayFrom, detail.VodPlayNote)
// v2 只保留m3u8播放源
md.PlayList = GenFilmPlayList(detail.VodPlayURL, detail.VodPlayNote)
md.DownloadList = GenFilmPlayList(detail.VodDownURL, detail.VodPlayNote)
return md
}
// GenFilmPlayList 处理影片播放地址数据, 只保留m3u8与mp4格式的链接,生成playList
func GenFilmPlayList(playUrl, separator string) [][]system.MovieUrlInfo {
var res [][]system.MovieUrlInfo
if separator != "" {
// 1. 通过分隔符切分播放源地址
for _, l := range strings.Split(playUrl, separator) {
// 2.只对m3u8播放源 和 .mp4下载地址进行处理
if strings.Contains(l, ".m3u8") || strings.Contains(l, ".mp4") {
// 2. 将每组播放源对应的播放列表信息存储到列表中
res = append(res, ConvertPlayUrl(l))
}
}
} else {
// 1.只对m3u8播放源 和 .mp4下载地址进行处理
if strings.Contains(playUrl, ".m3u8") || strings.Contains(playUrl, ".mp4") {
// 2. 将每组播放源对应的播放列表信息存储到列表中
res = append(res, ConvertPlayUrl(playUrl))
}
}
return res
}
// GenAllFilmPlayList 处理影片播放地址数据, 保留全部播放链接,生成playList
func GenAllFilmPlayList(playUrl, separator string) [][]system.MovieUrlInfo {
var res [][]system.MovieUrlInfo
if separator != "" {
// 1. 通过分隔符切分播放源地址
for _, l := range strings.Split(playUrl, separator) {
// 将playUrl中的所有播放格式链接均进行转换保存
res = append(res, ConvertPlayUrl(l))
}
return res
}
// 将playUrl中的所有播放格式链接均进行转换保存
res = append(res, ConvertPlayUrl(playUrl))
return res
}
// ConvertPlayUrl 将单个playFrom的播放地址字符串处理成列表形式
func ConvertPlayUrl(playUrl string) []system.MovieUrlInfo {
// 对每个片源的集数和播放地址进行分割 Episode$Link#Episode$Link
var l []system.MovieUrlInfo
for _, p := range strings.Split(playUrl, "#") {
// 处理 Episode$Link 形式的播放信息
if strings.Contains(p, "$") {
l = append(l, system.MovieUrlInfo{
Episode: strings.Split(p, "$")[0],
Link: strings.Split(p, "$")[1],
})
} else {
l = append(l, system.MovieUrlInfo{
Episode: "(`・ω・´)",
Link: p,
})
}
}
return l
}
// ConvertVirtualPicture 将影片详情信息转化为虚拟图片信息
func ConvertVirtualPicture(details []system.MovieDetail) []system.VirtualPicture {
var l []system.VirtualPicture
for _, d := range details {
if len(d.Picture) > 0 {
l = append(l, system.VirtualPicture{Id: d.Id, Link: d.Picture})
}
}
return l
}
// ----------------------------------Provide API---------------------------------------------------
// DetailCovertList 将影视详情信息转化为列表信息
func DetailCovertList(details []collect.FilmDetail) []collect.FilmList {
var l []collect.FilmList
for _, d := range details {
fl := collect.FilmList{
VodID: d.VodID,
VodName: d.VodName,
TypeID: d.TypeID,
TypeName: d.TypeName,
VodEn: d.VodEn,
VodTime: d.VodTime,
VodRemarks: d.VodRemarks,
VodPlayFrom: d.VodPlayFrom,
}
l = append(l, fl)
}
return l
}
// DetailCovertXml 将影片详情信息转化为Xml格式的对象
func DetailCovertXml(details []collect.FilmDetail) []collect.VideoDetail {
var vl []collect.VideoDetail
for _, d := range details {
vl = append(vl, collect.VideoDetail{
Last: d.VodTime,
ID: d.VodID,
Tid: d.TypeID,
Name: collect.CDATA{Text: d.VodName},
Type: d.TypeName,
Pic: d.VodPic,
Lang: d.VodLang,
Area: d.VodArea,
Year: d.VodYear,
State: d.VodState,
Note: collect.CDATA{Text: d.VodRemarks},
Actor: collect.CDATA{Text: d.VodActor},
Director: collect.CDATA{Text: d.VodDirector},
DL: collect.DL{DD: []collect.DD{collect.DD{Flag: d.VodPlayFrom, Value: d.VodPlayURL}}},
Des: collect.CDATA{Text: d.VodContent},
})
}
return vl
}
// DetailCovertListXml 将影片详情信息转化为Xml格式FilmList的对象
func DetailCovertListXml(details []collect.FilmDetail) []collect.VideoList {
var vl []collect.VideoList
for _, d := range details {
vl = append(vl, collect.VideoList{
Last: d.VodTime,
ID: d.VodID,
Tid: d.TypeID,
Name: collect.CDATA{Text: d.VodName},
Type: d.TypeName,
Dt: d.VodPlayFrom,
Note: collect.CDATA{Text: d.VodRemarks},
})
}
s, _ := xml.Marshal(vl[0])
log.Println(string(s))
return vl
}
// ClassListCovertXml 将影片分类列表转化为XML格式
func ClassListCovertXml(cl []collect.FilmClass) collect.ClassXL {
var l collect.ClassXL
for _, c := range cl {
l.ClassX = append(l.ClassX, collect.ClassX{ID: c.TypeID, Value: c.TypeName})
}
return l
}
// FilterFilmDetail 对影片详情数据进行处理, t 修饰类型 0-返回m3u8,mp4 | 1 返回 云播链接 | 2 返回全部
func FilterFilmDetail(fd collect.FilmDetail, t int64) collect.FilmDetail {
// 只保留 mu38 | mp4 格式的播放源, 如果包含多种格式的播放数据
if strings.Contains(fd.VodPlayURL, fd.VodPlayNote) {
switch t {
case 2:
fd.VodPlayFrom = config.PlayFormAll
case 1, 0:
for _, v := range strings.Split(fd.VodPlayURL, fd.VodPlayNote) {
if t == 0 && (strings.Contains(v, ".m3u8") || strings.Contains(v, ".mp4")) {
fd.VodPlayFrom = config.PlayForm
fd.VodPlayURL = v
} else if t == 1 && !strings.Contains(v, ".m3u8") && !strings.Contains(v, ".mp4") {
fd.VodPlayFrom = config.PlayFormCloud
fd.VodPlayURL = v
}
}
}
} else {
// 如果只有一种类型的播放链,则默认为m3u8 修改 VodPlayFrom 信息
fd.VodPlayFrom = config.PlayForm
}
return fd
}

View File

@@ -0,0 +1,53 @@
package conver
import (
"server/model/system"
"time"
)
/*
系统内部对象想换转换
*/
// CovertFilmDetailVo 将 FilmDetailVo 转化为 MovieDetail
func CovertFilmDetailVo(fd system.FilmDetailVo) (system.MovieDetail, error) {
t, err := time.ParseInLocation(time.DateTime, fd.AddTime, time.Local)
md := system.MovieDetail{
Id: fd.Id,
Cid: fd.Cid,
Pid: fd.Pid,
Name: fd.Name,
Picture: fd.Picture,
DownFrom: fd.DownFrom,
MovieDescriptor: system.MovieDescriptor{
SubTitle: fd.SubTitle,
CName: fd.CName,
EnName: fd.EnName,
Initial: fd.Initial,
ClassTag: fd.ClassTag,
Actor: fd.Actor,
Director: fd.Director,
Writer: fd.Writer,
Blurb: fd.Content,
Remarks: fd.Remarks,
ReleaseDate: fd.ReleaseDate,
Area: fd.Area,
Language: fd.Language,
Year: fd.Year,
State: fd.State,
UpdateTime: fd.UpdateTime,
AddTime: t.Unix(),
DbId: fd.DbId,
DbScore: fd.DbScore,
Hits: fd.Hits,
Content: fd.Content,
},
}
// 通过分割符切分播放源信息 PlaySeparator $$$
//md.PlayFrom = strings.Split(fd.VodPlayFrom, fd.VodPlayNote)
// v2 只保留m3u8播放源
md.PlayList = GenFilmPlayList(fd.PlayLink, "$$$")
//md.DownloadList = GenFilmPlayList(fd.DownloadLink, fd.VodPlayNote)
return md, err
}

View File

@@ -1,16 +1,16 @@
package dp
import (
"server/model"
"server/model/system"
)
// =================Spider数据处理=======================
// CategoryTree 组装树形菜单
func CategoryTree(list []model.ClassInfo) *model.CategoryTree {
func CategoryTree(list []system.ClassInfo) *system.CategoryTree {
// 遍历所有分类进行树形结构组装
tree := &model.CategoryTree{Category: &model.Category{Id: 0, Pid: -1, Name: "分类信息"}}
temp := make(map[int64]*model.CategoryTree)
tree := &system.CategoryTree{Category: &system.Category{Id: 0, Pid: -1, Name: "分类信息"}}
temp := make(map[int64]*system.CategoryTree)
temp[tree.Id] = tree
for _, c := range list {
@@ -18,10 +18,10 @@ func CategoryTree(list []model.ClassInfo) *model.CategoryTree {
category, ok := temp[c.Id]
if ok {
// 将当前节点信息保存
category.Category = &model.Category{Id: c.Id, Pid: c.Pid, Name: c.Name}
category.Category = &system.Category{Id: c.Id, Pid: c.Pid, Name: c.Name}
} else {
// 如果不存在则将当前分类存放到 temp中
category = &model.CategoryTree{Category: &model.Category{Id: c.Id, Pid: c.Pid, Name: c.Name}}
category = &system.CategoryTree{Category: &system.Category{Id: c.Id, Pid: c.Pid, Name: c.Name}}
temp[c.Id] = category
}
// 根据 pid获取父节点信息

View File

@@ -1,15 +1,15 @@
package dp
import (
"server/model"
"server/model/system"
"strings"
)
// ProcessMovieListInfo 处理影片列表中的信息
func ProcessMovieListInfo(list []model.MovieInfo) []model.Movie {
var movies []model.Movie
func ProcessMovieListInfo(list []system.MovieInfo) []system.Movie {
var movies []system.Movie
for _, info := range list {
movies = append(movies, model.Movie{
movies = append(movies, system.Movie{
Id: info.Id,
Name: info.Name,
Cid: info.Cid,
@@ -24,8 +24,8 @@ func ProcessMovieListInfo(list []model.MovieInfo) []model.Movie {
}
// ProcessMovieDetailList 处理影片详情列表数据
func ProcessMovieDetailList(list []model.MovieDetailInfo) []model.MovieDetail {
var detailList []model.MovieDetail
func ProcessMovieDetailList(list []system.MovieDetailInfo) []system.MovieDetail {
var detailList []system.MovieDetail
for _, d := range list {
detailList = append(detailList, ProcessMovieDetail(d))
}
@@ -33,15 +33,15 @@ func ProcessMovieDetailList(list []model.MovieDetailInfo) []model.MovieDetail {
}
// ProcessMovieDetail 处理单个影片详情信息
func ProcessMovieDetail(detail model.MovieDetailInfo) model.MovieDetail {
md := model.MovieDetail{
func ProcessMovieDetail(detail system.MovieDetailInfo) system.MovieDetail {
md := system.MovieDetail{
Id: detail.Id,
Cid: detail.Cid,
Pid: detail.Pid,
Name: detail.Name,
Picture: detail.Pic,
DownFrom: detail.DownFrom,
MovieDescriptor: model.MovieDescriptor{
MovieDescriptor: system.MovieDescriptor{
SubTitle: detail.SubTitle,
CName: detail.CName,
EnName: detail.EnName,
@@ -74,21 +74,21 @@ func ProcessMovieDetail(detail model.MovieDetailInfo) model.MovieDetail {
}
// ProcessPlayInfo 处理影片播放数据信息
func ProcessPlayInfo(info, sparator string) [][]model.MovieUrlInfo {
var res [][]model.MovieUrlInfo
func ProcessPlayInfo(info, separator string) [][]system.MovieUrlInfo {
var res [][]system.MovieUrlInfo
// 1. 通过分隔符区分多个片源数据
for _, l := range strings.Split(info, sparator) {
for _, l := range strings.Split(info, separator) {
// 2.对每个片源的集数和播放地址进行分割
var item []model.MovieUrlInfo
var item []system.MovieUrlInfo
for _, p := range strings.Split(l, "#") {
// 3. 处理 Episode$Link 形式的播放信息
if strings.Contains(p, "$") {
item = append(item, model.MovieUrlInfo{
item = append(item, system.MovieUrlInfo{
Episode: strings.Split(p, "$")[0],
Link: strings.Split(p, "$")[1],
})
} else {
item = append(item, model.MovieUrlInfo{
item = append(item, system.MovieUrlInfo{
Episode: "O(∩_∩)O",
Link: p,
})
@@ -101,24 +101,24 @@ func ProcessPlayInfo(info, sparator string) [][]model.MovieUrlInfo {
}
// ProcessPlayInfoV2 处理影片信息方案二 只保留m3u8播放源
func ProcessPlayInfoV2(info, sparator string) [][]model.MovieUrlInfo {
var res [][]model.MovieUrlInfo
if sparator != "" {
func ProcessPlayInfoV2(info, separator string) [][]system.MovieUrlInfo {
var res [][]system.MovieUrlInfo
if separator != "" {
// 1. 通过分隔符切分播放源地址
for _, l := range strings.Split(info, sparator) {
for _, l := range strings.Split(info, separator) {
// 只对m3u8播放源 和 .mp4下载地址进行处理
if strings.Contains(l, ".m3u8") || strings.Contains(l, ".mp4") {
// 2.对每个片源的集数和播放地址进行分割
var item []model.MovieUrlInfo
var item []system.MovieUrlInfo
for _, p := range strings.Split(l, "#") {
// 3. 处理 Episode$Link 形式的播放信息
if strings.Contains(p, "$") {
item = append(item, model.MovieUrlInfo{
item = append(item, system.MovieUrlInfo{
Episode: strings.Split(p, "$")[0],
Link: strings.Split(p, "$")[1],
})
} else {
item = append(item, model.MovieUrlInfo{
item = append(item, system.MovieUrlInfo{
Episode: "O(∩_∩)O",
Link: p,
})
@@ -132,16 +132,16 @@ func ProcessPlayInfoV2(info, sparator string) [][]model.MovieUrlInfo {
// 只对m3u8播放源 和 .mp4下载地址进行处理
if strings.Contains(info, ".m3u8") || strings.Contains(info, ".mp4") {
// 2.对每个片源的集数和播放地址进行分割
var item []model.MovieUrlInfo
var item []system.MovieUrlInfo
for _, p := range strings.Split(info, "#") {
// 3. 处理 Episode$Link 形式的播放信息
if strings.Contains(p, "$") {
item = append(item, model.MovieUrlInfo{
item = append(item, system.MovieUrlInfo{
Episode: strings.Split(p, "$")[0],
Link: strings.Split(p, "$")[1],
})
} else {
item = append(item, model.MovieUrlInfo{
item = append(item, system.MovieUrlInfo{
Episode: "O(∩_∩)O",
Link: p,
})

View File

@@ -2,8 +2,10 @@ package util
import (
"bufio"
"errors"
"os"
"path/filepath"
"server/config"
)
/*
@@ -11,11 +13,16 @@ import (
*/
// SaveOnlineFile 保存网络文件, 提供下载url和保存路径, 返回保存后的文件访问url相对路径
func SaveOnlineFile(url, dir string) (err error) {
func SaveOnlineFile(url, dir string) (path string, err error) {
// 请求获取文件内容
r := &RequestInfo{Uri: url}
ApiGet(r)
// 创建保存文件的目录
// 如果请求结果为空则直接跳过当前图片的同步, 等待后续触发时重试
if len(r.Resp) <= 0 {
err = errors.New("SyncPicture Failed: response is empty")
return
}
// 成功拿到图片数据 则创建保存文件的目录
if _, err = os.Stat(dir); os.IsNotExist(err) {
err = os.MkdirAll(dir, os.ModePerm)
if err != nil {
@@ -29,11 +36,18 @@ func SaveOnlineFile(url, dir string) (err error) {
return
}
defer file.Close()
//_, _ = file.Write(r.Resp)
// 将文件内容写入到file
writer := bufio.NewWriter(file)
_, err = writer.Write(r.Resp)
err = writer.Flush()
return
return filepath.Base(fileName), err
}
func CreateBaseDir() error {
// 如果不存在指定目录则创建该目录
if _, err := os.Stat(config.FilmPictureUploadDir); os.IsNotExist(err) {
return os.MkdirAll(config.FilmPictureUploadDir, os.ModePerm)
}
return nil
}

View File

@@ -36,7 +36,7 @@ func CreateClient() *colly.Collector {
c := colly.NewCollector()
// 设置请求使用clash的socks5代理
setProxy(c)
//setProxy(c)
// 设置代理信息
//if proxy, err := proxy.RoundRobinProxySwitcher("127.0.0.1:7890"); err != nil {
@@ -81,8 +81,8 @@ func ApiGet(r *RequestInfo) {
//extensions.Referer(Client)
// 请求成功后的响应
Client.OnResponse(func(response *colly.Response) {
// 将响应结构封装到 RequestInfo.Resp中
if len(response.Body) > 0 {
if (response.StatusCode == 200 || (response.StatusCode >= 300 && response.StatusCode <= 399)) && len(response.Body) > 0 {
// 将响应结构封装到 RequestInfo.Resp中
r.Resp = response.Body
} else {
r.Resp = []byte{}
@@ -99,6 +99,24 @@ func ApiGet(r *RequestInfo) {
}
}
// ApiTest 处理API请求后的数据, 主测试
func ApiTest(r *RequestInfo) error {
// 请求成功后的响应
Client.OnResponse(func(response *colly.Response) {
// 判断请求状态
if (response.StatusCode == 200 || (response.StatusCode >= 300 && response.StatusCode <= 399)) && len(response.Body) > 0 {
// 将响应结构封装到 RequestInfo.Resp中
r.Resp = response.Body
} else {
r.Resp = []byte{}
}
})
// 执行请求返回错误结果
err := Client.Visit(fmt.Sprintf("%s?%s", r.Uri, r.Params.Encode()))
log.Println(err)
return err
}
// 本地代理测试
func setProxy(c *colly.Collector) {
proxyUrl, _ := url.Parse("socks5://127.0.0.1:7890")

View File

@@ -0,0 +1,126 @@
package util
import (
"crypto/md5"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"log"
"net/url"
"regexp"
)
// GenerateUUID 生成UUID
func GenerateUUID() (uuid string) {
b := make([]byte, 16)
_, err := rand.Read(b)
if err != nil {
log.Fatal(err)
}
uuid = fmt.Sprintf("%X-%X-%X-%X-%X",
b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
return
}
// RandomString 生成指定长度两倍的随机字符串
func RandomString(length int) (uuid string) {
b := make([]byte, length)
_, err := rand.Read(b)
if err != nil {
log.Fatal(err)
}
uuid = fmt.Sprintf("%x", b)
return
}
// GenerateSalt 生成 length为16的随机字符串
func GenerateSalt() (uuid string) {
b := make([]byte, 8)
_, err := rand.Read(b)
if err != nil {
log.Fatal(err)
}
uuid = fmt.Sprintf("%X", b)
return
}
// PasswordEncrypt 密码加密 , (password+salt) md5 * 3
func PasswordEncrypt(password, salt string) string {
b := []byte(fmt.Sprint(password, salt)) // 将字符串转换为字节切片
var r [16]byte
for i := 0; i < 3; i++ {
r = md5.Sum(b) // 调用md5.Sum()函数进行加密
b = []byte(hex.EncodeToString(r[:]))
}
return hex.EncodeToString(r[:])
}
// ParsePriKeyBytes 解析私钥
func ParsePriKeyBytes(buf []byte) (*rsa.PrivateKey, error) {
p := &pem.Block{}
p, buf = pem.Decode(buf)
if p == nil {
return nil, errors.New("private key parse error")
}
return x509.ParsePKCS1PrivateKey(p.Bytes)
}
// ParsePubKeyBytes 解析公钥
func ParsePubKeyBytes(buf []byte) (*rsa.PublicKey, error) {
p, _ := pem.Decode(buf)
if p == nil {
return nil, errors.New("parse publicKey content nil")
}
pubKey, err := x509.ParsePKCS1PublicKey(p.Bytes)
if err != nil {
return nil, errors.New("x509.ParsePKCS1PublicKey error")
}
return pubKey, nil
}
// ValidDomain 域名校验(http://example.xxx)
func ValidDomain(s string) bool {
return regexp.MustCompile(`^(http|https)://[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*\.[a-z]{2,6}(:[0-9]{1,5})?$`).MatchString(s)
}
// ValidIPHost 校验是否符合http|https//ip 格式
func ValidIPHost(s string) bool {
return regexp.MustCompile(`^(http|https)://(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(:[0-9]{1,5})?$`).MatchString(s)
}
// ValidURL 校验http链接是否是符合规范的URL
func ValidURL(s string) bool {
_, err := url.ParseRequestURI(s)
if err != nil {
return false
}
return true
}
func ValidPwd(s string) error {
if len(s) < 8 || len(s) > 12 {
return fmt.Errorf("密码长度不符合规范, 必须为8-10位")
}
// 分别校验数字 大小写字母和特殊字符
num := `[0-9]{1}`
l := `[a-z]{1}`
u := `[A-Z]{1}`
symbol := `[!@#~$%^&*()+|_]{1}`
if b, err := regexp.MatchString(num, s); !b || err != nil {
return errors.New("密码必须包含数字 ")
}
if b, err := regexp.MatchString(l, s); !b || err != nil {
return errors.New("密码必须包含小写字母")
}
if b, err := regexp.MatchString(u, s); !b || err != nil {
return errors.New("密码必须包含大写字母")
}
if b, err := regexp.MatchString(symbol, s); !b || err != nil {
return errors.New("密码必须包含特殊字")
}
return nil
}

View File

@@ -3,6 +3,7 @@ package db
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"server/config"
)
@@ -24,7 +25,7 @@ func InitMysql() (err error) {
SingularTable: true, //是否使用 结构体名称作为表名 (关闭自动变复数)
//NameReplacer: strings.NewReplacer("spider_", ""), // 替表名和字段中的 Me 为 空
},
//Logger: logger.Default.LogMode(logger.Info), //设置日志级别为Info
Logger: logger.Default.LogMode(logger.Info), //设置日志级别为Info
})
return
}

View File

@@ -0,0 +1,42 @@
package middleware
import (
"github.com/gin-gonic/gin"
"log"
"net/http"
)
// Cors 开启跨域请求
func Cors() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
origin := c.Request.Header.Get("Origin") //请求头部
if origin != "" {
//接收客户端发送的origin (重要!)
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
//服务器支持的所有跨域请求的方法
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE")
//允许跨域设置可以返回其他子段,可以自定义字段
c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session, Content-Type")
// 允许浏览器(客户端)可以解析的头部 (重要)
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
//设置缓存时间
c.Header("Access-Control-Max-Age", "172800")
//允许客户端传递校验信息比如 cookie (重要)
c.Header("Access-Control-Allow-Credentials", "true")
}
//允许类型校验
if method == "OPTIONS" {
c.JSON(http.StatusOK, "ok!")
}
defer func() {
if err := recover(); err != nil {
log.Printf("Panic info is: %v\n", err)
}
}()
c.Next()
}
}

View File

@@ -0,0 +1,64 @@
package middleware
import (
"errors"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"net/http"
"server/config"
"server/model/system"
)
/*
*/
// AuthToken 用户登录Token拦截
func AuthToken() gin.HandlerFunc {
return func(c *gin.Context) {
// 从请求头中获取token
authToken := c.Request.Header.Get("auth-token")
// 如果没有登录信息则直接清退
if authToken == "" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "ok", "message": "用户未授权,请先登录."})
c.Abort()
return
}
// 解析token中的信息
uc, err := system.ParseToken(authToken)
// 从Redis中获取对应的token是否存在, 如果存在则刷新token
t := system.GetUserTokenById(uc.UserID)
// 如果 redis中获取的token为空则登录已过期需重新登录
if len(t) <= 0 {
c.JSON(http.StatusUnauthorized, gin.H{
"status": "ok",
"message": "身份验证信息已过期,请重新登录!!!",
})
c.Abort()
return
}
// 如果redis中存在对应token, 校验authToken是否与redis中的一致
if t != authToken {
// 如果不一致则证明authToken已经失效或在其他地方登录, 则需要重新登录
c.JSON(http.StatusUnauthorized, gin.H{
"status": "ok",
"message": "账号在其它设备登录,身份验证信息失效,请重新登录!!!",
})
c.Abort()
return
} else if err != nil && errors.Is(err, jwt.ErrTokenExpired) {
// 如果token已经过期,且redis中的token与authToken 相同则更新 token
// 生成新token
newToken, _ := system.GenToken(uc.UserID, uc.UserName)
// 将新token同步到redis中
_ = system.SaveUserToken(newToken, uc.UserID)
// 解析出新的 UserClaims
uc, _ = system.ParseToken(newToken)
c.Header("new-token", newToken)
}
// 将UserClaims存放到context中
c.Set(config.AuthUserClaims, uc)
c.Next()
}
}

View File

@@ -0,0 +1,15 @@
package middleware
import (
"encoding/xml"
"github.com/gin-gonic/gin"
)
func AddXmlHeader() gin.HandlerFunc {
return func(c *gin.Context) {
if c.NegotiateFormat(gin.MIMEXML, gin.MIMEJSON) == gin.MIMEXML {
_, _ = c.Writer.Write([]byte(xml.Header))
}
c.Next()
}
}

View File

@@ -1,138 +1,181 @@
package spider
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/url"
"server/config"
"server/model"
"server/plugin/common/dp"
"server/model/system"
"server/plugin/common/conver"
"server/plugin/common/util"
"time"
)
/*
舍弃第一版的数据处理思路, v2版本
直接分页获取采集站点的影片详情信息
采集逻辑 v3
*/
/*
1. 选择一个采集主站点, mysql检索表中只存储主站点检索的信息
2. 采集多个站点数据
2.1 主站点的采集数据完整地保存相关信息, basicInfo movieDetail search 等信息
2.2 其余站点数据只存储 name(影片名称), playUrl(播放url), 存储形式 Key<hash(name)>:value([]MovieUrlInfo)
3. api数据格式不变, 获取影片详情时通过subTitle 去redis匹配其他站点的对应播放源并整合到主站详情信息的playUrl中
4. 影片搜索时不再使用name进行匹配, 改为使用 subTitle 进行匹配
*/
var spiderCore = &JsonCollect{}
const (
MainSite = "https://cj.lzcaiji.com/api.php/provide/vod/"
)
// =========================通用采集方法==============================
type Site struct {
Name string
Uri string
}
// SiteList 播放源采集站
var SiteList = []Site{
// 备用采集站
//{"lz_bk", "https://cj.lzcaiji.com/api.php/provide/vod/"},
//{"fs", "https://www.feisuzyapi.com/api.php/provide/vod/"},
//{"su", "https://subocaiji.com/api.php/provide/vod/at/json"},
//{"bf", "https://bfzyapi.com/api.php/provide/vod/"},
//{"ff", "https://svip.ffzyapi8.com/api.php/provide/vod/"},
//{"lz", "https://cj.lziapi.com/api.php/provide/vod/"},
{"kk", "https://kuaikan-api.com/api.php/provide/vod/from/kuaikan"},
{"bf", "http://by.bfzyapi.com/api.php/provide/vod/"},
{"ff", "https://cj.ffzyapi.com/api.php/provide/vod/"},
}
// StartSpider 执行多源spider
func StartSpider() {
// 保存分类树
CategoryList()
log.Println("CategoryList 影片分类信息保存完毕")
// 爬取主站点数据
MainSiteSpider()
log.Println("MainSiteSpider 主站点影片信息保存完毕")
// 查找并创建search数据库, 保存search信息, 添加索引
time.Sleep(time.Second * 10)
model.CreateSearchTable()
SearchInfoToMdb()
model.AddSearchIndex()
log.Println("SearchInfoToMdb 影片检索信息保存完毕")
//获取其他站点数据
go MtSiteSpider()
log.Println("Spider End , 数据保存执行完成")
time.Sleep(time.Second * 10)
}
// CategoryList 获取分类数据
func CategoryList() {
// 设置请求参数信息
r := util.RequestInfo{Uri: MainSite, Params: url.Values{}}
r.Params.Set(`ac`, "list")
r.Params.Set(`pg`, "1")
r.Params.Set(`t`, "1")
// 执行请求, 获取一次list数据
util.ApiGet(&r)
// 解析resp数据
movieListInfo := model.MovieListInfo{}
if len(r.Resp) <= 0 {
log.Println("MovieListInfo数据获取异常 : Resp Is Empty")
// HandleCollect 影视采集 id-采集站ID h-时长/h
func HandleCollect(id string, h int) error {
// 1. 首先通过ID获取对应采集站信息
s := system.FindCollectSourceById(id)
if s == nil {
log.Println("Cannot Find Collect Source Site")
return errors.New(" Cannot Find Collect Source Site ")
} else if !s.State {
log.Println(" The acquisition site was disabled ")
return errors.New(" The acquisition site was disabled ")
}
// 如果是主站点且状态为启用则先获取分类tree信息
if s.Grade == system.MasterCollect && s.State {
// 是否存在分类树信息, 不存在则获取
if !system.ExistsCategoryTree() {
CollectCategory(s)
}
}
// 生成 RequestInfo
r := util.RequestInfo{Uri: s.Uri, Params: url.Values{}}
// 如果 h == 0 则直接返回错误信息
if h == 0 {
log.Println(" Collect time cannot be zero ")
return errors.New(" Collect time cannot be zer ")
}
// 如果 h = -1 则进行全量采集
if h > 0 {
r.Params.Set("h", fmt.Sprint(h))
}
// 2. 首先获取分页采集的页数
pageCount, err := spiderCore.GetPageCount(r)
// 分页页数失败 则再进行一次尝试
if err != nil {
// 如果第二次获取分页页数依旧获取失败则关闭当前采集任务
pageCount, err = spiderCore.GetPageCount(r)
if err != nil {
return err
}
}
// 通过采集类型分别执行不同的采集方法
switch s.CollectType {
case system.CollectVideo:
// 采集视频资源
if pageCount <= config.MAXGoroutine*2 {
// 少量数据不开启协程
for i := 1; i <= pageCount; i++ {
collectFilm(s, h, i)
}
} else {
// 如果分页数量较大则开启协程
ConcurrentPageSpider(pageCount, s, h, collectFilm)
}
// 视频数据采集完成后同步相关信息到mysql
if s.Grade == system.MasterCollect {
// 每次成功执行完都清理redis中的相关API接口数据缓存
clearCache()
// 执行影片信息更新操作
if h > 0 {
// 执行数据更新操作
system.SyncSearchInfo(1)
} else {
// 清空searchInfo中的数据并重新添加, 否则执行
system.SyncSearchInfo(0)
}
// 开启图片同步
if s.SyncPictures {
system.SyncFilmPicture()
}
}
case system.CollectArticle, system.CollectActor, system.CollectRole, system.CollectWebSite:
log.Println("暂未开放此采集功能!!!")
return errors.New("暂未开放此采集功能")
}
log.Println("Spider Task Exercise Success")
return nil
}
// CollectCategory 影视分类采集
func CollectCategory(s *system.FilmSource) {
// 获取分类树形数据
categoryTree, err := spiderCore.GetCategoryTree(util.RequestInfo{Uri: s.Uri, Params: url.Values{}})
if err != nil {
log.Println("GetCategoryTree Error: ", err)
return
}
_ = json.Unmarshal(r.Resp, &movieListInfo)
// 获取分类列表信息
classList := movieListInfo.Class
// 组装分类数据信息树形结构
categoryTree := dp.CategoryTree(classList)
// 序列化tree
data, _ := json.Marshal(categoryTree)
// 保存 tree 到redis
err := model.SaveCategoryTree(string(data))
err = system.SaveCategoryTree(categoryTree)
if err != nil {
log.Println("SaveCategoryTree Error: ", err)
}
}
// MainSiteSpider 主站点数据处理
func MainSiteSpider() {
// 获取分页页
pageCount, err := GetPageCount(util.RequestInfo{Uri: MainSite, Params: url.Values{}})
// 主站点分页出错直接终止程序
if err != nil {
panic(err)
// 影视详情采集
func collectFilm(s *system.FilmSource, h, pg int) {
// 生成请求参
r := util.RequestInfo{Uri: s.Uri, Params: url.Values{}}
// 设置分页页数
r.Params.Set("pg", fmt.Sprint(pg))
// 如果 h = -1 则进行全量采集
if h > 0 {
r.Params.Set("h", fmt.Sprint(h))
}
// 开启协程加快分页请求速度
ch := make(chan int, pageCount)
// 执行采集方法 获取影片详情list
list, err := spiderCore.GetFilmDetail(r)
if err != nil || len(list) <= 0 {
log.Println("GetMovieDetail Error: ", err)
return
}
// 通过采集站 Grade 类型, 执行不同的存储逻辑
switch s.Grade {
case system.MasterCollect:
// 主站点 保存完整影片详情信息到 redis
if err = system.SaveDetails(list); err != nil {
log.Println("SaveDetails Error: ", err)
}
// 如果主站点开启了图片同步, 则将图片url以及对应的mid存入ZSet集合中
if s.SyncPictures {
if err = system.SaveVirtualPic(conver.ConvertVirtualPicture(list)); err != nil {
log.Println("SaveVirtualPic Error: ", err)
}
}
case system.SlaveCollect:
// 附属站点 仅保存影片播放信息到redis
if err = system.SaveSitePlayList(s.Name, list); err != nil {
log.Println("SaveDetails Error: ", err)
}
}
}
// ConcurrentPageSpider 并发分页采集, 不限类型
func ConcurrentPageSpider(capacity int, s *system.FilmSource, h int, collectFunc func(s *system.FilmSource, hour, pageNumber int)) {
// 开启协程并发执行
ch := make(chan int, capacity)
waitCh := make(chan int)
for i := 1; i <= pageCount; i++ {
for i := 1; i <= capacity; i++ {
ch <- i
}
close(ch)
for i := 0; i < config.MAXGoroutine; i++ {
// 开启 MAXGoroutine 数量的协程, 如果分页页数小于协程数则将协程数限制为分页页数
var GoroutineNum = config.MAXGoroutine
if capacity < GoroutineNum {
GoroutineNum = capacity
}
for i := 0; i < GoroutineNum; i++ {
go func() {
defer func() { waitCh <- 0 }()
for {
// 从channel中获取 pageNumber
pg, ok := <-ch
if !ok {
break
}
list, e := GetMovieDetail(pg, util.RequestInfo{Uri: MainSite, Params: url.Values{}})
if e != nil {
log.Println("GetMovieDetail Error: ", err)
continue
}
// 保存影片详情信息到redis
if err = model.SaveDetails(list); err != nil {
log.Println("SaveDetails Error: ", err)
}
// 执行对应的采集方法
collectFunc(s, h, pg)
}
}()
}
@@ -141,198 +184,36 @@ func MainSiteSpider() {
}
}
// MtSiteSpider 附属数据源处理
func MtSiteSpider() {
for _, s := range SiteList {
// 执行每个站点的播放url缓存
PlayDetailSpider(s)
log.Println(s.Name, "playUrl 爬取完毕!!!")
}
}
// PlayDetailSpider SpiderSimpleInfo 获取单个站点的播放源
func PlayDetailSpider(s Site) {
// 获取分页页数
pageCount, err := GetPageCount(util.RequestInfo{Uri: s.Uri, Params: url.Values{}})
// 出错直接终止当前站点数据获取
if err != nil {
log.Println(err)
return
}
// 开启协程加快分页请求速度
ch := make(chan int, pageCount)
waitCh := make(chan int)
for i := 1; i <= pageCount; i++ {
ch <- i
}
close(ch)
for i := 0; i < config.MAXGoroutine; i++ {
go func() {
defer func() { waitCh <- 0 }()
for {
pg, ok := <-ch
if !ok {
break
}
list, e := GetMovieDetail(pg, util.RequestInfo{Uri: s.Uri, Params: url.Values{}})
if e != nil || len(list) <= 0 {
log.Println("GetMovieDetail Error: ", err)
continue
}
// 保存影片播放信息到redis
if err = model.SaveSitePlayList(s.Name, list); err != nil {
log.Println("SaveDetails Error: ", err)
}
}
}()
}
for i := 0; i < config.MAXGoroutine; i++ {
<-waitCh
}
}
// SearchInfoToMdb 扫描redis中的检索信息, 并批量存入mysql
func SearchInfoToMdb() {
// 1. 从redis的Zset集合中scan扫描数据, 每次100条
var cursor uint64 = 0
var count int64 = 100
for {
infoList, nextStar := model.ScanSearchInfo(cursor, count)
// 2. 将扫描到的数据插入mysql中
model.BatchSave(infoList)
// 3.设置下次开始的游标
cursor = nextStar
// 4. 判断迭代是否已经结束 cursor为0则表示已经迭代完毕
if cursor == 0 {
return
}
}
}
// UpdateMovieDetail 定时更新主站点和其余播放源信息
func UpdateMovieDetail() {
// 更新主站系列信息
UpdateMainDetail()
// 更新播放源数据信息
UpdatePlayDetail()
}
// UpdateMainDetail 更新主站点的最新影片
func UpdateMainDetail() {
// 获取分页页数
r := util.RequestInfo{Uri: MainSite, Params: url.Values{}}
r.Params.Set("h", config.UpdateInterval)
pageCount, err := GetPageCount(r)
if err != nil {
log.Printf("Update MianStieDetail failed")
}
// 保存本次更新的所有详情信息
var ds []model.MovieDetail
// 获取分页数据
for i := 1; i <= pageCount; i++ {
list, err := GetMovieDetail(i, r)
if err != nil {
continue
}
// 保存更新的影片信息, 同类型直接覆盖
if err = model.SaveDetails(list); err != nil {
log.Printf("Update MianStieDetail failed, SaveDetails Error ")
}
ds = append(ds, list...)
}
// 整合详情信息切片
var sl []model.SearchInfo
for _, d := range ds {
// 通过id 获取对应的详情信息
sl = append(sl, model.ConvertSearchInfo(d))
}
// 调用批量保存或更新方法, 如果对应mid数据存在则更新, 否则执行插入
model.BatchSaveOrUpdate(sl)
}
// UpdatePlayDetail 更新最x小时的影片播放源数据
func UpdatePlayDetail() {
for _, s := range SiteList {
// 获取单个站点的分页数
r := util.RequestInfo{Uri: s.Uri, Params: url.Values{}}
r.Params.Set("h", config.UpdateInterval)
pageCount, err := GetPageCount(r)
if err != nil {
log.Printf("Update %s playDetail failed", s.Name)
}
for i := 1; i <= pageCount; i++ {
// 获取详情信息, 保存到对应hashKey中
list, e := GetMovieDetail(i, r)
if e != nil || len(list) <= 0 {
log.Println("GetMovieDetail Error: ", err)
continue
}
// 保存影片播放信息到redis
if err = model.SaveSitePlayList(s.Name, list); err != nil {
log.Println("SaveDetails Error: ", err)
// BatchCollect 批量采集, 采集指定的所有站点最近x小时内更新的数据
func BatchCollect(h int, ids ...string) {
for _, id := range ids {
// 如果查询到对应Id的资源站信息, 且资源站处于启用状态
if fs := system.FindCollectSourceById(id); fs != nil && fs.State {
// 执行当前站点的采集任务
if err := HandleCollect(fs.Id, h); err != nil {
log.Println(err)
}
}
}
}
// StartSpiderRe 清空存储数据,从零开始获取
func StartSpiderRe() {
// 删除已有的存储数据, redis 和 mysql中的存储数据全部清空
model.RemoveAll()
// 执行完整数据获取
StartSpider()
}
// =========================公共方法==============================
// GetPageCount 获取总页数
func GetPageCount(r util.RequestInfo) (count int, err error) {
// 发送请求获取pageCount
r.Params.Set("ac", "detail")
r.Params.Set("pg", "2")
util.ApiGet(&r)
// 判断请求结果是否为空, 如果为空直接输出错误并终止
if len(r.Resp) <= 0 {
err = errors.New("response is empty")
return
}
// 获取pageCount
res := model.DetailListInfo{}
err = json.Unmarshal(r.Resp, &res)
if err != nil {
return
}
count = int(res.PageCount)
return
}
// GetMovieDetail 处理详情接口请求返回的数据
func GetMovieDetail(pageNumber int, r util.RequestInfo) (list []model.MovieDetail, err error) {
// 防止json解析异常引发panic
defer func() {
if e := recover(); e != nil {
log.Println("GetMovieDetail Failed : ", e)
// AutoCollect 自动进行对所有已启用站点的采集任务
func AutoCollect(h int) {
// 获取采集站中所有站点, 进行遍历
for _, s := range system.GetCollectSourceList() {
// 如果当前站点为启用状态 则执行 HandleCollect 进行数据采集
if s.State {
if err := HandleCollect(s.Id, h); err != nil {
log.Println(err)
}
}
}()
// 设置分页请求参数
r.Params.Set(`ac`, `detail`)
r.Params.Set(`pg`, fmt.Sprint(pageNumber))
util.ApiGet(&r)
// 影视详情信息
details := model.DetailListInfo{}
// 如果返回数据为空则直接结束本次循环
if len(r.Resp) <= 0 {
err = errors.New("response is empty")
return
}
// 序列化详情数据
if err = json.Unmarshal(r.Resp, &details); err != nil {
return
}
// 处理details信息
list = dp.ProcessMovieDetailList(details.List)
return
}
// StarZero 情况站点内所有影片信息
func StarZero(h int) {
// 首先清除影视信息
system.FilmZero()
// 开启自动采集
AutoCollect(h)
}

View File

@@ -1 +1,158 @@
package spider
import (
"encoding/json"
"errors"
"fmt"
"log"
"server/model/collect"
"server/model/system"
"server/plugin/common/conver"
"server/plugin/common/util"
)
/*
Spider 数据 爬取 & 处理 & 转换
*/
type FilmCollect interface {
// GetCategoryTree 获取影视分类数据
GetCategoryTree(r util.RequestInfo) (*system.CategoryTree, error)
// GetPageCount 获取API接口的分页页数
GetPageCount(r util.RequestInfo) (count int, err error)
// GetDetail 获取指定pageNumber的具体数据
GetDetail(pageNumber int, r util.RequestInfo) (list []system.MovieDetail, err error)
}
// ------------------------------------------------- JSON Collect -------------------------------------------------
// JsonCollect 处理返回值为JSON格式的采集数据
type JsonCollect struct {
}
// GetCategoryTree 获取分类树形数据
func (jc *JsonCollect) GetCategoryTree(r util.RequestInfo) (*system.CategoryTree, error) {
// 设置请求参数信息
r.Params.Set(`ac`, "list")
r.Params.Set(`pg`, "1")
// 执行请求, 获取一次list数据
util.ApiGet(&r)
// 解析resp数据
filmListPage := collect.FilmListPage{}
if len(r.Resp) <= 0 {
log.Println("filmListPage 数据获取异常 : Resp Is Empty")
return nil, errors.New("filmListPage 数据获取异常 : Resp Is Empty")
}
err := json.Unmarshal(r.Resp, &filmListPage)
// 获取分类列表信息
cl := filmListPage.Class
// 组装分类数据信息树形结构
tree := conver.GenCategoryTree(cl)
// 将分类列表信息存储到redis
_ = collect.SaveFilmClass(cl)
return tree, err
}
// GetPageCount 获取总页数
func (jc *JsonCollect) GetPageCount(r util.RequestInfo) (count int, err error) {
// 发送请求获取pageCount, 默认为获取 ac = detail
if len(r.Params.Get("ac")) <= 0 {
r.Params.Set("ac", "detail")
}
r.Params.Set("pg", "1")
util.ApiGet(&r)
// 判断请求结果是否为空, 如果为空直接输出错误并终止
if len(r.Resp) <= 0 {
err = errors.New("response is empty")
return
}
// 获取pageCount
res := collect.CommonPage{}
err = json.Unmarshal(r.Resp, &res)
if err != nil {
return
}
count = int(res.PageCount)
return
}
// GetDetail 处理详情接口请求返回的数据
func (jc *JsonCollect) GetDetail(pageNumber int, r util.RequestInfo) (list []system.MovieDetail, err error) {
// 防止json解析异常引发panic
defer func() {
if e := recover(); e != nil {
log.Println("GetMovieDetail Failed : ", e)
}
}()
// 设置分页请求参数
r.Params.Set(`ac`, `detail`)
r.Params.Set(`pg`, fmt.Sprint(pageNumber))
util.ApiGet(&r)
// 影视详情信息
detailPage := collect.FilmDetailLPage{}
//details := system.DetailListInfo{}
// 如果返回数据为空则直接结束本次循环
if len(r.Resp) <= 0 {
err = errors.New("response is empty")
return
}
// 序列化详情数据
if err = json.Unmarshal(r.Resp, &detailPage); err != nil {
return
}
// 将影视原始详情信息保存到redis中
// 获取主站点uri
mc := system.GetCollectSourceListByGrade(system.MasterCollect)[0]
if mc.Uri == r.Uri {
collect.BatchSaveOriginalDetail(detailPage.List)
}
// 处理details信息
list = conver.ConvertFilmDetails(detailPage.List)
return
}
// GetFilmDetail 通过 RequestInfo 获取并解析出对应的 MovieDetail list
func (jc *JsonCollect) GetFilmDetail(r util.RequestInfo) (list []system.MovieDetail, err error) {
// 防止json解析异常引发panic
defer func() {
if e := recover(); e != nil {
log.Println("GetMovieDetail Failed : ", e)
}
}()
// 设置分页请求参数
r.Params.Set(`ac`, `detail`)
util.ApiGet(&r)
// 影视详情信息
detailPage := collect.FilmDetailLPage{}
//details := system.DetailListInfo{}
// 如果返回数据为空则直接结束本次循环
if len(r.Resp) <= 0 {
err = errors.New("response is empty")
return
}
// 序列化详情数据
if err = json.Unmarshal(r.Resp, &detailPage); err != nil {
return
}
// 将影视原始详情信息保存到redis中
// 获取主站点uri
//mc := system.GetCollectSourceListByGrade(system.MasterCollect)[0]
//if mc.Uri == r.Uri {
// collect.BatchSaveOriginalDetail(detailPage.List)
//}
// 处理details信息
list = conver.ConvertFilmDetails(detailPage.List)
return
}
// ------------------------------------------------- XML Collect -------------------------------------------------
// XmlCollect 处理返回值为XML格式的采集数据
type XmlCollect struct {
}

View File

@@ -1,17 +1,25 @@
package spider
import (
"errors"
"fmt"
"github.com/robfig/cron/v3"
"log"
"server/config"
"server/model"
"server/model/system"
"time"
)
var (
CronCollect *cron.Cron = CreateCron()
)
// RegularUpdateMovie 定时更新, 每半小时获取一次站点的最近x小时数据
func RegularUpdateMovie() {
//创建一个定时任务对象
c := cron.New(cron.WithSeconds())
// 开启定时任务每x 分钟更新一次最近x小时的影片数据
_, err := c.AddFunc(config.CornMovieUpdate, func() {
// 添加定时任务每x 分钟更新一次最近x小时的影片数据
taskId, err := c.AddFunc(config.CornMovieUpdate, func() {
// 执行更新最近x小时影片的Spider
log.Println("执行一次影片更新任务...")
UpdateMovieDetail()
@@ -20,7 +28,7 @@ func RegularUpdateMovie() {
})
// 开启定时任务每月最后一天凌晨两点, 执行一次清库重取数据
_, err = c.AddFunc(config.CornUpdateAll, func() {
taskId2, err := c.AddFunc(config.CornUpdateAll, func() {
StartSpiderRe()
})
@@ -28,10 +36,91 @@ func RegularUpdateMovie() {
log.Println("Corn Start Error: ", err)
}
c.Start()
log.Println(taskId, "------", taskId2)
log.Printf("%v", c.Entries())
//c.Start()
}
// StartCrontab 启动定时任务
func StartCrontab() {
// 从redis中读取待启动的定时任务列表
// 影片更新定时任务列表
CronCollect.Start()
}
func CreateCron() *cron.Cron {
return cron.New(cron.WithSeconds())
}
// AddFilmUpdateCron 添加影片更新定时任务
func AddFilmUpdateCron(id, spec string) (cron.EntryID, error) {
// 校验 spec 表达式的有效性
if err := ValidSpec(spec); err != nil {
return -99, errors.New(fmt.Sprint("定时任务添加失败,Cron表达式校验失败: ", err.Error()))
}
return CronCollect.AddFunc(spec, func() {
// 通过创建任务时生成的 Id 获取任务相关数据
ft, err := system.GetFilmTaskById(id)
if err != nil {
log.Println("FilmCollectCron Exec Failed: ", err)
}
// 如果当前定时任务状态为开启则执行对应的采集任务
if ft.State && ft.Model == 1 {
// 对指定ids的资源站数据进行更新操作
BatchCollect(ft.Time, ft.Ids...)
}
// 任务执行完毕
log.Printf("执行一次定时任务: Task[%s]\n", ft.Id)
})
}
// AddAutoUpdateCron 自动更新定时任务
func AddAutoUpdateCron(id, spec string) (cron.EntryID, error) {
// 校验 spec 表达式的有效性
if err := ValidSpec(spec); err != nil {
return -99, errors.New(fmt.Sprint("定时任务添加失败,Cron表达式校验失败: ", err.Error()))
}
return CronCollect.AddFunc(spec, func() {
// 通过 Id 获取任务相关数据
ft, err := system.GetFilmTaskById(id)
if err != nil {
log.Println("FilmCollectCron Exec Failed: ", err)
}
// 开启对系统中已启用站点的自动更新
if ft.State && ft.Model == 0 {
AutoCollect(ft.Time)
log.Println("执行一次自动更新任务")
}
})
}
// RemoveCron 删除定时任务
func RemoveCron(id cron.EntryID) {
// 通过定时任务EntryID移出对应的定时任务
CronCollect.Remove(id)
}
// GetEntryById 返回定时任务的相关时间信息
func GetEntryById(id cron.EntryID) cron.Entry {
log.Printf("%+v\n", CronCollect.Entries())
log.Println("", CronCollect.Entry(id).Next.Format(time.DateTime))
return CronCollect.Entry(id)
}
// ValidSpec 校验cron表达式是否有效 不能精确到秒
func ValidSpec(spec string) error {
// 自定义解释器
parser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
//if _, err := parser.Parse(spec); err != nil {
// return err
//}
_, err := parser.Parse(spec)
return err
}
// 清理API接口数据缓存
func clearCache() {
model.RemoveCache(config.IndexCacheKey)
system.RemoveCache(config.IndexCacheKey)
}

View File

@@ -0,0 +1,275 @@
package spider
import (
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"log"
"net/url"
"server/config"
"server/model/collect"
"server/model/system"
"server/plugin/common/util"
"time"
)
/*
舍弃第一版的数据处理思路, v2版本
直接分页获取采集站点的影片详情信息
*/
/*
1. 选择一个采集主站点, mysql检索表中只存储主站点检索的信息
2. 采集多个站点数据
2.1 主站点的采集数据完整地保存相关信息, basicInfo movieDetail search 等信息
2.2 其余站点数据只存储 name(影片名称), playUrl(播放url), 存储形式 Key<hash(name)>:value([]MovieUrlInfo)
3. api数据格式不变, 获取影片详情时通过subTitle 去redis匹配其他站点的对应播放源并整合到主站详情信息的playUrl中
4. 影片搜索时不再使用name进行匹配, 改为使用 subTitle 进行匹配
*/
// StartSpider 执行多源spider
func StartSpider() {
// 保存分类树
CategoryList()
log.Println("CategoryList 影片分类信息保存完毕")
// 爬取主站点数据
MainSiteSpider()
log.Println("MainSiteSpider 主站点影片信息保存完毕")
// 查找并创建search数据库, 保存search信息, 添加索引
time.Sleep(time.Second * 10)
system.SyncSearchInfo(0)
system.AddSearchIndex()
log.Println("SearchInfoToMdb 影片检索信息保存完毕")
//获取其他站点数据
scl := system.GetCollectSourceListByGrade(system.SlaveCollect)
go MtSiteSpider(scl...)
log.Println("Spider End , 数据保存执行完成")
time.Sleep(time.Second * 10)
}
// CategoryList 获取分类数据
func CategoryList() {
// 获取主站点uri
mc := system.GetCollectSourceListByGrade(system.MasterCollect)[0]
// 获取分类树形数据
categoryTree, err := spiderCore.GetCategoryTree(util.RequestInfo{Uri: mc.Uri, Params: url.Values{}})
if err != nil {
log.Println("GetCategoryTree Error: ", err)
return
}
// 保存 tree 到redis
err = system.SaveCategoryTree(categoryTree)
if err != nil {
log.Println("SaveCategoryTree Error: ", err)
}
}
// MainSiteSpider 主站点数据处理
func MainSiteSpider() {
// 获取主站点uri
mc := system.GetCollectSourceListByGrade(system.MasterCollect)[0]
// 获取分页页数
pageCount, err := spiderCore.GetPageCount(util.RequestInfo{Uri: mc.Uri, Params: url.Values{}})
// 主站点分页出错直接终止程序
if err != nil {
panic(err)
}
// 开启协程加快分页请求速度
ch := make(chan int, pageCount)
waitCh := make(chan int)
for i := 1; i <= pageCount; i++ {
ch <- i
}
close(ch)
for i := 0; i < config.MAXGoroutine; i++ {
go func() {
defer func() { waitCh <- 0 }()
for {
pg, ok := <-ch
if !ok {
break
}
list, e := spiderCore.GetDetail(pg, util.RequestInfo{Uri: mc.Uri, Params: url.Values{}})
if e != nil {
log.Println("GetMovieDetail Error: ", err)
continue
}
// 保存影片详情信息到redis
if err = system.SaveDetails(list); err != nil {
log.Println("SaveDetails Error: ", err)
}
}
}()
}
for i := 0; i < config.MAXGoroutine; i++ {
<-waitCh
}
}
// MtSiteSpider 附属站点数据源处理
func MtSiteSpider(scl ...system.FilmSource) {
for _, s := range scl {
// 执行每个站点的播放url缓存
PlayDetailSpider(s)
log.Println(s.Name, "playUrl 爬取完毕!!!")
}
}
// PlayDetailSpider SpiderSimpleInfo 获取单个站点的播放源
func PlayDetailSpider(s system.FilmSource) {
// 获取分页页数
pageCount, err := spiderCore.GetPageCount(util.RequestInfo{Uri: s.Uri, Params: url.Values{}})
// 出错直接终止当前站点数据获取
if err != nil {
log.Println(err)
return
}
// 开启协程加快分页请求速度
ch := make(chan int, pageCount)
waitCh := make(chan int)
for i := 1; i <= pageCount; i++ {
ch <- i
}
close(ch)
for i := 0; i < config.MAXGoroutine; i++ {
go func() {
defer func() { waitCh <- 0 }()
for {
pg, ok := <-ch
if !ok {
break
}
list, e := spiderCore.GetDetail(pg, util.RequestInfo{Uri: s.Uri, Params: url.Values{}})
if e != nil || len(list) <= 0 {
log.Println("GetMovieDetail Error: ", err)
continue
}
// 保存影片播放信息到redis
if err = system.SaveSitePlayList(s.Name, list); err != nil {
log.Println("SaveDetails Error: ", err)
}
}
}()
}
for i := 0; i < config.MAXGoroutine; i++ {
<-waitCh
}
}
// UpdateMovieDetail 定时更新主站点和其余播放源信息
func UpdateMovieDetail() {
// 更新主站系列信息
UpdateMainDetail()
// 更新附属播放源数据信息
scl := system.GetCollectSourceListByGrade(system.SlaveCollect)
UpdatePlayDetail(scl...)
}
// UpdateMainDetail 更新主站点的最新影片
func UpdateMainDetail() {
// 获取主站点uri
l := system.GetCollectSourceListByGrade(system.MasterCollect)
mc := system.FilmSource{}
for _, v := range l {
if len(v.Uri) > 0 {
mc = v
break
}
}
// 获取分页页数
r := util.RequestInfo{Uri: mc.Uri, Params: url.Values{}}
r.Params.Set("h", config.UpdateInterval)
pageCount, err := spiderCore.GetPageCount(r)
if err != nil {
log.Printf("Update MianStieDetail failed\n")
}
// 保存本次更新的所有详情信息
var ds []system.MovieDetail
// 获取分页数据
for i := 1; i <= pageCount; i++ {
list, err := spiderCore.GetDetail(i, r)
if err != nil {
continue
}
// 保存更新的影片信息, 同类型直接覆盖
if err = system.SaveDetails(list); err != nil {
log.Println("Update MainSiteDetail failed, SaveDetails Error ")
}
ds = append(ds, list...)
}
// 整合详情信息切片
var sl []system.SearchInfo
for _, d := range ds {
// 通过id 获取对应的详情信息
sl = append(sl, system.ConvertSearchInfo(d))
}
// 调用批量保存或更新方法, 如果对应mid数据存在则更新, 否则执行插入
system.BatchSaveOrUpdate(sl)
}
// UpdatePlayDetail 更新最x小时的影片播放源数据
func UpdatePlayDetail(scl ...system.FilmSource) {
for _, s := range scl {
// 获取单个站点的分页数
r := util.RequestInfo{Uri: s.Uri, Params: url.Values{}}
r.Params.Set("h", config.UpdateInterval)
pageCount, err := spiderCore.GetPageCount(r)
if err != nil {
log.Printf("Update %s playDetail failed\n", s.Name)
}
for i := 1; i <= pageCount; i++ {
// 获取详情信息, 保存到对应hashKey中
list, e := spiderCore.GetDetail(i, r)
if e != nil || len(list) <= 0 {
log.Println("GetMovieDetail Error: ", err)
continue
}
// 保存影片播放信息到redis
if err = system.SaveSitePlayList(s.Name, list); err != nil {
log.Println("SaveDetails Error: ", err)
}
}
}
}
// StartSpiderRe 清空存储数据,从零开始获取
func StartSpiderRe() {
// 删除已有的存储数据, redis 和 mysql中的存储数据全部清空
system.FilmZero()
// 执行完整数据获取
StartSpider()
}
// =========================公共方法==============================
// CollectApiTest 测试采集接口是否可用
func CollectApiTest(s system.FilmSource) error {
// 使用当前采集站接口采集一页数据
r := util.RequestInfo{Uri: s.Uri, Params: url.Values{}}
r.Params.Set("ac", s.CollectType.GetActionType())
r.Params.Set("pg", "3")
err := util.ApiTest(&r)
// 首先核对接口返回值类型
if err == nil {
// 如果返回值类型为Json则执行Json序列化
if s.ResultModel == system.JsonResult {
var dp = collect.FilmDetailLPage{}
if err = json.Unmarshal(r.Resp, &dp); err != nil {
return errors.New(fmt.Sprint("测试失败, 返回数据异常, JSON序列化失败: ", err))
}
return nil
} else if s.ResultModel == system.XmlResult {
// 如果返回值类型为XML则执行XML序列化
var rd = collect.RssD{}
if err = xml.Unmarshal(r.Resp, &rd); err != nil {
return errors.New(fmt.Sprint("测试失败, 返回数据异常, XML序列化失败", err))
}
return nil
}
return errors.New("测试失败, 接口返回值类型不符合规范")
}
return errors.New(fmt.Sprint("测试失败, 请求响应异常 : ", err.Error()))
}

View File

@@ -2,20 +2,19 @@ package router
import (
"github.com/gin-gonic/gin"
"log"
"net/http"
"server/config"
"server/controller"
"server/plugin/middleware"
)
func SetupRouter() *gin.Engine {
r := gin.Default()
// 开启跨域
r.Use(Cors())
r.Use(middleware.Cors())
// 静态资源配置
r.Static("/static/image", config.ImageDir)
r.Static(config.FilmPictureUrlPath, config.FilmPictureUploadDir)
r.GET(`/index`, controller.Index)
r.GET(`/navCategory`, controller.CategoriesInfo)
@@ -24,53 +23,88 @@ func SetupRouter() *gin.Engine {
r.GET(`/searchFilm`, controller.SearchFilm)
r.GET(`/filmClassify`, controller.FilmClassify)
r.GET(`/filmClassifySearch`, controller.FilmTagSearch)
// 弃用
//r.GET(`/filmCategory`, controller.FilmCategory)
//r.GET(`/filmCategory`, controller.FilmCategory) 弃用
r.POST(`/login`, controller.Login)
r.GET(`/logout`, middleware.AuthToken(), controller.Logout)
r.POST(`/changePassword`, middleware.AuthToken(), controller.UserPasswordChange)
// 触发spider
spiderRoute := r.Group(`/spider`)
// 管理员API路由组
manageRoute := r.Group(`/manage`)
manageRoute.Use(middleware.AuthToken())
{
// 清空全部数据并从零开始获取数据
spiderRoute.GET("/SpiderRe", controller.SpiderRe)
// 获取影片详情, 用于网路不稳定导致的影片数据缺失
spiderRoute.GET(`/FixFilmDetail`, controller.FixFilmDetail)
spiderRoute.GET(`/RefreshSitePlay`, controller.RefreshSitePlay)
manageRoute.GET(`/index`, controller.ManageIndex)
// 系统相关
sysConfig := manageRoute.Group(`/config`)
{
sysConfig.GET("/basic", controller.SiteBasicConfig)
sysConfig.POST("/basic/update", controller.UpdateSiteBasic)
sysConfig.GET("/basic/reset", controller.ResetSiteBasic)
}
userRoute := manageRoute.Group(`/user`)
{
userRoute.GET(`/info`, controller.UserInfo)
}
// 采集路相关
collect := manageRoute.Group(`/collect`)
{
collect.GET(`/list`, controller.FilmSourceList)
collect.GET(`/find`, controller.FindFilmSource)
collect.POST(`/test`, controller.FilmSourceTest)
collect.POST(`/add`, controller.FilmSourceAdd)
collect.POST(`/update`, controller.FilmSourceUpdate)
collect.POST(`/change`, controller.FilmSourceChange)
//collect.GET(`/star`, controller.CollectFilm)
collect.GET(`/del`, controller.FilmSourceDel)
collect.GET(`/options`, controller.GetNormalFilmSource)
}
// 定时任务相关
collectCron := manageRoute.Group(`/cron`)
{
collectCron.GET(`/list`, controller.FilmCronTaskList)
collectCron.GET(`/find`, controller.GetFilmCronTask)
//collectCron.GET(`/options`, controller.GetNormalFilmSource)
collectCron.POST(`/add`, controller.FilmCronAdd)
collectCron.POST(`/update`, controller.FilmCronUpdate)
collectCron.POST(`/change`, controller.ChangeTaskState)
collectCron.GET(`/del`, controller.DelFilmCron)
}
// spider 数据采集
spiderRoute := manageRoute.Group(`/spider`)
{
spiderRoute.POST(`/start`, controller.StarSpider)
spiderRoute.GET(`/zero`, controller.SpiderReset)
spiderRoute.GET(`/class/cover`, controller.CoverFilmClass)
}
// filmManage 影视管理
filmRoute := manageRoute.Group(`/film`)
{
filmRoute.POST(`/add`, controller.FilmAdd)
filmRoute.GET(`/search/list`, controller.FilmSearchPage)
filmRoute.GET(`/class/tree`, controller.FilmClassTree)
filmRoute.GET(`/class/find`, controller.FindFilmClass)
filmRoute.POST(`/class/update`, controller.UpdateFilmClass)
filmRoute.GET(`/class/del`, controller.DelFilmClass)
}
// 文件管理
fileRoute := manageRoute.Group(`/file`)
{
fileRoute.POST(`/upload`, controller.SingleUpload)
fileRoute.GET(`/list`, controller.PhotoWall)
}
}
// 供第三方采集的API
//provideRoute := r.Group(`/provide`)
//{
// provideRoute.GET(`/vod`, controller.HandleProvide)
// provideRoute.GET(`/vod/xml`, middleware.AddXmlHeader(), controller.HandleProvideXml)
//}
return r
}
// Cors 开启跨域请求
func Cors() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
origin := c.Request.Header.Get("Origin") //请求头部
if origin != "" {
//接收客户端发送的origin (重要!)
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
//服务器支持的所有跨域请求的方法
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE")
//允许跨域设置可以返回其他子段,可以自定义字段
c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session, Content-Type")
// 允许浏览器(客户端)可以解析的头部 (重要)
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
//设置缓存时间
c.Header("Access-Control-Max-Age", "172800")
//允许客户端传递校验信息比如 cookie (重要)
c.Header("Access-Control-Allow-Credentials", "true")
}
//允许类型校验
if method == "OPTIONS" {
c.JSON(http.StatusOK, "ok!")
}
defer func() {
if err := recover(); err != nil {
log.Printf("Panic info is: %v", err)
}
}()
c.Next()
}
}