mirror of
https://github.com/ProudMuBai/GoFilm.git
synced 2026-02-17 08:04:42 +08:00
add BAM
This commit is contained in:
15
server/plugin/SystemInit/DatabaseInIt.go
Normal file
15
server/plugin/SystemInit/DatabaseInIt.go
Normal 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()
|
||||
}
|
||||
89
server/plugin/SystemInit/SpiderInit.go
Normal file
89
server/plugin/SystemInit/SpiderInit.go
Normal 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()
|
||||
}
|
||||
22
server/plugin/SystemInit/WebSiteInit.go
Normal file
22
server/plugin/SystemInit/WebSiteInit.go
Normal 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)
|
||||
}
|
||||
283
server/plugin/common/conver/Collect.go
Normal file
283
server/plugin/common/conver/Collect.go
Normal 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
|
||||
}
|
||||
53
server/plugin/common/conver/System.go
Normal file
53
server/plugin/common/conver/System.go
Normal 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
|
||||
}
|
||||
@@ -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获取父节点信息
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
126
server/plugin/common/util/StringUtil.go
Normal file
126
server/plugin/common/util/StringUtil.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
42
server/plugin/middleware/Cors.go
Normal file
42
server/plugin/middleware/Cors.go
Normal 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()
|
||||
}
|
||||
}
|
||||
64
server/plugin/middleware/HandleJwt.go
Normal file
64
server/plugin/middleware/HandleJwt.go
Normal 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()
|
||||
}
|
||||
}
|
||||
15
server/plugin/middleware/HandleXml.go
Normal file
15
server/plugin/middleware/HandleXml.go
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
275
server/plugin/spider/SpiderOriginal.go
Normal file
275
server/plugin/spider/SpiderOriginal.go
Normal 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()))
|
||||
}
|
||||
Reference in New Issue
Block a user