diff --git a/film/server/config/DataConfig.go b/film/server/config/DataConfig.go index f7cd47e..c96b6f4 100644 --- a/film/server/config/DataConfig.go +++ b/film/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,9 +47,29 @@ const ( ) const ( + + // 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" - RedisAddr = `redis:6379` + /* + redis 配置信息 + RedisAddr host:port + RedisPassword redis访问密码 + RedisDBNo 使用第几号库 + */ + //RedisAddr = `192.168.20.10:6379` + //RedisPassword = `root` + //RedisDBNo = 1 + + // RedisAddr docker compose 环境下运行使用如下配置信息 + RedisAddr = `redis:6379` RedisPassword = `root` - RedisDBNo = 0 + RedisDBNo = 0 ) diff --git a/film/server/controller/SpiderController.go b/film/server/controller/SpiderController.go index a232c52..aa59795 100644 --- a/film/server/controller/SpiderController.go +++ b/film/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/film/server/logic/IndexLogic.go b/film/server/logic/IndexLogic.go index ea04db5..9027a37 100644 --- a/film/server/logic/IndexLogic.go +++ b/film/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/film/server/main.go b/film/server/main.go index d233cb5..c327652 100644 --- a/film/server/main.go +++ b/film/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/film/server/model/Movies.go b/film/server/model/Movies.go index fea9e00..087ce32 100644 --- a/film/server/model/Movies.go +++ b/film/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/film/server/model/LZJson.go b/film/server/model/ResponseJson.go similarity index 97% rename from film/server/model/LZJson.go rename to film/server/model/ResponseJson.go index e0b1a18..d1e30f2 100644 --- a/film/server/model/LZJson.go +++ b/film/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/film/server/model/Search.go b/film/server/model/Search.go index bd03aa2..cecdcce 100644 --- a/film/server/model/Search.go +++ b/film/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/film/server/plugin/common/ProcessMovies.go b/film/server/plugin/common/ProcessMovies.go index 5b8f3b7..6234134 100644 --- a/film/server/plugin/common/ProcessMovies.go +++ b/film/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/film/server/plugin/spider/Spider.go b/film/server/plugin/spider/Spider.go index d206fdf..9986544 100644 --- a/film/server/plugin/spider/Spider.go +++ b/film/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/film/server/plugin/spider/SpiderCron.go b/film/server/plugin/spider/SpiderCron.go index 4e64373..bd5c39d 100644 --- a/film/server/plugin/spider/SpiderCron.go +++ b/film/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/film/server/plugin/spider/SpiderRequest.go b/film/server/plugin/spider/SpiderRequest.go index 198867f..cb9c829 100644 --- a/film/server/plugin/spider/SpiderRequest.go +++ b/film/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) })