Files
xvip/js/优夏.js
2026-08-15 10:28:06 +02:00

470 lines
16 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import cheerio from 'assets://js/lib/cheerio.min.js';
const appConfig = {
siteName: "伦理专题影片",
siteUrl: "http://yoxayg.com"
};
const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
async function init(ext) {
console.log("初始化爬虫:", appConfig.siteName);
}
const classList = [
{ type_id: "1", type_name: "电影" },
{ type_id: "2", type_name: "电视剧" },
{ type_id: "3", type_name: "综艺" },
{ type_id: "4", type_name: "动漫" },
{ type_id: "36", type_name: "短剧" },
{ type_id: "6", type_name: "动作片" },
{ type_id: "7", type_name: "喜剧片" },
{ type_id: "8", type_name: "爱情片" },
{ type_id: "9", type_name: "科幻片" },
{ type_id: "10", type_name: "恐怖片" },
{ type_id: "11", type_name: "剧情片" },
{ type_id: "12", type_name: "战争片" },
{ type_id: "20", type_name: "记录片" },
{ type_id: "34", type_name: "伦理片" },
{ type_id: "13", type_name: "国产剧" },
{ type_id: "14", type_name: "香港剧" },
{ type_id: "15", type_name: "韩国剧" },
{ type_id: "16", type_name: "欧美剧" },
{ type_id: "21", type_name: "台湾剧" },
{ type_id: "22", type_name: "日本剧" },
{ type_id: "23", type_name: "海外剧" },
{ type_id: "24", type_name: "泰国剧" },
{ type_id: "25", type_name: "大陆综艺" },
{ type_id: "26", type_name: "港台综艺" },
{ type_id: "27", type_name: "日韩综艺" },
{ type_id: "28", type_name: "欧美综艺" },
{ type_id: "29", type_name: "国产动漫" },
{ type_id: "30", type_name: "日韩动漫" },
{ type_id: "31", type_name: "欧美动漫" },
{ type_id: "32", type_name: "港台动漫" },
{ type_id: "33", type_name: "海外动漫" }
];
const typeSlugMap = {
"1": "dianying", "2": "dianshiju", "3": "zongyi", "4": "dongman", "6": "dongzuopian",
"7": "xijupian", "8": "aiqingpian", "9": "kehuanpian", "10": "kongbupian", "11": "juqingpian",
"12": "zhanzhengpian", "13": "guochanju", "14": "xianggangju", "15": "hanguoju", "16": "oumeiju",
"20": "jilupian", "21": "taiwanju", "22": "ribenju", "23": "haiwaiju", "24": "taiguoju",
"25": "daluzongyi", "26": "gangtaizongyi", "27": "rihanzongyi", "28": "oumeizongyi",
"29": "guochandongman", "30": "rihandongman", "31": "oumeidongman", "32": "gangtaidongman",
"33": "haiwaidongman", "34": "lunlipian", "36": "duanju"
};
const typeParentMap = {
"1": 0, "2": 0, "3": 0, "4": 0, "6": 1, "7": 1, "8": 1, "9": 1, "10": 1,
"11": 1, "12": 1, "13": 2, "14": 2, "15": 2, "16": 2, "20": 1, "21": 2, "22": 2,
"23": 2, "24": 2, "25": 3, "26": 3, "27": 3, "28": 3, "29": 4, "30": 4,
"31": 4, "32": 4, "33": 4, "34": 1, "36": 2
};
function topType(tid) {
tid = parseInt(tid || 0, 10);
if (tid === 36) return 36;
const p = parseInt(typeParentMap[String(tid)] || 0, 10);
return p > 0 ? p : tid;
}
function typeSlug(tid) {
return typeSlugMap[String(parseInt(tid || 0, 10))] || ('type' + parseInt(tid || 0, 10));
}
function getAreaFilter() {
return {
"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": "其他" }
]
};
}
function getYearFilter() {
let years = [{ "n": "全部", "v": "" }];
const currentYear = new Date().getFullYear();
for (let y = currentYear; y >= 2010; y--) {
years.push({ "n": String(y), "v": String(y) });
}
return { "key": "year", "name": "年份", "value": years };
}
const commonFilters = [getAreaFilter(), getYearFilter()];
const myFilters = {};
classList.forEach(item => {
myFilters[item.type_id] = commonFilters;
});
function fixUrl(u) {
if (!u) return '';
if (u.startsWith('http')) return u;
if (u.startsWith('//')) return 'http:' + u;
if (u.startsWith('/')) return appConfig.siteUrl + u;
return u;
}
async function home(filter) {
let list = [];
try {
const html = (await req(appConfig.siteUrl, {
method: "GET",
headers: { "User-Agent": UA }
})).content;
const $ = cheerio.load(html);
let seen = {};
$(".tpl-card[href]").each(function () {
let vod_id = $(this).attr("href");
if (!vod_id || seen[vod_id]) return;
let vod_name = $(this).attr("title") || $(this).find("strong").first().text().trim() || "";
vod_name = vod_name.replace(/免费在线观看$/, "");
let vod_pic = fixUrl($(this).find("img").first().attr("src") || "");
let vod_remarks = $(this).find("em").first().text().trim();
if (vod_name && vod_id) {
seen[vod_id] = true;
list.push({ vod_id, vod_name, vod_pic, vod_remarks });
}
});
} catch (e) {
console.error("首页推荐获取失败:", e.message);
}
return JSON.stringify({
class: classList,
filters: myFilters,
list: list.slice(0, 30)
});
}
function buildCategoryUrl(tid, pg, extend) {
extend = extend || {};
let top = topType(tid);
let topSlug = typeSlug(top);
let childSlug = (tid != top) ? typeSlug(tid) : '';
let path;
if (childSlug && tid != 36) {
path = `/${topSlug}/${childSlug}/`;
} else {
path = `/${topSlug}/`;
}
if (pg > 1) {
path += `page-${pg}.html`;
}
let area = extend.area || '';
let year = extend.year || '';
if (area || year) {
let params = [];
if (area) params.push('area=' + encodeURIComponent(area));
if (year) params.push('year=' + encodeURIComponent(year));
return appConfig.siteUrl + `/api_proxy.php?ac=list&t=${tid}&pg=${pg}&${params.join('&')}`;
}
return appConfig.siteUrl + path;
}
function parseListHtml(html) {
const $ = cheerio.load(html);
let list = [];
let vodIds = {};
$(".tpl-card.card-poster, .tpl-card.card-wide").each(function () {
let $a = $(this);
let vod_id = $a.attr("href");
if (!vod_id || vodIds[vod_id]) return;
let vod_name = $a.attr("title") || $a.find("strong").first().text().trim() || "";
vod_name = vod_name.replace(/免费在线观看$/, "");
let vod_pic = fixUrl($a.find("img").first().attr("src") || "");
let vod_remarks = $a.find("em").first().text().trim() || $a.find("small").first().text().trim();
if (vod_name && vod_id) {
vodIds[vod_id] = true;
list.push({ vod_id, vod_name, vod_pic, vod_remarks });
}
});
if (list.length === 0) {
try {
let data = JSON.parse(html);
if (data.list && Array.isArray(data.list)) {
data.list.forEach(item => {
let vod_id = `/${typeSlug(topType(item.type_id))}/${(item.vod_en || item.vod_name || '').toLowerCase().replace(/[^a-z0-9]+/g, '')}.html`;
let vod_name = item.vod_name || "";
let vod_pic = item.vod_pic || "";
let vod_remarks = item.vod_remarks || "";
if (vod_name) {
list.push({ vod_id, vod_name, vod_pic, vod_remarks });
}
});
}
} catch (e) {
}
}
let pagecount = 1;
$(".pagination a").each(function () {
let href = $(this).attr("href") || '';
let m = href.match(/page-(\d+)/);
if (m) {
let p = parseInt(m[1]);
if (p > pagecount) pagecount = p;
}
});
return { list, pagecount };
}
async function category(tid, pg, filter, extend) {
pg = pg || 1;
extend = extend || {};
let url = buildCategoryUrl(tid, pg, extend);
try {
const html = (await req(url, {
method: "GET",
headers: { "User-Agent": UA, "Referer": appConfig.siteUrl }
})).content;
const result = parseListHtml(html);
return JSON.stringify(result);
} catch (e) {
console.error("分类列表获取失败:", e.message);
return JSON.stringify({ list: [], pagecount: 0 });
}
}
async function search(wd, quick, page) {
page = page || 1;
try {
const url = `${appConfig.siteUrl}/search-${encodeURIComponent(wd)}${page > 1 ? '-page-' + page : ''}.html`;
const html = (await req(url, {
method: "GET",
headers: { "User-Agent": UA, "Referer": appConfig.siteUrl }
})).content;
const result = parseListHtml(html);
return JSON.stringify(result);
} catch (e) {
console.error("搜索失败:", e.message);
return JSON.stringify({ list: [], pagecount: 0 });
}
}
async function detail(id) {
try {
const html = (await req(appConfig.siteUrl + id, {
method: "GET",
headers: { "User-Agent": UA, "Referer": appConfig.siteUrl }
})).content;
const $ = cheerio.load(html);
let vod_name = $("h1").first().text().trim();
let vod_pic = fixUrl($(".detail-poster img").first().attr("src") || "");
let vod_director = "";
let vod_actor = "";
let vod_area = "";
let vod_year = "";
let vod_content = "";
let vod_class = "";
let vod_remarks = "";
$(".detail-tags span").each(function () {
let text = $(this).text().trim();
if (/^\d{4}$/.test(text) && !vod_year) vod_year = text;
else if (/大陆|香港|台湾|美国|日本|韩国|英国|法国|德国|泰国|印度|其他/.test(text) && !vod_area) vod_area = text;
else if (!vod_class && text !== vod_year && text !== vod_area && !/更新至|集|期|TC|HD|正片/.test(text)) vod_class = text;
});
vod_remarks = $(".detail-poster span").first().text().trim();
$(".detail-meta p").each(function () {
let text = $(this).text();
if (text.includes("主演:") && !vod_actor) {
vod_actor = text.replace("主演:", "").trim();
}
if (text.includes("导演:") && !vod_director) {
vod_director = text.replace("导演:", "").trim();
}
});
let $intro = $(".detail-intro");
if ($intro.length > 0) {
vod_content = $intro.first().text().replace("简介:", "").trim();
}
if (!vod_content) {
$intro = $(".article-text");
if ($intro.length > 0) {
vod_content = $intro.first().text().trim();
}
}
let lines = [];
let playlists = [];
let epArray = [];
$(".episode-grid a").each(function () {
let name = $(this).text().trim();
let href = $(this).attr('href') || '';
if (name && href) {
epArray.push({ name, href });
}
});
epArray.sort((a, b) => {
let numA = parseInt(a.name.match(/第(\d+)/)?.[1] || 0);
let numB = parseInt(b.name.match(/第(\d+)/)?.[1] || 0);
return numA - numB;
});
let episodes = epArray.map(ep => `${ep.name}$${ep.href}`);
if (episodes.length > 0) {
lines.push("默认");
playlists.push(episodes);
} else {
let playHref = $(".detail-actions a.btn-primary").attr("href") || "";
if (playHref) {
lines.push("默认");
playlists.push([`正片$${playHref}`]);
}
}
if (lines.length === 0) {
lines.push("默认");
playlists.push([`暂无播放地址$${id}`]);
}
const { vod_play_from, vod_play_url } = buildVodPlayData(lines, playlists);
return JSON.stringify({
list: [{
vod_id: id,
vod_name,
vod_pic,
vod_actor,
vod_director,
vod_remarks,
vod_year,
vod_area,
vod_content,
vod_class,
vod_play_from,
vod_play_url
}]
});
} catch (error) {
console.error(`解析详情页异常 [ID: ${id}]:`, error);
return JSON.stringify({ list: [] });
}
}
function buildVodPlayData(lines, playlists) {
const processedPlaylists = playlists.map(eps => eps.join('#'));
return {
vod_play_from: lines.filter(Boolean).join('$$$'),
vod_play_url: processedPlaylists.join('$$$')
};
}
async function play(flag, id, flags) {
try {
if (id.startsWith("http")) {
return JSON.stringify({
parse: 0,
Header: { "User-Agent": UA, "Referer": appConfig.siteUrl },
url: id
});
}
const html = (await req(`${appConfig.siteUrl}${id}`, {
method: "GET",
headers: { "User-Agent": UA, "Referer": appConfig.siteUrl }
})).content;
// 方式1:从 noscript 中提取 m3u8 URL
let noscriptMatch = html.match(/<noscript>.*?当前播放地址[:]\s*(https?:\/\/[^\s<"]+\.m3u8[^\s<"]*)/);
if (noscriptMatch) {
return JSON.stringify({
parse: 0,
Header: { "User-Agent": UA, "Referer": appConfig.siteUrl },
url: noscriptMatch[1]
});
}
// 方式2:从 JS 中提取 var src="..."
let srcMatch = html.match(/var\s+src\s*=\s*["'](https?:\/\/[^"']+\.m3u8[^"']*)/);
if (srcMatch) {
return JSON.stringify({
parse: 0,
Header: { "User-Agent": UA, "Referer": appConfig.siteUrl },
url: srcMatch[1]
});
}
// 方式3:直接匹配 m3u8 URL
let urlMatch = html.match(/(https?:\/\/[^"'\s<>]+\.m3u8[^"'\s<>]*)/);
if (urlMatch) {
return JSON.stringify({
parse: 0,
Header: { "User-Agent": UA, "Referer": appConfig.siteUrl },
url: urlMatch[1]
});
}
// 方式4player_aaaa
let playerMatch = html.match(/var player_aaaa\s*=\s*(\{.+?\});/);
if (playerMatch) {
try {
let playerData = JSON.parse(playerMatch[1]);
if (playerData.url) {
return JSON.stringify({
parse: 0,
Header: { "User-Agent": UA, "Referer": appConfig.siteUrl },
url: playerData.url
});
}
} catch (e) {}
}
// 方式5video 标签的 src
const $ = cheerio.load(html);
let videoSrc = $("video").attr("src") || $("video source").attr("src");
if (videoSrc) {
return JSON.stringify({
parse: 0,
Header: { "User-Agent": UA, "Referer": appConfig.siteUrl },
url: fixUrl(videoSrc)
});
}
return JSON.stringify({
parse: 1,
Header: { "User-Agent": UA, "Referer": appConfig.siteUrl },
url: appConfig.siteUrl + id
});
} catch (e) {
console.error("播放失败:", e);
return JSON.stringify({ parse: 0, url: "" });
}
}
export default {
init,
home,
category,
detail,
search,
play
};