This commit is contained in:
2026-03-24 18:40:17 +08:00
parent a53ca2fa61
commit 82656f8f2a
637 changed files with 3306118 additions and 0 deletions

300
wwwroot/batch_test.php Normal file
View File

@@ -0,0 +1,300 @@
<?php
// batch_test.php
// 批量测试目录下所有 Spider 插件
// 访问: http://127.0.0.1:9980/batch_test.php
// 参数: ?format=json 返回 JSON 格式, 否则返回可读文本
ini_set('display_errors', 1);
error_reporting(E_ALL);
date_default_timezone_set('Asia/Shanghai');
// 输出格式
$format = $_GET['format'] ?? 'text';
$isJson = $format === 'json';
// 排除的系统文件
$excludeFiles = [
'index.php',
'config.php',
'spider.php',
'example_t4.php',
'test_runner.php',
'batch_test.php'
];
// 获取当前目录下所有 PHP 文件
$dir = __DIR__;
$files = scandir($dir);
$spiderFiles = [];
foreach ($files as $file) {
if (pathinfo($file, PATHINFO_EXTENSION) !== 'php') continue;
if (in_array($file, $excludeFiles)) continue;
$spiderFiles[] = $file;
}
// 测试结果
$results = [];
$summary = [
'total' => count($spiderFiles),
'passed' => 0,
'failed' => 0,
'skipped' => 0,
'start_time' => date('Y-m-d H:i:s'),
];
if (!$isJson) {
header('Content-Type: text/plain; charset=utf-8');
echo "╔══════════════════════════════════════════════════════════════╗\n";
echo "║ PHP Spider 批量测试工具 v1.0 ║\n";
echo "╚══════════════════════════════════════════════════════════════╝\n\n";
echo "📁 扫描目录: $dir\n";
echo "📄 发现 " . count($spiderFiles) . " 个待测试文件\n";
echo "⏰ 开始时间: " . $summary['start_time'] . "\n";
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n";
}
foreach ($spiderFiles as $index => $file) {
$filePath = $dir . DIRECTORY_SEPARATOR . $file;
$testResult = [
'file' => $file,
'status' => 'unknown',
'tests' => [],
'error' => null,
'total_time' => 0,
];
$fileStartTime = microtime(true);
if (!$isJson) {
$num = $index + 1;
echo "┌─────────────────────────────────────────────────────────────┐\n";
echo "│ [$num/" . count($spiderFiles) . "] 测试文件: $file\n";
echo "└─────────────────────────────────────────────────────────────┘\n";
}
try {
// 使用输出缓冲捕获 require 过程中的输出
ob_start();
// 使用独立的命名空间避免类冲突
$tempFile = $dir . DIRECTORY_SEPARATOR . '.temp_test_' . uniqid() . '.php';
$wrapperCode = '<?php
namespace TestNS' . uniqid() . ';
' . file_get_contents($filePath) . '
';
// 由于命名空间会影响类名,我们改用不同的方法
// 直接 require但先检查 Spider 类是否已存在
ob_end_clean();
// 简单处理:如果 Spider 类已存在,跳过
if (class_exists('Spider', false)) {
// 重新定义类会报错,所以需要在新进程中测试
// 这里我们通过 HTTP 调用每个脚本的首页接口来测试
$testResult['status'] = 'http_test';
// 通过 HTTP 请求测试
$host = $_SERVER['HTTP_HOST'] ?? '127.0.0.1:9980';
$testUrl = "http://$host/$file?filter=true";
if (!$isJson) {
echo " 📡 使用 HTTP 模式测试...\n";
echo " 🔗 URL: $testUrl\n";
}
// 测试首页接口
$ctx = stream_context_create([
'http' => [
'timeout' => 15,
'ignore_errors' => true,
]
]);
$startTime = microtime(true);
$response = @file_get_contents($testUrl, false, $ctx);
$cost = round((microtime(true) - $startTime) * 1000, 2);
if ($response !== false) {
$data = json_decode($response, true);
if (json_last_error() === JSON_ERROR_NONE) {
$classes = $data['class'] ?? [];
$list = $data['list'] ?? [];
$testResult['tests']['home'] = [
'status' => !empty($classes) ? 'pass' : 'warn',
'time' => $cost,
'classes' => count($classes),
'list' => count($list),
];
if (!$isJson) {
if (!empty($classes)) {
echo " ✅ 首页接口: 通过 (分类: " . count($classes) . ", 耗时: {$cost}ms)\n";
} else {
echo " ⚠️ 首页接口: 无分类 (list: " . count($list) . ", 耗时: {$cost}ms)\n";
}
}
// 如果有分类,继续测试分类接口
if (!empty($classes)) {
$tid = $classes[0]['type_id'] ?? null;
if ($tid) {
$catUrl = "http://$host/$file?t=$tid&ac=detail&pg=1";
$startTime = microtime(true);
$catResponse = @file_get_contents($catUrl, false, $ctx);
$catCost = round((microtime(true) - $startTime) * 1000, 2);
if ($catResponse !== false) {
$catData = json_decode($catResponse, true);
$catList = $catData['list'] ?? [];
$testResult['tests']['category'] = [
'status' => !empty($catList) ? 'pass' : 'fail',
'time' => $catCost,
'count' => count($catList),
];
if (!$isJson) {
if (!empty($catList)) {
echo " ✅ 分类接口: 通过 (数据: " . count($catList) . " 条, 耗时: {$catCost}ms)\n";
} else {
echo " ❌ 分类接口: 无数据 (耗时: {$catCost}ms)\n";
}
}
// 如果有数据,继续测试详情接口
if (!empty($catList)) {
$vodId = $catList[0]['vod_id'] ?? null;
if ($vodId) {
$detailUrl = "http://$host/$file?ac=detail&ids=" . urlencode($vodId);
$startTime = microtime(true);
$detailResponse = @file_get_contents($detailUrl, false, $ctx);
$detailCost = round((microtime(true) - $startTime) * 1000, 2);
if ($detailResponse !== false) {
$detailData = json_decode($detailResponse, true);
$detailList = $detailData['list'] ?? [];
$hasPlayUrl = !empty($detailList[0]['vod_play_url'] ?? '');
$testResult['tests']['detail'] = [
'status' => !empty($detailList) ? ($hasPlayUrl ? 'pass' : 'warn') : 'fail',
'time' => $detailCost,
'has_play_url' => $hasPlayUrl,
];
if (!$isJson) {
if (!empty($detailList)) {
$name = $detailList[0]['vod_name'] ?? '未知';
if ($hasPlayUrl) {
echo " ✅ 详情接口: 通过 ($name, 耗时: {$detailCost}ms)\n";
} else {
echo " ⚠️ 详情接口: 无播放链接 ($name, 耗时: {$detailCost}ms)\n";
}
} else {
echo " ❌ 详情接口: 无数据 (耗时: {$detailCost}ms)\n";
}
}
}
}
}
}
}
}
$testResult['status'] = 'pass';
$summary['passed']++;
} else {
$testResult['status'] = 'fail';
$testResult['error'] = 'JSON 解析失败: ' . json_last_error_msg();
$summary['failed']++;
if (!$isJson) {
echo " ❌ 响应解析失败: " . json_last_error_msg() . "\n";
}
}
} else {
$testResult['status'] = 'fail';
$testResult['error'] = 'HTTP 请求失败';
$summary['failed']++;
if (!$isJson) {
echo " ❌ HTTP 请求失败\n";
}
}
} else {
// Spider 类不存在,直接 require 测试
ob_start();
require_once $filePath;
ob_end_clean();
if (!class_exists('Spider')) {
throw new Exception("未找到 Spider 类");
}
$spider = new Spider();
$spider->init();
// 测试首页
$startTime = microtime(true);
$home = $spider->homeContent(true);
$cost = round((microtime(true) - $startTime) * 1000, 2);
$classes = $home['class'] ?? [];
$testResult['tests']['home'] = [
'status' => !empty($classes) ? 'pass' : 'warn',
'time' => $cost,
'classes' => count($classes),
];
if (!$isJson) {
if (!empty($classes)) {
echo " ✅ 首页接口: 通过 (分类: " . count($classes) . ", 耗时: {$cost}ms)\n";
} else {
echo " ⚠️ 首页接口: 无分类 (耗时: {$cost}ms)\n";
}
}
$testResult['status'] = 'pass';
$summary['passed']++;
}
} catch (Throwable $e) {
$testResult['status'] = 'error';
$testResult['error'] = $e->getMessage();
$summary['failed']++;
if (!$isJson) {
echo " ⛔ 错误: " . $e->getMessage() . "\n";
}
}
$testResult['total_time'] = round((microtime(true) - $fileStartTime) * 1000, 2);
$results[] = $testResult;
if (!$isJson) {
echo " ⏱️ 总耗时: " . $testResult['total_time'] . "ms\n";
echo "\n";
}
}
$summary['end_time'] = date('Y-m-d H:i:s');
if ($isJson) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'summary' => $summary,
'results' => $results,
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
} else {
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
echo "📊 测试汇总\n";
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
echo " 📄 总文件数: " . $summary['total'] . "\n";
echo " ✅ 通过: " . $summary['passed'] . "\n";
echo " ❌ 失败: " . $summary['failed'] . "\n";
echo " ⏭️ 跳过: " . $summary['skipped'] . "\n";
echo " ⏰ 结束时间: " . $summary['end_time'] . "\n";
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
}

47
wwwroot/config.php Normal file
View File

@@ -0,0 +1,47 @@
<?php
// 设置返回为 JSON
// http://127.0.0.1:9980/config.php
header('Content-Type: application/json; charset=utf-8');
// 当前目录
$dir = __DIR__;
// 当前脚本名
$self = basename(__FILE__);
// 扫描目录
$files = scandir($dir);
$sites = [];
foreach ($files as $file) {
// 只处理 php 文件
if (pathinfo($file, PATHINFO_EXTENSION) !== 'php') {
continue;
}
// 排除自身和 index.php
if ($file === $self || $file === 'index.php') {
continue;
}
// 文件名(不含 .php
$filename = pathinfo($file, PATHINFO_FILENAME);
$sites[] = [
"key" => "php_" . $filename,
"name" => $filename . "(PHP)",
"type" => 4,
"api" => "http://127.0.0.1:9980/" . $filename . ".php",
"searchable" => 1,
"quickSearch" => 1,
"changeable" => 0
];
}
// 输出 JSON
echo json_encode(
["sites" => $sites],
JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT
);

182
wwwroot/example_t4.php Normal file
View File

@@ -0,0 +1,182 @@
<?php
/**
* T4 爬虫示例脚本 - Android 版本
*
* 演示 T4 类型爬虫的标准接口实现
* 这是一个模板,您可以基于此开发自己的爬虫
*/
header('Content-Type: application/json; charset=utf-8');
// 获取请求参数
$filter = $_GET['filter'] ?? null;
$ac = $_GET['ac'] ?? null;
$t = $_GET['t'] ?? null;
$pg = $_GET['pg'] ?? '1';
$ids = $_GET['ids'] ?? null;
$wd = $_GET['wd'] ?? null;
$flag = $_GET['flag'] ?? null;
$play = $_GET['play'] ?? null;
$ext = $_GET['ext'] ?? null;
// 解码 ext 参数Base64 编码的 JSON
$extData = [];
if ($ext) {
$extJson = base64_decode($ext);
if ($extJson) {
$extData = json_decode($extJson, true) ?: [];
}
}
// ============================================================================
// 首页/分类接口
// ============================================================================
if ($filter !== null) {
echo json_encode([
'class' => [
['type_id' => '1', 'type_name' => '电影'],
['type_id' => '2', 'type_name' => '电视剧'],
['type_id' => '3', 'type_name' => '综艺'],
['type_id' => '4', 'type_name' => '动漫'],
],
'filters' => [
'1' => [
[
'key' => 'year',
'name' => '年份',
'value' => [
['n' => '全部', 'v' => ''],
['n' => '2024', 'v' => '2024'],
['n' => '2023', 'v' => '2023'],
['n' => '2022', 'v' => '2022'],
]
],
[
'key' => 'area',
'name' => '地区',
'value' => [
['n' => '全部', 'v' => ''],
['n' => '大陆', 'v' => '大陆'],
['n' => '香港', 'v' => '香港'],
['n' => '美国', 'v' => '美国'],
]
]
]
]
], JSON_UNESCAPED_UNICODE);
exit;
}
// ============================================================================
// 分类列表
// ============================================================================
if ($ac === 'detail' && $t !== null) {
$page = (int)$pg;
$pageSize = 20;
// 模拟数据
$list = [];
for ($i = 1; $i <= $pageSize; $i++) {
$id = ($page - 1) * $pageSize + $i;
$list[] = [
'vod_id' => (string)$id,
'vod_name' => "示例影片 $id",
'vod_pic' => 'https://via.placeholder.com/300x400',
'vod_remarks' => '第' . rand(1, 20) . '集',
'vod_year' => (string)(2020 + rand(0, 4)),
];
}
echo json_encode([
'page' => $page,
'pagecount' => 10,
'limit' => $pageSize,
'total' => 200,
'list' => $list
], JSON_UNESCAPED_UNICODE);
exit;
}
// ============================================================================
// 详情接口
// ============================================================================
if ($ac === 'detail' && $ids !== null) {
echo json_encode([
'list' => [
[
'vod_id' => $ids,
'vod_name' => '示例电影',
'vod_pic' => 'https://via.placeholder.com/300x400',
'vod_year' => '2024',
'vod_area' => '中国',
'vod_director' => '导演名',
'vod_actor' => '演员A,演员B,演员C',
'vod_content' => '这是一部精彩的示例电影,讲述了一个引人入胜的故事...',
'vod_play_from' => '线路一$$$线路二$$$线路三',
'vod_play_url' => implode('$$$', [
'第1集$https://example.com/ep1.m3u8#第2集$https://example.com/ep2.m3u8#第3集$https://example.com/ep3.m3u8',
'第1集$https://backup1.com/ep1.m3u8#第2集$https://backup1.com/ep2.m3u8#第3集$https://backup1.com/ep3.m3u8',
'第1集$https://backup2.com/ep1.m3u8#第2集$https://backup2.com/ep2.m3u8#第3集$https://backup2.com/ep3.m3u8',
])
]
]
], JSON_UNESCAPED_UNICODE);
exit;
}
// ============================================================================
// 搜索接口
// ============================================================================
if ($wd !== null) {
$results = [];
for ($i = 1; $i <= 10; $i++) {
$results[] = [
'vod_id' => (string)(1000 + $i),
'vod_name' => "搜索结果: $wd ($i)",
'vod_pic' => 'https://via.placeholder.com/300x400',
'vod_remarks' => 'HD',
'vod_year' => '2024',
];
}
echo json_encode([
'page' => 1,
'pagecount' => 1,
'list' => $results
], JSON_UNESCAPED_UNICODE);
exit;
}
// ============================================================================
// 播放解析
// ============================================================================
if ($flag !== null && $play !== null) {
// 这里可以实现实际的解析逻辑
// 例如:调用第三方解析接口、提取真实播放地址等
echo json_encode([
'parse' => 0, // 0=直链, 1=需要解析
'url' => $play, // 直接返回原始 URL
'header' => [
'User-Agent' => 'Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36',
'Referer' => 'https://example.com/'
]
], JSON_UNESCAPED_UNICODE);
exit;
}
// ============================================================================
// 默认响应
// ============================================================================
echo json_encode([
'error' => '未知请求',
'params' => $_GET,
'info' => [
'name' => 'T4 示例爬虫',
'version' => '1.0.0',
'platform' => 'Android',
'php_version' => PHP_VERSION
]
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);

9
wwwroot/img.php Normal file
View File

@@ -0,0 +1,9 @@
<?php
/**
* PHP随机图显示
*/
header('Content-Type: text/html; charset=UTF-8');
$img_array = glob("./img/*.jpg",GLOB_BRACE);
$img = array_rand($img_array);
header("location:.$img_array[$img]");
?>

16
wwwroot/index.php Normal file
View File

@@ -0,0 +1,16 @@
<?php
/**
* PHP 服务状态检测 - Android 版本
*/
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'status' => 'ok',
'message' => 'PHP 服务运行正常',
'version' => PHP_VERSION,
'platform' => 'Android',
'time' => date('Y-m-d H:i:s'),
'extensions' => get_loaded_extensions()
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);

319
wwwroot/spider.php Normal file
View File

@@ -0,0 +1,319 @@
<?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);
}
}
}

248
wwwroot/test_runner.php Normal file
View File

@@ -0,0 +1,248 @@
<?php
// test_runner.php
// 这是一个用于测试 Spider 插件接口的脚本
// 用法: php test_runner.php [插件文件路径]
ini_set('display_errors', 1);
error_reporting(E_ALL);
// 设置默认时区,避免时间相关函数警告
date_default_timezone_set('Asia/Shanghai');
$file = $argv[1] ?? '';
if (!$file || !file_exists($file)) {
die("错误: 未找到文件 '$file'\n用法: php test_runner.php [插件文件路径]\n");
}
echo "==================================================\n";
echo "正在测试文件: $file\n";
echo "==================================================\n";
try {
// 使用输出缓冲捕获 require 过程中可能的输出(如 (new Spider())->run()
// 防止污染后续的测试输出
ob_start();
require_once $file;
ob_end_clean();
if (!class_exists('Spider')) {
die("错误: 在文件 '$file' 中未找到 'Spider' 类\n");
}
echo "[初始化] 实例化 Spider 类...\n";
$spider = new Spider();
$spider->init();
echo "[初始化] 完成\n\n";
// --- 1. 测试首页接口 (Home Interface) ---
echo ">>> [1/5] 测试首页接口 (homeContent)\n";
$startTime = microtime(true);
$home = $spider->homeContent(true);
$cost = round((microtime(true) - $startTime) * 1000, 2);
$classes = $home['class'] ?? [];
$filters = $home['filters'] ?? [];
if (!empty($classes)) {
echo " ✅ 通过 (耗时: {$cost}ms)\n";
echo " - 获取到 " . count($classes) . " 个分类\n";
// 打印前几个分类名称作为示例
$classNames = array_column(array_slice($classes, 0, 5), 'type_name');
echo " - 分类示例: " . implode(', ', $classNames) . (count($classes) > 5 ? ' ...' : '') . "\n";
if (!empty($filters)) {
echo " - 包含筛选配置 (Filters): " . count($filters) . "\n";
}
} else {
echo " ⚠️ 警告: 未获取到分类列表 (class 为空)\n";
}
// 确定用于测试分类接口的 type_id
$tid = $classes[0]['type_id'] ?? null;
$tname = $classes[0]['type_name'] ?? '未知分类';
if (!$tid && !empty($filters)) {
// 如果 class 为空但有 filters尝试从 filters 获取 key
foreach ($filters as $key => $val) {
$tid = $key;
$tname = "FilterKey:$key";
break;
}
}
echo "\n";
// --- 2. 测试分类接口 (Category Interface) ---
$vodId = null;
$vodName = null; // 用于搜索测试
if ($tid) {
echo ">>> [2/5] 测试分类接口 (categoryContent) - 测试分类: [$tname] (ID: $tid)\n";
$startTime = microtime(true);
// 模拟传入 filter 参数为空
$cat = $spider->categoryContent($tid, 1, false, []);
$cost = round((microtime(true) - $startTime) * 1000, 2);
$list = $cat['list'] ?? [];
if (!empty($list)) {
echo " ✅ 通过 (耗时: {$cost}ms)\n";
echo " - 获取到 " . count($list) . " 个资源\n";
$firstItem = $list[0];
$vodId = $firstItem['vod_id'] ?? null;
$vodName = $firstItem['vod_name'] ?? '未知名称';
echo " - 第一条数据: [$vodName] (ID: $vodId)\n";
} else {
echo " ❌ 失败: 未返回资源列表 (list 为空)\n";
}
} else {
echo ">>> [2/5] 测试分类接口: ⏭️ 跳过 (未找到有效的分类ID)\n";
}
echo "\n";
// --- 3. 测试详情接口 (Detail Interface) ---
$playUrl = null;
$playFrom = null;
if ($vodId) {
echo ">>> [3/5] 测试详情接口 (detailContent) - 测试资源ID: $vodId\n";
$startTime = microtime(true);
$detail = $spider->detailContent([$vodId]);
$cost = round((microtime(true) - $startTime) * 1000, 2);
$detailList = $detail['list'] ?? [];
if (!empty($detailList)) {
$vod = $detailList[0];
$name = $vod['vod_name'] ?? '未知';
// 更新 vodName详情页的名称通常更准确
if ($name && $name !== '未知') {
$vodName = $name;
}
$playUrl = $vod['vod_play_url'] ?? '';
$playFrom = $vod['vod_play_from'] ?? '';
$desc = $vod['vod_content'] ?? '';
echo " ✅ 通过 (耗时: {$cost}ms)\n";
echo " - 资源名称: $name\n";
echo " - 播放源 (vod_play_from): $playFrom\n";
// 检查播放地址
if (!empty($playUrl)) {
$urlCount = substr_count($playUrl, '$');
// 粗略估计集数,通常每集是 名称$url
$episodeCount = $urlCount > 0 ? ($urlCount + 1) / 2 : 1;
// 或者直接按 # 分割统计播放列表数
$playlistCount = substr_count($playFrom, '$$$') + 1;
echo " - 播放列表数据长度: " . strlen($playUrl) . " 字符\n";
// 简单展示部分播放链接
$previewUrl = mb_substr($playUrl, 0, 50) . '...';
echo " - 播放链接预览: $previewUrl\n";
} else {
echo " ⚠️ 警告: vod_play_url 为空!\n";
}
if (!empty($desc)) {
echo " - 简介长度: " . mb_strlen($desc) . "\n";
}
} else {
echo " ❌ 失败: 未返回详情数据\n";
}
} else {
echo ">>> [3/5] 测试详情接口: ⏭️ 跳过 (未找到有效的资源ID)\n";
}
echo "\n";
// --- 4. 测试搜索接口 (Search Interface) ---
// 使用之前获取到的 vodName 进行搜索,如果没有则使用默认关键词 "爱"
$searchKey = $vodName ?: "";
echo ">>> [4/5] 测试搜索接口 (searchContent) - 关键词: [$searchKey]\n";
try {
$startTime = microtime(true);
$searchRes = $spider->searchContent($searchKey, false, 1);
$cost = round((microtime(true) - $startTime) * 1000, 2);
$searchList = $searchRes['list'] ?? [];
if (!empty($searchList)) {
echo " ✅ 通过 (耗时: {$cost}ms)\n";
echo " - 搜索到 " . count($searchList) . " 个结果\n";
$firstSearch = $searchList[0];
echo " - 第一条结果: " . ($firstSearch['vod_name'] ?? '未知') . "\n";
} else {
echo " ⚠️ 警告: 搜索未返回结果 (但这不代表接口错误)\n";
}
} catch (Throwable $e) {
echo " ⚠️ 异常: 搜索接口调用失败 (允许失败)\n";
echo " 错误信息: " . $e->getMessage() . "\n";
}
echo "\n";
// --- 5. 测试播放接口 (Player Interface) ---
if ($playUrl && $playFrom) {
// 解析播放链接,取第一组的第一个链接
// 格式通常是: 播放源1$$$集数1$链接1#集数2$链接2...$$$播放源2...
// 或者是: 集数1$链接1#集数2$链接2...
// 简单处理:先按 $$$ 分割取第一个播放源对应的链接串
$playUrls = explode('$$$', $playUrl);
$currentUrlBlock = $playUrls[0] ?? '';
// 再按 # 分割取第一集
$episodes = explode('#', $currentUrlBlock);
$firstEp = $episodes[0] ?? '';
// 再按 $ 分割取链接 (通常是 名称$链接)
$parts = explode('$', $firstEp);
$targetUrl = end($parts); // 取最后一部分作为链接
// 播放源flag
$playFroms = explode('$$$', $playFrom);
$flag = $playFroms[0] ?? 'default';
echo ">>> [5/5] 测试播放接口 (playerContent) - Flag: [$flag]\n";
echo " - 目标链接: $targetUrl\n";
try {
$startTime = microtime(true);
// $flag, $id, $vipFlags
$playerRes = $spider->playerContent($flag, $targetUrl, []);
$cost = round((microtime(true) - $startTime) * 1000, 2);
if (!empty($playerRes)) {
echo " ✅ 通过 (耗时: {$cost}ms)\n";
// 打印返回的关键字段
$parse = $playerRes['parse'] ?? 'N/A';
$url = $playerRes['url'] ?? 'N/A';
$header = $playerRes['header'] ?? 'N/A';
echo " - Parse: $parse\n";
echo " - PlayUrl: $url\n";
if (is_array($header)) {
echo " - Header: " . json_encode($header, JSON_UNESCAPED_UNICODE) . "\n";
}
} else {
echo " ⚠️ 警告: 播放接口返回为空\n";
}
} catch (Throwable $e) {
echo " ⚠️ 异常: 播放接口调用失败 (允许失败)\n";
echo " 错误信息: " . $e->getMessage() . "\n";
}
} else {
echo ">>> [5/5] 测试播放接口: ⏭️ 跳过 (未获取到有效的播放链接或播放源信息)\n";
}
} catch (Throwable $e) {
echo "\n⛔ 严重错误 (CRITICAL ERROR):\n";
echo " 信息: " . $e->getMessage() . "\n";
echo " 位置: " . $e->getFile() . "" . $e->getLine() . "\n";
echo " 堆栈:\n" . $e->getTraceAsString() . "\n";
}
echo "==================================================\n";
echo "测试结束\n";

File diff suppressed because it is too large Load Diff

1028
wwwroot/自动接口.php Normal file

File diff suppressed because it is too large Load Diff

318
wwwroot/色色.php Normal file
View File

@@ -0,0 +1,318 @@
<?php
require_once __DIR__ . '/lib/spider.php';
class Spider extends BaseSpider {
private $host;
private $customHeaders;
public function getName() {
return "复古片";
}
public function init($extend = "") {
$this->host = "https://vintagepornfun.com";
$this->customHeaders = array(
"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",
"Referer" => $this->host,
"Origin" => $this->host,
"Connection" => "keep-alive"
);
}
private function _fetch($url, $headers = null) {
$reqHeaders = $headers ? $headers : $this->customHeaders;
$html = $this->fetch($url, array('headers' => $reqHeaders));
return $html;
}
private function _generateRandomString($length = 10) {
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$charactersLength = strlen($characters);
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters[rand(0, $charactersLength - 1)];
}
return $randomString;
}
private function _resolve_myvidplay($url) {
try {
$embed = str_replace("/d/", "/e/", $url);
if (strpos($embed, 'd000d.com') !== false) {
$embed = str_replace('d000d.com', 'myvidplay.com', $embed);
}
if (strpos($embed, 'doood.com') !== false) {
$embed = str_replace('doood.com', 'myvidplay.com', $embed);
}
$parsedUrl = parse_url($embed);
$host = $parsedUrl['scheme'] . '://' . $parsedUrl['host'];
$hReq = array(
"User-Agent" => $this->customHeaders['User-Agent'],
"Referer" => $this->host
);
$r = $this->fetch($embed, array('headers' => $hReq));
if (!$r) {
return array('parse' => 1, 'url' => $url, 'header' => $hReq);
}
$m = array();
preg_match('/\/pass_md5\/[^\'"]+/', $r, $m);
if (empty($m)) {
return array('parse' => 1, 'url' => $url, 'header' => $hReq);
}
$hReq["Referer"] = $embed;
$prefix = trim($this->fetch($host . $m[0], array('headers' => $hReq)));
if (substr($prefix, 0, 4) !== "http") {
return array('parse' => 1, 'url' => $url, 'header' => $hReq);
}
$parts = explode("/", $m[0]);
$token = end($parts);
$rnd = $this->_generateRandomString(10);
$finalHeaders = array(
'User-Agent' => $this->customHeaders['User-Agent'],
'Referer' => $host . '/',
'Connection' => 'keep-alive'
);
return array(
'parse' => 0,
'url' => $prefix . $rnd . '?token=' . $token,
'header' => $finalHeaders
);
} catch (Exception $e) {
return array('parse' => 1, 'url' => $url, 'header' => $this->customHeaders);
}
}
public function homeContent($filter) {
$classes = array(
array("type_name" => "最新更新", "type_id" => "latest"),
array("type_name" => "70年代", "type_id" => "70s-porn"),
array("type_name" => "80年代", "type_id" => "80s-porn"),
array("type_name" => "亚洲经典", "type_id" => "asian-vintage-porn"),
array("type_name" => "欧洲经典", "type_id" => "euro-porn-movies"),
array("type_name" => "日本经典", "type_id" => "japanese-vintage-porn"),
array("type_name" => "法国经典", "type_id" => "french-vintage-porn"),
array("type_name" => "德国经典", "type_id" => "german-vintage-porn"),
array("type_name" => "意大利经典", "type_id" => "italian-vintage-porn"),
array("type_name" => "经典影片", "type_id" => "classic-porn-movies")
);
$sortConf = array(
"key" => "order",
"name" => "排序",
"value" => array(
array("n" => "默认", "v" => ""),
array("n" => "最新", "v" => "date"),
array("n" => "随机", "v" => "rand"),
array("n" => "标题", "v" => "title"),
array("n" => "热度", "v" => "comment_count")
)
);
$tagConf = array(
"key" => "tag",
"name" => "标签",
"value" => array(
array("n" => "全部", "v" => ""),
array("n" => "70年代", "v" => "70s-porn"),
array("n" => "80年代", "v" => "80s-porn"),
array("n" => "90年代", "v" => "90s-porn"),
array("n" => "肛交", "v" => "anal-sex"),
array("n" => "亚洲", "v" => "asian"),
array("n" => "大胸", "v" => "big-boobs"),
array("n" => "金发", "v" => "blonde"),
array("n" => "经典", "v" => "classic"),
array("n" => "喜剧", "v" => "comedy"),
array("n" => "绿帽", "v" => "cuckold"),
array("n" => "黑人", "v" => "ebony"),
array("n" => "欧洲", "v" => "european"),
array("n" => "法国", "v" => "french"),
array("n" => "德国", "v" => "german"),
array("n" => "群交", "v" => "group-sex"),
array("n" => "多毛", "v" => "hairy-porn"),
array("n" => "跨种族", "v" => "interracial"),
array("n" => "意大利", "v" => "italian"),
array("n" => "女同", "v" => "lesbian"),
array("n" => "熟女", "v" => "milf"),
array("n" => "乱交", "v" => "orgy"),
array("n" => "户外", "v" => "public-sex"),
array("n" => "复古", "v" => "retro"),
array("n" => "少女", "v" => "teen-sex"),
array("n" => "3P", "v" => "threesome"),
array("n" => "老片", "v" => "vintage-porn"),
array("n" => "偷窥", "v" => "voyeur")
)
);
$filters = array();
foreach ($classes as $item) {
$filters[$item['type_id']] = array($sortConf, $tagConf);
}
return array("class" => $classes, "filters" => $filters);
}
public function homeVideoContent() {
return array("list" => array());
}
public function categoryContent($tid, $pg = 1, $filter = array(), $extend = array()) {
if ($tid == "latest") {
$url = ($pg == 1) ? $this->host : $this->host . "/page/" . $pg . "/";
} else {
$base = $this->host . "/category/" . $tid;
$url = ($pg == 1) ? $base . "/" : $base . "/page/" . $pg . "/";
}
$queryParts = array();
if (isset($extend['order']) && $extend['order']) {
$queryParts[] = "orderby=" . $extend['order'];
}
if (isset($extend['tag']) && $extend['tag']) {
$queryParts[] = "tag=" . $extend['tag'];
}
if (!empty($queryParts)) {
$sep = strpos($url, '?') !== false ? '&' : '?';
$url .= $sep . implode('&', $queryParts);
}
return $this->_get_list($url, intval($pg));
}
private function _get_list($url, $page = 1) {
$videos = array();
$html = $this->_fetch($url);
if ($html) {
$articles = array();
preg_match_all('/<article[^>]*>(.*?)<\/article>/s', $html, $articles);
if (isset($articles[1])) {
foreach ($articles[1] as $item) {
$aMatch = array();
if (!preg_match('/<a[^>]*href=["\']([^"\']+)["\']/', $item, $aMatch)) {
continue;
}
$href = $aMatch[1];
$pic = "";
$imgMatch = array();
if (preg_match('/<img[^>]*data-src=["\']([^"\']+)["\']/', $item, $imgMatch)) {
$pic = $imgMatch[1];
} elseif (preg_match('/<img[^>]*src=["\']([^"\']+)["\']/', $item, $imgMatch)) {
$pic = $imgMatch[1];
}
if ($pic && substr($pic, 0, 4) !== "http") {
$pic = $this->host . $pic;
}
$name = "";
$headMatch = array();
if (preg_match('/class="entry-header"[^>]*>(.*?)<\/div>/s', $item, $headMatch)) {
$name = strip_tags($headMatch[1]);
} else {
$titleMatch = array();
if (preg_match('/title=["\']([^"\']+)["\']/', $item, $titleMatch)) {
$name = $titleMatch[1];
}
}
$name = trim($name);
$remarks = "";
$remMatch = array();
if (preg_match('/class="rating-bar"[^>]*>(.*?)<\/div>/s', $item, $remMatch)) {
$remarks = trim(strip_tags($remMatch[1]));
}
$videos[] = array(
"vod_id" => $href,
"vod_name" => $name ? $name : "",
"vod_pic" => $pic,
"vod_remarks" => $remarks
);
}
}
}
$pagecount = (!empty($videos) ? $page + 1 : $page);
return array(
"list" => $videos,
"page" => $page,
"pagecount" => $pagecount,
"limit" => 20,
"total" => 999
);
}
public function detailContent($ids) {
$html = $this->_fetch($ids[0]);
if (!$html) {
return array("list" => array());
}
$metaImg = "";
$metaMatch = array();
if (preg_match('/<meta[^>]*property="og:image"[^>]*content=["\']([^"\']+)["\']/', $html, $metaMatch)) {
$metaImg = $metaMatch[1];
}
$metaDesc = "";
if (preg_match('/<meta[^>]*property="og:description"[^>]*content=["\']([^"\']+)["\']/', $html, $metaMatch)) {
$metaDesc = $metaMatch[1];
}
$name = "";
$h1Match = array();
if (preg_match('/<h1[^>]*>(.*?)<\/h1>/s', $html, $h1Match)) {
$name = trim(strip_tags($h1Match[1]));
}
$playUrl = "";
$m = array();
if (preg_match('/src=["\'](https?:\/\/(?:[^"\']*(?:d000d|doood|myvidplay)\.[a-z]+)\/e\/[a-zA-Z0-9]+)/i', $html, $m)) {
$playUrl = $m[1];
} else {
$iframeMatch = array();
if (preg_match('/<iframe[^>]*src=["\']([^"\']*\/e\/[^"\']+)["\']/', $html, $iframeMatch)) {
$playUrl = $iframeMatch[1];
}
}
$vodPlayUrl = $playUrl ? 'HD$' . $playUrl : '无资源$#';
$result = array(
"list" => array(
array(
"vod_id" => $ids[0],
"vod_name" => $name,
"vod_pic" => $metaImg,
"vod_content" => $metaDesc,
"vod_play_from" => "文艺复兴",
"vod_play_url" => $vodPlayUrl
)
)
);
return $result;
}
public function searchContent($key, $quick = false, $pg = 1) {
return $this->_get_list($this->host . "/page/" . $pg . "/?s=" . urlencode($key), intval($pg));
}
public function playerContent($flag, $id, $vipFlags = array()) {
if ($flag == 'myvidplay' || strpos($id, 'myvidplay') !== false || strpos($id, 'd000d') !== false || strpos($id, 'doood') !== false) {
return $this->_resolve_myvidplay($id);
}
return array("parse" => 1, "url" => $id, "header" => $this->customHeaders);
}
}
(new Spider())->run();

402
wwwroot/虎牙直播.php Normal file
View File

@@ -0,0 +1,402 @@
<?php
require_once __DIR__ . '/lib/spider.php';
class HuyaSpider extends BaseSpider
{
private const TITLE = "虎牙直播";
private const HOST = "https://www.huya.com";
private const HOME_URL = "/cache.php?m=LiveList&do=getLiveListByPage&gameId=2168&tagAll=0&page=1";
private const CATEGORY_URL_TEMPLATE = "/cache.php?m=LiveList&do=getLiveListByPage&gameId=fyfilter&tagAll=0&page=fypage";
private const ROOM_INFO_URL = "https://mp.huya.com/cache.php?m=Live&do=profileRoom&roomid=";
private const SEARCH_URL = "https://search.cdn.huya.com/?m=Search&do=getSearchContent&q=**&uid=0&v=4&typ=-5&livestate=0&rows=40&start=0";
public function init($extend = "")
{
}
public function homeContent($filter)
{
$classes = [
['type_id' => '8', 'type_name' => '娱乐'],
['type_id' => '1', 'type_name' => '网游'],
['type_id' => '2', 'type_name' => '单机'],
['type_id' => '3', 'type_name' => '手游']
];
$filters = $this->getFilters();
$recommend = $this->getRecommendVideos();
return [
'class' => $classes,
'filters' => $filters,
'list' => $recommend
];
}
public function categoryContent($tid, $pg = 1, $filter = [], $extend = [])
{
$page = max(1, $pg);
$gameId = $this->getGameIdFromParams($tid, $filter, $extend);
$url = self::HOST . str_replace(
['fyfilter', 'fypage'],
[$gameId, $page],
self::CATEGORY_URL_TEMPLATE
);
$html = $this->fetch($url);
$data = json_decode($html, true);
if (empty($data) || empty($data['data']['datas'])) {
return $this->pageResult([], $page);
}
$videos = [];
foreach ($data['data']['datas'] as $item) {
$profileRoom = $item['profileRoom'] ?? '';
$roomId = $this->extractRoomId($profileRoom);
$videos[] = [
'vod_id' => $roomId,
'vod_name' => $item['introduction'] ?? '未知标题',
'vod_pic' => $item['screenshot'] ?? '',
'vod_remarks' => '👁' . ($item['totalCount'] ?? 0) . ' 🆙' . ($item['nick'] ?? '')
];
}
$total = $data['data']['total'] ?? 0;
$pageSize = 8;
return $this->pageResult($videos, $page, $total, $pageSize);
}
private function getGameIdFromParams($tid, $filter, $extend)
{
$defaultGameIds = [
'8' => '2135',
'1' => '1',
'2' => '1732',
'3' => '2336'
];
if (!empty($extend) && isset($extend['cateId']) && !empty($extend['cateId'])) {
return $extend['cateId'];
}
if (is_string($filter) && !empty($filter)) {
if (strpos($filter, '{') === 0 || strpos($filter, '[') === 0) {
$decoded = json_decode($filter, true);
if (json_last_error() === JSON_ERROR_NONE && isset($decoded['cateId'])) {
return $decoded['cateId'];
}
} else {
return $filter;
}
}
if (is_array($filter) && !empty($filter)) {
if (isset($filter['cateId']) && !empty($filter['cateId'])) {
return $filter['cateId'];
}
}
return $defaultGameIds[$tid] ?? $tid;
}
public function detailContent($ids)
{
if (empty($ids) || !is_array($ids)) {
return ['list' => []];
}
$roomId = $ids[0];
if (empty($roomId)) {
return ['list' => []];
}
$roomInfo = $this->getRoomInfo($roomId);
$vodName = '虎牙直播间';
$vodPic = '';
$vodContent = '房间ID: ' . $roomId;
if ($roomInfo && is_array($roomInfo)) {
$vodName = isset($roomInfo['roomName']) ? (string)$roomInfo['roomName'] : $vodName;
$vodPic = isset($roomInfo['screenshot']) ? (string)$roomInfo['screenshot'] : $vodPic;
$introduction = isset($roomInfo['introduction']) ? (string)$roomInfo['introduction'] : '';
$nick = isset($roomInfo['nick']) ? (string)$roomInfo['nick'] : '';
$vodContent = trim($introduction . "\n主播: " . $nick);
}
return [
'list' => [
[
'vod_id' => (string)$roomId,
'vod_name' => $vodName,
'vod_pic' => $vodPic,
'vod_content' => $vodContent,
'vod_play_from' => '直播',
'vod_play_url' => (string)$roomId
]
]
];
}
public function searchContent($key, $quick = false, $pg = 1)
{
$page = max(1, $pg);
$start = ($page - 1) * 40;
$url = str_replace(
['**', 'start=0'],
[urlencode($key), "start={$start}"],
self::SEARCH_URL
);
$html = $this->fetch($url);
$data = json_decode($html, true);
if (empty($data) || empty($data[3]['docs'])) {
return $this->pageResult([]);
}
$videos = [];
foreach ($data[3]['docs'] as $item) {
$roomId = isset($item['room_id']) ? (string)$item['room_id'] : '';
$videos[] = [
'vod_id' => $roomId,
'vod_name' => isset($item['game_roomName']) ? (string)$item['game_roomName'] : '未知标题',
'vod_pic' => isset($item['game_screenshot']) ? (string)$item['game_screenshot'] : '',
'vod_remarks' => '主播: ' . (isset($item['game_nick']) ? (string)$item['game_nick'] : '')
];
}
return $this->pageResult($videos, $page);
}
public function playerContent($flag, $id, $vipFlags = [])
{
$rid = $this->extractRoomId($id);
if (empty($rid)) {
return ['parse' => 0, 'url' => ''];
}
$apiUrl = self::ROOM_INFO_URL . $rid;
$response = $this->fetch($apiUrl);
$data = json_decode($response, true);
if (empty($data['data']['stream']['flv']['multiLine'][0]['url'])) {
return ['parse' => 0, 'url' => ''];
}
$purl = $data['data']['stream']['flv']['multiLine'][0]['url'];
$realUrl = $this->getRealUrl($purl);
return [
'parse' => 0,
'jx' => 0,
'url' => $realUrl,
'header' => (object)[
'user-agent' => 'Mozilla/5.0'
]
];
}
private function getRecommendVideos()
{
$url = self::HOST . self::HOME_URL;
$html = $this->fetch($url);
$data = json_decode($html, true);
$videos = [];
if (!empty($data['data']['datas'])) {
foreach ($data['data']['datas'] as $item) {
$profileRoom = $item['profileRoom'] ?? '';
$roomId = $this->extractRoomId($profileRoom);
$videos[] = [
'vod_id' => $roomId,
'vod_name' => $item['introduction'] ?? '未知标题',
'vod_pic' => $item['screenshot'] ?? '',
'vod_remarks' => '👁' . ($item['totalCount'] ?? 0) . ' 🆙' . ($item['nick'] ?? '')
];
}
}
return $videos;
}
private function getRoomInfo($roomId)
{
if (empty($roomId)) {
return false;
}
$apiUrl = self::ROOM_INFO_URL . $roomId;
$response = $this->fetch($apiUrl);
$data = json_decode($response, true);
if (!empty($data['data'])) {
return $data['data'];
}
return false;
}
private function extractRoomId($url)
{
if (empty($url)) {
return '';
}
if (is_numeric($url)) {
return (string)$url;
}
preg_match('/(\d+)/', $url, $matches);
if (!empty($matches[1])) {
return (string)$matches[1];
}
return (string)$url;
}
private function getFilters()
{
// 简化分类数据,只保留主要分类
return [
'8' => [
[
'key' => 'cateId',
'name' => '分类',
'value' => [
['n' => '一起看', 'v' => '2135'],
['n' => '星秀', 'v' => '1663'],
['n' => '户外', 'v' => '2165'],
['n' => '二次元', 'v' => '2633'],
['n' => '颜值', 'v' => '2168']
]
]
],
'1' => [
[
'key' => 'cateId',
'name' => '分类',
'value' => [
['n' => '英雄联盟', 'v' => '1'],
['n' => 'CS2', 'v' => '862'],
['n' => '穿越火线', 'v' => '4'],
['n' => '无畏契约', 'v' => '5937'],
['n' => 'DOTA2', 'v' => '7']
]
]
],
'2' => [
[
'key' => 'cateId',
'name' => '分类',
'value' => [
['n' => '天天吃鸡', 'v' => '2793'],
['n' => '永劫无间', 'v' => '6219'],
['n' => '我的世界', 'v' => '1732'],
['n' => '主机游戏', 'v' => '100032'],
['n' => 'Apex英雄', 'v' => '5011']
]
]
],
'3' => [
[
'key' => 'cateId',
'name' => '分类',
'value' => [
['n' => '王者荣耀', 'v' => '2336'],
['n' => '和平精英', 'v' => '3203'],
['n' => '英雄联盟手游', 'v' => '6203'],
['n' => '原神', 'v' => '5489'],
['n' => '金铲铲之战', 'v' => '7185']
]
]
]
];
}
private function getRealUrl($live_url)
{
if (empty($live_url)) {
return '';
}
$parts = explode('?', $live_url, 2);
if (count($parts) < 2) {
return $live_url;
}
list($i, $b) = $parts;
$r = basename($i);
$s = preg_replace('/\.(flv|m3u8)$/', '', $r);
$params = explode('&', $b);
$params = array_filter($params);
$n = [];
$c_tmp2 = [];
foreach ($params as $index => $param) {
if ($index < 3) {
$pair = explode('=', $param, 2);
if (count($pair) == 2) {
$n[$pair[0]] = $pair[1];
}
} else {
$c_tmp2[] = $param;
}
}
$tmp2 = implode('&', $c_tmp2);
if (!empty($tmp2)) {
$pair = explode('=', $tmp2, 2);
if (count($pair) == 2) {
$n[$pair[0]] = $pair[1];
}
}
if (!isset($n['fm'])) {
return $live_url;
}
$fm = urldecode($n['fm']);
$fmParts = explode('&', $fm);
$fm = $fmParts[0] ?? '';
$u = base64_decode($fm);
if ($u === false) {
return $live_url;
}
$uParts = explode('_', $u);
$p = $uParts[0] ?? '';
$f = time() . '0000';
$ll = $n['wsTime'] ?? '';
$t = '0';
$h = "{$p}_{$t}_{$s}_{$f}_{$ll}";
$m = md5($h);
$result = $i . '?wsSecret=' . $m . '&wsTime=' . $ll . '&u=' . $t . '&seqid=' . $f;
if (!empty($c_tmp2)) {
$result .= '&' . end($c_tmp2);
}
return str_replace(['hls', 'm3u8'], ['flv', 'flv'], $result);
}
}
if (basename(__FILE__) == basename($_SERVER['SCRIPT_FILENAME'])) {
(new HuyaSpider())->run();
}