mirror of
https://github.com/ProudMuBai/GoFilm.git
synced 2026-04-13 18:47:29 +08:00
v2 Multiple play source
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "anime_client",
|
||||
"name": "client",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
@@ -13,7 +13,8 @@
|
||||
"@element-plus/icons-vue": "^2.1.0",
|
||||
"axios": "^1.3.4",
|
||||
"element-plus": "^2.3.2",
|
||||
"vue": "^3.2.47"
|
||||
"vue": "^3.2.47",
|
||||
"vue3-video-play": "^1.3.1-beta.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.1.0",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<!-- <img class="logo" src="/src/assets/logo.png">-->
|
||||
<a href="/" style="font-weight: 600;font-style: italic;font-size: 24px;margin-right: 5px">Boat</a>
|
||||
<div class="search_group">
|
||||
<input v-model="keyword" placeholder="搜索 动漫,剧集,电影 " class="search"/>
|
||||
<input v-model="keyword" @keydown="(e)=>{e.keyCode == 13 && searchFilm()}" placeholder="搜索 动漫,剧集,电影 " class="search"/>
|
||||
<el-button @click="searchFilm" :icon="Search"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -41,11 +41,11 @@ defineProps({
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-flow: wrap;
|
||||
justify-content: space-between;
|
||||
justify-content: start;
|
||||
}
|
||||
.container .item {
|
||||
width: calc(14% - 18px);
|
||||
margin-bottom: 18px;
|
||||
width: calc(14% - 20px);
|
||||
margin: 2px 10px 16px 10px;
|
||||
}
|
||||
.cus_content_link {
|
||||
border-radius: 5px;
|
||||
@@ -102,11 +102,11 @@ defineProps({
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-flow: wrap;
|
||||
justify-content: space-between;
|
||||
justify-content: start;
|
||||
}
|
||||
.container .item {
|
||||
width: calc(33% - 5px);
|
||||
margin-bottom: 18px;
|
||||
width: calc(33% - 8px);
|
||||
margin: 2px 4px 16px 4px;
|
||||
}
|
||||
.cus_content_link {
|
||||
border-radius: 5px;
|
||||
|
||||
@@ -7,6 +7,7 @@ import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
|
||||
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
|
||||
@@ -15,6 +16,8 @@ app.use(ElementPlus)
|
||||
app.use(router)
|
||||
|
||||
|
||||
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import axios from "axios";
|
||||
import {ElMessage, ElLoading } from "element-plus";
|
||||
|
||||
// 定义加载动画对象
|
||||
let loading:any
|
||||
// 创建loading加载动画对象
|
||||
const startLoading = ()=>{
|
||||
loading = ElLoading.service({
|
||||
return ElLoading.service({
|
||||
lock: true,
|
||||
text: `请求发送中...`,
|
||||
background: `rgba(255,255,255,0.5)`,
|
||||
text: `加载中...`,
|
||||
background: `rgba(0,0,0,0.5)`,
|
||||
// target: document.querySelector(`.content`)
|
||||
})
|
||||
}
|
||||
const closeLoading = ()=>{
|
||||
loading.close()
|
||||
}
|
||||
|
||||
const http = (options: any) => {
|
||||
// 开启loading动画
|
||||
let loading:any = startLoading()
|
||||
return new Promise((resolve, reject) => {
|
||||
// create an axios instance
|
||||
const service = axios.create({
|
||||
@@ -28,8 +26,7 @@ const http = (options: any) => {
|
||||
// request interceptor
|
||||
service.interceptors.request.use(
|
||||
(config: any) => {
|
||||
// 开启loading动画
|
||||
startLoading()
|
||||
|
||||
// let token: string = ""; //此处换成自己获取回来的token,通常存在在cookie或者store里面
|
||||
// if (token) {
|
||||
// // 让每个请求携带token-- ['X-Token']为自定义key 请根据实际情况自行修改
|
||||
@@ -48,12 +45,10 @@ const http = (options: any) => {
|
||||
// response interceptor
|
||||
service.interceptors.response.use(
|
||||
(response) => {
|
||||
closeLoading()
|
||||
|
||||
loading.close()
|
||||
return response.data;
|
||||
},
|
||||
(error) => {
|
||||
closeLoading()
|
||||
if (error.response.status == 403) {
|
||||
ElMessage.error("请求异常: ", error)
|
||||
} else {
|
||||
@@ -61,6 +56,7 @@ const http = (options: any) => {
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
);
|
||||
// 请求处理
|
||||
service(options)
|
||||
@@ -70,6 +66,7 @@ const http = (options: any) => {
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<div class="c_header">
|
||||
<a :class="`nav ${d.list.length >0 && d.list[0].cid == c.id?'active':''}`" href="javascript:;" @click="changeCategory(c.id)"
|
||||
v-for="c in d.category.children">{{ c.name }}</a>
|
||||
<!-- <a :class="`nav `" href="javascript:;" @click="changeCategory">{{ d.category.name }}</a>-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,10 +70,10 @@ const changeCurrent = (currentVal: number) => {
|
||||
}
|
||||
|
||||
// 点击分类事件
|
||||
const changeCategory = (cid: any) => {
|
||||
const changeCategory = (cid?: any) => {
|
||||
let params = new URLSearchParams(location.search)
|
||||
// location.href = `/categoryFilm?pid=${params.get('pid')}&cid=${cid}¤t=${params.get('current')?params.get('current'):1}`
|
||||
location.href = `/categoryFilm?pid=${params.get('pid')}&cid=${cid}¤t=1`
|
||||
location.href = cid?`/categoryFilm?pid=${params.get('pid')}&cid=${cid}¤t=1`:`/categoryFilm?pid=${params.get('pid')}`
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +106,7 @@ onMounted(() => {
|
||||
/*顶部内容区域*/
|
||||
.header {
|
||||
width: 100%;
|
||||
margin-bottom: 150px;
|
||||
margin-bottom: 100px;
|
||||
background: none!important;
|
||||
}
|
||||
.header p {
|
||||
@@ -177,14 +178,13 @@ onMounted(() => {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-flow: wrap;
|
||||
justify-content: space-between;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.c_content .item {
|
||||
flex-basis: calc(33% - 5px);
|
||||
flex-basis: calc(33% - 6px);
|
||||
max-width: 33%;
|
||||
margin-bottom: 20px;
|
||||
/*width: 100px;*/
|
||||
margin: 0 3px 20px 3px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -305,13 +305,12 @@ onMounted(() => {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-flow: wrap;
|
||||
justify-content: space-between;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.c_content .item {
|
||||
flex-basis: calc(14% - 16px);
|
||||
max-width: 14%;
|
||||
margin-bottom: 20px;
|
||||
margin: 0 8px 20px 8px;
|
||||
/*width: 100px;*/
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="film">
|
||||
<div class="film" v-show="data.loading">
|
||||
<!-- hidden-sm-and-up 移动端title -->
|
||||
<div class="hidden-sm-and-up">
|
||||
<div class="title_mt ">
|
||||
@@ -11,7 +11,7 @@
|
||||
</ul>
|
||||
<p><span>导演:</span> {{ data.detail.descriptor.director }}</p>
|
||||
<p><span>主演:</span>
|
||||
{{ `${data.detail.descriptor.actor}`.split(",")[0] + " " + `${data.detail.descriptor.actor}`.split(",")[1] + " " + `${data.detail.descriptor.actor}`.split(",")[2] }}
|
||||
{{ (data.detail.descriptor.actor && data.detail.descriptor.actor.length > 0) ? `${data.detail.descriptor.actor}`.split(",")[0] + " " + `${data.detail.descriptor.actor}`.split(",")[1] : ''}}
|
||||
</p>
|
||||
<p><span>上映:</span> {{ data.detail.descriptor.releaseDate }}</p>
|
||||
<p><span>地区:</span> {{ data.detail.descriptor.area }}</p>
|
||||
@@ -81,12 +81,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import {useRouter} from "vue-router";
|
||||
import {onMounted, reactive, ref,} from "vue";
|
||||
import {onBeforeMount, reactive, ref,} from "vue";
|
||||
import {ApiGet} from "../../utils/request";
|
||||
import {ElMessage} from 'element-plus'
|
||||
import {ElMessage, ElLoading} from 'element-plus'
|
||||
import {Promotion, CaretRight} from "@element-plus/icons-vue";
|
||||
import RelateList from "../../components/RelateList.vue";
|
||||
const show = ref(false)
|
||||
|
||||
// 获取路由对象
|
||||
const router = useRouter()
|
||||
@@ -94,15 +93,17 @@ const router = useRouter()
|
||||
const data = reactive({
|
||||
detail: {descriptor: {}},
|
||||
relate: [],
|
||||
loading: false,
|
||||
})
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
onBeforeMount(() => {
|
||||
let link = router.currentRoute.value.query.link
|
||||
ApiGet('/filmDetail', {id: link}).then((resp: any) => {
|
||||
if (resp.status === "ok") {
|
||||
data.detail = resp.data.detail
|
||||
data.relate = resp.data.relate
|
||||
data.loading = true
|
||||
} else {
|
||||
ElMessage({
|
||||
type: "error",
|
||||
@@ -180,7 +181,15 @@ const showContent = (flag: boolean) => {
|
||||
font-size: 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.play_content a {
|
||||
color: #ffffff;
|
||||
border-radius: 6px;
|
||||
margin: 6px 8px;
|
||||
background: #888888;
|
||||
flex-basis: calc(25% - 16px);
|
||||
font-size: 12px;
|
||||
padding: 6px 12px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -217,14 +226,16 @@ const showContent = (flag: boolean) => {
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.play_content a {
|
||||
font-size: 12px;
|
||||
min-width: 65px;
|
||||
padding: 6px 15px;
|
||||
color: #ffffff;
|
||||
border-radius: 6px;
|
||||
margin: 8px 8px;
|
||||
background: #888888;
|
||||
@media (min-width: 650px) {
|
||||
.play_content a {
|
||||
font-size: 12px;
|
||||
flex-basis: calc(10% - 24px);
|
||||
padding: 6px 15px;
|
||||
color: #ffffff;
|
||||
border-radius: 6px;
|
||||
margin: 8px 12px;
|
||||
background: #888888;
|
||||
}
|
||||
}
|
||||
|
||||
.plya_tabs {
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
<template>
|
||||
|
||||
<div class="player_area">
|
||||
<!-- 视频播放区域-->
|
||||
<div class="player_p">
|
||||
<iframe ref="iframe" class="player" :src="data.current.link"
|
||||
:name="data.detail.name"
|
||||
marginheight="0"
|
||||
marginwidth="0"
|
||||
framespacing="0"
|
||||
vspale="0"
|
||||
frameborder="0" allowfullscreen="true" scolling="no"
|
||||
sandbox="allow-scripts allow-same-origin allow-downloads">
|
||||
</iframe>
|
||||
</div>
|
||||
<div class="player_area" v-show="data.loading">
|
||||
<div class="player_p" >
|
||||
<videoPlay class="player" v-bind="data.options" poster='/src/assets/image/play.png'
|
||||
/>
|
||||
</div>
|
||||
<div class="current_play_info">
|
||||
<div class="play_info_left">
|
||||
<h3 class="current_play_title">{{ `${data.detail.name} ${data.current.episode}` }}</h3>
|
||||
@@ -32,13 +23,14 @@
|
||||
<!-- 播放选集 -->
|
||||
<div class="play_list">
|
||||
<h2 class="hidden-md-and-down">播放列表:(右侧切换播放源)</h2>
|
||||
<el-tabs type="card" v-model="data.currentTabName" class="plya_tabs" @tab-change="changeSource">
|
||||
<el-tabs type="card" v-model="data.currentTabName" class="plya_tabs" >
|
||||
<el-tab-pane v-for="(p,i) in data.detail.playList"
|
||||
:name="`tab-${i}`"
|
||||
:label="`播放地址${i+1}`">
|
||||
:label="`播放列表${i+1}`">
|
||||
<div class="play_content">
|
||||
<a v-for="(item,index) in p" href="javascript:void(false)" @click="playChange(item)"
|
||||
:class="data.current.link.search(item.link) !== -1?'play_active':''">{{ item.episode }}</a>
|
||||
<a v-for="(item,index) in p" href="javascript:void(false)" @click="playChange({sourceIndex: i, episodeIndex: index, target: this})"
|
||||
:class="item.link == data.current.link?'play_active':''">{{ item.episode }}</a>
|
||||
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
@@ -52,120 +44,97 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
import {onMounted, reactive, ref, withDirectives} from "vue";
|
||||
import {onBeforeMount, onMounted, reactive, ref, withDirectives} from "vue";
|
||||
import {useRouter} from "vue-router";
|
||||
import {ApiGet} from "../../utils/request";
|
||||
import RelateList from "../../components/RelateList.vue";
|
||||
import {Promotion} from "@element-plus/icons-vue";
|
||||
// 引入视频播放器组件
|
||||
import 'vue3-video-play/dist/style.css'
|
||||
import {videoPlay } from 'vue3-video-play'
|
||||
import {ElMessage} from "element-plus";
|
||||
|
||||
|
||||
// 播放页所需数据
|
||||
const data = reactive({
|
||||
loading: false,
|
||||
detail: {descriptor: {}, playList: [[{episode: '', link: ''}]]},
|
||||
current: {episode: '', link: ''},
|
||||
current: {index: 0, episode: '', link: ''},
|
||||
currentTabName: '',
|
||||
currentPlayFrom: 0,
|
||||
currentEpisode: 0,
|
||||
relate: [],
|
||||
relate: [{}],
|
||||
// vue3-video-play 播放属性设置
|
||||
options: {
|
||||
width: '100%', //播放器高度
|
||||
height: '100%', //播放器高度
|
||||
color: "rgba(155,73,231,0.72)", //主题色
|
||||
title: "", //视频名称
|
||||
src: "", //视频源
|
||||
type: 'm3u8', //视频类型
|
||||
muted: false, //静音
|
||||
webFullScreen: false,
|
||||
speedRate: ["0.75", "1.0", "1.25", "1.5", "2.0"], //播放倍速
|
||||
autoPlay: false, //自动播放
|
||||
loop: false, //循环播放
|
||||
mirror: false, //镜像画面
|
||||
ligthOff: false, //关灯模式
|
||||
volume: 0.3, //默认音量大小
|
||||
control: true, //是否显示控制器
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
// 获取路由信息
|
||||
const router = useRouter()
|
||||
onMounted(() => {
|
||||
onBeforeMount(() => {
|
||||
let query = router.currentRoute.value.query
|
||||
ApiGet(`/filmPlayInfo`, {id: query.id, playFrom: query.source, episode: query.episode}).then((resp: any) => {
|
||||
if (resp.status === 'ok') {
|
||||
data.detail = resp.data.detail
|
||||
resp.data.current.link = converLink(resp.data.current.link)
|
||||
data.current = resp.data.current
|
||||
data.current = {index: resp.data.currentEpisode, ...resp.data.current}
|
||||
data.currentPlayFrom = resp.data.currentPlayFrom
|
||||
data.currentEpisode = resp.data.currentEpisode
|
||||
data.relate = resp.data.relate
|
||||
// 设置当前选中的播放源
|
||||
data.currentTabName = `tab-${query.source}`
|
||||
// 设置当前的视频播放url
|
||||
data.options.src = data.current.link
|
||||
data.loading = true
|
||||
} else {
|
||||
ElMessage.error("影片信息加载失败,请尝试刷新页面!!!")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
// ===============================视频播放处理=======================================
|
||||
// 视频解析接口地址, 默认使用第一个
|
||||
const resolver = [
|
||||
// m3u8使用此解析
|
||||
'https://jx.jsonplayer.com/player/?url=',
|
||||
// 'https://jx.m3u8.tv/jiexi/?url=',
|
||||
|
||||
|
||||
// 'https://jx.jsonplayer.com/player/?url=',
|
||||
// 'https://vip.bljiex.com/?url=',
|
||||
// 'https://jx.bozrc.com:4433/player/?url=',
|
||||
// html视频使用此解析
|
||||
'http://www.82190555.com/index/qqvod.php?url=',
|
||||
// 'https://jx.bozrc.com:4433/player/?url=',
|
||||
// 'https://vip.bljiex.com/?url=',
|
||||
|
||||
// Google上随便找的
|
||||
'https://vip.bljiex.com/?url=',
|
||||
'https://jx.kingtail.xyz/?url=',
|
||||
'http://www.82190555.com/index/qqvod.php?url=',
|
||||
'https://www.nxflv.com/?url=',
|
||||
'http://www.wmxz.wang/video.php?url=',
|
||||
'https://www.feisuplayer.com/m3u8/?url=',
|
||||
// tampermonkey 脚本使用的解析
|
||||
'https://jx.bozrc.com:4433/player/?url=',
|
||||
'https://z1.m1907.top/?jx=',
|
||||
'https://jx.aidouer.net/?url=',
|
||||
'https://www.gai4.com/?url=',
|
||||
'https://okjx.cc/?url=',
|
||||
'https://jx.rdhk.net/?v=',
|
||||
'https://jx.blbo.cc:4433/?url=',
|
||||
'https://jsap.attakids.com/?url=',
|
||||
'https://jx.dj6u.com/?url=',
|
||||
]
|
||||
|
||||
// 添加视频解析前缀
|
||||
const converLink = (link: string): string => {
|
||||
// 视频统一使用第三方解析
|
||||
if (link.search("m3u8") != -1) {
|
||||
return `${resolver[0] + link}`
|
||||
}
|
||||
// return `${resolver[1]+link}`
|
||||
return `${link}`
|
||||
// 点击播集数播放对应影片
|
||||
const playChange = (play: { sourceIndex: number, episodeIndex: number, target:any }) => {
|
||||
let currPlay = data.detail.playList[play.sourceIndex][play.episodeIndex]
|
||||
data.current = {index: play.episodeIndex, episode: currPlay.episode, link: currPlay.link}
|
||||
data.options.src = currPlay.link
|
||||
data.options.title = `${data.detail.name} ${currPlay.episode} `
|
||||
}
|
||||
|
||||
// 点击播放对应影片
|
||||
const playChange = (info: { link: string, episode: string }) => {
|
||||
// 判断是否是m3u8播放器, 如果是则添加前缀
|
||||
data.current.link = converLink(info.link)
|
||||
data.current.episode = info.episode
|
||||
}
|
||||
// 点击播放源标签事件
|
||||
const changeSource = (tabName: any) => {
|
||||
data.currentTabName = tabName
|
||||
data.detail.playList.find((item, index) => {
|
||||
if (tabName.split("-")[1] - index == 0) {
|
||||
item.find(i => {
|
||||
if (i.episode == data.current.episode) {
|
||||
data.current.link = converLink(i.link)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 测试滑动音量调节
|
||||
const iframe = ref()
|
||||
// document.querySelector('#brightnessSlider').addEventListener('change', function() {
|
||||
// // 获取滑块的值
|
||||
// var brightness = this.value;
|
||||
//
|
||||
// // 设置亮度为滑块的值
|
||||
// iframe.style.filter = 'brightness(' + brightness + '%)';
|
||||
// })
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/*vue3-video-play 相关设置*/
|
||||
/*//播放器控件区域大小*/
|
||||
:deep(#refPlayerWrap) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
border-radius: 6px;
|
||||
position: absolute;
|
||||
}
|
||||
/*将鼠标右键触发元素的层级降低,回避触发右键视频菜单信息*/
|
||||
:deep(.d-player-contextmenu){
|
||||
z-index: -100!important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*当前播放的影片信息展示*/
|
||||
.current_play_info {
|
||||
width: 100%;
|
||||
@@ -184,9 +153,6 @@ const iframe = ref()
|
||||
.player_area {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
/*height: 1400px;*/
|
||||
/*background: red;*/
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -211,33 +177,30 @@ const iframe = ref()
|
||||
margin: 0 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.play_content a {
|
||||
font-size: 12px;
|
||||
flex-basis: calc(10% - 24px);
|
||||
padding: 6px 10px;
|
||||
color: #ffffff;
|
||||
border-radius: 6px;
|
||||
margin: 8px 12px;
|
||||
background: #888888;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
.player_p {
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
/*height: 700px;*/
|
||||
margin: 0;
|
||||
padding-bottom: 56.25% !important;
|
||||
/*padding-bottom: 42.25% !important;*/
|
||||
position: relative;
|
||||
background-image: url("/src/assets/image/play.png");
|
||||
background-size: cover;
|
||||
background: red;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border-radius: 6px;
|
||||
left: 0;
|
||||
width: 100%; /* 设置iframe元素的宽度为父容器的100% */
|
||||
height: 100%; /* 设置iframe元素的高度为0,以便自适应高度 */
|
||||
/*padding-bottom: 56.25%; !* 使用padding-bottom属性计算iframe元素的高度,这里假设视频的宽高比为16:9 *!*/
|
||||
/*border: none; !* 去除iframe元素的边框 *!*/
|
||||
/*transform: scale(1);*/
|
||||
position: absolute;
|
||||
|
||||
}
|
||||
|
||||
/*右侧播放源选择区域*/
|
||||
/*影片播放列表信息展示*/
|
||||
@@ -253,7 +216,7 @@ iframe {
|
||||
.play_content {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
padding: 10px 10px 10px 18px;
|
||||
padding: 10px 10px 10px 10px;
|
||||
|
||||
}
|
||||
|
||||
@@ -264,15 +227,7 @@ iframe {
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.play_content a {
|
||||
font-size: 12px;
|
||||
min-width: 65px;
|
||||
padding: 6px 15px;
|
||||
color: #ffffff;
|
||||
border-radius: 6px;
|
||||
margin: 8px 8px;
|
||||
background: #888888;
|
||||
}
|
||||
|
||||
|
||||
/*集数选中效果*/
|
||||
.play_active {
|
||||
@@ -342,6 +297,17 @@ iframe {
|
||||
border-radius: 6px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
|
||||
.play_content a {
|
||||
color: #ffffff;
|
||||
border-radius: 6px;
|
||||
margin: 6px 8px;
|
||||
background: #888888;
|
||||
flex-basis: calc(25% - 16px);
|
||||
font-size: 12px;
|
||||
padding: 6px 12px !important;
|
||||
}
|
||||
.tags span {
|
||||
padding: 6px 10px;
|
||||
background-color: #404042;
|
||||
@@ -350,6 +316,12 @@ iframe {
|
||||
margin: 0 3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
:deep(.el-tabs__item){
|
||||
width: 70px;
|
||||
height: 35px;
|
||||
margin: 17px 5px 0 0!important;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
357
client/src/views/index/PlayV1.vue
Normal file
357
client/src/views/index/PlayV1.vue
Normal file
@@ -0,0 +1,357 @@
|
||||
<template>
|
||||
|
||||
<div class="player_area">
|
||||
<!-- 视频播放区域-->
|
||||
<div class="player_p">
|
||||
<iframe ref="iframe" class="player" :src="data.current.link"
|
||||
:name="data.detail.name"
|
||||
marginheight="0"
|
||||
marginwidth="0"
|
||||
framespacing="0"
|
||||
vspale="0"
|
||||
allow="fullscreen"
|
||||
frameborder="0" scolling="no"
|
||||
sandbox="allow-scripts allow-same-origin allow-downloads">
|
||||
</iframe>
|
||||
|
||||
</div>
|
||||
<div class="current_play_info">
|
||||
<div class="play_info_left">
|
||||
<h3 class="current_play_title">{{ `${data.detail.name} ${data.current.episode}` }}</h3>
|
||||
<div class="tags">
|
||||
<b>
|
||||
<el-icon>
|
||||
<Promotion/>
|
||||
</el-icon>
|
||||
{{ data.detail.descriptor.cName }}</b>
|
||||
<span>{{ data.detail.descriptor.classTag }}</span>
|
||||
<span>{{ data.detail.descriptor.year }}</span>
|
||||
<span>{{ data.detail.descriptor.area }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- 播放选集 -->
|
||||
<div class="play_list">
|
||||
<h2 class="hidden-md-and-down">播放列表:(右侧切换播放源)</h2>
|
||||
<el-tabs type="card" v-model="data.currentTabName" class="plya_tabs" @tab-change="changeSource">
|
||||
<el-tab-pane v-for="(p,i) in data.detail.playList"
|
||||
:name="`tab-${i}`"
|
||||
:label="`播放地址${i+1}`">
|
||||
<div class="play_content">
|
||||
<a v-for="(item,index) in p" href="javascript:void(false)" @click="playChange(item)"
|
||||
:class="data.current.link.search(item.link) !== -1?'play_active':''">{{ item.episode }}</a>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
<div class="correlation">
|
||||
<RelateList :relate-list="data.relate"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
import {onMounted, reactive, ref, withDirectives} from "vue";
|
||||
import {useRouter} from "vue-router";
|
||||
import {ApiGet} from "../../utils/request";
|
||||
import RelateList from "../../components/RelateList.vue";
|
||||
import {Promotion} from "@element-plus/icons-vue";
|
||||
|
||||
// 播放页所需数据
|
||||
const data = reactive({
|
||||
detail: {descriptor: {}, playList: [[{episode: '', link: ''}]]},
|
||||
current: {episode: '', link: ''},
|
||||
currentTabName: '',
|
||||
currentPlayFrom: 0,
|
||||
currentEpisode: 0,
|
||||
relate: [],
|
||||
|
||||
})
|
||||
|
||||
// 获取路由信息
|
||||
const router = useRouter()
|
||||
onMounted(() => {
|
||||
let query = router.currentRoute.value.query
|
||||
ApiGet(`/filmPlayInfo`, {id: query.id, playFrom: query.source, episode: query.episode}).then((resp: any) => {
|
||||
if (resp.status === 'ok') {
|
||||
data.detail = resp.data.detail
|
||||
resp.data.current.link = converLink(resp.data.current.link)
|
||||
data.current = resp.data.current
|
||||
data.currentPlayFrom = resp.data.currentPlayFrom
|
||||
data.currentEpisode = resp.data.currentEpisode
|
||||
data.relate = resp.data.relate
|
||||
// 设置当前选中的播放源
|
||||
data.currentTabName = `tab-${query.source}`
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
// ===============================视频播放处理=======================================
|
||||
// 视频解析接口地址, 默认使用第一个
|
||||
const resolver = [
|
||||
// m3u8使用此解析
|
||||
'https://jx.jsonplayer.com/player/?url=',
|
||||
// 'https://jx.m3u8.tv/jiexi/?url=',
|
||||
|
||||
|
||||
// 'https://jx.jsonplayer.com/player/?url=',
|
||||
// 'https://vip.bljiex.com/?url=',
|
||||
// 'https://jx.bozrc.com:4433/player/?url=',
|
||||
// html视频使用此解析
|
||||
'http://www.82190555.com/index/qqvod.php?url=',
|
||||
// 'https://jx.bozrc.com:4433/player/?url=',
|
||||
// 'https://vip.bljiex.com/?url=',
|
||||
|
||||
// Google上随便找的
|
||||
'https://vip.bljiex.com/?url=',
|
||||
'https://jx.kingtail.xyz/?url=',
|
||||
'http://www.82190555.com/index/qqvod.php?url=',
|
||||
'https://www.nxflv.com/?url=',
|
||||
'http://www.wmxz.wang/video.php?url=',
|
||||
'https://www.feisuplayer.com/m3u8/?url=',
|
||||
// tampermonkey 脚本使用的解析
|
||||
'https://jx.bozrc.com:4433/player/?url=',
|
||||
'https://z1.m1907.top/?jx=',
|
||||
'https://jx.aidouer.net/?url=',
|
||||
'https://www.gai4.com/?url=',
|
||||
'https://okjx.cc/?url=',
|
||||
'https://jx.rdhk.net/?v=',
|
||||
'https://jx.blbo.cc:4433/?url=',
|
||||
'https://jsap.attakids.com/?url=',
|
||||
'https://jx.dj6u.com/?url=',
|
||||
]
|
||||
|
||||
// 添加视频解析前缀
|
||||
const converLink = (link: string): string => {
|
||||
// 视频统一使用第三方解析
|
||||
if (link.search("m3u8") != -1) {
|
||||
return `${resolver[0] + link}`
|
||||
}
|
||||
// return `${resolver[1]+link}`
|
||||
return `${link}`
|
||||
}
|
||||
|
||||
// 点击播放对应影片
|
||||
const playChange = (info: { link: string, episode: string }) => {
|
||||
// 判断是否是m3u8播放器, 如果是则添加前缀
|
||||
data.current.link = converLink(info.link)
|
||||
data.current.episode = info.episode
|
||||
}
|
||||
// 点击播放源标签事件
|
||||
const changeSource = (tabName: any) => {
|
||||
data.currentTabName = tabName
|
||||
data.detail.playList.find((item, index) => {
|
||||
if (tabName.split("-")[1] - index == 0) {
|
||||
item.find(i => {
|
||||
if (i.episode == data.current.episode) {
|
||||
data.current.link = converLink(i.link)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 测试滑动音量调节
|
||||
const iframe = ref()
|
||||
// document.querySelector('#brightnessSlider').addEventListener('change', function() {
|
||||
// // 获取滑块的值
|
||||
// var brightness = this.value;
|
||||
//
|
||||
// // 设置亮度为滑块的值
|
||||
// iframe.style.filter = 'brightness(' + brightness + '%)';
|
||||
// })
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/*当前播放的影片信息展示*/
|
||||
.current_play_info {
|
||||
width: 100%;
|
||||
padding: 15px 5px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.current_play_title {
|
||||
font-weight: 500;
|
||||
color: rgb(201, 196, 196);
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
|
||||
|
||||
/* 播放区域*/
|
||||
.player_area {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
/*height: 1400px;*/
|
||||
/*background: red;*/
|
||||
|
||||
}
|
||||
|
||||
|
||||
@media (min-width: 650px) {
|
||||
.player_area {
|
||||
padding: 10px 6%;
|
||||
}
|
||||
|
||||
.tags b {
|
||||
padding: 5px 10px;
|
||||
background-color: rgba(155, 73, 231, 0.72);
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.tags span {
|
||||
padding: 6px 12px;
|
||||
background-color: #404042;
|
||||
color: #b5b2b2;
|
||||
border-radius: 5px;
|
||||
margin: 0 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
.player_p {
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
margin: 0;
|
||||
padding-bottom: 56.25% !important;
|
||||
/*padding-bottom: 42.25% !important;*/
|
||||
position: relative;
|
||||
background-image: url("/src/assets/image/play.png");
|
||||
background-size: cover;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border-radius: 6px;
|
||||
left: 0;
|
||||
width: 100%; /* 设置iframe元素的宽度为父容器的100% */
|
||||
height: 100%; /* 设置iframe元素的高度为0,以便自适应高度 */
|
||||
/*padding-bottom: 56.25%; !* 使用padding-bottom属性计算iframe元素的高度,这里假设视频的宽高比为16:9 *!*/
|
||||
/*border: none; !* 去除iframe元素的边框 *!*/
|
||||
/*transform: scale(1);*/
|
||||
position: absolute;
|
||||
|
||||
}
|
||||
|
||||
/*右侧播放源选择区域*/
|
||||
/*影片播放列表信息展示*/
|
||||
/*影片播放列表信息展示*/
|
||||
.play_list {
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
background: #2e2e2e;
|
||||
margin-top: 50px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.play_content {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
padding: 10px 10px 10px 18px;
|
||||
|
||||
}
|
||||
|
||||
.play_list > h2 {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: -10px;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.play_content a {
|
||||
font-size: 12px;
|
||||
min-width: 65px;
|
||||
padding: 6px 15px;
|
||||
color: #ffffff;
|
||||
border-radius: 6px;
|
||||
margin: 8px 8px;
|
||||
background: #888888;
|
||||
}
|
||||
|
||||
/*集数选中效果*/
|
||||
.play_active {
|
||||
color: orange !important;
|
||||
background: #424242 !important;
|
||||
}
|
||||
|
||||
.play_content .play_tabs {
|
||||
background: #2e2e2e;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav-scroll) {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__header) {
|
||||
/*border-bottom: 1px solid #888888!important;*/
|
||||
margin-bottom: 0;
|
||||
border-bottom: none !important;
|
||||
background: rgb(34, 34, 34);
|
||||
height: 50px !important;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav) {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item.is-active) {
|
||||
color: #ee9600;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item:hover) {
|
||||
color: orange;
|
||||
background: #484646;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
margin-left: 2px;
|
||||
border-radius: 8px 8px 0 0;
|
||||
border: none !important;
|
||||
color: #ffffff;
|
||||
background: #2e2e2e;
|
||||
}
|
||||
|
||||
/*推荐列表区域*/
|
||||
.correlation {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<!--移动端-->
|
||||
<style scoped>
|
||||
|
||||
/*适应小尺寸*/
|
||||
@media (max-width: 650px) {
|
||||
.player_area {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
.tags b {
|
||||
padding: 5px 10px;
|
||||
background-color: rgba(155, 73, 231, 0.72);
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
.tags span {
|
||||
padding: 6px 10px;
|
||||
background-color: #404042;
|
||||
color: #b5b2b2;
|
||||
border-radius: 5px;
|
||||
margin: 0 3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="search_group">
|
||||
<input v-model="data.search" placeholder="搜索 动漫,剧集,电影 " class="search"/>
|
||||
<input v-model="data.search" @keydown="e=>{e.keyCode==13 && searchMovie()}" placeholder="搜索 动漫,剧集,电影 " class="search"/>
|
||||
<el-button @click="searchMovie" :icon="Search" style="" />
|
||||
</div>
|
||||
<div v-if="data.list.length > 0 " class="search_res">
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -36,7 +36,7 @@ func FixFilmDetail(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
// 如果指令正确,则执行详情数据获取
|
||||
spider.AllMovieInfo()
|
||||
spider.MainSiteSpider()
|
||||
log.Println("FilmDetail 重制完成!!!")
|
||||
// 先截断表中的数据
|
||||
model.TunCateSearchTable()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ func RegularUpdateMovie() {
|
||||
_, err := c.AddFunc(config.CornMovieUpdate, func() {
|
||||
// 执行更新最近x小时影片的Spider
|
||||
log.Println("执行一次影片更新任务...")
|
||||
GetRecentMovie()
|
||||
UpdateMovieDetail()
|
||||
})
|
||||
|
||||
// 开启定时任务每月最后一天凌晨两点, 执行一次清库重取数据
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user