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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coomer-downloader",
3
- "version": "3.4.1",
3
+ "version": "3.4.2",
4
4
  "author": "smartacephal",
5
5
  "license": "MIT",
6
6
  "description": "Downloads images/videos from Coomer/Kemono, Bunkr, GoFile, Reddit-NSFW user posts",
package/src/api/index.ts CHANGED
@@ -1,32 +1,6 @@
1
- import type { CoomerFileList } from '../services/filelist';
2
- import { getBunkrData } from './bunkr';
3
- import { getCoomerData } from './coomer-api';
4
- import { getGofileData } from './gofile';
5
- import { getRedditData } from './nsfw.xxx';
6
- import { getPlainFileData } from './plain-curl';
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';
@@ -0,0 +1,7 @@
1
+ import type { CoomerFileList } from '../core/filelist';
2
+
3
+ export interface ProviderAPI {
4
+ fixURL?(url: string, retries: number): string;
5
+ testURL(url: URL): boolean;
6
+ getData(url: string): Promise<CoomerFileList>;
7
+ }
@@ -1,7 +1,8 @@
1
1
  import * as cheerio from 'cheerio';
2
2
  import { fetch } from 'undici';
3
- import { CoomerFile } from '../services/file';
4
- import { CoomerFileList } from '../services/filelist';
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 async function getBunkrData(url: string): Promise<CoomerFileList> {
68
- const filelist = await getGalleryFiles(url);
69
- return filelist;
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 '../services/file';
2
- import { CoomerFileList } from '../services/filelist';
3
- import { isImage } from '../utils/mediatypes';
4
- import { fetchWithGlobalHeader, setGlobalHeaders } from '../utils/requests';
5
-
6
- type CoomerAPIUser = { domain: string; service: string; id: string; name?: string };
7
- type CoomerAPIUserData = { name: string };
8
- type CoomerAPIFile = { path: string; name: string };
9
- type CoomerAPIPost = {
10
- title: string;
11
- content: string;
12
- published: string;
13
- attachments: CoomerAPIFile[];
14
- file: CoomerAPIFile;
15
- };
16
-
17
- const SERVERS = ['n1', 'n2', 'n3', 'n4'];
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 async function getCoomerData(url: string): Promise<CoomerFileList> {
105
- setGlobalHeaders({ accept: 'text/css' });
106
- const user = await parseUser(url);
107
- const filelist = await getUserFiles(user);
108
- filelist.dirName = `${user.name}-${user.service}`;
109
- return filelist;
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 '../services/file';
3
- import { CoomerFileList } from '../services/filelist';
4
- import { setGlobalHeaders } from '../utils/requests';
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
- throw new Error('cannot get token');
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 async function getGofileData(url: string): Promise<CoomerFileList> {
58
- const id = url.match(/gofile.io\/d\/(\w+)/)?.[1] as string;
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
- const token = await getToken();
61
- const websiteToken = await getWebsiteToken();
67
+ const token = await getToken();
68
+ const websiteToken = await getWebsiteToken();
62
69
 
63
- const filelist = await getFolderFiles(id, token, websiteToken);
64
- filelist.dirName = `gofile-${id}`;
70
+ const filelist = await getFolderFiles(id, token, websiteToken);
71
+ filelist.dirName = `gofile-${id}`;
65
72
 
66
- setGlobalHeaders({ Cookie: `accountToken=${token}` });
73
+ setGlobalHeaders({ Cookie: `accountToken=${token}` });
67
74
 
68
- return filelist;
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 '../services/file';
4
- import { CoomerFileList } from '../services/filelist';
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 async function getRedditData(url: string): Promise<CoomerFileList> {
55
- const user = url.match(/u\/(\w+)/)?.[1] as string;
56
- console.log('Fetching user posts...');
57
- const posts = await getUserPosts(user);
58
- console.log('Fetching posts data...');
59
- const filelist = await getPostsData(posts);
60
- filelist.dirName = `${user}-reddit`;
61
- return filelist;
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
+ }
@@ -1,8 +1,8 @@
1
1
  import { Box } from 'ink';
2
2
  import React from 'react';
3
- import { CoomerFileList } from '../../services/filelist';
3
+ import { CoomerFileList } from '../../core';
4
4
  import {
5
- FileBox,
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 filelist={filelist} />
27
+ <FileListStateBox />
28
28
  </Box>
29
29
  <Box flexBasis={30}>
30
30
  <KeyboardControlsInfo />
31
31
  </Box>
32
32
  </Box>
33
33
 
34
- {filelist?.getActiveFiles().map((file) => {
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 '../../../services/file';
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 function FileBox({ file }: FileBoxProps) {
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 { CoomerFileList } from '../../../services/file';
2
+ import type { CoomerFile } from '../../../core';
3
+ import { useDownloaderHook } from '../hooks/downloader';
4
+ import { FileBox } from './file';
4
5
 
5
- interface FileListStateBoxProps {
6
- filelist: CoomerFileList;
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
- <Box
12
- paddingX={1}
13
- flexDirection="column"
14
- borderStyle={'single'}
15
- borderColor={'magenta'}
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 { FileListStateBox } from './filelist';
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 '../../../services/file';
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
- const targets = [
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);
@@ -1,5 +1,5 @@
1
1
  import { create } from 'zustand';
2
- import type { Downloader } from '../../../services/downloader';
2
+ import type { Downloader } from '../../../core';
3
3
 
4
4
  interface InkState {
5
5
  preview: boolean;
@@ -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(file: CoomerFile, retries = this.fetchRetries): Promise<void> {
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 (/coomer|kemono/.test(file.url)) {
134
- file.url = tryFixCoomerUrl(file.url, retries);
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 logger from '../logger';
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
 
@@ -0,0 +1,3 @@
1
+ export { Downloader } from './downloader';
2
+ export { CoomerFile } from './file';
3
+ export { CoomerFileList } from './filelist';