coomer-downloader 3.4.1 → 3.4.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
@@ -3,7 +3,7 @@
3
3
  // src/index.ts
4
4
  import process2 from "node:process";
5
5
 
6
- // src/api/bunkr.ts
6
+ // src/api/providers/bunkr.ts
7
7
  import * as cheerio from "cheerio";
8
8
  import { fetch } from "undici";
9
9
 
@@ -39,7 +39,7 @@ function sanitizeFilename(name) {
39
39
  return name.replace(/[<>:"/\\|?*\x00-\x1F]/g, "-").replace(/\s+/g, " ").trim().replace(/[.]+$/, "");
40
40
  }
41
41
 
42
- // src/services/file.ts
42
+ // src/core/file.ts
43
43
  var CoomerFile = class _CoomerFile {
44
44
  constructor(name, url, filepath = "", size, downloaded = 0, content) {
45
45
  this.name = name;
@@ -64,7 +64,7 @@ var CoomerFile = class _CoomerFile {
64
64
  }
65
65
  };
66
66
 
67
- // src/services/filelist.ts
67
+ // src/core/filelist.ts
68
68
  import os from "node:os";
69
69
  import path from "node:path";
70
70
 
@@ -124,13 +124,14 @@ function testMediaType(name, type) {
124
124
  return type === "image" ? isImage(name) : isVideo(name);
125
125
  }
126
126
 
127
- // src/services/filelist.ts
127
+ // src/core/filelist.ts
128
128
  var CoomerFileList = class {
129
129
  constructor(files = []) {
130
130
  this.files = files;
131
131
  }
132
132
  dirPath;
133
133
  dirName;
134
+ provider;
134
135
  setDirPath(dir, dirName) {
135
136
  dirName = dirName || this.dirName;
136
137
  if (dir === "./") {
@@ -185,7 +186,7 @@ var CoomerFileList = class {
185
186
  }
186
187
  };
187
188
 
188
- // src/api/bunkr.ts
189
+ // src/api/providers/bunkr.ts
189
190
  async function getEncryptionData(slug) {
190
191
  const response = await fetch("https://bunkr.cr/api/vs", {
191
192
  method: "POST",
@@ -232,10 +233,15 @@ async function getGalleryFiles(url) {
232
233
  }
233
234
  return filelist;
234
235
  }
235
- async function getBunkrData(url) {
236
- const filelist = await getGalleryFiles(url);
237
- return filelist;
238
- }
236
+ var BunkrAPI = class {
237
+ testURL(url) {
238
+ return /bunkr/.test(url.origin);
239
+ }
240
+ async getData(url) {
241
+ const filelist = await getGalleryFiles(url);
242
+ return filelist;
243
+ }
244
+ };
239
245
 
240
246
  // src/utils/requests.ts
241
247
  import { CookieAgent } from "http-cookie-agent/undici";
@@ -266,20 +272,7 @@ function fetchByteRange(url, downloadedSize, signal) {
266
272
  return fetch2(url, { headers: requestHeaders, signal });
267
273
  }
268
274
 
269
- // src/api/coomer-api.ts
270
- var SERVERS = ["n1", "n2", "n3", "n4"];
271
- function tryFixCoomerUrl(url, attempts) {
272
- if (attempts < 2 && isImage(url)) {
273
- return url.replace(/\/data\//, "/thumbnail/data/").replace(/n\d\./, "img.");
274
- }
275
- const server = url.match(/n\d\./)?.[0].slice(0, 2);
276
- const i = SERVERS.indexOf(server);
277
- if (i !== -1) {
278
- const newServer = SERVERS[(i + 1) % SERVERS.length];
279
- return url.replace(/n\d./, `${newServer}.`);
280
- }
281
- return url;
282
- }
275
+ // src/api/providers/coomer.ts
283
276
  async function getUserProfileData(user) {
284
277
  const url = `${user.domain}/api/v1/${user.service}/user/${user.id}/profile`;
285
278
  const result = await fetchWithGlobalHeader(url).then((r) => r.json());
@@ -332,15 +325,33 @@ async function parseUser(url) {
332
325
  const { name } = await getUserProfileData({ domain, service, id });
333
326
  return { domain, service, id, name };
334
327
  }
335
- async function getCoomerData(url) {
336
- setGlobalHeaders({ accept: "text/css" });
337
- const user = await parseUser(url);
338
- const filelist = await getUserFiles(user);
339
- filelist.dirName = `${user.name}-${user.service}`;
340
- return filelist;
341
- }
328
+ var CoomerAPI = class _CoomerAPI {
329
+ static SERVERS = ["n1", "n2", "n3", "n4"];
330
+ fixURL(url, retries) {
331
+ if (retries < 2 && isImage(url)) {
332
+ return url.replace(/\/data\//, "/thumbnail/data/").replace(/n\d\./, "img.");
333
+ }
334
+ const server = url.match(/n\d\./)?.[0].slice(0, 2);
335
+ const i = _CoomerAPI.SERVERS.indexOf(server);
336
+ if (i !== -1) {
337
+ const newServer = _CoomerAPI.SERVERS[(i + 1) % _CoomerAPI.SERVERS.length];
338
+ return url.replace(/n\d./, `${newServer}.`);
339
+ }
340
+ return url;
341
+ }
342
+ testURL(url) {
343
+ return /coomer|kemono/.test(url.origin);
344
+ }
345
+ async getData(url) {
346
+ setGlobalHeaders({ accept: "text/css" });
347
+ const user = await parseUser(url);
348
+ const filelist = await getUserFiles(user);
349
+ filelist.dirName = `${user.name}-${user.service}`;
350
+ return filelist;
351
+ }
352
+ };
342
353
 
343
- // src/api/gofile.ts
354
+ // src/api/providers/gofile.ts
344
355
  import { fetch as fetch3 } from "undici";
345
356
  async function getToken() {
346
357
  const response = await fetch3("https://api.gofile.io/accounts", {
@@ -350,7 +361,7 @@ async function getToken() {
350
361
  if (data.status === "ok") {
351
362
  return data.data.token;
352
363
  }
353
- throw new Error("cannot get token");
364
+ throw new Error("Token Not Found");
354
365
  }
355
366
  async function getWebsiteToken() {
356
367
  const response = await fetch3("https://gofile.io/dist/js/global.js");
@@ -380,19 +391,54 @@ async function getFolderFiles(id, token, websiteToken) {
380
391
  );
381
392
  return new CoomerFileList(files);
382
393
  }
383
- async function getGofileData(url) {
384
- const id = url.match(/gofile.io\/d\/(\w+)/)?.[1];
385
- const token = await getToken();
386
- const websiteToken = await getWebsiteToken();
387
- const filelist = await getFolderFiles(id, token, websiteToken);
388
- filelist.dirName = `gofile-${id}`;
389
- setGlobalHeaders({ Cookie: `accountToken=${token}` });
390
- return filelist;
391
- }
394
+ var GofileAPI = class {
395
+ testURL(url) {
396
+ return /gofile\.io/.test(url.origin);
397
+ }
398
+ async getData(url) {
399
+ const id = url.match(/gofile.io\/d\/(\w+)/)?.[1];
400
+ const token = await getToken();
401
+ const websiteToken = await getWebsiteToken();
402
+ const filelist = await getFolderFiles(id, token, websiteToken);
403
+ filelist.dirName = `gofile-${id}`;
404
+ setGlobalHeaders({ Cookie: `accountToken=${token}` });
405
+ return filelist;
406
+ }
407
+ };
408
+
409
+ // src/api/providers/plainfile.ts
410
+ var PlainFileAPI = class {
411
+ testURL(url) {
412
+ return /\.\w+/.test(url.pathname);
413
+ }
414
+ async getData(url) {
415
+ const name = url.split("/").pop();
416
+ const file = CoomerFile.from({ name, url });
417
+ const filelist = new CoomerFileList([file]);
418
+ filelist.dirName = "";
419
+ return filelist;
420
+ }
421
+ };
392
422
 
393
- // src/api/nsfw.xxx.ts
423
+ // src/api/providers/reddit.ts
394
424
  import * as cheerio2 from "cheerio";
395
425
  import { fetch as fetch4 } from "undici";
426
+
427
+ // src/utils/logger.ts
428
+ import pino from "pino";
429
+ var logger = pino(
430
+ {
431
+ level: "debug"
432
+ },
433
+ pino.destination({
434
+ dest: "./debug.log",
435
+ append: false,
436
+ sync: true
437
+ })
438
+ );
439
+ var logger_default = logger;
440
+
441
+ // src/api/providers/reddit.ts
396
442
  async function getUserPage(user, offset) {
397
443
  const url = `https://nsfw.xxx/page/${offset}?nsfw[]=0&types[]=image&types[]=video&types[]=gallery&slider=1&jsload=1&user=${user}&_=${Date.now()}`;
398
444
  return fetch4(url).then((r) => r.text());
@@ -404,6 +450,7 @@ async function getUserPosts(user) {
404
450
  if (page.length < 1) break;
405
451
  const $ = cheerio2.load(page);
406
452
  const newPosts = $("a").map((_, a) => $(a).attr("href")).get().filter((href) => href?.startsWith("https://nsfw.xxx/post"));
453
+ logger_default.debug({ count: posts.length });
407
454
  posts.push(...newPosts);
408
455
  }
409
456
  return posts;
@@ -419,46 +466,35 @@ async function getPostsData(posts) {
419
466
  const date = $(".sh-section .sh-section__passed").first().text().replace(/ /g, "-") || "";
420
467
  const ext = src.split(".").pop();
421
468
  const name = `${slug}-${date}.${ext}`;
469
+ logger_default.debug({ hehe: filelist.files.length, src });
422
470
  filelist.files.push(CoomerFile.from({ name, url: src }));
423
471
  }
424
472
  return filelist;
425
473
  }
426
- async function getRedditData(url) {
427
- const user = url.match(/u\/(\w+)/)?.[1];
428
- console.log("Fetching user posts...");
429
- const posts = await getUserPosts(user);
430
- console.log("Fetching posts data...");
431
- const filelist = await getPostsData(posts);
432
- filelist.dirName = `${user}-reddit`;
433
- return filelist;
434
- }
435
-
436
- // src/api/plain-curl.ts
437
- async function getPlainFileData(url) {
438
- const name = url.split("/").pop();
439
- const file = CoomerFile.from({ name, url });
440
- const filelist = new CoomerFileList([file]);
441
- filelist.dirName = "";
442
- return filelist;
443
- }
444
-
445
- // src/api/index.ts
446
- async function apiHandler(url_) {
447
- const url = new URL(url_);
448
- if (/^u\/\w+$/.test(url.origin)) {
449
- return getRedditData(url.href);
450
- }
451
- if (/coomer|kemono/.test(url.origin)) {
452
- return getCoomerData(url.href);
474
+ var RedditAPI = class {
475
+ testURL(url) {
476
+ return /^\/user\/[\w-]+$/.test(url.pathname);
453
477
  }
454
- if (/bunkr/.test(url.origin)) {
455
- return getBunkrData(url.href);
456
- }
457
- if (/gofile\.io/.test(url.origin)) {
458
- return getGofileData(url.href);
478
+ async getData(url) {
479
+ const user = url.match(/^\/user\/([\w-]+)/)?.[1];
480
+ const posts = await getUserPosts(user);
481
+ const filelist = await getPostsData(posts);
482
+ filelist.dirName = `${user}-reddit`;
483
+ return filelist;
459
484
  }
460
- if (/\.\w+/.test(url.pathname)) {
461
- return getPlainFileData(url.href);
485
+ };
486
+
487
+ // src/api/resolver.ts
488
+ var providers = [RedditAPI, CoomerAPI, BunkrAPI, GofileAPI, PlainFileAPI];
489
+ async function resolveAPI(url_) {
490
+ const url = new URL(url_);
491
+ for (const p of providers) {
492
+ const provider = new p();
493
+ if (provider.testURL(url)) {
494
+ const filelist = await provider.getData(url.toString());
495
+ filelist.provider = provider;
496
+ return filelist;
497
+ }
462
498
  }
463
499
  throw Error("Invalid URL");
464
500
  }
@@ -509,208 +545,13 @@ function argumentHander() {
509
545
 
510
546
  // src/cli/ui/index.tsx
511
547
  import { render } from "ink";
512
- import React9 from "react";
548
+ import React10 from "react";
513
549
 
514
550
  // src/cli/ui/app.tsx
515
551
  import { Box as Box7 } from "ink";
516
- import React8 from "react";
517
-
518
- // src/cli/ui/components/file.tsx
519
- import { Box as Box2, Spacer, Text as Text2 } from "ink";
520
- import React3 from "react";
521
-
522
- // src/utils/strings.ts
523
- function b2mb(bytes) {
524
- return (bytes / 1048576).toFixed(2);
525
- }
526
-
527
- // src/cli/ui/components/preview.tsx
528
- import { Box } from "ink";
529
- import Image, { TerminalInfoProvider } from "ink-picture";
530
- import React from "react";
531
-
532
- // src/cli/ui/store/index.ts
533
- import { create } from "zustand";
534
- var useInkStore = create((set) => ({
535
- preview: false,
536
- switchPreview: () => set((state) => ({
537
- preview: !state.preview
538
- })),
539
- downloader: void 0,
540
- setDownloader: (downloader) => set({ downloader })
541
- }));
542
-
543
- // src/cli/ui/components/preview.tsx
544
- function Preview({ file }) {
545
- const previewEnabled = useInkStore((state) => state.preview);
546
- const bigEnough = file.downloaded > 50 * 1024;
547
- const shouldShow = previewEnabled && bigEnough && isImage(file.filepath);
548
- const imgInfo = `
549
- can't read partial images yet...
550
- actual size: ${file.size}}
551
- downloaded: ${file.downloaded}}
552
- `;
553
- 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 }))));
554
- }
555
-
556
- // src/cli/ui/components/spinner.tsx
557
- import spinners from "cli-spinners";
558
- import { Text } from "ink";
559
- import React2, { useEffect, useState } from "react";
560
- function Spinner({ type = "dots" }) {
561
- const spinner = spinners[type];
562
- const randomFrame = spinner.frames.length * Math.random() | 0;
563
- const [frame, setFrame] = useState(randomFrame);
564
- useEffect(() => {
565
- const timer = setInterval(() => {
566
- setFrame((previousFrame) => {
567
- return (previousFrame + 1) % spinner.frames.length;
568
- });
569
- }, spinner.interval);
570
- return () => {
571
- clearInterval(timer);
572
- };
573
- }, [spinner]);
574
- return /* @__PURE__ */ React2.createElement(Text, null, spinner.frames[frame]);
575
- }
576
-
577
- // src/cli/ui/components/file.tsx
578
- function FileBox({ file }) {
579
- const percentage = Number(file.downloaded / file.size * 100).toFixed(2);
580
- return /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement(
581
- Box2,
582
- {
583
- borderStyle: "single",
584
- borderColor: "magentaBright",
585
- borderDimColor: true,
586
- paddingX: 1,
587
- flexDirection: "column"
588
- },
589
- /* @__PURE__ */ React3.createElement(Box2, null, /* @__PURE__ */ React3.createElement(Text2, { color: "blue", dimColor: true, wrap: "truncate-middle" }, file.name)),
590
- /* @__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)))
591
- ), /* @__PURE__ */ React3.createElement(Preview, { file }));
592
- }
593
-
594
- // src/cli/ui/components/filelist.tsx
595
- import { Box as Box3, Text as Text3 } from "ink";
596
- import React4 from "react";
597
- function FileListStateBox({ filelist }) {
598
- return /* @__PURE__ */ React4.createElement(
599
- Box3,
600
- {
601
- paddingX: 1,
602
- flexDirection: "column",
603
- borderStyle: "single",
604
- borderColor: "magenta",
605
- borderDimColor: true
606
- },
607
- /* @__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)),
608
- /* @__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)),
609
- /* @__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)))
610
- );
611
- }
612
-
613
- // src/cli/ui/components/keyboardinfo.tsx
614
- import { Box as Box4, Text as Text4 } from "ink";
615
- import React5 from "react";
616
- var info = {
617
- "s ": "skip current file",
618
- p: "on/off image preview"
619
- };
620
- function KeyboardControlsInfo() {
621
- const infoRender = Object.entries(info).map(([key, value]) => {
622
- 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));
623
- });
624
- return /* @__PURE__ */ React5.createElement(
625
- Box4,
626
- {
627
- flexDirection: "column",
628
- paddingX: 1,
629
- borderStyle: "single",
630
- borderColor: "gray",
631
- borderDimColor: true
632
- },
633
- /* @__PURE__ */ React5.createElement(Box4, null, /* @__PURE__ */ React5.createElement(Text4, { color: "red", dimColor: true, bold: true }, "Keyboard controls:")),
634
- infoRender
635
- );
636
- }
637
-
638
- // src/cli/ui/components/loading.tsx
639
- import { Box as Box5, Text as Text5 } from "ink";
640
- import React6 from "react";
641
- function Loading() {
642
- 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" }))));
643
- }
644
-
645
- // src/cli/ui/components/titlebar.tsx
646
- import { Box as Box6, Spacer as Spacer2, Text as Text6 } from "ink";
647
- import React7 from "react";
648
-
649
- // package.json
650
- var version = "3.4.1";
651
-
652
- // src/cli/ui/components/titlebar.tsx
653
- function TitleBar() {
654
- 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));
655
- }
656
-
657
- // src/cli/ui/hooks/downloader.ts
658
- import { useRef, useSyncExternalStore } from "react";
659
- var useDownloaderHook = () => {
660
- const downloader = useInkStore((state) => state.downloader);
661
- const versionRef = useRef(0);
662
- useSyncExternalStore(
663
- (onStoreChange) => {
664
- if (!downloader) return () => {
665
- };
666
- const sub = downloader.subject.subscribe(({ type }) => {
667
- const targets = [
668
- "FILE_DOWNLOADING_START",
669
- "FILE_DOWNLOADING_END",
670
- "CHUNK_DOWNLOADING_UPDATE"
671
- ];
672
- if (targets.includes(type)) {
673
- versionRef.current++;
674
- onStoreChange();
675
- }
676
- });
677
- return () => sub.unsubscribe();
678
- },
679
- () => versionRef.current
680
- );
681
- return downloader?.filelist;
682
- };
683
-
684
- // src/cli/ui/hooks/input.ts
685
- import { useInput } from "ink";
686
- var useInputHook = () => {
687
- const downloader = useInkStore((state) => state.downloader);
688
- const switchPreview = useInkStore((state) => state.switchPreview);
689
- useInput((input) => {
690
- if (input === "s") {
691
- downloader?.skip();
692
- }
693
- if (input === "p") {
694
- switchPreview();
695
- }
696
- });
697
- };
698
-
699
- // src/cli/ui/app.tsx
700
- function App() {
701
- useInputHook();
702
- const filelist = useDownloaderHook();
703
- return /* @__PURE__ */ React8.createElement(Box7, { borderStyle: "single", flexDirection: "column", borderColor: "blue", width: 80 }, /* @__PURE__ */ React8.createElement(TitleBar, null), !(filelist instanceof CoomerFileList) ? /* @__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: 30 }, /* @__PURE__ */ React8.createElement(KeyboardControlsInfo, null))), filelist?.getActiveFiles().map((file) => {
704
- return /* @__PURE__ */ React8.createElement(FileBox, { file, key: file.name });
705
- })));
706
- }
707
-
708
- // src/cli/ui/index.tsx
709
- function createReactInk() {
710
- return render(/* @__PURE__ */ React9.createElement(App, null));
711
- }
552
+ import React9 from "react";
712
553
 
713
- // src/services/downloader.ts
554
+ // src/core/downloader.ts
714
555
  import fs2 from "node:fs";
715
556
  import { Readable, Transform } from "node:stream";
716
557
  import { pipeline as pipeline2 } from "node:stream/promises";
@@ -757,7 +598,7 @@ var Timer = class _Timer {
757
598
  }
758
599
  };
759
600
 
760
- // src/services/downloader.ts
601
+ // src/core/downloader.ts
761
602
  var Downloader = class {
762
603
  constructor(filelist, minSize, maxSize, chunkTimeout = 3e4, chunkFetchRetries = 5, fetchRetries = 7) {
763
604
  this.filelist = filelist;
@@ -852,8 +693,8 @@ var Downloader = class {
852
693
  if (signal.reason === "FILE_SKIP") return;
853
694
  }
854
695
  if (retries > 0) {
855
- if (/coomer|kemono/.test(file.url)) {
856
- file.url = tryFixCoomerUrl(file.url, retries);
696
+ if (this.filelist.provider?.fixURL) {
697
+ file.url = this.filelist.provider.fixURL(file.url, retries);
857
698
  }
858
699
  await sleep(1e3);
859
700
  return await this.downloadFile(file, retries - 1);
@@ -875,11 +716,210 @@ var Downloader = class {
875
716
  }
876
717
  };
877
718
 
719
+ // src/cli/ui/components/file.tsx
720
+ import { Box as Box2, Spacer, Text as Text2 } from "ink";
721
+ import React3 from "react";
722
+
723
+ // src/utils/strings.ts
724
+ function b2mb(bytes) {
725
+ return (bytes / 1048576).toFixed(2);
726
+ }
727
+
728
+ // src/cli/ui/hooks/downloader.ts
729
+ import { useRef, useSyncExternalStore } from "react";
730
+
731
+ // src/cli/ui/store/index.ts
732
+ import { create } from "zustand";
733
+ var useInkStore = create((set) => ({
734
+ preview: false,
735
+ switchPreview: () => set((state) => ({
736
+ preview: !state.preview
737
+ })),
738
+ downloader: void 0,
739
+ setDownloader: (downloader) => set({ downloader })
740
+ }));
741
+
742
+ // src/cli/ui/hooks/downloader.ts
743
+ var useDownloaderHook = (subjectEvents) => {
744
+ const downloader = useInkStore((state) => state.downloader);
745
+ const versionRef = useRef(0);
746
+ useSyncExternalStore(
747
+ (onStoreChange) => {
748
+ if (!downloader) return () => {
749
+ };
750
+ const sub = downloader.subject.subscribe(({ type }) => {
751
+ if (subjectEvents.includes(type)) {
752
+ versionRef.current++;
753
+ onStoreChange();
754
+ }
755
+ });
756
+ return () => sub.unsubscribe();
757
+ },
758
+ () => versionRef.current
759
+ );
760
+ return downloader?.filelist;
761
+ };
762
+
763
+ // src/cli/ui/components/preview.tsx
764
+ import { Box } from "ink";
765
+ import Image, { TerminalInfoProvider } from "ink-picture";
766
+ import React from "react";
767
+ function Preview({ file }) {
768
+ const previewEnabled = useInkStore((state) => state.preview);
769
+ const bigEnough = file.downloaded > 50 * 1024;
770
+ const shouldShow = previewEnabled && bigEnough && isImage(file.filepath);
771
+ const imgInfo = `
772
+ can't read partial images yet...
773
+ actual size: ${file.size}}
774
+ downloaded: ${file.downloaded}}
775
+ `;
776
+ 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 }))));
777
+ }
778
+
779
+ // src/cli/ui/components/spinner.tsx
780
+ import spinners from "cli-spinners";
781
+ import { Text } from "ink";
782
+ import React2, { useEffect, useState } from "react";
783
+ function Spinner({ type = "dots" }) {
784
+ const spinner = spinners[type];
785
+ const randomFrame = spinner.frames.length * Math.random() | 0;
786
+ const [frame, setFrame] = useState(randomFrame);
787
+ useEffect(() => {
788
+ const timer = setInterval(() => {
789
+ setFrame((previousFrame) => {
790
+ return (previousFrame + 1) % spinner.frames.length;
791
+ });
792
+ }, spinner.interval);
793
+ return () => {
794
+ clearInterval(timer);
795
+ };
796
+ }, [spinner]);
797
+ return /* @__PURE__ */ React2.createElement(Text, null, spinner.frames[frame]);
798
+ }
799
+
800
+ // src/cli/ui/components/file.tsx
801
+ var FileBox = React3.memo(({ file }) => {
802
+ useDownloaderHook(["CHUNK_DOWNLOADING_UPDATE"]);
803
+ const percentage = Number(file.downloaded / file.size * 100).toFixed(2);
804
+ return /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement(
805
+ Box2,
806
+ {
807
+ borderStyle: "single",
808
+ borderColor: "magentaBright",
809
+ borderDimColor: true,
810
+ paddingX: 1,
811
+ flexDirection: "column"
812
+ },
813
+ /* @__PURE__ */ React3.createElement(Box2, null, /* @__PURE__ */ React3.createElement(Text2, { color: "blue", dimColor: true, wrap: "truncate-middle" }, file.name)),
814
+ /* @__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)))
815
+ ), /* @__PURE__ */ React3.createElement(Preview, { file }));
816
+ });
817
+
818
+ // src/cli/ui/components/filelist.tsx
819
+ import React4 from "react";
820
+ function FileListBox() {
821
+ const filelist = useDownloaderHook(["FILE_DOWNLOADING_START", "FILE_DOWNLOADING_END"]);
822
+ return /* @__PURE__ */ React4.createElement(React4.Fragment, null, filelist?.getActiveFiles().map((file) => {
823
+ return /* @__PURE__ */ React4.createElement(FileBox, { file, key: file.name });
824
+ }));
825
+ }
826
+
827
+ // src/cli/ui/components/filelist-state.tsx
828
+ import { Box as Box3, Text as Text3 } from "ink";
829
+ import React5 from "react";
830
+ function FileListStateBox() {
831
+ const filelist = useDownloaderHook(["FILE_DOWNLOADING_START", "FILE_DOWNLOADING_END"]);
832
+ return /* @__PURE__ */ React5.createElement(
833
+ Box3,
834
+ {
835
+ paddingX: 1,
836
+ flexDirection: "column",
837
+ borderStyle: "single",
838
+ borderColor: "magenta",
839
+ borderDimColor: true
840
+ },
841
+ /* @__PURE__ */ React5.createElement(Box3, null, /* @__PURE__ */ React5.createElement(Box3, { marginRight: 1 }, /* @__PURE__ */ React5.createElement(Text3, { color: "cyanBright", dimColor: true }, "Found:")), /* @__PURE__ */ React5.createElement(Text3, { color: "blue", dimColor: true, wrap: "wrap" }, filelist?.files.length)),
842
+ /* @__PURE__ */ React5.createElement(Box3, null, /* @__PURE__ */ React5.createElement(Box3, { marginRight: 1 }, /* @__PURE__ */ React5.createElement(Text3, { color: "cyanBright", dimColor: true }, "Downloaded:")), /* @__PURE__ */ React5.createElement(Text3, { color: "blue", dimColor: true, wrap: "wrap" })),
843
+ /* @__PURE__ */ React5.createElement(Box3, null, /* @__PURE__ */ React5.createElement(Box3, { width: 9 }, /* @__PURE__ */ React5.createElement(Text3, { color: "cyanBright", dimColor: true }, "Folder:")), /* @__PURE__ */ React5.createElement(Box3, { flexGrow: 1 }, /* @__PURE__ */ React5.createElement(Text3, { color: "blue", dimColor: true, wrap: "truncate-middle" }, filelist?.dirPath)))
844
+ );
845
+ }
846
+
847
+ // src/cli/ui/components/keyboardinfo.tsx
848
+ import { Box as Box4, Text as Text4 } from "ink";
849
+ import React6 from "react";
850
+ var info = {
851
+ "s ": "skip current file",
852
+ p: "on/off image preview"
853
+ };
854
+ function KeyboardControlsInfo() {
855
+ const infoRender = Object.entries(info).map(([key, value]) => {
856
+ return /* @__PURE__ */ React6.createElement(Box4, { key }, /* @__PURE__ */ React6.createElement(Box4, { marginRight: 2 }, /* @__PURE__ */ React6.createElement(Text4, { color: "red", dimColor: true, bold: true }, key)), /* @__PURE__ */ React6.createElement(Text4, { dimColor: true, bold: false }, value));
857
+ });
858
+ return /* @__PURE__ */ React6.createElement(
859
+ Box4,
860
+ {
861
+ flexDirection: "column",
862
+ paddingX: 1,
863
+ borderStyle: "single",
864
+ borderColor: "gray",
865
+ borderDimColor: true
866
+ },
867
+ /* @__PURE__ */ React6.createElement(Box4, null, /* @__PURE__ */ React6.createElement(Text4, { color: "red", dimColor: true, bold: true }, "Keyboard controls:")),
868
+ infoRender
869
+ );
870
+ }
871
+
872
+ // src/cli/ui/components/loading.tsx
873
+ import { Box as Box5, Text as Text5 } from "ink";
874
+ import React7 from "react";
875
+ function Loading() {
876
+ return /* @__PURE__ */ React7.createElement(Box5, { paddingX: 1, borderDimColor: true, flexDirection: "column" }, /* @__PURE__ */ React7.createElement(Box5, { alignSelf: "center" }, /* @__PURE__ */ React7.createElement(Text5, { dimColor: true, color: "redBright" }, "Fetching Data")), /* @__PURE__ */ React7.createElement(Box5, { alignSelf: "center" }, /* @__PURE__ */ React7.createElement(Text5, { color: "blueBright", dimColor: true }, /* @__PURE__ */ React7.createElement(Spinner, { type: "grenade" }))));
877
+ }
878
+
879
+ // src/cli/ui/components/titlebar.tsx
880
+ import { Box as Box6, Spacer as Spacer2, Text as Text6 } from "ink";
881
+ import React8 from "react";
882
+
883
+ // package.json
884
+ var version = "3.4.2";
885
+
886
+ // src/cli/ui/components/titlebar.tsx
887
+ function TitleBar() {
888
+ return /* @__PURE__ */ React8.createElement(Box6, null, /* @__PURE__ */ React8.createElement(Spacer2, null), /* @__PURE__ */ React8.createElement(Box6, { borderColor: "magenta", borderStyle: "arrow" }, /* @__PURE__ */ React8.createElement(Text6, { color: "cyanBright" }, "Coomer-Downloader ", version)), /* @__PURE__ */ React8.createElement(Spacer2, null));
889
+ }
890
+
891
+ // src/cli/ui/hooks/input.ts
892
+ import { useInput } from "ink";
893
+ var useInputHook = () => {
894
+ const downloader = useInkStore((state) => state.downloader);
895
+ const switchPreview = useInkStore((state) => state.switchPreview);
896
+ useInput((input) => {
897
+ if (input === "s") {
898
+ downloader?.skip();
899
+ }
900
+ if (input === "p") {
901
+ switchPreview();
902
+ }
903
+ });
904
+ };
905
+
906
+ // src/cli/ui/app.tsx
907
+ function App() {
908
+ useInputHook();
909
+ const filelist = useDownloaderHook(["FILES_DOWNLOADING_START"]);
910
+ return /* @__PURE__ */ React9.createElement(Box7, { borderStyle: "single", flexDirection: "column", borderColor: "blue", width: 80 }, /* @__PURE__ */ React9.createElement(TitleBar, null), !(filelist instanceof CoomerFileList) ? /* @__PURE__ */ React9.createElement(Loading, null) : /* @__PURE__ */ React9.createElement(React9.Fragment, null, /* @__PURE__ */ React9.createElement(Box7, null, /* @__PURE__ */ React9.createElement(Box7, null, /* @__PURE__ */ React9.createElement(FileListStateBox, null)), /* @__PURE__ */ React9.createElement(Box7, { flexBasis: 30 }, /* @__PURE__ */ React9.createElement(KeyboardControlsInfo, null))), /* @__PURE__ */ React9.createElement(FileListBox, null)));
911
+ }
912
+
913
+ // src/cli/ui/index.tsx
914
+ function createReactInk() {
915
+ return render(/* @__PURE__ */ React10.createElement(App, null));
916
+ }
917
+
878
918
  // src/index.ts
879
919
  async function run() {
880
920
  createReactInk();
881
921
  const { url, dir, media, include, exclude, minSize, maxSize, skip, removeDupilicates } = argumentHander();
882
- const filelist = await apiHandler(url);
922
+ const filelist = await resolveAPI(url);
883
923
  filelist.setDirPath(dir).skip(skip).filterByText(include, exclude).filterByMediaType(media);
884
924
  if (removeDupilicates) {
885
925
  filelist.removeURLDuplicates();