coomer-downloader 3.0.10 → 3.2.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/src/utils/file.ts CHANGED
@@ -1,26 +1,75 @@
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
- // }
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
+ }
@@ -1,23 +1,34 @@
1
- import type { File, MediaType } from '../types';
1
+ import type { MediaType } from '../types';
2
2
 
3
- export const isImage = (name: string) => /\.(jpg|jpeg|png|gif|bmp|tiff|webp|avif)$/i.test(name);
3
+ export function isImage(name: string) {
4
+ return /\.(jpg|jpeg|png|gif|bmp|tiff|webp|avif)$/i.test(name);
5
+ }
4
6
 
5
- export const isVideo = (name: string) =>
6
- /\.(mp4|m4v|avi|mov|mkv|webm|flv|wmv|mpeg|mpg|3gp)$/i.test(name);
7
+ export function isVideo(name: string) {
8
+ return /\.(mp4|m4v|avi|mov|mkv|webm|flv|wmv|mpeg|mpg|3gp)$/i.test(name);
9
+ }
7
10
 
8
- export const testMediaType = (name: string, type: MediaType) =>
9
- type === 'all' ? true : type === 'image' ? isImage(name) : isVideo(name);
11
+ export function testMediaType(name: string, type: MediaType) {
12
+ return type === 'all' ? true : type === 'image' ? isImage(name) : isVideo(name);
13
+ }
10
14
 
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());
15
+ function includesAllWords(str: string, words: string[]) {
16
+ if (!words.length) return true;
17
+ return words.every((w) => str.includes(w));
18
+ }
14
19
 
15
- const isValid = (text: string) =>
16
- incl.some((e) => text.includes(e)) &&
17
- (!exclude.trim().length || excl.every((e) => !text.includes(e)));
20
+ function includesNoWords(str: string, words: string[]) {
21
+ if (!words.length) return true;
22
+ return words.every((w) => !str.includes(w));
23
+ }
24
+
25
+ function parseQuery(query: string) {
26
+ return query
27
+ .split(',')
28
+ .map((x) => x.toLowerCase().trim())
29
+ .filter((_) => _);
30
+ }
18
31
 
19
- return files.filter((f) => {
20
- const text = `${f.name || ''} ${f.content || ''}`.toLowerCase();
21
- return isValid(text);
22
- });
32
+ export function filterString(text: string, include: string, exclude: string): boolean {
33
+ return includesAllWords(text, parseQuery(include)) && includesNoWords(text, parseQuery(exclude));
23
34
  }
@@ -1,6 +1,11 @@
1
- export { downloadFiles } from './downloader';
2
- export { getFileSize, mkdir } from './files';
3
- export { filterKeywords, isImage, isVideo, testMediaType } from './filters';
1
+ export { Downloader } from './downloader';
2
+ export { isImage, isVideo, testMediaType } from './filters';
3
+ export { getFileSize, mkdir } from './io';
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';
2
- import { subject } from './downloader';
1
+ import { MultiBar, type Options, type SingleBar } from 'cli-progress';
2
+ import type { Downloader } 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
- export function createMultibar() {
13
+ export function createMultibar(downloader: Downloader) {
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
- subject.subscribe({
26
- next: ({ type, filesCount, index, file }) => {
20
+ downloader.subject.subscribe({
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
  }
@@ -2,20 +2,11 @@ export function b2mb(bytes: number) {
2
2
  return Number.parseFloat((bytes / 1048576).toFixed(2));
3
3
  }
4
4
 
5
- export function sanitizeString(str: string) {
6
- return (
7
- str
8
- .match(/(\w| |-)/g)
9
- ?.join('')
10
- .replace(/ +/g, ' ') || ''
11
- );
12
- }
13
-
14
5
  export function formatNameStdout(pathname: string) {
15
6
  const name = pathname.split('/').pop() || '';
16
7
  const consoleWidth = process.stdout.columns;
17
8
  const width = Math.max((consoleWidth / 2) | 0, 40);
18
9
  if (name.length < width) return name.trim();
19
- const result = `${name.slice(0,width-15)} ... ${name.slice(-10)}`.replace(/ +/g, ' ');
10
+ const result = `${name.slice(0, width - 15)} ... ${name.slice(-10)}`.replace(/ +/g, ' ');
20
11
  return result;
21
12
  }
@@ -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
  }
File without changes