更新
This commit is contained in:
142
js/core/uzHome.js
Normal file
142
js/core/uzHome.js
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* @file 首页推荐扩展
|
||||
*/
|
||||
|
||||
//MARK: - 列表展示 UI类型 如需添加更多 UI 类型请联系 https://t.me/uzVideoAppbot
|
||||
const UIType = {
|
||||
/**
|
||||
* 轮播海报
|
||||
*/
|
||||
banner: "banner",
|
||||
|
||||
/**
|
||||
* 横滑小海报
|
||||
*/
|
||||
smallCard: "smallCard",
|
||||
|
||||
/**
|
||||
* 横滑大海报
|
||||
*/
|
||||
largeCard: "largeCard",
|
||||
};
|
||||
|
||||
//MARK: - 单个广告位展示数据
|
||||
/**
|
||||
* UI 展示数据
|
||||
*/
|
||||
class RepAd {
|
||||
constructor() {
|
||||
/**
|
||||
* UI 类型
|
||||
* @type {UIType}
|
||||
*/
|
||||
this.uiType = UIType.smallCard;
|
||||
|
||||
/**
|
||||
* 海报宽高比
|
||||
*/
|
||||
this.ratio = 10.0 / 16.0;
|
||||
|
||||
/**
|
||||
* 标题 为空不展示标题
|
||||
* @type {string}
|
||||
*/
|
||||
this.title = "";
|
||||
|
||||
/**
|
||||
* 展示数据
|
||||
* @type {VideoDetail} 必要字段 vod_name、vod_pic,
|
||||
* 次要 vod_remarks(海报上的小标签),
|
||||
* 后续用于提高匹配度的字段 vod_year vod_director type_name
|
||||
*/
|
||||
this.data = [];
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - tab 列表数据
|
||||
/**
|
||||
* tab列表数据,data 与 filter 二选一
|
||||
* 当有 filter 时展示为筛选列表样式
|
||||
* 当有 data 时展示为普通列表样式
|
||||
*/
|
||||
class RepTabList {
|
||||
constructor() {
|
||||
/**
|
||||
* tab 列表数据
|
||||
* @type {Array <RepAd>}
|
||||
*/
|
||||
this.data = [];
|
||||
|
||||
/**
|
||||
* 筛选列表
|
||||
* @type {FilterTitle[]}
|
||||
*/
|
||||
this.filter = [];
|
||||
|
||||
this.error = "";
|
||||
}
|
||||
}
|
||||
|
||||
class HomeTabModel {
|
||||
constructor() {
|
||||
// 当前分类的链接
|
||||
this.id = "";
|
||||
// 分类名称
|
||||
this.name = "";
|
||||
/**
|
||||
* 是否是筛选列表
|
||||
*/
|
||||
this.isFilter = false;
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - 首页数据
|
||||
/**
|
||||
* 首页数据
|
||||
*/
|
||||
class RepHome {
|
||||
constructor() {
|
||||
/**
|
||||
* 各 tab 页面名称,用于 getTab() 入参
|
||||
* @type {Array <HomeTabModel>}
|
||||
*/
|
||||
this.data = [];
|
||||
this.error = "";
|
||||
}
|
||||
}
|
||||
|
||||
//MARK:- 首页扩展类
|
||||
/**
|
||||
* 首页扩展,固定实例名称为 uzHomeJs,(例如 const uzHomeJs = new UZHomeJS();)
|
||||
*/
|
||||
class UZHome {
|
||||
/**
|
||||
* 获取首页
|
||||
* @returns {Promise<RepHome>}
|
||||
*/
|
||||
async getHome() {
|
||||
let repData = new RepHome();
|
||||
return JSON.stringify(repData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 tab
|
||||
* @param {UZArgs} args 主要参数 args.url args.page
|
||||
* @returns {Promise<RepTabList>}
|
||||
*/
|
||||
async getTab(args) {
|
||||
let repData = new RepTabList();
|
||||
return JSON.stringify(repData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取筛选列表数据
|
||||
* 当 getTab() 返回 RepTabList.filter 时调用
|
||||
* @param {UZSubclassVideoListArgs} args 主要参数 args.mainClassId(即 VideoClass.id) 、 args.filter( 按 getTab() 返回的 filter 顺序传入)
|
||||
* @returns {Promise<RepVideoList>}返回筛选列表
|
||||
*/
|
||||
async getFilterList(args) {
|
||||
let repData = new RepVideoList();
|
||||
return JSON.stringify(repData);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* @file 工具类
|
||||
*/
|
||||
|
||||
class UZUtils {
|
||||
/**
|
||||
* 从链接中获取域名
|
||||
@@ -54,3 +58,71 @@ class UZUtils {
|
||||
sendMessage("debugLog", JSON.stringify([...arguments]));
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - 网络请求返回数据
|
||||
/**
|
||||
* req 返回的数据
|
||||
*/
|
||||
class ProData {
|
||||
constructor() {
|
||||
this.error = "";
|
||||
this.data;
|
||||
/**
|
||||
* @type {object} 响应头
|
||||
*/
|
||||
this.headers;
|
||||
/**
|
||||
* @type {number} 状态码
|
||||
*/
|
||||
this.code;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求响应类型
|
||||
*/
|
||||
const ReqResponseType = {
|
||||
json: "json",
|
||||
arraybuffer: "arraybuffer",
|
||||
bytes: "bytes",
|
||||
plain: "plain",
|
||||
stream: "stream",
|
||||
};
|
||||
|
||||
//MARK: - 网络请求
|
||||
/**
|
||||
* 网络请求
|
||||
* @param {string} url 请求的URL
|
||||
* @param {object} options 请求参数 {headers:{},method:"POST",data:{},responseType:ReqResponseType}
|
||||
* @returns {Promise<ProData>}
|
||||
*/
|
||||
async function req(url, options) {
|
||||
let pro = await sendMessage(
|
||||
"req",
|
||||
JSON.stringify({ url: url, options: options })
|
||||
);
|
||||
return pro;
|
||||
}
|
||||
|
||||
// /**
|
||||
// * 展示 toast
|
||||
// */
|
||||
// function toast() {
|
||||
// sendMessage("toast", JSON.stringify([...arguments]));
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 读取环境变量
|
||||
// */
|
||||
// async function getEnv(key) {
|
||||
// let res = await sendMessage("getEnv", JSON.stringify([key]));
|
||||
// return JSON.parse(res);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 写入环境变量
|
||||
// */
|
||||
// async function setEnv(key, value) {
|
||||
// let res = await sendMessage("setEnv", JSON.stringify([key, value]));
|
||||
// return JSON.parse(res);
|
||||
// }
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
/**
|
||||
* @file 视频源扩展
|
||||
*/
|
||||
|
||||
//MARK: - 筛选标签
|
||||
/**
|
||||
* 筛选标签
|
||||
*/
|
||||
class FilterLabel {
|
||||
constructor() {
|
||||
// 筛选名称
|
||||
/**
|
||||
* 筛选名称
|
||||
*/
|
||||
this.name = "";
|
||||
// 标识参数
|
||||
this.id;
|
||||
/**
|
||||
* 标识值 根据情况赋值
|
||||
*/
|
||||
this.id = "";
|
||||
/**
|
||||
* 标识key 根据情况赋值
|
||||
*/
|
||||
this.key = "";
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - 筛选标题
|
||||
/**
|
||||
* 筛选标题
|
||||
*/
|
||||
@@ -25,6 +39,7 @@ class FilterTitle {
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - 视频分类
|
||||
/**
|
||||
* 视频分类
|
||||
*/
|
||||
@@ -42,6 +57,7 @@ class VideoClass {
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - 视频二级分类
|
||||
/**
|
||||
* 视频二级分类,二级分类可以是 分类,也可以是筛选,都有值优先取筛选
|
||||
*/
|
||||
@@ -61,6 +77,7 @@ class VideoSubclass {
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - 视频详情
|
||||
/**
|
||||
* 视频详情
|
||||
*/
|
||||
@@ -104,6 +121,7 @@ class VideoDetail {
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - 分类列表数据
|
||||
/**
|
||||
* 返回分类列表
|
||||
*/
|
||||
@@ -117,6 +135,7 @@ class RepVideoClassList {
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - 二级分类列表/筛选列表数据
|
||||
/**
|
||||
* 返回二级分类列表(包括筛选列表)
|
||||
*/
|
||||
@@ -131,6 +150,7 @@ class RepVideoSubclassList {
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - 视频列表数据
|
||||
/**
|
||||
* 返回视频列表
|
||||
*/
|
||||
@@ -145,6 +165,7 @@ class RepVideoList {
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - 视频详情数据
|
||||
/**
|
||||
* 返回视频详情
|
||||
*/
|
||||
@@ -158,6 +179,7 @@ class RepVideoDetail {
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - 视频播放地址数据
|
||||
/**
|
||||
* 返回播放地址
|
||||
*/
|
||||
@@ -175,6 +197,7 @@ class RepVideoPlayUrl {
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - 传入参数
|
||||
/**
|
||||
* UZArgs 封装一组参数,用于构建请求URL或进行数据查询。
|
||||
*/
|
||||
@@ -189,6 +212,7 @@ class UZArgs {
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - 二级分类传入参数
|
||||
/**
|
||||
* getSubclassVideoList 方法传入的参数
|
||||
*/
|
||||
@@ -205,13 +229,14 @@ class UZSubclassVideoListArgs extends UZArgs {
|
||||
this.subclassId = "";
|
||||
|
||||
/**
|
||||
* 筛选标签,按返回的顺序传入 即扩展返回的 @type {RepVideoSubclassList}.data.filter.
|
||||
* 筛选标签,按返回的顺序传入 即扩展返回的 {RepVideoSubclassList}.data.filter.
|
||||
* @type {FilterLabel[]}
|
||||
*/
|
||||
this.filter = [];
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - 视频源扩展基类
|
||||
/**
|
||||
* 扩展基类
|
||||
*/
|
||||
@@ -279,35 +304,3 @@ class WebApiBase {
|
||||
return JSON.stringify(new RepVideoList());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* req 返回的数据
|
||||
*/
|
||||
class ProData {
|
||||
constructor() {
|
||||
this.error = "";
|
||||
this.data;
|
||||
/**
|
||||
* @type {object} 响应头
|
||||
*/
|
||||
this.headers;
|
||||
/**
|
||||
* @type {number} 状态码
|
||||
*/
|
||||
this.code;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络请求,也可以使用 fetch
|
||||
* @param {string} url 请求的URL
|
||||
* @param {object} options 请求参数 {headers:{},method:"POST",data:{},responseType:"json"/arraybuffer/bytes/plain/stream}
|
||||
* @returns {Promise<ProData>}
|
||||
*/
|
||||
async function req(url, options) {
|
||||
let pro = await sendMessage(
|
||||
"req",
|
||||
JSON.stringify({ url: url, options: options })
|
||||
);
|
||||
return pro;
|
||||
}
|
||||
116
js/core/verifyServer.js
Normal file
116
js/core/verifyServer.js
Normal file
@@ -0,0 +1,116 @@
|
||||
// TODO: 替换为自己的盐
|
||||
// 服务器接收来自 app 的请求的盐
|
||||
const saltApp2End = "123321";
|
||||
// 服务器发送数据到 app 的盐
|
||||
const saltEnd2App = "123321";
|
||||
|
||||
addEventListener("fetch", (event) => {
|
||||
event.respondWith(handleRequest(event.request));
|
||||
});
|
||||
|
||||
async function handleRequest(request) {
|
||||
let params;
|
||||
|
||||
try {
|
||||
params = await request.json();
|
||||
} catch (error) {
|
||||
return new Response();
|
||||
}
|
||||
|
||||
if (!params || Object.keys(params).length === 0) {
|
||||
return new Response();
|
||||
}
|
||||
|
||||
if (!(await _verifySignFromApp(params))) {
|
||||
return new Response();
|
||||
}
|
||||
|
||||
if (!_verifyTimestamp(params.date)) {
|
||||
return new Response();
|
||||
}
|
||||
|
||||
let responseData = {};
|
||||
let sid = params.sid;
|
||||
//TODO: 判断 sid 是否是允许的用户
|
||||
// 如果使用 cloudflare worker,可以使用 KV 存储 key: sid value:与用户定义的其它标识 例如邮箱
|
||||
if (sid) {
|
||||
responseData.pass = true;
|
||||
} else {
|
||||
responseData.pass = false;
|
||||
}
|
||||
|
||||
const signedResponse = await _signResponse2App(
|
||||
responseData,
|
||||
params.sessionId
|
||||
);
|
||||
|
||||
return new Response(JSON.stringify(signedResponse), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
async function _verifySignFromApp(params) {
|
||||
const { sign, ...otherParams } = params;
|
||||
if (
|
||||
!sign ||
|
||||
!otherParams.random ||
|
||||
!otherParams.date ||
|
||||
!otherParams.sessionId ||
|
||||
!otherParams.sid
|
||||
)
|
||||
return false;
|
||||
|
||||
const sortedParams = Object.entries(otherParams)
|
||||
.sort((a, b) => b[0].localeCompare(a[0]))
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join("&");
|
||||
|
||||
const salt =
|
||||
saltApp2End + otherParams.random.slice(-4) + otherParams.date.slice(-4);
|
||||
const signStr = salt + sortedParams;
|
||||
const calculatedSign = await _keyToSha256(signStr);
|
||||
|
||||
return calculatedSign === sign;
|
||||
}
|
||||
|
||||
async function _signResponse2App(data, sessionId) {
|
||||
const signMap = { ...data };
|
||||
// 64位随机数
|
||||
const random = crypto.randomUUID().slice(-64);
|
||||
const date = Date.now().toString();
|
||||
|
||||
signMap.random = random;
|
||||
signMap.date = date;
|
||||
signMap.sessionId = sessionId;
|
||||
|
||||
const sortedParams = Object.entries(signMap)
|
||||
.sort((a, b) => b[0].localeCompare(a[0]))
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join("&");
|
||||
|
||||
const salt = saltEnd2App + random.slice(-4) + date.slice(-4);
|
||||
const signStr = salt + sortedParams;
|
||||
signMap.sign = await _keyToSha256(signStr);
|
||||
|
||||
return signMap;
|
||||
}
|
||||
|
||||
async function _keyToSha256(input) {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(input);
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
||||
return Array.from(new Uint8Array(hashBuffer))
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
|
||||
// 验证时间戳
|
||||
function _verifyTimestamp(timestamp) {
|
||||
// 时间为了保证任意时区都一致 所以使用格林威治时间
|
||||
const currentTimeString = new Date().toISOString();
|
||||
const currentTime = new Date(currentTimeString).getTime();
|
||||
const requestTime = parseInt(timestamp);
|
||||
const timeDifference = Math.abs(currentTime - requestTime);
|
||||
// 检查时间差是否小于2分钟(120000毫秒)
|
||||
return timeDifference < 120000;
|
||||
}
|
||||
5
js/recommendHome.json
Normal file
5
js/recommendHome.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "豆瓣推荐 影视推荐",
|
||||
"codeID": "f2GRQWVkMifmWtoE",
|
||||
"url": "https://mirror.ghproxy.com/https://raw.githubusercontent.com/YYDS678/uzVideo/main/js/spider/recommendHome.txt"
|
||||
}
|
||||
1
js/spider/recommendHome.txt
Normal file
1
js/spider/recommendHome.txt
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user