Files
a/scripts/spider.php
2026-03-24 18:40:17 +08:00

320 lines
9.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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);
}
}
}