mirror of
https://github.com/ProudMuBai/GoFilm.git
synced 2026-02-16 07:34:41 +08:00
init
This commit is contained in:
8
server/.idea/.gitignore
generated
vendored
Normal file
8
server/.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
8
server/.idea/modules.xml
generated
Normal file
8
server/.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/server.iml" filepath="$PROJECT_DIR$/.idea/server.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
9
server/.idea/server.iml
generated
Normal file
9
server/.idea/server.iml
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
83
server/README.md
Normal file
83
server/README.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Film Server
|
||||
|
||||
## 简介
|
||||
|
||||
- server 是本项目的后端项目
|
||||
- 主要用于提供前端项目需要的 API数据接口, 以及数据搜集和更新
|
||||
- 实现思路 :
|
||||
- 使用 gocolly 获取公开的影视资源,
|
||||
- 将请求数据通过程序处理整合成统一格式后使用redis进行暂存
|
||||
- 使用 mysql 存储收录的影片的检索信息, 用于影片检索, 分类
|
||||
- 使用 gin 作为web服务, 提供相应api接口
|
||||
- 项目依赖
|
||||
|
||||
```go
|
||||
# gin web服务框架, 用于处理与前端工程的交互
|
||||
github.com/gin-gonic/gin v1.9.0
|
||||
# gocolly go语言爬虫框架, 用于搜集公共影视资源
|
||||
github.com/gocolly/colly/v2 v2.1.0
|
||||
# go-redis redis交互程序
|
||||
github.com/redis/go-redis/v9 v9.0.2
|
||||
# gorm 用于处理与mysql数据库的交互
|
||||
gorm.io/gorm v1.24.6
|
||||
gorm.io/driver/mysql v1.4.7
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 项目结构
|
||||
|
||||
> 项目主要目录结构
|
||||
|
||||
- config 用于存放项目中使用的配置信息和静态常量
|
||||
- controller 请求处理控制器
|
||||
- logic 请求处理逻辑实现
|
||||
- model 数据模型结构体以及与数据库交互
|
||||
- plugin 项目所需的插件工具集合
|
||||
- common 公共依赖
|
||||
- db 数据库配置信息
|
||||
- spider gocolly配置, 执行逻辑, 数据前置处理等
|
||||
|
||||
```text
|
||||
server
|
||||
├─ config
|
||||
│ └─ DataConfig.go
|
||||
├─ controller
|
||||
│ ├─ IndexController.go
|
||||
│ └─ SpiderController.go
|
||||
├─ logic
|
||||
│ └─ IndexLogic.go
|
||||
├─ model
|
||||
│ ├─ Categories.go
|
||||
│ ├─ LZJson.go
|
||||
│ ├─ Movies.go
|
||||
│ └─ Search.go
|
||||
├─ plugin
|
||||
│ ├─ common
|
||||
│ │ ├─ JsonUtils.go
|
||||
│ │ ├─ ProcessCategory.go
|
||||
│ │ └─ ProcessMovies.go
|
||||
│ ├─ db
|
||||
│ │ ├─ mysql.go
|
||||
│ │ └─ redis.go
|
||||
│ └─ spider
|
||||
│ ├─ Spider.go
|
||||
│ ├─ SpiderCron.go
|
||||
│ └─ SpiderRequest.go
|
||||
├─ router
|
||||
│ └─ router.go
|
||||
├─ go.mod
|
||||
├─ go.sum
|
||||
├─ main.go
|
||||
└─ README.md
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 启动方式
|
||||
|
||||
### 本地运行
|
||||
|
||||
1. 修改 /server/plugin/db 目录下的 mysql.go 和 redis.go 中的连接地址和用户名密码
|
||||
2. 在 server 目录下执行 `go run main.go`
|
||||
|
||||
70
server/config/DataConfig.go
Normal file
70
server/config/DataConfig.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
/*
|
||||
定义一些数据库存放的key值
|
||||
*/
|
||||
const (
|
||||
|
||||
// CategoryTreeKey 分类树 key
|
||||
CategoryTreeKey = "CategoryTree"
|
||||
CategoryTreeExpired = time.Hour * 24 * 90
|
||||
// MovieListInfoKey movies分类列表 key
|
||||
MovieListInfoKey = "MovieList:Cid%d"
|
||||
// MAXGoroutine max goroutine, 执行spider中对协程的数量限制
|
||||
MAXGoroutine = 6
|
||||
// MovieDetailKey movie detail影视详情信息 可以
|
||||
MovieDetailKey = "MovieDetail:Cid%d:Id%d"
|
||||
// MovieBasicInfoKey 影片基本信息, 简略版本
|
||||
MovieBasicInfoKey = "MovieBasicInfoKey:Cid%d:Id%d"
|
||||
|
||||
// SearchCount Search scan 识别范围
|
||||
SearchCount = 3000
|
||||
// SearchKeys Search Key Hash
|
||||
SearchKeys = "SearchKeys"
|
||||
// SearchScoreListKey 根据评分检索的key
|
||||
SearchScoreListKey = "Search:SearchScoreList"
|
||||
SearchTimeListKey = "Search:SearchTimeList"
|
||||
SearchHeatListKey = "Search:SearchHeatList"
|
||||
// SearchInfoTemp redis暂存检索数据信息
|
||||
SearchInfoTemp = "Search:SearchInfoTemp"
|
||||
|
||||
// CornMovieUpdate 影片更新定时任务间隔
|
||||
CornMovieUpdate = "0 0/20 * * * ?"
|
||||
// UpdateInterval 获取最近几小时更新的影片 (h 小时) 默认3小时
|
||||
UpdateInterval = "3"
|
||||
// CornUpdateAll 每月28执行一次清库更新
|
||||
CornUpdateAll = "0 0 2 28 * ?"
|
||||
|
||||
// SpiderCipher 设置Spider触发指令
|
||||
SpiderCipher = "Life in a different world from zero"
|
||||
)
|
||||
|
||||
const (
|
||||
/*
|
||||
mysql服务配置信息
|
||||
*/
|
||||
MysqlDsn = "root:root@(192.168.20.10:3307)/FilmSite?charset=utf8mb4&parseTime=True&loc=Local"
|
||||
/*
|
||||
docker compose 环境下的链接信息
|
||||
mysql:3306 为 docker compose 中 mysql服务对应的网络名称和端口
|
||||
UserName:Password 设置mysql账户的用户名和密码
|
||||
*/
|
||||
//MysqlDsn = "UserName:Password@(mysql:3306)/FilmSite?charset=utf8mb4&parseTime=True&loc=Local"
|
||||
|
||||
/*
|
||||
redis 配置信息
|
||||
RedisAddr host:port
|
||||
RedisPassword redis访问密码
|
||||
RedisDBNo 使用第几号库
|
||||
*/
|
||||
RedisAddr = `192.168.20.10:6379`
|
||||
RedisPassword = `root`
|
||||
RedisDBNo = 0
|
||||
|
||||
// docker compose 环境下运行如下配置信息
|
||||
//RedisAddr = `redis:6379`
|
||||
//RedisPassword = `Password`
|
||||
//RedisDBNo = 0
|
||||
)
|
||||
160
server/controller/IndexController.go
Normal file
160
server/controller/IndexController.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"server/logic"
|
||||
"server/model"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
StatusOk = "ok"
|
||||
StatusFailed = "failed"
|
||||
)
|
||||
|
||||
// Index 首页数据
|
||||
func Index(c *gin.Context) {
|
||||
// 获取首页所需数据
|
||||
data := logic.IL.IndexPage()
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": StatusOk,
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
|
||||
// CategoriesInfo 分类信息获取
|
||||
func CategoriesInfo(c *gin.Context) {
|
||||
data := logic.IL.GetCategoryInfo()
|
||||
|
||||
if data == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
`status`: StatusFailed,
|
||||
`message`: `暂无分类信息!!!`,
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
`status`: StatusOk,
|
||||
`data`: data,
|
||||
})
|
||||
}
|
||||
|
||||
// FilmDetail 影片详情信息查询
|
||||
func FilmDetail(c *gin.Context) {
|
||||
// 获取请求参数
|
||||
id, err := strconv.Atoi(c.Query("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": StatusFailed,
|
||||
"message": "请求异常,暂无影片信息!!!",
|
||||
})
|
||||
return
|
||||
}
|
||||
// 获取影片详情信息
|
||||
detail := logic.IL.GetFilmDetail(id)
|
||||
// 获取相关推荐影片数据
|
||||
page := model.Page{Current: 0, PageSize: 14}
|
||||
relateMovie := logic.IL.RelateMovie(detail, &page)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": StatusOk,
|
||||
"data": gin.H{
|
||||
"detail": detail,
|
||||
"relate": relateMovie,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// FilmPlayInfo 影视播放页数据
|
||||
func FilmPlayInfo(c *gin.Context) {
|
||||
// 获取请求参数
|
||||
id, err := strconv.Atoi(c.DefaultQuery("id", "0"))
|
||||
playFrom, err := strconv.Atoi(c.DefaultQuery("playFrom", "0"))
|
||||
episode, err := strconv.Atoi(c.DefaultQuery("episode", "0"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": StatusFailed,
|
||||
"message": "请求异常,暂无影片信息!!!",
|
||||
})
|
||||
return
|
||||
}
|
||||
// 获取影片详情信息
|
||||
detail := logic.IL.GetFilmDetail(id)
|
||||
// 推荐影片信息
|
||||
page := model.Page{Current: 0, PageSize: 14}
|
||||
relateMovie := logic.IL.RelateMovie(detail, &page)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": StatusOk,
|
||||
"data": gin.H{
|
||||
"detail": detail,
|
||||
"current": detail.PlayList[playFrom][episode],
|
||||
"currentPlayFrom": playFrom,
|
||||
"currentEpisode": episode,
|
||||
"relate": relateMovie,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// SearchFilm 通过片名模糊匹配库存中的信息
|
||||
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}
|
||||
bl := logic.IL.SearchFilmInfo(strings.TrimSpace(keyword), &page)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": StatusOk,
|
||||
"data": gin.H{
|
||||
"list": bl,
|
||||
"page": page,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// FilmCategory 获取指定分类的影片分页数据,
|
||||
func FilmCategory(c *gin.Context) {
|
||||
// 1.1 首先获取Cid 二级分类id是否存在
|
||||
cidStr := c.DefaultQuery("cid", "")
|
||||
// 1.2 如果pid也不存在直接返回错误信息
|
||||
pidStr := c.DefaultQuery("pid", "")
|
||||
if pidStr == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": StatusFailed,
|
||||
"message": "缺少分类信息",
|
||||
})
|
||||
return
|
||||
}
|
||||
// 1.3 获取pid对应的分类信息
|
||||
pid, _ := strconv.ParseInt(pidStr, 10, 64)
|
||||
category := logic.IL.GetPidCategory(pid)
|
||||
|
||||
// 2 设置分页信息
|
||||
currentStr := c.DefaultQuery("current", "1")
|
||||
current, _ := strconv.Atoi(currentStr)
|
||||
page := model.Page{PageSize: 49, Current: current}
|
||||
// 2.1 如果不存在cid则根据Pid进行查询
|
||||
if cidStr == "" {
|
||||
// 2.2 如果存在pid则根据pid进行查找
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": StatusOk,
|
||||
"data": gin.H{
|
||||
"list": logic.IL.GetFilmCategory(pid, "pid", &page),
|
||||
"category": category,
|
||||
},
|
||||
"page": page,
|
||||
})
|
||||
return
|
||||
}
|
||||
// 2.2 如果存在cid 则根据具体的cid去查询数据
|
||||
cid, _ := strconv.ParseInt(cidStr, 10, 64)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": StatusOk,
|
||||
"data": gin.H{
|
||||
"list": logic.IL.GetFilmCategory(cid, "cid", &page),
|
||||
"category": category,
|
||||
},
|
||||
"page": page,
|
||||
})
|
||||
}
|
||||
46
server/controller/SpiderController.go
Normal file
46
server/controller/SpiderController.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"log"
|
||||
"net/http"
|
||||
"server/config"
|
||||
"server/model"
|
||||
"server/plugin/spider"
|
||||
)
|
||||
|
||||
// SpiderRe 数据清零重开
|
||||
func SpiderRe(c *gin.Context) {
|
||||
// 获取指令参数
|
||||
cip := c.Query("cipher")
|
||||
if cip != config.SpiderCipher {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": StatusFailed,
|
||||
"message": "指令错误无法进行此操作",
|
||||
})
|
||||
return
|
||||
}
|
||||
// 如果指令正确,则执行重制
|
||||
spider.StartSpiderRe()
|
||||
}
|
||||
|
||||
// FixFilmDetail 修复因网络异常造成的影片详情数据丢失
|
||||
func FixFilmDetail(c *gin.Context) {
|
||||
// 获取指令参数
|
||||
cip := c.Query("cipher")
|
||||
if cip != config.SpiderCipher {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": StatusFailed,
|
||||
"message": "指令错误无法进行此操作",
|
||||
})
|
||||
return
|
||||
}
|
||||
// 如果指令正确,则执行详情数据获取
|
||||
spider.AllMovieInfo()
|
||||
log.Println("FilmDetail 重制完成!!!")
|
||||
// 先截断表中的数据
|
||||
model.TunCateSearchTable()
|
||||
// 重新扫描完整的信息到mysql中
|
||||
spider.SearchInfoToMdb()
|
||||
log.Println("SearchInfo 重制完成!!!")
|
||||
}
|
||||
55
server/go.mod
Normal file
55
server/go.mod
Normal file
@@ -0,0 +1,55 @@
|
||||
module server
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.9.0
|
||||
github.com/gocolly/colly/v2 v2.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
|
||||
github.com/antchfx/htmlquery v1.2.3 // indirect
|
||||
github.com/antchfx/xmlquery v1.2.4 // indirect
|
||||
github.com/antchfx/xpath v1.1.8 // indirect
|
||||
github.com/bytedance/sonic v1.8.5 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.12.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
|
||||
github.com/golang/protobuf v1.5.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kennygrant/sanitize v1.2.4 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/leodido/go-urn v1.2.2 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.7 // indirect
|
||||
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca // indirect
|
||||
github.com/temoto/robotstxt v1.1.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.7.0 // indirect
|
||||
golang.org/x/net v0.8.0 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/text v0.8.0 // indirect
|
||||
google.golang.org/appengine v1.6.6 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
209
server/go.sum
Normal file
209
server/go.sum
Normal file
@@ -0,0 +1,209 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/andybalholm/cascadia v1.2.0 h1:vuRCkM5Ozh/BfmsaTm26kbjm0mIOM3yS5Ek/F5h18aE=
|
||||
github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY=
|
||||
github.com/antchfx/htmlquery v1.2.3 h1:sP3NFDneHx2stfNXCKbhHFo8XgNjCACnU/4AO5gWz6M=
|
||||
github.com/antchfx/htmlquery v1.2.3/go.mod h1:B0ABL+F5irhhMWg54ymEZinzMSi0Kt3I2if0BLYa3V0=
|
||||
github.com/antchfx/xmlquery v1.2.4 h1:T/SH1bYdzdjTMoz2RgsfVKbM5uWh3gjDYYepFqQmFv4=
|
||||
github.com/antchfx/xmlquery v1.2.4/go.mod h1:KQQuESaxSlqugE2ZBcM/qn+ebIpt+d+4Xx7YcSGAIrM=
|
||||
github.com/antchfx/xpath v1.1.6/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
|
||||
github.com/antchfx/xpath v1.1.8 h1:PcL6bIX42Px5usSx6xRYw/wjB3wYGkj0MJ9MBzEKVgk=
|
||||
github.com/antchfx/xpath v1.1.8/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
|
||||
github.com/bsm/ginkgo/v2 v2.5.0 h1:aOAnND1T40wEdAtkGSkvSICWeQ8L3UASX7YVCqQx+eQ=
|
||||
github.com/bsm/gomega v1.20.0 h1:JhAwLmtRzXFTx2AkALSLa8ijZafntmhSoU63Ok18Uq8=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.8.5 h1:kjX0/vo5acEQ/sinD/18SkA/lDDUk23F0RcaHvI7omc=
|
||||
github.com/bytedance/sonic v1.8.5/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
|
||||
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI=
|
||||
github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
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.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/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=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/jawher/mow.cli v1.1.0/go.mod h1:aNaQlc7ozF3vw6IJ2dHjp2ZFiA4ozMIYY6PyuRJwlUg=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o=
|
||||
github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/leodido/go-urn v1.2.2 h1:7z68G0FCGvDk646jz1AelTYNYWrTNm0bEcFAo147wt4=
|
||||
github.com/leodido/go-urn v1.2.2/go.mod h1:kUaIbLZWttglzwNuG0pgsh5vuV6u2YcGBYz1hIPjtOQ=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us=
|
||||
github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/redis/go-redis/v9 v9.0.2 h1:BA426Zqe/7r56kCcvxYLWe1mkaz71LKF77GwgFzSxfE=
|
||||
github.com/redis/go-redis/v9 v9.0.2/go.mod h1:/xDTe9EF1LM61hek62Poq2nzQSGj0xSrEtEHbBQevps=
|
||||
github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E=
|
||||
github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef/go.mod h1:8AEUvGVi2uQ5b24BIhcr0GCcpd/RNAFWaN2CJFrWIIQ=
|
||||
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI=
|
||||
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/temoto/robotstxt v1.1.1 h1:Gh8RCs8ouX3hRSxxK7B1mO5RFByQ4CmJZDwgom++JaA=
|
||||
github.com/temoto/robotstxt v1.1.1/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.4.7 h1:rY46lkCspzGHn7+IYsNpSfEv9tA+SU4SkkB+GFX125Y=
|
||||
gorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc=
|
||||
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gorm.io/gorm v1.24.6 h1:wy98aq9oFEetsc4CAbKD2SoBCdMzsbSIvSUUFJuHi5s=
|
||||
gorm.io/gorm v1.24.6/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
137
server/logic/IndexLogic.go
Normal file
137
server/logic/IndexLogic.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"server/config"
|
||||
"server/model"
|
||||
"server/plugin/db"
|
||||
)
|
||||
|
||||
/*
|
||||
*
|
||||
IndexController数据处理
|
||||
*/
|
||||
|
||||
type IndexLogic struct {
|
||||
}
|
||||
|
||||
var IL *IndexLogic
|
||||
|
||||
// IndexPage 首页数据处理
|
||||
func (i *IndexLogic) IndexPage() gin.H {
|
||||
Info := gin.H{}
|
||||
// 首页分类数据处理
|
||||
|
||||
// 1. 导航分类数据处理, 只提供 电影 电视剧 综艺 动漫 四大顶级分类和其子分类
|
||||
tree := model.CategoryTree{Category: &model.Category{Id: 0, Name: "分类信息"}}
|
||||
sysTree := model.GetCategoryTree()
|
||||
// 由于采集源数据格式不一,因此采用名称匹配
|
||||
for _, c := range sysTree.Children {
|
||||
switch c.Category.Name {
|
||||
case "电影", "电影片", "连续剧", "电视剧", "综艺", "综艺片", "动漫", "动漫片":
|
||||
tree.Children = append(tree.Children, c)
|
||||
}
|
||||
}
|
||||
Info["category"] = tree
|
||||
// 2. 提供用于首页展示的顶级分类影片信息, 每分类 14条数据
|
||||
var list []gin.H
|
||||
for _, c := range tree.Children {
|
||||
page := model.Page{PageSize: 14, Current: 1}
|
||||
movies := model.GetMovieListByPid(c.Id, &page)
|
||||
item := gin.H{"nav": c, "movies": movies}
|
||||
list = append(list, item)
|
||||
}
|
||||
Info["content"] = list
|
||||
|
||||
return Info
|
||||
}
|
||||
|
||||
// GetFilmDetail 影片详情信息页面处理
|
||||
func (i *IndexLogic) GetFilmDetail(id int) model.MovieDetail {
|
||||
// 通过Id 获取影片search信息
|
||||
search := model.SearchInfo{}
|
||||
db.Mdb.Where("mid", id).First(&search)
|
||||
// 获取redis中的完整影视信息 MovieDetail:Cid11:Id24676
|
||||
movieDetail := model.GetDetailByKey(fmt.Sprintf(config.MovieDetailKey, search.Cid, search.Mid))
|
||||
return movieDetail
|
||||
}
|
||||
|
||||
// GetCategoryInfo 分类信息获取, 组装导航栏需要的信息
|
||||
func (i *IndexLogic) GetCategoryInfo() gin.H {
|
||||
// 组装nav导航所需的信息
|
||||
nav := gin.H{}
|
||||
// 1.获取所有分类信息
|
||||
tree := model.GetCategoryTree()
|
||||
// 2. 过滤出主页四大分类的tree信息
|
||||
for _, t := range tree.Children {
|
||||
switch t.Category.Name {
|
||||
case "动漫", "动漫片":
|
||||
nav["cartoon"] = t
|
||||
case "电影", "电影片":
|
||||
nav["film"] = t
|
||||
case "连续剧", "电视剧":
|
||||
nav["tv"] = t
|
||||
case "综艺", "综艺片":
|
||||
nav["variety"] = t
|
||||
}
|
||||
}
|
||||
// 获取所有的分类
|
||||
return nav
|
||||
}
|
||||
|
||||
// SearchFilmInfo 获取关键字匹配的影片信息
|
||||
func (i *IndexLogic) SearchFilmInfo(key string, page *model.Page) []model.MovieBasicInfo {
|
||||
// 1. 从mysql中获取满足条件的数据, 每页10条
|
||||
sl := model.SearchFilmKeyword(key, page)
|
||||
// 2. 获取redis中的basicMovieInfo信息
|
||||
var bl []model.MovieBasicInfo
|
||||
for _, s := range sl {
|
||||
bl = append(bl, model.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 {
|
||||
// 1. 根据不同类型进不同的查找
|
||||
var basicList []model.MovieBasicInfo
|
||||
switch idType {
|
||||
case "pid":
|
||||
basicList = model.GetMovieListByPid(id, page)
|
||||
case "cid":
|
||||
basicList = model.GetMovieListByCid(id, page)
|
||||
}
|
||||
return basicList
|
||||
}
|
||||
|
||||
// GetPidCategory 获取pid对应的分类信息
|
||||
func (i *IndexLogic) GetPidCategory(pid int64) *model.CategoryTree {
|
||||
tree := model.GetCategoryTree()
|
||||
for _, t := range tree.Children {
|
||||
if t.Id == pid {
|
||||
return t
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RelateMovie 根据当前影片信息匹配相关的影片
|
||||
func (i *IndexLogic) RelateMovie(detail model.MovieDetail, page *model.Page) []model.MovieBasicInfo {
|
||||
/*
|
||||
根据当前影片信息匹配相关的影片
|
||||
1. 分类Cid,
|
||||
2. 影片名Name
|
||||
3. 剧情内容标签class_tag
|
||||
4. 地区 area
|
||||
5. 语言 Language
|
||||
*/
|
||||
search := model.SearchInfo{
|
||||
Cid: detail.Cid,
|
||||
Name: detail.Name,
|
||||
ClassTag: detail.ClassTag,
|
||||
Area: detail.Area,
|
||||
Language: detail.Language,
|
||||
}
|
||||
return model.GetRelateMovieBasicInfo(search, page)
|
||||
}
|
||||
47
server/main.go
Normal file
47
server/main.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"server/model"
|
||||
"server/plugin/db"
|
||||
"server/plugin/spider"
|
||||
"server/router"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// 执行初始化前等待30s , 让mysql服务完成初始化指令
|
||||
time.Sleep(time.Second * 30)
|
||||
//初始化redis客户端
|
||||
err := db.InitRedisConn()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// 初始化mysql
|
||||
err = db.InitMysql()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
func main() {
|
||||
start()
|
||||
}
|
||||
|
||||
func start() {
|
||||
// 开启前先判断是否需要执行Spider
|
||||
ExecSpider()
|
||||
// web服务启动后开启定时任务, 用于定期更新资源
|
||||
spider.RegularUpdateMovie()
|
||||
// 开启路由监听
|
||||
r := router.SetupRouter()
|
||||
_ = r.Run(`:3601`)
|
||||
|
||||
}
|
||||
|
||||
func ExecSpider() {
|
||||
// 判断分类信息是否存在
|
||||
isStart := model.ExistsCategoryTree()
|
||||
// 如果分类信息不存在则进行一次完整爬取
|
||||
if !isStart {
|
||||
spider.StartSpider()
|
||||
}
|
||||
}
|
||||
43
server/model/Categories.go
Normal file
43
server/model/Categories.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"server/config"
|
||||
"server/plugin/db"
|
||||
)
|
||||
|
||||
// Category 分类信息
|
||||
type Category struct {
|
||||
Id int64 `json:"id"` // 分类ID
|
||||
Pid int64 `json:"pid"` // 父级分类ID
|
||||
Name string `json:"name"` // 分类名称
|
||||
}
|
||||
|
||||
// CategoryTree 分类信息树形结构
|
||||
type CategoryTree struct {
|
||||
*Category
|
||||
Children []*CategoryTree `json:"children"` // 子分类信息
|
||||
}
|
||||
|
||||
// SaveCategoryTree 保存影片分类信息
|
||||
func SaveCategoryTree(tree string) error {
|
||||
return db.Rdb.Set(db.Cxt, config.CategoryTreeKey, tree, config.CategoryTreeExpired).Err()
|
||||
}
|
||||
|
||||
// GetCategoryTree 获取影片分类信息
|
||||
func GetCategoryTree() CategoryTree {
|
||||
data := db.Rdb.Get(db.Cxt, config.CategoryTreeKey).Val()
|
||||
tree := CategoryTree{}
|
||||
_ = json.Unmarshal([]byte(data), &tree)
|
||||
return tree
|
||||
}
|
||||
|
||||
// ExistsCategoryTree 查询分类信息是否存在
|
||||
func ExistsCategoryTree() bool {
|
||||
exists, err := db.Rdb.Exists(db.Cxt, config.CategoryTreeKey).Result()
|
||||
if err != nil {
|
||||
log.Println("ExistsCategoryTree Error", err)
|
||||
}
|
||||
return exists == 1
|
||||
}
|
||||
81
server/model/LZJson.go
Normal file
81
server/model/LZJson.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package model
|
||||
|
||||
/*
|
||||
量子资源JSON解析
|
||||
*/
|
||||
|
||||
// ClassInfo class 分类数据
|
||||
type ClassInfo struct {
|
||||
Id int64 `json:"type_id"` //分类ID
|
||||
Pid int64 `json:"type_pid"` //上级分类ID
|
||||
Name string `json:"type_name"` //分类名称
|
||||
}
|
||||
|
||||
// MovieInfo 影片数据
|
||||
type MovieInfo struct {
|
||||
Id int64 `json:"vod_id"` // 影片ID
|
||||
Name string `json:"vod_name"` // 影片名
|
||||
Cid int64 `json:"type_id"` // 所属分类ID
|
||||
CName string `json:"type_name"` // 所属分类名称
|
||||
EnName string `json:"vod_en"` // 英文片名
|
||||
Time string `json:"vod_time"` // 更新时间
|
||||
Remarks string `json:"vod_remarks"` // 备注 | 清晰度
|
||||
PlayFrom string `json:"vod_play_from"` // 播放来源
|
||||
}
|
||||
|
||||
// MovieListInfo 影视列表响应数据
|
||||
type MovieListInfo struct {
|
||||
Code int64 `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Page string `json:"page"`
|
||||
PageCount int64 `json:"pagecount"`
|
||||
Limit string `json:"limit"`
|
||||
Total int64 `json:"total"`
|
||||
List []MovieInfo `json:"list"`
|
||||
Class []ClassInfo `json:"class"`
|
||||
}
|
||||
|
||||
// MovieDetailInfo 影片详情数据 (只保留需要的部分)
|
||||
type MovieDetailInfo struct {
|
||||
Id int64 `json:"vod_id"` //影片Id
|
||||
Cid int64 `json:"type_id"` //分类ID
|
||||
Pid int64 `json:"type_id_1"` //一级分类ID
|
||||
Name string `json:"vod_name"` //片名
|
||||
SubTitle string `json:"vod_sub"` //子标题
|
||||
CName string `json:"type_name"` //分类名称
|
||||
EnName string `json:"vod_en"` //英文名
|
||||
Initial string `json:"vod_letter"` //首字母
|
||||
ClassTag string `json:"vod_class"` //分类标签
|
||||
Pic string `json:"vod_pic"` //简介图片
|
||||
Actor string `json:"vod_actor"` //主演
|
||||
Director string `json:"vod_director"` //导演
|
||||
Writer string `json:"vod_writer"` //作者
|
||||
Blurb string `json:"vod_blurb"` //简介, 残缺,不建议使用
|
||||
Remarks string `json:"vod_remarks"` // 更新情况
|
||||
PubDate string `json:"vod_pubdate"` //上映时间
|
||||
Area string `json:"vod_area"` // 地区
|
||||
Language string `json:"vod_lang"` //语言
|
||||
Year string `json:"vod_year"` //年份
|
||||
State string `json:"vod_state"` //影片状态 正片|预告...
|
||||
UpdateTime string `json:"vod_time"` //更新时间
|
||||
AddTime int64 `json:"vod_time_add"` //资源添加时间戳
|
||||
DbId int64 `json:"vod_douban_id"` //豆瓣id
|
||||
DbScore string `json:"vod_douban_score"` // 豆瓣评分
|
||||
Content string `json:"vod_content"` //内容简介
|
||||
PlayFrom string `json:"vod_play_from"` // 播放来源
|
||||
PlaySeparator string `json:"vod_play_note"` // 播放信息分隔符
|
||||
PlayUrl string `json:"vod_play_url"` //播放地址url
|
||||
DownFrom string `json:"vod_down_from"` //下载来源 例: http
|
||||
DownUrl string `json:"vod_down_url"` // 下载url地址
|
||||
}
|
||||
|
||||
// DetailListInfo 影视详情信息
|
||||
type DetailListInfo struct {
|
||||
Code int64 `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Page int64 `json:"page"`
|
||||
PageCount int64 `json:"pagecount"`
|
||||
Limit string `json:"limit"`
|
||||
Total int64 `json:"total"`
|
||||
List []MovieDetailInfo `json:"list"`
|
||||
}
|
||||
332
server/model/Movies.go
Normal file
332
server/model/Movies.go
Normal file
@@ -0,0 +1,332 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"log"
|
||||
"server/config"
|
||||
"server/plugin/db"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Movie 影片基本信息
|
||||
type Movie struct {
|
||||
Id int64 `json:"id"` // 影片ID
|
||||
Name string `json:"name"` // 影片名
|
||||
Cid int64 `json:"cid"` // 所属分类ID
|
||||
CName string `json:"CName"` // 所属分类名称
|
||||
EnName string `json:"enName"` // 英文片名
|
||||
Time string `json:"time"` // 更新时间
|
||||
Remarks string `json:"remarks"` // 备注 | 清晰度
|
||||
PlayFrom string `json:"playFrom"` // 播放来源
|
||||
}
|
||||
|
||||
// MovieDescriptor 影片详情介绍信息
|
||||
type MovieDescriptor struct {
|
||||
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"` //作者
|
||||
Blurb string `json:"blurb"` //简介, 残缺,不建议使用
|
||||
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 int64 `json:"addTime"` //资源添加时间戳
|
||||
DbId int64 `json:"dbId"` //豆瓣id
|
||||
DbScore string `json:"dbScore"` // 豆瓣评分
|
||||
Content string `json:"content"` //内容简介
|
||||
}
|
||||
|
||||
// MovieBasicInfo 影片基本信息
|
||||
type MovieBasicInfo struct {
|
||||
Id int64 `json:"id"` //影片Id
|
||||
Cid int64 `json:"cid"` //分类ID
|
||||
Pid int64 `json:"pid"` //一级分类ID
|
||||
Name string `json:"name"` //片名
|
||||
SubTitle string `json:"subTitle"` //子标题
|
||||
CName string `json:"cName"` //分类名称
|
||||
State string `json:"state"` //影片状态 正片|预告...
|
||||
Picture string `json:"picture"` //简介图片
|
||||
Actor string `json:"actor"` //主演
|
||||
Director string `json:"director"` //导演
|
||||
Blurb string `json:"blurb"` //简介, 不完整
|
||||
Remarks string `json:"remarks"` // 更新情况
|
||||
Area string `json:"area"` // 地区
|
||||
Year string `json:"year"` //年份
|
||||
}
|
||||
|
||||
// MovieUrlInfo 影视资源url信息
|
||||
type MovieUrlInfo struct {
|
||||
Episode string `json:"episode"` // 集数
|
||||
Link string `json:"link"` // 播放地址
|
||||
}
|
||||
|
||||
// MovieDetail 影片详情信息
|
||||
type MovieDetail 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
|
||||
//PlaySeparator string `json:"playSeparator"` // 播放信息分隔符
|
||||
PlayList [][]MovieUrlInfo `json:"playList"` //播放地址url
|
||||
DownloadList [][]MovieUrlInfo `json:"downloadList"` // 下载url地址
|
||||
MovieDescriptor `json:"descriptor"` //影片描述信息
|
||||
}
|
||||
|
||||
// SaveMoves 保存影片分页请求list
|
||||
func SaveMoves(list []Movie) (err error) {
|
||||
// 整合数据
|
||||
for _, m := range list {
|
||||
//score, _ := time.ParseInLocation(time.DateTime, m.Time, time.Local)
|
||||
movie, _ := json.Marshal(m)
|
||||
// 以Cid为目录为集合进行存储, 便于后续搜索, 以影片id为分值进行存储 例 MovieList:Cid%d
|
||||
err = db.Rdb.ZAdd(db.Cxt, fmt.Sprintf(config.MovieListInfoKey, m.Cid), redis.Z{Score: float64(m.Id), Member: movie}).Err()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// AllMovieInfoKey 获取redis中所有的影视列表信息key MovieList:Cid
|
||||
func AllMovieInfoKey() []string {
|
||||
return db.Rdb.Keys(db.Cxt, fmt.Sprint("MovieList:Cid*")).Val()
|
||||
}
|
||||
|
||||
// GetMovieListByKey 获取指定分类的影片列表数据
|
||||
func GetMovieListByKey(key string) []string {
|
||||
return db.Rdb.ZRange(db.Cxt, key, 0, -1).Val()
|
||||
}
|
||||
|
||||
// SaveDetails 保存影片详情信息到redis中 格式: MovieDetail:Cid?:Id?
|
||||
func SaveDetails(list []MovieDetail) (err error) {
|
||||
// 遍历list中的信息
|
||||
for _, detail := range list {
|
||||
// 序列化影片详情信息
|
||||
data, _ := json.Marshal(detail)
|
||||
// 1. 原使用Zset存储, 但是不便于单个检索 db.Rdb.ZAdd(db.Cxt, fmt.Sprintf("%s:Cid%d", config.MovieDetailKey, detail.Cid), redis.Z{Score: float64(detail.Id), Member: member}).Err()
|
||||
// 改为普通 k v 存储, k-> id关键字, v json序列化的结果, //只保留十天, 没周更新一次
|
||||
err = db.Rdb.Set(db.Cxt, fmt.Sprintf(config.MovieDetailKey, detail.Cid, detail.Id), data, config.CategoryTreeExpired).Err()
|
||||
// 2. 同步保存简略信息到redis中
|
||||
SaveMovieBasicInfo(detail)
|
||||
// 3. 保存Search检索信息到redis
|
||||
if err == nil {
|
||||
// 转换 detail信息
|
||||
searchInfo := ConvertSearchInfo(detail)
|
||||
// 放弃redis进行检索, 多条件处理不方便
|
||||
//err = AddSearchInfo(searchInfo)
|
||||
// 只存储用于检索对应影片的关键字信息
|
||||
SearchKeyword(searchInfo)
|
||||
}
|
||||
|
||||
}
|
||||
// 保存一份search信息到mysql, 批量存储
|
||||
BatchSaveSearchInfo(list)
|
||||
return err
|
||||
}
|
||||
|
||||
// SaveMovieBasicInfo 摘取影片的详情部分信息转存为影视基本信息
|
||||
func SaveMovieBasicInfo(detail MovieDetail) {
|
||||
basicInfo := MovieBasicInfo{
|
||||
Id: detail.Id,
|
||||
Cid: detail.Cid,
|
||||
Pid: detail.Pid,
|
||||
Name: detail.Name,
|
||||
SubTitle: detail.SubTitle,
|
||||
CName: detail.CName,
|
||||
State: detail.State,
|
||||
Picture: detail.Picture,
|
||||
Actor: detail.Actor,
|
||||
Director: detail.Director,
|
||||
Blurb: detail.Blurb,
|
||||
Remarks: detail.Remarks,
|
||||
Area: detail.Area,
|
||||
Year: detail.Year,
|
||||
}
|
||||
data, _ := json.Marshal(basicInfo)
|
||||
_ = db.Rdb.Set(db.Cxt, fmt.Sprintf(config.MovieBasicInfoKey, detail.Cid, detail.Id), data, config.CategoryTreeExpired).Err()
|
||||
}
|
||||
|
||||
// AddSearchInfo 将影片关键字信息整合后存入search 集合中
|
||||
func AddSearchInfo(searchInfo SearchInfo) (err error) {
|
||||
// 片名 Name 分类 CName 类别标签 classTag 地区 Area 语言 Language 年份 Year 首字母 Initial, 排序
|
||||
data, _ := json.Marshal(searchInfo)
|
||||
// 时间排序 score -->时间戳 DbId 排序 --> 热度, 评分排序 DbScore
|
||||
err = db.Rdb.ZAdd(db.Cxt, fmt.Sprintf("%s:Pid%d", config.SearchTimeListKey, searchInfo.Pid), redis.Z{Score: float64(searchInfo.Time), Member: data}).Err()
|
||||
err = db.Rdb.ZAdd(db.Cxt, fmt.Sprintf("%s:Pid%d", config.SearchScoreListKey, searchInfo.Pid), redis.Z{Score: searchInfo.Score, Member: data}).Err()
|
||||
err = db.Rdb.ZAdd(db.Cxt, fmt.Sprintf("%s:Pid%d", config.SearchHeatListKey, searchInfo.Pid), redis.Z{Score: float64(searchInfo.Rank), Member: data}).Err()
|
||||
// 添加搜索关键字信息
|
||||
SearchKeyword(searchInfo)
|
||||
return
|
||||
}
|
||||
|
||||
// SearchKeyword 设置search关键字集合
|
||||
func SearchKeyword(search SearchInfo) {
|
||||
// 首先获取redis中的search 关键字信息
|
||||
key := fmt.Sprintf("%s:Pid%d", config.SearchKeys, search.Pid)
|
||||
keyword := db.Rdb.HGetAll(db.Cxt, key).Val()
|
||||
if keyword["Year"] == "" {
|
||||
currentYear := time.Now().Year()
|
||||
year := ""
|
||||
for i := 0; i < 12; i++ {
|
||||
// 提供当前年份前推十二年的搜索
|
||||
year = fmt.Sprintf("%s,%d", year, currentYear-i)
|
||||
}
|
||||
initial := ""
|
||||
for i := 65; i <= 90; i++ {
|
||||
initial = fmt.Sprintf("%s,%c", initial, i)
|
||||
}
|
||||
keyword = map[string]string{
|
||||
//"Name": "",
|
||||
"Category": "",
|
||||
"Tag": "",
|
||||
"Area": "",
|
||||
"Language": "",
|
||||
"Year": strings.Trim(year, ","),
|
||||
"Initial": strings.Trim(initial, ","),
|
||||
"Sort": "Time,Db,Score", // 默认,一般不修改
|
||||
}
|
||||
}
|
||||
// 分类标签处理
|
||||
if !strings.Contains(keyword["Category"], search.CName) {
|
||||
keyword["Category"] = strings.Trim(fmt.Sprintf("%s,%s", keyword["Category"], search.CName), ",")
|
||||
}
|
||||
// 影视内容分类处理
|
||||
if strings.Contains(search.ClassTag, "/") {
|
||||
for _, t := range strings.Split(search.ClassTag, "/") {
|
||||
if !strings.Contains(keyword["Tag"], t) {
|
||||
keyword["Tag"] = strings.Trim(fmt.Sprintf("%s,%s", keyword["Tag"], t), ",")
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(search.ClassTag, ",") {
|
||||
for _, t := range strings.Split(search.ClassTag, ",") {
|
||||
if !strings.Contains(keyword["Tag"], t) {
|
||||
keyword["Tag"] = strings.Trim(fmt.Sprintf("%s,%s", keyword["Tag"], t), ",")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !strings.Contains(keyword["Tag"], search.ClassTag) {
|
||||
keyword["Tag"] = strings.Trim(fmt.Sprintf("%s,%s", keyword["Tag"], search.ClassTag), ",")
|
||||
}
|
||||
}
|
||||
// 如果地区中包含 / 分隔符 则先进行切分处理
|
||||
if strings.Contains(search.Area, "/") {
|
||||
for _, s := range strings.Split(search.Area, "/") {
|
||||
if !strings.Contains(keyword["Area"], strings.TrimSpace(s)) {
|
||||
keyword["Area"] = strings.Trim(fmt.Sprintf("%s,%s", keyword["Area"], s), ",")
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(search.Area, ",") {
|
||||
for _, s := range strings.Split(search.Area, ",") {
|
||||
if !strings.Contains(keyword["Area"], strings.TrimSpace(s)) {
|
||||
keyword["Area"] = strings.Trim(fmt.Sprintf("%s,%s", keyword["Area"], s), ",")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !strings.Contains(keyword["Area"], search.Area) {
|
||||
keyword["Area"] = strings.Trim(fmt.Sprintf("%s,%s", keyword["Area"], search.Area), ",")
|
||||
}
|
||||
}
|
||||
// 语言处理
|
||||
if strings.Contains(search.Language, "/") {
|
||||
for _, l := range strings.Split(search.Language, "/") {
|
||||
if !strings.Contains(keyword["Language"], l) {
|
||||
keyword["Language"] = strings.Trim(fmt.Sprintf("%s,%s", keyword["Language"], l), ",")
|
||||
}
|
||||
}
|
||||
|
||||
} else if strings.Contains(search.Language, ",") {
|
||||
for _, l := range strings.Split(search.Language, ",") {
|
||||
if !strings.Contains(keyword["Language"], l) {
|
||||
keyword["Language"] = strings.Trim(fmt.Sprintf("%s,%s", keyword["Language"], l), ",")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !strings.Contains(keyword["Language"], search.Language) {
|
||||
keyword["Language"] = strings.Trim(fmt.Sprintf("%s,%s", keyword["Language"], search.Language), ",")
|
||||
}
|
||||
}
|
||||
_ = db.Rdb.HMSet(db.Cxt, key, keyword).Err()
|
||||
}
|
||||
|
||||
// BatchSaveSearchInfo 批量保存Search信息
|
||||
func BatchSaveSearchInfo(list []MovieDetail) {
|
||||
var infoList []SearchInfo
|
||||
for _, v := range list {
|
||||
infoList = append(infoList, ConvertSearchInfo(v))
|
||||
}
|
||||
// 将检索信息存入redis中做一次转存
|
||||
RdbSaveSearchInfo(infoList)
|
||||
|
||||
// 废弃方案, 频繁大量入库容易引起主键冲突, 事务影响速率
|
||||
// 批量插入时应对已存在数据进行检测, 使用mysql事务进行锁表
|
||||
//BatchSave(infoList)
|
||||
// 使用批量添加or更新
|
||||
//BatchSaveOrUpdate(infoList)
|
||||
}
|
||||
|
||||
// ConvertSearchInfo 将detail信息处理成 searchInfo
|
||||
func ConvertSearchInfo(detail MovieDetail) SearchInfo {
|
||||
score, _ := strconv.ParseFloat(detail.DbScore, 64)
|
||||
stamp, _ := time.ParseInLocation(time.DateTime, detail.UpdateTime, time.Local)
|
||||
year, err := strconv.ParseInt(detail.Year, 10, 64)
|
||||
if err != nil {
|
||||
year = 0
|
||||
}
|
||||
|
||||
return SearchInfo{
|
||||
Mid: detail.Id,
|
||||
Cid: detail.Cid,
|
||||
Pid: detail.Pid,
|
||||
Name: detail.Name,
|
||||
CName: detail.CName,
|
||||
ClassTag: detail.ClassTag,
|
||||
Area: detail.Area,
|
||||
Language: detail.Language,
|
||||
Year: year,
|
||||
Initial: detail.Initial,
|
||||
Score: score,
|
||||
Rank: detail.DbId,
|
||||
Time: stamp.Unix(),
|
||||
State: detail.State,
|
||||
Remarks: detail.Remarks,
|
||||
// releaseDate 部分影片缺失该参数, 所以使用添加时间作为上映时间排序
|
||||
ReleaseDate: detail.AddTime,
|
||||
}
|
||||
}
|
||||
|
||||
// GetBasicInfoByKey 获取Id对应的影片基本信息
|
||||
func GetBasicInfoByKey(key string) MovieBasicInfo {
|
||||
// 反序列化得到的结果
|
||||
data := []byte(db.Rdb.Get(db.Cxt, key).Val())
|
||||
basic := MovieBasicInfo{}
|
||||
_ = json.Unmarshal(data, &basic)
|
||||
return basic
|
||||
}
|
||||
|
||||
// GetDetailByKey 获取影片对应的详情信息
|
||||
func GetDetailByKey(key string) MovieDetail {
|
||||
// 反序列化得到的结果
|
||||
data := []byte(db.Rdb.Get(db.Cxt, key).Val())
|
||||
detail := MovieDetail{}
|
||||
_ = json.Unmarshal(data, &detail)
|
||||
return detail
|
||||
}
|
||||
|
||||
// SearchMovie 搜索关键字影片
|
||||
func SearchMovie() {
|
||||
data, err := db.Rdb.ZScan(db.Cxt, "MovieList:cid30", 0, `*天使*`, config.SearchCount).Val()
|
||||
log.Println(err)
|
||||
fmt.Println(data)
|
||||
}
|
||||
291
server/model/Search.go
Normal file
291
server/model/Search.go
Normal file
@@ -0,0 +1,291 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gorm.io/gorm"
|
||||
"log"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"server/config"
|
||||
"server/plugin/db"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SearchInfo 存储用于检索的信息
|
||||
type SearchInfo struct {
|
||||
gorm.Model
|
||||
Mid int64 `json:"mid" gorm:"uniqueIndex:idx_mid"` //影片ID
|
||||
Cid int64 `json:"cid"` //分类ID
|
||||
Pid int64 `json:"pid"` //上级分类ID
|
||||
Name string `json:"name"` // 片名
|
||||
CName string `json:"CName"` // 分类名称
|
||||
ClassTag string `json:"classTag"` //类型标签
|
||||
Area string `json:"area"` // 地区
|
||||
Language string `json:"language"` // 语言
|
||||
Year int64 `json:"year"` // 年份
|
||||
Initial string `json:"initial"` // 首字母
|
||||
Score float64 `json:"score"` //评分
|
||||
Time int64 `json:"time"` // 更新时间
|
||||
Rank int64 `json:"rank"` // 热度排行id
|
||||
State string `json:"state"` //状态 正片|预告
|
||||
Remarks string `json:"remarks"` // 完结 | 更新至x集
|
||||
ReleaseDate int64 `json:"releaseDate"` //上映时间 时间戳
|
||||
}
|
||||
|
||||
// Page 分页信息结构体
|
||||
type Page struct {
|
||||
PageSize int `json:"pageSize"` // 每页大小
|
||||
Current int `json:"current"` // 当前页
|
||||
PageCount int `json:"pageCount"` // 总页数
|
||||
Total int `json:"total"` // 总记录数
|
||||
//List []interface{} `json:"list"` // 数据
|
||||
}
|
||||
|
||||
func (s *SearchInfo) TableName() string {
|
||||
return "search_lz"
|
||||
//return "search_fs"
|
||||
}
|
||||
|
||||
// ================================= Spider 数据处理(redis) =================================
|
||||
|
||||
// RdbSaveSearchInfo 批量保存检索信息到redis
|
||||
func RdbSaveSearchInfo(list []SearchInfo) {
|
||||
// 1.整合一下zset数据集
|
||||
var members []redis.Z
|
||||
for _, s := range list {
|
||||
member, _ := json.Marshal(s)
|
||||
members = append(members, redis.Z{Score: float64(s.Mid), Member: member})
|
||||
}
|
||||
// 2.批量保存到zset集合中
|
||||
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() {
|
||||
// 删除redis中当前库存储的所有数据
|
||||
db.Rdb.FlushDB(db.Cxt)
|
||||
// 删除mysql中留存的检索表
|
||||
var s *SearchInfo
|
||||
db.Mdb.Exec(fmt.Sprintf(`drop table if exists %s`, s.TableName()))
|
||||
}
|
||||
|
||||
// ================================= Spider 数据处理(mysql) =================================
|
||||
|
||||
// CreateSearchTable 创建存储检索信息的数据表
|
||||
func CreateSearchTable() {
|
||||
// 1. 判断表中是否存在当前表
|
||||
isExist := db.Mdb.Migrator().HasTable(&SearchInfo{})
|
||||
// 如果不存在则创建表
|
||||
if !isExist {
|
||||
err := db.Mdb.AutoMigrate(&SearchInfo{})
|
||||
if err != nil {
|
||||
log.Println("Create Table SearchInfo Failed: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BatchSave 批量保存影片search信息
|
||||
func BatchSave(list []SearchInfo) {
|
||||
tx := db.Mdb.Begin()
|
||||
// 防止程序异常终止
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
if err := tx.CreateInBatches(list, len(list)).Error; err != nil {
|
||||
// 插入失败则回滚事务, 重新进行插入
|
||||
tx.Rollback()
|
||||
return
|
||||
}
|
||||
// 插入成功后输出一下成功信息
|
||||
//log.Println("BatchSave SearchInfo Successful, Count: ", len(list))
|
||||
tx.Commit()
|
||||
}
|
||||
|
||||
// BatchSaveOrUpdate 判断数据库中是否存在对应mid的数据, 如果存在则更新, 否则插入
|
||||
func BatchSaveOrUpdate(list []SearchInfo) {
|
||||
tx := db.Mdb.Begin()
|
||||
// 失败则回滚事务
|
||||
//defer func() {
|
||||
// if r := recover(); r != nil {
|
||||
// tx.Rollback()
|
||||
// }
|
||||
//}()
|
||||
for _, info := range list {
|
||||
var count int64
|
||||
// 通过当前影片id 对应的记录数
|
||||
tx.Model(&SearchInfo{}).Where("mid", info.Mid).Count(&count)
|
||||
// 如果存在对应数据则进行更新, 否则进行删除
|
||||
if count > 0 {
|
||||
// 记录已经存在则执行更新部分内容
|
||||
err := tx.Model(&SearchInfo{}).Where("mid", info.Mid).Updates(SearchInfo{Time: info.Time, Rank: info.Rank, State: info.State,
|
||||
Remarks: info.Remarks, Score: info.Score, ReleaseDate: info.ReleaseDate}).Error
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
} else {
|
||||
// 执行插入操作
|
||||
if err := tx.Create(&info).Error; err != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}
|
||||
}
|
||||
// 提交事务
|
||||
tx.Commit()
|
||||
}
|
||||
|
||||
// SaveSearchData 添加影片检索信息
|
||||
func SaveSearchData(s SearchInfo) {
|
||||
// 先查询数据库中是否存在对应记录
|
||||
isExist := SearchMovieInfo(s.Mid)
|
||||
// 如果不存在对应记录则 保存当前记录
|
||||
if !isExist {
|
||||
db.Mdb.Create(&s)
|
||||
}
|
||||
}
|
||||
|
||||
// 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{})
|
||||
}
|
||||
|
||||
// TunCateSearchTable 截断SearchInfo数据表
|
||||
func TunCateSearchTable() {
|
||||
var searchInfo *SearchInfo
|
||||
err := db.Mdb.Exec(fmt.Sprint("TRUNCATE TABLE ", searchInfo.TableName())).Error
|
||||
if err != nil {
|
||||
log.Println("TRUNCATE TABLE Error: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ================================= API 数据接口信息处理 =================================
|
||||
|
||||
// GetMovieListByPid 通过Pid 分类ID 获取对应影片的数据信息
|
||||
func GetMovieListByPid(pid int64, page *Page) []MovieBasicInfo {
|
||||
// 返回分页参数
|
||||
var count int64
|
||||
db.Mdb.Model(&SearchInfo{}).Where("pid", pid).Count(&count)
|
||||
page.Total = int(count)
|
||||
page.PageCount = int((page.Total + page.PageSize - 1) / page.PageSize)
|
||||
// 进行具体的信息查询
|
||||
var s []SearchInfo
|
||||
if err := db.Mdb.Limit(page.PageSize).Offset((page.Current-1)*page.PageSize).Where("pid", pid).Order("year DESC, time DESC").Find(&s).Error; err != nil {
|
||||
log.Println(err)
|
||||
return nil
|
||||
}
|
||||
// 通过影片ID去redis中获取id对应数据信息
|
||||
var list []MovieBasicInfo
|
||||
for _, v := range s {
|
||||
// 通过key搜索指定的影片信息 , MovieDetail:Cid6:Id15441
|
||||
list = append(list, GetBasicInfoByKey(fmt.Sprintf(config.MovieBasicInfoKey, v.Cid, v.Mid)))
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// SearchFilmKeyword 通过关键字搜索库存中满足条件的影片名
|
||||
func SearchFilmKeyword(keyword string, page *Page) []SearchInfo {
|
||||
var searchList []SearchInfo
|
||||
// 1. 先统计搜索满足条件的数据量
|
||||
var count int64
|
||||
db.Mdb.Model(&SearchInfo{}).Where("name LIKE ?", fmt.Sprint(`%`, keyword, `%`)).Count(&count)
|
||||
page.Total = int(count)
|
||||
page.PageCount = int((page.Total + page.PageSize - 1) / page.PageSize)
|
||||
// 2. 获取满足条件的数据
|
||||
db.Mdb.Limit(page.PageSize).Offset((page.Current-1)*page.PageSize).
|
||||
Where("name LIKE ?", fmt.Sprint(`%`, keyword, `%`)).Order("year DESC, time DESC").Find(&searchList)
|
||||
return searchList
|
||||
}
|
||||
|
||||
// GetMovieListByCid 通过Cid查找对应的影片分页数据, 不适合GetMovieListByPid 糅合
|
||||
func GetMovieListByCid(cid int64, page *Page) []MovieBasicInfo {
|
||||
// 返回分页参数
|
||||
var count int64
|
||||
db.Mdb.Model(&SearchInfo{}).Where("cid", cid).Count(&count)
|
||||
page.Total = int(count)
|
||||
page.PageCount = int((page.Total + page.PageSize - 1) / page.PageSize)
|
||||
// 进行具体的信息查询
|
||||
var s []SearchInfo
|
||||
if err := db.Mdb.Limit(page.PageSize).Offset((page.Current-1)*page.PageSize).Where("cid", cid).Order("year DESC, time DESC").Find(&s).Error; err != nil {
|
||||
log.Println(err)
|
||||
return nil
|
||||
}
|
||||
// 通过影片ID去redis中获取id对应数据信息
|
||||
var list []MovieBasicInfo
|
||||
for _, v := range s {
|
||||
// 通过key搜索指定的影片信息 , MovieDetail:Cid6:Id15441
|
||||
list = append(list, GetBasicInfoByKey(fmt.Sprintf(config.MovieBasicInfoKey, v.Cid, v.Mid)))
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// GetRelateMovieBasicInfo GetRelateMovie 根据SearchInfo获取相关影片
|
||||
func GetRelateMovieBasicInfo(search SearchInfo, page *Page) []MovieBasicInfo {
|
||||
/*
|
||||
根据当前影片信息匹配相关的影片
|
||||
1. 分类Cid,
|
||||
2. 如果影片名称含有第x季 则根据影片名进行模糊匹配
|
||||
3. class_tag 剧情内容匹配, 切分后使用 or 进行匹配
|
||||
4. area 地区
|
||||
5. 语言 Language
|
||||
*/
|
||||
|
||||
// sql 拼接查询条件
|
||||
sql := ""
|
||||
// 优先进行名称相似匹配
|
||||
re := regexp.MustCompile("第.{1,3}季")
|
||||
if re.MatchString(search.Name) {
|
||||
search.Name = re.ReplaceAllString(search.Name, "")
|
||||
sql = fmt.Sprintf(`select * from %s where name LIKE "%%%s%%" union`, search.TableName(), search.Name)
|
||||
}
|
||||
// 执行后续匹配内容
|
||||
//sql = fmt.Sprintf(`%s select * from %s where cid=%d AND area="%s" AND language="%s" AND`, sql, search.TableName(), search.Cid, search.Area, search.Language)
|
||||
|
||||
// 地区限制取消, 过滤掉的影片太多
|
||||
sql = fmt.Sprintf(`%s select * from %s where cid=%d AND language="%s" AND`, sql, search.TableName(), search.Cid, search.Language)
|
||||
if strings.Contains(search.ClassTag, ",") {
|
||||
s := "("
|
||||
for _, t := range strings.Split(search.ClassTag, ",") {
|
||||
s = fmt.Sprintf(`%s class_tag = "%s" OR`, s, t)
|
||||
}
|
||||
sql = fmt.Sprintf("%s %s)", sql, strings.TrimSuffix(s, "OR"))
|
||||
} else {
|
||||
sql = fmt.Sprintf(`%s class_tag = "%s"`, sql, search.ClassTag)
|
||||
}
|
||||
// 条件拼接完成后加上limit参数
|
||||
sql = fmt.Sprintf("(%s) limit %d,%d", sql, page.Current, page.PageSize)
|
||||
// 执行sql
|
||||
list := []SearchInfo{}
|
||||
db.Mdb.Raw(sql).Scan(&list)
|
||||
// 根据list 获取对应的BasicInfo
|
||||
var basicList []MovieBasicInfo
|
||||
for _, s := range list {
|
||||
// 通过key获取对应的影片基本数据
|
||||
basicList = append(basicList, GetBasicInfoByKey(fmt.Sprintf(config.MovieBasicInfoKey, s.Cid, s.Mid)))
|
||||
}
|
||||
|
||||
return basicList
|
||||
}
|
||||
38
server/plugin/common/ProcessCategory.go
Normal file
38
server/plugin/common/ProcessCategory.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"server/model"
|
||||
)
|
||||
|
||||
// =================Spider数据处理=======================
|
||||
|
||||
// CategoryTree 组装树形菜单
|
||||
func CategoryTree(list []model.ClassInfo) *model.CategoryTree {
|
||||
// 遍历所有分类进行树形结构组装
|
||||
tree := &model.CategoryTree{Category: &model.Category{Id: 0, Pid: -1, Name: "分类信息"}}
|
||||
temp := make(map[int64]*model.CategoryTree)
|
||||
temp[tree.Id] = tree
|
||||
|
||||
for _, c := range list {
|
||||
// 判断当前节点ID是否存在于 temp中
|
||||
category, ok := temp[c.Id]
|
||||
if ok {
|
||||
// 将当前节点信息保存
|
||||
category.Category = &model.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}}
|
||||
temp[c.Id] = category
|
||||
}
|
||||
// 根据 pid获取父节点信息
|
||||
parent, ok := temp[category.Pid]
|
||||
if !ok {
|
||||
// 如果不存在父节点存在, 则将父节点存放到temp中
|
||||
temp[c.Pid] = parent
|
||||
}
|
||||
// 将当前节点存放到父节点的Children中
|
||||
parent.Children = append(parent.Children, category)
|
||||
}
|
||||
|
||||
return tree
|
||||
}
|
||||
99
server/plugin/common/ProcessMovies.go
Normal file
99
server/plugin/common/ProcessMovies.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"server/model"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ProcessMovieListInfo 处理影片列表中的信息, 后续增加片源可提通过type属性进行对应转换
|
||||
func ProcessMovieListInfo(list []model.MovieInfo) []model.Movie {
|
||||
var movies []model.Movie
|
||||
for _, info := range list {
|
||||
movies = append(movies, model.Movie{
|
||||
Id: info.Id,
|
||||
Name: info.Name,
|
||||
Cid: info.Cid,
|
||||
CName: info.CName,
|
||||
EnName: info.EnName,
|
||||
Time: info.Time,
|
||||
Remarks: info.Remarks,
|
||||
PlayFrom: info.PlayFrom,
|
||||
})
|
||||
}
|
||||
return movies
|
||||
}
|
||||
|
||||
// ProcessMovieDetailList 处理影片详情列表数据
|
||||
func ProcessMovieDetailList(list []model.MovieDetailInfo) []model.MovieDetail {
|
||||
var detailList []model.MovieDetail
|
||||
for _, d := range list {
|
||||
detailList = append(detailList, ProcessMovieDetail(d))
|
||||
}
|
||||
return detailList
|
||||
}
|
||||
|
||||
// ProcessMovieDetail 处理单个影片详情信息
|
||||
func ProcessMovieDetail(detail model.MovieDetailInfo) model.MovieDetail {
|
||||
md := model.MovieDetail{
|
||||
Id: detail.Id,
|
||||
Cid: detail.Cid,
|
||||
Pid: detail.Pid,
|
||||
Name: detail.Name,
|
||||
Picture: detail.Pic,
|
||||
DownFrom: detail.DownFrom,
|
||||
MovieDescriptor: model.MovieDescriptor{
|
||||
SubTitle: detail.SubTitle,
|
||||
CName: detail.CName,
|
||||
EnName: detail.EnName,
|
||||
Initial: detail.Initial,
|
||||
ClassTag: detail.ClassTag,
|
||||
Actor: detail.Actor,
|
||||
Director: detail.Director,
|
||||
Writer: detail.Writer,
|
||||
Blurb: detail.Blurb,
|
||||
Remarks: detail.Remarks,
|
||||
ReleaseDate: detail.PubDate,
|
||||
Area: detail.Area,
|
||||
Language: detail.Language,
|
||||
Year: detail.Year,
|
||||
State: detail.State,
|
||||
UpdateTime: detail.UpdateTime,
|
||||
AddTime: detail.AddTime,
|
||||
DbId: detail.DbId,
|
||||
DbScore: detail.DbScore,
|
||||
Content: detail.Content,
|
||||
},
|
||||
}
|
||||
// 通过分割符切分播放源信息 PlaySeparator $$$
|
||||
md.PlayFrom = strings.Split(detail.PlayFrom, detail.PlaySeparator)
|
||||
md.PlayList = ProcessPlayInfo(detail.PlayUrl, detail.PlaySeparator)
|
||||
md.DownloadList = ProcessPlayInfo(detail.DownUrl, detail.PlaySeparator)
|
||||
return md
|
||||
}
|
||||
|
||||
// ProcessPlayInfo 处理影片播放数据信息
|
||||
func ProcessPlayInfo(info, sparator string) [][]model.MovieUrlInfo {
|
||||
var res [][]model.MovieUrlInfo
|
||||
// 1. 通过分隔符区分多个片源数据
|
||||
for _, l := range strings.Split(info, sparator) {
|
||||
// 2.对每个片源的集数和播放地址进行分割
|
||||
var item []model.MovieUrlInfo
|
||||
for _, p := range strings.Split(l, "#") {
|
||||
// 3. 处理 Episode$Link 形式的播放信息
|
||||
if strings.Contains(p, "$") {
|
||||
item = append(item, model.MovieUrlInfo{
|
||||
Episode: strings.Split(p, "$")[0],
|
||||
Link: strings.Split(p, "$")[1],
|
||||
})
|
||||
} else {
|
||||
item = append(item, model.MovieUrlInfo{
|
||||
Episode: "O(∩_∩)O",
|
||||
Link: p,
|
||||
})
|
||||
}
|
||||
}
|
||||
// 3. 将每组播放源对应的播放列表信息存储到列表中
|
||||
res = append(res, item)
|
||||
}
|
||||
return res
|
||||
}
|
||||
30
server/plugin/db/mysql.go
Normal file
30
server/plugin/db/mysql.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/schema"
|
||||
"server/config"
|
||||
)
|
||||
|
||||
var Mdb *gorm.DB
|
||||
|
||||
func InitMysql() (err error) {
|
||||
// client 相关属性设置
|
||||
Mdb, err = gorm.Open(mysql.New(mysql.Config{
|
||||
DSN: config.MysqlDsn,
|
||||
DefaultStringSize: 255, //string类型字段默认长度
|
||||
DisableDatetimePrecision: true, // 禁用 datetime 精度
|
||||
DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式
|
||||
DontSupportRenameColumn: true, // 用change 重命名列
|
||||
SkipInitializeWithVersion: false, // 根据当前Mysql版本自动配置
|
||||
}), &gorm.Config{
|
||||
NamingStrategy: schema.NamingStrategy{
|
||||
//TablePrefix: "t_", //设置创建表时的前缀
|
||||
SingularTable: true, //是否使用 结构体名称作为表名 (关闭自动变复数)
|
||||
//NameReplacer: strings.NewReplacer("spider_", ""), // 替表名和字段中的 Me 为 空
|
||||
},
|
||||
//Logger: logger.Default.LogMode(logger.Info), //设置日志级别为Info
|
||||
})
|
||||
return
|
||||
}
|
||||
37
server/plugin/db/redis.go
Normal file
37
server/plugin/db/redis.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"server/config"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
redis 工具类
|
||||
*/
|
||||
var Rdb *redis.Client
|
||||
var Cxt = context.Background()
|
||||
|
||||
// InitRedisConn 初始化redis客户端
|
||||
func InitRedisConn() error {
|
||||
|
||||
Rdb = redis.NewClient(&redis.Options{
|
||||
Addr: config.RedisAddr,
|
||||
Password: config.RedisPassword,
|
||||
DB: config.RedisDBNo,
|
||||
PoolSize: 10, // 默认连接数
|
||||
DialTimeout: time.Second * 10, // 超时时间
|
||||
})
|
||||
// 测试连接是否正常
|
||||
_, err := Rdb.Ping(Cxt).Result()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 关闭redis连接
|
||||
func CloseRedis() error {
|
||||
return Rdb.Close()
|
||||
}
|
||||
350
server/plugin/spider/Spider.go
Normal file
350
server/plugin/spider/Spider.go
Normal file
@@ -0,0 +1,350 @@
|
||||
package spider
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"server/config"
|
||||
"server/model"
|
||||
"server/plugin/common"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
公共资源采集站点
|
||||
1. 视频列表请求参数
|
||||
ac=list 列表数据, t 影视类型ID, pg 页码, wd 关键字, h 几小时内数据
|
||||
2. 视频详情请求参数
|
||||
ac=detail 详情数据, ids 影片id列表, h, pg, t 影视类型ID
|
||||
*/
|
||||
const (
|
||||
LZ_MOVIES_URL = "https://cj.lziapi.com/api.php/provide/vod/"
|
||||
LZ_MOVIES_Bk_URL = "https://cj.lzcaiji.com/api.php/provide/vod/"
|
||||
TK_MOVIES_URL = "https://api.tiankongapi.com/api.php/provide/vod"
|
||||
KC_MOVIES_URL = "https://caiji.kczyapi.com/api.php/provide/vod/"
|
||||
FS_MOVIES_URL = "https://www.feisuzyapi.com/api.php/provide/vod/"
|
||||
|
||||
// FILM_COLLECT_SITE 当前使用的采集URL
|
||||
FILM_COLLECT_SITE = "https://www.feisuzyapi.com/api.php/provide/vod/"
|
||||
)
|
||||
|
||||
// 定义一个同步等待组
|
||||
var wg = &sync.WaitGroup{}
|
||||
|
||||
func StartSpider() {
|
||||
// 1. 先拉取全部分类信息
|
||||
CategoryList()
|
||||
|
||||
//2. 拉取所有分类下的影片基本信息
|
||||
tree := model.GetCategoryTree()
|
||||
AllMovies(&tree)
|
||||
wg.Wait()
|
||||
log.Println("AllMovies 影片列表获取完毕")
|
||||
|
||||
// 3. 获取入库的所有影片详情信息
|
||||
// 3.2 获取入库的所有影片的详情信息
|
||||
AllMovieInfo()
|
||||
log.Println("AllMovieInfo 所有影片详情获取完毕")
|
||||
|
||||
// 4. mysql批量插入与数据爬取同时进行容易出现主键冲突, 因此滞后
|
||||
// 4.1 先一步将输入存入redis中, 待网络io结束后再进行分批扫描入库
|
||||
// 3.1 先查找并创建search数据库
|
||||
time.Sleep(time.Second * 10)
|
||||
model.CreateSearchTable()
|
||||
SearchInfoToMdb()
|
||||
log.Println("SearchInfoToMdb 影片检索信息保存完毕")
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
|
||||
// CategoryList 获取分类数据
|
||||
func CategoryList() {
|
||||
// 设置请求参数信息
|
||||
r := RequestInfo{Uri: FILM_COLLECT_SITE, Params: url.Values{}}
|
||||
r.Params.Set(`ac`, "list")
|
||||
r.Params.Set(`pg`, "1")
|
||||
r.Params.Set(`t`, "1")
|
||||
// 执行请求, 获取一次list数据
|
||||
ApiGet(&r)
|
||||
// 解析resp数据
|
||||
movieListInfo := model.MovieListInfo{}
|
||||
if len(r.Resp) <= 0 {
|
||||
log.Println("MovieListInfo数据获取异常 : Resp Is Empty")
|
||||
}
|
||||
_ = json.Unmarshal(r.Resp, &movieListInfo)
|
||||
// 获取分类列表信息
|
||||
classList := movieListInfo.Class
|
||||
// 组装分类数据信息树形结构
|
||||
categoryTree := common.CategoryTree(classList)
|
||||
// 序列化tree
|
||||
data, _ := json.Marshal(categoryTree)
|
||||
// 保存 tree 到redis
|
||||
err := model.SaveCategoryTree(string(data))
|
||||
if err != nil {
|
||||
log.Println("SaveCategoryTree Error: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
// AllMovies 遍历所有分类, 获取所有二级分类数据
|
||||
func AllMovies(tree *model.CategoryTree) {
|
||||
// 遍历一级分类
|
||||
for _, c := range tree.Children {
|
||||
// 遍历二级分类, 屏蔽主页不需要的影片信息, 只获取 电影1 电视剧2 综艺3 动漫4等分类下的信息
|
||||
//len(c.Children) > 0 && c.Id <= 4
|
||||
if len(c.Children) > 0 {
|
||||
for _, cInfo := range c.Children {
|
||||
//go CategoryAllMovie(cInfo.Category)
|
||||
CategoryAllMoviePlus(cInfo.Category)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CategoryAllMovie 获取指定分类的所有影片基本信息
|
||||
func CategoryAllMovie(c *model.Category) {
|
||||
// 添加一个等待任务, 执行完减去一个任务
|
||||
wg.Add(1)
|
||||
defer wg.Done()
|
||||
// 设置请求参数
|
||||
r := &RequestInfo{Uri: FILM_COLLECT_SITE, Params: url.Values{}}
|
||||
r.Params.Set(`ac`, "list")
|
||||
r.Params.Set(`t`, fmt.Sprint(c.Id))
|
||||
ApiGet(r)
|
||||
// 解析请求数据
|
||||
listInfo := model.MovieListInfo{}
|
||||
_ = json.Unmarshal(r.Resp, &listInfo)
|
||||
// 获取pageCount信息, 循环获取所有页数据
|
||||
pageCount := listInfo.PageCount
|
||||
// 开始获取所有信息, 使用协程并发获取数据
|
||||
for i := 1; i <= int(pageCount); i++ {
|
||||
// 使用新的 请求参数
|
||||
r.Params.Set(`pg`, fmt.Sprint(i))
|
||||
// 保存当前分类下的影片信息
|
||||
info := model.MovieListInfo{}
|
||||
ApiGet(r)
|
||||
// 如果返回数据中的list为空,则直接结束本分类的资源获取
|
||||
if len(r.Resp) <= 0 {
|
||||
log.Println("SaveMoves Error Response Is Empty")
|
||||
break
|
||||
}
|
||||
_ = json.Unmarshal(r.Resp, &info)
|
||||
if info.List == nil {
|
||||
log.Println("MovieList Is Empty")
|
||||
break
|
||||
}
|
||||
// 处理影片信息
|
||||
list := common.ProcessMovieListInfo(info.List)
|
||||
// 保存影片信息至redis
|
||||
_ = model.SaveMoves(list)
|
||||
}
|
||||
}
|
||||
|
||||
// CategoryAllMoviePlus 部分分类页数很多,因此采用单分类多协程拉取
|
||||
func CategoryAllMoviePlus(c *model.Category) {
|
||||
// 设置请求参数
|
||||
r := &RequestInfo{Uri: FILM_COLLECT_SITE, Params: url.Values{}}
|
||||
r.Params.Set(`ac`, "list")
|
||||
r.Params.Set(`t`, fmt.Sprint(c.Id))
|
||||
ApiGet(r)
|
||||
// 解析请求数据
|
||||
listInfo := model.MovieListInfo{}
|
||||
_ = json.Unmarshal(r.Resp, &listInfo)
|
||||
// 获取pageCount信息, 循环获取所有页数据
|
||||
pageCount := listInfo.PageCount
|
||||
// 使用chan + goroutine 进行并发获取
|
||||
chPg := make(chan int, int(pageCount))
|
||||
chClose := make(chan int)
|
||||
// 开始获取所有信息, 使用协程并发获取数据
|
||||
for i := 1; i <= int(pageCount); i++ {
|
||||
// 将当前分类的所有页码存入chPg
|
||||
chPg <- i
|
||||
}
|
||||
close(chPg)
|
||||
// 开启MAXGoroutine数量的协程进行请求
|
||||
for i := 0; i < config.MAXGoroutine; i++ {
|
||||
go func() {
|
||||
// 当前协程结束后向 chClose中写入一次数据
|
||||
defer func() { chClose <- 0 }()
|
||||
for {
|
||||
pg, ok := <-chPg
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// 使用新的 请求参数
|
||||
req := RequestInfo{Uri: FILM_COLLECT_SITE, Params: url.Values{}}
|
||||
req.Params.Set(`ac`, "list")
|
||||
req.Params.Set(`t`, fmt.Sprint(c.Id))
|
||||
req.Params.Set(`pg`, fmt.Sprint(pg))
|
||||
// 保存当前分类下的影片信息
|
||||
info := model.MovieListInfo{}
|
||||
ApiGet(&req)
|
||||
// 如果返回数据中的list为空,则直接结束本分类的资源获取
|
||||
if len(r.Resp) <= 0 {
|
||||
log.Println("SaveMoves Error Response Is Empty")
|
||||
return
|
||||
}
|
||||
_ = json.Unmarshal(r.Resp, &info)
|
||||
if info.List == nil {
|
||||
log.Println("MovieList Is Empty")
|
||||
return
|
||||
}
|
||||
// 处理影片信息
|
||||
list := common.ProcessMovieListInfo(info.List)
|
||||
// 保存影片信息至redis
|
||||
_ = model.SaveMoves(list)
|
||||
}
|
||||
}()
|
||||
}
|
||||
// 使用chClose等待当前分类列表数据请求完毕
|
||||
for i := 0; i < config.MAXGoroutine; i++ {
|
||||
<-chClose
|
||||
}
|
||||
}
|
||||
|
||||
// AllMovieInfo 拉取全部影片的基本信息
|
||||
func AllMovieInfo() {
|
||||
keys := model.AllMovieInfoKey()
|
||||
for _, key := range keys {
|
||||
// 获取当前分类下的sort set数据集合
|
||||
movies := model.GetMovieListByKey(key)
|
||||
ids := ""
|
||||
for i, m := range movies {
|
||||
// 反序列化获取影片基本信息
|
||||
movie := model.Movie{}
|
||||
err := json.Unmarshal([]byte(m), &movie)
|
||||
if err == nil && movie.Id != 0 {
|
||||
// 拼接ids信息
|
||||
ids = fmt.Sprintf("%s,%d", ids, movie.Id)
|
||||
}
|
||||
// 每20个id执行一次请求, limit 最多20
|
||||
if (i+1)%20 == 0 {
|
||||
// ids对应影片的详情信息
|
||||
go MoviesDetails(strings.Trim(ids, ","))
|
||||
ids = ""
|
||||
}
|
||||
}
|
||||
// 如果ids != "" , 将剩余id执行一次请求
|
||||
MoviesDetails(strings.Trim(ids, ","))
|
||||
}
|
||||
}
|
||||
|
||||
// MoviesDetails 获取影片详情信息, ids 影片id,id,....
|
||||
func MoviesDetails(ids string) {
|
||||
// // 添加一个等待任务, 执行完减去一个任务
|
||||
//wg.Add(1)
|
||||
//defer wg.Done()
|
||||
// 如果ids为空数据则直接返回
|
||||
if len(ids) <= 0 {
|
||||
return
|
||||
}
|
||||
// 设置请求参数
|
||||
r := RequestInfo{
|
||||
Uri: FILM_COLLECT_SITE,
|
||||
Params: url.Values{},
|
||||
}
|
||||
r.Params.Set("ac", "detail")
|
||||
r.Params.Set("ids", ids)
|
||||
ApiGet(&r)
|
||||
// 映射详情信息
|
||||
details := model.DetailListInfo{}
|
||||
// 如果返回数据为空则直接结束本次方法
|
||||
if len(r.Resp) <= 0 {
|
||||
return
|
||||
}
|
||||
// 序列化详情数据
|
||||
err := json.Unmarshal(r.Resp, &details)
|
||||
if err != nil {
|
||||
log.Println("DetailListInfo Unmarshal Error: ", err)
|
||||
return
|
||||
}
|
||||
// 处理details信息
|
||||
list := common.ProcessMovieDetailList(details.List)
|
||||
// 保存影片详情信息到redis
|
||||
err = model.SaveDetails(list)
|
||||
if err != nil {
|
||||
log.Println("SaveDetails Error: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// GetRecentMovie 获取最近更的影片, 默认最近3小时
|
||||
func GetRecentMovie() {
|
||||
// 请求URL URI?ac=list&h=6
|
||||
r := RequestInfo{Uri: FILM_COLLECT_SITE, Params: url.Values{}}
|
||||
r.Params.Set("ac", "list")
|
||||
r.Params.Set("pg", "1")
|
||||
r.Params.Set("h", config.UpdateInterval)
|
||||
// 执行请求获取分页信息
|
||||
ApiGet(&r)
|
||||
if len(r.Resp) < 0 {
|
||||
log.Println("更新数据获取失败")
|
||||
return
|
||||
}
|
||||
pageInfo := model.MovieListInfo{}
|
||||
_ = json.Unmarshal(r.Resp, &pageInfo)
|
||||
// 获取分页数据
|
||||
ids := ""
|
||||
// 存储检索信息
|
||||
var tempSearchList []model.SearchInfo
|
||||
// 获取影片详细数据,并保存到redis中
|
||||
for i := 1; i <= int(pageInfo.PageCount); i++ {
|
||||
// 执行获取影片基本信息
|
||||
r.Params.Set("pg", fmt.Sprint(i))
|
||||
ApiGet(&r)
|
||||
// 解析请求的结果
|
||||
if len(r.Resp) < 0 {
|
||||
log.Println("更新数据获取失败")
|
||||
return
|
||||
}
|
||||
info := model.MovieListInfo{}
|
||||
_ = json.Unmarshal(r.Resp, &info)
|
||||
// 将影片信息保存到 movieList
|
||||
list := common.ProcessMovieListInfo(info.List)
|
||||
_ = model.SaveMoves(list)
|
||||
// 拼接ids 用于请求detail信息
|
||||
for _, m := range list {
|
||||
ids = fmt.Sprintf("%s,%d", ids, m.Id)
|
||||
// 保存一份id切片用于添加mysql检索信息
|
||||
tempSearchList = append(tempSearchList, model.SearchInfo{Mid: m.Id, Cid: m.Cid})
|
||||
}
|
||||
// 执行获取详情请求
|
||||
MoviesDetails(strings.Trim(ids, ","))
|
||||
ids = ""
|
||||
}
|
||||
// 根据idList 补全对应影片的searInfo信息
|
||||
var sl []model.SearchInfo
|
||||
for _, s := range tempSearchList {
|
||||
// 通过id 获取对应的详情信息
|
||||
sl = append(sl, model.ConvertSearchInfo(model.GetDetailByKey(fmt.Sprintf(config.MovieDetailKey, s.Cid, s.Mid))))
|
||||
}
|
||||
// 调用批量保存或更新方法, 如果对应mid数据存在则更新, 否则执行插入
|
||||
model.BatchSaveOrUpdate(sl)
|
||||
}
|
||||
|
||||
// StartSpiderRe 清空存储数据,从零开始获取
|
||||
func StartSpiderRe() {
|
||||
// 删除已有的存储数据, redis 和 mysql中的存储数据全部清空
|
||||
model.RemoveAll()
|
||||
// 执行完整数据获取
|
||||
StartSpider()
|
||||
}
|
||||
29
server/plugin/spider/SpiderCron.go
Normal file
29
server/plugin/spider/SpiderCron.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package spider
|
||||
|
||||
import (
|
||||
"github.com/robfig/cron/v3"
|
||||
"log"
|
||||
"server/config"
|
||||
)
|
||||
|
||||
// RegularUpdateMovie 定时更新, 每半小时获取一次站点的最近x小时数据
|
||||
func RegularUpdateMovie() {
|
||||
c := cron.New(cron.WithSeconds())
|
||||
// 开启定时任务每x 分钟更新一次最近x小时的影片数据
|
||||
_, err := c.AddFunc(config.CornMovieUpdate, func() {
|
||||
// 执行更新最近x小时影片的Spider
|
||||
log.Println("执行一次影片更新任务...")
|
||||
GetRecentMovie()
|
||||
})
|
||||
|
||||
// 开启定时任务每月最后一天凌晨两点, 执行一次清库重取数据
|
||||
_, err = c.AddFunc(config.CornUpdateAll, func() {
|
||||
StartSpiderRe()
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Println("Corn Start Error: ", err)
|
||||
}
|
||||
|
||||
c.Start()
|
||||
}
|
||||
68
server/plugin/spider/SpiderRequest.go
Normal file
68
server/plugin/spider/SpiderRequest.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package spider
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gocolly/colly/v2"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
Client = CreateClient()
|
||||
)
|
||||
|
||||
// RequestInfo 请求参数结构体
|
||||
type RequestInfo struct {
|
||||
Uri string `json:"uri"` // 请求url地址
|
||||
Params url.Values `json:"params"` // 请求参数
|
||||
Header http.Header `json:"header"` // 请求头数据
|
||||
Resp []byte `json:"resp"` // 响应结果数据
|
||||
}
|
||||
|
||||
// CreateClient 初始化请求客户端
|
||||
func CreateClient() *colly.Collector {
|
||||
c := colly.NewCollector()
|
||||
// 设置代理信息
|
||||
//if proxy, err := proxy.RoundRobinProxySwitcher("127.0.0.1:7890"); err != nil {
|
||||
// c.SetProxyFunc(proxy)
|
||||
//}
|
||||
// 设置并发数量控制
|
||||
//c.Async = true
|
||||
// 访问深度
|
||||
c.MaxDepth = 1
|
||||
//可重复访问
|
||||
c.AllowURLRevisit = true
|
||||
// 设置超时时间 默认10s
|
||||
c.SetRequestTimeout(20 * time.Second)
|
||||
// 发起请求之前会调用的方法
|
||||
c.OnRequest(func(request *colly.Request) {
|
||||
// 设置一些请求头信息
|
||||
request.Headers.Set("Content-Type", "application/json;charset=UTF-8")
|
||||
request.Headers.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36")
|
||||
//request.Headers.Set("cookie", "ge_ua_key=sxo%2Bz4kkS7clWpEtg2m7HioRfIo%3D")
|
||||
request.Headers.Set("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
|
||||
})
|
||||
// 请求期间报错的回调
|
||||
c.OnError(func(response *colly.Response, err error) {
|
||||
log.Printf("请求异常: URL: %s Error: %s\n", response.Request.URL, err)
|
||||
})
|
||||
return c
|
||||
}
|
||||
|
||||
// ApiGet 请求数据的方法
|
||||
func ApiGet(r *RequestInfo) {
|
||||
// 请求成功后的响应
|
||||
Client.OnResponse(func(response *colly.Response) {
|
||||
// 将响应结构封装到 RequestInfo.Resp中
|
||||
r.Resp = response.Body
|
||||
// 拿到response后输出请求url
|
||||
//log.Println("\n请求成功: ", response.Request.URL)
|
||||
})
|
||||
// 处理请求参数
|
||||
err := Client.Visit(fmt.Sprintf("%s?%s", r.Uri, r.Params.Encode()))
|
||||
if err != nil {
|
||||
log.Println("获取数据失败: ", err)
|
||||
}
|
||||
}
|
||||
68
server/router/router.go
Normal file
68
server/router/router.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"log"
|
||||
"net/http"
|
||||
"server/controller"
|
||||
)
|
||||
|
||||
func SetupRouter() *gin.Engine {
|
||||
|
||||
r := gin.Default()
|
||||
// 开启跨域
|
||||
r.Use(Cors())
|
||||
|
||||
r.GET(`/index`, controller.Index)
|
||||
r.GET(`/navCategory`, controller.CategoriesInfo)
|
||||
r.GET(`/filmDetail`, controller.FilmDetail)
|
||||
r.GET(`/filmPlayInfo`, controller.FilmPlayInfo)
|
||||
r.GET(`/searchFilm`, controller.SearchFilm)
|
||||
r.GET(`/filmCategory`, controller.FilmCategory)
|
||||
|
||||
// 触发spider
|
||||
spiderRoute := r.Group(`/spider`)
|
||||
{
|
||||
// 清空全部数据并从零开始获取数据
|
||||
spiderRoute.GET("/SpiderRe", controller.SpiderRe)
|
||||
// 获取影片详情, 用于网路不稳定导致的影片数据缺失
|
||||
spiderRoute.GET(`/FixFilmDetail`, controller.FixFilmDetail)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user