-
+ {e.keyCode==13 && searchMovie()}" placeholder="搜索 动漫,剧集,电影 " class="search"/>
@@ -116,14 +116,13 @@ const changeCurrent = (currentVal: number) => {
}
.film_item {
flex-basis: calc(100% - 20px);
- margin: 0 10px;
+ margin: 0 10px 25px 10px;
display: flex;
background: #2e2e2e;
padding: 10px;
min-height: 180px;
max-height: 200px;
border-radius: 16px;
- margin-bottom: 25px;
}
.film_item a {
flex: 2;
diff --git a/client/vite.config.ts b/client/vite.config.ts
index 54cad59..4e4a417 100644
--- a/client/vite.config.ts
+++ b/client/vite.config.ts
@@ -7,29 +7,29 @@ import {ElementPlusResolver} from "unplugin-vue-components/resolvers";
// https://vitejs.dev/config/
export default defineConfig({
// 本地测试环境
- server: {
- host: '0.0.0.0',
- port: 3600,
- proxy: {
- "/api": {
- target: `http://127.0.0.1:3601`,
- changeOrigin: true, // 允许跨域
- rewrite: path => path.replace(/^\/api/,'')
- }
- },
- },
- // nginx发布构建时使用此配置
// server: {
- // host: 'localhost',
+ // host: '0.0.0.0',
// port: 3600,
// proxy: {
// "/api": {
- // target: `http://localhost`,
+ // target: `http://127.0.0.1:3601`,
// changeOrigin: true, // 允许跨域
// rewrite: path => path.replace(/^\/api/,'')
// }
// },
// },
+ // nginx发布构建时使用此配置
+ server: {
+ host: 'localhost',
+ port: 3600,
+ proxy: {
+ "/api": {
+ target: `http://localhost`,
+ changeOrigin: true, // 允许跨域
+ rewrite: path => path.replace(/^\/api/,'')
+ }
+ },
+ },
plugins: [
vue(),
diff --git a/server/config/DataConfig.go b/server/config/DataConfig.go
index 0717641..c96b6f4 100644
--- a/server/config/DataConfig.go
+++ b/server/config/DataConfig.go
@@ -7,18 +7,23 @@ import "time"
*/
const (
+ // MAXGoroutine max goroutine, 执行spider中对协程的数量限制
+ MAXGoroutine = 10
+
// 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"
+ // MultipleSiteDetail 多站点影片信息存储key
+ MultipleSiteDetail = "MultipleSource:%s"
+
// SearchCount Search scan 识别范围
SearchCount = 3000
// SearchKeys Search Key Hash
@@ -42,16 +47,16 @@ const (
)
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"
+
+ // SearchTableName 存放检索信息的数据表名
+ SearchTableName = "search"
+
+ //mysql服务配置信息 root:root 设置mysql账户的用户名和密码
+
+ //MysqlDsn = "root:root@(192.168.20.10:3307)/FilmSite?charset=utf8mb4&parseTime=True&loc=Local"
+
+ // MysqlDsn docker compose 环境下的链接信息 mysql:3306 为 docker compose 中 mysql服务对应的网络名称和端口
+ MysqlDsn = "root:root@(mysql:3306)/FilmSite?charset=utf8mb4&parseTime=True&loc=Local"
/*
redis 配置信息
@@ -59,12 +64,12 @@ const (
RedisPassword redis访问密码
RedisDBNo 使用第几号库
*/
- RedisAddr = `192.168.20.10:6379`
+ //RedisAddr = `192.168.20.10:6379`
+ //RedisPassword = `root`
+ //RedisDBNo = 1
+
+ // RedisAddr docker compose 环境下运行使用如下配置信息
+ RedisAddr = `redis:6379`
RedisPassword = `root`
RedisDBNo = 0
-
- // docker compose 环境下运行如下配置信息
- //RedisAddr = `redis:6379`
- //RedisPassword = `Password`
- //RedisDBNo = 0
)
diff --git a/server/controller/SpiderController.go b/server/controller/SpiderController.go
index a232c52..aa59795 100644
--- a/server/controller/SpiderController.go
+++ b/server/controller/SpiderController.go
@@ -36,7 +36,7 @@ func FixFilmDetail(c *gin.Context) {
return
}
// 如果指令正确,则执行详情数据获取
- spider.AllMovieInfo()
+ spider.MainSiteSpider()
log.Println("FilmDetail 重制完成!!!")
// 先截断表中的数据
model.TunCateSearchTable()
diff --git a/server/logic/IndexLogic.go b/server/logic/IndexLogic.go
index ea04db5..9027a37 100644
--- a/server/logic/IndexLogic.go
+++ b/server/logic/IndexLogic.go
@@ -3,9 +3,12 @@ package logic
import (
"fmt"
"github.com/gin-gonic/gin"
+ "regexp"
"server/config"
"server/model"
"server/plugin/db"
+ "server/plugin/spider"
+ "strings"
)
/*
@@ -54,6 +57,8 @@ func (i *IndexLogic) GetFilmDetail(id int) model.MovieDetail {
db.Mdb.Where("mid", id).First(&search)
// 获取redis中的完整影视信息 MovieDetail:Cid11:Id24676
movieDetail := model.GetDetailByKey(fmt.Sprintf(config.MovieDetailKey, search.Cid, search.Mid))
+ //查找其他站点是否存在影片对应的播放源
+ multipleSource(&movieDetail)
return movieDetail
}
@@ -135,3 +140,36 @@ func (i *IndexLogic) RelateMovie(detail model.MovieDetail, page *model.Page) []m
}
return model.GetRelateMovieBasicInfo(search, page)
}
+
+// 将多个站点的对应影视播放源追加到主站点播放列表中
+func multipleSource(detail *model.MovieDetail) {
+ // 整合多播放源, 处理部分站点中影片名称的空格
+ names := map[string]int{model.HashKey(detail.Name): 0}
+ // 不同站点影片别名匹配
+ re := regexp.MustCompile(`第一季$`)
+ alias := strings.TrimSpace(re.ReplaceAllString(detail.Name, ""))
+ names[model.HashKey(alias)] = 0
+ // 将多个影片别名进行切分,放入names中
+ if len(detail.SubTitle) > 0 && strings.Contains(detail.SubTitle, ",") {
+ for _, v := range strings.Split(detail.SubTitle, ",") {
+ names[model.HashKey(v)] = 0
+ }
+ }
+ if len(detail.SubTitle) > 0 && strings.Contains(detail.SubTitle, "/") {
+ for _, v := range strings.Split(detail.SubTitle, "/") {
+ names[model.HashKey(v)] = 0
+ }
+ }
+ // 遍历站点列表
+ for _, s := range spider.SiteList {
+ for k, _ := range names {
+ pl := model.GetMultiplePlay(s.Name, k)
+ if len(pl) > 0 {
+ // 如果当前站点已经匹配到数据则直接退出当前循环
+ detail.PlayList = append(detail.PlayList, pl)
+ break
+ }
+ }
+ }
+
+}
diff --git a/server/main.go b/server/main.go
index d233cb5..c327652 100644
--- a/server/main.go
+++ b/server/main.go
@@ -9,8 +9,8 @@ import (
)
func init() {
- // 执行初始化前等待30s , 让mysql服务完成初始化指令
- time.Sleep(time.Second * 30)
+ // 执行初始化前等待20s , 让mysql服务完成初始化指令
+ time.Sleep(time.Second * 20)
//初始化redis客户端
err := db.InitRedisConn()
if err != nil {
diff --git a/server/model/Movies.go b/server/model/Movies.go
index fea9e00..087ce32 100644
--- a/server/model/Movies.go
+++ b/server/model/Movies.go
@@ -4,7 +4,7 @@ import (
"encoding/json"
"fmt"
"github.com/redis/go-redis/v9"
- "log"
+ "hash/fnv"
"server/config"
"server/plugin/db"
"strconv"
@@ -116,19 +116,19 @@ func SaveDetails(list []MovieDetail) (err error) {
// 序列化影片详情信息
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序列化的结果, //只保留十天, 没周更新一次
+ // 改为普通 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)
- }
+ // 3. 保存Search检索信息到redis, 暂时搁置
+ //if err == nil {
+ // // 转换 detail信息
+ // searchInfo := ConvertSearchInfo(detail)
+ // // 放弃redis进行检索, 多条件处理不方便
+ // //err = AddSearchInfo(searchInfo)
+ // // 只存储用于检索对应影片的关键字信息
+ // SearchKeyword(searchInfo)
+ //}
}
// 保存一份search信息到mysql, 批量存储
@@ -158,6 +158,24 @@ func SaveMovieBasicInfo(detail MovieDetail) {
_ = db.Rdb.Set(db.Cxt, fmt.Sprintf(config.MovieBasicInfoKey, detail.Cid, detail.Id), data, config.CategoryTreeExpired).Err()
}
+// SaveSitePlayList 仅保存播放url列表信息到当前站点
+func SaveSitePlayList(siteName string, list []MovieDetail) (err error) {
+ // 如果list 为空则直接返回
+ if len(list) <= 0 {
+ return nil
+ }
+ res := make(map[string]string)
+ for _, d := range list {
+ if len(d.PlayList) > 0 {
+ data, _ := json.Marshal(d.PlayList[0])
+ res[HashKey(d.Name)] = string(data)
+ }
+ }
+ // 保存形式 key: MultipleSource:siteName Hash[hash(movieName)]list
+ err = db.Rdb.HMSet(db.Cxt, fmt.Sprintf(config.MultipleSiteDetail, siteName), res).Err()
+ return
+}
+
// AddSearchInfo 将影片关键字信息整合后存入search 集合中
func AddSearchInfo(searchInfo SearchInfo) (err error) {
// 片名 Name 分类 CName 类别标签 classTag 地区 Area 语言 Language 年份 Year 首字母 Initial, 排序
@@ -284,12 +302,12 @@ func ConvertSearchInfo(detail MovieDetail) SearchInfo {
if err != nil {
year = 0
}
-
return SearchInfo{
Mid: detail.Id,
Cid: detail.Cid,
Pid: detail.Pid,
Name: detail.Name,
+ SubTitle: detail.SubTitle,
CName: detail.CName,
ClassTag: detail.ClassTag,
Area: detail.Area,
@@ -324,9 +342,12 @@ func GetDetailByKey(key string) MovieDetail {
return detail
}
-// SearchMovie 搜索关键字影片
-func SearchMovie() {
- data, err := db.Rdb.ZScan(db.Cxt, "MovieList:cid30", 0, `*天使*`, config.SearchCount).Val()
- log.Println(err)
- fmt.Println(data)
+// HashKey 将字符串转化为hash值
+func HashKey(str string) string {
+ h := fnv.New32a()
+ _, err := h.Write([]byte(str))
+ if err != nil {
+ return ""
+ }
+ return fmt.Sprint(h.Sum32())
}
diff --git a/server/model/LZJson.go b/server/model/ResponseJson.go
similarity index 97%
rename from server/model/LZJson.go
rename to server/model/ResponseJson.go
index e0b1a18..d1e30f2 100644
--- a/server/model/LZJson.go
+++ b/server/model/ResponseJson.go
@@ -27,7 +27,7 @@ type MovieInfo struct {
type MovieListInfo struct {
Code int64 `json:"code"`
Msg string `json:"msg"`
- Page string `json:"page"`
+ Page any `json:"page"`
PageCount int64 `json:"pagecount"`
Limit string `json:"limit"`
Total int64 `json:"total"`
@@ -73,7 +73,7 @@ type MovieDetailInfo struct {
type DetailListInfo struct {
Code int64 `json:"code"`
Msg string `json:"msg"`
- Page int64 `json:"page"`
+ Page any `json:"page"`
PageCount int64 `json:"pagecount"`
Limit string `json:"limit"`
Total int64 `json:"total"`
diff --git a/server/model/Search.go b/server/model/Search.go
index bd03aa2..cecdcce 100644
--- a/server/model/Search.go
+++ b/server/model/Search.go
@@ -20,6 +20,7 @@ type SearchInfo struct {
Cid int64 `json:"cid"` //分类ID
Pid int64 `json:"pid"` //上级分类ID
Name string `json:"name"` // 片名
+ SubTitle string `json:"subTitle"` // 影片子标题
CName string `json:"CName"` // 分类名称
ClassTag string `json:"classTag"` //类型标签
Area string `json:"area"` // 地区
@@ -44,8 +45,7 @@ type Page struct {
}
func (s *SearchInfo) TableName() string {
- return "search_lz"
- //return "search_fs"
+ return config.SearchTableName
}
// ================================= Spider 数据处理(redis) =================================
@@ -289,3 +289,11 @@ func GetRelateMovieBasicInfo(search SearchInfo, page *Page) []MovieBasicInfo {
return basicList
}
+
+// GetMultiplePlay 通过影片名hash值匹配播放源
+func GetMultiplePlay(siteName, key string) []MovieUrlInfo {
+ data := db.Rdb.HGet(db.Cxt, fmt.Sprintf(config.MultipleSiteDetail, siteName), key).Val()
+ var playList []MovieUrlInfo
+ _ = json.Unmarshal([]byte(data), &playList)
+ return playList
+}
diff --git a/server/plugin/common/ProcessMovies.go b/server/plugin/common/ProcessMovies.go
index 5b8f3b7..6234134 100644
--- a/server/plugin/common/ProcessMovies.go
+++ b/server/plugin/common/ProcessMovies.go
@@ -5,7 +5,7 @@ import (
"strings"
)
-// ProcessMovieListInfo 处理影片列表中的信息, 后续增加片源可提通过type属性进行对应转换
+// ProcessMovieListInfo 处理影片列表中的信息
func ProcessMovieListInfo(list []model.MovieInfo) []model.Movie {
var movies []model.Movie
for _, info := range list {
@@ -66,8 +66,9 @@ func ProcessMovieDetail(detail model.MovieDetailInfo) model.MovieDetail {
}
// 通过分割符切分播放源信息 PlaySeparator $$$
md.PlayFrom = strings.Split(detail.PlayFrom, detail.PlaySeparator)
- md.PlayList = ProcessPlayInfo(detail.PlayUrl, detail.PlaySeparator)
- md.DownloadList = ProcessPlayInfo(detail.DownUrl, detail.PlaySeparator)
+ // v2 只保留m3u8播放源
+ md.PlayList = ProcessPlayInfoV2(detail.PlayUrl, detail.PlaySeparator)
+ md.DownloadList = ProcessPlayInfoV2(detail.DownUrl, detail.PlaySeparator)
return md
}
@@ -97,3 +98,57 @@ func ProcessPlayInfo(info, sparator string) [][]model.MovieUrlInfo {
}
return res
}
+
+// ProcessPlayInfoV2 处理影片信息方案二 只保留m3u8播放源
+func ProcessPlayInfoV2(info, sparator string) [][]model.MovieUrlInfo {
+ var res [][]model.MovieUrlInfo
+ if sparator != "" {
+ // 1. 通过分隔符切分播放源地址
+ for _, l := range strings.Split(info, sparator) {
+ // 只对m3u8播放源 和 .mp4下载地址进行处理
+ if strings.Contains(l, ".m3u8") || strings.Contains(l, ".mp4") {
+ // 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)
+ }
+ }
+ } else {
+ // 只对m3u8播放源 和 .mp4下载地址进行处理
+ if strings.Contains(info, ".m3u8") || strings.Contains(info, ".mp4") {
+ // 2.对每个片源的集数和播放地址进行分割
+ var item []model.MovieUrlInfo
+ for _, p := range strings.Split(info, "#") {
+ // 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
+}
diff --git a/server/plugin/spider/Spider.go b/server/plugin/spider/Spider.go
index d206fdf..9986544 100644
--- a/server/plugin/spider/Spider.go
+++ b/server/plugin/spider/Spider.go
@@ -2,67 +2,73 @@ package spider
import (
"encoding/json"
+ "errors"
"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
+ 舍弃第一版的数据处理思路, v2版本
+ 直接分页获取采集站点的影片详情信息
+
+
+*/
+
+/*
+ 1. 选择一个采集主站点, mysql检索表中只存储主站点检索的信息
+ 2. 采集多个站点数据
+ 2.1 主站点的采集数据完整地保存相关信息, basicInfo movieDetail search 等信息
+ 2.2 其余站点数据只存储 name(影片名称), playUrl(播放url), 存储形式 Key:value([]MovieUrlInfo)
+ 3. api数据格式不变, 获取影片详情时通过subTitle 去redis匹配其他站点的对应播放源并整合到主站详情信息的playUrl中
+ 4. 影片搜索时不再使用name进行匹配, 改为使用 subTitle 进行匹配
*/
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/"
+ MainSite = "https://www.feisuzyapi.com/api.php/provide/vod/"
)
-// 定义一个同步等待组
-var wg = &sync.WaitGroup{}
+type Site struct {
+ Name string
+ Uri string
+}
+// SiteList 播放源采集站
+var SiteList = []Site{
+ //{"tk", "https://api.tiankongapi.com/api.php/provide/vod"},
+ //{"yh", "https://m3u8.apiyhzy.com/api.php/provide/vod/"},
+ {"su", "https://subocaiji.com/api.php/provide/vod/at/json"},
+ {"lz", "https://cj.lziapi.com/api.php/provide/vod/"},
+ {"ff", "https://cj.ffzyapi.com/api.php/provide/vod/"},
+ //{"fs", "https://www.feisuzyapi.com/api.php/provide/vod/"},
+}
+
+// StartSpider 执行多源spider
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数据库
+ log.Println("CategoryList 影片分类信息保存完毕")
+ // 爬取主站点数据
+ MainSiteSpider()
+ log.Println("MainSiteSpider 主站点影片信息保存完毕")
+ // 查找并创建search数据库
time.Sleep(time.Second * 10)
model.CreateSearchTable()
SearchInfoToMdb()
log.Println("SearchInfoToMdb 影片检索信息保存完毕")
- time.Sleep(time.Second * 10)
+ // 获取其他站点数据
+ go MtSiteSpider()
+ log.Println("Spider End , 数据保存执行完成")
+ //time.Sleep(time.Second * 10)
}
// CategoryList 获取分类数据
func CategoryList() {
// 设置请求参数信息
- r := RequestInfo{Uri: FILM_COLLECT_SITE, Params: url.Values{}}
+ r := RequestInfo{Uri: MainSite, Params: url.Values{}}
r.Params.Set(`ac`, "list")
r.Params.Set(`pg`, "1")
r.Params.Set(`t`, "1")
@@ -87,184 +93,94 @@ func CategoryList() {
}
}
-// 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)
- }
- }
+// MainSiteSpider 主站点数据处理
+func MainSiteSpider() {
+ // 获取分页页数
+ pageCount, err := GetPageCount(RequestInfo{Uri: MainSite, Params: url.Values{}})
+ // 主站点分页出错直接终止程序
+ if err != nil {
+ panic(err)
}
-}
-
-// 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)
+ // 开启协程加快分页请求速度
+ ch := make(chan int, pageCount)
+ waitCh := make(chan int)
+ for i := 1; i <= pageCount; i++ {
+ ch <- i
}
-}
-
-// 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数量的协程进行请求
+ close(ch)
for i := 0; i < config.MAXGoroutine; i++ {
go func() {
- // 当前协程结束后向 chClose中写入一次数据
- defer func() { chClose <- 0 }()
+ defer func() { waitCh <- 0 }()
for {
- pg, ok := <-chPg
+ pg, ok := <-ch
if !ok {
- return
+ break
}
- // 使用新的 请求参数
- 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
+ list, e := GetMovieDetail(pg, RequestInfo{Uri: MainSite, Params: url.Values{}})
+ if e != nil {
+ log.Println("GetMovieDetail Error: ", err)
+ continue
}
- _ = json.Unmarshal(r.Resp, &info)
- if info.List == nil {
- log.Println("MovieList Is Empty")
- return
+ // 保存影片详情信息到redis
+ if err = model.SaveDetails(list); err != nil {
+ log.Println("SaveDetails Error: ", err)
}
- // 处理影片信息
- list := common.ProcessMovieListInfo(info.List)
- // 保存影片信息至redis
- _ = model.SaveMoves(list)
}
}()
}
- // 使用chClose等待当前分类列表数据请求完毕
for i := 0; i < config.MAXGoroutine; i++ {
- <-chClose
+ <-waitCh
}
}
-// 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, ","))
+// MtSiteSpider 附属数据源处理
+func MtSiteSpider() {
+ for _, s := range SiteList {
+ // 执行每个站点的播放url缓存
+ PlayDetailSpider(s)
+ log.Println(s.Name, "playUrl 爬取完毕!!!")
}
}
-// 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)
+// PlayDetailSpider SpiderSimpleInfo 获取单个站点的播放源
+func PlayDetailSpider(s Site) {
+ // 获取分页页数
+ pageCount, err := GetPageCount(RequestInfo{Uri: s.Uri, Params: url.Values{}})
+ // 出错直接终止当前站点数据获取
if err != nil {
- log.Println("DetailListInfo Unmarshal Error: ", err)
+ log.Println(err)
return
}
- // 处理details信息
- list := common.ProcessMovieDetailList(details.List)
- // 保存影片详情信息到redis
- err = model.SaveDetails(list)
- if err != nil {
- log.Println("SaveDetails Error: ", err)
+
+ // 开启协程加快分页请求速度
+ ch := make(chan int, pageCount)
+ waitCh := make(chan int)
+ for i := 1; i <= pageCount; i++ {
+ ch <- i
+ }
+ close(ch)
+ for i := 0; i < config.MAXGoroutine; i++ {
+ go func() {
+ defer func() { waitCh <- 0 }()
+ for {
+ pg, ok := <-ch
+ if !ok {
+ break
+ }
+ list, e := GetMovieDetail(pg, RequestInfo{Uri: s.Uri, Params: url.Values{}})
+ if e != nil || len(list) <= 0 {
+ log.Println("GetMovieDetail Error: ", err)
+ continue
+ }
+ // 保存影片播放信息到redis
+ if err = model.SaveSitePlayList(s.Name, list); err != nil {
+ log.Println("SaveDetails Error: ", err)
+ }
+ }
+ }()
+ }
+ for i := 0; i < config.MAXGoroutine; i++ {
+ <-waitCh
}
}
@@ -287,60 +203,73 @@ func SearchInfoToMdb() {
}
-// 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")
+// UpdateMovieDetail 定时更新主站点和其余播放源信息
+func UpdateMovieDetail() {
+ // 更新主站系列信息
+ UpdateMainDetail()
+ // 更新播放源数据信息
+ UpdatePlayDetail()
+}
+
+// UpdateMainDetail 更新主站点的最新影片
+func UpdateMainDetail() {
+ // 获取分页页数
+ r := RequestInfo{Uri: MainSite, Params: url.Values{}}
r.Params.Set("h", config.UpdateInterval)
- // 执行请求获取分页信息
- ApiGet(&r)
- if len(r.Resp) < 0 {
- log.Println("更新数据获取失败")
- return
+ pageCount, err := GetPageCount(r)
+ if err != nil {
+ log.Printf("Update MianStieDetail failed")
}
- pageInfo := model.MovieListInfo{}
- _ = json.Unmarshal(r.Resp, &pageInfo)
+ // 保存本次更新的所有详情信息
+ var ds []model.MovieDetail
// 获取分页数据
- 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
+ for i := 1; i <= pageCount; i++ {
+ list, err := GetMovieDetail(i, r)
+ if err != nil {
+ continue
}
- 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})
+ // 保存更新的影片信息, 同类型直接覆盖
+ if err = model.SaveDetails(list); err != nil {
+ log.Printf("Update MianStieDetail failed, SaveDetails Error ")
}
- // 执行获取详情请求
- MoviesDetails(strings.Trim(ids, ","))
- ids = ""
+ ds = append(ds, list...)
}
- // 根据idList 补全对应影片的searInfo信息
+
+ // 整合详情信息切片
var sl []model.SearchInfo
- for _, s := range tempSearchList {
+ for _, d := range ds {
// 通过id 获取对应的详情信息
- sl = append(sl, model.ConvertSearchInfo(model.GetDetailByKey(fmt.Sprintf(config.MovieDetailKey, s.Cid, s.Mid))))
+ sl = append(sl, model.ConvertSearchInfo(d))
}
// 调用批量保存或更新方法, 如果对应mid数据存在则更新, 否则执行插入
model.BatchSaveOrUpdate(sl)
}
+// UpdatePlayDetail 更新最x小时的影片播放源数据
+func UpdatePlayDetail() {
+ for _, s := range SiteList {
+ // 获取单个站点的分页数
+ r := RequestInfo{Uri: MainSite, Params: url.Values{}}
+ r.Params.Set("h", config.UpdateInterval)
+ pageCount, err := GetPageCount(r)
+ if err != nil {
+ log.Printf("Update %s playDetail failed", s.Name)
+ }
+ for i := 1; i <= pageCount; i++ {
+ // 获取详情信息, 保存到对应hashKey中
+ list, e := GetMovieDetail(i, r)
+ if e != nil || len(list) <= 0 {
+ log.Println("GetMovieDetail Error: ", err)
+ continue
+ }
+ // 保存影片播放信息到redis
+ if err = model.SaveSitePlayList(s.Name, list); err != nil {
+ log.Println("SaveDetails Error: ", err)
+ }
+ }
+ }
+}
+
// StartSpiderRe 清空存储数据,从零开始获取
func StartSpiderRe() {
// 删除已有的存储数据, redis 和 mysql中的存储数据全部清空
@@ -348,3 +277,54 @@ func StartSpiderRe() {
// 执行完整数据获取
StartSpider()
}
+
+// =========================公共方法==============================
+
+// GetPageCount 获取总页数
+func GetPageCount(r RequestInfo) (count int, err error) {
+ // 发送请求获取pageCount
+ r.Params.Set("ac", "detail")
+ r.Params.Set("pg", "1")
+ ApiGet(&r)
+ // 判断请求结果是否为空, 如果为空直接输出错误并终止
+ if len(r.Resp) <= 0 {
+ err = errors.New("response is empty")
+ return
+ }
+ // 获取pageCount
+ res := model.DetailListInfo{}
+ err = json.Unmarshal(r.Resp, &res)
+ if err != nil {
+ return
+ }
+ count = int(res.PageCount)
+ return
+}
+
+// GetMovieDetail 处理详情接口请求返回的数据
+func GetMovieDetail(pageNumber int, r RequestInfo) (list []model.MovieDetail, err error) {
+ // 防止json解析异常引发panic
+ defer func() {
+ if e := recover(); e != nil {
+ log.Println("GetMovieDetail Failed : ", e)
+ }
+ }()
+ // 设置分页请求参数
+ r.Params.Set(`ac`, `detail`)
+ r.Params.Set(`pg`, fmt.Sprint(pageNumber))
+ ApiGet(&r)
+ // 影视详情信息
+ details := model.DetailListInfo{}
+ // 如果返回数据为空则直接结束本次循环
+ if len(r.Resp) <= 0 {
+ err = errors.New("response is empty")
+ return
+ }
+ // 序列化详情数据
+ if err = json.Unmarshal(r.Resp, &details); err != nil {
+ return
+ }
+ // 处理details信息
+ list = common.ProcessMovieDetailList(details.List)
+ return
+}
diff --git a/server/plugin/spider/SpiderCron.go b/server/plugin/spider/SpiderCron.go
index 4e64373..bd5c39d 100644
--- a/server/plugin/spider/SpiderCron.go
+++ b/server/plugin/spider/SpiderCron.go
@@ -13,7 +13,7 @@ func RegularUpdateMovie() {
_, err := c.AddFunc(config.CornMovieUpdate, func() {
// 执行更新最近x小时影片的Spider
log.Println("执行一次影片更新任务...")
- GetRecentMovie()
+ UpdateMovieDetail()
})
// 开启定时任务每月最后一天凌晨两点, 执行一次清库重取数据
diff --git a/server/plugin/spider/SpiderRequest.go b/server/plugin/spider/SpiderRequest.go
index 198867f..cb9c829 100644
--- a/server/plugin/spider/SpiderRequest.go
+++ b/server/plugin/spider/SpiderRequest.go
@@ -3,9 +3,11 @@ package spider
import (
"fmt"
"github.com/gocolly/colly/v2"
+ "github.com/gocolly/colly/v2/extensions"
"log"
"net/http"
"net/url"
+ "strconv"
"time"
)
@@ -40,9 +42,7 @@ func CreateClient() *colly.Collector {
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")
+ //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) {
@@ -53,10 +53,22 @@ func CreateClient() *colly.Collector {
// ApiGet 请求数据的方法
func ApiGet(r *RequestInfo) {
+ if r.Header != nil {
+ if t, err := strconv.Atoi(r.Header.Get("timeout")); err != nil && t > 0 {
+ Client.SetRequestTimeout(time.Duration(t) * time.Second)
+ }
+ }
+ // 设置随机请求头
+ extensions.RandomUserAgent(Client)
+ //extensions.Referer(Client)
// 请求成功后的响应
Client.OnResponse(func(response *colly.Response) {
// 将响应结构封装到 RequestInfo.Resp中
- r.Resp = response.Body
+ if len(response.Body) > 0 {
+ r.Resp = response.Body
+ } else {
+ r.Resp = []byte{}
+ }
// 拿到response后输出请求url
//log.Println("\n请求成功: ", response.Request.URL)
})