Skip to content

Commit ed5825b

Browse files
committed
update:更新版本
1. alist升级支持账号密码 2. 支持不夜多线程磁盘加速
1 parent e32288d commit ed5825b

File tree

9 files changed

+487
-82
lines changed

9 files changed

+487
-82
lines changed

Diff for: .gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
/nodejs/node_modules/
22
/nodejs/dist/
33
/.idea/
4+
/nodejs/vod_cache

Diff for: README.md

+6
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ yarn build
4343

4444
# 更新记录
4545

46+
### 20250123
47+
48+
1. alist支持带密码目录
49+
2. 兼容不夜筛选
50+
3. 兼容不夜多线程加速
51+
4652
### 20250120
4753

4854
1. 小说自动加标题

Diff for: nodejs/package.json

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"name": "cat_vod_nodejs_fastify",
3-
"version": "1.0.1",
4-
"description": "CatVodOpen nodejs config api server demo.",
3+
"version": "1.0.2",
54
"type": "module",
5+
"main": "src/dev.js",
66
"scripts": {
77
"dev": "cross-env DEV_HTTP_PORT=5758 nodemon --config nodemon.json src/dev.js",
88
"_build": "rimraf dist && node esbuild.js && node esbuild-config.js",
@@ -15,7 +15,13 @@
1515
"build:rollup:config(obsolete)": "cross-env NODE_ENV=production node rollup-config.js",
1616
"build-old:dbg:copy": "rimraf dist && cross-env NODE_ENV=development node esbuild.js && cross-env NODE_ENV=development node esbuild-config.js && node copyDist.js D:\\soft\\猫影视\\Release\\data\\flutter_assets\\asset\\js"
1717
},
18+
"engines": {
19+
"node": ">17 <23"
20+
},
21+
"repository": "https://github.com/hjdhnx/CatPawOpen/tree/ds-cat",
1822
"author": "道长",
23+
"description": "道长修仙,法力无边。一统江湖,只需半年。\n君子袒蛋蛋,小人藏鸡鸡。",
24+
"license": "MIT",
1925
"devDependencies": {
2026
"@babel/plugin-transform-runtime": "^7.23.9",
2127
"@babel/preset-env": "^7.23.9",

Diff for: nodejs/source/ds-cat.20250123-1.7z

504 KB
Binary file not shown.

Diff for: nodejs/src/index.config.js

+21
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,33 @@ export default {
1010
hipy_sniffer_url: 'http://127.0.0.1:5708/sniffer?url=',
1111
parse_count: 6, // 最多显示多少条解析
1212
parse_timeout: 5000, // 解析超时毫秒数
13+
test_thread_proxy: 'http://127.0.0.1:5759/proxy?thread=8&chunkSize=256&url=https://vdse.bdstatic.com//628ca08719cef5987ea2ae3c6f0d2386.mp4',
1314
},
1415
ffm3u8: {
1516
url: 'https://cj.ffzyapi.com/api.php/provide/vod/from/ffm3u8',
1617
categories: ['国产剧', '香港剧', '韩国剧', '欧美剧', '台湾剧', '日本剧', '海外剧', '泰国剧', '短剧', '动作片', '喜剧片', '爱情片', '科幻片', '恐怖片', '剧情片', '战争片', '动漫片', '大陆综艺', '港台综艺', '日韩综艺', '欧美综艺', '国产动漫', '日韩动漫', '欧美动漫', '港台动漫', '海外动漫', '记录片'],
1718
},
1819
alist: [
20+
{
21+
"name": "丫仙女",
22+
"server": "http://localhost:5244",
23+
"startPage": "/",
24+
"showAll": false,
25+
"search": true,
26+
"login": {
27+
"username": "admin",
28+
"password": "admin",
29+
"otp_code": ""
30+
},
31+
"params": {
32+
"/abc": {
33+
"password": "123"
34+
},
35+
"/abc/abc": {
36+
"password": "123"
37+
}
38+
}
39+
},
1940
{
2041
name: '🐉神族九帝',
2142
server: 'https://alist.shenzjd.com',

Diff for: nodejs/src/router.js

+80-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import ffm3u8 from './spider/video/ffm3u8.js';
77
import drpyS from './spider/video/drpyS.js';
88
import {request} from "./util/request.js";
99
import {extractTags} from "./util/tool.js";
10+
import {md5} from "./util/crypto-util.js";
11+
import chunkStream from "./util/chunk.js";
1012

1113
const spiders = [ffm3u8, push, alist, _13bqg, copymanga, drpyS];
1214
const spiderPrefix = '/spider';
@@ -187,10 +189,87 @@ export default async function router(fastify) {
187189
config.parses = dsParses;
188190
config.drpyS_error = drpyS_error;
189191
drpyS.updateDsCache('parses', dsParses);
190-
console.log(JSON.stringify(config));
192+
// console.log(JSON.stringify(config));
193+
console.log(`共计加载了 ${config.video.sites.length} 个视频源,其他源暂不统计,正常加载完毕`);
191194
reply.send(config);
192195
}
193196
);
197+
198+
fastify.all('/proxy', async (request, reply) => {
199+
try {
200+
const {thread, chunkSize, url, header} = request.query;
201+
202+
if (!url) {
203+
reply.code(400).send({error: 'url is required'});
204+
return;
205+
}
206+
207+
// 解码 URL 和 Header
208+
const decodedUrl = decodeURIComponent(url);
209+
const decodedHeader = header ? JSON.parse(decodeURIComponent(header)) : {};
210+
211+
// 获取当前请求头
212+
const currentHeaders = request.headers;
213+
214+
// 解析目标 URL
215+
const targetUrl = new URL(decodedUrl);
216+
217+
// 更新特殊头部
218+
const proxyHeaders = {
219+
...currentHeaders,
220+
...decodedHeader,
221+
host: targetUrl.host, // 确保 Host 对应目标网站
222+
origin: `${targetUrl.protocol}//${targetUrl.host}`, // Origin
223+
referer: targetUrl.href, // Referer
224+
};
225+
226+
// 删除本地无关头部
227+
delete proxyHeaders['content-length']; // 避免因修改内容导致不匹配
228+
delete proxyHeaders['transfer-encoding'];
229+
230+
// 添加缺省值或更新
231+
proxyHeaders['user-agent'] =
232+
proxyHeaders['user-agent'] ||
233+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36';
234+
proxyHeaders['accept'] = proxyHeaders['accept'] || '*/*';
235+
proxyHeaders['accept-language'] = proxyHeaders['accept-language'] || 'en-US,en;q=0.9';
236+
proxyHeaders['accept-encoding'] = proxyHeaders['accept-encoding'] || 'gzip, deflate, br';
237+
238+
239+
// delete proxyHeaders['host'];
240+
// delete proxyHeaders['origin'];
241+
// delete proxyHeaders['referer'];
242+
// delete proxyHeaders['cookie'];
243+
// delete proxyHeaders['accept'];
244+
245+
delete proxyHeaders['sec-fetch-site'];
246+
delete proxyHeaders['sec-fetch-mode'];
247+
delete proxyHeaders['sec-fetch-dest'];
248+
delete proxyHeaders['sec-ch-ua'];
249+
delete proxyHeaders['sec-ch-ua-mobile'];
250+
delete proxyHeaders['sec-ch-ua-platform'];
251+
// delete proxyHeaders['connection'];
252+
// delete proxyHeaders['user-agent'];
253+
delete proxyHeaders['range']; // 必须删除,后面chunkStream会从request取出来的
254+
// console.log(`proxyHeaders:`, proxyHeaders);
255+
256+
// 处理选项
257+
const option = {
258+
chunkSize: chunkSize ? 1024 * parseInt(chunkSize, 10) : 1024 * 256,
259+
poolSize: thread ? parseInt(thread, 10) : 5,
260+
timeout: 1000 * 10, // 默认 10 秒超时
261+
};
262+
263+
// console.log(`option:`, option);
264+
// 计算 urlKey (MD5)
265+
const urlKey = md5(decodedUrl);
266+
267+
// 调用 chunkStream
268+
return await chunkStream(request, reply, decodedUrl, urlKey, proxyHeaders, option);
269+
} catch (err) {
270+
reply.code(500).send({error: err.message});
271+
}
272+
});
194273
}
195274
);
196275
}

Diff for: nodejs/src/spider/pan/alist.js

+103-79
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import req from '../../util/req.js';
2+
import CryptoJS from 'crypto-js';
3+
4+
const suffix = '-https://github.com/alist-org/alist'
25

36
const http = async function (url, options = {}) {
47
if (options.method == 'POST' && options.data) {
@@ -44,6 +47,7 @@ async function get_drives(name) {
4447
settings.enableSearch = false; //v3 没有找到 搜索配置
4548
}
4649
//不同版本 接口不一样
50+
api.login = settings.v3 ? '/api/auth/login/hash' : '/api/auth/login/hash';
4751
api.path = settings.v3 ? '/api/fs/list' : '/api/public/path';
4852
api.file = settings.v3 ? '/api/fs/get' : '/api/public/path';
4953
api.search = settings.v3 ? '/api/public/search' : '/api/public/search';
@@ -55,82 +59,102 @@ async function get_drives(name) {
5559
async function init(inReq, _outResp) {
5660
inReq.server.config.alist.forEach(
5761
(item) =>
58-
(__drives[item.name] = {
59-
name: item.name,
60-
server: item.server.endsWith('/') ? item.server.substring(0, item.server.length - 1) : item.server,
61-
startPage: item.startPage || '/', //首页
62-
showAll: item.showAll === true, //默认只显示 视频和文件夹,如果想显示全部 showAll 设置true
63-
params: item.params || {},
64-
_path_param: item.params
65-
? Object.keys(item.params).sort(function (x, y) {
66-
return y.length - x.length;
67-
})
68-
: [],
69-
settings: {},
70-
api: {},
71-
getParams(path) {
72-
const key = this._path_param.find((x) => path.startsWith(x));
73-
return Object.assign({}, this.params[key], { path });
74-
},
75-
async getPath(path) {
76-
const res = (await http.post(this.server + this.api.path, { data: this.getParams(path) })).json();
77-
return this.settings.v3 ? res.data.content : res.data.files;
78-
},
79-
async getFile(path) {
80-
const res = (await http.post(this.server + this.api.file, { data: this.getParams(path) })).json();
81-
const data = this.settings.v3 ? res.data : res.data.files[0];
82-
if (!this.settings.v3) data.raw_url = data.url; //v2 的url和v3不一样
83-
return data;
84-
},
85-
async getOther(method, path) {
86-
const data = this.getParams(path);
87-
data.method = method;
88-
const res = (await http.post(this.server + this.api.other, { data: data })).json();
89-
return res;
90-
},
91-
isFolder(data) {
92-
return data.type == 1;
93-
},
94-
isVideo(data) {
95-
//判断是否是 视频文件
96-
return this.settings.v3 ? data.type == 2 : data.type == 3;
97-
},
98-
isSubtitle(data) {
99-
if (data.type == 1) return false;
100-
const ext = ['.srt', '.ass', '.scc', '.stl', '.ttml'];
101-
return ext.some((x) => data.name.endsWith(x));
102-
},
103-
getType(data) {
104-
const isVideo = this.isVideo(data);
105-
return this.isFolder(data) ? 0 : isVideo ? 10 : 1;
106-
},
107-
getPic(data) {
108-
let pic = this.settings.v3 ? data.thumb : data.thumbnail;
109-
return pic || (this.isFolder(data) ? 'http://img1.3png.com/281e284a670865a71d91515866552b5f172b.png' : '');
110-
},
111-
getSize(data) {
112-
let sz = data.size || 0;
113-
if (sz <= 0) return '';
114-
let filesize = '';
115-
if (sz > 1024 * 1024 * 1024 * 1024.0) {
116-
sz /= 1024 * 1024 * 1024 * 1024.0;
117-
filesize = 'TB';
118-
} else if (sz > 1024 * 1024 * 1024.0) {
119-
sz /= 1024 * 1024 * 1024.0;
120-
filesize = 'GB';
121-
} else if (sz > 1024 * 1024.0) {
122-
sz /= 1024 * 1024.0;
123-
filesize = 'MB';
124-
} else {
125-
sz /= 1024.0;
126-
filesize = 'KB';
127-
}
128-
return sz.toFixed(2) + filesize;
129-
},
130-
getRemark(_data) {
131-
return '';
132-
},
133-
})
62+
(__drives[item.name] = {
63+
name: item.name,
64+
server: item.server.endsWith('/') ? item.server.substring(0, item.server.length - 1) : item.server,
65+
startPage: item.startPage || '/', //首页
66+
showAll: item.showAll === true, //默认只显示 视频和文件夹,如果想显示全部 showAll 设置true
67+
login: item.login || {},
68+
params: item.params || {},
69+
_path_param: item.params
70+
? Object.keys(item.params).sort(function (x, y) {
71+
return y.length - x.length;
72+
})
73+
: [],
74+
settings: {},
75+
api: {},
76+
getLogin() {
77+
const pass = CryptoJS.SHA256(this.login.password + suffix).toString(CryptoJS.enc.Hex);
78+
const res = {
79+
"username": this.login.username,
80+
"password": pass,
81+
"otp_code": this.login.otp_code
82+
}
83+
return res;
84+
},
85+
getParams(path) {
86+
const key = this._path_param.find((x) => path.startsWith(x));
87+
return Object.assign({}, this.params[key], { path });
88+
},
89+
async getHeaders() {
90+
const res = (await http.post(this.server + this.api.login, { data: this.getLogin() })).json();
91+
return { "Authorization": res.data.token };
92+
},
93+
async getRes(api, path) {
94+
let re;
95+
if (JSON.stringify(this.login) === "{}") re = (await http.post(this.server + api, { data: this.getParams(path) })).json();
96+
else re = (await http.post(this.server + api, { data: this.getParams(path), headers: await this.getHeaders() })).json();
97+
return re;
98+
},
99+
async getPath(path) {
100+
const res = await this.getRes(this.api.path, path);
101+
return this.settings.v3 ? res.data.content : res.data.files;
102+
},
103+
async getFile(path) {
104+
const res = await this.getRes(this.api.file, path);
105+
const data = this.settings.v3 ? res.data : res.data.files[0];
106+
if (!this.settings.v3) data.raw_url = data.url; //v2 的url和v3不一样
107+
return data;
108+
},
109+
async getOther(method, path) {
110+
const data = this.getParams(path);
111+
data.method = method;
112+
const res = await this.getRes(this.api.other, path);
113+
return res;
114+
},
115+
isFolder(data) {
116+
return data.type == 1;
117+
},
118+
isVideo(data) {
119+
//判断是否是 视频文件
120+
return this.settings.v3 ? data.type == 2 : data.type == 3;
121+
},
122+
isSubtitle(data) {
123+
if (data.type == 1) return false;
124+
const ext = ['.srt', '.ass', '.scc', '.stl', '.ttml'];
125+
return ext.some((x) => data.name.endsWith(x));
126+
},
127+
getType(data) {
128+
const isVideo = this.isVideo(data);
129+
return this.isFolder(data) ? 0 : isVideo ? 10 : 1;
130+
},
131+
getPic(data) {
132+
let pic = this.settings.v3 ? data.thumb : data.thumbnail;
133+
return pic || (this.isFolder(data) ? 'http://img1.3png.com/281e284a670865a71d91515866552b5f172b.png' : '');
134+
},
135+
getSize(data) {
136+
let sz = data.size || 0;
137+
if (sz <= 0) return '';
138+
let filesize = '';
139+
if (sz > 1024 * 1024 * 1024 * 1024.0) {
140+
sz /= 1024 * 1024 * 1024 * 1024.0;
141+
filesize = 'TB';
142+
} else if (sz > 1024 * 1024 * 1024.0) {
143+
sz /= 1024 * 1024 * 1024.0;
144+
filesize = 'GB';
145+
} else if (sz > 1024 * 1024.0) {
146+
sz /= 1024 * 1024.0;
147+
filesize = 'MB';
148+
} else {
149+
sz /= 1024.0;
150+
filesize = 'KB';
151+
}
152+
return sz.toFixed(2) + filesize;
153+
},
154+
getRemark(_data) {
155+
return '';
156+
},
157+
})
134158
);
135159
// const deviceKey = inReq.server.prefix + '/device';
136160
// device = await inReq.server.db.getObjectDefault(deviceKey, {});
@@ -210,7 +234,7 @@ async function file(inReq, _outResp) {
210234
let subP = await get_drives_path(sub);
211235
const subItem = await drives.getFile(subP.path);
212236
subs.push(subItem.raw_url);
213-
} catch (error) {}
237+
} catch (error) { }
214238
}
215239
}
216240
if ((item.provider === 'AliyundriveShare2Open' || item.provider == 'AliyundriveOpen') && drives.api.other) {
@@ -223,7 +247,7 @@ async function file(inReq, _outResp) {
223247
urls.push(live.url);
224248
}
225249
}
226-
} catch (error) {}
250+
} catch (error) { }
227251
const result = {
228252
name: item.name,
229253
url: urls,
@@ -239,7 +263,7 @@ async function file(inReq, _outResp) {
239263
let url = item.raw_url;
240264
try {
241265
url = (await http.get(url)).json().data.redirect_url;
242-
} catch (error) {}
266+
} catch (error) { }
243267
const result = {
244268
name: item.name,
245269
url: url,

0 commit comments

Comments
 (0)