coomer-downloader 3.2.0 → 3.4.0
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 +14 -3
- package/biome.json +6 -4
- package/dist/index.js +539 -285
- package/docs/images/Screenshot 01.jpg +0 -0
- package/package.json +14 -5
- package/src/api/bunkr.ts +1 -1
- package/src/api/coomer-api.ts +23 -6
- package/src/api/gofile.ts +2 -2
- package/src/api/index.ts +5 -1
- package/src/api/nsfw.xxx.ts +3 -3
- package/src/api/plain-curl.ts +1 -1
- package/src/{args-handler.ts → cli/args-handler.ts} +17 -12
- package/src/cli/ui/app.tsx +40 -0
- package/src/cli/ui/components/file.tsx +44 -0
- package/src/cli/ui/components/filelist.tsx +52 -0
- package/src/cli/ui/components/index.ts +6 -0
- package/src/cli/ui/components/keyboardinfo.tsx +41 -0
- package/src/cli/ui/components/loading.tsx +20 -0
- package/src/cli/ui/components/preview.tsx +32 -0
- package/src/cli/ui/components/spinner.tsx +28 -0
- package/src/cli/ui/components/titlebar.tsx +15 -0
- package/src/cli/ui/hooks/downloader.ts +21 -0
- package/src/cli/ui/hooks/input.ts +17 -0
- package/src/cli/ui/index.tsx +7 -0
- package/src/cli/ui/store/index.ts +19 -0
- package/src/index.ts +42 -23
- package/src/logger/index.ts +15 -0
- package/src/services/downloader.ts +161 -0
- package/src/services/file.ts +113 -0
- package/src/types/index.ts +16 -1
- package/src/utils/duplicates.ts +23 -0
- package/src/utils/filters.ts +15 -15
- package/src/utils/io.ts +25 -0
- package/src/utils/mediatypes.ts +13 -0
- package/src/utils/promise.ts +0 -50
- package/src/utils/requests.ts +2 -2
- package/src/utils/strings.ts +1 -10
- package/src/utils/timer.ts +11 -9
- package/tsconfig.json +2 -1
- package/src/utils/downloader.ts +0 -108
- package/src/utils/file.ts +0 -75
- package/src/utils/index.ts +0 -11
- package/src/utils/multibar.ts +0 -62
package/src/utils/downloader.ts
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import { Readable, Transform } from 'node:stream';
|
|
3
|
-
import { pipeline } from 'node:stream/promises';
|
|
4
|
-
import { Subject } from 'rxjs';
|
|
5
|
-
import { tryFixCoomerUrl } from '../api/coomer-api';
|
|
6
|
-
import type { CoomerFile, CoomerFileList } from './file';
|
|
7
|
-
import { getFileSize, mkdir } from './io';
|
|
8
|
-
import { PromiseRetry } from './promise';
|
|
9
|
-
import { fetchByteRange } from './requests';
|
|
10
|
-
import { Timer } from './timer';
|
|
11
|
-
|
|
12
|
-
type DownloaderSubject = {
|
|
13
|
-
type: string;
|
|
14
|
-
file?: CoomerFile;
|
|
15
|
-
filesCount?: number;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export class Downloader {
|
|
19
|
-
public subject = new Subject<DownloaderSubject>();
|
|
20
|
-
|
|
21
|
-
constructor(
|
|
22
|
-
public chunkTimeout = 30_000,
|
|
23
|
-
public chunkFetchRetries = 5,
|
|
24
|
-
public fetchRetries = 7,
|
|
25
|
-
) {}
|
|
26
|
-
|
|
27
|
-
async fetchStream(file: CoomerFile, stream: Readable): Promise<void> {
|
|
28
|
-
const { subject, chunkTimeout } = this;
|
|
29
|
-
const { timer, signal } = Timer.withSignal(chunkTimeout, 'chunkTimeout');
|
|
30
|
-
|
|
31
|
-
const fileStream = fs.createWriteStream(file.filepath as string, { flags: 'a' });
|
|
32
|
-
|
|
33
|
-
const progressStream = new Transform({
|
|
34
|
-
transform(chunk, _encoding, callback) {
|
|
35
|
-
this.push(chunk);
|
|
36
|
-
file.downloaded += chunk.length;
|
|
37
|
-
timer.reset();
|
|
38
|
-
subject.next({ type: 'CHUNK_DOWNLOADING_UPDATE', file });
|
|
39
|
-
callback();
|
|
40
|
-
},
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
subject.next({ type: 'CHUNK_DOWNLOADING_START', file });
|
|
45
|
-
await pipeline(stream, progressStream, fileStream, { signal });
|
|
46
|
-
} catch (error) {
|
|
47
|
-
console.error((error as Error).name === 'AbortError' ? signal.reason : error);
|
|
48
|
-
} finally {
|
|
49
|
-
subject.next({ type: 'CHUNK_DOWNLOADING_END', file });
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async downloadFile(file: CoomerFile): Promise<void> {
|
|
54
|
-
file.downloaded = await getFileSize(file.filepath as string);
|
|
55
|
-
|
|
56
|
-
const response = await fetchByteRange(file.url, file.downloaded);
|
|
57
|
-
|
|
58
|
-
if (!response?.ok && response?.status !== 416) {
|
|
59
|
-
throw new Error(`HTTP error! status: ${response?.status}`);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const contentLength = response.headers.get('Content-Length') as string;
|
|
63
|
-
|
|
64
|
-
if (!contentLength && file.downloaded > 0) return;
|
|
65
|
-
|
|
66
|
-
const restFileSize = parseInt(contentLength);
|
|
67
|
-
file.size = restFileSize + file.downloaded;
|
|
68
|
-
|
|
69
|
-
if (file.size > file.downloaded && response.body) {
|
|
70
|
-
const stream = Readable.fromWeb(response.body);
|
|
71
|
-
const sizeOld = file.downloaded;
|
|
72
|
-
|
|
73
|
-
await PromiseRetry.create({
|
|
74
|
-
retries: this.chunkFetchRetries,
|
|
75
|
-
callback: () => {
|
|
76
|
-
if (sizeOld !== file.downloaded) {
|
|
77
|
-
return { newRetries: 5 };
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
|
-
}).execute(async () => await this.fetchStream(file, stream));
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
this.subject.next({ type: 'FILE_DOWNLOADING_END' });
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async downloadFiles(filelist: CoomerFileList): Promise<void> {
|
|
87
|
-
mkdir(filelist.dirPath as string);
|
|
88
|
-
|
|
89
|
-
this.subject.next({ type: 'FILES_DOWNLOADING_START', filesCount: filelist.files.length });
|
|
90
|
-
|
|
91
|
-
for (const file of filelist.files) {
|
|
92
|
-
this.subject.next({ type: 'FILE_DOWNLOADING_START' });
|
|
93
|
-
|
|
94
|
-
await PromiseRetry.create({
|
|
95
|
-
retries: this.fetchRetries,
|
|
96
|
-
callback: (retries) => {
|
|
97
|
-
if (/coomer|kemono/.test(file.url)) {
|
|
98
|
-
file.url = tryFixCoomerUrl(file.url, retries);
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
}).execute(async () => await this.downloadFile(file));
|
|
102
|
-
|
|
103
|
-
this.subject.next({ type: 'FILE_DOWNLOADING_END' });
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
this.subject.next({ type: 'FILES_DOWNLOADING_END' });
|
|
107
|
-
}
|
|
108
|
-
}
|
package/src/utils/file.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import os from 'node:os';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import type { MediaType } from '../types';
|
|
4
|
-
import { filterString, testMediaType } from './filters';
|
|
5
|
-
|
|
6
|
-
interface ICoomerFile {
|
|
7
|
-
name: string;
|
|
8
|
-
url: string;
|
|
9
|
-
filepath?: string;
|
|
10
|
-
size?: number;
|
|
11
|
-
downloaded?: number;
|
|
12
|
-
content?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export class CoomerFile {
|
|
16
|
-
public state: 'downloading' | 'pause' = 'pause';
|
|
17
|
-
|
|
18
|
-
constructor(
|
|
19
|
-
public name: string,
|
|
20
|
-
public url: string,
|
|
21
|
-
public filepath?: string,
|
|
22
|
-
public size?: number,
|
|
23
|
-
public downloaded?: number,
|
|
24
|
-
public content?: string,
|
|
25
|
-
) {}
|
|
26
|
-
|
|
27
|
-
get textContent() {
|
|
28
|
-
const text = `${this.name || ''} ${this.content || ''}`.toLowerCase();
|
|
29
|
-
return text;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
public static from(f: ICoomerFile) {
|
|
33
|
-
return new CoomerFile(f.name, f.url, f.filepath, f.size, f.downloaded, f.content);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export class CoomerFileList {
|
|
38
|
-
public dirPath?: string;
|
|
39
|
-
public dirName?: string;
|
|
40
|
-
|
|
41
|
-
constructor(public files: CoomerFile[] = []) {}
|
|
42
|
-
|
|
43
|
-
public setDirPath(dir: string, dirName?: string) {
|
|
44
|
-
dirName = dirName || (this.dirName as string);
|
|
45
|
-
|
|
46
|
-
if (dir === './') {
|
|
47
|
-
this.dirPath = path.resolve(dir, dirName);
|
|
48
|
-
} else {
|
|
49
|
-
this.dirPath = path.join(os.homedir(), path.join(dir, dirName));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
this.files.forEach((file) => {
|
|
53
|
-
file.filepath = path.join(this.dirPath as string, file.name);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
return this;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
public filterByText(include: string, exclude: string) {
|
|
60
|
-
this.files = this.files.filter((f) => filterString(f.textContent, include, exclude));
|
|
61
|
-
return this;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
public filterByMediaType(media?: string) {
|
|
65
|
-
if (media) {
|
|
66
|
-
this.files = this.files.filter((f) => testMediaType(f.name, media as MediaType));
|
|
67
|
-
}
|
|
68
|
-
return this;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
public skip(n: number) {
|
|
72
|
-
this.files = this.files.slice(n);
|
|
73
|
-
return this;
|
|
74
|
-
}
|
|
75
|
-
}
|
package/src/utils/index.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export { Downloader } from './downloader';
|
|
2
|
-
export { isImage, isVideo, testMediaType } from './filters';
|
|
3
|
-
export { getFileSize, mkdir } from './io';
|
|
4
|
-
export { createMultibar } from './multibar';
|
|
5
|
-
export {
|
|
6
|
-
fetchByteRange,
|
|
7
|
-
fetchWithGlobalHeader,
|
|
8
|
-
HeadersDefault,
|
|
9
|
-
setGlobalHeaders,
|
|
10
|
-
} from './requests';
|
|
11
|
-
export { b2mb } from './strings';
|
package/src/utils/multibar.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { MultiBar, type Options, type SingleBar } from 'cli-progress';
|
|
2
|
-
import type { Downloader } from './downloader';
|
|
3
|
-
import { b2mb, formatNameStdout } from './strings';
|
|
4
|
-
|
|
5
|
-
const config: Options = {
|
|
6
|
-
clearOnComplete: true,
|
|
7
|
-
gracefulExit: true,
|
|
8
|
-
autopadding: true,
|
|
9
|
-
hideCursor: true,
|
|
10
|
-
format: '{percentage}% | {filename} | {value}/{total}{size}',
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export function createMultibar(downloader: Downloader) {
|
|
14
|
-
const multibar = new MultiBar(config);
|
|
15
|
-
let bar: SingleBar;
|
|
16
|
-
let minibar: SingleBar;
|
|
17
|
-
let filename: string;
|
|
18
|
-
let index = 0;
|
|
19
|
-
|
|
20
|
-
downloader.subject.subscribe({
|
|
21
|
-
next: ({ type, filesCount, file }) => {
|
|
22
|
-
switch (type) {
|
|
23
|
-
case 'FILES_DOWNLOADING_START':
|
|
24
|
-
bar?.stop();
|
|
25
|
-
bar = multibar.create(filesCount as number, 0);
|
|
26
|
-
break;
|
|
27
|
-
|
|
28
|
-
case 'FILES_DOWNLOADING_END':
|
|
29
|
-
bar?.stop();
|
|
30
|
-
break;
|
|
31
|
-
|
|
32
|
-
case 'FILE_DOWNLOADING_START':
|
|
33
|
-
bar?.update(++index, { filename: 'Downloaded files', size: '' });
|
|
34
|
-
break;
|
|
35
|
-
|
|
36
|
-
case 'FILE_DOWNLOADING_END':
|
|
37
|
-
multibar.remove(minibar);
|
|
38
|
-
break;
|
|
39
|
-
|
|
40
|
-
case 'CHUNK_DOWNLOADING_START':
|
|
41
|
-
multibar?.remove(minibar);
|
|
42
|
-
filename = formatNameStdout(file?.filepath as string);
|
|
43
|
-
minibar = multibar.create(b2mb(file?.size as number), b2mb(file?.downloaded as number));
|
|
44
|
-
break;
|
|
45
|
-
|
|
46
|
-
case 'CHUNK_DOWNLOADING_UPDATE':
|
|
47
|
-
minibar?.update(b2mb(file?.downloaded as number), {
|
|
48
|
-
filename: filename as string,
|
|
49
|
-
size: 'mb',
|
|
50
|
-
});
|
|
51
|
-
break;
|
|
52
|
-
|
|
53
|
-
case 'CHUNK_DOWNLOADING_END':
|
|
54
|
-
multibar?.remove(minibar);
|
|
55
|
-
break;
|
|
56
|
-
|
|
57
|
-
default:
|
|
58
|
-
break;
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
}
|