This commit is contained in:
lizhuang
2023-04-04 17:56:44 +08:00
parent 6a3fa25b7d
commit 46025f6011
93 changed files with 8110 additions and 0 deletions

8
server/.idea/.gitignore generated vendored Normal file
View 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
View 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
View 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
View 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`

View 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
)

View 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,
})
}

View 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
View 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
View 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
View 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
View 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()
}
}

View 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
View 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
View 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
View 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
}

View 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
}

View 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
View 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
View 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()
}

View 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()
}

View 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()
}

View 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
View 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()
}
}