wx2tg-puppet 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/Message.js +123 -42
- package/dist/entities/config.entity.d.ts +2 -0
- package/dist/entities/config.entity.js +8 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +26 -3
- package/dist/services/config.service.d.ts +1 -1
- package/dist/services/config.service.js +7 -7
- package/dist/utils/FileChunkHelper.d.ts +30 -0
- package/dist/utils/FileChunkHelper.js +50 -0
- package/package.json +1 -1
package/dist/core/Message.js
CHANGED
|
@@ -59,6 +59,32 @@ const audioUtil_1 = require("../utils/audioUtil");
|
|
|
59
59
|
const path_1 = __importDefault(require("path"));
|
|
60
60
|
const common_2 = require("../action/common");
|
|
61
61
|
const fs = __importStar(require("fs"));
|
|
62
|
+
const FileChunkHelper_1 = require("../utils/FileChunkHelper");
|
|
63
|
+
const MAX_RETRIES = 3;
|
|
64
|
+
const RETRY_DELAY = 1000; // 1 second
|
|
65
|
+
// @ts-ignore - Ignore type errors for the entire function since we're dealing with a mix of old and new axios types
|
|
66
|
+
function retryDownload(downloadFn) {
|
|
67
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
68
|
+
var _a, _b, _c, _d, _e, _f;
|
|
69
|
+
let lastError;
|
|
70
|
+
for (let i = 0; i < MAX_RETRIES; i++) {
|
|
71
|
+
try {
|
|
72
|
+
const response = yield downloadFn();
|
|
73
|
+
if (((_c = (_b = (_a = response === null || response === void 0 ? void 0 : response.data) === null || _a === void 0 ? void 0 : _a.Data) === null || _b === void 0 ? void 0 : _b.BaseResponse) === null || _c === void 0 ? void 0 : _c.ret) === 0) {
|
|
74
|
+
return response;
|
|
75
|
+
}
|
|
76
|
+
lastError = new Error(`Download failed with ret code: ${(_f = (_e = (_d = response === null || response === void 0 ? void 0 : response.data) === null || _d === void 0 ? void 0 : _d.Data) === null || _e === void 0 ? void 0 : _e.BaseResponse) === null || _f === void 0 ? void 0 : _f.ret}`);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
lastError = error;
|
|
80
|
+
}
|
|
81
|
+
if (i < MAX_RETRIES - 1) {
|
|
82
|
+
yield new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
throw new Error(`Download failed after ${MAX_RETRIES} attempts. Last error: ${lastError === null || lastError === void 0 ? void 0 : lastError.message}`);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
62
88
|
/**
|
|
63
89
|
* Message class for handling and interacting with messages in cache.
|
|
64
90
|
*/
|
|
@@ -319,39 +345,117 @@ class Message {
|
|
|
319
345
|
const xml = this.msg.Content;
|
|
320
346
|
const json = Message.getXmlToJson(xml);
|
|
321
347
|
const config = yield config_service_1.configService.get();
|
|
322
|
-
const
|
|
348
|
+
const totalSize = parseInt(json.msg.img.length);
|
|
349
|
+
// Get initial chunk size from first request
|
|
350
|
+
const firstChunk = FileChunkHelper_1.FileChunkHelper.getChunk(totalSize, 0);
|
|
351
|
+
if (!firstChunk)
|
|
352
|
+
return null;
|
|
353
|
+
// @ts-ignore - Using any type for response due to mixed axios versions
|
|
354
|
+
const firstResp = yield retryDownload(() => ToolsApi_1.toolsApi.DownloadImg({
|
|
323
355
|
"CompressType": 0,
|
|
324
|
-
"DataLen":
|
|
356
|
+
"DataLen": totalSize,
|
|
325
357
|
"MsgId": this.msg.MsgId,
|
|
326
|
-
"Section":
|
|
327
|
-
"DataLen": parseInt(json.msg.img.length),
|
|
328
|
-
"StartPos": 0
|
|
329
|
-
},
|
|
358
|
+
"Section": firstChunk,
|
|
330
359
|
"ToWxid": this.msg.ToUserName,
|
|
331
360
|
"Wxid": (config === null || config === void 0 ? void 0 : config.wxid) || ''
|
|
332
|
-
});
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
361
|
+
}));
|
|
362
|
+
const downloadedLength = firstResp.data.Data.data.iLen;
|
|
363
|
+
const chunks = FileChunkHelper_1.FileChunkHelper.calculateChunks(totalSize, downloadedLength);
|
|
364
|
+
let completeBuffer = Buffer.from(firstResp.data.Data.data.buffer, 'base64');
|
|
365
|
+
// Start from second chunk since we already have the first one
|
|
366
|
+
for (let i = 1; i < chunks.length; i++) {
|
|
367
|
+
const chunk = chunks[i];
|
|
368
|
+
// @ts-ignore - Using any type for response due to mixed axios versions
|
|
369
|
+
const resp = yield retryDownload(() => ToolsApi_1.toolsApi.DownloadImg({
|
|
370
|
+
"CompressType": 0,
|
|
371
|
+
"DataLen": totalSize,
|
|
372
|
+
"MsgId": this.msg.MsgId,
|
|
373
|
+
"Section": chunk,
|
|
374
|
+
"ToWxid": this.msg.ToUserName,
|
|
375
|
+
"Wxid": (config === null || config === void 0 ? void 0 : config.wxid) || ''
|
|
376
|
+
}));
|
|
377
|
+
const chunkData = Buffer.from(resp.data.Data.data.buffer, 'base64');
|
|
378
|
+
completeBuffer = Buffer.concat([completeBuffer, chunkData]);
|
|
379
|
+
}
|
|
380
|
+
const fileBox = file_box_1.FileBox.fromBuffer(completeBuffer, `${this.msg.MsgId}.png`);
|
|
336
381
|
return fileBox;
|
|
337
382
|
}
|
|
338
383
|
else if (this.type() === MessageType_1.MessageType.Video) {
|
|
339
384
|
const xml = this.msg.Content;
|
|
340
385
|
const json = Message.getXmlToJson(xml);
|
|
341
386
|
const config = yield config_service_1.configService.get();
|
|
342
|
-
const
|
|
387
|
+
const totalSize = parseInt(json.msg.videomsg.length);
|
|
388
|
+
// Get initial chunk size from first request
|
|
389
|
+
const firstChunk = FileChunkHelper_1.FileChunkHelper.getChunk(totalSize, 0);
|
|
390
|
+
if (!firstChunk)
|
|
391
|
+
return null;
|
|
392
|
+
// @ts-ignore - Using any type for response due to mixed axios versions
|
|
393
|
+
const firstResp = yield retryDownload(() => ToolsApi_1.toolsApi.DownloadVideo({
|
|
343
394
|
"CompressType": 0,
|
|
344
|
-
"DataLen":
|
|
395
|
+
"DataLen": totalSize,
|
|
345
396
|
"MsgId": this.msg.MsgId,
|
|
346
|
-
"Section":
|
|
347
|
-
"DataLen": parseInt(json.msg.videomsg.length),
|
|
348
|
-
"StartPos": 0
|
|
349
|
-
},
|
|
397
|
+
"Section": firstChunk,
|
|
350
398
|
"ToWxid": this.msg.ToUserName,
|
|
351
399
|
"Wxid": (config === null || config === void 0 ? void 0 : config.wxid) || ''
|
|
352
|
-
});
|
|
353
|
-
const
|
|
354
|
-
const
|
|
400
|
+
}));
|
|
401
|
+
const downloadedLength = firstResp.data.Data.data.iLen;
|
|
402
|
+
const chunks = FileChunkHelper_1.FileChunkHelper.calculateChunks(totalSize, downloadedLength);
|
|
403
|
+
let completeBuffer = Buffer.from(firstResp.data.Data.data.buffer, 'base64');
|
|
404
|
+
// Start from second chunk since we already have the first one
|
|
405
|
+
for (let i = 1; i < chunks.length; i++) {
|
|
406
|
+
const chunk = chunks[i];
|
|
407
|
+
// @ts-ignore - Using any type for response due to mixed axios versions
|
|
408
|
+
const resp = yield retryDownload(() => ToolsApi_1.toolsApi.DownloadVideo({
|
|
409
|
+
"CompressType": 0,
|
|
410
|
+
"DataLen": totalSize,
|
|
411
|
+
"MsgId": this.msg.MsgId,
|
|
412
|
+
"Section": chunk,
|
|
413
|
+
"ToWxid": this.msg.ToUserName,
|
|
414
|
+
"Wxid": (config === null || config === void 0 ? void 0 : config.wxid) || ''
|
|
415
|
+
}));
|
|
416
|
+
const chunkData = Buffer.from(resp.data.Data.data.buffer, 'base64');
|
|
417
|
+
completeBuffer = Buffer.concat([completeBuffer, chunkData]);
|
|
418
|
+
}
|
|
419
|
+
const fileBox = file_box_1.FileBox.fromBuffer(completeBuffer, `${this.msg.MsgId}.mp4`);
|
|
420
|
+
return fileBox;
|
|
421
|
+
}
|
|
422
|
+
else if (this.type() === MessageType_1.MessageType.File) {
|
|
423
|
+
const xml = this.msg.Content;
|
|
424
|
+
const json = Message.getXmlToJson(xml);
|
|
425
|
+
const config = yield config_service_1.configService.get();
|
|
426
|
+
const totalSize = parseInt(json.msg.appmsg.appattach.totallen);
|
|
427
|
+
// Get initial chunk size from first request
|
|
428
|
+
const firstChunk = FileChunkHelper_1.FileChunkHelper.getChunk(totalSize, 0);
|
|
429
|
+
if (!firstChunk)
|
|
430
|
+
return null;
|
|
431
|
+
// @ts-ignore - Using any type for response due to mixed axios versions
|
|
432
|
+
const firstResp = yield retryDownload(() => ToolsApi_1.toolsApi.DownloadFile({
|
|
433
|
+
"AppID": json.msg.appmsg.appid,
|
|
434
|
+
"AttachId": json.msg.appmsg.appattach.attachid,
|
|
435
|
+
"DataLen": totalSize,
|
|
436
|
+
"Section": firstChunk,
|
|
437
|
+
"UserName": this.msg.FromUserName,
|
|
438
|
+
"Wxid": (config === null || config === void 0 ? void 0 : config.wxid) || ''
|
|
439
|
+
}));
|
|
440
|
+
const downloadedLength = firstResp.data.Data.data.iLen;
|
|
441
|
+
const chunks = FileChunkHelper_1.FileChunkHelper.calculateChunks(totalSize, downloadedLength);
|
|
442
|
+
let completeBuffer = Buffer.from(firstResp.data.Data.data.buffer, 'base64');
|
|
443
|
+
// Start from second chunk since we already have the first one
|
|
444
|
+
for (let i = 1; i < chunks.length; i++) {
|
|
445
|
+
const chunk = chunks[i];
|
|
446
|
+
// @ts-ignore - Using any type for response due to mixed axios versions
|
|
447
|
+
const resp = yield retryDownload(() => ToolsApi_1.toolsApi.DownloadFile({
|
|
448
|
+
"AppID": json.msg.appmsg.appid,
|
|
449
|
+
"AttachId": json.msg.appmsg.appattach.attachid,
|
|
450
|
+
"DataLen": totalSize,
|
|
451
|
+
"Section": chunk,
|
|
452
|
+
"UserName": this.msg.FromUserName,
|
|
453
|
+
"Wxid": (config === null || config === void 0 ? void 0 : config.wxid) || ''
|
|
454
|
+
}));
|
|
455
|
+
const chunkData = Buffer.from(resp.data.Data.data.buffer, 'base64');
|
|
456
|
+
completeBuffer = Buffer.concat([completeBuffer, chunkData]);
|
|
457
|
+
}
|
|
458
|
+
const fileBox = file_box_1.FileBox.fromBuffer(completeBuffer, json.msg.appmsg.title);
|
|
355
459
|
return fileBox;
|
|
356
460
|
}
|
|
357
461
|
else if (this.type() === MessageType_1.MessageType.Voice) {
|
|
@@ -369,35 +473,12 @@ class Message {
|
|
|
369
473
|
const silkFileBox = file_box_1.FileBox.fromBase64(base64Data, `${this.msg.MsgId}.silk`);
|
|
370
474
|
const silkFilePath = path_1.default.resolve(__dirname, `${this.msg.MsgId}.silk`);
|
|
371
475
|
const mp3FilePath = path_1.default.resolve(__dirname, `${this.msg.MsgId}.mp3`);
|
|
372
|
-
// 保存 Silk 文件到本地
|
|
373
476
|
yield silkFileBox.toFile(silkFilePath);
|
|
374
|
-
// 使用 SilkToMp3Converter 转换为 MP3
|
|
375
477
|
const converter = new audioUtil_1.SilkToMp3Converter();
|
|
376
478
|
yield converter.convertToMp3(silkFilePath, mp3FilePath);
|
|
377
|
-
// 删除 Silk 文件,只保留 MP3
|
|
378
479
|
fs.unlinkSync(silkFilePath);
|
|
379
|
-
// 返回 MP3 文件的 FileBox
|
|
380
480
|
return file_box_1.FileBox.fromFile(mp3FilePath);
|
|
381
481
|
}
|
|
382
|
-
else if (this.type() === MessageType_1.MessageType.File) {
|
|
383
|
-
const xml = this.msg.Content;
|
|
384
|
-
const json = Message.getXmlToJson(xml);
|
|
385
|
-
const config = yield config_service_1.configService.get();
|
|
386
|
-
const resp = yield ToolsApi_1.toolsApi.DownloadFile({
|
|
387
|
-
"AppID": json.msg.appmsg.appid,
|
|
388
|
-
"AttachId": json.msg.appmsg.appattach.attachid,
|
|
389
|
-
"DataLen": json.msg.appmsg.appattach.totallen,
|
|
390
|
-
"Section": {
|
|
391
|
-
"DataLen": json.msg.appmsg.appattach.totallen,
|
|
392
|
-
"StartPos": 0
|
|
393
|
-
},
|
|
394
|
-
"UserName": this.msg.FromUserName,
|
|
395
|
-
"Wxid": (config === null || config === void 0 ? void 0 : config.wxid) || ''
|
|
396
|
-
});
|
|
397
|
-
const base64Data = resp.data.Data.data.buffer;
|
|
398
|
-
const fileBox = file_box_1.FileBox.fromBase64(base64Data, json.msg.appmsg.title);
|
|
399
|
-
return fileBox;
|
|
400
|
-
}
|
|
401
482
|
else if (this.type() === MessageType_1.MessageType.Emoji) {
|
|
402
483
|
const xml = this.msg.Content;
|
|
403
484
|
const json = Message.getXmlToJson(xml);
|
|
@@ -26,6 +26,14 @@ __decorate([
|
|
|
26
26
|
(0, typeorm_1.Column)({ nullable: true }),
|
|
27
27
|
__metadata("design:type", String)
|
|
28
28
|
], Config.prototype, "wxid", void 0);
|
|
29
|
+
__decorate([
|
|
30
|
+
(0, typeorm_1.Column)({ nullable: true }),
|
|
31
|
+
__metadata("design:type", String)
|
|
32
|
+
], Config.prototype, "deviceId", void 0);
|
|
33
|
+
__decorate([
|
|
34
|
+
(0, typeorm_1.Column)({ nullable: true }),
|
|
35
|
+
__metadata("design:type", String)
|
|
36
|
+
], Config.prototype, "DeviceName", void 0);
|
|
29
37
|
exports.Config = Config = __decorate([
|
|
30
38
|
(0, typeorm_1.Entity)()
|
|
31
39
|
], Config);
|
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export declare class WxBot extends EventEmitter {
|
|
|
20
20
|
private interval;
|
|
21
21
|
private heartbeatInterval;
|
|
22
22
|
private msgInterval;
|
|
23
|
+
private recentMsgIds;
|
|
23
24
|
constructor({ baseUrl, databasePath }: WxBotOptions);
|
|
24
25
|
private fetchAndShowQRCode;
|
|
25
26
|
private setupHeartbeat;
|
package/dist/index.js
CHANGED
|
@@ -43,6 +43,7 @@ class WxBot extends events_1.EventEmitter {
|
|
|
43
43
|
this.interval = null;
|
|
44
44
|
this.heartbeatInterval = null;
|
|
45
45
|
this.msgInterval = null;
|
|
46
|
+
this.recentMsgIds = []; // Store recent message IDs
|
|
46
47
|
this.baseUrl = baseUrl;
|
|
47
48
|
this.databasePath = databasePath;
|
|
48
49
|
(0, axiosInstance_1.setBaseUrl)(baseUrl);
|
|
@@ -50,9 +51,10 @@ class WxBot extends events_1.EventEmitter {
|
|
|
50
51
|
fetchAndShowQRCode() {
|
|
51
52
|
return __awaiter(this, void 0, void 0, function* () {
|
|
52
53
|
try {
|
|
54
|
+
const config = yield config_service_1.configService.get();
|
|
53
55
|
const response = yield LoginApi_1.loginApi.GetQR({
|
|
54
|
-
"DeviceID": "",
|
|
55
|
-
"DeviceName": "",
|
|
56
|
+
"DeviceID": (config === null || config === void 0 ? void 0 : config.deviceId) || "",
|
|
57
|
+
"DeviceName": (config === null || config === void 0 ? void 0 : config.DeviceName) || "",
|
|
56
58
|
"OSModel": "",
|
|
57
59
|
"Proxy": {
|
|
58
60
|
"ProxyIp": "",
|
|
@@ -106,7 +108,10 @@ class WxBot extends events_1.EventEmitter {
|
|
|
106
108
|
const statusData = statusResp.data;
|
|
107
109
|
if (statusData.Success) {
|
|
108
110
|
if (statusData.Message === '登陆成功') {
|
|
109
|
-
yield config_service_1.configService.save(
|
|
111
|
+
yield config_service_1.configService.save({
|
|
112
|
+
uuid: this.uuid,
|
|
113
|
+
wxid: statusData.Data.acctSectResp.userName,
|
|
114
|
+
});
|
|
110
115
|
this.loginSuccess(statusData.Data.acctSectResp.userName);
|
|
111
116
|
if (this.interval)
|
|
112
117
|
clearInterval(this.interval);
|
|
@@ -164,6 +169,14 @@ class WxBot extends events_1.EventEmitter {
|
|
|
164
169
|
return;
|
|
165
170
|
}
|
|
166
171
|
console.log('好友列表获取成功,开始更新联系人信息');
|
|
172
|
+
// 设置设备信息
|
|
173
|
+
const cacheInfo = yield LoginApi_1.loginApi.GetCacheInfo(userName);
|
|
174
|
+
if (cacheInfo.data.Message === '成功') {
|
|
175
|
+
yield config_service_1.configService.save({
|
|
176
|
+
deviceId: cacheInfo.data.Data.Deviceid_str,
|
|
177
|
+
DeviceName: cacheInfo.data.Data.DeviceName,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
167
180
|
// 获取详情
|
|
168
181
|
const data = res.data;
|
|
169
182
|
data.Data.ContactUsernameList.forEach((contact) => {
|
|
@@ -191,6 +204,16 @@ class WxBot extends events_1.EventEmitter {
|
|
|
191
204
|
// 更新联系人或者群组信息
|
|
192
205
|
// 新增:触发 message 事件
|
|
193
206
|
for (const msg of data.Data.AddMsgs) {
|
|
207
|
+
// Check for duplicate messages
|
|
208
|
+
const newMsgId = BigInt(msg.NewMsgId);
|
|
209
|
+
if (this.recentMsgIds.includes(newMsgId)) {
|
|
210
|
+
continue; // Skip duplicate message
|
|
211
|
+
}
|
|
212
|
+
// Add to recent messages and maintain only last 10
|
|
213
|
+
this.recentMsgIds.push(newMsgId);
|
|
214
|
+
if (this.recentMsgIds.length > 10) {
|
|
215
|
+
this.recentMsgIds.shift(); // Remove oldest message ID
|
|
216
|
+
}
|
|
194
217
|
// 将msg转换为Message类型
|
|
195
218
|
const message = new Message_1.Message({
|
|
196
219
|
MsgId: msg.MsgId,
|
|
@@ -2,7 +2,7 @@ import { Config } from '../entities/config.entity';
|
|
|
2
2
|
declare class ConfigService {
|
|
3
3
|
private get repo();
|
|
4
4
|
get(): Promise<Config | null>;
|
|
5
|
-
save(
|
|
5
|
+
save(config: Partial<Config>): Promise<Config>;
|
|
6
6
|
}
|
|
7
7
|
export declare const configService: ConfigService;
|
|
8
8
|
export {};
|
|
@@ -21,17 +21,17 @@ class ConfigService {
|
|
|
21
21
|
return this.repo.findOne({ where: {} });
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
|
-
save(
|
|
24
|
+
save(config) {
|
|
25
25
|
return __awaiter(this, void 0, void 0, function* () {
|
|
26
|
-
let
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
let existingConfig = yield this.get();
|
|
27
|
+
if (existingConfig) {
|
|
28
|
+
Object.assign(existingConfig, config);
|
|
29
|
+
return this.repo.save(existingConfig);
|
|
30
30
|
}
|
|
31
31
|
else {
|
|
32
|
-
|
|
32
|
+
const newConfig = this.repo.create(config);
|
|
33
|
+
return this.repo.save(newConfig);
|
|
33
34
|
}
|
|
34
|
-
return this.repo.save(config);
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface FileChunk {
|
|
2
|
+
DataLen: number;
|
|
3
|
+
StartPos: number;
|
|
4
|
+
}
|
|
5
|
+
export declare class FileChunkHelper {
|
|
6
|
+
private static readonly INITIAL_CHUNK_SIZE;
|
|
7
|
+
private static lastDownloadedLength;
|
|
8
|
+
/**
|
|
9
|
+
* Calculate file chunks based on total file size
|
|
10
|
+
* @param totalSize Total file size in bytes
|
|
11
|
+
* @param downloadedLength Optional length from previous download (iLen)
|
|
12
|
+
* @returns Array of chunks with DataLen and StartPos
|
|
13
|
+
*/
|
|
14
|
+
static calculateChunks(totalSize: number, downloadedLength?: number): FileChunk[];
|
|
15
|
+
/**
|
|
16
|
+
* Get a specific chunk information
|
|
17
|
+
* @param totalSize Total file size in bytes
|
|
18
|
+
* @param chunkIndex Chunk index (0-based)
|
|
19
|
+
* @param downloadedLength Optional length from previous download (iLen)
|
|
20
|
+
* @returns Chunk information or null if index is out of range
|
|
21
|
+
*/
|
|
22
|
+
static getChunk(totalSize: number, chunkIndex: number, downloadedLength?: number): FileChunk | null;
|
|
23
|
+
/**
|
|
24
|
+
* Get total number of chunks needed for a file
|
|
25
|
+
* @param totalSize Total file size in bytes
|
|
26
|
+
* @param downloadedLength Optional length from previous download (iLen)
|
|
27
|
+
* @returns Total number of chunks
|
|
28
|
+
*/
|
|
29
|
+
static getTotalChunks(totalSize: number, downloadedLength?: number): number;
|
|
30
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FileChunkHelper = void 0;
|
|
4
|
+
class FileChunkHelper {
|
|
5
|
+
/**
|
|
6
|
+
* Calculate file chunks based on total file size
|
|
7
|
+
* @param totalSize Total file size in bytes
|
|
8
|
+
* @param downloadedLength Optional length from previous download (iLen)
|
|
9
|
+
* @returns Array of chunks with DataLen and StartPos
|
|
10
|
+
*/
|
|
11
|
+
static calculateChunks(totalSize, downloadedLength) {
|
|
12
|
+
const chunks = [];
|
|
13
|
+
let position = 0;
|
|
14
|
+
// Use consistent chunk size throughout
|
|
15
|
+
const chunkSize = downloadedLength || this.INITIAL_CHUNK_SIZE;
|
|
16
|
+
while (position < totalSize) {
|
|
17
|
+
const remainingSize = totalSize - position;
|
|
18
|
+
const currentChunkSize = Math.min(chunkSize, remainingSize);
|
|
19
|
+
chunks.push({
|
|
20
|
+
DataLen: currentChunkSize,
|
|
21
|
+
StartPos: position
|
|
22
|
+
});
|
|
23
|
+
position += currentChunkSize;
|
|
24
|
+
}
|
|
25
|
+
return chunks;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Get a specific chunk information
|
|
29
|
+
* @param totalSize Total file size in bytes
|
|
30
|
+
* @param chunkIndex Chunk index (0-based)
|
|
31
|
+
* @param downloadedLength Optional length from previous download (iLen)
|
|
32
|
+
* @returns Chunk information or null if index is out of range
|
|
33
|
+
*/
|
|
34
|
+
static getChunk(totalSize, chunkIndex, downloadedLength) {
|
|
35
|
+
const chunks = this.calculateChunks(totalSize, downloadedLength);
|
|
36
|
+
return chunks[chunkIndex] || null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Get total number of chunks needed for a file
|
|
40
|
+
* @param totalSize Total file size in bytes
|
|
41
|
+
* @param downloadedLength Optional length from previous download (iLen)
|
|
42
|
+
* @returns Total number of chunks
|
|
43
|
+
*/
|
|
44
|
+
static getTotalChunks(totalSize, downloadedLength) {
|
|
45
|
+
return this.calculateChunks(totalSize, downloadedLength).length;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
exports.FileChunkHelper = FileChunkHelper;
|
|
49
|
+
FileChunkHelper.INITIAL_CHUNK_SIZE = 1024; // Initial chunk size for first request
|
|
50
|
+
FileChunkHelper.lastDownloadedLength = null; // Store the last downloaded length
|