forked from tfornik/RussiaTools
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.
319 lines
9.8 KiB
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);
|
|
}
|
|
}
|
|
}
|
|
|