481 baris
16 KiB
JavaScript
481 baris
16 KiB
JavaScript
// ============================================
|
|
// 茶杯狐 TVBox JS版 - 完整可用
|
|
// 完全复制Python版请求行为
|
|
// ============================================
|
|
|
|
let siteUrl = 'http://www.bnjxjd.com';
|
|
let 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',
|
|
'Referer': 'http://www.bnjxjd.com'
|
|
};
|
|
|
|
// ============ 工具函数 ============
|
|
|
|
// 完全复制Python版fetch行为
|
|
async function fetchUrl(url) {
|
|
try {
|
|
let res = await req(url, {
|
|
method: 'get',
|
|
headers: headers,
|
|
timeout: 15000
|
|
});
|
|
|
|
let content = res.content || '';
|
|
|
|
// 如果返回的是Buffer/ArrayBuffer,转成UTF-8字符串
|
|
if (typeof content === 'object' && !(content instanceof String)) {
|
|
try {
|
|
content = new TextDecoder('utf-8').decode(content);
|
|
} catch (e) {
|
|
content = String(content);
|
|
}
|
|
}
|
|
|
|
// 处理可能的乱码
|
|
if (typeof content === 'string') {
|
|
// 移除BOM头
|
|
if (content.charCodeAt(0) === 0xFEFF) {
|
|
content = content.slice(1);
|
|
}
|
|
}
|
|
|
|
return content;
|
|
} catch (e) {
|
|
console.log('请求失败:', url, e.message);
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function cleanText(text) {
|
|
if (!text) return '';
|
|
return text.replace(/ /g, ' ')
|
|
.replace(/\n/g, ' ')
|
|
.replace(/\r/g, ' ')
|
|
.replace(/\s+/g, ' ')
|
|
.trim();
|
|
}
|
|
|
|
// ============ 核心函数 ============
|
|
|
|
async function init(cfg) {
|
|
// 支持外部传入地址
|
|
if (cfg && cfg.ext && typeof cfg.ext === 'string' && cfg.ext.startsWith('http')) {
|
|
siteUrl = cfg.ext;
|
|
}
|
|
if (siteUrl.endsWith('/')) {
|
|
siteUrl = siteUrl.slice(0, -1);
|
|
}
|
|
headers.Referer = siteUrl;
|
|
}
|
|
|
|
async function home(filter) {
|
|
let classes = [
|
|
{ type_id: '1', type_name: '电影' },
|
|
{ type_id: '2', type_name: '电视剧' },
|
|
{ type_id: '3', type_name: '综艺' },
|
|
{ type_id: '4', type_name: '动漫' }
|
|
];
|
|
|
|
let filters = {
|
|
'1': [
|
|
{ key: 'class', name: '类型', value: [
|
|
{n:'全部',v:'1'},{n:'动作片',v:'6'},{n:'喜剧片',v:'7'},{n:'爱情片',v:'8'},
|
|
{n:'科幻片',v:'9'},{n:'恐怖片',v:'10'},{n:'剧情片',v:'11'},{n:'战争片',v:'12'},
|
|
{n:'记录片',v:'13'},{n:'悬疑片',v:'14'},{n:'犯罪片',v:'15'},{n:'奇幻片',v:'16'},{n:'动画片',v:'31'}
|
|
]},
|
|
{ key: 'area', name: '地区', value: [
|
|
{n:'全部',v:'0'},{n:'大陆',v:'大陆'},{n:'香港',v:'香港'},{n:'台湾',v:'台湾'},
|
|
{n:'美国',v:'美国'},{n:'日本',v:'日本'},{n:'韩国',v:'韩国'},{n:'英国',v:'英国'},
|
|
{n:'法国',v:'法国'},{n:'德国',v:'德国'},{n:'泰国',v:'泰国'},{n:'印度',v:'印度'},
|
|
{n:'加拿大',v:'加拿大'},{n:'其他',v:'其他'}
|
|
]},
|
|
{ key: 'year', name: '年份', value: [
|
|
{n:'全部',v:'0'},{n:'2026',v:'2026'},{n:'2025',v:'2025'},{n:'2024',v:'2024'},
|
|
{n:'2023',v:'2023'},{n:'2022',v:'2022'},{n:'2021',v:'2021'},{n:'2020',v:'2020'},
|
|
{n:'2019',v:'2019'},{n:'2018',v:'2018'},{n:'2017',v:'2017'},{n:'2016',v:'2016'},
|
|
{n:'2015',v:'2015'},{n:'2014',v:'2014'},{n:'更早',v:'2009'}
|
|
]},
|
|
{ key: 'sort', name: '排序', value: [
|
|
{n:'时间',v:'time'},{n:'人气',v:'hits'},{n:'评分',v:'score'}
|
|
]}
|
|
],
|
|
'2': [
|
|
{ key: 'class', name: '类型', value: [
|
|
{n:'全部',v:'2'},{n:'国产剧',v:'17'},{n:'欧美剧',v:'21'},{n:'日韩剧',v:'20'},
|
|
{n:'港台剧',v:'18'},{n:'海外剧',v:'22'}
|
|
]},
|
|
{ key: 'area', name: '地区', value: [
|
|
{n:'全部',v:'0'},{n:'大陆',v:'大陆'},{n:'香港',v:'香港'},{n:'台湾',v:'台湾'},
|
|
{n:'美国',v:'美国'},{n:'日本',v:'日本'},{n:'韩国',v:'韩国'},{n:'英国',v:'英国'}
|
|
]},
|
|
{ key: 'year', name: '年份', value: [
|
|
{n:'全部',v:'0'},{n:'2026',v:'2026'},{n:'2025',v:'2025'},{n:'2024',v:'2024'},
|
|
{n:'2023',v:'2023'},{n:'2022',v:'2022'},{n:'2021',v:'2021'},{n:'2020',v:'2020'},
|
|
{n:'2019',v:'2019'},{n:'2018',v:'2018'},{n:'2017',v:'2017'},{n:'2016',v:'2016'},
|
|
{n:'2015',v:'2015'},{n:'2014',v:'2014'},{n:'更早',v:'2009'}
|
|
]},
|
|
{ key: 'sort', name: '排序', value: [
|
|
{n:'时间',v:'time'},{n:'人气',v:'hits'},{n:'评分',v:'score'}
|
|
]}
|
|
],
|
|
'3': [
|
|
{ key: 'year', name: '年份', value: [
|
|
{n:'全部',v:'0'},{n:'2026',v:'2026'},{n:'2025',v:'2025'},{n:'2024',v:'2024'}
|
|
]},
|
|
{ key: 'sort', name: '排序', value: [
|
|
{n:'时间',v:'time'},{n:'人气',v:'hits'}
|
|
]}
|
|
],
|
|
'4': [
|
|
{ key: 'area', name: '地区', value: [
|
|
{n:'全部',v:'0'},{n:'大陆',v:'大陆'},{n:'日本',v:'日本'},{n:'美国',v:'美国'}
|
|
]},
|
|
{ key: 'year', name: '年份', value: [
|
|
{n:'全部',v:'0'},{n:'2026',v:'2026'},{n:'2025',v:'2025'},{n:'2024',v:'2024'}
|
|
]},
|
|
{ key: 'sort', name: '排序', value: [
|
|
{n:'时间',v:'time'},{n:'人气',v:'hits'}
|
|
]}
|
|
]
|
|
};
|
|
|
|
return JSON.stringify({
|
|
class: classes,
|
|
filters: filters
|
|
});
|
|
}
|
|
|
|
async function homeVod() {
|
|
let html = await fetchUrl(siteUrl);
|
|
if (!html) return JSON.stringify({ list: [] });
|
|
|
|
let videos = parseVideoList(html);
|
|
return JSON.stringify({ list: videos });
|
|
}
|
|
|
|
async function category(tid, pg, filter, extend) {
|
|
let page = parseInt(pg) || 1;
|
|
extend = extend || {};
|
|
|
|
let classVal = extend['class'] || '';
|
|
let areaVal = extend['area'] || '0';
|
|
let yearVal = extend['year'] || '0';
|
|
let sortVal = extend['sort'] || 'time';
|
|
|
|
let c = (classVal && classVal !== '1' && classVal !== '2') ? classVal : '';
|
|
let a = areaVal !== '0' ? areaVal : '';
|
|
let y = yearVal !== '0' ? yearVal : '';
|
|
|
|
let url;
|
|
if (c || a || y) {
|
|
url = `${siteUrl}/movie/fenlei${tid}--${c}--${a}--${y}---${page}---.html`;
|
|
} else {
|
|
url = `${siteUrl}/movie/fenlei${tid}---${page}.html`;
|
|
}
|
|
|
|
let html = await fetchUrl(url);
|
|
let videos = html ? parseVideoList(html) : [];
|
|
|
|
return JSON.stringify({
|
|
list: videos,
|
|
page: page,
|
|
pagecount: 9999,
|
|
limit: 90,
|
|
total: 999999
|
|
});
|
|
}
|
|
|
|
async function detail(id) {
|
|
let url = id.startsWith('http') ? id : siteUrl + (id.startsWith('/') ? '' : '/') + id;
|
|
let html = await fetchUrl(url);
|
|
if (!html) return JSON.stringify({ list: [] });
|
|
|
|
const $ = load(html);
|
|
let info = { vod_id: id };
|
|
|
|
// 标题
|
|
let title = $('h1.title').text().trim() || $('h1').text().trim();
|
|
if (title) info.vod_name = cleanText(title);
|
|
|
|
// 简介
|
|
let content = $('span.detail-content').html() || $('p.col-pd').html() || '';
|
|
if (content) {
|
|
info.vod_content = cleanText(content.replace(/<[^>]+>/g, ''));
|
|
}
|
|
|
|
// 海报
|
|
let pic = $('img.img-responsive').attr('src') || '';
|
|
if (!pic) {
|
|
$('img.lazy').each((i, n) => {
|
|
let src = $(n).attr('src') || $(n).attr('data-original') || '';
|
|
if (src) pic = src;
|
|
});
|
|
}
|
|
if (pic && !pic.startsWith('http')) {
|
|
pic = siteUrl + (pic.startsWith('/') ? '' : '/') + pic;
|
|
}
|
|
if (pic) info.vod_pic = pic;
|
|
|
|
// 用正则提取详情信息(和Python版一致)
|
|
let actorMatch = html.match(/<span class="text-muted">主演:<\/span>([\s\S]*?)<\/p>/);
|
|
if (actorMatch) info.vod_actor = cleanText(actorMatch[1].replace(/<[^>]+>/g, ''));
|
|
|
|
let directorMatch = html.match(/<span class="text-muted">导演:<\/span>([\s\S]*?)<\/p>/);
|
|
if (directorMatch) info.vod_director = cleanText(directorMatch[1].replace(/<[^>]+>/g, ''));
|
|
|
|
let areaMatch = html.match(/<span class="text-muted">地区:<\/span>([\s\S]*?)<\/p>/);
|
|
if (areaMatch) info.vod_area = cleanText(areaMatch[1].replace(/<[^>]+>/g, ''));
|
|
|
|
let yearMatch = html.match(/<span class="text-muted">年份:<\/span>([\s\S]*?)<\/p>/);
|
|
if (yearMatch) info.vod_year = cleanText(yearMatch[1].replace(/<[^>]+>/g, ''));
|
|
|
|
let typeMatch = html.match(/<span class="text-muted">类型:<\/span>([\s\S]*?)<\/p>/);
|
|
if (typeMatch) info.type_name = cleanText(typeMatch[1].replace(/<[^>]+>/g, ''));
|
|
|
|
// 播放列表(和Python版逻辑一致)
|
|
let ktabs = [];
|
|
let klists = [];
|
|
|
|
// 提取线路名称
|
|
let tabs = [];
|
|
let tabMatches = html.match(/<li[^>]*>.*?<a[^>]*href="#playlist\d+"[^>]*>([\s\S]*?)<\/a>/g) || [];
|
|
tabMatches.forEach(tab => {
|
|
let nameMatch = tab.match(/<a[^>]*>([\s\S]*?)<\/a>/);
|
|
if (nameMatch) {
|
|
let name = cleanText(nameMatch[1].replace(/<[^>]+>/g, ''));
|
|
if (name && !['猜您喜欢','同类型','大家还在看','影片评论','播放地址'].includes(name)) {
|
|
tabs.push(name);
|
|
}
|
|
}
|
|
});
|
|
|
|
// 提取剧集
|
|
let ulMatches = html.match(/<ul[^>]*class="stui-content__playlist[^"]*"[^>]*>([\s\S]*?)<\/ul>/g) || [];
|
|
ulMatches.forEach((ulContent, idx) => {
|
|
let liMatches = ulContent.match(/<li[^>]*>.*?<a[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g) || [];
|
|
let episodes = [];
|
|
liMatches.forEach(li => {
|
|
let hrefMatch = li.match(/href="([^"]+)"/);
|
|
let nameMatch = li.match(/<a[^>]*>([\s\S]*?)<\/a>/);
|
|
if (hrefMatch && nameMatch) {
|
|
let epLink = hrefMatch[1];
|
|
let epName = cleanText(nameMatch[1]);
|
|
if (epName && epLink) {
|
|
if (!epLink.startsWith('http')) {
|
|
epLink = siteUrl + (epLink.startsWith('/') ? '' : '/') + epLink;
|
|
}
|
|
episodes.push(`${epName}$${epLink}`);
|
|
}
|
|
}
|
|
});
|
|
if (episodes.length > 0) {
|
|
let sourceName = tabs[idx] || `播放源${idx+1}`;
|
|
ktabs.push(sourceName);
|
|
klists.push(episodes.join('#'));
|
|
}
|
|
});
|
|
|
|
// 备用:直接找所有播放链接
|
|
if (ktabs.length === 0) {
|
|
let allLinks = html.match(/<a[^>]*href="([^"]*)"[^>]*>([^<]+)<\/a>/g) || [];
|
|
let episodes = [];
|
|
allLinks.forEach(link => {
|
|
let hrefMatch = link.match(/href="([^"]+)"/);
|
|
let nameMatch = link.match(/<a[^>]*>([^<]+)<\/a>/);
|
|
if (hrefMatch && nameMatch) {
|
|
let epLink = hrefMatch[1];
|
|
let epName = cleanText(nameMatch[1]);
|
|
if (epLink && epName && epLink.includes('/play/')) {
|
|
if (!['立即播放','播放','观看'].includes(epName)) {
|
|
if (!epLink.startsWith('http')) {
|
|
epLink = siteUrl + (epLink.startsWith('/') ? '' : '/') + epLink;
|
|
}
|
|
episodes.push(`${epName}$${epLink}`);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
if (episodes.length > 0) {
|
|
ktabs.push('茶杯狐');
|
|
klists.push(episodes.join('#'));
|
|
}
|
|
}
|
|
|
|
info.vod_play_from = ktabs.join('$$$') || '茶杯狐';
|
|
info.vod_play_url = klists.join('$$$') || '';
|
|
|
|
return JSON.stringify({ list: [info] });
|
|
}
|
|
|
|
async function play(flag, id, flags) {
|
|
let url = id.startsWith('http') ? id : siteUrl + (id.startsWith('/') ? '' : '/') + id;
|
|
let html = await fetchUrl(url);
|
|
if (!html) {
|
|
return JSON.stringify({ parse: 1, url: url });
|
|
}
|
|
|
|
// 提取m3u8地址(和Python版一致)
|
|
let realUrl = extractM3u8(html);
|
|
|
|
if (realUrl) {
|
|
realUrl = realUrl.replace(/\\\//g, '/');
|
|
if (realUrl.startsWith('//')) {
|
|
realUrl = 'https:' + realUrl;
|
|
}
|
|
let parse = /\.(m3u8|mp4|mkv|flv|ts)$/i.test(realUrl) ? 0 : 1;
|
|
return JSON.stringify({
|
|
parse: parse,
|
|
url: realUrl,
|
|
header: {
|
|
'User-Agent': headers['User-Agent'],
|
|
'Referer': siteUrl
|
|
}
|
|
});
|
|
}
|
|
|
|
// 如果没找到,尝试iframe(和Python版一致)
|
|
let iframeMatch = html.match(/<iframe[^>]*src=["']([^"']+)["']/i);
|
|
if (iframeMatch) {
|
|
let src = iframeMatch[1];
|
|
if (src.startsWith('//')) src = 'https:' + src;
|
|
else if (src.startsWith('/')) src = siteUrl + src;
|
|
let iframeHtml = await fetchUrl(src);
|
|
if (iframeHtml) {
|
|
let iframeUrl = extractM3u8(iframeHtml);
|
|
if (iframeUrl) {
|
|
return JSON.stringify({
|
|
parse: /\.(m3u8|mp4|mkv|flv|ts)$/i.test(iframeUrl) ? 0 : 1,
|
|
url: iframeUrl,
|
|
header: {
|
|
'User-Agent': headers['User-Agent'],
|
|
'Referer': siteUrl
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return JSON.stringify({
|
|
parse: 1,
|
|
url: url,
|
|
header: {
|
|
'User-Agent': headers['User-Agent'],
|
|
'Referer': siteUrl
|
|
}
|
|
});
|
|
}
|
|
|
|
async function search(wd, quick, pg) {
|
|
let page = parseInt(pg) || 1;
|
|
let encodedKey = encodeURIComponent(wd);
|
|
let url = `${siteUrl}/vodsearch/wd/${encodedKey}.html`;
|
|
|
|
let html = await fetchUrl(url);
|
|
let videos = html ? parseSearchList(html) : [];
|
|
|
|
return JSON.stringify({
|
|
list: videos,
|
|
page: page,
|
|
pagecount: 9999,
|
|
limit: 90,
|
|
total: 999999
|
|
});
|
|
}
|
|
|
|
// ============ 辅助解析函数 ============
|
|
|
|
function parseVideoList(html) {
|
|
let videos = [];
|
|
const $ = load(html);
|
|
|
|
$('.stui-vodlist__box, .stui-vodlist__item').each((i, n) => {
|
|
let link = $(n).find('.stui-vodlist__thumb');
|
|
if (!link.length) return;
|
|
|
|
let name = cleanText(link.attr('title') || '');
|
|
let pic = link.attr('data-original') || link.attr('src') || '';
|
|
let href = link.attr('href') || '';
|
|
let remark = $(n).find('.pic-text.text-right').text().trim() || '';
|
|
|
|
if (pic && !pic.startsWith('http')) {
|
|
pic = siteUrl + (pic.startsWith('/') ? '' : '/') + pic;
|
|
}
|
|
if (href && !href.startsWith('http')) {
|
|
href = siteUrl + (href.startsWith('/') ? '' : '/') + href;
|
|
}
|
|
|
|
if (name && href) {
|
|
videos.push({
|
|
vod_id: href,
|
|
vod_name: name,
|
|
vod_pic: pic,
|
|
vod_remarks: cleanText(remark)
|
|
});
|
|
}
|
|
});
|
|
|
|
return videos;
|
|
}
|
|
|
|
function parseSearchList(html) {
|
|
let videos = [];
|
|
const $ = load(html);
|
|
|
|
$('.stui-vodlist__box, .stui-vodlist__media li, .stui-vodlist__item').each((i, n) => {
|
|
let link = $(n).find('.stui-vodlist__thumb');
|
|
if (!link.length) return;
|
|
|
|
let name = cleanText(link.attr('title') || '');
|
|
let pic = link.attr('data-original') || link.attr('src') || '';
|
|
let href = link.attr('href') || '';
|
|
let remark = $(n).find('.pic-text.text-right, .pic-text').text().trim() || '';
|
|
|
|
if (pic && !pic.startsWith('http')) {
|
|
pic = siteUrl + (pic.startsWith('/') ? '' : '/') + pic;
|
|
}
|
|
if (href && !href.startsWith('http')) {
|
|
href = siteUrl + (href.startsWith('/') ? '' : '/') + href;
|
|
}
|
|
|
|
if (name && href) {
|
|
videos.push({
|
|
vod_id: href,
|
|
vod_name: name,
|
|
vod_pic: pic,
|
|
vod_remarks: cleanText(remark)
|
|
});
|
|
}
|
|
});
|
|
|
|
return videos;
|
|
}
|
|
|
|
function extractM3u8(html) {
|
|
let patterns = [
|
|
/"url"\s*:\s*"([^"]+\.m3u8[^"]*)"/,
|
|
/url\s*:\s*"([^"]+\.m3u8[^"]*)"/,
|
|
/"url"\s*:\s*'([^']+\.m3u8[^']*)'/,
|
|
/https?:\/\/[^"']+\.m3u8[^"']*/
|
|
];
|
|
|
|
for (let p of patterns) {
|
|
let match = html.match(p);
|
|
if (match) {
|
|
return match[1] || match[0];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// ============ 导出 ============
|
|
export function __jsEvalReturn() {
|
|
return {
|
|
init: init,
|
|
home: home,
|
|
homeVod: homeVod,
|
|
category: category,
|
|
detail: detail,
|
|
play: play,
|
|
search: search
|
|
};
|
|
} |