coomer-downloader 3.0.10 → 3.1.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/dist/index.js CHANGED
@@ -32,12 +32,12 @@ function tryFixCoomerUrl(url, attempts) {
32
32
  }
33
33
  async function getUserProfileAPI(user) {
34
34
  const url = `${user.domain}/api/v1/${user.service}/user/${user.id}/profile`;
35
- const result = await fetch_(url).then((r) => r.json());
35
+ const result = await fetchWithGlobalHeader(url).then((r) => r.json());
36
36
  return result;
37
37
  }
38
38
  async function getUserPostsAPI(user, offset) {
39
39
  const url = `${user.domain}/api/v1/${user.service}/user/${user.id}/posts?o=${offset}`;
40
- const posts = await fetch_(url).then((r) => r.json());
40
+ const posts = await fetchWithGlobalHeader(url).then((r) => r.json());
41
41
  return posts;
42
42
  }
43
43
  async function getUserFiles(user, mediaType) {
@@ -95,10 +95,50 @@ function mkdir(filepath) {
95
95
  }
96
96
  }
97
97
 
98
+ // src/utils/promise.ts
99
+ async function sleep(time) {
100
+ return new Promise((resolve) => setTimeout(resolve, time));
101
+ }
102
+ var PromiseRetry = class _PromiseRetry {
103
+ retries;
104
+ delay;
105
+ callback;
106
+ constructor(options) {
107
+ this.retries = options.retries || 3;
108
+ this.delay = options.delay || 1e3;
109
+ this.callback = options.callback;
110
+ }
111
+ async execute(fn) {
112
+ let retries = this.retries;
113
+ while (true) {
114
+ try {
115
+ return await fn();
116
+ } catch (error) {
117
+ if (retries <= 0) {
118
+ throw error;
119
+ }
120
+ if (this.callback) {
121
+ const res = this.callback(retries, error);
122
+ if (res) {
123
+ const { newRetries } = res;
124
+ if (newRetries === 0) throw error;
125
+ this.retries = newRetries || retries;
126
+ }
127
+ }
128
+ await sleep(this.delay);
129
+ retries--;
130
+ }
131
+ }
132
+ }
133
+ static create(options) {
134
+ return new _PromiseRetry(options);
135
+ }
136
+ };
137
+
98
138
  // src/utils/requests.ts
99
139
  import { CookieAgent } from "http-cookie-agent/undici";
100
140
  import { CookieJar } from "tough-cookie";
101
- import { fetch, getGlobalDispatcher, interceptors, setGlobalDispatcher } from "undici";
141
+ import { fetch, interceptors, setGlobalDispatcher } from "undici";
102
142
  function setCookieJarDispatcher() {
103
143
  const jar = new CookieJar();
104
144
  const agent = new CookieAgent({ cookies: { jar } }).compose(interceptors.retry()).compose(interceptors.redirect({ maxRedirections: 3 }));
@@ -114,7 +154,7 @@ function setGlobalHeaders(headers) {
114
154
  HeadersDefault.set(k, headers[k]);
115
155
  });
116
156
  }
117
- function fetch_(url) {
157
+ function fetchWithGlobalHeader(url) {
118
158
  const requestHeaders = new Headers(HeadersDefault);
119
159
  return fetch(url, { headers: requestHeaders });
120
160
  }
@@ -125,7 +165,7 @@ function fetchByteRange(url, downloadedSize) {
125
165
  }
126
166
 
127
167
  // src/utils/timer.ts
128
- var Timer = class {
168
+ var Timer = class _Timer {
129
169
  constructor(timeout = 1e4, timeoutCallback) {
130
170
  this.timeout = timeout;
131
171
  this.timeoutCallback = timeoutCallback;
@@ -133,7 +173,10 @@ var Timer = class {
133
173
  }
134
174
  timer = void 0;
135
175
  start() {
136
- this.timer = setTimeout(this.timeoutCallback, this.timeout);
176
+ this.timer = setTimeout(() => {
177
+ this.stop();
178
+ this.timeoutCallback();
179
+ }, this.timeout);
137
180
  return this;
138
181
  }
139
182
  stop() {
@@ -148,19 +191,27 @@ var Timer = class {
148
191
  this.start();
149
192
  return this;
150
193
  }
194
+ static withSignal(timeout, message) {
195
+ const controller = new AbortController();
196
+ const callback = () => {
197
+ controller.abort(message);
198
+ };
199
+ const timer = new _Timer(timeout, callback).start();
200
+ return {
201
+ timer,
202
+ signal: controller.signal
203
+ };
204
+ }
151
205
  };
152
206
 
153
207
  // src/utils/downloader.ts
154
208
  var subject = new Subject();
155
209
  var CHUNK_TIMEOUT = 3e4;
156
- var DOWNLOAD_ATTEMPTS = 7;
157
- async function downloadStream(file, stream) {
158
- subject.next({ type: "CHUNK_DOWNLOADING_STARTED", file });
210
+ var CHUNK_FETCH_RETRIES = 5;
211
+ var FETCH_RETRIES = 7;
212
+ async function fetchStream(file, stream) {
213
+ const { timer, signal } = Timer.withSignal(CHUNK_TIMEOUT, "CHUNK_TIMEOUT");
159
214
  const fileStream = fs2.createWriteStream(file.filepath, { flags: "a" });
160
- const controller = new AbortController();
161
- const timer = new Timer(CHUNK_TIMEOUT, () => {
162
- controller.abort("Stream is stuck.");
163
- }).start();
164
215
  const progressStream = new Transform({
165
216
  transform(chunk, _encoding, callback) {
166
217
  this.push(chunk);
@@ -170,71 +221,82 @@ async function downloadStream(file, stream) {
170
221
  callback();
171
222
  }
172
223
  });
173
- await pipeline(stream, progressStream, fileStream, { signal: controller.signal });
174
- timer.stop();
175
- subject.next({ type: "FILE_DOWNLOADING_FINISHED" });
176
- }
177
- function handleFetchError(error, file, attempts) {
178
- const url = file?.url;
179
- if (/coomer|kemono/.test(url)) {
180
- file.url = tryFixCoomerUrl(url, attempts);
181
- }
182
- throw error;
183
- }
184
- async function downloadFile(file, attempts = DOWNLOAD_ATTEMPTS) {
185
- const downloadedOld = file.downloaded || 0;
186
224
  try {
187
- file.downloaded = await getFileSize(file.filepath);
188
- const response = await fetchByteRange(file.url, file.downloaded).catch(
189
- (error) => handleFetchError(error, file, --attempts)
190
- );
191
- if (!response?.ok && response?.status !== 416) {
192
- throw new Error(`HTTP error! status: ${response?.status}`);
193
- }
194
- const contentLength = response.headers.get("Content-Length");
195
- if (!contentLength && file.downloaded > 0) {
196
- return;
197
- }
198
- const restFileSize = parseInt(contentLength);
199
- file.size = restFileSize + file.downloaded;
200
- if (file.size > file.downloaded && response.body) {
201
- const stream = Readable.fromWeb(response.body);
202
- await downloadStream(file, stream);
203
- }
225
+ subject.next({ type: "CHUNK_DOWNLOADING_START", file });
226
+ await pipeline(stream, progressStream, fileStream, { signal });
204
227
  } catch (error) {
205
- if (downloadedOld < (file.downloaded || 0)) {
206
- attempts = DOWNLOAD_ATTEMPTS;
207
- }
208
- if (attempts < 1) {
209
- console.error(file.url);
210
- console.error(error);
211
- } else {
212
- await downloadFile(file, attempts - 1);
213
- }
228
+ console.error(error.name === "AbortError" ? signal.reason : error);
229
+ } finally {
230
+ subject.next({ type: "CHUNK_DOWNLOADING_END", file });
214
231
  }
215
232
  }
233
+ async function downloadFile(file) {
234
+ file.downloaded = await getFileSize(file.filepath);
235
+ const response = await fetchByteRange(file.url, file.downloaded);
236
+ if (!response?.ok && response?.status !== 416) {
237
+ throw new Error(`HTTP error! status: ${response?.status}`);
238
+ }
239
+ const contentLength = response.headers.get("Content-Length");
240
+ if (!contentLength && file.downloaded > 0) {
241
+ return;
242
+ }
243
+ const restFileSize = parseInt(contentLength);
244
+ file.size = restFileSize + file.downloaded;
245
+ if (file.size > file.downloaded && response.body) {
246
+ const stream = Readable.fromWeb(response.body);
247
+ const sizeOld = file.downloaded;
248
+ await PromiseRetry.create({
249
+ retries: CHUNK_FETCH_RETRIES,
250
+ callback: () => {
251
+ if (sizeOld !== file.downloaded) {
252
+ return { newRetries: 5 };
253
+ }
254
+ }
255
+ }).execute(async () => await fetchStream(file, stream));
256
+ }
257
+ subject.next({ type: "FILE_DOWNLOADING_END" });
258
+ }
216
259
  async function downloadFiles(data, downloadDir) {
217
260
  mkdir(downloadDir);
218
- subject.next({ type: "FILES_DOWNLOADING_STARTED", filesCount: data.length });
219
- for (const [index, file] of data.entries()) {
261
+ subject.next({ type: "FILES_DOWNLOADING_START", filesCount: data.length });
262
+ for (const [_, file] of data.entries()) {
220
263
  file.filepath = path.join(downloadDir, file.name);
221
- subject.next({ type: "FILE_DOWNLOADING_STARTED", index });
222
- await downloadFile(file);
264
+ subject.next({ type: "FILE_DOWNLOADING_START" });
265
+ await PromiseRetry.create({
266
+ retries: FETCH_RETRIES,
267
+ callback: (retries) => {
268
+ if (/coomer|kemono/.test(file.url)) {
269
+ file.url = tryFixCoomerUrl(file.url, retries);
270
+ }
271
+ }
272
+ }).execute(async () => await downloadFile(file));
273
+ subject.next({ type: "FILE_DOWNLOADING_END" });
223
274
  }
224
- subject.next({ type: "FILES_DOWNLOADING_STARTED" });
275
+ subject.next({ type: "FILES_DOWNLOADING_END" });
225
276
  }
226
277
 
227
278
  // src/utils/filters.ts
228
279
  var isImage = (name) => /\.(jpg|jpeg|png|gif|bmp|tiff|webp|avif)$/i.test(name);
229
280
  var isVideo = (name) => /\.(mp4|m4v|avi|mov|mkv|webm|flv|wmv|mpeg|mpg|3gp)$/i.test(name);
230
281
  var testMediaType = (name, type) => type === "all" ? true : type === "image" ? isImage(name) : isVideo(name);
282
+ function includesAllWords(str, words) {
283
+ if (!words.length) return true;
284
+ return words.every((w) => str.includes(w));
285
+ }
286
+ function includesNoWords(str, words) {
287
+ if (!words.length) return true;
288
+ return words.every((w) => !str.includes(w));
289
+ }
290
+ function parseQuery(query) {
291
+ return query.split(",").map((x) => x.toLowerCase().trim()).filter((_) => _);
292
+ }
293
+ function filterString(text, include, exclude) {
294
+ return includesAllWords(text, parseQuery(include)) && includesNoWords(text, parseQuery(exclude));
295
+ }
231
296
  function filterKeywords(files, include, exclude) {
232
- const incl = include.split(",").map((x) => x.toLowerCase().trim());
233
- const excl = exclude.split(",").map((x) => x.toLowerCase().trim());
234
- const isValid = (text) => incl.some((e) => text.includes(e)) && (!exclude.trim().length || excl.every((e) => !text.includes(e)));
235
297
  return files.filter((f) => {
236
298
  const text = `${f.name || ""} ${f.content || ""}`.toLowerCase();
237
- return isValid(text);
299
+ return filterString(text, include, exclude);
238
300
  });
239
301
  }
240
302
 
@@ -267,33 +329,37 @@ function createMultibar() {
267
329
  let bar;
268
330
  let minibar;
269
331
  let filename;
332
+ let index = 0;
270
333
  subject.subscribe({
271
- next: ({ type, filesCount, index, file }) => {
334
+ next: ({ type, filesCount, file }) => {
272
335
  switch (type) {
273
- case "FILES_DOWNLOADING_STARTED":
336
+ case "FILES_DOWNLOADING_START":
274
337
  bar?.stop();
275
338
  bar = multibar.create(filesCount, 0);
276
339
  break;
277
- case "FILES_DOWNLOADING_FINISHED":
340
+ case "FILES_DOWNLOADING_END":
278
341
  bar?.stop();
279
342
  break;
280
- case "FILE_DOWNLOADING_STARTED":
281
- bar?.update(index + 1, { filename: "Downloaded files", size: "" });
343
+ case "FILE_DOWNLOADING_START":
344
+ bar?.update(++index, { filename: "Downloaded files", size: "" });
345
+ break;
346
+ case "FILE_DOWNLOADING_END":
347
+ multibar.remove(minibar);
282
348
  break;
283
- case "CHUNK_DOWNLOADING_STARTED":
349
+ case "CHUNK_DOWNLOADING_START":
284
350
  multibar?.remove(minibar);
285
351
  filename = formatNameStdout(file?.filepath);
286
352
  minibar = multibar.create(b2mb(file?.size), b2mb(file?.downloaded));
287
353
  break;
288
- case "FILE_DOWNLOADING_FINISHED":
289
- multibar.remove(minibar);
290
- break;
291
354
  case "CHUNK_DOWNLOADING_UPDATE":
292
355
  minibar?.update(b2mb(file?.downloaded), {
293
356
  filename,
294
357
  size: "mb"
295
358
  });
296
359
  break;
360
+ case "CHUNK_DOWNLOADING_END":
361
+ multibar?.remove(minibar);
362
+ break;
297
363
  default:
298
364
  break;
299
365
  }
@@ -515,7 +581,7 @@ async function run() {
515
581
  const { url, dir, media, include, exclude, skip } = argumentHander();
516
582
  const { dirName, files } = await apiHandler(url, media);
517
583
  const downloadDir = dir === "./" ? path2.resolve(dir, dirName) : path2.join(os.homedir(), path2.join(dir, dirName));
518
- const filteredFiles = filterKeywords(files, include, exclude).slice(skip);
584
+ const filteredFiles = filterKeywords(files.slice(skip), include, exclude);
519
585
  console.table([
520
586
  {
521
587
  found: files.length,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coomer-downloader",
3
- "version": "3.0.10",
3
+ "version": "3.1.0",
4
4
  "author": "smartacephal",
5
5
  "license": "MIT",
6
6
  "description": "Downloads images/videos from Coomer/Kemono, Bunkr, GoFile, Reddit-NSFW user posts",
@@ -1,5 +1,5 @@
1
1
  import type { ApiResult, File, MediaType } from '../types/index.js';
2
- import { fetch_, isImage, setGlobalHeaders, testMediaType } from '../utils/index.js';
2
+ import { fetchWithGlobalHeader, isImage, setGlobalHeaders, testMediaType } from '../utils/index.js';
3
3
 
4
4
  type CoomerUser = { domain: string; service: string; id: string; name?: string };
5
5
  type CoomerUserApi = { name: string };
@@ -29,13 +29,13 @@ export function tryFixCoomerUrl(url: string, attempts: number) {
29
29
 
30
30
  async function getUserProfileAPI(user: CoomerUser): Promise<CoomerUserApi> {
31
31
  const url = `${user.domain}/api/v1/${user.service}/user/${user.id}/profile`;
32
- const result = await fetch_(url).then((r) => r.json());
32
+ const result = await fetchWithGlobalHeader(url).then((r) => r.json());
33
33
  return result as CoomerUserApi;
34
34
  }
35
35
 
36
36
  async function getUserPostsAPI(user: CoomerUser, offset: number): Promise<CoomerPost[]> {
37
37
  const url = `${user.domain}/api/v1/${user.service}/user/${user.id}/posts?o=${offset}`;
38
- const posts = await fetch_(url).then((r) => r.json());
38
+ const posts = await fetchWithGlobalHeader(url).then((r) => r.json());
39
39
  return posts as CoomerPost[];
40
40
  }
41
41
 
package/src/index.ts CHANGED
@@ -15,7 +15,7 @@ async function run() {
15
15
  const downloadDir =
16
16
  dir === './' ? path.resolve(dir, dirName) : path.join(os.homedir(), path.join(dir, dirName));
17
17
 
18
- const filteredFiles = filterKeywords(files, include, exclude).slice(skip);
18
+ const filteredFiles = filterKeywords(files.slice(skip), include, exclude);
19
19
 
20
20
  console.table([
21
21
  {
@@ -29,6 +29,7 @@ async function run() {
29
29
  setGlobalHeaders({ Referer: url });
30
30
 
31
31
  createMultibar();
32
+
32
33
  await downloadFiles(filteredFiles, downloadDir);
33
34
 
34
35
  process.kill(process.pid, 'SIGINT');
@@ -6,24 +6,21 @@ import { Subject } from 'rxjs';
6
6
  import { tryFixCoomerUrl } from '../api/coomer-api';
7
7
  import type { DownloaderSubject, File } from '../types';
8
8
  import { getFileSize, mkdir } from './files';
9
+ import { PromiseRetry } from './promise';
9
10
  import { fetchByteRange } from './requests';
10
11
  import { Timer } from './timer';
11
12
 
12
13
  export const subject = new Subject<DownloaderSubject>();
13
14
 
14
15
  const CHUNK_TIMEOUT = 30_000;
15
- const DOWNLOAD_ATTEMPTS = 7;
16
+ const CHUNK_FETCH_RETRIES = 5;
17
+ const FETCH_RETRIES = 7;
16
18
 
17
- async function downloadStream(file: File, stream: Readable): Promise<void> {
18
- subject.next({ type: 'CHUNK_DOWNLOADING_STARTED', file });
19
+ async function fetchStream(file: File, stream: Readable): Promise<void> {
20
+ const { timer, signal } = Timer.withSignal(CHUNK_TIMEOUT, 'CHUNK_TIMEOUT');
19
21
 
20
22
  const fileStream = fs.createWriteStream(file.filepath as string, { flags: 'a' });
21
23
 
22
- const controller = new AbortController();
23
- const timer = new Timer(CHUNK_TIMEOUT, () => {
24
- controller.abort('Stream is stuck.');
25
- }).start();
26
-
27
24
  const progressStream = new Transform({
28
25
  transform(chunk, _encoding, callback) {
29
26
  this.push(chunk);
@@ -34,68 +31,72 @@ async function downloadStream(file: File, stream: Readable): Promise<void> {
34
31
  },
35
32
  });
36
33
 
37
- await pipeline(stream, progressStream, fileStream, { signal: controller.signal });
38
- timer.stop();
39
- subject.next({ type: 'FILE_DOWNLOADING_FINISHED' });
40
- }
41
-
42
- function handleFetchError(error: Error, file: File, attempts: number): void {
43
- const url = file?.url as string;
44
- if (/coomer|kemono/.test(url)) {
45
- file.url = tryFixCoomerUrl(url, attempts);
34
+ try {
35
+ subject.next({ type: 'CHUNK_DOWNLOADING_START', file });
36
+ await pipeline(stream, progressStream, fileStream, { signal });
37
+ } catch (error) {
38
+ console.error((error as Error).name === 'AbortError' ? signal.reason : error);
39
+ } finally {
40
+ subject.next({ type: 'CHUNK_DOWNLOADING_END', file });
46
41
  }
47
- throw error;
48
42
  }
49
43
 
50
- async function downloadFile(file: File, attempts = DOWNLOAD_ATTEMPTS): Promise<void> {
51
- const downloadedOld = file.downloaded || 0;
52
- try {
53
- file.downloaded = await getFileSize(file.filepath as string);
54
-
55
- const response = await fetchByteRange(file.url, file.downloaded).catch((error) =>
56
- handleFetchError(error, file, --attempts),
57
- );
44
+ async function downloadFile(file: File): Promise<void> {
45
+ file.downloaded = await getFileSize(file.filepath as string);
58
46
 
59
- if (!response?.ok && response?.status !== 416) {
60
- throw new Error(`HTTP error! status: ${response?.status}`);
61
- }
47
+ const response = await fetchByteRange(file.url, file.downloaded);
62
48
 
63
- const contentLength = response.headers.get('Content-Length') as string;
49
+ if (!response?.ok && response?.status !== 416) {
50
+ throw new Error(`HTTP error! status: ${response?.status}`);
51
+ }
64
52
 
65
- if (!contentLength && file.downloaded > 0) {
66
- return;
67
- }
53
+ const contentLength = response.headers.get('Content-Length') as string;
68
54
 
69
- const restFileSize = parseInt(contentLength);
70
- file.size = restFileSize + file.downloaded;
55
+ if (!contentLength && file.downloaded > 0) {
56
+ return;
57
+ }
71
58
 
72
- if (file.size > file.downloaded && response.body) {
73
- const stream = Readable.fromWeb(response.body);
74
- await downloadStream(file, stream);
75
- }
76
- } catch (error) {
77
- if (downloadedOld < (file.downloaded || 0)) {
78
- attempts = DOWNLOAD_ATTEMPTS;
79
- }
80
- if (attempts < 1) {
81
- console.error(file.url);
82
- console.error(error);
83
- } else {
84
- await downloadFile(file, attempts - 1);
85
- }
59
+ const restFileSize = parseInt(contentLength);
60
+ file.size = restFileSize + file.downloaded;
61
+
62
+ if (file.size > file.downloaded && response.body) {
63
+ const stream = Readable.fromWeb(response.body);
64
+ const sizeOld = file.downloaded;
65
+
66
+ await PromiseRetry.create({
67
+ retries: CHUNK_FETCH_RETRIES,
68
+ callback: () => {
69
+ if (sizeOld !== file.downloaded) {
70
+ return { newRetries: 5 };
71
+ }
72
+ },
73
+ }).execute(async () => await fetchStream(file, stream));
86
74
  }
75
+
76
+ subject.next({ type: 'FILE_DOWNLOADING_END' });
87
77
  }
88
78
 
89
79
  export async function downloadFiles(data: File[], downloadDir: string): Promise<void> {
90
80
  mkdir(downloadDir);
91
81
 
92
- subject.next({ type: 'FILES_DOWNLOADING_STARTED', filesCount: data.length });
82
+ subject.next({ type: 'FILES_DOWNLOADING_START', filesCount: data.length });
93
83
 
94
- for (const [index, file] of data.entries()) {
84
+ for (const [_, file] of data.entries()) {
95
85
  file.filepath = path.join(downloadDir, file.name);
96
- subject.next({ type: 'FILE_DOWNLOADING_STARTED', index });
97
- await downloadFile(file);
86
+
87
+ subject.next({ type: 'FILE_DOWNLOADING_START' });
88
+
89
+ await PromiseRetry.create({
90
+ retries: FETCH_RETRIES,
91
+ callback: (retries) => {
92
+ if (/coomer|kemono/.test(file.url)) {
93
+ file.url = tryFixCoomerUrl(file.url, retries);
94
+ }
95
+ },
96
+ }).execute(async () => await downloadFile(file));
97
+
98
+ subject.next({ type: 'FILE_DOWNLOADING_END' });
98
99
  }
99
100
 
100
- subject.next({ type: 'FILES_DOWNLOADING_STARTED' });
101
+ subject.next({ type: 'FILES_DOWNLOADING_END' });
101
102
  }
@@ -8,16 +8,30 @@ export const isVideo = (name: string) =>
8
8
  export const testMediaType = (name: string, type: MediaType) =>
9
9
  type === 'all' ? true : type === 'image' ? isImage(name) : isVideo(name);
10
10
 
11
- export function filterKeywords(files: File[], include: string, exclude: string) {
12
- const incl = include.split(',').map((x) => x.toLowerCase().trim());
13
- const excl = exclude.split(',').map((x) => x.toLowerCase().trim());
11
+ function includesAllWords(str: string, words: string[]) {
12
+ if (!words.length) return true;
13
+ return words.every((w) => str.includes(w));
14
+ }
15
+
16
+ function includesNoWords(str: string, words: string[]) {
17
+ if (!words.length) return true;
18
+ return words.every((w) => !str.includes(w));
19
+ }
14
20
 
15
- const isValid = (text: string) =>
16
- incl.some((e) => text.includes(e)) &&
17
- (!exclude.trim().length || excl.every((e) => !text.includes(e)));
21
+ function parseQuery(query: string) {
22
+ return query
23
+ .split(',')
24
+ .map((x) => x.toLowerCase().trim())
25
+ .filter((_) => _);
26
+ }
27
+
28
+ function filterString(text: string, include: string, exclude: string): boolean {
29
+ return includesAllWords(text, parseQuery(include)) && includesNoWords(text, parseQuery(exclude));
30
+ }
18
31
 
32
+ export function filterKeywords(files: File[], include: string, exclude: string) {
19
33
  return files.filter((f) => {
20
34
  const text = `${f.name || ''} ${f.content || ''}`.toLowerCase();
21
- return isValid(text);
35
+ return filterString(text, include, exclude);
22
36
  });
23
37
  }
@@ -2,5 +2,10 @@ export { downloadFiles } from './downloader';
2
2
  export { getFileSize, mkdir } from './files';
3
3
  export { filterKeywords, isImage, isVideo, testMediaType } from './filters';
4
4
  export { createMultibar } from './multibar';
5
- export { fetch_, fetchByteRange, HeadersDefault, setGlobalHeaders } from './requests';
5
+ export {
6
+ fetchByteRange,
7
+ fetchWithGlobalHeader,
8
+ HeadersDefault,
9
+ setGlobalHeaders,
10
+ } from './requests';
6
11
  export { b2mb } from './strings';
@@ -1,8 +1,8 @@
1
- import { MultiBar, type SingleBar } from 'cli-progress';
1
+ import { MultiBar, type Options, type SingleBar } from 'cli-progress';
2
2
  import { subject } from './downloader';
3
3
  import { b2mb, formatNameStdout } from './strings';
4
4
 
5
- const config = {
5
+ const config: Options = {
6
6
  clearOnComplete: true,
7
7
  gracefulExit: true,
8
8
  autopadding: true,
@@ -10,44 +10,39 @@ const config = {
10
10
  format: '{percentage}% | {filename} | {value}/{total}{size}',
11
11
  };
12
12
 
13
- // interface IBarState {
14
- // totalFiles: number;
15
- // totalDownloadedFiles: number;
16
- // filesInProcess: File[];
17
- // }
18
-
19
13
  export function createMultibar() {
20
14
  const multibar = new MultiBar(config);
21
15
  let bar: SingleBar;
22
16
  let minibar: SingleBar;
23
17
  let filename: string;
18
+ let index = 0;
24
19
 
25
20
  subject.subscribe({
26
- next: ({ type, filesCount, index, file }) => {
21
+ next: ({ type, filesCount, file }) => {
27
22
  switch (type) {
28
- case 'FILES_DOWNLOADING_STARTED':
23
+ case 'FILES_DOWNLOADING_START':
29
24
  bar?.stop();
30
25
  bar = multibar.create(filesCount as number, 0);
31
26
  break;
32
27
 
33
- case 'FILES_DOWNLOADING_FINISHED':
28
+ case 'FILES_DOWNLOADING_END':
34
29
  bar?.stop();
35
30
  break;
36
31
 
37
- case 'FILE_DOWNLOADING_STARTED':
38
- bar?.update((index as number) + 1, { filename: 'Downloaded files', size: '' });
32
+ case 'FILE_DOWNLOADING_START':
33
+ bar?.update(++index, { filename: 'Downloaded files', size: '' });
39
34
  break;
40
35
 
41
- case 'CHUNK_DOWNLOADING_STARTED':
36
+ case 'FILE_DOWNLOADING_END':
37
+ multibar.remove(minibar);
38
+ break;
39
+
40
+ case 'CHUNK_DOWNLOADING_START':
42
41
  multibar?.remove(minibar);
43
42
  filename = formatNameStdout(file?.filepath as string);
44
43
  minibar = multibar.create(b2mb(file?.size as number), b2mb(file?.downloaded as number));
45
44
  break;
46
45
 
47
- case 'FILE_DOWNLOADING_FINISHED':
48
- multibar.remove(minibar);
49
- break;
50
-
51
46
  case 'CHUNK_DOWNLOADING_UPDATE':
52
47
  minibar?.update(b2mb(file?.downloaded as number), {
53
48
  filename: filename as string,
@@ -55,6 +50,10 @@ export function createMultibar() {
55
50
  });
56
51
  break;
57
52
 
53
+ case 'CHUNK_DOWNLOADING_END':
54
+ multibar?.remove(minibar);
55
+ break;
56
+
58
57
  default:
59
58
  break;
60
59
  }
@@ -0,0 +1,53 @@
1
+ export async function sleep(time: number) {
2
+ return new Promise((resolve) => setTimeout(resolve, time));
3
+ }
4
+
5
+ type PromiseRetryCallback = (retries: number, error: Error) => void | { newRetries?: number };
6
+
7
+ interface PromiseRetryOptions {
8
+ retries?: number;
9
+ callback?: PromiseRetryCallback;
10
+ delay?: number;
11
+ }
12
+
13
+ export class PromiseRetry {
14
+ private retries: number;
15
+ private delay: number;
16
+ private callback?: PromiseRetryCallback;
17
+
18
+ constructor(options: PromiseRetryOptions) {
19
+ this.retries = options.retries || 3;
20
+ this.delay = options.delay || 1000;
21
+ this.callback = options.callback;
22
+ }
23
+
24
+ async execute(fn: () => Promise<void>) {
25
+ let retries = this.retries;
26
+
27
+ while (true) {
28
+ try {
29
+ return await fn();
30
+ } catch (error) {
31
+ if (retries <= 0) {
32
+ throw error;
33
+ }
34
+
35
+ if (this.callback) {
36
+ const res = this.callback(retries, error as Error);
37
+ if (res) {
38
+ const { newRetries } = res;
39
+ if (newRetries === 0) throw error;
40
+ this.retries = newRetries || retries;
41
+ }
42
+ }
43
+
44
+ await sleep(this.delay);
45
+ retries--;
46
+ }
47
+ }
48
+ }
49
+
50
+ static create(options: PromiseRetryOptions) {
51
+ return new PromiseRetry(options);
52
+ }
53
+ }
@@ -1,6 +1,6 @@
1
1
  import { CookieAgent } from 'http-cookie-agent/undici';
2
2
  import { CookieJar } from 'tough-cookie';
3
- import { fetch, getGlobalDispatcher, interceptors, setGlobalDispatcher } from 'undici';
3
+ import { fetch, interceptors, setGlobalDispatcher } from 'undici';
4
4
 
5
5
  function setCookieJarDispatcher() {
6
6
  const jar = new CookieJar();
@@ -12,20 +12,6 @@ function setCookieJarDispatcher() {
12
12
 
13
13
  setCookieJarDispatcher();
14
14
 
15
- export function setRetryDispatcher(maxRetries = 3) {
16
- setGlobalDispatcher(
17
- getGlobalDispatcher().compose(
18
- interceptors.retry({
19
- maxRetries,
20
- // minTimeout: 1000,
21
- // maxTimeout: 10000,
22
- timeoutFactor: 2,
23
- retryAfter: true,
24
- }),
25
- ),
26
- );
27
- }
28
-
29
15
  export const HeadersDefault = new Headers({
30
16
  accept: 'application/json, text/css',
31
17
  'User-Agent':
@@ -38,7 +24,7 @@ export function setGlobalHeaders(headers: Record<string, string>) {
38
24
  });
39
25
  }
40
26
 
41
- export function fetch_(url: string) {
27
+ export function fetchWithGlobalHeader(url: string) {
42
28
  const requestHeaders = new Headers(HeadersDefault);
43
29
  return fetch(url, { headers: requestHeaders });
44
30
  }
@@ -16,6 +16,6 @@ export function formatNameStdout(pathname: string) {
16
16
  const consoleWidth = process.stdout.columns;
17
17
  const width = Math.max((consoleWidth / 2) | 0, 40);
18
18
  if (name.length < width) return name.trim();
19
- const result = `${name.slice(0,width-15)} ... ${name.slice(-10)}`.replace(/ +/g, ' ');
19
+ const result = `${name.slice(0, width - 15)} ... ${name.slice(-10)}`.replace(/ +/g, ' ');
20
20
  return result;
21
21
  }
@@ -9,7 +9,10 @@ export class Timer {
9
9
  }
10
10
 
11
11
  start() {
12
- this.timer = setTimeout(this.timeoutCallback, this.timeout);
12
+ this.timer = setTimeout(() => {
13
+ this.stop();
14
+ this.timeoutCallback();
15
+ }, this.timeout);
13
16
  return this;
14
17
  }
15
18
 
@@ -26,4 +29,19 @@ export class Timer {
26
29
  this.start();
27
30
  return this;
28
31
  }
32
+
33
+ static withSignal(timeout?: number, message?: string) {
34
+ const controller = new AbortController();
35
+
36
+ const callback = () => {
37
+ controller.abort(message);
38
+ };
39
+
40
+ const timer = new Timer(timeout, callback).start();
41
+
42
+ return {
43
+ timer,
44
+ signal: controller.signal,
45
+ };
46
+ }
29
47
  }
package/src/utils/file.ts DELETED
@@ -1,26 +0,0 @@
1
- // import path from 'node:path';
2
-
3
- // class CFileList {
4
- // files: CFile[];
5
- // active: CFile[];
6
- // }
7
-
8
- // export class CFile {
9
- // constructor(
10
- // public name = '',
11
- // public url = '',
12
- // public filepath = '',
13
- // public content = '',
14
- // public size = 0,
15
- // public downloaded = 0,
16
- // public maybeFixURL?: undefined | ((url: string) => string),
17
- // ) {}
18
-
19
- // get text() {
20
- // return `${this.content} ${this.name}`.toLowerCase();
21
- // }
22
-
23
- // setDir(dir: string) {
24
- // this.filepath = path.join(dir, this.name);
25
- // }
26
- // }