mirror of
https://github.com/ls125781003/tvboxtg.git
synced 2026-11-03 18:23:27 +00:00
更新
This commit is contained in:
1 parent
b75d009cbe
commit
565a401ef8
36 files changed
+11267
-5195
No files matched your search
+833
@@ -0,0 +1,833 @@
|
||||
import { Crypto, _ } from 'assets://js/lib/cat.js';
|
||||
|
||||
let siteUrl = '';
|
||||
let key = '';
|
||||
let iv = '';
|
||||
let siteKey = '';
|
||||
let siteType = 3;
|
||||
let apiPrefix = 'getappapi.index';
|
||||
|
||||
const prefixMap = {
|
||||
'1': 'getappapi.index',
|
||||
'2': 'qijiappapi.index',
|
||||
'3': 'appapi'
|
||||
};
|
||||
|
||||
let enableVerifyTimeSign = false;
|
||||
let headers = {
|
||||
'User-Agent': 'okhttp/3.10.0',
|
||||
'app-user-device-id': '291b226282010337c9443590d6457be15',
|
||||
'app-version-code': '112'
|
||||
};
|
||||
|
||||
let parseMap = {};
|
||||
let homeVods = [];
|
||||
let Searchstatus = false;
|
||||
let searchApiSuffix = '';
|
||||
let souParamName = '';
|
||||
let souSalt = '';
|
||||
let extraSearchHeaders = {};
|
||||
let initSuffix = 'init';
|
||||
let hasCustomInit = false;
|
||||
let thirdDanmuBaseUrl = '';
|
||||
let customPlayUa = ''; // 新增:自定义播放链接UA
|
||||
|
||||
let request = async (reqUrl, data, header, method) => {
|
||||
let finalHeaders = { ...headers };
|
||||
if (enableVerifyTimeSign) {
|
||||
const timestamp = Math.floor(Date.now() / 1000).toString();
|
||||
const sign = aesEncode(timestamp, key, iv);
|
||||
finalHeaders['app-api-verify-time'] = timestamp;
|
||||
finalHeaders['app-api-verify-sign'] = sign;
|
||||
}
|
||||
if (header) {
|
||||
finalHeaders = { ...finalHeaders, ...header };
|
||||
}
|
||||
let res = await req(reqUrl, {
|
||||
method: method || 'get',
|
||||
data: data || '',
|
||||
headers: finalHeaders,
|
||||
postType: method === 'post' ? 'form' : '',
|
||||
timeout: 10000,
|
||||
});
|
||||
return res.content;
|
||||
};
|
||||
|
||||
let request_text = async (reqUrl, data, header, method, tobase64) => {
|
||||
let finalHeaders = { ...headers };
|
||||
if (enableVerifyTimeSign) {
|
||||
const timestamp = Math.floor(Date.now() / 1000).toString();
|
||||
const sign = aesEncode(timestamp, key, iv);
|
||||
finalHeaders['app-api-verify-time'] = timestamp;
|
||||
finalHeaders['app-api-verify-sign'] = sign;
|
||||
}
|
||||
if (header) {
|
||||
finalHeaders = { ...finalHeaders, ...header };
|
||||
}
|
||||
let optObj = {
|
||||
headers: finalHeaders,
|
||||
method: method || 'get',
|
||||
data: method === 'post' ? data : undefined,
|
||||
postType: method === 'post' ? (data ? 'raw' : 'form') : undefined,
|
||||
timeout: 10000,
|
||||
};
|
||||
if (tobase64) {
|
||||
optObj.buffer = 2;
|
||||
}
|
||||
let res = await req(reqUrl, optObj);
|
||||
return res.content;
|
||||
};
|
||||
|
||||
async function init(cfg) {
|
||||
siteKey = cfg.skey;
|
||||
siteType = cfg.stype;
|
||||
if (!cfg.ext) return;
|
||||
let extObj;
|
||||
if (typeof cfg.ext === 'string') {
|
||||
extObj = JSON.parse(cfg.ext);
|
||||
} else if (typeof cfg.ext === 'object' && cfg.ext !== null) {
|
||||
extObj = cfg.ext;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if (extObj.init && typeof extObj.init === 'string' && extObj.init.startsWith('V')) {
|
||||
initSuffix = 'init' + extObj.init;
|
||||
hasCustomInit = true;
|
||||
}
|
||||
if (extObj.time !== undefined) {
|
||||
const timeVal = String(extObj.time);
|
||||
enableVerifyTimeSign = (timeVal === '1' || timeVal === 'true' || timeVal === '1');
|
||||
}
|
||||
if (extObj.code !== undefined) {
|
||||
forceVerifyCode = String(extObj.code).trim();
|
||||
}
|
||||
// 新增:读取自定义播放UA
|
||||
if (extObj.ua2 !== undefined) {
|
||||
customPlayUa = String(extObj.ua2).trim();
|
||||
}
|
||||
if (extObj.head && typeof extObj.head === 'string') {
|
||||
const headStr = extObj.head.trim();
|
||||
if (headStr) {
|
||||
const items = headStr.split(',');
|
||||
for (let item of items) {
|
||||
const trimmed = item.trim();
|
||||
if (!trimmed) continue;
|
||||
const colonIndex = trimmed.indexOf(':');
|
||||
if (colonIndex > 0) {
|
||||
const hKey = trimmed.substring(0, colonIndex).trim();
|
||||
const hValue = trimmed.substring(colonIndex + 1).trim();
|
||||
if (hKey && hValue) {
|
||||
headers[hKey] = hValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let host = extObj.host || '';
|
||||
if (host) {
|
||||
if (host.startsWith('http') && (host.endsWith('.txt') || host.endsWith('.json'))) {
|
||||
let response = await request(host, null, headers, 'get');
|
||||
let urls = response.split('\n').map(line => line.trim()).filter(line => line && line.startsWith('http'));
|
||||
if (urls.length > 0) {
|
||||
host = urls[0];
|
||||
}
|
||||
}
|
||||
siteUrl = host.replace(/\/+$/, '');
|
||||
if (!siteUrl.includes('php')) {
|
||||
siteUrl += '/api.php';
|
||||
}
|
||||
}
|
||||
key = extObj.key || '';
|
||||
iv = extObj.iv || key;
|
||||
if (extObj.api && prefixMap[extObj.api]) {
|
||||
apiPrefix = prefixMap[extObj.api];
|
||||
}
|
||||
if (extObj.version) {
|
||||
headers['app-version-code'] = String(extObj.version);
|
||||
}
|
||||
if (extObj.id) {
|
||||
headers['app-user-device-id'] = extObj.id;
|
||||
}
|
||||
if (extObj.token) {
|
||||
headers['app-user-token'] = extObj.token;
|
||||
}
|
||||
if (extObj.ua) {
|
||||
headers['User-Agent'] = extObj.ua;
|
||||
}
|
||||
}
|
||||
|
||||
function processSignatureValue(sig) {
|
||||
if (sig.length < 8) {
|
||||
return sig.split('').reverse().join('');
|
||||
} else {
|
||||
let rear = sig.slice(-8).split('').reverse().join('');
|
||||
let front = sig.slice(0, -8).split('').reverse().join('');
|
||||
return front + rear;
|
||||
}
|
||||
}
|
||||
|
||||
async function home(filter) {
|
||||
let initUrl = `${siteUrl}/${apiPrefix}/${initSuffix}`;
|
||||
let rets = JSON.parse(await request(initUrl)).data;
|
||||
let data = JSON.parse(aesDecode(rets, key, iv));
|
||||
|
||||
let rawDanmuUrl = data.config?.third_danmu_url || '';
|
||||
|
||||
if (Array.isArray(rawDanmuUrl)) {
|
||||
rawDanmuUrl = rawDanmuUrl.find(u => u && typeof u === 'string' && u.trim()) || '';
|
||||
} else if (typeof rawDanmuUrl !== 'string') {
|
||||
rawDanmuUrl = '';
|
||||
}
|
||||
|
||||
if (rawDanmuUrl) {
|
||||
thirdDanmuBaseUrl = rawDanmuUrl.trim();
|
||||
let lower = thirdDanmuBaseUrl.toLowerCase();
|
||||
if (!lower.match(/[?&]url=$/)) {
|
||||
thirdDanmuBaseUrl += thirdDanmuBaseUrl.includes('?') ? '&url=' : '?url=';
|
||||
}
|
||||
} else {
|
||||
thirdDanmuBaseUrl = 'https://dmku.hls.one/?ac=dm&url=';
|
||||
}
|
||||
|
||||
if (data.box_config) {
|
||||
let originalKey = key;
|
||||
let swappedKey = originalKey.split('').reverse().join('');
|
||||
let md5Key = md5(swappedKey);
|
||||
let dynamicIv = md5Key.substring(0, 16);
|
||||
let decrypted = aesDecode(data.box_config, swappedKey, dynamicIv);
|
||||
let boxJson = JSON.parse(decrypted);
|
||||
if (boxJson.search_name) {
|
||||
searchApiSuffix = boxJson.search_name;
|
||||
}
|
||||
if (boxJson.signature_name && boxJson.signature_value) {
|
||||
souParamName = boxJson.signature_name;
|
||||
souSalt = processSignatureValue(boxJson.signature_value);
|
||||
}
|
||||
if (boxJson.api_header && boxJson.api_header.key && boxJson.api_header.value) {
|
||||
extraSearchHeaders[boxJson.api_header.key] = boxJson.api_header.value;
|
||||
}
|
||||
} else {
|
||||
searchApiSuffix = 'searchList';
|
||||
souParamName = '';
|
||||
souSalt = '';
|
||||
}
|
||||
|
||||
Searchstatus = data.config?.system_search_verify_status || false;
|
||||
|
||||
let filters = {};
|
||||
let classes = [];
|
||||
homeVods = [];
|
||||
|
||||
_.forEach(data.type_list, item => {
|
||||
if (item.type_id > 0) {
|
||||
if (item.recommend_list && Array.isArray(item.recommend_list)) {
|
||||
homeVods = homeVods.concat(item.recommend_list);
|
||||
}
|
||||
}
|
||||
|
||||
classes.push({
|
||||
type_id: item.type_id,
|
||||
type_name: item.type_name,
|
||||
});
|
||||
|
||||
let filterList = [];
|
||||
_.forEach(item.filter_type_list, f => {
|
||||
let filter = {};
|
||||
if (f.name === 'class') {
|
||||
filter['name'] = '分类';
|
||||
filter['key'] = f.name;
|
||||
filter['value'] = _.map(f.list, i => ({ v: i, n: i }));
|
||||
}
|
||||
if (f.name === 'area') {
|
||||
filter['name'] = '区域';
|
||||
filter['key'] = f.name;
|
||||
filter['value'] = _.map(f.list, i => ({ v: i, n: i }));
|
||||
}
|
||||
if (f.name === 'lang') {
|
||||
filter['name'] = '语言';
|
||||
filter['key'] = f.name;
|
||||
filter['value'] = _.map(f.list, i => ({ v: i, n: i }));
|
||||
}
|
||||
if (f.name === 'year') {
|
||||
filter['name'] = '年份';
|
||||
filter['key'] = f.name;
|
||||
filter['value'] = _.map(f.list, i => ({ v: i, n: i }));
|
||||
}
|
||||
if (f.name === 'sort') {
|
||||
filter['name'] = '排序';
|
||||
filter['key'] = f.name;
|
||||
filter['value'] = _.map(f.list, i => ({ v: i, n: i }));
|
||||
}
|
||||
if (Object.keys(filter).length > 0) {
|
||||
filterList.push(filter);
|
||||
}
|
||||
});
|
||||
if (filterList.length > 0) {
|
||||
filters[item.type_id] = filterList;
|
||||
}
|
||||
});
|
||||
|
||||
return JSON.stringify({
|
||||
'class': classes,
|
||||
'filters': filters,
|
||||
});
|
||||
}
|
||||
|
||||
async function homeVod() {
|
||||
return JSON.stringify({
|
||||
list: homeVods,
|
||||
});
|
||||
}
|
||||
|
||||
async function category(tid, pg, filter, extend) {
|
||||
if (pg <= 0) pg = 1;
|
||||
let url = `${siteUrl}/${apiPrefix}/typeFilterVodList`;
|
||||
let params = {
|
||||
"area": extend['area'] || "全部",
|
||||
"sort": extend['sort'] || "最新",
|
||||
"class": extend['class'] || "全部",
|
||||
"type_id": tid,
|
||||
"year": extend['year'] || "全部",
|
||||
"lang": extend['lang'] || '全部',
|
||||
"page": pg,
|
||||
};
|
||||
let encData = JSON.parse(await request(url, params, '', 'post')).data;
|
||||
let videos = JSON.parse(aesDecode(encData, key, iv)).recommend_list;
|
||||
return JSON.stringify({
|
||||
page: pg,
|
||||
pagecount: 9999,
|
||||
list: videos,
|
||||
});
|
||||
}
|
||||
|
||||
async function detail(id) {
|
||||
let url = `${siteUrl}/${apiPrefix}/vodDetail`;
|
||||
let resp = await request(url, { vod_id: id }, '', 'post');
|
||||
let jsonResp = JSON.parse(resp);
|
||||
let encData = jsonResp.data;
|
||||
let decoded = aesDecode(encData, key, iv);
|
||||
let info = JSON.parse(decoded);
|
||||
let videos = {
|
||||
vod_id: info.vod.vod_id,
|
||||
vod_name: info.vod.vod_name,
|
||||
vod_area: info.vod.vod_area,
|
||||
vod_director: info.vod.vod_director,
|
||||
vod_actor: info.vod.vod_actor,
|
||||
vod_pic: info.vod.vod_pic,
|
||||
vod_content: info.vod.vod_content,
|
||||
type_name: info.vod.vod_class,
|
||||
vod_year: info.vod.vod_year
|
||||
};
|
||||
let froms = [];
|
||||
let urls = [];
|
||||
let playSources = _.map(info.vod_play_list, item => {
|
||||
const playerInfo = item.player_info || {};
|
||||
const parse = playerInfo.parse || '';
|
||||
const ua = playerInfo.user_agent || customPlayUa || ''; // 修改:优先使用自定义UA,如果没有则使用原有的
|
||||
const nameUrls = _.map(item.urls || [], item2 => {
|
||||
const { name = '', url = '', token = '', parse_api_url = '', nid = 1 } = item2;
|
||||
return `${name}$${url}@@${parse}@@${token}@@${parse_api_url}@@${ua}@@${info.vod.vod_id}@@${nid}`;
|
||||
}).join('#');
|
||||
return {
|
||||
show: playerInfo.show || 'Unknown',
|
||||
urls: nameUrls
|
||||
};
|
||||
});
|
||||
let showCount = {};
|
||||
playSources = _.map(playSources, source => {
|
||||
let showName = source.show;
|
||||
if (showCount[showName]) {
|
||||
showCount[showName]++;
|
||||
showName = `${showName}${showCount[showName]}`;
|
||||
} else {
|
||||
showCount[showName] = 1;
|
||||
}
|
||||
return {
|
||||
show: showName,
|
||||
urls: source.urls
|
||||
};
|
||||
});
|
||||
playSources.sort((a, b) => {
|
||||
const aShow = a.show.toLowerCase();
|
||||
const bShow = b.show.toLowerCase();
|
||||
const getPriority = (show) => {
|
||||
if (show.includes('4k')) return 1;
|
||||
if (show.includes('K')) return 2;
|
||||
if (show.includes('独家')) return 3;
|
||||
if (show.includes('秒播')) return 4;
|
||||
if (show.includes('自建')) return 5;
|
||||
if (show.includes('蓝光')) return 6;
|
||||
if (show.includes('专线')) return 7;
|
||||
return 8;
|
||||
};
|
||||
return getPriority(aShow) - getPriority(bShow);
|
||||
});
|
||||
froms = _.map(playSources, source => source.show);
|
||||
urls = _.map(playSources, source => source.urls);
|
||||
videos.vod_play_from = froms.join('$$$');
|
||||
videos.vod_play_url = urls.join('$$$');
|
||||
return JSON.stringify({
|
||||
list: [videos],
|
||||
});
|
||||
}
|
||||
|
||||
async function play(flag, id, flags) {
|
||||
let parts = id.split('@@');
|
||||
let playUrl = parts[0];
|
||||
let parse = parts[1] || '';
|
||||
let token = parts[2] || '';
|
||||
let parse_api_url = parts[3] || '';
|
||||
let ua = parts[4] || ''; // 新增:获取UA
|
||||
let vodId = parts[5] || '';
|
||||
let nidStr = parts[6] || '1';
|
||||
|
||||
let danmakuUrl = '';
|
||||
if (thirdDanmuBaseUrl && vodId) {
|
||||
let nid = parseInt(nidStr, 10);
|
||||
if (isNaN(nid) || nid < 1) nid = 1;
|
||||
let urlPosition = nid - 1;
|
||||
let danmuParams = {
|
||||
url_position: urlPosition.toString(),
|
||||
vod_id: vodId
|
||||
};
|
||||
let danmuRet = await request(`${siteUrl}/${apiPrefix}/danmuList`, danmuParams, '', 'post');
|
||||
let danmuResponse = JSON.parse(danmuRet);
|
||||
if (danmuResponse.data) {
|
||||
let decryptedDanmu = JSON.parse(aesDecode(danmuResponse.data, key, iv));
|
||||
if (decryptedDanmu && decryptedDanmu.official_url) {
|
||||
let realDanmuUrl = thirdDanmuBaseUrl + decryptedDanmu.official_url;
|
||||
let EncodedUrl = encodeURIComponent(realDanmuUrl);
|
||||
let headerJson = JSON.stringify(headers);
|
||||
danmakuUrl = js2Proxy(
|
||||
false,
|
||||
siteType,
|
||||
siteKey || '',
|
||||
EncodedUrl,
|
||||
headers
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 修改:如果存在自定义UA,添加到返回参数中
|
||||
if (
|
||||
(playUrl.includes('http://') || playUrl.includes('https://')) &&
|
||||
(playUrl.includes('m3u8') || playUrl.includes('mp4') || playUrl.includes('mkv')) &&
|
||||
!parse_api_url
|
||||
) {
|
||||
let result = {
|
||||
parse: 0,
|
||||
url: playUrl,
|
||||
danmaku: danmakuUrl
|
||||
};
|
||||
// 如果存在自定义UA,添加到返回结果中
|
||||
if (ua) {
|
||||
result.header = { 'User-Agent': ua };
|
||||
}
|
||||
return JSON.stringify(result);
|
||||
}
|
||||
if (parse.startsWith("http")) {
|
||||
let parseUrl = parse + playUrl;
|
||||
if (token) {
|
||||
parseUrl += '&token=' + token;
|
||||
}
|
||||
// 如果存在自定义UA,在请求解析接口时使用
|
||||
let requestHeaders = {};
|
||||
if (ua) {
|
||||
requestHeaders['User-Agent'] = ua;
|
||||
}
|
||||
let rets = await request(parseUrl, null, requestHeaders, 'get');
|
||||
if (rets.indexOf('DOCTYPE html') > -1) {
|
||||
let result = {
|
||||
parse: 1,
|
||||
url: parseUrl,
|
||||
danmaku: danmakuUrl
|
||||
};
|
||||
if (ua) {
|
||||
result.header = { 'User-Agent': ua };
|
||||
}
|
||||
return JSON.stringify(result);
|
||||
}
|
||||
let parseJson = JSON.parse(rets);
|
||||
let result = {
|
||||
parse: 0,
|
||||
url: parseJson['url'] || parseJson['data']['url'] || '',
|
||||
danmaku: danmakuUrl
|
||||
};
|
||||
// 如果存在自定义UA,添加到返回结果中
|
||||
if (ua) {
|
||||
result.header = { 'User-Agent': ua };
|
||||
}
|
||||
return JSON.stringify(result);
|
||||
}
|
||||
let params = {
|
||||
'parse_api': parse,
|
||||
'url': aesEncode(playUrl, key, iv),
|
||||
'token': token,
|
||||
};
|
||||
// 如果存在自定义UA,在请求时使用
|
||||
let requestHeaders = {};
|
||||
if (ua) {
|
||||
requestHeaders['User-Agent'] = ua;
|
||||
}
|
||||
let rets = await request(`${siteUrl}/${apiPrefix}/vodParse`, params, requestHeaders, 'post');
|
||||
let urlDecoded = aesDecode(JSON.parse(rets).data, key, iv);
|
||||
let finalPlayUrl = '';
|
||||
let parsed = JSON.parse(urlDecoded);
|
||||
finalPlayUrl = JSON.parse(parsed.json).url || '';
|
||||
let result = {
|
||||
parse: 0,
|
||||
url: finalPlayUrl,
|
||||
danmaku: danmakuUrl
|
||||
};
|
||||
// 如果存在自定义UA,添加到返回结果中
|
||||
if (ua) {
|
||||
result.header = { 'User-Agent': ua };
|
||||
}
|
||||
return JSON.stringify(result);
|
||||
}
|
||||
|
||||
async function search(wd, quick, pg) {
|
||||
if (hasCustomInit) {
|
||||
await home({});
|
||||
}
|
||||
let searchPath = searchApiSuffix || 'searchList';
|
||||
let url = `${siteUrl}/${apiPrefix}/${searchPath}`;
|
||||
let retryCount = 0;
|
||||
const maxRetries = 1;
|
||||
let videos = [];
|
||||
let attemptedCaptcha = false;
|
||||
let attemptedSlider = false;
|
||||
let sliderVerified = false;
|
||||
let sliderId = '';
|
||||
|
||||
let params = {
|
||||
'page': '1',
|
||||
'type_id': '0',
|
||||
'keywords': wd,
|
||||
};
|
||||
|
||||
if (souParamName && souSalt) {
|
||||
const currentTimestamp = Math.floor(Date.now() / 1000).toString();
|
||||
const souString = `/${souParamName}-${currentTimestamp}-sb-0-${souSalt}`;
|
||||
const md5Value = md5(souString);
|
||||
const finalValue = `${currentTimestamp}-sb-0-${md5Value}`;
|
||||
params[souParamName] = finalValue;
|
||||
}
|
||||
|
||||
let searchHeaders = { ...headers, ...extraSearchHeaders };
|
||||
|
||||
if (forceVerifyCode) {
|
||||
params['code'] = forceVerifyCode;
|
||||
const random_uuid = generateUUID();
|
||||
params['key'] = random_uuid;
|
||||
}
|
||||
|
||||
const maxWaitRetries = 2;
|
||||
let waitRetryCount = 0;
|
||||
|
||||
while (true) {
|
||||
let encData = await request(url, params, searchHeaders, 'post');
|
||||
let response = JSON.parse(encData);
|
||||
|
||||
if (response.code === 1001 && response.need_slider === true && !attemptedSlider) {
|
||||
attemptedSlider = true;
|
||||
|
||||
let getSliderUrl = `${siteUrl}/${apiPrefix}/getSlider`;
|
||||
let sliderResponse = await request(getSliderUrl, {}, searchHeaders, 'post');
|
||||
let sliderJson = JSON.parse(sliderResponse);
|
||||
|
||||
if (sliderJson.code === 1 && sliderJson.data) {
|
||||
let sliderDecrypted = aesDecode(sliderJson.data, key, iv);
|
||||
let sliderData = JSON.parse(sliderDecrypted);
|
||||
|
||||
sliderId = sliderData.slider_id || '';
|
||||
let targetX = parseInt(sliderData.target_x) || 0;
|
||||
let targetY = parseInt(sliderData.target_y) || 0;
|
||||
|
||||
if (sliderId && targetX > 0) {
|
||||
let posX = targetX;
|
||||
if (targetX > 0) {
|
||||
let randomOffset = Math.floor(Math.random() * 3) - 1;
|
||||
posX = targetX + randomOffset;
|
||||
if (posX < 1) posX = 1;
|
||||
if (posX > (sliderData.bg_width || 280)) posX = sliderData.bg_width || 280;
|
||||
}
|
||||
|
||||
let timestamp = Date.now();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
let verifyParams = {
|
||||
pos_x: posX.toString(),
|
||||
slider_id: sliderId,
|
||||
timestamp: timestamp.toString()
|
||||
};
|
||||
|
||||
let verifySliderUrl = `${siteUrl}/${apiPrefix}/verifySlider`;
|
||||
let verifyResponse = await request(verifySliderUrl, verifyParams, searchHeaders, 'post');
|
||||
let verifyJson = JSON.parse(verifyResponse);
|
||||
|
||||
if (verifyJson.code === 1 && verifyJson.data) {
|
||||
let verifyDecrypted = aesDecode(verifyJson.data, key, iv);
|
||||
let verifyResult = JSON.parse(verifyDecrypted);
|
||||
|
||||
if (verifyResult.verified === true) {
|
||||
sliderVerified = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!sliderVerified) {
|
||||
if (!forceVerifyCode && (Searchstatus || (response.code === 0 && response.msg && response.msg.includes('验证码')))) {
|
||||
if (attemptedCaptcha) break;
|
||||
attemptedCaptcha = true;
|
||||
const random_uuid = generateUUID();
|
||||
let modifiedApiPrefix = apiPrefix.replace('.index', '');
|
||||
let verifyUrl = `${siteUrl}/${modifiedApiPrefix}.verify/create?key=${random_uuid}`;
|
||||
let base64Img = await request_text(verifyUrl, null, headers, 'get', true);
|
||||
base64Img = base64Img.replace(/\n/g, '');
|
||||
let ocrResult = await request_text('https://api.nn.ci/ocr/b64/text', base64Img, { 'User-Agent': 'okhttp/3.10.0' }, 'post');
|
||||
params['code'] = ocrResult.trim();
|
||||
params['key'] = random_uuid;
|
||||
retryCount++;
|
||||
continue;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (response.code === 0 &&
|
||||
(!response.data || response.data.length === 0) &&
|
||||
response.msg &&
|
||||
/等.*秒|等待.*秒|等|搜|请等待|稍等|稍候/i.test(response.msg)) {
|
||||
if (waitRetryCount >= maxWaitRetries) break;
|
||||
let waitSeconds = extractWaitTime(response.msg);
|
||||
if (waitSeconds < 1) waitSeconds = 2;
|
||||
if (waitSeconds > 30) waitSeconds = 30;
|
||||
const actualWait = waitSeconds + 1;
|
||||
await new Promise(resolve => setTimeout(resolve, actualWait * 1000));
|
||||
waitRetryCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (response.data && response.data.length > 0) {
|
||||
let decodedData = JSON.parse(aesDecode(response.data, key, iv));
|
||||
videos = decodedData.search_list || [];
|
||||
break;
|
||||
} else if (!forceVerifyCode && (Searchstatus || (response.code === 0 && response.msg && response.msg.includes('验证码')))) {
|
||||
if (attemptedCaptcha) break;
|
||||
attemptedCaptcha = true;
|
||||
const random_uuid = generateUUID();
|
||||
let modifiedApiPrefix = apiPrefix.replace('.index', '');
|
||||
let verifyUrl = `${siteUrl}/${modifiedApiPrefix}.verify/create?key=${random_uuid}`;
|
||||
let base64Img = await request_text(verifyUrl, null, headers, 'get', true);
|
||||
base64Img = base64Img.replace(/\n/g, '');
|
||||
let ocrResult = await request_text('https://api.nn.ci/ocr/b64/text', base64Img, { 'User-Agent': 'okhttp/3.10.0' }, 'post');
|
||||
params['code'] = ocrResult.trim();
|
||||
params['key'] = random_uuid;
|
||||
retryCount++;
|
||||
continue;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify({
|
||||
list: videos,
|
||||
});
|
||||
}
|
||||
|
||||
function generateUUID() {
|
||||
const chars = '0123456789abcdef';
|
||||
const uuidTemplate = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
|
||||
return uuidTemplate.replace(/[xy]/g, (c) => {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return chars[v];
|
||||
});
|
||||
}
|
||||
|
||||
function aesDecode(str, keyStr, ivStr, type) {
|
||||
const key = Crypto.enc.Utf8.parse(keyStr);
|
||||
if (type === 'hex') {
|
||||
str = Crypto.enc.Hex.parse(str);
|
||||
return Crypto.AES.decrypt({
|
||||
ciphertext: str
|
||||
}, key, {
|
||||
iv: Crypto.enc.Utf8.parse(ivStr),
|
||||
mode: Crypto.mode.CBC,
|
||||
padding: Crypto.pad.Pkcs7
|
||||
}).toString(Crypto.enc.Utf8);
|
||||
} else {
|
||||
return Crypto.AES.decrypt(str, key, {
|
||||
iv: Crypto.enc.Utf8.parse(ivStr),
|
||||
mode: Crypto.mode.CBC,
|
||||
padding: Crypto.pad.Pkcs7
|
||||
}).toString(Crypto.enc.Utf8);
|
||||
}
|
||||
}
|
||||
|
||||
function aesEncode(str, keyStr, ivStr, type) {
|
||||
const key = Crypto.enc.Utf8.parse(keyStr);
|
||||
let encData = Crypto.AES.encrypt(str, key, {
|
||||
iv: Crypto.enc.Utf8.parse(ivStr),
|
||||
mode: Crypto.mode.CBC,
|
||||
padding: Crypto.pad.Pkcs7
|
||||
});
|
||||
if (type === 'hex') return encData.ciphertext.toString(Crypto.enc.Hex);
|
||||
return encData.toString();
|
||||
}
|
||||
|
||||
function md5(text) {
|
||||
return Crypto.MD5(text).toString();
|
||||
}
|
||||
|
||||
function chineseToNumber(chineseStr) {
|
||||
const chineseNumMap = {
|
||||
'零': 0, '〇': 0, '一': 1, '壹': 1, '二': 2, '贰': 2, '两': 2,
|
||||
'三': 3, '叁': 3, '四': 4, '肆': 4, '五': 5, '伍': 5,
|
||||
'六': 6, '陆': 6, '七': 7, '柒': 7, '八': 8, '捌': 8,
|
||||
'九': 9, '玖': 9, '十': 10, '拾': 10
|
||||
};
|
||||
|
||||
if (chineseStr.length === 1) {
|
||||
return chineseNumMap[chineseStr] || 2;
|
||||
}
|
||||
|
||||
if (chineseStr.startsWith('十') && chineseStr.length === 2) {
|
||||
const units = chineseNumMap[chineseStr[1]];
|
||||
return units ? 10 + units : 10;
|
||||
}
|
||||
|
||||
if (chineseStr.endsWith('十') && chineseStr.length === 2) {
|
||||
const tens = chineseNumMap[chineseStr[0]];
|
||||
return tens ? tens * 10 : 10;
|
||||
}
|
||||
|
||||
if (chineseStr.length === 3 && chineseStr.includes('十')) {
|
||||
const tens = chineseNumMap[chineseStr[0]];
|
||||
const units = chineseNumMap[chineseStr[2]];
|
||||
return tens && units ? tens * 10 + units : (tens ? tens * 10 : 10);
|
||||
}
|
||||
|
||||
return 2;
|
||||
}
|
||||
|
||||
function extractWaitTime(message) {
|
||||
const digitMatch = message.match(/(\d+)\s*秒/);
|
||||
if (digitMatch && digitMatch[1]) {
|
||||
return parseInt(digitMatch[1], 10);
|
||||
}
|
||||
|
||||
const chineseMatch = message.match(/([零一二三四五六七八九十两壹贰叁肆伍陆柒捌玖拾〇]+)\s*秒/);
|
||||
if (chineseMatch && chineseMatch[1]) {
|
||||
return chineseToNumber(chineseMatch[1]);
|
||||
}
|
||||
|
||||
if (/几秒|数秒|若干秒/i.test(message)) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
return 2;
|
||||
}
|
||||
|
||||
function hexToDec(hex) {
|
||||
hex = (hex || '#FFFFFF').replace(/^#/, '').toUpperCase();
|
||||
if (hex.length !== 6) {
|
||||
while (hex.length < 6) hex += '0';
|
||||
hex = hex.slice(0, 6);
|
||||
}
|
||||
return parseInt(hex, 16);
|
||||
}
|
||||
|
||||
function escapeXml(str) {
|
||||
if (typeof str !== 'string') return '';
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function jsonDanmakuToXml(json) {
|
||||
if (!json || !json.danmuku || !Array.isArray(json.danmuku)) {
|
||||
return '<?xml version="1.0" encoding="utf-8"?><i><code>0</code><id>error_no_danmaku</id></i>';
|
||||
}
|
||||
let lines = [];
|
||||
lines.push('<?xml version="1.0" encoding="utf-8"?>');
|
||||
lines.push('<i>');
|
||||
lines.push(` <code>${json.code || 0}</code>`);
|
||||
lines.push(` <id>${escapeXml(json.name || '12345')}</id>`);
|
||||
const positionMap = {
|
||||
'top': 5,
|
||||
'right': 1,
|
||||
'bottom': 4,
|
||||
'left': 6,
|
||||
'scroll': 1
|
||||
};
|
||||
json.danmuku.forEach(danmu => {
|
||||
if (!Array.isArray(danmu) || danmu.length < 5) return;
|
||||
let [
|
||||
time = 0,
|
||||
mode = 'scroll',
|
||||
colorHex = '#FFFFFF',
|
||||
,
|
||||
text = '',
|
||||
, ,
|
||||
fontSize = "24px"
|
||||
] = danmu;
|
||||
colorHex = '#' + Array(6).fill(0).map(() => '0123456789ABCDEF'[Math.floor(Math.random() * 16)]).join('');
|
||||
|
||||
let pos = positionMap[mode] || 1;
|
||||
let colorDec = hexToDec(colorHex);
|
||||
let fs = parseInt(String(fontSize).replace(/px/gi, '')) || 24;
|
||||
let p = `${Number(time).toFixed(3)},${pos},${fs},${colorDec},,,,,`;
|
||||
let safeText = escapeXml(text);
|
||||
lines.push(` <d p="${p}">${safeText}</d>`);
|
||||
});
|
||||
lines.push('</i>');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
async function proxy(params) {
|
||||
let jsonUrl = decodeURIComponent(
|
||||
params.url || params[0] || params.danmaku || '' || ''
|
||||
);
|
||||
if (!jsonUrl || !jsonUrl.startsWith('http')) {
|
||||
return JSON.stringify({
|
||||
code: 400,
|
||||
content: "缺少有效的弹幕 URL",
|
||||
headers: { "Content-Type": "text/plain" }
|
||||
});
|
||||
}
|
||||
let jsonContent = await request_text(jsonUrl, null, headers, 'get');
|
||||
let jsonObj = JSON.parse(jsonContent);
|
||||
let xmlContent = jsonDanmakuToXml(jsonObj);
|
||||
return JSON.stringify({
|
||||
code: 200,
|
||||
content: xmlContent,
|
||||
headers: {
|
||||
"Content-Type": "text/xml; charset=utf-8",
|
||||
"Cache-Control": "no-cache"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let forceVerifyCode = null;
|
||||
|
||||
export function __jsEvalReturn() {
|
||||
return {
|
||||
init: init,
|
||||
home: home,
|
||||
homeVod: homeVod,
|
||||
category: category,
|
||||
detail: detail,
|
||||
play: play,
|
||||
search: search,
|
||||
proxy: proxy
|
||||
};
|
||||
}
|
||||
+552
@@ -0,0 +1,552 @@
|
||||
# coding=utf-8
|
||||
# !/usr/bin/python
|
||||
|
||||
"""
|
||||
|
||||
作者 精彩一瞬间 内容均从互联网收集而来 仅供交流学习使用 严禁用于商业用途 请于24小时内删除
|
||||
====================Diudiumiao====================
|
||||
|
||||
"""
|
||||
|
||||
from Crypto.Util.Padding import unpad
|
||||
from Crypto.Util.Padding import pad
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import unquote
|
||||
from Crypto.Cipher import ARC4
|
||||
from urllib.parse import quote
|
||||
from base.spider import Spider
|
||||
from Crypto.Cipher import AES
|
||||
from datetime import datetime
|
||||
from bs4 import BeautifulSoup
|
||||
from base64 import b64decode
|
||||
import concurrent.futures
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import datetime
|
||||
import binascii
|
||||
import requests
|
||||
import hashlib
|
||||
import base64
|
||||
import json
|
||||
import time
|
||||
import hmac
|
||||
import sys
|
||||
import re
|
||||
import os
|
||||
|
||||
sys.path.append('..')
|
||||
|
||||
xurl = "https://www.wasu.cn"
|
||||
|
||||
headerx = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.87 Safari/537.36'
|
||||
}
|
||||
|
||||
headerz = {
|
||||
'accept': '*/*',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
|
||||
'cache-control': 'no-cache',
|
||||
'origin': xurl,
|
||||
'pragma': 'no-cache',
|
||||
'priority': 'u=1, i',
|
||||
'referer': xurl,
|
||||
'sec-ch-ua': '"Microsoft Edge";v="143", "Chromium";v="143", "Not A(Brand";v="24"',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
'sec-fetch-dest': 'empty',
|
||||
'sec-fetch-mode': 'cors',
|
||||
'sec-fetch-site': 'cross-site',
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 Edg/143.0.0.0',
|
||||
}
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def getName(self):
|
||||
return "精彩"
|
||||
|
||||
def init(self, extend):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
result = {"class": [{"type_id": "961", "type_name": "电影"},
|
||||
{"type_id": "962", "type_name": "剧集"},
|
||||
{"type_id": "963", "type_name": "少儿"},
|
||||
{"type_id": "965", "type_name": "栏目"},
|
||||
{"type_id": "966", "type_name": "新闻"}],
|
||||
"list": [],
|
||||
"filters": {"961": [{"key": "地区",
|
||||
"name": "地区",
|
||||
"value": [{"n": "全部", "v": ""},
|
||||
{"n": "内地", "v": "内地"},
|
||||
{"n": "港台", "v": "港台"},
|
||||
{"n": "欧美", "v": "欧美"},
|
||||
{"n": "日韩", "v": "日韩"},
|
||||
{"n": "泰国", "v": "泰国"},
|
||||
{"n": "其他", "v": "其他"}]},
|
||||
{"key": "类型",
|
||||
"name": "类型",
|
||||
"value": [{"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": "恐怖"},
|
||||
{"n": "战争", "v": "战争"},
|
||||
{"n": "武侠", "v": "武侠"}]},
|
||||
{"key": "年代",
|
||||
"name": "年代",
|
||||
"value": [{"n": "全部", "v": ""},
|
||||
|
||||
{"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": "2013", "v": "2013"},
|
||||
{"n": "2012", "v": "2012"},
|
||||
{"n": "2011", "v": "2011"},
|
||||
{"n": "2010", "v": "2010"}]}],
|
||||
"962": [{"key": "地区",
|
||||
"name": "地区",
|
||||
"value": [{"n": "全部", "v": ""},
|
||||
{"n": "内地", "v": "内地"},
|
||||
{"n": "港台", "v": "港台"},
|
||||
{"n": "日韩", "v": "日韩"},
|
||||
{"n": "欧美", "v": "欧美"},
|
||||
{"n": "泰国", "v": "泰国"},
|
||||
{"n": "其他", "v": "其他"}]},
|
||||
{"key": "类型",
|
||||
"name": "类型",
|
||||
"value": [{"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": "武侠"},
|
||||
{"n": "历史", "v": "历史"}]},
|
||||
{"key": "年代",
|
||||
"name": "年代",
|
||||
"value": [{"n": "全部", "v": ""},
|
||||
|
||||
{"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": "2013", "v": "2013"},
|
||||
{"n": "2012", "v": "2012"},
|
||||
{"n": "2011", "v": "2011"},
|
||||
{"n": "2010", "v": "2010"}]}],
|
||||
"963": [{"key": "地区",
|
||||
"name": "地区",
|
||||
"value": [{"n": "全部", "v": ""},
|
||||
{"n": "内地", "v": "内地"},
|
||||
{"n": "日韩", "v": "日韩"},
|
||||
{"n": "欧美", "v": "欧美"},
|
||||
{"n": "港台", "v": "港台"},
|
||||
{"n": "其他", "v": "其他"}]},
|
||||
{"key": "类型",
|
||||
"name": "类型",
|
||||
"value": [{"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": "治愈"},
|
||||
{"n": "儿歌", "v": "儿歌"},
|
||||
{"n": "教育", "v": "教育"},
|
||||
{"n": "校园", "v": "校园"},
|
||||
{"n": "童话", "v": "童话"},
|
||||
{"n": "推理", "v": "推理"},
|
||||
{"n": "怀旧", "v": "怀旧"},
|
||||
{"n": "宠物", "v": "宠物"},
|
||||
{"n": "舞蹈", "v": "舞蹈"}]},
|
||||
{"key": "年代",
|
||||
"name": "年代",
|
||||
"value": [{"n": "全部", "v": ""},
|
||||
|
||||
{"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": "2013", "v": "2013"},
|
||||
{"n": "2012", "v": "2012"},
|
||||
{"n": "2011", "v": "2011"},
|
||||
{"n": "2010", "v": "2010"}]}],
|
||||
"965": [{"key": "地区",
|
||||
"name": "地区",
|
||||
"value": [{"n": "全部", "v": ""},
|
||||
{"n": "内地", "v": "内地"},
|
||||
{"n": "欧美", "v": "欧美"}]},
|
||||
{"key": "类型",
|
||||
"name": "类型",
|
||||
"value": [{"n": "全部", "v": ""},
|
||||
{"n": "文化", "v": "文化"},
|
||||
{"n": "纪实", "v": "纪实"},
|
||||
{"n": "访谈", "v": "访谈"},
|
||||
{"n": "历史", "v": "历史"},
|
||||
{"n": "美食", "v": "美食"},
|
||||
{"n": "旅游", "v": "旅游"},
|
||||
{"n": "时尚", "v": "时尚"},
|
||||
{"n": "情感", "v": "情感"},
|
||||
{"n": "生活", "v": "生活"},
|
||||
{"n": "真人秀", "v": "真人秀"}]},
|
||||
{"key": "年代",
|
||||
"name": "年代",
|
||||
"value": [{"n": "全部", "v": ""},
|
||||
{"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": "2013", "v": "2013"},
|
||||
{"n": "2012", "v": "2012"},
|
||||
{"n": "2011", "v": "2011"},
|
||||
{"n": "2010", "v": "2010"}]}],
|
||||
"966": [{"key": "类型",
|
||||
"name": "类型",
|
||||
"value": [{"n": "全部", "v": ""},
|
||||
{"n": "国内视野", "v": "国内视野"},
|
||||
{"n": "国际纵览", "v": "国际纵览"},
|
||||
{"n": "军事话题", "v": "军事话题"},
|
||||
{"n": "社会百态", "v": "社会百态"},
|
||||
{"n": "央视频", "v": "央视频"}]}]}}
|
||||
return result
|
||||
|
||||
def generate_x_sign(self, secret_b64):
|
||||
secret_key = self.decode_secret_key(secret_b64)
|
||||
data = self.get_data_string()
|
||||
signature = self.compute_signature(secret_key, data)
|
||||
x_sign = self.encode_signature(signature)
|
||||
return x_sign
|
||||
|
||||
def decode_secret_key(self, secret_b64):
|
||||
return base64.b64decode(secret_b64)
|
||||
|
||||
def get_data_string(self):
|
||||
return "{}"
|
||||
|
||||
def compute_signature(self, secret_key, data):
|
||||
return hmac.new(secret_key, data.encode('utf-8'), hashlib.sha256).digest()
|
||||
|
||||
def encode_signature(self, signature):
|
||||
return base64.b64encode(signature).decode('utf-8')
|
||||
|
||||
def get_current_app_key(self, xurl, headerx):
|
||||
resp = self.fetch_index_page(xurl, headerx)
|
||||
js_path = self.extract_js_path(resp.text)
|
||||
js_url = f"{xurl}{js_path}"
|
||||
js_content = self.fetch_js_content(js_url, headerx)
|
||||
target_key_b64 = self.extract_target_key(js_content)
|
||||
return target_key_b64
|
||||
|
||||
def fetch_index_page(self, xurl, headerx):
|
||||
resp = requests.get(xurl, headers=headerx)
|
||||
resp.raise_for_status()
|
||||
return resp
|
||||
|
||||
def extract_js_path(self, html_content):
|
||||
js_path_pattern = r'src="(/[\d\.]+/assets/js/index-[\w\.-]+\.js)"'
|
||||
match = re.search(js_path_pattern, html_content)
|
||||
return match.group(1)
|
||||
|
||||
def fetch_js_content(self, js_url, headerx):
|
||||
js_resp = requests.get(js_url, headers=headerx)
|
||||
js_resp.raise_for_status()
|
||||
return js_resp.text
|
||||
|
||||
def extract_target_key(self, js_content):
|
||||
key_pattern = r'const \w+="([^"]+)",\w+="([^"]+)",\w+="([^"]+)";'
|
||||
key_match = re.search(key_pattern, js_content)
|
||||
return key_match.group(2)
|
||||
|
||||
def get_headers(self, sign1, xurl):
|
||||
return {
|
||||
'accept': 'application/json, text/plain, */*',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
|
||||
'cache-control': 'no-cache',
|
||||
'launchchannel': 'web_channel',
|
||||
'origin': xurl,
|
||||
'pragma': 'no-cache',
|
||||
'priority': 'u=1, i',
|
||||
'referer': xurl,
|
||||
'sec-ch-ua': '"Microsoft Edge";v="143", "Chromium";v="143", "Not A(Brand";v="24"',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
'sec-fetch-dest': 'empty',
|
||||
'sec-fetch-mode': 'cors',
|
||||
'sec-fetch-site': 'cross-site',
|
||||
'siteid': '1000101',
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 Edg/143.0.0.0',
|
||||
'x-sign': sign1,
|
||||
}
|
||||
|
||||
def categoryContent(self, cid, pg, filter, ext):
|
||||
secret_b64 = self.get_current_app_key(xurl, headerx)
|
||||
sign1 = self.generate_x_sign(secret_b64)
|
||||
page = self.get_page_number(pg)
|
||||
NdType, DqType, LxType = self.extract_filter_types(ext)
|
||||
headers = self.get_headers(sign1, xurl)
|
||||
params = self.build_category_params(cid, page, NdType, DqType, LxType)
|
||||
data = self.fetch_category_data(params, headers)
|
||||
videos = self.parse_category_videos(data)
|
||||
return self.build_category_result(videos, pg)
|
||||
|
||||
def get_page_number(self, pg):
|
||||
return int(pg) if pg else 1
|
||||
|
||||
def extract_filter_types(self, ext):
|
||||
NdType = ext.get('年代', '全部')
|
||||
DqType = ext.get('地区', '全部')
|
||||
LxType = ext.get('类型', '全部')
|
||||
return NdType, DqType, LxType
|
||||
|
||||
def build_category_params(self, cid, page, NdType, DqType, LxType):
|
||||
return {'functionName': 'getNewsSearchedByCondition','nodeId': cid,'nodeTag': LxType,'yearTag': NdType,'countryTag': DqType,'orderType': '0','pageSize': '40','page': page,'keyword': '','siteId': '1000101',}
|
||||
|
||||
def fetch_category_data(self, params, headers):
|
||||
detail = requests.get('https://ups.5g.wasu.tv/rmp-user-suggest/1000101/hzhs/searchServlet', params=params,headers=headers)
|
||||
detail.encoding = "utf-8"
|
||||
return detail.json()
|
||||
|
||||
def parse_category_videos(self, data):
|
||||
videos = []
|
||||
for vod in data['data']:
|
||||
videos.append(self.parse_category_video(vod))
|
||||
return videos
|
||||
|
||||
def parse_category_video(self, vod):
|
||||
return {"vod_id": f"{vod['nodeId']}@{vod['newsId']}","vod_name": vod['title'],"vod_pic": vod['hPic'],"vod_year": vod.get('pubTime', '暂无日期'),"vod_remarks": vod.get('episodeDesc', '暂无备注')}
|
||||
|
||||
def build_category_result(self, videos, pg):
|
||||
result = {'list': videos}
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
did = ids[0]
|
||||
secret_b64 = self.get_current_app_key(xurl, headerx)
|
||||
sign1 = self.generate_x_sign(secret_b64)
|
||||
fenge = did.split("@")
|
||||
headers = self.get_headers(sign1, xurl)
|
||||
params = self.build_detail_params(fenge)
|
||||
data = self.fetch_detail_data(params, headers)
|
||||
content = self.build_content(data)
|
||||
director = self.extract_detail_field(data, 'director')
|
||||
actor = self.extract_detail_field(data, 'actor')
|
||||
remarks = self.extract_detail_field(data, 'episodeDesc')
|
||||
year = self.extract_detail_field(data, 'pubTime')
|
||||
area = self.extract_detail_field(data, 'countryTag')
|
||||
bofang = self.build_play_url(data)
|
||||
videos = [self.build_video_data(did, director, actor, remarks, year, area, content, bofang)]
|
||||
return self.build_result(videos)
|
||||
|
||||
def build_detail_params(self, fenge):
|
||||
return {'siteId': '1000101', 'functionName': 'getCurrentNews', 'nodeId': fenge[0], 'newsId': fenge[1],'platform': 'web',}
|
||||
|
||||
def fetch_detail_data(self, params, headers):
|
||||
detail = requests.get('https://mcspapp.5g.wasu.tv/bvradio_app/hzhs/newsServlet', params=params, headers=headers)
|
||||
detail.encoding = "utf-8"
|
||||
return detail.json()
|
||||
|
||||
def build_content(self, data):
|
||||
return '介绍剧情👉' + data.get('data', {}).get('newsAbstract', '')
|
||||
|
||||
def extract_detail_field(self, data, field_name):
|
||||
return data.get('data', {}).get(field_name, '')
|
||||
|
||||
def build_play_url(self, data):
|
||||
bofang = ''
|
||||
for vod in data['data']['vodList']:
|
||||
name = vod['title']
|
||||
if len(vod['fileList']) > 1:
|
||||
id = vod['fileList'][1]['playUrl']
|
||||
else:
|
||||
id = vod['fileList'][0]['playUrl']
|
||||
bofang += name + '$' + id + '#'
|
||||
return bofang[:-1]
|
||||
|
||||
def build_video_data(self, did, director, actor, remarks, year, area, content, bofang):
|
||||
return {"vod_id": did, "vod_director": director, "vod_actor": actor, "vod_remarks": remarks, "vod_year": year, "vod_area": area, "vod_content": content, "vod_play_from": "华数", "vod_play_url": bofang}
|
||||
|
||||
def build_result(self, videos):
|
||||
result = {}
|
||||
result['list'] = videos
|
||||
return result
|
||||
|
||||
def get_x_sign_for_post(self, secret_b64, playUrl):
|
||||
secret_key = self.decode_secret_key(secret_b64)
|
||||
payload_dict = self.build_payload_dict(playUrl)
|
||||
data_string = self.serialize_payload(payload_dict)
|
||||
signature = self.compute_hmac_signature(secret_key, data_string)
|
||||
x_sign = self.encode_signature(signature)
|
||||
return x_sign
|
||||
|
||||
def decode_secret_key(self, secret_b64):
|
||||
return base64.b64decode(secret_b64)
|
||||
|
||||
def build_payload_dict(self, playUrl):
|
||||
return {"playUrl": playUrl, "platform": "web"}
|
||||
|
||||
def serialize_payload(self, payload_dict):
|
||||
return json.dumps(payload_dict, separators=(',', ':'), ensure_ascii=False)
|
||||
|
||||
def compute_hmac_signature(self, secret_key, data_string):
|
||||
return hmac.new(secret_key, data_string.encode('utf-8'), hashlib.sha256).digest()
|
||||
|
||||
def encode_signature(self, signature):
|
||||
return base64.b64encode(signature).decode('utf-8')
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
secret_b64 = self.get_current_app_key(xurl, headerx)
|
||||
id = self.normalize_play_url(id)
|
||||
sign2 = self.get_x_sign_for_post(secret_b64, id)
|
||||
headers = self.get_headers(sign2, xurl)
|
||||
json_data = self.build_request_json(id)
|
||||
result = self.post_play_url_request(headers, json_data)
|
||||
play_url = result['data']['playUrl']
|
||||
return self.build_player_result(play_url)
|
||||
|
||||
def normalize_play_url(self, id):
|
||||
return id.replace('.mp4', '/playlist.m3u8')
|
||||
|
||||
def build_request_json(self, playUrl):
|
||||
return {'playUrl': playUrl, 'platform': 'web', }
|
||||
|
||||
def post_play_url_request(self, headers, json_data):
|
||||
response = requests.post('https://mcspapp.5g.wasu.tv/thirdApiFile/file/getPlayUrl', headers=headers, json=json_data)
|
||||
return response.json()
|
||||
|
||||
def build_player_result(self, play_url):
|
||||
result = {}
|
||||
result["parse"] = 0
|
||||
result["playUrl"] = ''
|
||||
result["url"] = play_url
|
||||
result["header"] = headerz
|
||||
return result
|
||||
|
||||
def searchContentPage(self, key, quick, pg):
|
||||
page = self.get_page_number(pg)
|
||||
secret_b64 = self.get_current_app_key(xurl, headerx)
|
||||
sign1 = self.generate_x_sign(secret_b64)
|
||||
headers = self.get_headers(sign1, xurl)
|
||||
params = self.build_search_params(key, page)
|
||||
data = self.fetch_search_data(params, headers)
|
||||
videos = self.parse_search_videos(data)
|
||||
return self.build_search_result(videos, pg)
|
||||
|
||||
def get_page_number(self, pg):
|
||||
return int(pg) if pg else 1
|
||||
|
||||
def build_search_params(self, key, page):
|
||||
return {'functionName': 'getNewsSearched', 'searchNewsType': '3,4,5', 'keyword': key, 'pageSize': 10, 'page': page, 'siteId': 1000101}
|
||||
|
||||
def fetch_search_data(self, params, headers):
|
||||
detail = requests.get("https://ups.5g.wasu.tv/rmp-user-suggest/1000101/hzhs/searchServlet", params=params, headers=headers)
|
||||
detail.encoding = "utf-8"
|
||||
return detail.json()
|
||||
|
||||
def parse_search_videos(self, data):
|
||||
videos = []
|
||||
for vod in data['data']:
|
||||
videos.append(self.parse_search_video(vod))
|
||||
return videos
|
||||
|
||||
def parse_search_video(self, vod):
|
||||
return {"vod_id": f"{vod['nodeId']}@{vod['newsId']}", "vod_name": vod['title'], "vod_pic": vod['hPic'], "vod_year": vod.get('pubTime', '暂无日期'), "vod_remarks": vod.get('episodeDesc', '暂无备注')}
|
||||
|
||||
def build_search_result(self, videos, pg):
|
||||
result = {'list': videos}
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
return self.searchContentPage(key, quick, '1')
|
||||
|
||||
def localProxy(self, params):
|
||||
if params['type'] == "m3u8":
|
||||
return self.proxyM3u8(params)
|
||||
elif params['type'] == "media":
|
||||
return self.proxyMedia(params)
|
||||
elif params['type'] == "ts":
|
||||
return self.proxyTs(params)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import json
|
||||
import sys
|
||||
import threading
|
||||
import uuid
|
||||
import requests
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
import time
|
||||
from Crypto.Hash import MD5, SHA1
|
||||
|
||||
class Spider(Spider):
|
||||
'''
|
||||
配置示例:
|
||||
{
|
||||
"key": "xxxx",
|
||||
"name": "xxxx",
|
||||
"type": 3,
|
||||
"api": ".所在路径/金牌.py",
|
||||
"searchable": 1,
|
||||
"quickSearch": 1,
|
||||
"filterable": 1,
|
||||
"changeable": 1,
|
||||
"ext": {
|
||||
"site": "https://www.jiabaide.cn,域名2,域名3"
|
||||
}
|
||||
},
|
||||
'''
|
||||
def init(self, extend=""):
|
||||
if extend:
|
||||
hosts=json.loads(extend)['site']
|
||||
self.host = self.host_late(hosts)
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
cdata = self.fetch(f"{self.host}/api/mw-movie/anonymous/get/filer/type", headers=self.getheaders()).json()
|
||||
fdata = self.fetch(f"{self.host}/api/mw-movie/anonymous/v1/get/filer/list", headers=self.getheaders()).json()
|
||||
result = {}
|
||||
classes = []
|
||||
filters={}
|
||||
for k in cdata['data']:
|
||||
classes.append({
|
||||
'type_name': k['typeName'],
|
||||
'type_id': str(k['typeId']),
|
||||
})
|
||||
sort_values = [{"n": "最近更新", "v": "2"},{"n": "人气高低", "v": "3"}, {"n": "评分高低", "v": "4"}]
|
||||
for tid, d in fdata['data'].items():
|
||||
current_sort_values = sort_values.copy()
|
||||
if tid == '1':
|
||||
del current_sort_values[0]
|
||||
filters[tid] = [
|
||||
{"key": "type", "name": "类型",
|
||||
"value": [{"n": i["itemText"], "v": i["itemValue"]} for i in d["typeList"]]},
|
||||
|
||||
*([] if not d["plotList"] else [{"key": "v_class", "name": "剧情",
|
||||
"value": [{"n": i["itemText"], "v": i["itemText"]}
|
||||
for i in d["plotList"]]}]),
|
||||
|
||||
{"key": "area", "name": "地区",
|
||||
"value": [{"n": i["itemText"], "v": i["itemText"]} for i in d["districtList"]]},
|
||||
|
||||
{"key": "year", "name": "年份",
|
||||
"value": [{"n": i["itemText"], "v": i["itemText"]} for i in d["yearList"]]},
|
||||
|
||||
{"key": "lang", "name": "语言",
|
||||
"value": [{"n": i["itemText"], "v": i["itemText"]} for i in d["languageList"]]},
|
||||
|
||||
{"key": "sort", "name": "排序", "value": current_sort_values}
|
||||
]
|
||||
result['class'] = classes
|
||||
result['filters'] = filters
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
data1 = self.fetch(f"{self.host}/api/mw-movie/anonymous/v1/home/all/list", headers=self.getheaders()).json()
|
||||
data2=self.fetch(f"{self.host}/api/mw-movie/anonymous/home/hotSearch",headers=self.getheaders()).json()
|
||||
data=[]
|
||||
for i in data1['data'].values():
|
||||
data.extend(i['list'])
|
||||
data.extend(data2['data'])
|
||||
vods=self.getvod(data)
|
||||
return {'list':vods}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
|
||||
params = {
|
||||
"area": extend.get('area', ''),
|
||||
"filterStatus": "1",
|
||||
"lang": extend.get('lang', ''),
|
||||
"pageNum": pg,
|
||||
"pageSize": "30",
|
||||
"sort": extend.get('sort', '1'),
|
||||
"sortBy": "1",
|
||||
"type": extend.get('type', ''),
|
||||
"type1": tid,
|
||||
"v_class": extend.get('v_class', ''),
|
||||
"year": extend.get('year', '')
|
||||
}
|
||||
data = self.fetch(f"{self.host}/api/mw-movie/anonymous/video/list?{self.js(params)}", headers=self.getheaders(params)).json()
|
||||
result = {}
|
||||
result['list'] = self.getvod(data['data']['list'])
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
data=self.fetch(f"{self.host}/api/mw-movie/anonymous/video/detail?id={ids[0]}",headers=self.getheaders({'id':ids[0]})).json()
|
||||
vod=self.getvod([data['data']])[0]
|
||||
vod['vod_play_from']='拾光有文才'
|
||||
vod['vod_play_url'] = '#'.join(
|
||||
f"{i['name'] if len(vod['episodelist']) > 1 else vod['vod_name']}${ids[0]}@@{i['nid']}" for i in
|
||||
vod['episodelist'])
|
||||
vod.pop('episodelist', None)
|
||||
return {'list':[vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
params = {
|
||||
"keyword": key,
|
||||
"pageNum": pg,
|
||||
"pageSize": "8",
|
||||
"sourceCode": "1"
|
||||
}
|
||||
data=self.fetch(f"{self.host}/api/mw-movie/anonymous/video/searchByWord?{self.js(params)}",headers=self.getheaders(params)).json()
|
||||
vods=self.getvod(data['data']['result']['list'])
|
||||
return {'list':vods,'page':pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
self.header = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.61 Chrome/126.0.6478.61 Not/A)Brand/8 Safari/537.36',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
'DNT': '1',
|
||||
'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="126", "Google Chrome";v="126"',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'Origin': self.host,
|
||||
'Referer': f'{self.host}/'
|
||||
}
|
||||
ids=id.split('@@')
|
||||
pdata = self.fetch(f"{self.host}/api/mw-movie/anonymous/v2/video/episode/url?clientType=1&id={ids[0]}&nid={ids[1]}",headers=self.getheaders({'clientType':'1','id': ids[0], 'nid': ids[1]})).json()
|
||||
vlist=[]
|
||||
for i in pdata['data']['list']:vlist.extend([i['resolutionName'],i['url']])
|
||||
return {'parse':0,'url':vlist,'header':self.header}
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def host_late(self, url_list):
|
||||
if isinstance(url_list, str):
|
||||
urls = [u.strip() for u in url_list.split(',')]
|
||||
else:
|
||||
urls = url_list
|
||||
if len(urls) <= 1:
|
||||
return urls[0] if urls else ''
|
||||
|
||||
results = {}
|
||||
threads = []
|
||||
|
||||
def test_host(url):
|
||||
try:
|
||||
start_time = time.time()
|
||||
response = requests.head(url, timeout=1.0, allow_redirects=False)
|
||||
delay = (time.time() - start_time) * 1000
|
||||
results[url] = delay
|
||||
except Exception as e:
|
||||
results[url] = float('inf')
|
||||
for url in urls:
|
||||
t = threading.Thread(target=test_host, args=(url,))
|
||||
threads.append(t)
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
return min(results.items(), key=lambda x: x[1])[0]
|
||||
|
||||
def md5(self, sign_key):
|
||||
md5_hash = MD5.new()
|
||||
md5_hash.update(sign_key.encode('utf-8'))
|
||||
md5_result = md5_hash.hexdigest()
|
||||
return md5_result
|
||||
|
||||
def js(self, param):
|
||||
return '&'.join(f"{k}={v}" for k, v in param.items())
|
||||
|
||||
def getheaders(self, param=None):
|
||||
if param is None:param = {}
|
||||
t=str(int(time.time()*1000))
|
||||
param['key']='cb808529bae6b6be45ecfab29a4889bc'
|
||||
param['t']=t
|
||||
sha1_hash = SHA1.new()
|
||||
sha1_hash.update(self.md5(self.js(param)).encode('utf-8'))
|
||||
sign = sha1_hash.hexdigest()
|
||||
deviceid = str(uuid.uuid4())
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.61 Chrome/126.0.6478.61 Not/A)Brand/8 Safari/537.36',
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'sign': sign,
|
||||
't': t,
|
||||
'deviceid':deviceid
|
||||
}
|
||||
return headers
|
||||
|
||||
def convert_field_name(self, field):
|
||||
field = field.lower()
|
||||
if field.startswith('vod') and len(field) > 3:
|
||||
field = field.replace('vod', 'vod_')
|
||||
if field.startswith('type') and len(field) > 4:
|
||||
field = field.replace('type', 'type_')
|
||||
return field
|
||||
|
||||
def getvod(self, array):
|
||||
return [{self.convert_field_name(k): v for k, v in item.items()} for item in array]
|
||||
|
||||
-772
@@ -1,772 +0,0 @@
|
||||
/**
|
||||
title: "耐看影视",
|
||||
author: "",
|
||||
logo: "https://nkvod.org/upload/site/20241223-1/a9ca623af581c93ed3c2f3e98e3a5d4b.png",
|
||||
more: {
|
||||
sourceTag: "在线播放"
|
||||
}
|
||||
*/
|
||||
import { Crypto, load, _ } from 'assets://js/lib/cat.js';
|
||||
|
||||
let HOST = 'https://nkvod.org';
|
||||
let siteKey = "", siteType = "", sourceKey = "", ext = "";
|
||||
let searchCookie = '';
|
||||
|
||||
const UA = 'Mozilla/5.0 (Linux; Android 12; ALN-AL00 Build/HUAWEIALN-AL00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/114.0.5735.196 Mobile Safari/537.36';
|
||||
|
||||
async function getCaptchaBinaryData(url) {
|
||||
try {
|
||||
const res = await req(url, {
|
||||
headers: {
|
||||
'User-Agent': UA,
|
||||
'Referer': HOST
|
||||
},
|
||||
buffer: 1
|
||||
});
|
||||
|
||||
let binaryData;
|
||||
if (Array.isArray(res.content)) {
|
||||
binaryData = new Uint8Array(res.content);
|
||||
} else {
|
||||
binaryData = res.content;
|
||||
}
|
||||
|
||||
return {
|
||||
headers: res.headers,
|
||||
binaryData: binaryData
|
||||
};
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function request(reqUrl, options = {}) {
|
||||
const defaultOptions = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'User-Agent': UA,
|
||||
'Referer': HOST
|
||||
}
|
||||
};
|
||||
|
||||
if (searchCookie) {
|
||||
defaultOptions.headers['Cookie'] = searchCookie;
|
||||
}
|
||||
|
||||
const mergedOptions = {...defaultOptions, ...options};
|
||||
|
||||
let res = await req(reqUrl, mergedOptions);
|
||||
return res.content;
|
||||
}
|
||||
|
||||
async function requestWithCaptcha(reqUrl, options = {}) {
|
||||
let verifyType = 'search';
|
||||
if (typeof options === 'string') {
|
||||
verifyType = options;
|
||||
options = {};
|
||||
} else if (options.verifyType) {
|
||||
verifyType = options.verifyType;
|
||||
}
|
||||
|
||||
let html = await request(reqUrl, options);
|
||||
|
||||
const needCaptcha = html && (
|
||||
html.includes('系统安全验证') ||
|
||||
html.includes('请输入验证码') ||
|
||||
html.includes('verify/index.html') ||
|
||||
html.includes('verify_check')
|
||||
);
|
||||
|
||||
if (needCaptcha) {
|
||||
searchCookie = '';
|
||||
await fetchCk(verifyType);
|
||||
|
||||
if (searchCookie) {
|
||||
html = await request(reqUrl, {
|
||||
...options,
|
||||
headers: {
|
||||
...options.headers,
|
||||
'Cookie': searchCookie
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const stillNeedCaptcha = html && (
|
||||
html.includes('系统安全验证') ||
|
||||
html.includes('请输入验证码')
|
||||
);
|
||||
|
||||
if (stillNeedCaptcha) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
async function fetchCk(verifyType = 'search') {
|
||||
try {
|
||||
const yzm = HOST + "/index.php/verify/index.html?" + Date.now();
|
||||
|
||||
const yzmResponse = await getCaptchaBinaryData(yzm);
|
||||
|
||||
let cookie = "";
|
||||
const headers = yzmResponse.headers;
|
||||
|
||||
for (const key in headers) {
|
||||
if (key.toLowerCase() === "set-cookie") {
|
||||
const cookieValue = headers[key];
|
||||
if (Array.isArray(cookieValue)) {
|
||||
cookie = cookieValue[0].split(";")[0];
|
||||
} else if (typeof cookieValue === 'string') {
|
||||
cookie = cookieValue.split(";")[0];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!cookie) {
|
||||
return;
|
||||
}
|
||||
|
||||
let base64Image;
|
||||
if (yzmResponse.binaryData instanceof Uint8Array) {
|
||||
const wordArray = Crypto.lib.WordArray.create(yzmResponse.binaryData);
|
||||
base64Image = Crypto.enc.Base64.stringify(wordArray);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!base64Image) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const ocrResponse = await req("https://api.nn.ci/ocr/b64/text", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
'User-Agent': UA,
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: base64Image,
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
if (!ocrResponse || !ocrResponse.content) {
|
||||
return;
|
||||
}
|
||||
|
||||
let verifyCode = ocrResponse.content.trim();
|
||||
if (!verifyCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
verifyCode = verifyCode.replace(/[^a-zA-Z0-9]/g, '');
|
||||
|
||||
if (verifyCode.length < 4) {
|
||||
return;
|
||||
}
|
||||
|
||||
const submit_url = `${HOST}/index.php/ajax/verify_check?type=${verifyType}&verify=${verifyCode}`;
|
||||
|
||||
const submitResponse = await req(submit_url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'User-Agent': UA,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Origin': HOST,
|
||||
'Referer': HOST + '/',
|
||||
'Cookie': cookie,
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: ''
|
||||
});
|
||||
|
||||
if (submitResponse && submitResponse.content) {
|
||||
try {
|
||||
const submitJson = JSON.parse(submitResponse.content);
|
||||
if (submitJson && submitJson.code === 1) {
|
||||
searchCookie = cookie;
|
||||
}
|
||||
} catch (parseError) {
|
||||
if (submitResponse.content.includes('"code":1') || submitResponse.content.includes('验证成功')) {
|
||||
searchCookie = cookie;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (ocrError) {
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
|
||||
function init(cfg) {
|
||||
siteKey = cfg.skey;
|
||||
siteType = cfg.stype;
|
||||
sourceKey = cfg.sourceKey;
|
||||
ext = cfg.ext;
|
||||
if (ext && ext.indexOf('http') == 0) {
|
||||
HOST = ext;
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助函数:获取元素文本内容(兼容 flutter_js 的 cheerio)
|
||||
// flutter_js 的 cheerio 实现中 .text() 方法可能返回空,需要使用 .html() 作为备用
|
||||
function getText($elem) {
|
||||
if (!$elem || !$elem.length) return '';
|
||||
// 优先使用 text(),如果返回空则尝试从 html() 提取
|
||||
let text = '';
|
||||
try {
|
||||
text = $elem.text();
|
||||
} catch (e) {}
|
||||
if (!text || text.trim() === '') {
|
||||
try {
|
||||
// 从 html 中去除标签获取纯文本
|
||||
const html = $elem.html();
|
||||
if (html) {
|
||||
text = html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ');
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
return (text || '').trim();
|
||||
}
|
||||
|
||||
async function home(filter) {
|
||||
const classes = [
|
||||
{ type_id: "1", type_name: "电影" },
|
||||
{ type_id: "2", type_name: "剧集" },
|
||||
{ type_id: "4", type_name: "动漫" },
|
||||
{ type_id: "3", type_name: "综艺" }
|
||||
];
|
||||
|
||||
const filterObj = {
|
||||
"1":[{"key":"class","name":"剧情","value":[{"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":"武侠"},{"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":"儿童"},{"n":"网络电影","v":"网络电影"}]},{"key":"area","name":"地区","value":[{"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":"意大利"},{"n":"西班牙","v":"西班牙"},{"n":"加拿大","v":"加拿大"},{"n":"其它","v":"其它"}]},{"key":"year","name":"年份","value":[{"n":"全部","v":""},{"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":"2013","v":"2013"},{"n":"2012","v":"2012"},{"n":"2011","v":"2011"},{"n":"2010","v":"2010"}]},{"key":"lang","name":"语言","value":[{"n":"全部","v":""},{"n":"国语","v":"国语"},{"n":"英语","v":"英语"},{"n":"粤语","v":"粤语"},{"n":"闽南语","v":"闽南语"},{"n":"韩语","v":"韩语"},{"n":"日语","v":"日语"},{"n":"法语","v":"法语"},{"n":"德语","v":"德语"},{"n":"其它","v":"其它"}]},{"key":"letter","name":"字母","value":[{"n":"字母","v":""},{"n":"A","v":"A"},{"n":"B","v":"B"},{"n":"C","v":"C"},{"n":"D","v":"D"},{"n":"E","v":"E"},{"n":"F","v":"F"},{"n":"G","v":"G"},{"n":"H","v":"H"},{"n":"I","v":"I"},{"n":"J","v":"J"},{"n":"K","v":"K"},{"n":"L","v":"L"},{"n":"M","v":"M"},{"n":"N","v":"N"},{"n":"O","v":"O"},{"n":"P","v":"P"},{"n":"Q","v":"Q"},{"n":"R","v":"R"},{"n":"S","v":"S"},{"n":"T","v":"T"},{"n":"U","v":"U"},{"n":"V","v":"V"},{"n":"W","v":"W"},{"n":"X","v":"X"},{"n":"Y","v":"Y"},{"n":"Z","v":"Z"},{"n":"0-9","v":"0-9"}]},{"key":"by","name":"排序","value":[{"n":"时间排序","v":"time"},{"n":"人气排序","v":"hits"},{"n":"评分排序","v":"score"}]}],
|
||||
"2":[{"key":"class","name":"剧情","value":[{"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":"商战"},{"n":"网剧","v":"网剧"},{"n":"其他","v":"其他"}]},{"key":"area","name":"地区","value":[{"n":"全部","v":""},{"n":"内地","v":"内地"},{"n":"韩国","v":"韩国"},{"n":"香港","v":"香港"},{"n":"台湾","v":"台湾"},{"n":"日本","v":"日本"},{"n":"美国","v":"美国"},{"n":"泰国","v":"泰国"},{"n":"英国","v":"英国"},{"n":"新加坡","v":"新加坡"},{"n":"其他","v":"其他"}]},{"key":"lang","name":"语言","value":[{"n":"全部","v":""},{"n":"国语","v":"国语"},{"n":"英语","v":"英语"},{"n":"粤语","v":"粤语"},{"n":"闽南语","v":"闽南语"},{"n":"韩语","v":"韩语"},{"n":"日语","v":"日语"},{"n":"其它","v":"其它"}]},{"key":"year","name":"年份","value":[{"n":"全部","v":""},{"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":"2013","v":"2013"},{"n":"2012","v":"2012"},{"n":"2011","v":"2011"},{"n":"2010","v":"2010"},{"n":"2009","v":"2009"},{"n":"2008","v":"2008"},{"n":"2006","v":"2006"},{"n":"2005","v":"2005"},{"n":"2004","v":"2004"}]},{"key":"letter","name":"字母","value":[{"n":"字母","v":""},{"n":"A","v":"A"},{"n":"B","v":"B"},{"n":"C","v":"C"},{"n":"D","v":"D"},{"n":"E","v":"E"},{"n":"F","v":"F"},{"n":"G","v":"G"},{"n":"H","v":"H"},{"n":"I","v":"I"},{"n":"J","v":"J"},{"n":"K","v":"K"},{"n":"L","v":"L"},{"n":"M","v":"M"},{"n":"N","v":"N"},{"n":"O","v":"O"},{"n":"P","v":"P"},{"n":"Q","v":"Q"},{"n":"R","v":"R"},{"n":"S","v":"S"},{"n":"T","v":"T"},{"n":"U","v":"U"},{"n":"V","v":"V"},{"n":"W","v":"W"},{"n":"X","v":"X"},{"n":"Y","v":"Y"},{"n":"Z","v":"Z"},{"n":"0-9","v":"0-9"}]},{"key":"by","name":"排序","value":[{"n":"时间排序","v":"time"},{"n":"人气排序","v":"hits"},{"n":"评分排序","v":"score"}]}],
|
||||
"3":[{"key":"class","name":"剧情","value":[{"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":"财经"},{"n":"求职","v":"求职"}]},{"key":"area","name":"地区","value":[{"n":"全部","v":""},{"n":"内地","v":"内地"},{"n":"港台","v":"港台"},{"n":"日韩","v":"日韩"},{"n":"欧美","v":"欧美"}]},{"key":"lang","name":"语言","value":[{"n":"全部","v":""},{"n":"国语","v":"国语"},{"n":"英语","v":"英语"},{"n":"粤语","v":"粤语"},{"n":"闽南语","v":"闽南语"},{"n":"韩语","v":"韩语"},{"n":"日语","v":"日语"},{"n":"其它","v":"其它"}]},{"key":"year","name":"年份","value":[{"n":"全部","v":""},{"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":"2013","v":"2013"},{"n":"2012","v":"2012"},{"n":"2011","v":"2011"},{"n":"2010","v":"2010"},{"n":"2009","v":"2009"},{"n":"2008","v":"2008"},{"n":"2007","v":"2007"},{"n":"2006","v":"2006"},{"n":"2005","v":"2005"},{"n":"2004","v":"2004"}]},{"key":"letter","name":"字母","value":[{"n":"字母","v":""},{"n":"A","v":"A"},{"n":"B","v":"B"},{"n":"C","v":"C"},{"n":"D","v":"D"},{"n":"E","v":"E"},{"n":"F","v":"F"},{"n":"G","v":"G"},{"n":"H","v":"H"},{"n":"I","v":"I"},{"n":"J","v":"J"},{"n":"K","v":"K"},{"n":"L","v":"L"},{"n":"M","v":"M"},{"n":"N","v":"N"},{"n":"O","v":"O"},{"n":"P","v":"P"},{"n":"Q","v":"Q"},{"n":"R","v":"R"},{"n":"S","v":"S"},{"n":"T","v":"T"},{"n":"U","v":"U"},{"n":"V","v":"V"},{"n":"W","v":"W"},{"n":"X","v":"X"},{"n":"Y","v":"Y"},{"n":"Z","v":"Z"},{"n":"0-9","v":"0-9"}]},{"key":"by","name":"排序","value":[{"n":"时间排序","v":"time"},{"n":"人气排序","v":"hits"},{"n":"评分排序","v":"score"}]}],
|
||||
|
||||
"4":[{"key":"class","name":"剧情","value":[{"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":"战争"},{"n":"少年","v":"少年"},{"n":"少女","v":"少女"},{"n":"社会","v":"社会"},{"n":"原创","v":"原创"},{"n":"亲子","v":"亲子"},{"n":"益智","v":"益智"},{"n":"励志","v":"励志"},{"n":"其他","v":"其他"}]},{"key":"area","name":"地区","value":[{"n":"全部","v":""},{"n":"国产","v":"国产"},{"n":"日本","v":"日本"},{"n":"欧美","v":"欧美"},{"n":"其他","v":"其他"}]},{"key":"lang","name":"语言","value":[{"n":"全部","v":""},{"n":"国语","v":"国语"},{"n":"英语","v":"英语"},{"n":"粤语","v":"粤语"},{"n":"闽南语","v":"闽南语"},{"n":"韩语","v":"韩语"},{"n":"日语","v":"日语"},{"n":"其它","v":"其它"}]},{"key":"year","name":"年份","value":[{"n":"全部","v":""},{"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":"2013","v":"2013"},{"n":"2012","v":"2012"},{"n":"2011","v":"2011"},{"n":"2010","v":"2010"},{"n":"2009","v":"2009"},{"n":"2008","v":"2008"},{"n":"2007","v":"2007"},{"n":"2006","v":"2006"},{"n":"2005","v":"2005"},{"n":"2004","v":"2004"}]},{"key":"letter","name":"字母","value":[{"n":"字母","v":""},{"n":"A","v":"A"},{"n":"B","v":"B"},{"n":"C","v":"C"},{"n":"D","v":"D"},{"n":"E","v":"E"},{"n":"F","v":"F"},{"n":"G","v":"G"},{"n":"H","v":"H"},{"n":"I","v":"I"},{"n":"J","v":"J"},{"n":"K","v":"K"},{"n":"L","v":"L"},{"n":"M","v":"M"},{"n":"N","v":"N"},{"n":"O","v":"O"},{"n":"P","v":"P"},{"n":"Q","v":"Q"},{"n":"R","v":"R"},{"n":"S","v":"S"},{"n":"T","v":"T"},{"n":"U","v":"U"},{"n":"V","v":"V"},{"n":"W","v":"W"},{"n":"X","v":"X"},{"n":"Y","v":"Y"},{"n":"Z","v":"Z"},{"n":"0-9","v":"0-9"}]},{"key":"by","name":"排序","value":[{"n":"时间排序","v":"time"},{"n":"人气排序","v":"hits"},{"n":"评分排序","v":"score"}]}]}
|
||||
|
||||
return JSON.stringify({
|
||||
class: classes,
|
||||
filters: filterObj
|
||||
});
|
||||
}
|
||||
|
||||
async function homeVod() {
|
||||
const link = `${HOST}/`;
|
||||
try {
|
||||
const html = await request(link);
|
||||
if (!html) return null;
|
||||
const $ = load(html);
|
||||
|
||||
const hotSection = $('.title-a:contains("热播推荐")').closest('.box-width');
|
||||
if (hotSection.length === 0) return null;
|
||||
|
||||
const items = hotSection.find('.public-list-box');
|
||||
if (items.length === 0) return null;
|
||||
|
||||
const videos = _.map(items, (item) => {
|
||||
const $item = $(item);
|
||||
const $link = $item.find('.public-list-exp');
|
||||
const $img = $item.find('.gen-movie-img');
|
||||
const $title = $item.find('.time-title');
|
||||
const $subtitle = $item.find('.public-list-subtitle');
|
||||
|
||||
const href = $link.attr('href');
|
||||
const title = $title.attr('title') || getText($title);
|
||||
const imgSrc = $img.attr('data-src');
|
||||
const remarks = getText($subtitle);
|
||||
|
||||
const vod_id = href && href.startsWith('http') ? href : HOST + (href || '');
|
||||
const vod_name = title;
|
||||
const vod_pic = imgSrc ? (imgSrc.startsWith('http') ? imgSrc : HOST + imgSrc) : '';
|
||||
const vod_remarks = remarks;
|
||||
|
||||
return {
|
||||
vod_id,
|
||||
vod_name,
|
||||
vod_pic,
|
||||
vod_remarks
|
||||
};
|
||||
});
|
||||
|
||||
return JSON.stringify({
|
||||
list: videos
|
||||
});
|
||||
} catch (error) {
|
||||
return JSON.stringify({
|
||||
list: []
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function category(tid, pg, filter, extend) {
|
||||
if (pg <= 0) pg = 1;
|
||||
const homeFilter = {};
|
||||
const homeResult = JSON.parse(await home(homeFilter));
|
||||
const filterObj = homeResult.filters;
|
||||
if (!filterObj) return null;
|
||||
const filterGroup = filterObj[tid];
|
||||
if (!filterGroup) return null;
|
||||
const area = getFilterValue(filterGroup, 'area', extend);
|
||||
const by = getFilterValue(filterGroup, 'by', extend);
|
||||
const classValue = getFilterValue(filterGroup, 'class', extend);
|
||||
const lang = getFilterValue(filterGroup, 'lang', extend);
|
||||
const letter = getFilterValue(filterGroup, 'letter', extend);
|
||||
const year = getFilterValue(filterGroup, 'year', extend);
|
||||
const cateId = getFilterValue(filterGroup, 'cateId', extend);
|
||||
const baseUrl = `${HOST}/show/${tid}`;
|
||||
const link = `${baseUrl}-${area}-${by}-${classValue}-${lang}-${letter}---${pg}---${year}.html`;
|
||||
try {
|
||||
const html = await requestWithCaptcha(link, 'show');
|
||||
if (!html) return null;
|
||||
const $ = load(html);
|
||||
|
||||
const isParsedSuccessfully = $('.public-list-box').length > 0;
|
||||
if (!isParsedSuccessfully) return null;
|
||||
|
||||
const items = $('.public-list-box');
|
||||
const videos = _.map(items, (item) => {
|
||||
const $item = $(item);
|
||||
|
||||
const $link = $item.find('.public-list-exp');
|
||||
const $title = $item.find('.time-title');
|
||||
const $img = $item.find('.gen-movie-img');
|
||||
const $remarks = $item.find('.public-list-subtitle');
|
||||
|
||||
const href = $link.attr('href');
|
||||
const title = $title.attr('title') || getText($title);
|
||||
const imgSrc = $img.attr('data-src');
|
||||
const remarks = getText($remarks);
|
||||
|
||||
const vod_id = href && href.startsWith('http') ? href : HOST + (href || '');
|
||||
const vod_name = title;
|
||||
const vod_pic = imgSrc ? (imgSrc.startsWith('http') ? imgSrc : HOST + imgSrc) : '';
|
||||
const vod_remarks = remarks;
|
||||
|
||||
return {
|
||||
'vod_id': vod_id,
|
||||
'vod_name': vod_name,
|
||||
'vod_pic': vod_pic,
|
||||
'vod_remarks': vod_remarks
|
||||
};
|
||||
});
|
||||
|
||||
return JSON.stringify({
|
||||
list: videos
|
||||
});
|
||||
} catch (error) {
|
||||
return JSON.stringify({
|
||||
list: []
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getFilterValue(filterGroup, key, extend) {
|
||||
const filterItem = filterGroup.find(item => item.key === key);
|
||||
if (!filterItem) return '';
|
||||
const userValue = extend[key];
|
||||
if (userValue) {
|
||||
const option = filterItem.value.find(opt => opt.v === userValue);
|
||||
return option ? option.v : '';
|
||||
}
|
||||
return filterItem.value[0].v;
|
||||
}
|
||||
|
||||
async function search(wd) {
|
||||
const searchUrl = `${HOST}/nk/-------------.html?wd=${encodeURIComponent(wd)}`;
|
||||
const lowerWd = wd.toLowerCase();
|
||||
try {
|
||||
const html = await requestWithCaptcha(searchUrl, 'search');
|
||||
if (!html) return JSON.stringify({ list: [], error: '需要验证码但无法自动识别' });
|
||||
const $ = load(html);
|
||||
|
||||
const hasSearchResults = $('.public-list-box.search-box').length > 0;
|
||||
const hasSearchInput = $('input[name="wd"]').length > 0;
|
||||
const hasSearchHeader = $('.m-search').length > 0;
|
||||
|
||||
if ((hasSearchInput || hasSearchHeader) && !hasSearchResults) {
|
||||
return JSON.stringify({ list: [], error: '搜索无结果或需要验证码' });
|
||||
}
|
||||
|
||||
if (!hasSearchInput && !hasSearchHeader && !hasSearchResults) {
|
||||
return JSON.stringify({ list: [], error: '需要验证码但无法自动识别' });
|
||||
}
|
||||
|
||||
const items = $('.public-list-box.search-box');
|
||||
const videos = [];
|
||||
|
||||
items.each((index, item) => {
|
||||
const $item = $(item);
|
||||
|
||||
const $link = $item.find('.public-list-exp');
|
||||
const href = $link.attr('href');
|
||||
if (!href) return;
|
||||
|
||||
const $title = $item.find('.thumb-txt a');
|
||||
const vod_name = getText($title);
|
||||
if (!vod_name) return;
|
||||
|
||||
const $img = $item.find('.gen-movie-img');
|
||||
const vod_pic = $img.attr('data-src');
|
||||
|
||||
const $remarks = $item.find('.public-list-prb');
|
||||
const vod_remarks = getText($remarks);
|
||||
|
||||
const $info = $item.find('.thumb-else');
|
||||
const yearText = getText($info.find('a[href*="202"]').first());
|
||||
const areaText = getText($info.find('a[href*="美国"], a[href*="英国"], a[href*="泰国"]').first());
|
||||
|
||||
const lowerTitle = vod_name.toLowerCase();
|
||||
if (lowerTitle.includes(lowerWd)) {
|
||||
videos.push({
|
||||
'vod_id': HOST + href,
|
||||
'vod_name': vod_name,
|
||||
'vod_pic': vod_pic,
|
||||
'vod_remarks': vod_remarks,
|
||||
'vod_year': yearText,
|
||||
'vod_area': areaText
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return JSON.stringify({ list: videos });
|
||||
} catch (error) {
|
||||
return JSON.stringify({ list: [] });
|
||||
}
|
||||
}
|
||||
|
||||
async function detail(id) {
|
||||
try {
|
||||
const html = await request(id);
|
||||
if (!html) return JSON.stringify({ list: [] });
|
||||
const $ = load(html);
|
||||
|
||||
const vod = {
|
||||
vod_id: id,
|
||||
vod_name: getText($('.slide-info-title')),
|
||||
vod_pic: $('.detail-pic img').attr('data-src') || $('.detail-pic img').attr('src'),
|
||||
vod_year: '',
|
||||
vod_area: '',
|
||||
vod_remarks: '',
|
||||
vod_actor: '',
|
||||
vod_director: '',
|
||||
vod_content: getText($('#height_limit')),
|
||||
vod_play_from: '',
|
||||
vod_play_url: ''
|
||||
};
|
||||
|
||||
$('.slide-info').each((i, elem) => {
|
||||
const $elem = $(elem);
|
||||
const text = getText($elem);
|
||||
|
||||
if (text.includes('备注 :') || text.includes('备注 :')) {
|
||||
vod.vod_remarks = text.replace(/备注\s*[::]/g, '').trim();
|
||||
} else if (text.includes('导演 :') || text.includes('导演 :')) {
|
||||
vod.vod_director = text.replace(/导演\s*[::]/g, '').replace(/[\/\s]+$/, '').trim();
|
||||
} else if (text.includes('演员 :') || text.includes('演员 :')) {
|
||||
vod.vod_actor = text.replace(/演员\s*[::]/g, '').replace(/[\/\s]+$/, '').trim();
|
||||
} else if (text.includes('更新 :') || text.includes('更新 :')) {
|
||||
const updateText = text.replace(/更新\s*[::]/g, '').trim();
|
||||
const yearMatch = updateText.match(/\d{4}/);
|
||||
if (yearMatch) {
|
||||
vod.vod_year = yearMatch[0];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 从链接中提取年份和地区
|
||||
const yearLink = $('.slide-info a[href*="202"]').first();
|
||||
if (yearLink.length) {
|
||||
const yearText = getText(yearLink);
|
||||
if (/^\d{4}$/.test(yearText)) vod.vod_year = yearText;
|
||||
}
|
||||
|
||||
// 尝试从 slide-info-remarks 获取年份和地区
|
||||
$('.slide-info-remarks a').each((i, elem) => {
|
||||
const $a = $(elem);
|
||||
const href = $a.attr('href') || '';
|
||||
const aText = getText($a);
|
||||
if (/\d{4}/.test(aText) && !vod.vod_year) {
|
||||
vod.vod_year = aText.match(/\d{4}/)[0];
|
||||
}
|
||||
if ((href.includes('内地') || href.includes('香港') || href.includes('台湾') ||
|
||||
href.includes('美国') || href.includes('韩国') || href.includes('日本') ||
|
||||
href.includes('泰国') || href.includes('英国')) && !vod.vod_area) {
|
||||
vod.vod_area = aText;
|
||||
}
|
||||
});
|
||||
|
||||
const playFrom = [];
|
||||
const playUrl = [];
|
||||
$('.anthology-tab .swiper-slide').each((i, tab) => {
|
||||
const $tab = $(tab);
|
||||
let from = $tab.attr('data-from') || '';
|
||||
if (!from) from = `nk${i + 1}`;
|
||||
// 获取线路名称,优先从文本中提取
|
||||
let show = getText($tab);
|
||||
// 清理文本:去除图标字符、badge数字等
|
||||
show = show.replace(/[\ue000-\uf8ff]/g, '') // 去除图标字体
|
||||
.replace(/&\w+;/g, '')
|
||||
.replace(/\s*\d+\s*$/, '') // 去除末尾的badge数字
|
||||
.trim();
|
||||
if (!show) show = from;
|
||||
playFrom.push(show);
|
||||
});
|
||||
|
||||
$('.anthology-list-box').each((i, box) => {
|
||||
const episodes = [];
|
||||
const $tab = $('.anthology-tab .swiper-slide').eq(i);
|
||||
let from = $tab.attr('data-from') || '';
|
||||
if (!from) from = `nk${i + 1}`;
|
||||
$(box).find('.anthology-list-play a').each((j, link) => {
|
||||
const $link = $(link);
|
||||
let episodeName = getText($link);
|
||||
if (!episodeName) {
|
||||
// 如果还是获取不到,从 href 中提取
|
||||
const href = $link.attr('href') || '';
|
||||
const match = href.match(/-(\d+)\.html$/);
|
||||
episodeName = match ? `第${match[1]}集` : `第${j + 1}集`;
|
||||
}
|
||||
const episodeUrl = HOST + $link.attr('href');
|
||||
episodes.push(`${episodeName}$${episodeUrl}@${from}`);
|
||||
});
|
||||
playUrl.push(episodes.join('#'));
|
||||
});
|
||||
|
||||
vod.vod_play_from = playFrom.join('$$$');
|
||||
vod.vod_play_url = playUrl.join('$$$');
|
||||
|
||||
return JSON.stringify({ list: [vod] });
|
||||
} catch (error) {
|
||||
return JSON.stringify({ list: [] });
|
||||
}
|
||||
}
|
||||
|
||||
async function play(flag, id, flags) {
|
||||
try {
|
||||
const parts = id.split('@');
|
||||
if (parts.length < 2) {
|
||||
throw new Error('播放链接格式错误,缺少英文标识');
|
||||
}
|
||||
|
||||
const playUrl = parts[0];
|
||||
const from = parts[1];
|
||||
const html = await request(playUrl);
|
||||
const playerStart = html.indexOf('var player_aaaa=');
|
||||
if (playerStart === -1) throw new Error('未找到player_aaaa');
|
||||
const playerEnd = html.indexOf('</script>', playerStart);
|
||||
if (playerEnd === -1) throw new Error('未找到脚本结束标签');
|
||||
|
||||
let playerScript = html.substring(playerStart + 'var player_aaaa='.length, playerEnd);
|
||||
playerScript = playerScript.split(';')[0].trim()
|
||||
.replace(/,\s*$/, '')
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
.replace(/\/\/.*/g, '');
|
||||
|
||||
let playerData;
|
||||
try {
|
||||
playerData = JSON.parse(playerScript);
|
||||
} catch (parseErr) {
|
||||
eval(`playerData = ${playerScript}`);
|
||||
}
|
||||
if (!playerData || !playerData.url) {
|
||||
throw new Error('播放数据格式异常: url 缺失');
|
||||
}
|
||||
|
||||
let playurl = playerData.url;
|
||||
if (/m3u8|mp4/i.test(playurl)) {
|
||||
return JSON.stringify({ parse: 0, url: playurl });
|
||||
}
|
||||
|
||||
const configUrl = `${HOST}/static/js/playerconfig.js?t=20251225`;
|
||||
const configJs = await request(configUrl);
|
||||
if (!configJs) throw new Error('获取 playerconfig.js 失败');
|
||||
|
||||
let MacPlayerConfig = {};
|
||||
try {
|
||||
const evalCode = `(function(){
|
||||
var MacPlayerConfig = {};
|
||||
${configJs.replace(/^var MacPlayerConfig=.*?;/, '')}
|
||||
return MacPlayerConfig;
|
||||
})()`;
|
||||
MacPlayerConfig = eval(evalCode);
|
||||
} catch (e) {
|
||||
MacPlayerConfig = {
|
||||
player_list: {
|
||||
nk1: { parse: 'https://gg.xn--it-if7c19g5s4bps5c.com/nkvod3.php' },
|
||||
nk2: { parse: 'https://gg.xn--it-if7c19g5s4bps5c.com/nkvod3.php' },
|
||||
nk4k: { parse: 'https://gg.xn--it-if7c19g5s4bps5c.com/nkvod3.php' }
|
||||
}
|
||||
};
|
||||
}
|
||||
const playerList = MacPlayerConfig.player_list || {};
|
||||
let parseDomain = playerList[from]?.parse || playerList.nk4k.parse;
|
||||
|
||||
let jx = '';
|
||||
if (parseDomain.includes('?')) {
|
||||
jx = `${parseDomain}&url=${encodeURIComponent(playurl)}&next=${encodeURIComponent(playerData.link_next || '')}&title=${encodeURIComponent(playerData.vod_data?.vod_name || '')}`;
|
||||
} else {
|
||||
jx = `${parseDomain}?url=${encodeURIComponent(playurl)}&next=${encodeURIComponent(playerData.link_next || '')}&title=${encodeURIComponent(playerData.vod_data?.vod_name || '')}`;
|
||||
}
|
||||
|
||||
const jxhtml = await request(jx);
|
||||
if (!jxhtml) throw new Error('解析接口响应为空');
|
||||
|
||||
let raw_key = [], iv = [], encrypted = '';
|
||||
|
||||
const rawKeyMatch = jxhtml.match(/var\s+raw_key\s*=\s*\[([^\]]+)\]/);
|
||||
if (rawKeyMatch) {
|
||||
raw_key = rawKeyMatch[1].split(',').map(item => parseInt(item.trim())).filter(num => !isNaN(num));
|
||||
}
|
||||
|
||||
const ivMatch = jxhtml.match(/var\s+iv\s*=\s*\[([^\]]+)\]/);
|
||||
if (ivMatch) {
|
||||
iv = ivMatch[1].split(',').map(item => parseInt(item.trim())).filter(num => !isNaN(num));
|
||||
}
|
||||
|
||||
const encryptedMatch = jxhtml.match(/var\s+encrypted\s*=\s*["']([a-fA-F0-9]+)["']/);
|
||||
if (encryptedMatch) {
|
||||
encrypted = encryptedMatch[1];
|
||||
}
|
||||
|
||||
const validKeyLengths = [16, 32];
|
||||
const validIVLength = 16;
|
||||
if (!encrypted || !validKeyLengths.includes(raw_key.length) || iv.length !== validIVLength) {
|
||||
throw new Error(`加密参数提取失败或长度异常 → key:${raw_key.length}, iv:${iv.length}, encrypted:${encrypted.length}`);
|
||||
}
|
||||
|
||||
const decryptedText = decryptData(encrypted, raw_key, iv);
|
||||
|
||||
if (!decryptedText) {
|
||||
throw new Error('解密失败');
|
||||
}
|
||||
|
||||
let finalPlayUrl = '';
|
||||
const randomMatch = decryptedText.match(/getrandom\(['"]([^'"]+)['"]\)/);
|
||||
if (randomMatch) {
|
||||
finalPlayUrl = getrandom(randomMatch[1]);
|
||||
} else {
|
||||
const urlPattern = /https?:\/\/[^\s"'<>]+/gi;
|
||||
const urlMatches = decryptedText.match(urlPattern);
|
||||
if (urlMatches && urlMatches.length > 0) {
|
||||
finalPlayUrl = urlMatches[0];
|
||||
} else {
|
||||
throw new Error('未找到 getrandom 参数或URL');
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify({ parse: 0, url: finalPlayUrl });
|
||||
} catch (error) {
|
||||
return JSON.stringify({ parse: 1, url: id, error: error.message });
|
||||
}
|
||||
}
|
||||
function validateHexData(hexStr) {
|
||||
if (hexStr.length % 2 !== 0) {
|
||||
return false;
|
||||
}
|
||||
if (!/^[0-9a-fA-F]+$/.test(hexStr)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function decryptData(encryptedHex, rawKeyArray, ivArray) {
|
||||
if (!validateHexData(encryptedHex)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawKey = Crypto.lib.WordArray.create(new Uint8Array(rawKeyArray));
|
||||
const iv = Crypto.lib.WordArray.create(new Uint8Array(ivArray));
|
||||
const encrypted = Crypto.enc.Hex.parse(encryptedHex);
|
||||
|
||||
const modes = [
|
||||
{ name: 'CBC', mode: Crypto.mode.CBC },
|
||||
{ name: 'OFB', mode: Crypto.mode.OFB },
|
||||
{ name: 'CTR', mode: Crypto.mode.CTR },
|
||||
{ name: 'ECB', mode: Crypto.mode.ECB },
|
||||
{ name: 'CFB', mode: Crypto.mode.CFB }
|
||||
];
|
||||
|
||||
for (let modeInfo of modes) {
|
||||
try {
|
||||
const decryptionOptions = {
|
||||
mode: modeInfo.mode,
|
||||
padding: Crypto.pad.Pkcs7
|
||||
};
|
||||
if (modeInfo.name !== 'ECB') decryptionOptions.iv = iv;
|
||||
|
||||
const decrypted = Crypto.AES.decrypt({ ciphertext: encrypted }, rawKey, decryptionOptions);
|
||||
const result = decrypted.toString(Crypto.enc.Utf8);
|
||||
if (result && result.length > 0) {
|
||||
return result;
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getrandom(b) {
|
||||
try {
|
||||
const string = b.substring(10);
|
||||
let substr;
|
||||
try {
|
||||
const base64Data = Crypto.enc.Base64.parse(string);
|
||||
substr = base64Data.toString(Crypto.enc.Latin1);
|
||||
} catch (e) {
|
||||
throw new Error('Base64解码失败');
|
||||
}
|
||||
|
||||
if (!substr) {
|
||||
throw new Error('Base64解码结果为空');
|
||||
}
|
||||
|
||||
const substrTrim = substr.substring(10);
|
||||
let data2 = substrTrim.replace('_nanke', '');
|
||||
let data3 = data2.slice(0, 20) + data2.slice(21);
|
||||
const hexStr = data3.replace(/[^0-9a-fA-F]/g, '');
|
||||
const finalUrl = hexDecodeAndFilter(hexStr);
|
||||
return finalUrl;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function hexDecodeAndFilter(hexStr) {
|
||||
try {
|
||||
let pureHex = hexStr.replace(/[^0-9a-fA-F]/g, '');
|
||||
if (pureHex.length % 2 !== 0) {
|
||||
pureHex += '0';
|
||||
}
|
||||
let decoded = '';
|
||||
for (let i = 0; i < pureHex.length; i += 2) {
|
||||
const hexByte = pureHex.substr(i, 2);
|
||||
const charCode = parseInt(hexByte, 16);
|
||||
const char = String.fromCharCode(charCode);
|
||||
const isUrlValid = /[a-zA-Z0-9:\/\.\-\?\&=\%]/.test(char);
|
||||
if (isUrlValid) {
|
||||
decoded += char;
|
||||
}
|
||||
}
|
||||
const urlMatch = decoded.match(/https?:\/\/[^\s]+/);
|
||||
decoded = urlMatch ? urlMatch[0] : decoded.trim();
|
||||
return decoded;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export function __jsEvalReturn() {
|
||||
return {
|
||||
init: init,
|
||||
detail: detail,
|
||||
home: home,
|
||||
play: play,
|
||||
search: search,
|
||||
homeVod: homeVod,
|
||||
category: category
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user