Sync all projects

This commit is contained in:
github-actions[bot]
2026-07-25 14:13:59 +00:00
parent f1cf300521
commit 0fe69536c3
24 changed files with 10265 additions and 9145 deletions
+329
View File
@@ -0,0 +1,329 @@
/**
* OK影视 / CatVodSpider JS版 - 片库网爬虫(性能优化版)
*/
const HOST = "https://4k01.pianku.online";
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";
const parseAPiUrl = "https://svip.qlplayer.cyou/?url=";
const HEADERS = {
"User-Agent": UA,
};
// 预编译高频使用的正则,提升运行效率并节省内存开销
const VOD_ITEM_REG = /<div class="vod-item">[\s\S]*?<a href="\/voddetail\/(\d+)\.html" title="(.*?)"[\s\S]*?<img src="(.*?)"[\s\S]*?<span class="remarks">(.*?)<\/span>/g;
const CLEAN_TAG_REG = /<[^>]+>/g;
const DIRECT_URL_REG = /\.(m3u8|mp4)/i;
function buildUrl(path) {
if (!path) return "";
if (path.startsWith("http")) return path;
return HOST + (path.startsWith("/") ? "" : "/") + path;
}
function mylog(...args) {
console.log(`[片库网]`, ...args);
}
function getVodList(html) {
if (!html) return [];
const list = [];
VOD_ITEM_REG.lastIndex = 0; // 重置正则索引
let match;
while ((match = VOD_ITEM_REG.exec(html)) !== null) {
list.push({
vod_id: match[1],
vod_name: match[2],
vod_pic: buildUrl(match[3]),
vod_remarks: match[4].trim()
});
}
mylog(`共提取到 ${list.length} 条数据`);
return list;
}
async function init(cfg) {
mylog("Spider Init Done");
}
async function home(filter) {
mylog(`开始加载首页,filter=${filter}`);
try {
const classes = [
{ "type_id": "20", "type_name": "电影" },
{ "type_id": "37", "type_name": "剧集" },
{ "type_id": "43", "type_name": "动漫" },
{ "type_id": "45", "type_name": "综艺" }
];
const filters = {
"20": [{
"key": "tid",
"name": "分类",
"value": [
{ "n": "全部", "v": "20" },
{ "n": "动作片", "v": "21" }, { "n": "喜剧片", "v": "22" },
{ "n": "爱情片", "v": "23" }, { "n": "科幻片", "v": "24" },
{ "n": "恐怖片", "v": "25" }, { "n": "剧情片", "v": "26" },
{ "n": "战争片", "v": "27" }, { "n": "惊悚片", "v": "28" },
{ "n": "犯罪片", "v": "29" }, { "n": "冒险篇", "v": "30" },
{ "n": "动画片", "v": "31" }, { "n": "悬疑片", "v": "32" },
{ "n": "武侠片", "v": "33" }, { "n": "奇幻片", "v": "34" },
{ "n": "纪录片", "v": "35" }, { "n": "其他片", "v": "36" }
]
}]
};
const res = await req(HOST, { headers: HEADERS });
const vodList = getVodList(res.content);
return JSON.stringify({
class: classes,
filters: filter ? filters : {},
list: vodList
});
} catch (e) {
mylog(e.message);
return JSON.stringify({ class: [], list: [] });
}
}
async function homeVod() {
mylog("获取首页推荐视频");
try {
const res = await req(HOST, { headers: HEADERS });
const vodList = getVodList(res.content);
return JSON.stringify({ list: vodList });
} catch (e) {
mylog(e.message);
return JSON.stringify({ list: [] });
}
}
async function category(tid, pg, filter, extend) {
let realTid = (extend && extend.tid) ? extend.tid : tid;
const page = pg || "1";
const url = page === "1" ? `${HOST}/vodtype/${realTid}.html` : `${HOST}/vodtype/${realTid}-${page}.html`;
mylog(`请求分类 URL: ${url}`);
try {
const res = await req(url, { headers: HEADERS });
const html = res.content;
const vodList = getVodList(html);
let pagecount = parseInt(page) + 1;
let total = 0;
const pageMatch = html.match(/尾页.*?href=".*?-(\d+)\.html"/);
if (pageMatch) {
pagecount = parseInt(pageMatch[1]);
total = pagecount * 24;
}
return JSON.stringify({
list: vodList,
page: parseInt(page),
pagecount: pagecount,
limit: 24,
total: total
});
} catch (e) {
mylog(e.message);
return JSON.stringify({ list: [], page: 1, pagecount: 1, limit: 24, total: 0 });
}
}
async function detail(id) {
const url = `${HOST}/voddetail/${id}.html`;
mylog(`获取详情页 URL: ${url}`);
try {
const res = await req(url, { headers: HEADERS });
const html = res.content || "";
const getMatch = (re) => {
const m = html.match(re);
return m ? m[1].trim() : "";
};
const title = getMatch(/<h1[^>]*class="detail-title"[^>]*>(.*?)(?:<span|<\/h1>)/s);
let pic = getMatch(/class="detail-poster"[^>]*>[\s\S]*?<img src="(.*?)"/);
if (pic) pic = buildUrl(pic);
const remarks = getMatch(/class="detail-remarks"[^>]*>(.*?)<\/span>/);
const content = getMatch(/class="detail-desc"[^>]*>[\s\S]*?<p>(.*?)<\/p>/);
// 提取演员元数据
let director = "", actor = "", area = "", year = "";
const metaRegex = /<(?:span|p|div)[^>]*>(导演|主演|地区|年份)[:](.*?)(?:<\/span>|<\/p>|<\/div>)/g;
let metaMatch;
while ((metaMatch = metaRegex.exec(html)) !== null) {
const key = metaMatch[1];
const val = metaMatch[2].replace(CLEAN_TAG_REG, "").trim();
if (key === "导演") director = val;
else if (key === "主演") actor = val;
else if (key === "地区") area = val;
else if (key === "年份") year = val;
}
// 提取播放线路
const playFromList = [];
const tabRegex = /class="source-tab-item[^"]*"[^>]*>(.*?)<\/span>/g;
let tabMatch;
while ((tabMatch = tabRegex.exec(html)) !== null) {
let from = tabMatch[1].trim();
if (from.includes("自营4K60帧") || from.includes("自营4k60帧")) {
from = `${from}(注意直连)`;
}
playFromList.push(from);
}
// 优化剧集面板提取:采用分割(split)避免正则跨大段 HTML 回溯
const playUrlList = [];
const panes = html.split('class="source-pane');
for (let i = 1; i < panes.length; i++) {
const paneHtml = panes[i].split("</div>")[0] || panes[i];
const episodes = [];
const epRegex = /href="(\/vodplay\/[^"]+)"[^>]*>(.*?)<\/a>/g;
let epMatch;
while ((epMatch = epRegex.exec(paneHtml)) !== null) {
const epName = epMatch[2].replace(CLEAN_TAG_REG, "").strip ? epMatch[2].replace(CLEAN_TAG_REG, "").strip() : epMatch[2].replace(CLEAN_TAG_REG, "").trim();
const epUrl = buildUrl(epMatch[1]);
episodes.push(`${epName}$${epUrl}`);
}
if (episodes.length > 0) {
playUrlList.push(episodes.join("#"));
}
}
// 补充默认线路名
if (playFromList.length === 0 && playUrlList.length > 0) {
playUrlList.forEach((_, i) => playFromList.push(`线路 ${i + 1}`));
}
const vod = {
vod_id: id,
vod_name: title,
vod_pic: pic,
vod_type_name: "",
vod_year: year,
vod_area: area,
vod_remarks: remarks,
vod_actor: actor,
vod_director: director,
vod_content: content,
vod_play_from: playFromList.join("$$$"),
vod_play_url: playUrlList.join("$$$")
};
mylog(`成功解析视频详情: ${title}`);
return JSON.stringify({ list: [vod] });
} catch (e) {
mylog(e.message);
return JSON.stringify({ list: [] });
}
}
async function search(key, quick, pg) {
const encodedKey = encodeURIComponent(key);
const url = `${HOST}/vodsearch/-------------.html?wd=${encodedKey}`;
mylog(`开始搜索关键词: ${key} -> URL: ${url}`);
try {
const res = await req(url, { headers: HEADERS });
const vodList = getVodList(res.content);
return JSON.stringify({
list: vodList,
page: 1,
pagecount: 1
});
} catch (e) {
mylog(e.message);
return JSON.stringify({ list: [] });
}
}
function formatUrl(url) {
if (!url) return "";
return url.replace(/\\/g, "").replace(/^(https?:\/)((?!\/))/i, "$1/");
}
function extractConfig(html) {
const apiTokenMatch = html.match(/apiToken\s*:\s*["']([^"']+)["']/);
return {
apiToken: apiTokenMatch ? apiTokenMatch[1] : null
};
}
// 修复判定逻辑 Bug
function isDirectVideoUrl(url) {
return DIRECT_URL_REG.test(url);
}
async function parseVideoUrl(url) {
try {
const resoleUrl = parseAPiUrl + url;
mylog("解析地址", resoleUrl);
const html1 = (await req(resoleUrl)).content || "";
const { apiToken } = extractConfig(html1);
if (!apiToken) return "";
const parseTokenUrl = `https://svip.qlplayer.cyou/api/resolve.php?token=${encodeURIComponent(apiToken)}`;
mylog("parseTokenUrl", parseTokenUrl);
const res = await req(parseTokenUrl);
const data = JSON.parse(res.content);
mylog("data", data);
const finalUrl = formatUrl(data.url);
mylog("finalUrl", finalUrl);
return finalUrl;
} catch (e) {
mylog("视频解析失败:", e.message);
return "";
}
}
async function play(flag, id, flags) {
const playUrl = buildUrl(id);
mylog(`开始获取播放地址: ${playUrl}`);
try {
const res = await req(playUrl, { headers: HEADERS });
const html = res.content || "";
const match = html.match(/player_aaaa\s*=\s*(\{[\s\S]*?\})/);
if (match && match[1]) {
const playerData = JSON.parse(match[1]);
const targetUrl = playerData.url || "";
if (isDirectVideoUrl(targetUrl)) {
mylog("是直连,", targetUrl);
return JSON.stringify({ parse: 0, url: targetUrl });
}
const finalPlayUrl = await parseVideoUrl(targetUrl);
return JSON.stringify({ parse: 0, url: finalPlayUrl });
} else {
mylog("未在网页 HTML 中找到 player_aaaa 匹配项");
}
} catch (e) {
mylog(`网络请求失败: ${e.message}`);
}
return JSON.stringify({ parse: 0, url: "" });
}
export default {
init,
home,
homeVod,
category,
detail,
search,
play
};