Files
tvbox_collect/jaychouqq/yingshi/js7/wbbb.js
T
2026-06-29 03:41:14 +00:00

525 lines
16 KiB
JavaScript

const axios = require('axios');
const http = require('http');
const https = require('https');
const vm = require('vm');
const CryptoJS = require('crypto-js');
const SITE = {
key: 'wbbb_note',
name: '歪比巴卜',
api: '/video/wbbb_note',
host: 'https://wbbb1.com',
ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36',
timeout: 20000,
};
const CATS = [
{ type_id: '1', type_name: '电影' },
{ type_id: '2', type_name: '剧集' },
{ type_id: '3', type_name: '动漫' },
{ type_id: '4', type_name: '综艺' },
];
const LINE_DIRECT_RE = /^(?:lzm3u8|bfzym3u8)$/i;
const httpClient = axios.create({
timeout: SITE.timeout,
httpAgent: new http.Agent({ keepAlive: true }),
httpsAgent: new https.Agent({ keepAlive: true, rejectUnauthorized: false }),
validateStatus: () => true,
headers: {
'User-Agent': SITE.ua,
'Referer': `${SITE.host}/`,
'Accept-Language': 'zh-CN,zh;q=0.9',
},
});
function ok(res) {
if (res.status < 200 || res.status >= 300) {
throw new Error(`HTTP ${res.status}`);
}
return res.data || '';
}
async function getHtml(url) {
const full = /^https?:\/\//.test(url) ? url : `${SITE.host}${url}`;
const res = await httpClient.get(full);
return ok(res);
}
function pickMatch(str, reg, idx = 1, def = '') {
const m = str.match(reg);
return m ? (m[idx] || def) : def;
}
function stripTags(str = '') {
return String(str)
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
.replace(/<[^>]+>/g, ' ')
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/\s+/g, ' ')
.trim();
}
function cleanSlashText(str = '') {
return stripTags(str)
.replace(/\s*\/\s*/g, '/')
.replace(/^\/+|\/+$/g, '')
.trim();
}
function absUrl(url = '') {
if (!url) return '';
if (/^https?:\/\//i.test(url)) return url;
if (url.startsWith('//')) return `https:${url}`;
if (url.startsWith('/')) return `${SITE.host}${url}`;
return `${SITE.host}/${url}`;
}
function normalizeVodFromCard(block) {
const vod_id = pickMatch(block, /href="\/detail\/(\d+)\.html"/, 1, '');
const vod_name = stripTags(pickMatch(block, /<div class="module-(?:poster-item-title|card-item-title)">([\s\S]*?)<\/div>/, 1, '')) || pickMatch(block, /alt="([^"]+)"/, 1, '');
const vod_pic = absUrl(pickMatch(block, /(?:data-original|data-src|src)="([^"]+)"/, 1, ''));
const vod_remarks = stripTags(pickMatch(block, /<div class="module-item-note">([\s\S]*?)<\/div>/, 1, ''));
return {
vod_id,
vod_name,
vod_pic,
vod_remarks,
};
}
function parseCards(html) {
const list = [];
const reg = /<a href="\/detail\/(\d+)\.html" title="([^"]+)" class="module-poster-item module-item">([\s\S]*?)<\/a>/g;
let m;
while ((m = reg.exec(html)) !== null) {
const block = m[0];
list.push({
vod_id: m[1],
vod_name: m[2],
vod_pic: absUrl(pickMatch(block, /(?:data-original|data-src|src)="([^"]+)"/, 1, '')),
vod_remarks: stripTags(pickMatch(block, /<div class="module-item-note">([\s\S]*?)<\/div>/, 1, '')),
});
}
if (list.length) return list;
const reg2 = /<div class="module-card-item module-item">([\s\S]*?)<\/div>\s*<\/div>/g;
while ((m = reg2.exec(html)) !== null) {
const vod = normalizeVodFromCard(m[1]);
if (vod.vod_id && vod.vod_name) list.push(vod);
}
return list;
}
function parseTabs(detailHtml) {
const tabs = [];
const regs = [
/<div class="module-tab-item tab-item"[^>]*data-dropdown-value="([^"]+)"[\s\S]*?<small>([^<]*)<\/small>/g,
/<div class="module-tab-item[^"]*"[^>]*data-dropdown-value="([^"]+)"/g,
];
for (const reg of regs) {
let m;
while ((m = reg.exec(detailHtml)) !== null) {
const name = (m[1] || m[2] || '').trim();
if (name) tabs.push(name);
}
if (tabs.length) break;
}
return tabs;
}
function parsePlayGroups(detailHtml) {
const groups = [];
const blockReg = /<div class="module-play-list-content[^"]*">([\s\S]*?)<\/div>/g;
let m;
while ((m = blockReg.exec(detailHtml)) !== null) {
const block = m[1];
const items = [];
const aReg = /<a[^>]+class="module-play-list-link[^"]*"[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g;
let a;
while ((a = aReg.exec(block)) !== null) {
const href = absUrl(a[1]);
const label = stripTags(a[2]).trim();
if (!href || !label) continue;
items.push(`${label}$${href}`);
}
if (items.length) groups.push(items);
}
if (groups.length) return groups;
const hrefOnly = [...detailHtml.matchAll(/<a[^>]+href="([^"]*\/(?:vplay|play)\/[^"#]+)"[^>]*>([\s\S]*?)<\/a>/g)];
if (hrefOnly.length) {
return [hrefOnly.map((m) => `${stripTags(m[2]).trim() || '播放'}$${absUrl(m[1])}`)];
}
return groups;
}
function parsePlayer(html) {
const patterns = [
/var\s+player_aaaa\s*=\s*(\{.*?\})<\/script>/s,
/var\s+player_.*?=\s*(\{.*?\})<\/script>/s,
/player_aaaa\s*=\s*(\{.*?\})\s*;/s,
/player_data\s*=\s*(\{.*?\})\s*;/s,
];
for (const reg of patterns) {
const m = html.match(reg);
if (!m) continue;
try {
return JSON.parse(m[1]);
} catch (_) {}
}
return null;
}
function decodePlayUrl(raw, encrypt) {
let url = String(raw || '').trim();
const enc = String(encrypt || '').trim();
if (enc === '1') {
try { url = unescape(url); } catch (_) {}
} else if (enc === '2') {
try { url = Buffer.from(url, 'base64').toString('utf8'); } catch (_) {}
try { url = unescape(url); } catch (_) {}
}
if (url.startsWith('//')) {
url = `https:${url}`;
} else if (url.startsWith('/')) {
url = absUrl(url);
}
return url;
}
function isDirectMediaUrl(url, from) {
return /^https?:\/\//i.test(url)
|| /\.(?:m3u8|mp4)(?:\?|$)/i.test(url)
|| LINE_DIRECT_RE.test(from || '');
}
async function resolveWbbbPlayerUrl(playerToken, nextUrl, title) {
const parsePageUrl = `https://xn--qvr2v.850088.xyz/player/?url=${encodeURIComponent(playerToken)}${nextUrl ? `&next=${encodeURIComponent(nextUrl)}` : ''}${title ? `&title=${encodeURIComponent(title)}` : ''}`;
const settingJs = await getHtml('https://xn--qvr2v.850088.xyz/player/artplayer/js/setting.js');
let postCall = null;
const dummy = new Proxy(function () {}, { get: () => dummy, apply: () => dummy });
const loc = new URL(parsePageUrl);
const el = { style: {}, addEventListener() {}, appendChild() {}, setAttribute() {}, remove() {}, innerHTML: '' };
const context = {
console,
CryptoJS,
setTimeout: () => 0,
clearTimeout() {},
setInterval: () => 0,
clearInterval() {},
fetch: async () => ({ json: async () => ({ data: [] }) }),
localStorage: { getItem: () => null, setItem() {}, removeItem() {} },
navigator: { userAgent: SITE.ua, platform: 'Linux', appVersion: 'Linux' },
location: { href: loc.href, search: loc.search, origin: loc.origin, hostname: loc.hostname, host: loc.host, pathname: loc.pathname, toString() { return this.href; } },
window: null,
document: {
title: title || '',
referrer: `${SITE.host}/`,
querySelector: () => null,
getElementById: () => el,
createElement: () => ({ ...el }),
body: { appendChild() {} },
head: { appendChild() {} },
getElementsByTagName: () => [{ parentNode: { insertBefore() {} } }],
addEventListener() {},
write() {},
},
URLSearchParams,
URL,
Math,
Date,
JSON,
encodeURIComponent,
decodeURIComponent,
escape,
unescape,
atob: (s) => Buffer.from(s, 'base64').toString('binary'),
btoa: (s) => Buffer.from(s, 'binary').toString('base64'),
Artplayer: function () { return dummy; },
artplayerPluginDanmuku: () => dummy,
Hls: { isSupported: () => false },
flvjs: { isSupported: () => false },
dashjs: { MediaPlayer: () => ({ create: () => ({ initialize() {} }) }) },
};
const $ = function () {
return {
hide() { return this; }, show() { return this; }, text() { return this; }, html() { return this; },
on() { return this; }, css() { return this; }, remove() { return this; }, append() { return this; },
get() { return [{ play: () => Promise.resolve(), pause() {}, currentTime: 0 }]; },
};
};
$.ajaxSettings = {};
$.ajaxSetup = (o) => Object.assign($.ajaxSettings, o || {});
$.post = (url, data, cb) => { postCall = { url, data }; if (cb) cb({ code: 500, msg: 'stub' }); return { fail() {} }; };
$.get = (...args) => { const cb = args.find((x) => typeof x === 'function'); if (cb) cb({}); return { fail() {} }; };
context.$ = context.jQuery = $;
context.window = context;
vm.createContext(context);
try { vm.runInContext(settingJs, context, { timeout: 5000 }); } catch (_) {}
if (!context.stray || typeof context.stray.start !== 'function') throw new Error('player bootstrap missing');
try { context.stray.start(); } catch (_) {}
if (!postCall || !postCall.url || !postCall.data) throw new Error('player api call capture failed');
const apiUrl = /^https?:\/\//i.test(postCall.url) ? postCall.url : `https://xn--qvr2v.850088.xyz/player/${String(postCall.url).replace(/^\//, '')}`;
const apiRes = await httpClient.post(apiUrl, new URLSearchParams(postCall.data).toString(), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest',
'Referer': parsePageUrl,
},
});
const payload = typeof apiRes.data === 'string' ? JSON.parse(apiRes.data) : apiRes.data;
if (!payload || Number(payload.code) !== 200 || !payload.url) throw new Error(payload && payload.msg ? payload.msg : 'parse api failed');
context.stray = context.stray || {};
Object.assign(context.stray, payload);
if (typeof context.decrypt === 'function' && payload.aes_key && payload.aes_iv) {
const finalUrl = context.decrypt(payload.url);
if (finalUrl) return { url: finalUrl, type: payload.type || '' };
}
return { url: payload.url, type: payload.type || '' };
}
async function home() {
return { class: CATS, list: [] };
}
async function category({ id, page }) {
const pg = parseInt(page || '1', 10) || 1;
const path = pg > 1 ? `/show/${id}--------${pg}---.html` : `/show/${id}-----------.html`;
const html = await getHtml(path);
const list = parseCards(html);
const hasNext = html.includes('title="下一页"');
return {
list,
page: pg,
pagecount: hasNext ? pg + 1 : pg,
total: list.length,
};
}
async function detail({ id }) {
const list = [];
for (const rawId of id) {
const mediaId = String(rawId).trim();
const html = await getHtml(`/detail/${mediaId}.html`);
const vod_name = stripTags(pickMatch(html, /<h1[^>]*>([\s\S]*?)<\/h1>/, 1, ''));
const vod_pic = absUrl(pickMatch(html, /<div class="module-item-pic">[\s\S]*?<img[^>]+data-original="([^"]+)"/, 1, ''))
|| absUrl(pickMatch(html, /<div class="module-item-pic">[\s\S]*?<img[^>]+(?:data-src|src)="([^"]+)"/, 1, ''));
const vod_content = stripTags(pickMatch(html, /<div class="module-info-introduction-content[^>]*>([\s\S]*?)<\/div>/, 1, ''));
const tagLinks = [...html.matchAll(/<div class="module-info-tag-link">([\s\S]*?)<\/div>/g)].map(x => x[1]);
const vod_year = stripTags(tagLinks[0] || '');
const vod_area = stripTags(tagLinks[1] || '');
const vod_type = cleanSlashText(tagLinks[2] || '');
const vod_remarks = stripTags(pickMatch(html, /<div class="module-item-note">([\s\S]*?)<\/div>/, 1, '')) || stripTags(pickMatch(html, /更新至[^<\s]+/, 0, ''));
const vod_director = cleanSlashText(pickMatch(html, /导演:[\s\S]*?<div[^>]*class="module-info-item-content">([\s\S]*?)<\/div>/, 1, ''));
const vod_actor = cleanSlashText(pickMatch(html, /主演:[\s\S]*?<div[^>]*class="module-info-item-content">([\s\S]*?)<\/div>/, 1, ''));
const tabs = parseTabs(html);
const groups = parsePlayGroups(html);
const playFrom = [];
const playUrl = [];
groups.forEach((items, idx) => {
const lineName = tabs[idx] || `线路${idx + 1}`;
if (!items || !items.length) return;
playFrom.push(lineName);
playUrl.push(items.join('#'));
});
list.push({
vod_id: mediaId,
vod_name,
vod_pic,
type_name: vod_type,
vod_year,
vod_area,
vod_remarks,
vod_actor,
vod_director,
vod_content,
vod_play_from: playFrom.join('$$$'),
vod_play_url: playUrl.join('$$$'),
});
}
return { list };
}
async function search({ page, wd }) {
const pg = parseInt(page || '1', 10) || 1;
const path = pg > 1
? `/search/-------------.html?wd=${encodeURIComponent(wd)}&page=${pg}`
: `/search/-------------.html?wd=${encodeURIComponent(wd)}`;
const html = await getHtml(path);
const list = parseCards(html);
return {
list,
page: pg,
pagecount: list.length >= 20 ? pg + 1 : pg,
total: list.length,
};
}
async function play({ id }) {
const html = await getHtml(id);
const player = parsePlayer(html);
if (!player) {
return {
parse: 1,
jx: 1,
url: id,
};
}
let url = decodePlayUrl(player.url, player.encrypt);
const from = String(player.from || '').trim();
const nextUrl = player.link_next ? absUrl(player.link_next) : '';
const title = player.vod_data && player.vod_data.vod_name ? String(player.vod_data.vod_name) : '';
if (isDirectMediaUrl(url, from)) {
return {
parse: 0,
jx: 0,
url,
header: {
'User-Agent': SITE.ua,
'Referer': `${SITE.host}/`,
},
};
}
const parseApi = String(player.parse || player.jx || '').trim();
if (parseApi) {
const jump = /^https?:\/\//i.test(parseApi)
? `${parseApi}${encodeURIComponent(url)}`
: absUrl(`${parseApi}${encodeURIComponent(url)}`);
return {
parse: 0,
jx: 0,
url: jump,
header: {
'User-Agent': SITE.ua,
'Referer': `${SITE.host}/`,
},
};
}
try {
const resolved = await resolveWbbbPlayerUrl(String(player.url || ''), nextUrl, title);
if (resolved && resolved.url) {
return {
parse: 0,
jx: 0,
url: resolved.url,
header: {
'User-Agent': SITE.ua,
'Referer': `${SITE.host}/`,
},
};
}
} catch (_) {}
const parseCandidates = [
'/static/js/playerconfig.js',
`/static/player/${from}.js`,
`/static/player/config.js`,
'/js/playerconfig.js',
'/player/config.js',
];
for (const p of parseCandidates) {
try {
const conf = await getHtml(p);
const escapedFrom = from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regs = [
new RegExp(`player_list\\s*\\[\\s*['\"]${escapedFrom}['\"]\\s*\\]\\s*=\\s*\\{[\\s\\S]*?parse\\s*:\\s*['\"]([^'\"]+)['\"]`, 'i'),
new RegExp(`${escapedFrom}[\\s\\S]*?parse\\s*[:=]\\s*['\"]([^'\"]+)['\"]`, 'i'),
new RegExp(`"${escapedFrom}"\\s*:\\s*\\{[\\s\\S]*?"parse"\\s*:\\s*"([^"]*)"`, 'i'),
/MacPlayerConfig[\s\S]*?parse['"]?\s*[:=]\s*['"]([^'"]+)['"]/i,
];
for (const reg of regs) {
const m = conf.match(reg);
if (!m || typeof m[1] !== 'string') continue;
const prefix = m[1].trim();
if (!prefix) continue;
const jump = /^https?:\/\//i.test(prefix)
? `${prefix}${encodeURIComponent(url)}`
: absUrl(`${prefix}${encodeURIComponent(url)}`);
return {
parse: 0,
jx: 0,
url: jump,
header: {
'User-Agent': SITE.ua,
'Referer': `${SITE.host}/`,
},
};
}
} catch (_) {}
}
return {
parse: 1,
jx: 1,
url,
header: {
'User-Agent': SITE.ua,
'Referer': `${SITE.host}/`,
},
};
}
const meta = {
key: SITE.key,
name: SITE.name,
type: 4,
api: SITE.api,
searchable: 1,
quickSearch: 1,
changeable: 0,
};
module.exports = async (app, opt) => {
app.get(meta.api, async (req) => {
try {
const { ac, t, pg, ids, play: playId, wd } = req.query;
if (playId) {
return await play({ id: playId });
}
if (wd) {
return await search({ page: pg, wd });
}
if (!ac) {
return await home();
}
if (ac === 'detail') {
if (t) {
return await category({ id: t, page: pg });
}
if (ids) {
return await detail({ id: ids.split(',').map(x => x.trim()).filter(Boolean) });
}
}
return { class: CATS, list: [] };
} catch (e) {
return {
class: CATS,
list: [],
error: e.message || String(e),
};
}
});
opt.sites.push(meta);
};