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/package.json
CHANGED
package/src/api/index.ts
CHANGED
|
@@ -1,32 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export async function apiHandler(url_: string): Promise<CoomerFileList> {
|
|
9
|
-
const url = new URL(url_);
|
|
10
|
-
|
|
11
|
-
if (/^u\/\w+$/.test(url.origin)) {
|
|
12
|
-
return getRedditData(url.href);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
if (/coomer|kemono/.test(url.origin)) {
|
|
16
|
-
return getCoomerData(url.href);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (/bunkr/.test(url.origin)) {
|
|
20
|
-
return getBunkrData(url.href);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (/gofile\.io/.test(url.origin)) {
|
|
24
|
-
return getGofileData(url.href);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (/\.\w+/.test(url.pathname)) {
|
|
28
|
-
return getPlainFileData(url.href);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
throw Error('Invalid URL');
|
|
32
|
-
}
|
|
1
|
+
export { BunkrAPI } from './providers/bunkr';
|
|
2
|
+
export { CoomerAPI } from './providers/coomer';
|
|
3
|
+
export { GofileAPI } from './providers/gofile';
|
|
4
|
+
export { PlainFileAPI } from './providers/plainfile';
|
|
5
|
+
export { RedditAPI } from './providers/reddit';
|
|
6
|
+
export { resolveAPI } from './resolver';
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as cheerio from 'cheerio';
|
|
2
2
|
import { fetch } from 'undici';
|
|
3
|
-
import { CoomerFile } from '
|
|
4
|
-
import { CoomerFileList } from '
|
|
3
|
+
import { CoomerFile } from '../../core/file';
|
|
4
|
+
import { CoomerFileList } from '../../core/filelist';
|
|
5
|
+
import type { ProviderAPI } from '../provider';
|
|
5
6
|
|
|
6
7
|
type EncData = { url: string; timestamp: number };
|
|
7
8
|
|
|
@@ -64,7 +65,13 @@ async function getGalleryFiles(url: string): Promise<CoomerFileList> {
|
|
|
64
65
|
return filelist;
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
export
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
export class BunkrAPI implements ProviderAPI {
|
|
69
|
+
public testURL(url: URL) {
|
|
70
|
+
return /bunkr/.test(url.origin);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public async getData(url: string): Promise<CoomerFileList> {
|
|
74
|
+
const filelist = await getGalleryFiles(url);
|
|
75
|
+
return filelist;
|
|
76
|
+
}
|
|
70
77
|
}
|
|
@@ -1,34 +1,27 @@
|
|
|
1
|
-
import { CoomerFile } from '
|
|
2
|
-
import { CoomerFileList } from '
|
|
3
|
-
import { isImage } from '
|
|
4
|
-
import { fetchWithGlobalHeader, setGlobalHeaders } from '
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
export function tryFixCoomerUrl(url: string, attempts: number) {
|
|
20
|
-
if (attempts < 2 && isImage(url)) {
|
|
21
|
-
return url.replace(/\/data\//, '/thumbnail/data/').replace(/n\d\./, 'img.');
|
|
22
|
-
}
|
|
23
|
-
const server = url.match(/n\d\./)?.[0].slice(0, 2) as string;
|
|
24
|
-
const i = SERVERS.indexOf(server);
|
|
25
|
-
if (i !== -1) {
|
|
26
|
-
const newServer = SERVERS[(i + 1) % SERVERS.length];
|
|
27
|
-
return url.replace(/n\d./, `${newServer}.`);
|
|
28
|
-
}
|
|
29
|
-
return url;
|
|
1
|
+
import { CoomerFile } from '../../core/file';
|
|
2
|
+
import { CoomerFileList } from '../../core/filelist';
|
|
3
|
+
import { isImage } from '../../utils/mediatypes';
|
|
4
|
+
import { fetchWithGlobalHeader, setGlobalHeaders } from '../../utils/requests';
|
|
5
|
+
import type { ProviderAPI } from '../provider';
|
|
6
|
+
|
|
7
|
+
interface CoomerServiceAPI {
|
|
8
|
+
user: { domain: string; service: string; id: string; name?: string };
|
|
9
|
+
userData: { name: string };
|
|
10
|
+
file: { path: string; name: string };
|
|
11
|
+
post: {
|
|
12
|
+
title: string;
|
|
13
|
+
content: string;
|
|
14
|
+
published: string;
|
|
15
|
+
attachments: CoomerServiceAPI['file'][];
|
|
16
|
+
file: CoomerServiceAPI['file'];
|
|
17
|
+
};
|
|
30
18
|
}
|
|
31
19
|
|
|
20
|
+
type CoomerAPIUser = CoomerServiceAPI['user'];
|
|
21
|
+
type CoomerAPIUserData = CoomerServiceAPI['userData'];
|
|
22
|
+
type CoomerAPIFile = CoomerServiceAPI['file'];
|
|
23
|
+
type CoomerAPIPost = CoomerServiceAPI['post'];
|
|
24
|
+
|
|
32
25
|
async function getUserProfileData(user: CoomerAPIUser): Promise<CoomerAPIUserData> {
|
|
33
26
|
const url = `${user.domain}/api/v1/${user.service}/user/${user.id}/profile`;
|
|
34
27
|
const result = await fetchWithGlobalHeader(url).then((r) => r.json());
|
|
@@ -101,10 +94,31 @@ async function parseUser(url: string): Promise<CoomerAPIUser> {
|
|
|
101
94
|
return { domain, service, id, name };
|
|
102
95
|
}
|
|
103
96
|
|
|
104
|
-
export
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
97
|
+
export class CoomerAPI implements ProviderAPI {
|
|
98
|
+
private static readonly SERVERS = ['n1', 'n2', 'n3', 'n4'];
|
|
99
|
+
|
|
100
|
+
public fixURL(url: string, retries: number): string {
|
|
101
|
+
if (retries < 2 && isImage(url)) {
|
|
102
|
+
return url.replace(/\/data\//, '/thumbnail/data/').replace(/n\d\./, 'img.');
|
|
103
|
+
}
|
|
104
|
+
const server = url.match(/n\d\./)?.[0].slice(0, 2) as string;
|
|
105
|
+
const i = CoomerAPI.SERVERS.indexOf(server);
|
|
106
|
+
if (i !== -1) {
|
|
107
|
+
const newServer = CoomerAPI.SERVERS[(i + 1) % CoomerAPI.SERVERS.length];
|
|
108
|
+
return url.replace(/n\d./, `${newServer}.`);
|
|
109
|
+
}
|
|
110
|
+
return url;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public testURL(url: URL) {
|
|
114
|
+
return /coomer|kemono/.test(url.origin);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public async getData(url: string): Promise<CoomerFileList> {
|
|
118
|
+
setGlobalHeaders({ accept: 'text/css' });
|
|
119
|
+
const user = await parseUser(url);
|
|
120
|
+
const filelist = await getUserFiles(user);
|
|
121
|
+
filelist.dirName = `${user.name}-${user.service}`;
|
|
122
|
+
return filelist;
|
|
123
|
+
}
|
|
110
124
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { fetch } from 'undici';
|
|
2
|
-
import { CoomerFile } from '
|
|
3
|
-
import { CoomerFileList } from '
|
|
4
|
-
import { setGlobalHeaders } from '
|
|
2
|
+
import { CoomerFile } from '../../core/file';
|
|
3
|
+
import { CoomerFileList } from '../../core/filelist';
|
|
4
|
+
import { setGlobalHeaders } from '../../utils/requests';
|
|
5
|
+
import type { ProviderAPI } from '../provider';
|
|
5
6
|
|
|
6
7
|
type GoFileAPIToken = { status: string; data: { token: string } };
|
|
7
8
|
type GoFileAPIFilelist = { data: { children: { link: string; name: string }[] } };
|
|
@@ -14,7 +15,8 @@ async function getToken(): Promise<string> {
|
|
|
14
15
|
if (data.status === 'ok') {
|
|
15
16
|
return data.data.token;
|
|
16
17
|
}
|
|
17
|
-
|
|
18
|
+
|
|
19
|
+
throw new Error('Token Not Found');
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
async function getWebsiteToken() {
|
|
@@ -54,16 +56,22 @@ async function getFolderFiles(
|
|
|
54
56
|
return new CoomerFileList(files);
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
export
|
|
58
|
-
|
|
59
|
+
export class GofileAPI implements ProviderAPI {
|
|
60
|
+
public testURL(url: URL) {
|
|
61
|
+
return /gofile\.io/.test(url.origin);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public async getData(url: string): Promise<CoomerFileList> {
|
|
65
|
+
const id = url.match(/gofile.io\/d\/(\w+)/)?.[1] as string;
|
|
59
66
|
|
|
60
|
-
|
|
61
|
-
|
|
67
|
+
const token = await getToken();
|
|
68
|
+
const websiteToken = await getWebsiteToken();
|
|
62
69
|
|
|
63
|
-
|
|
64
|
-
|
|
70
|
+
const filelist = await getFolderFiles(id, token, websiteToken);
|
|
71
|
+
filelist.dirName = `gofile-${id}`;
|
|
65
72
|
|
|
66
|
-
|
|
73
|
+
setGlobalHeaders({ Cookie: `accountToken=${token}` });
|
|
67
74
|
|
|
68
|
-
|
|
75
|
+
return filelist;
|
|
76
|
+
}
|
|
69
77
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { CoomerFile } from '../../core/file';
|
|
2
|
+
import { CoomerFileList } from '../../core/filelist';
|
|
3
|
+
import type { ProviderAPI } from '../provider';
|
|
4
|
+
|
|
5
|
+
export class PlainFileAPI implements ProviderAPI {
|
|
6
|
+
public testURL(url: URL) {
|
|
7
|
+
return /\.\w+/.test(url.pathname);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
public async getData(url: string): Promise<CoomerFileList> {
|
|
11
|
+
const name = url.split('/').pop() as string;
|
|
12
|
+
const file = CoomerFile.from({ name, url });
|
|
13
|
+
const filelist = new CoomerFileList([file]);
|
|
14
|
+
filelist.dirName = '';
|
|
15
|
+
return filelist;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import * as cheerio from 'cheerio';
|
|
2
2
|
import { fetch } from 'undici';
|
|
3
|
-
import { CoomerFile } from '
|
|
4
|
-
import { CoomerFileList } from '
|
|
3
|
+
import { CoomerFile } from '../../core/file';
|
|
4
|
+
import { CoomerFileList } from '../../core/filelist';
|
|
5
|
+
import logger from '../../utils/logger';
|
|
6
|
+
import type { ProviderAPI } from '../provider';
|
|
5
7
|
|
|
6
8
|
async function getUserPage(user: string, offset: number) {
|
|
7
9
|
const url = `https://nsfw.xxx/page/${offset}?nsfw[]=0&types[]=image&types[]=video&types[]=gallery&slider=1&jsload=1&user=${user}&_=${Date.now()}`;
|
|
@@ -20,6 +22,7 @@ async function getUserPosts(user: string): Promise<string[]> {
|
|
|
20
22
|
.get()
|
|
21
23
|
.filter((href) => href?.startsWith('https://nsfw.xxx/post'));
|
|
22
24
|
|
|
25
|
+
logger.debug({ count: posts.length });
|
|
23
26
|
posts.push(...newPosts);
|
|
24
27
|
}
|
|
25
28
|
return posts;
|
|
@@ -45,18 +48,23 @@ async function getPostsData(posts: string[]): Promise<CoomerFileList> {
|
|
|
45
48
|
const ext = src.split('.').pop();
|
|
46
49
|
const name = `${slug}-${date}.${ext}`;
|
|
47
50
|
|
|
51
|
+
logger.debug({ hehe: filelist.files.length, src });
|
|
48
52
|
filelist.files.push(CoomerFile.from({ name, url: src }));
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
return filelist;
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
export
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
export class RedditAPI implements ProviderAPI {
|
|
59
|
+
public testURL(url: URL) {
|
|
60
|
+
return /^\/user\/[\w-]+$/.test(url.pathname);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public async getData(url: string): Promise<CoomerFileList> {
|
|
64
|
+
const user = url.match(/^\/user\/([\w-]+)/)?.[1] as string;
|
|
65
|
+
const posts = await getUserPosts(user);
|
|
66
|
+
const filelist = await getPostsData(posts);
|
|
67
|
+
filelist.dirName = `${user}-reddit`;
|
|
68
|
+
return filelist;
|
|
69
|
+
}
|
|
62
70
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { CoomerFileList } from '../core/filelist';
|
|
2
|
+
import { BunkrAPI } from './providers/bunkr';
|
|
3
|
+
import { CoomerAPI } from './providers/coomer';
|
|
4
|
+
import { GofileAPI } from './providers/gofile';
|
|
5
|
+
import { PlainFileAPI } from './providers/plainfile';
|
|
6
|
+
import { RedditAPI } from './providers/reddit';
|
|
7
|
+
|
|
8
|
+
const providers = [RedditAPI, CoomerAPI, BunkrAPI, GofileAPI, PlainFileAPI];
|
|
9
|
+
|
|
10
|
+
export async function resolveAPI(url_: string): Promise<CoomerFileList> {
|
|
11
|
+
const url = new URL(url_);
|
|
12
|
+
|
|
13
|
+
for (const p of providers) {
|
|
14
|
+
const provider = new p();
|
|
15
|
+
if (provider.testURL(url)) {
|
|
16
|
+
const filelist = await provider.getData(url.toString());
|
|
17
|
+
filelist.provider = provider;
|
|
18
|
+
return filelist;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
throw Error('Invalid URL');
|
|
23
|
+
}
|
package/src/cli/ui/app.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Box } from 'ink';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { CoomerFileList } from '../../
|
|
3
|
+
import { CoomerFileList } from '../../core';
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
FileListBox,
|
|
6
6
|
FileListStateBox,
|
|
7
7
|
KeyboardControlsInfo,
|
|
8
8
|
Loading,
|
|
@@ -13,7 +13,7 @@ import { useInputHook } from './hooks/input';
|
|
|
13
13
|
|
|
14
14
|
export function App() {
|
|
15
15
|
useInputHook();
|
|
16
|
-
const filelist = useDownloaderHook();
|
|
16
|
+
const filelist = useDownloaderHook(['FILES_DOWNLOADING_START']);
|
|
17
17
|
|
|
18
18
|
return (
|
|
19
19
|
<Box borderStyle="single" flexDirection="column" borderColor="blue" width={80}>
|
|
@@ -24,16 +24,14 @@ export function App() {
|
|
|
24
24
|
<>
|
|
25
25
|
<Box>
|
|
26
26
|
<Box>
|
|
27
|
-
<FileListStateBox
|
|
27
|
+
<FileListStateBox />
|
|
28
28
|
</Box>
|
|
29
29
|
<Box flexBasis={30}>
|
|
30
30
|
<KeyboardControlsInfo />
|
|
31
31
|
</Box>
|
|
32
32
|
</Box>
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
return <FileBox file={file} key={file.name} />;
|
|
36
|
-
})}
|
|
34
|
+
<FileListBox />
|
|
37
35
|
</>
|
|
38
36
|
)}
|
|
39
37
|
</Box>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Box, Spacer, Text } from 'ink';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import type { CoomerFile } from '../../../
|
|
3
|
+
import type { CoomerFile } from '../../../core';
|
|
4
4
|
import { b2mb } from '../../../utils/strings';
|
|
5
|
+
import { useDownloaderHook } from '../hooks/downloader';
|
|
5
6
|
import { Preview } from './preview';
|
|
6
7
|
import { Spinner } from './spinner';
|
|
7
8
|
|
|
@@ -9,8 +10,11 @@ interface FileBoxProps {
|
|
|
9
10
|
file: CoomerFile;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
export
|
|
13
|
+
export const FileBox = React.memo(({ file }: FileBoxProps) => {
|
|
14
|
+
useDownloaderHook(['CHUNK_DOWNLOADING_UPDATE']);
|
|
15
|
+
|
|
13
16
|
const percentage = Number((file.downloaded / (file.size as number)) * 100).toFixed(2);
|
|
17
|
+
|
|
14
18
|
return (
|
|
15
19
|
<>
|
|
16
20
|
<Box
|
|
@@ -41,4 +45,4 @@ export function FileBox({ file }: FileBoxProps) {
|
|
|
41
45
|
<Preview file={file} />
|
|
42
46
|
</>
|
|
43
47
|
);
|
|
44
|
-
}
|
|
48
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Box, Text } from 'ink';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { useDownloaderHook } from '../hooks/downloader';
|
|
4
|
+
|
|
5
|
+
export function FileListStateBox() {
|
|
6
|
+
const filelist = useDownloaderHook(['FILE_DOWNLOADING_START', 'FILE_DOWNLOADING_END']);
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<Box
|
|
10
|
+
paddingX={1}
|
|
11
|
+
flexDirection="column"
|
|
12
|
+
borderStyle={'single'}
|
|
13
|
+
borderColor={'magenta'}
|
|
14
|
+
borderDimColor
|
|
15
|
+
>
|
|
16
|
+
<Box>
|
|
17
|
+
<Box marginRight={1}>
|
|
18
|
+
<Text color="cyanBright" dimColor>
|
|
19
|
+
Found:
|
|
20
|
+
</Text>
|
|
21
|
+
</Box>
|
|
22
|
+
<Text color="blue" dimColor wrap="wrap">
|
|
23
|
+
{filelist?.files.length}
|
|
24
|
+
</Text>
|
|
25
|
+
</Box>
|
|
26
|
+
<Box>
|
|
27
|
+
<Box marginRight={1}>
|
|
28
|
+
<Text color="cyanBright" dimColor>
|
|
29
|
+
Downloaded:
|
|
30
|
+
</Text>
|
|
31
|
+
</Box>
|
|
32
|
+
<Text color="blue" dimColor wrap="wrap"></Text>
|
|
33
|
+
</Box>
|
|
34
|
+
<Box>
|
|
35
|
+
<Box width={9}>
|
|
36
|
+
<Text color="cyanBright" dimColor>
|
|
37
|
+
Folder:
|
|
38
|
+
</Text>
|
|
39
|
+
</Box>
|
|
40
|
+
<Box flexGrow={1}>
|
|
41
|
+
<Text color="blue" dimColor wrap="truncate-middle">
|
|
42
|
+
{filelist?.dirPath}
|
|
43
|
+
</Text>
|
|
44
|
+
</Box>
|
|
45
|
+
</Box>
|
|
46
|
+
</Box>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -1,52 +1,16 @@
|
|
|
1
|
-
import { Box, Text } from 'ink';
|
|
2
1
|
import React from 'react';
|
|
3
|
-
import type {
|
|
2
|
+
import type { CoomerFile } from '../../../core';
|
|
3
|
+
import { useDownloaderHook } from '../hooks/downloader';
|
|
4
|
+
import { FileBox } from './file';
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
filelist
|
|
7
|
-
}
|
|
6
|
+
export function FileListBox() {
|
|
7
|
+
const filelist = useDownloaderHook(['FILE_DOWNLOADING_START', 'FILE_DOWNLOADING_END']);
|
|
8
8
|
|
|
9
|
-
export function FileListStateBox({ filelist }: FileListStateBoxProps) {
|
|
10
9
|
return (
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
borderDimColor
|
|
17
|
-
>
|
|
18
|
-
<Box>
|
|
19
|
-
<Box marginRight={1}>
|
|
20
|
-
<Text color="cyanBright" dimColor>
|
|
21
|
-
Found:
|
|
22
|
-
</Text>
|
|
23
|
-
</Box>
|
|
24
|
-
<Text color="blue" dimColor wrap="wrap">
|
|
25
|
-
{filelist.files.length}
|
|
26
|
-
</Text>
|
|
27
|
-
</Box>
|
|
28
|
-
<Box>
|
|
29
|
-
<Box marginRight={1}>
|
|
30
|
-
<Text color="cyanBright" dimColor>
|
|
31
|
-
Downloaded:
|
|
32
|
-
</Text>
|
|
33
|
-
</Box>
|
|
34
|
-
<Text color="blue" dimColor wrap="wrap">
|
|
35
|
-
{filelist.getDownloaded().length}
|
|
36
|
-
</Text>
|
|
37
|
-
</Box>
|
|
38
|
-
<Box>
|
|
39
|
-
<Box width={9}>
|
|
40
|
-
<Text color="cyanBright" dimColor>
|
|
41
|
-
Folder:
|
|
42
|
-
</Text>
|
|
43
|
-
</Box>
|
|
44
|
-
<Box flexGrow={1}>
|
|
45
|
-
<Text color="blue" dimColor wrap="truncate-middle">
|
|
46
|
-
{filelist.dirPath}
|
|
47
|
-
</Text>
|
|
48
|
-
</Box>
|
|
49
|
-
</Box>
|
|
50
|
-
</Box>
|
|
10
|
+
<>
|
|
11
|
+
{filelist?.getActiveFiles().map((file: CoomerFile) => {
|
|
12
|
+
return <FileBox file={file} key={file.name} />;
|
|
13
|
+
})}
|
|
14
|
+
</>
|
|
51
15
|
);
|
|
52
16
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { FileBox } from './file';
|
|
2
|
-
export {
|
|
2
|
+
export { FileListBox } from './filelist';
|
|
3
|
+
export { FileListStateBox } from './filelist-state';
|
|
3
4
|
export { KeyboardControlsInfo } from './keyboardinfo';
|
|
4
5
|
export { Loading } from './loading';
|
|
5
6
|
export { Spinner } from './spinner';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Box } from 'ink';
|
|
2
2
|
import Image, { TerminalInfoProvider } from 'ink-picture';
|
|
3
3
|
import React from 'react';
|
|
4
|
-
import type { CoomerFile } from '../../../
|
|
4
|
+
import type { CoomerFile } from '../../../core';
|
|
5
5
|
import { isImage } from '../../../utils/mediatypes';
|
|
6
6
|
import { useInkStore } from '../store';
|
|
7
7
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useRef, useSyncExternalStore } from 'react';
|
|
2
|
+
import type { DownloaderSubject, DownloaderSubjectSignal } from '../../../types';
|
|
2
3
|
import { useInkStore } from '../store';
|
|
3
4
|
|
|
4
|
-
export const useDownloaderHook = () => {
|
|
5
|
+
export const useDownloaderHook = (subjectEvents: DownloaderSubjectSignal[]) => {
|
|
5
6
|
const downloader = useInkStore((state) => state.downloader);
|
|
6
7
|
|
|
7
8
|
const versionRef = useRef(0);
|
|
@@ -10,14 +11,8 @@ export const useDownloaderHook = () => {
|
|
|
10
11
|
(onStoreChange) => {
|
|
11
12
|
if (!downloader) return () => {};
|
|
12
13
|
|
|
13
|
-
const sub = downloader.subject.subscribe(({ type }) => {
|
|
14
|
-
|
|
15
|
-
'FILE_DOWNLOADING_START',
|
|
16
|
-
'FILE_DOWNLOADING_END',
|
|
17
|
-
'CHUNK_DOWNLOADING_UPDATE',
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
if (targets.includes(type)) {
|
|
14
|
+
const sub = downloader.subject.subscribe(({ type }: DownloaderSubject) => {
|
|
15
|
+
if (subjectEvents.includes(type)) {
|
|
21
16
|
versionRef.current++;
|
|
22
17
|
onStoreChange();
|
|
23
18
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { useInput } from 'ink';
|
|
2
2
|
import { useInkStore } from '../store';
|
|
3
3
|
|
|
4
|
-
// problems with tsx watch
|
|
5
4
|
export const useInputHook = () => {
|
|
6
5
|
const downloader = useInkStore((state) => state.downloader);
|
|
7
6
|
const switchPreview = useInkStore((state) => state.switchPreview);
|
|
@@ -2,7 +2,6 @@ import fs from 'node:fs';
|
|
|
2
2
|
import { Readable, Transform } from 'node:stream';
|
|
3
3
|
import { pipeline } from 'node:stream/promises';
|
|
4
4
|
import { Subject } from 'rxjs';
|
|
5
|
-
import { tryFixCoomerUrl } from '../api/coomer-api';
|
|
6
5
|
import type { AbortControllerSubject, DownloaderSubject } from '../types';
|
|
7
6
|
import { deleteFile, getFileSize, mkdir } from '../utils/io';
|
|
8
7
|
import { sleep } from '../utils/promise';
|
|
@@ -35,7 +34,7 @@ export class Downloader {
|
|
|
35
34
|
this.setAbortControllerListener();
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
async fetchStream(
|
|
37
|
+
private async fetchStream(
|
|
39
38
|
file: CoomerFile,
|
|
40
39
|
stream: Readable,
|
|
41
40
|
sizeOld = 0,
|
|
@@ -100,7 +99,10 @@ export class Downloader {
|
|
|
100
99
|
}
|
|
101
100
|
}
|
|
102
101
|
|
|
103
|
-
async downloadFile(
|
|
102
|
+
public async downloadFile(
|
|
103
|
+
file: CoomerFile,
|
|
104
|
+
retries = this.fetchRetries,
|
|
105
|
+
): Promise<void> {
|
|
104
106
|
const signal = this.abortController.signal;
|
|
105
107
|
try {
|
|
106
108
|
file.downloaded = await getFileSize(file.filepath as string);
|
|
@@ -130,8 +132,8 @@ export class Downloader {
|
|
|
130
132
|
if (signal.reason === 'FILE_SKIP') return;
|
|
131
133
|
}
|
|
132
134
|
if (retries > 0) {
|
|
133
|
-
if (
|
|
134
|
-
file.url =
|
|
135
|
+
if (this.filelist.provider?.fixURL) {
|
|
136
|
+
file.url = this.filelist.provider.fixURL(file.url, retries);
|
|
135
137
|
}
|
|
136
138
|
await sleep(1000);
|
|
137
139
|
return await this.downloadFile(file, retries - 1);
|
|
@@ -140,7 +142,7 @@ export class Downloader {
|
|
|
140
142
|
}
|
|
141
143
|
}
|
|
142
144
|
|
|
143
|
-
async downloadFiles(): Promise<void> {
|
|
145
|
+
public async downloadFiles(): Promise<void> {
|
|
144
146
|
mkdir(this.filelist.dirPath as string);
|
|
145
147
|
|
|
146
148
|
this.subject.next({ type: 'FILES_DOWNLOADING_START' });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import
|
|
3
|
+
import type { ProviderAPI } from '../api/provider';
|
|
4
4
|
import type { MediaType } from '../types';
|
|
5
5
|
import { collectUniquesAndDuplicatesBy, removeDuplicatesBy } from '../utils/duplicates';
|
|
6
6
|
import { filterString } from '../utils/filters';
|
|
@@ -11,6 +11,7 @@ import type { CoomerFile } from './file';
|
|
|
11
11
|
export class CoomerFileList {
|
|
12
12
|
public dirPath?: string;
|
|
13
13
|
public dirName?: string;
|
|
14
|
+
public provider?: ProviderAPI;
|
|
14
15
|
|
|
15
16
|
constructor(public files: CoomerFile[] = []) {}
|
|
16
17
|
|