You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Ru/c/scripts/spider.php

319 lines
9.8 KiB

<?php
/**
* Copyright 道长所有
* Date: 2026/01/23
*/
/**
* PHP Spider Base Class
* 旨在模仿 JS 版 TVBox Spider 的写法,简化 PHP 源开发
*/
header('Content-Type: application/json; charset=utf-8');
// 屏蔽一般警告,避免污染 JSON 输出
error_reporting(E_ALL);
ini_set('display_errors', '1');
abstract class BaseSpider {
// 默认请求头
protected $headers = [
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
'Accept-Language' => 'zh-CN,zh;q=0.9',
];
/**
* 初始化方法
* @param string $extend 扩展参数
*/
public function init($extend = '') {
// 子类实现
}
/**
* 获取首页分类
* @param array $filter 筛选条件
* @return array
*/
public function homeContent($filter) {
return ['class' => []];
}
/**
* 获取首页推荐视频
* @return array
*/
public function homeVideoContent() {
return ['list' => []];
}
/**
* 获取分类详情
* @param string $tid 分类ID
* @param int $pg 页码
* @param array $filter 筛选条件
* @param array $extend 扩展参数
* @return array
*/
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
return ['list' => [], 'page' => $pg, 'pagecount' => 1, 'limit' => 20, 'total' => 0];
}
/**
* 获取视频详情
* @param array $ids 视频ID列表
* @return array
*/
public function detailContent($ids) {
return ['list' => []];
}
/**
* 搜索视频
* @param string $key 关键词
* @param bool $quick 快速搜索
* @param int $pg 页码
* @return array
*/
public function searchContent($key, $quick = false, $pg = 1) {
return ['list' => []];
}
/**
* 获取播放地址
* @param string $flag 播放线路
* @param string $id 视频播放ID
* @param array $vipFlags VIP标识
* @return array
*/
public function playContent($flag, $id, $vipFlags = []) {
return ['parse' => 0, 'url' => '', 'header' => []];
}
/**
* 代理请求 (可选)
* @param array $params
* @return mixed
*/
public function localProxy($params) {
return null;
}
/**
* 执行 Action (可选)
* @param string $action 动作名称
* @param string $value 参数值
* @return mixed
*/
public function action($action, $value) {
return '';
}
// ================== 辅助方法 ==================
/**
* 快速构建分页返回结果
* @param array $list 视频列表
* @param int $pg 当前页码
* @param int $total 总记录数 (可选)
* @param int $limit 每页条数 (默认 20)
* @return array
*/
protected function pageResult($list, $pg, $total = 0, $limit = 20) {
$pg = max(1, intval($pg));
$count = count($list);
if ($total > 0) {
$pagecount = ceil($total / $limit);
} else {
// 如果没有提供 total,尝试根据当前列表数量估算
if ($count < $limit) {
// 当前页数据少于限制,说明是最后一页
$pagecount = $pg;
$total = ($pg - 1) * $limit + $count;
} else {
// 还有下一页,设置一个较大的页数
$pagecount = 9999;
$total = 99999;
}
}
return [
'list' => $list,
'page' => $pg,
'pagecount' => intval($pagecount),
'limit' => intval($limit),
'total' => intval($total)
];
}
/**
* 封装 HTTP 请求
* @param string $url 请求地址
* @param array $options CURL 选项
* @param array $headers 请求头
* @return string|bool
*/
protected function fetch($url, $options = [], $headers = []) {
$ch = curl_init();
// 1. 解析自定义 header 为关联数组
$customHeaders = [];
foreach ($headers as $k => $v) {
if (is_numeric($k)) {
// 处理 "Key: Value" 格式
$parts = explode(':', $v, 2);
if (count($parts) === 2) {
$key = trim($parts[0]);
$value = trim($parts[1]);
$customHeaders[$key] = $value;
}
} else {
$customHeaders[$k] = $v;
}
}
// 2. 合并请求头 (自定义覆盖默认)
$finalHeadersMap = array_merge($this->headers, $customHeaders);
// 3. 转换回 CURL 所需的索引数组
$mergedHeaders = [];
foreach ($finalHeadersMap as $k => $v) {
$mergedHeaders[] = "$k: $v";
}
$defaultOptions = [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_ENCODING => '', // 支持 GZIP 自动解压
CURLOPT_HTTPHEADER => $mergedHeaders,
];
// 处理 POST 数据
if (isset($options['body'])) {
$defaultOptions[CURLOPT_POST] = true;
$defaultOptions[CURLOPT_POSTFIELDS] = $options['body'];
unset($options['body']);
}
// 处理 Cookie
if (isset($options['cookie'])) {
$defaultOptions[CURLOPT_COOKIE] = $options['cookie'];
unset($options['cookie']);
}
// 合并用户自定义选项
foreach ($options as $k => $v) {
$defaultOptions[$k] = $v;
}
curl_setopt_array($ch, $defaultOptions);
$result = curl_exec($ch);
if (is_resource($ch)) {
curl_close($ch);
}
return $result;
}
/**
* 自动运行,处理路由
*/
public function run() {
$ac = $_GET['ac'] ?? '';
$t = $_GET['t'] ?? '';
$pg = $_GET['pg'] ?? '1';
$wd = $_GET['wd'] ?? '';
$ids = $_GET['ids'] ?? '';
$play = $_GET['play'] ?? ''; // 某些源使用 play 参数传递播放ID
$flag = $_GET['flag'] ?? ''; // 播放线路
$filter = isset($_GET['filter']) && $_GET['filter'] === 'true'; // 是否过滤
$extend = $_GET['ext'] ?? ''; // 扩展参数
if (!empty($extend) && is_string($extend)) {
$decoded = json_decode(base64_decode($extend), true);
if (is_array($decoded)) {
$extend = $decoded;
}
}
$action = $_GET['action'] ?? ''; // Action 动作
$value = $_GET['value'] ?? ''; // Action 参数
$this->init($extend);
try {
// 0. Action (优先处理)
if ($ac === 'action') {
echo json_encode($this->action($action, $value), JSON_UNESCAPED_UNICODE);
return;
}
// 1. 播放 (Play)
// 优先检测 play 参数或 ac=play
if ($ac === 'play' || !empty($play)) {
$playId = !empty($play) ? $play : ($_GET['id'] ?? '');
echo json_encode($this->playContent($flag, $playId), JSON_UNESCAPED_UNICODE);
return;
}
// 2. 搜索 (Search)
// 有 wd 则是搜索
if (!empty($wd)) {
echo json_encode($this->searchContent($wd, false, $pg), JSON_UNESCAPED_UNICODE);
return;
}
// 3. 详情 (Detail)
// 有 ids 且 ac 不为空
if (!empty($ids) && !empty($ac)) {
// ids 可能是逗号分隔的字符串
$idList = explode(',', $ids);
echo json_encode($this->detailContent($idList), JSON_UNESCAPED_UNICODE);
return;
}
// 4. 分类 (Category)
// 有 t 且 ac 不为空
if ($t !== '' && !empty($ac)) {
// 处理 filter
$filterData = []; // 暂未实现复杂 filter 解析,可根据需要扩展
echo json_encode($this->categoryContent($t, $pg, $filterData, $extend), JSON_UNESCAPED_UNICODE);
return;
}
// 5. 首页 (默认)
// 通常返回 {class: [...], list: [...]}
// 可以分别调用 homeContent 和 homeVideoContent 合并
$homeData = $this->homeContent($filter);
$videoData = $this->homeVideoContent();
$result = [
'class' => $homeData['class'] ?? [],
];
// 如果 homeContent 只有 class,合并 homeVideoContent 的 list
if (isset($videoData['list'])) {
$result['list'] = $videoData['list'];
}
// 如果 homeContent 也有 list,优先使用 homeContent 的 list (视具体逻辑而定,这里简单的合并)
if (isset($homeData['list']) && !empty($homeData['list'])) {
$result['list'] = $homeData['list'];
}
// 兼容:如果 homeContent 返回了 filters
if (isset($homeData['filters'])) {
$result['filters'] = $homeData['filters'];
}
echo json_encode($result, JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
echo json_encode(['code' => 500, 'msg' => $e->getMessage()], JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
echo json_encode(['code' => 500, 'msg' => $e->getMessage()], JSON_UNESCAPED_UNICODE);
}
}
}