coomer-downloader 3.2.0 → 3.3.2

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/index.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env -S node --no-warnings=ExperimentalWarning
2
2
 
3
3
  // src/index.ts
4
4
  import process2 from "node:process";
@@ -7,7 +7,7 @@ import process2 from "node:process";
7
7
  import * as cheerio from "cheerio";
8
8
  import { fetch } from "undici";
9
9
 
10
- // src/utils/file.ts
10
+ // src/services/file.ts
11
11
  import os from "node:os";
12
12
  import path from "node:path";
13
13
 
@@ -36,9 +36,24 @@ function filterString(text, include, exclude) {
36
36
  return includesAllWords(text, parseQuery(include)) && includesNoWords(text, parseQuery(exclude));
37
37
  }
38
38
 
39
- // src/utils/file.ts
39
+ // src/utils/io.ts
40
+ import fs from "node:fs";
41
+ async function getFileSize(filepath) {
42
+ let size = 0;
43
+ if (fs.existsSync(filepath)) {
44
+ size = (await fs.promises.stat(filepath)).size || 0;
45
+ }
46
+ return size;
47
+ }
48
+ function mkdir(filepath) {
49
+ if (!fs.existsSync(filepath)) {
50
+ fs.mkdirSync(filepath, { recursive: true });
51
+ }
52
+ }
53
+
54
+ // src/services/file.ts
40
55
  var CoomerFile = class _CoomerFile {
41
- constructor(name, url, filepath, size, downloaded, content) {
56
+ constructor(name, url, filepath, size, downloaded = 0, content) {
42
57
  this.name = name;
43
58
  this.url = url;
44
59
  this.filepath = filepath;
@@ -46,7 +61,11 @@ var CoomerFile = class _CoomerFile {
46
61
  this.downloaded = downloaded;
47
62
  this.content = content;
48
63
  }
49
- state = "pause";
64
+ active = false;
65
+ async getDownloadedSize() {
66
+ this.downloaded = await getFileSize(this.filepath);
67
+ return this;
68
+ }
50
69
  get textContent() {
51
70
  const text = `${this.name || ""} ${this.content || ""}`.toLowerCase();
52
71
  return text;
@@ -87,6 +106,17 @@ var CoomerFileList = class {
87
106
  this.files = this.files.slice(n);
88
107
  return this;
89
108
  }
109
+ async calculateFileSizes() {
110
+ for (const file of this.files) {
111
+ await file.getDownloadedSize();
112
+ }
113
+ }
114
+ getActiveFiles() {
115
+ return this.files.filter((f) => f.active);
116
+ }
117
+ getDownloaded() {
118
+ return this.files.filter((f) => f.size && f.size <= f.downloaded);
119
+ }
90
120
  };
91
121
 
92
122
  // src/api/bunkr.ts
@@ -139,67 +169,6 @@ async function getBunkrData(url) {
139
169
  return filelist;
140
170
  }
141
171
 
142
- // src/utils/downloader.ts
143
- import fs2 from "node:fs";
144
- import { Readable, Transform } from "node:stream";
145
- import { pipeline } from "node:stream/promises";
146
- import { Subject } from "rxjs";
147
-
148
- // src/utils/io.ts
149
- import fs from "node:fs";
150
- async function getFileSize(filepath) {
151
- let size = 0;
152
- if (fs.existsSync(filepath)) {
153
- size = (await fs.promises.stat(filepath)).size || 0;
154
- }
155
- return size;
156
- }
157
- function mkdir(filepath) {
158
- if (!fs.existsSync(filepath)) {
159
- fs.mkdirSync(filepath, { recursive: true });
160
- }
161
- }
162
-
163
- // src/utils/promise.ts
164
- async function sleep(time) {
165
- return new Promise((resolve) => setTimeout(resolve, time));
166
- }
167
- var PromiseRetry = class _PromiseRetry {
168
- retries;
169
- delay;
170
- callback;
171
- constructor(options) {
172
- this.retries = options.retries || 3;
173
- this.delay = options.delay || 1e3;
174
- this.callback = options.callback;
175
- }
176
- async execute(fn) {
177
- let retries = this.retries;
178
- while (true) {
179
- try {
180
- return await fn();
181
- } catch (error) {
182
- if (retries <= 0) {
183
- throw error;
184
- }
185
- if (this.callback) {
186
- const res = this.callback(retries, error);
187
- if (res) {
188
- const { newRetries } = res;
189
- if (newRetries === 0) throw error;
190
- this.retries = newRetries || retries;
191
- }
192
- }
193
- await sleep(this.delay);
194
- retries--;
195
- }
196
- }
197
- }
198
- static create(options) {
199
- return new _PromiseRetry(options);
200
- }
201
- };
202
-
203
172
  // src/utils/requests.ts
204
173
  import { CookieAgent } from "http-cookie-agent/undici";
205
174
  import { CookieJar } from "tough-cookie";
@@ -223,190 +192,10 @@ function fetchWithGlobalHeader(url) {
223
192
  const requestHeaders = new Headers(HeadersDefault);
224
193
  return fetch2(url, { headers: requestHeaders });
225
194
  }
226
- function fetchByteRange(url, downloadedSize) {
195
+ function fetchByteRange(url, downloadedSize, signal) {
227
196
  const requestHeaders = new Headers(HeadersDefault);
228
197
  requestHeaders.set("Range", `bytes=${downloadedSize}-`);
229
- return fetch2(url, { headers: requestHeaders });
230
- }
231
-
232
- // src/utils/timer.ts
233
- var Timer = class _Timer {
234
- constructor(timeout = 1e4, timeoutCallback) {
235
- this.timeout = timeout;
236
- this.timeoutCallback = timeoutCallback;
237
- this.timeout = timeout;
238
- }
239
- timer = void 0;
240
- start() {
241
- this.timer = setTimeout(() => {
242
- this.stop();
243
- this.timeoutCallback();
244
- }, this.timeout);
245
- return this;
246
- }
247
- stop() {
248
- if (this.timer) {
249
- clearTimeout(this.timer);
250
- this.timer = void 0;
251
- }
252
- return this;
253
- }
254
- reset() {
255
- this.stop();
256
- this.start();
257
- return this;
258
- }
259
- static withSignal(timeout, message) {
260
- const controller = new AbortController();
261
- const callback = () => {
262
- controller.abort(message);
263
- };
264
- const timer = new _Timer(timeout, callback).start();
265
- return {
266
- timer,
267
- signal: controller.signal
268
- };
269
- }
270
- };
271
-
272
- // src/utils/downloader.ts
273
- var Downloader = class {
274
- constructor(chunkTimeout = 3e4, chunkFetchRetries = 5, fetchRetries = 7) {
275
- this.chunkTimeout = chunkTimeout;
276
- this.chunkFetchRetries = chunkFetchRetries;
277
- this.fetchRetries = fetchRetries;
278
- }
279
- subject = new Subject();
280
- async fetchStream(file, stream) {
281
- const { subject, chunkTimeout } = this;
282
- const { timer, signal } = Timer.withSignal(chunkTimeout, "chunkTimeout");
283
- const fileStream = fs2.createWriteStream(file.filepath, { flags: "a" });
284
- const progressStream = new Transform({
285
- transform(chunk, _encoding, callback) {
286
- this.push(chunk);
287
- file.downloaded += chunk.length;
288
- timer.reset();
289
- subject.next({ type: "CHUNK_DOWNLOADING_UPDATE", file });
290
- callback();
291
- }
292
- });
293
- try {
294
- subject.next({ type: "CHUNK_DOWNLOADING_START", file });
295
- await pipeline(stream, progressStream, fileStream, { signal });
296
- } catch (error) {
297
- console.error(error.name === "AbortError" ? signal.reason : error);
298
- } finally {
299
- subject.next({ type: "CHUNK_DOWNLOADING_END", file });
300
- }
301
- }
302
- async downloadFile(file) {
303
- file.downloaded = await getFileSize(file.filepath);
304
- const response = await fetchByteRange(file.url, file.downloaded);
305
- if (!response?.ok && response?.status !== 416) {
306
- throw new Error(`HTTP error! status: ${response?.status}`);
307
- }
308
- const contentLength = response.headers.get("Content-Length");
309
- if (!contentLength && file.downloaded > 0) return;
310
- const restFileSize = parseInt(contentLength);
311
- file.size = restFileSize + file.downloaded;
312
- if (file.size > file.downloaded && response.body) {
313
- const stream = Readable.fromWeb(response.body);
314
- const sizeOld = file.downloaded;
315
- await PromiseRetry.create({
316
- retries: this.chunkFetchRetries,
317
- callback: () => {
318
- if (sizeOld !== file.downloaded) {
319
- return { newRetries: 5 };
320
- }
321
- }
322
- }).execute(async () => await this.fetchStream(file, stream));
323
- }
324
- this.subject.next({ type: "FILE_DOWNLOADING_END" });
325
- }
326
- async downloadFiles(filelist) {
327
- mkdir(filelist.dirPath);
328
- this.subject.next({ type: "FILES_DOWNLOADING_START", filesCount: filelist.files.length });
329
- for (const file of filelist.files) {
330
- this.subject.next({ type: "FILE_DOWNLOADING_START" });
331
- await PromiseRetry.create({
332
- retries: this.fetchRetries,
333
- callback: (retries) => {
334
- if (/coomer|kemono/.test(file.url)) {
335
- file.url = tryFixCoomerUrl(file.url, retries);
336
- }
337
- }
338
- }).execute(async () => await this.downloadFile(file));
339
- this.subject.next({ type: "FILE_DOWNLOADING_END" });
340
- }
341
- this.subject.next({ type: "FILES_DOWNLOADING_END" });
342
- }
343
- };
344
-
345
- // src/utils/multibar.ts
346
- import { MultiBar } from "cli-progress";
347
-
348
- // src/utils/strings.ts
349
- function b2mb(bytes) {
350
- return Number.parseFloat((bytes / 1048576).toFixed(2));
351
- }
352
- function formatNameStdout(pathname) {
353
- const name = pathname.split("/").pop() || "";
354
- const consoleWidth = process.stdout.columns;
355
- const width = Math.max(consoleWidth / 2 | 0, 40);
356
- if (name.length < width) return name.trim();
357
- const result = `${name.slice(0, width - 15)} ... ${name.slice(-10)}`.replace(/ +/g, " ");
358
- return result;
359
- }
360
-
361
- // src/utils/multibar.ts
362
- var config = {
363
- clearOnComplete: true,
364
- gracefulExit: true,
365
- autopadding: true,
366
- hideCursor: true,
367
- format: "{percentage}% | {filename} | {value}/{total}{size}"
368
- };
369
- function createMultibar(downloader) {
370
- const multibar = new MultiBar(config);
371
- let bar;
372
- let minibar;
373
- let filename;
374
- let index = 0;
375
- downloader.subject.subscribe({
376
- next: ({ type, filesCount, file }) => {
377
- switch (type) {
378
- case "FILES_DOWNLOADING_START":
379
- bar?.stop();
380
- bar = multibar.create(filesCount, 0);
381
- break;
382
- case "FILES_DOWNLOADING_END":
383
- bar?.stop();
384
- break;
385
- case "FILE_DOWNLOADING_START":
386
- bar?.update(++index, { filename: "Downloaded files", size: "" });
387
- break;
388
- case "FILE_DOWNLOADING_END":
389
- multibar.remove(minibar);
390
- break;
391
- case "CHUNK_DOWNLOADING_START":
392
- multibar?.remove(minibar);
393
- filename = formatNameStdout(file?.filepath);
394
- minibar = multibar.create(b2mb(file?.size), b2mb(file?.downloaded));
395
- break;
396
- case "CHUNK_DOWNLOADING_UPDATE":
397
- minibar?.update(b2mb(file?.downloaded), {
398
- filename,
399
- size: "mb"
400
- });
401
- break;
402
- case "CHUNK_DOWNLOADING_END":
403
- multibar?.remove(minibar);
404
- break;
405
- default:
406
- break;
407
- }
408
- }
409
- });
198
+ return fetch2(url, { headers: requestHeaders, signal });
410
199
  }
411
200
 
412
201
  // src/api/coomer-api.ts
@@ -596,7 +385,7 @@ async function apiHandler(url_) {
596
385
  throw Error("Invalid URL");
597
386
  }
598
387
 
599
- // src/args-handler.ts
388
+ // src/cli/args-handler.ts
600
389
  import yargs from "yargs";
601
390
  import { hideBin } from "yargs/helpers";
602
391
  function argumentHander() {
@@ -629,27 +418,361 @@ function argumentHander() {
629
418
  }).help().alias("help", "h").parseSync();
630
419
  }
631
420
 
421
+ // src/cli/ui/index.tsx
422
+ import { render } from "ink";
423
+ import React9 from "react";
424
+
425
+ // src/cli/ui/app.tsx
426
+ import { Box as Box7 } from "ink";
427
+ import React8 from "react";
428
+
429
+ // src/cli/ui/components/file.tsx
430
+ import { Box as Box2, Spacer, Text as Text2 } from "ink";
431
+ import React3 from "react";
432
+
433
+ // src/utils/strings.ts
434
+ function b2mb(bytes) {
435
+ return (bytes / 1048576).toFixed(2);
436
+ }
437
+
438
+ // src/cli/ui/components/preview.tsx
439
+ import { Box } from "ink";
440
+ import Image, { TerminalInfoProvider } from "ink-picture";
441
+ import React from "react";
442
+
443
+ // src/cli/ui/store/index.ts
444
+ import { create } from "zustand";
445
+ var useInkStore = create((set) => ({
446
+ preview: false,
447
+ switchPreview: () => set((state) => ({
448
+ preview: !state.preview
449
+ })),
450
+ downloader: void 0,
451
+ setDownloader: (downloader) => set({ downloader })
452
+ }));
453
+
454
+ // src/cli/ui/components/preview.tsx
455
+ function Preview({ file }) {
456
+ const previewEnabled = useInkStore((state) => state.preview);
457
+ const bigEnough = file.downloaded > 50 * 1024;
458
+ const shouldShow = previewEnabled && bigEnough && isImage(file.filepath);
459
+ const imgInfo = `
460
+ can't read partial images yet...
461
+ actual size: ${file.size}}
462
+ downloaded: ${file.downloaded}}
463
+ `;
464
+ return shouldShow && /* @__PURE__ */ React.createElement(Box, { paddingX: 1 }, /* @__PURE__ */ React.createElement(TerminalInfoProvider, null, /* @__PURE__ */ React.createElement(Box, { width: 30, height: 15 }, /* @__PURE__ */ React.createElement(Image, { src: file.filepath, alt: imgInfo }))));
465
+ }
466
+
467
+ // src/cli/ui/components/spinner.tsx
468
+ import spinners from "cli-spinners";
469
+ import { Text } from "ink";
470
+ import React2, { useEffect, useState } from "react";
471
+ function Spinner({ type = "dots" }) {
472
+ const spinner = spinners[type];
473
+ const randomFrame = spinner.frames.length * Math.random() | 0;
474
+ const [frame, setFrame] = useState(randomFrame);
475
+ useEffect(() => {
476
+ const timer = setInterval(() => {
477
+ setFrame((previousFrame) => {
478
+ return (previousFrame + 1) % spinner.frames.length;
479
+ });
480
+ }, spinner.interval);
481
+ return () => {
482
+ clearInterval(timer);
483
+ };
484
+ }, [spinner]);
485
+ return /* @__PURE__ */ React2.createElement(Text, null, spinner.frames[frame]);
486
+ }
487
+
488
+ // src/cli/ui/components/file.tsx
489
+ function FileBox({ file }) {
490
+ const percentage = Number(file.downloaded / file.size * 100).toFixed(2);
491
+ return /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement(
492
+ Box2,
493
+ {
494
+ borderStyle: "single",
495
+ borderColor: "magentaBright",
496
+ borderDimColor: true,
497
+ paddingX: 1,
498
+ flexDirection: "column"
499
+ },
500
+ /* @__PURE__ */ React3.createElement(Box2, null, /* @__PURE__ */ React3.createElement(Text2, { color: "blue", dimColor: true, wrap: "truncate-middle" }, file.name)),
501
+ /* @__PURE__ */ React3.createElement(Box2, { flexDirection: "row-reverse" }, /* @__PURE__ */ React3.createElement(Text2, { color: "cyan", dimColor: true }, b2mb(file.downloaded), "/", file.size ? b2mb(file.size) : "\u221E", " MB"), /* @__PURE__ */ React3.createElement(Text2, { color: "redBright", dimColor: true }, file.size ? ` ${percentage}% ` : ""), /* @__PURE__ */ React3.createElement(Spacer, null), /* @__PURE__ */ React3.createElement(Text2, { color: "green", dimColor: true }, /* @__PURE__ */ React3.createElement(Spinner, null)))
502
+ ), /* @__PURE__ */ React3.createElement(Preview, { file }));
503
+ }
504
+
505
+ // src/cli/ui/components/filelist.tsx
506
+ import { Box as Box3, Text as Text3 } from "ink";
507
+ import React4 from "react";
508
+ function FileListStateBox({ filelist }) {
509
+ return /* @__PURE__ */ React4.createElement(
510
+ Box3,
511
+ {
512
+ paddingX: 1,
513
+ flexDirection: "column",
514
+ borderStyle: "single",
515
+ borderColor: "magenta",
516
+ borderDimColor: true
517
+ },
518
+ /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Box3, { marginRight: 1 }, /* @__PURE__ */ React4.createElement(Text3, { color: "cyanBright", dimColor: true }, "Found:")), /* @__PURE__ */ React4.createElement(Text3, { color: "blue", dimColor: true, wrap: "wrap" }, filelist.files.length)),
519
+ /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Box3, { marginRight: 1 }, /* @__PURE__ */ React4.createElement(Text3, { color: "cyanBright", dimColor: true }, "Downloaded:")), /* @__PURE__ */ React4.createElement(Text3, { color: "blue", dimColor: true, wrap: "wrap" }, filelist.getDownloaded().length)),
520
+ /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Box3, { width: 9 }, /* @__PURE__ */ React4.createElement(Text3, { color: "cyanBright", dimColor: true }, "Folder:")), /* @__PURE__ */ React4.createElement(Box3, { flexGrow: 1 }, /* @__PURE__ */ React4.createElement(Text3, { color: "blue", dimColor: true, wrap: "truncate-middle" }, filelist.dirPath)))
521
+ );
522
+ }
523
+
524
+ // src/cli/ui/components/keyboardinfo.tsx
525
+ import { Box as Box4, Text as Text4 } from "ink";
526
+ import React5 from "react";
527
+ var info = {
528
+ "s ": "skip current file",
529
+ p: "on/off image preview"
530
+ };
531
+ function KeyboardControlsInfo() {
532
+ const infoRender = Object.entries(info).map(([key, value]) => {
533
+ return /* @__PURE__ */ React5.createElement(Box4, { key }, /* @__PURE__ */ React5.createElement(Box4, { marginRight: 2 }, /* @__PURE__ */ React5.createElement(Text4, { color: "red", dimColor: true, bold: true }, key)), /* @__PURE__ */ React5.createElement(Text4, { dimColor: true, bold: false }, value));
534
+ });
535
+ return /* @__PURE__ */ React5.createElement(
536
+ Box4,
537
+ {
538
+ flexDirection: "column",
539
+ paddingX: 1,
540
+ borderStyle: "single",
541
+ borderColor: "gray",
542
+ borderDimColor: true
543
+ },
544
+ /* @__PURE__ */ React5.createElement(Box4, null, /* @__PURE__ */ React5.createElement(Text4, { color: "red", dimColor: true, bold: true }, "Keyboard controls:")),
545
+ infoRender
546
+ );
547
+ }
548
+
549
+ // src/cli/ui/components/loading.tsx
550
+ import { Box as Box5, Text as Text5 } from "ink";
551
+ import React6 from "react";
552
+ function Loading() {
553
+ return /* @__PURE__ */ React6.createElement(Box5, { paddingX: 1, borderDimColor: true, flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Box5, { alignSelf: "center" }, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true, color: "redBright" }, "Fetching Data")), /* @__PURE__ */ React6.createElement(Box5, { alignSelf: "center" }, /* @__PURE__ */ React6.createElement(Text5, { color: "blueBright", dimColor: true }, /* @__PURE__ */ React6.createElement(Spinner, { type: "grenade" }))));
554
+ }
555
+
556
+ // src/cli/ui/components/titlebar.tsx
557
+ import { Box as Box6, Spacer as Spacer2, Text as Text6 } from "ink";
558
+ import React7 from "react";
559
+
560
+ // package.json
561
+ var version = "3.3.2";
562
+
563
+ // src/cli/ui/components/titlebar.tsx
564
+ function TitleBar() {
565
+ return /* @__PURE__ */ React7.createElement(Box6, null, /* @__PURE__ */ React7.createElement(Spacer2, null), /* @__PURE__ */ React7.createElement(Box6, { borderColor: "magenta", borderStyle: "arrow" }, /* @__PURE__ */ React7.createElement(Text6, { color: "cyanBright" }, "Coomer-Downloader ", version)), /* @__PURE__ */ React7.createElement(Spacer2, null));
566
+ }
567
+
568
+ // src/cli/ui/hooks/downloader.ts
569
+ import { useEffect as useEffect2, useState as useState2 } from "react";
570
+ var useDownloaderHook = () => {
571
+ const downloader = useInkStore((state) => state.downloader);
572
+ const filelist = downloader?.filelist;
573
+ const [_, setHelper] = useState2(0);
574
+ useEffect2(() => {
575
+ downloader?.subject.subscribe(({ type }) => {
576
+ if (type === "FILE_DOWNLOADING_START" || type === "FILE_DOWNLOADING_END" || type === "CHUNK_DOWNLOADING_UPDATE") {
577
+ setHelper(Date.now());
578
+ }
579
+ });
580
+ });
581
+ };
582
+
583
+ // src/cli/ui/hooks/input.ts
584
+ import { useInput } from "ink";
585
+ var useInputHook = () => {
586
+ const downloader = useInkStore((state) => state.downloader);
587
+ const switchPreview = useInkStore((state) => state.switchPreview);
588
+ useInput((input) => {
589
+ if (input === "s") {
590
+ downloader?.skip();
591
+ }
592
+ if (input === "p") {
593
+ switchPreview();
594
+ }
595
+ });
596
+ };
597
+
598
+ // src/cli/ui/app.tsx
599
+ function App() {
600
+ useInputHook();
601
+ useDownloaderHook();
602
+ const downloader = useInkStore((state) => state.downloader);
603
+ const filelist = downloader?.filelist;
604
+ const isFilelist = filelist instanceof CoomerFileList;
605
+ return /* @__PURE__ */ React8.createElement(Box7, { borderStyle: "single", flexDirection: "column", borderColor: "blue", width: 80 }, /* @__PURE__ */ React8.createElement(TitleBar, null), !isFilelist ? /* @__PURE__ */ React8.createElement(Loading, null) : /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(Box7, null, /* @__PURE__ */ React8.createElement(Box7, null, /* @__PURE__ */ React8.createElement(FileListStateBox, { filelist })), /* @__PURE__ */ React8.createElement(Box7, { flexBasis: 29 }, /* @__PURE__ */ React8.createElement(KeyboardControlsInfo, null))), filelist.getActiveFiles().map((file) => {
606
+ return /* @__PURE__ */ React8.createElement(FileBox, { file, key: file.name });
607
+ })));
608
+ }
609
+
610
+ // src/cli/ui/index.tsx
611
+ function createReactInk() {
612
+ return render(/* @__PURE__ */ React9.createElement(App, null));
613
+ }
614
+
615
+ // src/services/downloader.ts
616
+ import fs2 from "node:fs";
617
+ import { Readable, Transform } from "node:stream";
618
+ import { pipeline } from "node:stream/promises";
619
+ import { Subject } from "rxjs";
620
+
621
+ // src/utils/promise.ts
622
+ async function sleep(time) {
623
+ return new Promise((resolve) => setTimeout(resolve, time));
624
+ }
625
+
626
+ // src/utils/timer.ts
627
+ var Timer = class _Timer {
628
+ constructor(timeout = 1e4, timeoutCallback) {
629
+ this.timeout = timeout;
630
+ this.timeoutCallback = timeoutCallback;
631
+ this.timeout = timeout;
632
+ }
633
+ timer;
634
+ start() {
635
+ this.timer = setTimeout(() => {
636
+ this.stop();
637
+ this.timeoutCallback();
638
+ }, this.timeout);
639
+ return this;
640
+ }
641
+ stop() {
642
+ if (this.timer) {
643
+ clearTimeout(this.timer);
644
+ this.timer = void 0;
645
+ }
646
+ return this;
647
+ }
648
+ reset() {
649
+ this.stop();
650
+ this.start();
651
+ return this;
652
+ }
653
+ static withAbortController(timeout, abortControllerSubject, message = "Timeout") {
654
+ const callback = () => {
655
+ abortControllerSubject.next(message);
656
+ };
657
+ const timer = new _Timer(timeout, callback).start();
658
+ return { timer };
659
+ }
660
+ };
661
+
662
+ // src/services/downloader.ts
663
+ var Downloader = class {
664
+ constructor(filelist, chunkTimeout = 3e4, chunkFetchRetries = 5, fetchRetries = 7) {
665
+ this.filelist = filelist;
666
+ this.chunkTimeout = chunkTimeout;
667
+ this.chunkFetchRetries = chunkFetchRetries;
668
+ this.fetchRetries = fetchRetries;
669
+ this.setAbortControllerListener();
670
+ }
671
+ subject = new Subject();
672
+ abortController = new AbortController();
673
+ abortControllerSubject = new Subject();
674
+ setAbortControllerListener() {
675
+ this.abortControllerSubject.subscribe((type) => {
676
+ this.abortController.abort(type);
677
+ this.abortController = new AbortController();
678
+ });
679
+ }
680
+ async fetchStream(file, stream, sizeOld = 0, retries = this.chunkFetchRetries) {
681
+ const signal = this.abortController.signal;
682
+ const subject = this.subject;
683
+ const { timer } = Timer.withAbortController(this.chunkTimeout, this.abortControllerSubject);
684
+ let i;
685
+ try {
686
+ const fileStream = fs2.createWriteStream(file.filepath, { flags: "a" });
687
+ const progressStream = new Transform({
688
+ transform(chunk, _encoding, callback) {
689
+ this.push(chunk);
690
+ file.downloaded += chunk.length;
691
+ timer.reset();
692
+ subject.next({ type: "CHUNK_DOWNLOADING_UPDATE" });
693
+ callback();
694
+ }
695
+ });
696
+ subject.next({ type: "CHUNK_DOWNLOADING_START" });
697
+ await pipeline(stream, progressStream, fileStream, { signal });
698
+ } catch (error) {
699
+ if (signal.aborted) {
700
+ if (signal.reason === "FILE_SKIP") return;
701
+ if (signal.reason === "TIMEOUT") {
702
+ if (retries === 0 && sizeOld < file.downloaded) {
703
+ retries += this.chunkFetchRetries;
704
+ sizeOld = file.downloaded;
705
+ }
706
+ if (retries === 0) return;
707
+ return await this.fetchStream(file, stream, sizeOld, retries - 1);
708
+ }
709
+ }
710
+ throw error;
711
+ } finally {
712
+ subject.next({ type: "CHUNK_DOWNLOADING_END" });
713
+ timer.stop();
714
+ clearInterval(i);
715
+ }
716
+ }
717
+ skip() {
718
+ this.abortControllerSubject.next("FILE_SKIP");
719
+ }
720
+ async downloadFile(file, retries = this.fetchRetries) {
721
+ const signal = this.abortController.signal;
722
+ try {
723
+ file.downloaded = await getFileSize(file.filepath);
724
+ const response = await fetchByteRange(file.url, file.downloaded, signal);
725
+ if (!response?.ok && response?.status !== 416) {
726
+ throw new Error(`HTTP error! status: ${response?.status}`);
727
+ }
728
+ const contentLength = response.headers.get("Content-Length");
729
+ if (!contentLength && file.downloaded > 0) return;
730
+ const restFileSize = parseInt(contentLength);
731
+ file.size = restFileSize + file.downloaded;
732
+ if (file.size > file.downloaded && response.body) {
733
+ const stream = Readable.fromWeb(response.body);
734
+ stream.setMaxListeners(20);
735
+ await this.fetchStream(file, stream, file.downloaded);
736
+ }
737
+ } catch (error) {
738
+ if (signal.aborted) {
739
+ if (signal.reason === "FILE_SKIP") return;
740
+ }
741
+ if (retries > 0) {
742
+ if (/coomer|kemono/.test(file.url)) {
743
+ file.url = tryFixCoomerUrl(file.url, retries);
744
+ }
745
+ await sleep(1e3);
746
+ return await this.downloadFile(file, retries - 1);
747
+ }
748
+ throw error;
749
+ }
750
+ }
751
+ async downloadFiles() {
752
+ mkdir(this.filelist.dirPath);
753
+ this.subject.next({ type: "FILES_DOWNLOADING_START" });
754
+ for (const file of this.filelist.files) {
755
+ file.active = true;
756
+ this.subject.next({ type: "FILE_DOWNLOADING_START" });
757
+ await this.downloadFile(file);
758
+ file.active = false;
759
+ this.subject.next({ type: "FILE_DOWNLOADING_END" });
760
+ }
761
+ this.subject.next({ type: "FILES_DOWNLOADING_END" });
762
+ }
763
+ };
764
+
632
765
  // src/index.ts
633
766
  async function run() {
767
+ createReactInk();
634
768
  const { url, dir, media, include, exclude, skip } = argumentHander();
635
769
  const filelist = await apiHandler(url);
636
- const found = filelist.files.length;
637
- filelist.setDirPath(dir);
638
- filelist.skip(skip);
639
- filelist.filterByText(include, exclude);
640
- filelist.filterByMediaType(media);
641
- console.table([
642
- {
643
- found,
644
- skip,
645
- filtered: found - filelist.files.length,
646
- folder: filelist.dirPath
647
- }
648
- ]);
770
+ filelist.setDirPath(dir).skip(skip).filterByText(include, exclude).filterByMediaType(media);
771
+ await filelist.calculateFileSizes();
649
772
  setGlobalHeaders({ Referer: url });
650
- const downloader = new Downloader();
651
- createMultibar(downloader);
652
- await downloader.downloadFiles(filelist);
773
+ const downloader = new Downloader(filelist);
774
+ useInkStore.getState().setDownloader(downloader);
775
+ await downloader.downloadFiles();
653
776
  process2.kill(process2.pid, "SIGINT");
654
777
  }
655
778
  run();