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.
441 lines
17 KiB
441 lines
17 KiB
<?php
|
|
// B站视频爬虫 - 简洁可用版(移除search相关代码)
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
|
|
class BiliBiliSpider {
|
|
private $extendDict = [];
|
|
private $cookie = [];
|
|
private $header = [
|
|
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.54 Safari/537.36",
|
|
"Referer" => "https://www.bilibili.com"
|
|
];
|
|
|
|
public function __construct() {
|
|
$this->extendDict = $this->getExtendDict();
|
|
$this->cookie = $this->getCookie();
|
|
}
|
|
|
|
private function getExtendDict() {
|
|
return [
|
|
'cookie' => $this->getConfigCookie(),
|
|
'thread' => '0'
|
|
];
|
|
}
|
|
|
|
private function getConfigCookie() {
|
|
// 配置您的B站Cookie
|
|
return '';
|
|
}
|
|
|
|
private function getCookie() {
|
|
$cookie = $this->extendDict['cookie'] ?? '';
|
|
if (empty($cookie)) return [];
|
|
|
|
$cookies = [];
|
|
$pairs = explode(';', $cookie);
|
|
foreach ($pairs as $pair) {
|
|
$pair = trim($pair);
|
|
if (strpos($pair, '=') !== false) {
|
|
list($name, $value) = explode('=', $pair, 2);
|
|
$cookies[trim($name)] = trim($value);
|
|
}
|
|
}
|
|
return $cookies;
|
|
}
|
|
|
|
private function httpRequest($url, $params = []) {
|
|
$ch = curl_init();
|
|
|
|
if (!empty($params)) {
|
|
$url .= '?' . http_build_query($params);
|
|
}
|
|
|
|
$headers = [];
|
|
foreach ($this->header as $key => $value) {
|
|
$headers[] = $key . ': ' . $value;
|
|
}
|
|
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_URL => $url,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 10,
|
|
CURLOPT_HTTPHEADER => $headers,
|
|
CURLOPT_COOKIE => $this->buildCookieString(),
|
|
CURLOPT_SSL_VERIFYPEER => false,
|
|
CURLOPT_FOLLOWLOCATION => true
|
|
]);
|
|
|
|
$response = curl_exec($ch);
|
|
curl_close($ch);
|
|
|
|
return json_decode($response, true) ?: [];
|
|
}
|
|
|
|
private function buildCookieString() {
|
|
$pairs = [];
|
|
foreach ($this->cookie as $name => $value) {
|
|
$pairs[] = $name . '=' . $value;
|
|
}
|
|
return implode('; ', $pairs);
|
|
}
|
|
|
|
// homeContent - 首页分类
|
|
public function homeContent() {
|
|
$classes = [
|
|
["type_id" => "沙雕仙逆", "type_name" => "傻屌仙逆"],
|
|
["type_id" => "沙雕动画", "type_name" => "沙雕动画"],
|
|
["type_id" => "纪录片超清", "type_name" => "纪录片"],
|
|
["type_id" => "演唱会超清", "type_name" => "演唱会"],
|
|
["type_id" => "音乐超清", "type_name" => "流行音乐"],
|
|
["type_id" => "美食超清", "type_name" => "美食"],
|
|
["type_id" => "食谱", "type_name" => "食谱"],
|
|
["type_id" => "体育超清", "type_name" => "体育"],
|
|
["type_id" => "球星", "type_name" => "球星"],
|
|
["type_id" => "中小学教育", "type_name" => "教育"],
|
|
["type_id" => "幼儿教育", "type_name" => "幼儿教育"],
|
|
["type_id" => "旅游", "type_name" => "旅游"],
|
|
["type_id" => "风景4K", "type_name" => "风景"],
|
|
["type_id" => "说案", "type_name" => "说案"],
|
|
["type_id" => "知名UP主", "type_name" => "知名UP主"],
|
|
["type_id" => "探索发现超清", "type_name" => "探索发现"],
|
|
["type_id" => "鬼畜", "type_name" => "鬼畜"],
|
|
["type_id" => "搞笑超清", "type_name" => "搞笑"],
|
|
["type_id" => "儿童超清", "type_name" => "儿童"],
|
|
["type_id" => "动物世界超清", "type_name" => "动物世界"],
|
|
["type_id" => "相声小品超清", "type_name" => "相声小品"],
|
|
["type_id" => "戏曲", "type_name" => "戏曲"],
|
|
["type_id" => "解说", "type_name" => "解说"],
|
|
["type_id" => "演讲", "type_name" => "演讲"],
|
|
["type_id" => "小姐姐超清", "type_name" => "小姐姐"],
|
|
["type_id" => "荒野求生超清", "type_name" => "荒野求生"],
|
|
["type_id" => "健身", "type_name" => "健身"],
|
|
["type_id" => "帕梅拉", "type_name" => "帕梅拉"],
|
|
["type_id" => "太极拳", "type_name" => "太极拳"],
|
|
["type_id" => "广场舞", "type_name" => "广场舞"],
|
|
["type_id" => "舞蹈", "type_name" => "舞蹈"],
|
|
["type_id" => "音乐", "type_name" => "音乐"],
|
|
["type_id" => "歌曲", "type_name" => "歌曲"],
|
|
["type_id" => "MV4K", "type_name" => "MV"],
|
|
["type_id" => "舞曲超清", "type_name" => "舞曲"],
|
|
["type_id" => "4K", "type_name" => "4K"],
|
|
["type_id" => "电影", "type_name" => "电影"],
|
|
["type_id" => "电视剧", "type_name" => "电视剧"],
|
|
["type_id" => "白噪音超清", "type_name" => "白噪音"],
|
|
["type_id" => "考公考证", "type_name" => "考公考证"],
|
|
["type_id" => "平面设计教学", "type_name" => "平面设计教学"],
|
|
["type_id" => "软件教程", "type_name" => "软件教程"],
|
|
["type_id" => "Windows", "type_name" => "Windows"]
|
|
];
|
|
|
|
return ['class' => $classes];
|
|
}
|
|
|
|
// homeVideoContent - 首页推荐视频
|
|
public function homeVideoContent() {
|
|
$url = 'https://api.bilibili.com/x/web-interface/popular';
|
|
$data = $this->httpRequest($url, ['ps' => 20, 'pn' => 1]);
|
|
|
|
$videos = [];
|
|
if (isset($data['data']['list'])) {
|
|
foreach ($data['data']['list'] as $item) {
|
|
$videos[] = [
|
|
'vod_id' => $item['aid'],
|
|
'vod_name' => strip_tags($item['title']),
|
|
'vod_pic' => $item['pic'],
|
|
'vod_remarks' => $this->formatDuration($item['duration'])
|
|
];
|
|
}
|
|
}
|
|
|
|
return ['list' => $videos];
|
|
}
|
|
|
|
// categoryContent - 分类内容(使用搜索API)
|
|
public function categoryContent($tid, $page, $filters = []) {
|
|
$page = max(1, intval($page));
|
|
|
|
$url = 'https://api.bilibili.com/x/web-interface/search/type';
|
|
$params = [
|
|
'search_type' => 'video',
|
|
'keyword' => $tid,
|
|
'page' => $page
|
|
];
|
|
|
|
$data = $this->httpRequest($url, $params);
|
|
|
|
$videos = [];
|
|
if (isset($data['data']['result'])) {
|
|
foreach ($data['data']['result'] as $item) {
|
|
if ($item['type'] !== 'video') continue;
|
|
|
|
$videos[] = [
|
|
'vod_id' => $item['aid'],
|
|
'vod_name' => strip_tags($item['title']),
|
|
'vod_pic' => 'https:' . $item['pic'],
|
|
'vod_remarks' => $this->formatSearchDuration($item['duration'])
|
|
];
|
|
}
|
|
}
|
|
|
|
$pageCount = $data['data']['numPages'] ?? 1;
|
|
$total = $data['data']['numResults'] ?? count($videos);
|
|
|
|
return [
|
|
'list' => $videos,
|
|
'page' => $page,
|
|
'pagecount' => $pageCount,
|
|
'limit' => 20,
|
|
'total' => $total
|
|
];
|
|
}
|
|
|
|
// detailContent - 视频详情
|
|
public function detailContent($vid) {
|
|
$url = 'https://api.bilibili.com/x/web-interface/view';
|
|
$data = $this->httpRequest($url, ['aid' => $vid]);
|
|
|
|
if (!isset($data['data'])) {
|
|
return ['list' => []];
|
|
}
|
|
|
|
$video = $data['data'];
|
|
|
|
// 构建播放列表
|
|
$playUrl = '';
|
|
foreach ($video['pages'] as $index => $page) {
|
|
$part = $page['part'] ?: '第' . ($index + 1) . '集';
|
|
$duration = $this->formatDuration($page['duration']);
|
|
$playUrl .= "{$part}\${$vid}_{$page['cid']}#";
|
|
}
|
|
|
|
$vod = [
|
|
"vod_id" => $vid,
|
|
"vod_name" => strip_tags($video['title']),
|
|
"vod_pic" => $video['pic'],
|
|
"vod_content" => $video['desc'],
|
|
"vod_play_from" => "B站视频",
|
|
"vod_play_url" => rtrim($playUrl, '#')
|
|
];
|
|
|
|
return ['list' => [$vod]];
|
|
}
|
|
|
|
// playContent - 播放地址(高清优化)
|
|
public function playContent($vid) {
|
|
if (strpos($vid, '_') !== false) {
|
|
list($avid, $cid) = explode('_', $vid);
|
|
} else {
|
|
return $this->errorResponse('无效的视频ID格式');
|
|
}
|
|
|
|
// 使用高质量参数
|
|
$url = 'https://api.bilibili.com/x/player/playurl';
|
|
$params = [
|
|
'avid' => $avid,
|
|
'cid' => $cid,
|
|
'qn' => 112, // 原画质量
|
|
'fnval' => 0,
|
|
];
|
|
|
|
$data = $this->httpRequest($url, $params);
|
|
|
|
if (!isset($data['data']) || $data['code'] !== 0) {
|
|
return $this->errorResponse('获取播放地址失败');
|
|
}
|
|
|
|
// 直接返回第一个播放地址
|
|
if (isset($data['data']['durl'][0]['url'])) {
|
|
$playUrl = $data['data']['durl'][0]['url'];
|
|
|
|
$headers = $this->header;
|
|
$headers['Referer'] = 'https://www.bilibili.com/video/av' . $avid;
|
|
$headers['Origin'] = 'https://www.bilibili.com';
|
|
|
|
return [
|
|
'parse' => 0,
|
|
'url' => $playUrl,
|
|
'header' => $headers,
|
|
'danmaku' => "https://api.bilibili.com/x/v1/dm/list.so?oid={$cid}"
|
|
];
|
|
}
|
|
|
|
return $this->errorResponse('无法获取播放地址');
|
|
}
|
|
|
|
// 工具函数
|
|
private function formatDuration($seconds) {
|
|
if ($seconds <= 0) return '00:00';
|
|
$minutes = floor($seconds / 60);
|
|
$secs = $seconds % 60;
|
|
return sprintf('%02d:%02d', $minutes, $secs);
|
|
}
|
|
|
|
private function formatSearchDuration($duration) {
|
|
$parts = explode(':', $duration);
|
|
if (count($parts) === 2) {
|
|
return $duration;
|
|
}
|
|
return '00:00';
|
|
}
|
|
|
|
private function errorResponse($message) {
|
|
return [
|
|
'parse' => 0,
|
|
'url' => '',
|
|
'error' => $message
|
|
];
|
|
}
|
|
}
|
|
|
|
// 获取请求参数
|
|
$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) ?: [];
|
|
}
|
|
}
|
|
|
|
$spider = new BiliBiliSpider();
|
|
|
|
try {
|
|
// ============================================================================
|
|
// 首页/分类接口
|
|
// ============================================================================
|
|
if ($filter !== null) {
|
|
echo json_encode([
|
|
'class' => [
|
|
["type_id" => "沙雕仙逆", "type_name" => "傻屌仙逆"],
|
|
["type_id" => "沙雕动画", "type_name" => "沙雕动画"],
|
|
["type_id" => "纪录片超清", "type_name" => "纪录片"],
|
|
["type_id" => "演唱会超清", "type_name" => "演唱会"],
|
|
["type_id" => "音乐超清", "type_name" => "流行音乐"],
|
|
["type_id" => "美食超清", "type_name" => "美食"],
|
|
["type_id" => "食谱", "type_name" => "食谱"],
|
|
["type_id" => "体育超清", "type_name" => "体育"],
|
|
["type_id" => "球星", "type_name" => "球星"],
|
|
["type_id" => "中小学教育", "type_name" => "教育"],
|
|
["type_id" => "幼儿教育", "type_name" => "幼儿教育"],
|
|
["type_id" => "旅游", "type_name" => "旅游"],
|
|
["type_id" => "风景4K", "type_name" => "风景"],
|
|
["type_id" => "说案", "type_name" => "说案"],
|
|
["type_id" => "知名UP主", "type_name" => "知名UP主"],
|
|
["type_id" => "探索发现超清", "type_name" => "探索发现"],
|
|
["type_id" => "鬼畜", "type_name" => "鬼畜"],
|
|
["type_id" => "搞笑超清", "type_name" => "搞笑"],
|
|
["type_id" => "儿童超清", "type_name" => "儿童"],
|
|
["type_id" => "动物世界超清", "type_name" => "动物世界"],
|
|
["type_id" => "相声小品超清", "type_name" => "相声小品"],
|
|
["type_id" => "戏曲", "type_name" => "戏曲"],
|
|
["type_id" => "解说", "type_name" => "解说"],
|
|
["type_id" => "演讲", "type_name" => "演讲"],
|
|
["type_id" => "小姐姐超清", "type_name" => "小姐姐"],
|
|
["type_id" => "荒野求生超清", "type_name" => "荒野求生"],
|
|
["type_id" => "健身", "type_name" => "健身"],
|
|
["type_id" => "帕梅拉", "type_name" => "帕梅拉"],
|
|
["type_id" => "太极拳", "type_name" => "太极拳"],
|
|
["type_id" => "广场舞", "type_name" => "广场舞"],
|
|
["type_id" => "舞蹈", "type_name" => "舞蹈"],
|
|
["type_id" => "音乐", "type_name" => "音乐"],
|
|
["type_id" => "歌曲", "type_name" => "歌曲"],
|
|
["type_id" => "MV4K", "type_name" => "MV"],
|
|
["type_id" => "舞曲超清", "type_name" => "舞曲"],
|
|
["type_id" => "4K", "type_name" => "4K"],
|
|
["type_id" => "电影", "type_name" => "电影"],
|
|
["type_id" => "电视剧", "type_name" => "电视剧"],
|
|
["type_id" => "白噪音超清", "type_name" => "白噪音"],
|
|
["type_id" => "考公考证", "type_name" => "考公考证"],
|
|
["type_id" => "平面设计教学", "type_name" => "平面设计教学"],
|
|
["type_id" => "软件教程", "type_name" => "软件教程"],
|
|
["type_id" => "Windows", "type_name" => "Windows"]
|
|
],
|
|
'filters' => [
|
|
'1001' => [
|
|
[
|
|
'key' => 'sort',
|
|
'name' => '排序',
|
|
'value' => [
|
|
['n' => '最新', 'v' => 'new'],
|
|
['n' => '最热', 'v' => 'hot'],
|
|
['n' => '推荐', 'v' => 'recommend']
|
|
]
|
|
]
|
|
]
|
|
]
|
|
], JSON_UNESCAPED_UNICODE);
|
|
exit;
|
|
}
|
|
|
|
// ============================================================================
|
|
// 首页视频内容
|
|
// ============================================================================
|
|
if ($ac === 'detail' && empty($t) && empty($ids)) {
|
|
$result = $spider->homeContent();
|
|
$videoResult = $spider->homeVideoContent();
|
|
$result['list'] = $videoResult['list'];
|
|
echo json_encode($result, JSON_UNESCAPED_UNICODE);
|
|
exit;
|
|
}
|
|
|
|
// ============================================================================
|
|
// 分类列表接口
|
|
// ============================================================================
|
|
if ($ac === 'detail' && $t !== null) {
|
|
$filters = !empty($extData) ? $extData : [];
|
|
echo json_encode($spider->categoryContent($t, $pg, $filters), JSON_UNESCAPED_UNICODE);
|
|
exit;
|
|
}
|
|
|
|
// ============================================================================
|
|
// 详情接口
|
|
// ============================================================================
|
|
if ($ac === 'detail' && $ids !== null) {
|
|
echo json_encode($spider->detailContent($ids), JSON_UNESCAPED_UNICODE);
|
|
exit;
|
|
}
|
|
|
|
// ============================================================================
|
|
// 播放解析接口
|
|
// ============================================================================
|
|
if ($flag !== null && $play !== null) {
|
|
// 解析B站视频地址
|
|
if (strpos($play, 'bilibili.com') !== false) {
|
|
// 对于B站地址,直接返回(TVBox会自行解析)
|
|
echo json_encode([
|
|
'parse' => 0,
|
|
'url' => $play,
|
|
'header' => [
|
|
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.54 Safari/537.36',
|
|
'Referer' => 'https://www.bilibili.com/'
|
|
]
|
|
], JSON_UNESCAPED_UNICODE);
|
|
} else {
|
|
// 使用spider的playContent方法解析内部格式
|
|
echo json_encode($spider->playContent($play), JSON_UNESCAPED_UNICODE);
|
|
}
|
|
exit;
|
|
}
|
|
|
|
// ============================================================================
|
|
// 搜索接口(简单实现)
|
|
// ============================================================================
|
|
if ($wd !== null) {
|
|
echo json_encode($spider->categoryContent($wd, $pg), JSON_UNESCAPED_UNICODE);
|
|
exit;
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
echo json_encode(['error' => $e->getMessage()], JSON_UNESCAPED_UNICODE);
|
|
}
|
|
?>
|