From 06149fb48bdfb75bc2486a659e398247b6edddfb Mon Sep 17 00:00:00 2001 From: Saul Hetherman <126445083+YYDS678@users.noreply.github.com> Date: Tue, 16 Jul 2024 21:31:36 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=8C=E7=BA=A7=E5=88=86?= =?UTF-8?q?=E7=B1=BB=E5=8F=8A=E7=AD=9B=E9=80=89=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/README.md | 40 +++++ js/core/uzCode.js | 111 ++++++++++++- js/spider/keke20240712.js | 342 ++++++++++++++++++++++++++++++++++++++ js/spider_sources.json | 7 + 4 files changed, 499 insertions(+), 1 deletion(-) create mode 100644 js/spider/keke20240712.js diff --git a/js/README.md b/js/README.md index 7904ae9..e6f75cd 100644 --- a/js/README.md +++ b/js/README.md @@ -19,10 +19,50 @@ ``` +# 脚本运行说明 + +1. 执行每个方法都会为 `webSite` 进行赋值 +2. 流程图 + +```mermaid + +graph TD + + A[开始] --> A1[uz 调用 getClassList 获取一级分类] -->|返回数据 rep: RepVideoClassList| B[判断 rep.data 列表内 VideoClass 的 hasSubclass 是否为 true] + + B -->|是,存在二级分类或者筛选列表| C[调用 getSubclassList 获取二级分类或筛选列表] + + B -->|否,不存在二级分类| D[调用 getVideoList 获取视频列表] + + + C --> C1[调用 getSubclassVideoList 获取二级分类视频列表或者筛选视频列表] -->|点击单个视频| E + + + + E[调用 getVideoDetail 获取视频详情] + + D -->|点击单个视频| E + + E -->|点击某一集| F[调用 getVideoPlayUrl 获取播放链接] + + F --> G[结束] + + S[搜索] -->S1[调用 searchVideo 返回视频列表] -->|点击单个视频| E + +``` + # 修改记录 +### v1.4.00 + +1. 增加二级分类和筛选列表功能 + ### v1.3.00 1. 去掉 `cat.js`, 更改为 `cheerio` `Crypto` `Encrypt` `parse` 2. `VideoDetail` 新增 `quarkUrl` 支持夸克网盘 3. `RepVideoPlayUrl` 新增 `headers` 支持设置播放 `header` + +``` + +``` diff --git a/js/core/uzCode.js b/js/core/uzCode.js index 6492573..79d1432 100644 --- a/js/core/uzCode.js +++ b/js/core/uzCode.js @@ -1,3 +1,30 @@ +/** + * 筛选标签 + */ +class FilterLabel { + constructor() { + // 筛选名称 + this.name = ""; + // 标识参数 + this.id; + } +} + +/** + * 筛选标题 + */ +class FilterTitle { + constructor() { + // 筛选标题 + this.name = ""; + /** + * 筛选标签列表 + * @type {FilterLabel[]} + */ + this.list = []; + } +} + /** * 视频分类 */ @@ -7,6 +34,30 @@ class VideoClass { this.type_id = ""; // 分类名称 this.type_name = ""; + + /** + * 是否存在 筛选列表、子分类。 存在会调用 getSubclassList + */ + this.hasSubclass = false; + } +} + +/** + * 视频二级分类,二级分类可以是 分类,也可以是筛选,都有值优先取筛选 + */ +class VideoSubclass { + constructor() { + /** + * 子分类 + * @type {VideoClass[]} + */ + this.class = []; + /** + * 筛选列表 + * 请求二级分类列表 getSubclassList 时返回该数据或者 data, + * @type {FilterTitle[]} + */ + this.filter = []; } } @@ -66,6 +117,20 @@ class RepVideoClassList { } } +/** + * 返回二级分类列表(包括筛选列表) + */ +class RepVideoSubclassList { + constructor() { + /** + * 二级分类数据 + * @type {VideoSubclass} + */ + this.data = new VideoSubclass(); + this.error = ""; + } +} + /** * 返回视频列表 */ @@ -74,7 +139,7 @@ class RepVideoList { /** * @type {VideoDetail[]} */ - this.data = null; + this.data = []; this.error = ""; this.total = 0; } @@ -120,10 +185,36 @@ class UZArgs { this.searchWord = ""; } } + +/** + * getSubclassVideoList 方法传入的参数 + */ +class UZSubclassVideoListArgs extends UZArgs { + constructor() { + /** + * 主分类ID 即脚本返回的 @type {RepVideoClassList}.data[0].type_id + */ + this.mainClassId = ""; + + /** + * 二级分类ID 即脚本返回的 @type {RepVideoSubclassList}.data.class.type_id + */ + this.subclassId = ""; + + /** + * 筛选标签,按返回的顺序传入 即脚本返回的 @type {RepVideoSubclassList}.data.filter. + * @type {FilterLabel[]} + */ + this.filter = []; + } +} + /** * 脚本基类 */ class WebApiBase { + // 网站主页 + webSite = ""; /** * 异步获取分类列表的方法。 * @param {UZArgs} args @@ -133,6 +224,15 @@ class WebApiBase { return JSON.stringify(new RepVideoClassList()); } + /** + * 获取二级分类列表筛选列表的方法。 + * @param {UZArgs} args + * @returns {@Promise} + */ + async getSubclassList(args) { + return JSON.stringify(new RepVideoSubclassList()); + } + /** * 获取分类视频列表 * @param {UZArgs} args @@ -141,6 +241,15 @@ class WebApiBase { async getVideoList(args) { return JSON.stringify(new RepVideoList()); } + + /** + * 获取二级分类视频列表 或 筛选视频列表 + * @param {UZSubclassVideoListArgs} args + * @returns {@Promise} + */ + async getSubclassVideoList(args) { + return JSON.stringify(new RepVideoList()); + } /** * 获取视频详情 diff --git a/js/spider/keke20240712.js b/js/spider/keke20240712.js new file mode 100644 index 0000000..065a692 --- /dev/null +++ b/js/spider/keke20240712.js @@ -0,0 +1,342 @@ +// ignore +import { WebApiBase, VideoClass } from "../core/uzCode.js"; +import { parse } from "node-html-parser"; +// ignore + +// 类名要特殊 +class Keke20240712 extends WebApiBase { + webSite = "https://www.keke12.com:51111"; + /** + * 异步获取分类列表的方法。 + * @param {UZArgs} args + * @returns {Promise} + */ + async getClassList(args) { + let webUrl = args.url; + var backData = new RepVideoClassList(); + backData.data = []; + try { + const pro = await req(webUrl); + backData.error = pro.error; + let proData = pro.data; + if (proData) { + var document = parse(proData); + var ulList = document.querySelectorAll("div.main > ul") ?? []; + + if (ulList.length >= 1) { + var li = ulList[1].querySelectorAll("li") ?? []; + for (let i = 0; i < li.length; i++) { + const element = li[i]; + const title = element.querySelector(".menu-item-label").text; + const path = element.querySelector("a").attributes["href"]; + const id = UZUtils.getStrByRegexDefault(/\/(\d+)\.html/, path); + var videoClass = new VideoClass(); + videoClass.hasSubclass = true; + videoClass.type_id = id; + videoClass.type_name = title; + backData.data.push(videoClass); + } + } + } + } catch (error) { + backData.error = "获取分类失败~"; + } + return JSON.stringify(backData); + } + + async getSubclassList(args) { + var backData = new RepVideoSubclassList(); + backData.data = new VideoSubclass(); + const id = args.url; + try { + var url = + UZUtils.removeTrailingSlash(this.webSite) + + "/show/" + + id + + "------1.html"; + const pro = await req(url); + backData.error = pro.error; + let proData = pro.data; + if (proData) { + var document = parse(proData); + var filterTitleList = document.querySelectorAll("div.filter-row") ?? []; + for (let i = 0; i < filterTitleList.length; i++) { + const element = filterTitleList[i]; + const title = element.querySelector(".filter-row-side > strong").text; + const items = element.querySelectorAll(".filter-item") ?? []; + // 2-惊悚-中国香港-英语-2022-1-1 + // 分类-类型-地区-语言-年代-排序-页码 + var filterTitle = new FilterTitle(); + filterTitle.name = title.replace(":", ""); + filterTitle.list = []; + for (let j = 0; j < items.length; j++) { + const item = items[j]; + const name = item.text; + const path = item.attributes["href"] ?? ""; + const regex = /\/show\/(.*?)\.html/; + const match = path.match(regex); + const parsStr = match ? match[1] : null; + if (parsStr) { + const parList = parsStr.split("-"); + const id = parList[i + 1]; + var filterLab = new FilterLabel(); + filterLab.name = name; + filterLab.id = id; + filterTitle.list.push(filterLab); + } + } + + backData.data.filter.push(filterTitle); + } + if (id === 6 || id === "6") { + // 短剧 + if (backData.data.filter.length > 0) { + const list = backData.data.filter[0].list; + var classList = []; + for (let index = 0; index < list.length; index++) { + const element = list[index]; + var subclass = new VideoClass(); + subclass.type_id = element.id; + subclass.type_name = element.name; + classList.push(subclass); + } + backData.data.filter = []; + backData.data.class = classList; + } + } + } + } catch (error) { + backData.error = "获取分类失败~ " + error; + } + return JSON.stringify(backData); + } + /** + * 获取二级分类视频列表 或 筛选视频列表 + * @param {UZSubclassVideoListArgs} args + * @returns {@Promise} + */ + async getSubclassVideoList(args) { + var backData = new RepVideoList(); + backData.data = []; + try { + var pList = [args.mainClassId]; + if (args.filter.length > 0) { + // 筛选 + for (let index = 0; index < args.filter.length; index++) { + const element = args.filter[index]; + pList.push(element.id); + } + } else { + pList.push(args.subclassId); + for (let index = 0; index < 4; index++) { + pList.push(""); + } + } + pList.push(args.page); + var path = pList.join("-"); + const url = + UZUtils.removeTrailingSlash(this.webSite) + "/show/" + path + ".html"; + + const pro = await req(url); + backData.error = pro.error; + let proData = pro.data; + if (proData) { + var document = parse(proData); + var allVideo = document.querySelectorAll("div.module-item") ?? []; + var videos = []; + for (let index = 0; index < allVideo.length; index++) { + const element = allVideo[index]; + var vodUrl = element.querySelector("a")?.attributes["href"] ?? ""; + var avaImg = + document.querySelector("img.user-avatar-img")?.attributes["src"] ?? + ""; + var path = + element.querySelector("img")?.attributes["data-original"] ?? ""; + var vodPic = UZUtils.getHostFromURL(avaImg) + path; + var vodName = element.querySelector("img")?.attributes["title"] ?? ""; + var vod_remarks = + element.querySelector("div.v-item-bottom > span")?.text ?? + element.querySelector("div.v-item-top-left > span")?.text; + if (vodUrl && vodPic && vodName) { + var video = new VideoDetail(); + video.vod_id = vodUrl; + video.vod_pic = vodPic; + video.vod_name = vodName; + video.vod_remarks = vod_remarks; + videos.push(video); + } + } + backData.data = videos; + } + } catch (error) { + backData.error = "获取视频列表失败~ " + error; + } + + return JSON.stringify(backData); + } + + /** + * 获取分类视频列表 + * @param {UZArgs} args + * @returns {Promise} + */ + async getVideoList(args) { + var backData = new RepVideoList(); + return JSON.stringify(backData); + } + + /** + * 获取视频详情 + * @param {UZArgs} args + * @returns {Promise} + */ + async getVideoDetail(args) { + var backData = new RepVideoDetail(); + try { + var webUrl = UZUtils.removeTrailingSlash(this.webSite) + args.url; + let pro = await req(webUrl, null); + backData.error = pro.error; + let proData = pro.data; + if (proData) { + var document = parse(proData); + + var detList = document.querySelectorAll("div.detail-info-row") ?? []; + var vod_year = ""; + var vod_director = ""; + var vod_actor = ""; + + for (let index = 0; index < detList.length; index++) { + const element = detList[index]; + var title = + element.querySelector("div.detail-info-row-side")?.text ?? ""; + var contentE = element.querySelector("div.detail-info-row-main"); + if (title.includes("首映")) { + vod_year = contentE.text ?? ""; + } else if (title.includes("导演")) { + vod_director = contentE?.querySelector("a")?.text ?? ""; + } else if (title.includes("演员")) { + contentE.querySelectorAll("a").forEach((element) => { + vod_actor += element.text + "、"; + }); + } + } + var vod_content = + document.querySelector("div.detail-desc > p").text?.trim() ?? ""; + + var fromListE = + document.querySelectorAll("span.source-item-label") ?? []; + var fromListStr = []; + for (let index = 0; index < fromListE.length; index++) { + fromListStr.push(fromListE[index].text); + } + var vod_play_from = fromListStr.join("$$$"); + + var playListE = document.querySelectorAll("div.episode-list") ?? []; + var vod_play_url = ""; + for (let index = 0; index < playListE.length; index++) { + const epList = playListE[index].querySelectorAll("a") ?? []; + + for (let index = 0; index < epList.length; index++) { + const element = epList[index]; + vod_play_url += element.text; + vod_play_url += "$"; + vod_play_url += element.attributes["href"]; + vod_play_url += "#"; + } + vod_play_url += "$$$"; + } + + let detModel = new VideoDetail(); + detModel.vod_year = vod_year; + detModel.vod_director = vod_director; + detModel.vod_actor = vod_actor; + detModel.vod_content = vod_content; + detModel.vod_play_url = vod_play_url; + detModel.vod_play_from = vod_play_from; + detModel.vod_id = args.url; + backData.data = detModel; + } + } catch (error) { + backData.error = "获取视频详情失败 ~ " + error; + } + + return JSON.stringify(backData); + } + + /** + * 获取视频的播放地址 + * @param {UZArgs} args + * @returns {Promise} + */ + async getVideoPlayUrl(args) { + var backData = new RepVideoPlayUrl(); + backData.error = "获取播放链接失败~"; + try { + const pro = await req( + UZUtils.removeTrailingSlash(this.webSite) + args.url + ); + backData.error = pro.error; + let proData = pro.data; + const regex = /playSource\s*=\s*{[^}]*src:\s*"([^"]*)"/; + const match = proData.match(regex); + const result = match ? match[1] : ""; + backData.data = result; + } catch (error) { + backData.error = "获取视频播放地址失败~ " + error; + } + return JSON.stringify(backData); + } + + /** + * 搜索视频 + * @param {UZArgs} args + * @returns {Promise} + */ + async searchVideo(args) { + var backData = new RepVideoList(); + backData.data = []; + try { + const rep = await req( + UZUtils.removeTrailingSlash(this.webSite) + + "/search?k=" + + args.searchWord + + "&page=" + + args.page + ); + backData.error = rep.error; + let repData = rep.data; + var document = parse(repData); + var count = document.querySelectorAll("span.highlight-text") ?? []; + if (count.length > 1) { + backData.total = count[1].text.trim(); + } + const allVideoE = document.querySelectorAll("a.search-result-item") ?? []; + + for (let index = 0; index < allVideoE.length; index++) { + const element = allVideoE[index]; + var video = new VideoDetail(); + video.vod_id = element.attributes["href"]; + + var avaImg = + document.querySelector("img.user-avatar-img")?.attributes["src"] ?? + ""; + var path = + element.querySelector("img")?.attributes["data-original"] ?? ""; + var vodPic = UZUtils.getHostFromURL(avaImg) + path; + video.vod_pic = vodPic; + + video.vod_name = element.querySelector("div.title")?.text ?? ""; + video.vod_remarks = + element.querySelector("div.search-result-item-header > div")?.text ?? + ""; + + backData.data.push(video); + } + } catch (error) { + backData.error = "搜索视频失败 ~ " + error; + } + return JSON.stringify(backData); + } +} +// json 中 instance 的值,这个名称一定要特殊 +var keke20240712 = new Keke20240712(); diff --git a/js/spider_sources.json b/js/spider_sources.json index bca3313..7613c47 100644 --- a/js/spider_sources.json +++ b/js/spider_sources.json @@ -5,5 +5,12 @@ "instance": "changZhang20240614", "webSite": "https://www.czzy77.com", "remark": "部分视频不会解密,不支持搜索" + }, + { + "name": "可可影视", + "api": "https://mirror.ghproxy.com/https://raw.githubusercontent.com/YYDS678/uzVideo/main/js/spider/keke20240712.js", + "instance": "keke20240712", + "webSite": "https://www.keke12.com:51111", + "remark": "" } ]