TV / js /api.js
samlax12's picture
Upload 30 files
7eff83b verified
// 改进的API请求处理函数
async function handleApiRequest(url) {
const customApi = url.searchParams.get('customApi') || '';
const source = url.searchParams.get('source') || 'heimuer';
try {
if (url.pathname === '/api/search') {
const searchQuery = url.searchParams.get('wd');
if (!searchQuery) {
throw new Error('缺少搜索参数');
}
// 验证API和source的有效性
if (source === 'custom' && !customApi) {
throw new Error('使用自定义API时必须提供API地址');
}
if (!API_SITES[source] && source !== 'custom') {
throw new Error('无效的API来源');
}
const apiUrl = customApi
? `${customApi}${API_CONFIG.search.path}${encodeURIComponent(searchQuery)}`
: `${API_SITES[source].api}${API_CONFIG.search.path}${encodeURIComponent(searchQuery)}`;
// 添加超时处理
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch(PROXY_URL + encodeURIComponent(apiUrl), {
headers: API_CONFIG.search.headers,
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`API请求失败: ${response.status}`);
}
const data = await response.json();
// 检查JSON格式的有效性
if (!data || !Array.isArray(data.list)) {
throw new Error('API返回的数据格式无效');
}
// 添加源信息到每个结果
data.list.forEach(item => {
item.source_name = source === 'custom' ? '自定义源' : API_SITES[source].name;
item.source_code = source;
// 对于自定义源,添加API URL信息
if (source === 'custom') {
item.api_url = customApi;
}
});
return JSON.stringify({
code: 200,
list: data.list || [],
});
} catch (fetchError) {
clearTimeout(timeoutId);
throw fetchError;
}
}
// 详情处理
if (url.pathname === '/api/detail') {
const id = url.searchParams.get('id');
const sourceCode = url.searchParams.get('source') || 'heimuer'; // 获取源代码
if (!id) {
throw new Error('缺少视频ID参数');
}
// 验证ID格式 - 只允许数字和有限的特殊字符
if (!/^[\w-]+$/.test(id)) {
throw new Error('无效的视频ID格式');
}
// 验证API和source的有效性
if (sourceCode === 'custom' && !customApi) {
throw new Error('使用自定义API时必须提供API地址');
}
if (!API_SITES[sourceCode] && sourceCode !== 'custom') {
throw new Error('无效的API来源');
}
// 对于特殊源,使用特殊处理方式
if ((sourceCode === 'ffzy' || sourceCode === 'jisu' || sourceCode === 'huangcang') && API_SITES[sourceCode].detail) {
return await handleSpecialSourceDetail(id, sourceCode);
}
// 如果是自定义API,并且传递了detail参数,尝试特殊处理
if (sourceCode === 'custom' && url.searchParams.get('useDetail') === 'true') {
return await handleCustomApiSpecialDetail(id, customApi);
}
const detailUrl = customApi
? `${customApi}${API_CONFIG.detail.path}${id}`
: `${API_SITES[sourceCode].api}${API_CONFIG.detail.path}${id}`;
// 添加超时处理
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch(PROXY_URL + encodeURIComponent(detailUrl), {
headers: API_CONFIG.detail.headers,
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`详情请求失败: ${response.status}`);
}
// 解析JSON
const data = await response.json();
// 检查返回的数据是否有效
if (!data || !data.list || !Array.isArray(data.list) || data.list.length === 0) {
throw new Error('获取到的详情内容无效');
}
// 获取第一个匹配的视频详情
const videoDetail = data.list[0];
// 提取播放地址
let episodes = [];
if (videoDetail.vod_play_url) {
// 分割不同播放源
const playSources = videoDetail.vod_play_url.split('$$$');
// 提取第一个播放源的集数(通常为主要源)
if (playSources.length > 0) {
const mainSource = playSources[0];
const episodeList = mainSource.split('#');
// 从每个集数中提取URL
episodes = episodeList.map(ep => {
const parts = ep.split('$');
// 返回URL部分(通常是第二部分,如果有的话)
return parts.length > 1 ? parts[1] : '';
}).filter(url => url && (url.startsWith('http://') || url.startsWith('https://')));
}
}
// 如果没有找到播放地址,尝试使用正则表达式查找m3u8链接
if (episodes.length === 0 && videoDetail.vod_content) {
const matches = videoDetail.vod_content.match(M3U8_PATTERN) || [];
episodes = matches.map(link => link.replace(/^\$/, ''));
}
return JSON.stringify({
code: 200,
episodes: episodes,
detailUrl: detailUrl,
videoInfo: {
title: videoDetail.vod_name,
cover: videoDetail.vod_pic,
desc: videoDetail.vod_content,
type: videoDetail.type_name,
year: videoDetail.vod_year,
area: videoDetail.vod_area,
director: videoDetail.vod_director,
actor: videoDetail.vod_actor,
remarks: videoDetail.vod_remarks,
// 添加源信息
source_name: sourceCode === 'custom' ? '自定义源' : API_SITES[sourceCode].name,
source_code: sourceCode
}
});
} catch (fetchError) {
clearTimeout(timeoutId);
throw fetchError;
}
}
throw new Error('未知的API路径');
} catch (error) {
console.error('API处理错误:', error);
return JSON.stringify({
code: 400,
msg: error.message || '请求处理失败',
list: [],
episodes: [],
});
}
}
// 处理自定义API的特殊详情页
async function handleCustomApiSpecialDetail(id, customApi) {
try {
// 构建详情页URL
const detailUrl = `${customApi}/index.php/vod/detail/id/${id}.html`;
// 添加超时处理
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
// 获取详情页HTML
const response = await fetch(PROXY_URL + encodeURIComponent(detailUrl), {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
},
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`自定义API详情页请求失败: ${response.status}`);
}
// 获取HTML内容
const html = await response.text();
// 使用通用模式提取m3u8链接
const generalPattern = /\$(https?:\/\/[^"'\s]+?\.m3u8)/g;
let matches = html.match(generalPattern) || [];
// 处理链接
matches = matches.map(link => {
link = link.substring(1, link.length);
const parenIndex = link.indexOf('(');
return parenIndex > 0 ? link.substring(0, parenIndex) : link;
});
// 提取基本信息
const titleMatch = html.match(/<h1[^>]*>([^<]+)<\/h1>/);
const titleText = titleMatch ? titleMatch[1].trim() : '';
const descMatch = html.match(/<div[^>]*class=["']sketch["'][^>]*>([\s\S]*?)<\/div>/);
const descText = descMatch ? descMatch[1].replace(/<[^>]+>/g, ' ').trim() : '';
return JSON.stringify({
code: 200,
episodes: matches,
detailUrl: detailUrl,
videoInfo: {
title: titleText,
desc: descText,
source_name: '自定义源',
source_code: 'custom'
}
});
} catch (error) {
console.error(`自定义API详情获取失败:`, error);
throw error;
}
}
// 处理极速资源详情的特殊函数
async function handleJisuDetail(id, sourceCode) {
// 直接复用通用的特殊源处理函数,传入相应参数
return await handleSpecialSourceDetail(id, sourceCode);
}
// 处理非凡影视详情的特殊函数
async function handleFFZYDetail(id, sourceCode) {
// 直接复用通用的特殊源处理函数,传入相应参数
return await handleSpecialSourceDetail(id, sourceCode);
}
// 通用特殊源详情处理函数
async function handleSpecialSourceDetail(id, sourceCode) {
try {
// 构建详情页URL(使用配置中的detail URL而不是api URL)
const detailUrl = `${API_SITES[sourceCode].detail}/index.php/vod/detail/id/${id}.html`;
// 添加超时处理
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
// 获取详情页HTML
const response = await fetch(PROXY_URL + encodeURIComponent(detailUrl), {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
},
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`详情页请求失败: ${response.status}`);
}
// 获取HTML内容
const html = await response.text();
// 根据不同源类型使用不同的正则表达式
let matches = [];
if (sourceCode === 'ffzy') {
// 非凡影视使用特定的正则表达式
const ffzyPattern = /\$(https?:\/\/[^"'\s]+?\/\d{8}\/\d+_[a-f0-9]+\/index\.m3u8)/g;
matches = html.match(ffzyPattern) || [];
}
// 如果没有找到链接或者是其他源类型,尝试一个更通用的模式
if (matches.length === 0) {
const generalPattern = /\$(https?:\/\/[^"'\s]+?\.m3u8)/g;
matches = html.match(generalPattern) || [];
}
// 去重处理,避免一个播放源多集显示
matches = [...new Set(matches)];
// 处理链接
matches = matches.map(link => {
link = link.substring(1, link.length);
const parenIndex = link.indexOf('(');
return parenIndex > 0 ? link.substring(0, parenIndex) : link;
});
// 提取可能存在的标题、简介等基本信息
const titleMatch = html.match(/<h1[^>]*>([^<]+)<\/h1>/);
const titleText = titleMatch ? titleMatch[1].trim() : '';
const descMatch = html.match(/<div[^>]*class=["']sketch["'][^>]*>([\s\S]*?)<\/div>/);
const descText = descMatch ? descMatch[1].replace(/<[^>]+>/g, ' ').trim() : '';
return JSON.stringify({
code: 200,
episodes: matches,
detailUrl: detailUrl,
videoInfo: {
title: titleText,
desc: descText,
source_name: API_SITES[sourceCode].name,
source_code: sourceCode
}
});
} catch (error) {
console.error(`${API_SITES[sourceCode].name}详情获取失败:`, error);
throw error;
}
}
// 处理聚合搜索
async function handleAggregatedSearch(searchQuery) {
// 获取可用的API源列表(排除aggregated和custom)
const availableSources = Object.keys(API_SITES).filter(key =>
key !== 'aggregated' && key !== 'custom'
);
if (availableSources.length === 0) {
throw new Error('没有可用的API源');
}
// 创建所有API源的搜索请求
const searchPromises = availableSources.map(async (source) => {
try {
const apiUrl = `${API_SITES[source].api}${API_CONFIG.search.path}${encodeURIComponent(searchQuery)}`;
// 使用Promise.race添加超时处理
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`${source}源搜索超时`)), 8000)
);
const fetchPromise = fetch(PROXY_URL + encodeURIComponent(apiUrl), {
headers: API_CONFIG.search.headers
});
const response = await Promise.race([fetchPromise, timeoutPromise]);
if (!response.ok) {
throw new Error(`${source}源请求失败: ${response.status}`);
}
const data = await response.json();
if (!data || !Array.isArray(data.list)) {
throw new Error(`${source}源返回的数据格式无效`);
}
// 为搜索结果添加源信息
const results = data.list.map(item => ({
...item,
source_name: API_SITES[source].name,
source_code: source
}));
return results;
} catch (error) {
console.warn(`${source}源搜索失败:`, error);
return []; // 返回空数组表示该源搜索失败
}
});
try {
// 并行执行所有搜索请求
const resultsArray = await Promise.all(searchPromises);
// 合并所有结果
let allResults = [];
resultsArray.forEach(results => {
if (Array.isArray(results) && results.length > 0) {
allResults = allResults.concat(results);
}
});
// 如果没有搜索结果,返回空结果
if (allResults.length === 0) {
return JSON.stringify({
code: 200,
list: [],
msg: '所有源均无搜索结果'
});
}
// 去重(根据vod_id和source_code组合)
const uniqueResults = [];
const seen = new Set();
allResults.forEach(item => {
const key = `${item.source_code}_${item.vod_id}`;
if (!seen.has(key)) {
seen.add(key);
uniqueResults.push(item);
}
});
// 按照视频名称和来源排序
uniqueResults.sort((a, b) => {
// 首先按照视频名称排序
const nameCompare = (a.vod_name || '').localeCompare(b.vod_name || '');
if (nameCompare !== 0) return nameCompare;
// 如果名称相同,则按照来源排序
return (a.source_name || '').localeCompare(b.source_name || '');
});
return JSON.stringify({
code: 200,
list: uniqueResults,
});
} catch (error) {
console.error('聚合搜索处理错误:', error);
return JSON.stringify({
code: 400,
msg: '聚合搜索处理失败: ' + error.message,
list: []
});
}
}
// 处理多个自定义API源的聚合搜索
async function handleMultipleCustomSearch(searchQuery, customApiUrls) {
// 解析自定义API列表
const apiUrls = customApiUrls.split(CUSTOM_API_CONFIG.separator)
.map(url => url.trim())
.filter(url => url.length > 0 && /^https?:\/\//.test(url))
.slice(0, CUSTOM_API_CONFIG.maxSources);
if (apiUrls.length === 0) {
throw new Error('没有提供有效的自定义API地址');
}
// 为每个API创建搜索请求
const searchPromises = apiUrls.map(async (apiUrl, index) => {
try {
const fullUrl = `${apiUrl}${API_CONFIG.search.path}${encodeURIComponent(searchQuery)}`;
// 使用Promise.race添加超时处理
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`自定义API ${index+1} 搜索超时`)), 8000)
);
const fetchPromise = fetch(PROXY_URL + encodeURIComponent(fullUrl), {
headers: API_CONFIG.search.headers
});
const response = await Promise.race([fetchPromise, timeoutPromise]);
if (!response.ok) {
throw new Error(`自定义API ${index+1} 请求失败: ${response.status}`);
}
const data = await response.json();
if (!data || !Array.isArray(data.list)) {
throw new Error(`自定义API ${index+1} 返回的数据格式无效`);
}
// 为搜索结果添加源信息
const results = data.list.map(item => ({
...item,
source_name: `${CUSTOM_API_CONFIG.namePrefix}${index+1}`,
source_code: 'custom',
api_url: apiUrl // 保存API URL以便详情获取
}));
return results;
} catch (error) {
console.warn(`自定义API ${index+1} 搜索失败:`, error);
return []; // 返回空数组表示该源搜索失败
}
});
try {
// 并行执行所有搜索请求
const resultsArray = await Promise.all(searchPromises);
// 合并所有结果
let allResults = [];
resultsArray.forEach(results => {
if (Array.isArray(results) && results.length > 0) {
allResults = allResults.concat(results);
}
});
// 如果没有搜索结果,返回空结果
if (allResults.length === 0) {
return JSON.stringify({
code: 200,
list: [],
msg: '所有自定义API源均无搜索结果'
});
}
// 去重(根据vod_id和api_url组合)
const uniqueResults = [];
const seen = new Set();
allResults.forEach(item => {
const key = `${item.api_url || ''}_${item.vod_id}`;
if (!seen.has(key)) {
seen.add(key);
uniqueResults.push(item);
}
});
return JSON.stringify({
code: 200,
list: uniqueResults,
});
} catch (error) {
console.error('自定义API聚合搜索处理错误:', error);
return JSON.stringify({
code: 400,
msg: '自定义API聚合搜索处理失败: ' + error.message,
list: []
});
}
}
// 拦截API请求
(function() {
const originalFetch = window.fetch;
window.fetch = async function(input, init) {
const requestUrl = typeof input === 'string' ? new URL(input, window.location.origin) : input.url;
if (requestUrl.pathname.startsWith('/api/')) {
if (window.isPasswordProtected && window.isPasswordVerified) {
if (window.isPasswordProtected() && !window.isPasswordVerified()) {
return;
}
}
try {
const data = await handleApiRequest(requestUrl);
return new Response(data, {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
});
} catch (error) {
return new Response(JSON.stringify({
code: 500,
msg: '服务器内部错误',
}), {
status: 500,
headers: {
'Content-Type': 'application/json',
},
});
}
}
// 非API请求使用原始fetch
return originalFetch.apply(this, arguments);
};
})();
async function testSiteAvailability(apiUrl) {
try {
// 使用更简单的测试查询
const response = await fetch('/api/search?wd=test&customApi=' + encodeURIComponent(apiUrl), {
// 添加超时
signal: AbortSignal.timeout(5000)
});
// 检查响应状态
if (!response.ok) {
return false;
}
const data = await response.json();
// 检查API响应的有效性
return data && data.code !== 400 && Array.isArray(data.list);
} catch (error) {
console.error('站点可用性测试失败:', error);
return false;
}
}