v2 Multiple play source

This commit is contained in:
mubai
2023-04-09 22:46:07 +08:00
parent 1505cc05e5
commit 945aae9224
22 changed files with 928 additions and 470 deletions

View File

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

View File

@@ -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<hash(name)>: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
}

View File

@@ -13,7 +13,7 @@ func RegularUpdateMovie() {
_, err := c.AddFunc(config.CornMovieUpdate, func() {
// 执行更新最近x小时影片的Spider
log.Println("执行一次影片更新任务...")
GetRecentMovie()
UpdateMovieDetail()
})
// 开启定时任务每月最后一天凌晨两点, 执行一次清库重取数据

View File

@@ -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)
})