Skip to content

Commit 3d546f3

Browse files
committed
fix: fquni: missing volumes
1 parent 644dbdb commit 3d546f3

7 files changed

Lines changed: 378 additions & 342 deletions

File tree

forward.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
const LISTEN_PORT = 7890;
2+
const TARGET_HOST = "cloud.imzlh.top";
3+
const TARGET_PORT = 7890;
4+
5+
const listener = Deno.listen({ port: LISTEN_PORT, transport: "tcp", hostname: '0.0.0.0' });
6+
console.log(`TCP proxy listening on port ${LISTEN_PORT} -> ${TARGET_HOST}:${TARGET_PORT}`);
7+
8+
for await (const clientConn of listener) {
9+
handleConnection(clientConn);
10+
}
11+
12+
async function handleConnection(clientConn: Deno.Conn) {
13+
const clientAddr = clientConn.remoteAddr as Deno.NetAddr;
14+
console.log(`[+] Connection from ${clientAddr.hostname}:${clientAddr.port}`);
15+
16+
let serverConn: Deno.Conn | null = null;
17+
18+
try {
19+
serverConn = await Deno.connect({
20+
hostname: TARGET_HOST,
21+
port: TARGET_PORT,
22+
transport: "tcp"
23+
});
24+
25+
const clientToServer = clientConn.readable.pipeTo(serverConn.writable).catch(() => {});
26+
const serverToClient = serverConn.readable.pipeTo(clientConn.writable).catch(() => {});
27+
28+
await Promise.race([clientToServer, serverToClient]);
29+
} catch (err) {
30+
console.error(`[!] Error: ${err}`);
31+
} finally {
32+
try{ clientConn.close(); }catch{}
33+
try{ serverConn?.close(); }catch{}
34+
console.log(`[-] Disconnected ${clientAddr.hostname}:${clientAddr.port}`);
35+
}
36+
}

fqunisrv.ts

Lines changed: 70 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
#!/usr/bin/env -S deno run --allow-read --allow-write --allow-net
22

3+
import { delay } from "https://deno.land/std@0.224.0/async/delay.ts";
4+
import { escape } from "npm:entities";
5+
36
interface ChapterInfo {
47
itemId: string;
58
title: string;
@@ -66,22 +69,27 @@ async function fetchBatchChapters(
6669

6770
console.log(`获取 ${chapterIds.length} 章内容...`);
6871

69-
const response = await fetch(url, {
70-
method: "POST",
71-
headers: {
72-
"Content-Type": "application/json",
73-
},
74-
body: JSON.stringify({
75-
bookId,
76-
chapterIds,
77-
}),
78-
});
72+
let response
73+
while (true){
74+
response = await fetch(url, {
75+
method: "POST",
76+
headers: {
77+
"Content-Type": "application/json",
78+
},
79+
body: JSON.stringify({
80+
bookId,
81+
chapterIds,
82+
}),
83+
});
84+
85+
if (!response.ok) {
86+
console.error(`获取章节失败: ${response.statusText}`);
87+
await delay(10000 + 1000 * Math.random());
88+
continue;
89+
}
7990

80-
if (!response.ok) {
81-
throw new Error(`获取章节失败: ${response.statusText}`);
91+
return await response.json();
8292
}
83-
84-
return await response.json();
8593
}
8694

8795
function extractCategories(categorySchema: string): string[] {
@@ -109,7 +117,9 @@ async function mergeToTxt(
109117

110118
// 从番茄 API 获取章节目录
111119
const detailData = await fetchDetailJson(bookId);
112-
const chapterList: ChapterInfo[] = detailData.data.chapterListWithVolume[0] || [];
120+
const volumeNameList: string[] = detailData.data.volumeNameList || [];
121+
const volumeChapterLists: ChapterInfo[][] = detailData.data.chapterListWithVolume || [];
122+
const chapterList: ChapterInfo[] = volumeChapterLists.flat();
113123
const totalChapters = chapterList.length;
114124

115125
console.log(` ✓ 书名: ${bookData.bookName}`);
@@ -123,16 +133,17 @@ async function mergeToTxt(
123133

124134
const batchSize = 30;
125135
const batches = Math.ceil(totalChapters / batchSize);
136+
const max_retries = 3;
126137

127-
for (let i = 0; i < batches; i++) {
138+
for (let i = 0; i < batches; i++) for(let j = 0; j < max_retries; j++){
128139
const startIdx = i * batchSize;
129140
const endIdx = Math.min((i + 1) * batchSize, totalChapters);
130141
const batchChapterIds = chapterList.slice(startIdx, endIdx).map(ch => ch.itemId);
131142

132143
const batchData = await fetchBatchChapters(bookId, batchChapterIds, baseUrl);
133144

134145
if (batchData.code !== 0) {
135-
console.warn(` ⚠ 批次 ${i + 1}/${batches} 获取失败: ${batchData.message}`);
146+
console.warn(` ⚠ 批次 ${i + 1}/${batches} 获取失败: ${batchData.message},重试 ${j + 1}/${max_retries}...`);
136147
continue;
137148
}
138149

@@ -151,8 +162,10 @@ async function mergeToTxt(
151162

152163
// 避免请求过快
153164
if (i < batches - 1) {
154-
await new Promise(resolve => setTimeout(resolve, 4000 + Math.random() * 2000));
165+
await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 2000));
155166
}
167+
168+
break;
156169
}
157170

158171
console.log(` ✓ 共获取 ${Object.keys(allChapters).length} 章内容\n`);
@@ -214,24 +227,30 @@ async function mergeToTxt(
214227

215228
txtContent += `\n${"=".repeat(60)}\n\n`;
216229

217-
// 按章节顺序添加内容
230+
// 按卷、章节顺序添加内容
218231
let successCount = 0;
219232
let missingCount = 0;
220233

221-
for (const chapterInfo of chapterList) {
222-
const chapter = allChapters[chapterInfo.itemId];
223-
224-
if (chapter && chapter.txtContent) {
225-
txtContent += `${chapter.chapterName || chapterInfo.title}\n\n`;
226-
txtContent += `${chapter.txtContent}\n\n`;
227-
txtContent += `${"=".repeat(60)}\n\n`;
228-
successCount++;
229-
} else {
230-
txtContent += `${chapterInfo.title}\n\n`;
231-
txtContent += `[章节内容缺失]\n\n`;
232-
txtContent += `${"=".repeat(60)}\n\n`;
233-
missingCount++;
234-
console.warn(` ⚠ 章节内容缺失: ${chapterInfo.title} (ID: ${chapterInfo.itemId})`);
234+
for (let vi = 0; vi < volumeChapterLists.length; vi++) {
235+
const volName = volumeNameList[vi] || `第${vi + 1}卷`;
236+
txtContent += `第${vi + 1}${volName}\n\n`;
237+
238+
const chapters = volumeChapterLists[vi] || [];
239+
for (const chapterInfo of chapters) {
240+
const chapter = allChapters[chapterInfo.itemId];
241+
242+
if (chapter && chapter.txtContent) {
243+
txtContent += `${chapter.chapterName || chapterInfo.title}\n\n`;
244+
txtContent += `${escape(chapter.txtContent)}\n\n`;
245+
txtContent += `${"=".repeat(60)}\n\n`;
246+
successCount++;
247+
} else {
248+
txtContent += `${chapterInfo.title}\n\n`;
249+
txtContent += `[章节内容缺失]\n\n`;
250+
txtContent += `${"=".repeat(60)}\n\n`;
251+
missingCount++;
252+
console.warn(` ⚠ 章节内容缺失: ${chapterInfo.title} (ID: ${chapterInfo.itemId})`);
253+
}
235254
}
236255
}
237256

@@ -273,19 +292,29 @@ if (import.meta.main) {
273292
console.log("");
274293
}
275294

276-
const bookId = args[0] ?? prompt("请输入书籍ID:");
295+
const bookIds = args.slice(0);
277296
const outputPath = args[1];
278297
const baseUrl = args[2] || "http://127.0.0.1:9999";
279298

280-
if (!bookId) {
299+
if (!bookIds.length) {
281300
console.error("\n✗ 错误: 缺少书籍ID");
282-
Deno.exit(1);
301+
console.log('接下来输入小说ID,每行一个,空行结束!');
302+
while (true) {
303+
const line = prompt("小说ID: ")?.trim();
304+
if (!line) {
305+
break;
306+
}
307+
bookIds.push(line);
308+
}
309+
if (!bookIds.length) Deno.exit(1)
283310
}
284311

285-
try {
286-
await mergeToTxt(bookId, outputPath, baseUrl);
287-
} catch (error) {
288-
console.error(`\n✗ 错误: ${error}`);
289-
Deno.exit(1);
312+
for (const bookId of bookIds) {
313+
try {
314+
console.log(`\n正在处理书籍ID: ${bookId}`);
315+
await mergeToTxt(bookId, outputPath, baseUrl);
316+
} catch (error) {
317+
console.error(`\n✗ 错误: ${error}`);
318+
}
290319
}
291320
}

lanzoudl.ts

Lines changed: 56 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -191,11 +191,12 @@ function setCookieEval(jscode: string, site: string) {
191191
}
192192

193193
const getFiles = async function (page: string, parentPath = '', files: LanZouFile[]) {
194-
const doc = await getDocument(page);
194+
const doc = await fetch3(page, {}, 'document') as Document;
195195
const script = doc.getElementsByTagName('script').find(s =>
196196
s.innerHTML.includes('$.ajax')
197197
);
198198
if (!script) {
199+
console.warn(doc.body.innerHTML);
199200
throw new Error('找不到script部分,确保这是蓝奏云分享链接!');
200201
}
201202

@@ -210,10 +211,10 @@ const getFiles = async function (page: string, parentPath = '', files: LanZouFil
210211
let lastNum = 50; // 每页显示50个文件
211212
while (lastNum == 50) {
212213
formData.set('pg', pgnum.toString());
213-
const list = await fetch2(new URL(url, page), {
214+
const list = await fetch3(new URL(url, page), {
214215
method: 'POST',
215216
body: formData
216-
}).then(r => r.json());
217+
}, 'json')
217218
if (list.info != 'sucess') {
218219
if (typeof list.info === 'string' && list.info.includes('重试')) {
219220
console.warn(`获取第 ${pgnum} 页文件列表出现问题:蓝奏云限制!`);
@@ -250,6 +251,54 @@ const getFiles = async function (page: string, parentPath = '', files: LanZouFil
250251
return files;
251252
}
252253

254+
async function fetch3(urlRaw: URL | string, fetchOps?: any, expect: 'document' | 'binary' | 'json' = 'json') {
255+
let textpath = new URL(urlRaw);
256+
let text2 = await fetch2(urlRaw, fetchOps);
257+
while (true) {
258+
let document: Document;
259+
if (expect == 'binary') {
260+
if (text2.headers.get('Content-Type')?.startsWith('text/html'))
261+
document = new DOMParser().parseFromString(await text2.text(), 'text/html');
262+
else return text2;
263+
} else {
264+
const text = await text2.text();
265+
document = new DOMParser().parseFromString(text, 'text/html');
266+
if (document.body.innerText.trim())
267+
if (expect == 'json') return JSON.parse(text);
268+
else return document;
269+
}
270+
const script = document.getElementsByTagName('script').at(-1)!;
271+
272+
// 处理acw_sc__v2
273+
if (script.innerHTML.includes('acw_sc')) {
274+
setCookieEval(script.innerHTML, textpath.href);
275+
text2 = await fetch2(textpath);
276+
continue; // retry
277+
}
278+
279+
const func = extractFunctionByName(script.innerHTML, 'down_r')!;
280+
const { url, data } = sandboxEval(func, 'var el = 2;' + script.innerHTML);
281+
const formData = new FormData();
282+
for (const [key, value] of Object.entries(data)) {
283+
formData.append(key, String(value));
284+
}
285+
await delay(2241 + 1000 * Math.random());
286+
const file2 = await fetch2(new URL(url, textpath), {
287+
body: formData,
288+
method: 'POST',
289+
referrer: textpath.href,
290+
headers: {
291+
Origin: textpath.origin,
292+
"X-Requested-With": "XMLHttpRequest"
293+
}
294+
}).then(r => r.json());
295+
if (file2.zt != 1) throw new Error('验证网络:链接超时');
296+
const urlreal = new URL(file2.url, textpath);
297+
await delay(1000 * Math.random() + 621);
298+
return await fetch2(urlreal);
299+
}
300+
}
301+
253302
async function downloadFile(docurl: string) {
254303
const document1 = await getDocument(docurl);
255304
for (const iframe of document1.getElementsByTagName('iframe')) {
@@ -264,56 +313,22 @@ async function downloadFile(docurl: string) {
264313
formData.append(key, String(value));
265314
}
266315
await delay(143 + 1000 * Math.random());
267-
const file = await fetch2(new URL(url, docurl), {
316+
const file = await fetch3(new URL(url, docurl), {
268317
body: formData,
269318
method: 'POST',
270319
referrer: docurl2.href,
271320
headers: {
272321
Origin: docurl2.origin,
273322
"X-Requested-With": "XMLHttpRequest"
274323
}
275-
}).then(r => r.json());
324+
}, 'json')
276325
if (file.zt != 1) throw new Error('下载 ' + file.name + ' 失败: 链接超时');
277326
const realpath = file.dom + '/file/' + file.url;
278327

279328
await delay(324 + 1000 * Math.random());
280329
const textpath = new URL(realpath, docurl);
281-
let text2 = await fetch2(textpath);
282-
// 网络验证
283-
if (text2.headers.get('Content-Type')?.includes('text/html')) while (true) {
284-
const document = new DOMParser().parseFromString(await text2.text(), 'text/html');
285-
const script = document.getElementsByTagName('script').at(-1)!;
286-
287-
// 处理acw_sc__v2
288-
if (script.innerHTML.includes('acw_sc')) {
289-
setCookieEval(script.innerHTML, textpath.href);
290-
text2 = await fetch2(textpath);
291-
continue; // retry
292-
}
293-
294-
const func = extractFunctionByName(script.innerHTML, 'down_r')!;
295-
const { url, data } = sandboxEval(func, 'var el = 2;' + script.innerHTML);
296-
const formData = new FormData();
297-
for (const [key, value] of Object.entries(data)) {
298-
formData.append(key, String(value));
299-
}
300-
await delay(2241 + 1000 * Math.random());
301-
const file2 = await fetch2(new URL(url, textpath), {
302-
body: formData,
303-
method: 'POST',
304-
referrer: textpath.href,
305-
headers: {
306-
Origin: textpath.origin,
307-
"X-Requested-With": "XMLHttpRequest"
308-
}
309-
}).then(r => r.json());
310-
if (file2.zt != 1) throw new Error('下载 ' + file.name + ' 失败: 验证网络:链接超时');
311-
const urlreal = new URL(file2.url, textpath);
312-
await delay(1000 * Math.random() + 621);
313-
return await fetch2(urlreal);
314-
} else {
315-
return text2;
316-
}
330+
let text2 = await fetch3(textpath, {}, 'binary');
331+
return text2;
317332
}
318333
throw new Error('下载 ' + docurl + ' 失败: 找不到文件下载链接!');
319334
}

lib/www.luoxiawu.com.t.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export default {
2+
title: '#nr_title',
3+
content: '#nr1',
4+
next_link: 'ul > li.next > a'
5+
} satisfies TraditionalConfig;

lib/www.wodeshucheng.net.t.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export default {
2+
title: '#container > div > div > div.reader-main > h1',
3+
content: '#content',
4+
next_link: '#next_url',
5+
} satisfies TraditionalConfig;

0 commit comments

Comments
 (0)