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/README.md +1 -1
- package/dist/index.js +318 -278
- package/package.json +1 -1
- package/src/api/index.ts +6 -32
- package/src/api/provider.ts +7 -0
- package/src/api/{bunkr.ts → providers/bunkr.ts} +12 -5
- package/src/api/{coomer-api.ts → providers/coomer.ts} +49 -35
- package/src/api/{gofile.ts → providers/gofile.ts} +20 -12
- package/src/api/providers/plainfile.ts +17 -0
- package/src/api/{nsfw.xxx.ts → providers/reddit.ts} +18 -10
- package/src/api/resolver.ts +23 -0
- package/src/cli/ui/app.tsx +5 -7
- package/src/cli/ui/components/file.tsx +7 -3
- package/src/cli/ui/components/filelist-state.tsx +48 -0
- package/src/cli/ui/components/filelist.tsx +10 -46
- package/src/cli/ui/components/index.ts +2 -1
- package/src/cli/ui/components/preview.tsx +1 -1
- package/src/cli/ui/hooks/downloader.ts +4 -9
- package/src/cli/ui/hooks/input.ts +0 -1
- package/src/cli/ui/store/index.ts +1 -1
- package/src/{services → core}/downloader.ts +8 -6
- package/src/{services → core}/filelist.ts +2 -1
- package/src/core/index.ts +3 -0
- package/src/index.ts +3 -3
- package/src/api/plain-curl.ts +0 -10
- /package/src/{services → core}/file.ts +0 -0
- /package/src/{logger/index.ts → utils/logger.ts} +0 -0
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/
|
|
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/
|
|
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/
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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("
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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/
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
|
|
461
|
-
|
|
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
|
|
548
|
+
import React10 from "react";
|
|
513
549
|
|
|
514
550
|
// src/cli/ui/app.tsx
|
|
515
551
|
import { Box as Box7 } from "ink";
|
|
516
|
-
import
|
|
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/
|
|
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/
|
|
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 (
|
|
856
|
-
file.url =
|
|
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
|
|
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();
|