coomer-downloader 3.1.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coomer-downloader",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
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/bunkr.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import * as cheerio from 'cheerio';
2
2
  import { fetch } from 'undici';
3
- import type { ApiResult, MediaType } from '../types/index.js';
4
- import { testMediaType } from '../utils/index.js';
3
+ import { CoomerFile, CoomerFileList } from '../utils/file';
5
4
 
6
5
  type EncData = { url: string; timestamp: number };
7
6
 
@@ -23,46 +22,46 @@ function decryptEncryptedUrl(encryptionData: EncData) {
23
22
  .join('');
24
23
  }
25
24
 
26
- async function getFileData(url: string, name: string) {
25
+ async function getFileData(url: string, name: string): Promise<CoomerFile> {
27
26
  const slug = url.split('/').pop() as string;
28
27
  const encryptionData = await getEncryptionData(slug);
29
28
  const src = decryptEncryptedUrl(encryptionData);
30
- return { name, url: src };
29
+ return CoomerFile.from({ name, url: src });
31
30
  }
32
31
 
33
- async function getGalleryFiles(url: string, mediaType: MediaType) {
34
- const data = [];
32
+ async function getGalleryFiles(url: string): Promise<CoomerFileList> {
33
+ const filelist = new CoomerFileList();
35
34
  const page = await fetch(url).then((r) => r.text());
36
35
  const $ = cheerio.load(page);
37
- const title = $('title').text();
36
+ const dirName = $('title').text();
37
+ filelist.dirName = `${dirName.split('|')[0].trim()}-bunkr`;
38
38
  const url_ = new URL(url);
39
39
 
40
40
  if (url_.pathname.startsWith('/f/')) {
41
41
  const fileName = $('h1').text();
42
42
  const singleFile = await getFileData(url, fileName);
43
- data.push(singleFile);
44
- return { title, files: data.filter((f) => testMediaType(f.name, mediaType)) };
43
+ filelist.files.push(singleFile);
44
+ return filelist;
45
45
  }
46
46
 
47
47
  const fileNames = Array.from($('div[title]').map((_, e) => $(e).attr('title')));
48
48
 
49
- const files = Array.from($('a').map((_, e) => $(e).attr('href')))
49
+ const data = Array.from($('a').map((_, e) => $(e).attr('href')))
50
50
  .filter((a) => /\/f\/\w+/.test(a))
51
51
  .map((a, i) => ({
52
52
  url: `${url_.origin}${a}`,
53
53
  name: fileNames[i] || (url.split('/').pop() as string),
54
54
  }));
55
55
 
56
- for (const { name, url } of files) {
56
+ for (const { name, url } of data) {
57
57
  const res = await getFileData(url, name);
58
- data.push(res);
58
+ filelist.files.push(res);
59
59
  }
60
60
 
61
- return { title, files: data.filter((f) => testMediaType(f.name, mediaType)) };
61
+ return filelist;
62
62
  }
63
63
 
64
- export async function getBunkrData(url: string, mediaType: MediaType): Promise<ApiResult> {
65
- const { files, title } = await getGalleryFiles(url, mediaType);
66
- const dirName = `${title.split('|')[0].trim()}-bunkr`;
67
- return { dirName, files };
64
+ export async function getBunkrData(url: string): Promise<CoomerFileList> {
65
+ const filelist = await getGalleryFiles(url);
66
+ return filelist;
68
67
  }
@@ -1,15 +1,15 @@
1
- import type { ApiResult, File, MediaType } from '../types/index.js';
2
- import { fetchWithGlobalHeader, isImage, setGlobalHeaders, testMediaType } from '../utils/index.js';
1
+ import { CoomerFile, CoomerFileList } from '../utils/file.js';
2
+ import { fetchWithGlobalHeader, isImage, setGlobalHeaders } from '../utils/index.js';
3
3
 
4
- type CoomerUser = { domain: string; service: string; id: string; name?: string };
5
- type CoomerUserApi = { name: string };
6
- type CoomerFile = { path: string; name: string };
7
- type CoomerPost = {
4
+ type CoomerAPIUser = { domain: string; service: string; id: string; name?: string };
5
+ type CoomerAPIUserData = { name: string };
6
+ type CoomerAPIFile = { path: string; name: string };
7
+ type CoomerAPIPost = {
8
8
  title: string;
9
9
  content: string;
10
10
  published: string;
11
- attachments: CoomerFile[];
12
- file: CoomerFile;
11
+ attachments: CoomerAPIFile[];
12
+ file: CoomerAPIFile;
13
13
  };
14
14
 
15
15
  const SERVERS = ['n1', 'n2', 'n3', 'n4'];
@@ -27,19 +27,19 @@ export function tryFixCoomerUrl(url: string, attempts: number) {
27
27
  return url;
28
28
  }
29
29
 
30
- async function getUserProfileAPI(user: CoomerUser): Promise<CoomerUserApi> {
30
+ async function getUserProfileData(user: CoomerAPIUser): Promise<CoomerAPIUserData> {
31
31
  const url = `${user.domain}/api/v1/${user.service}/user/${user.id}/profile`;
32
32
  const result = await fetchWithGlobalHeader(url).then((r) => r.json());
33
- return result as CoomerUserApi;
33
+ return result as CoomerAPIUserData;
34
34
  }
35
35
 
36
- async function getUserPostsAPI(user: CoomerUser, offset: number): Promise<CoomerPost[]> {
36
+ async function getUserPostsAPI(user: CoomerAPIUser, offset: number): Promise<CoomerAPIPost[]> {
37
37
  const url = `${user.domain}/api/v1/${user.service}/user/${user.id}/posts?o=${offset}`;
38
38
  const posts = await fetchWithGlobalHeader(url).then((r) => r.json());
39
- return posts as CoomerPost[];
39
+ return posts as CoomerAPIPost[];
40
40
  }
41
41
 
42
- export async function getUserFiles(user: CoomerUser, mediaType: MediaType): Promise<File[]> {
42
+ export async function getUserFiles(user: CoomerAPIUser): Promise<CoomerFileList> {
43
43
  const userPosts = [];
44
44
 
45
45
  const offset = 50;
@@ -49,7 +49,7 @@ export async function getUserFiles(user: CoomerUser, mediaType: MediaType): Prom
49
49
  if (posts.length < 50) break;
50
50
  }
51
51
 
52
- const files: File[] = [];
52
+ const filelist = new CoomerFileList();
53
53
 
54
54
  for (const p of userPosts) {
55
55
  const title = p.title.match(/\w+/g)?.join(' ') || '';
@@ -59,35 +59,34 @@ export async function getUserFiles(user: CoomerUser, mediaType: MediaType): Prom
59
59
 
60
60
  const postFiles = [...p.attachments, p.file]
61
61
  .filter((f) => f.path)
62
- .filter((f) => testMediaType(f.name, mediaType))
63
62
  .map((f, i) => {
64
63
  const ext = f.name.split('.').pop();
65
64
  const name = `${datentitle} ${i + 1}.${ext}`;
66
65
  const url = `${user.domain}/${f.path}`;
67
- return { name, url, content };
66
+ return CoomerFile.from({ name, url, content });
68
67
  });
69
68
 
70
- files.push(...postFiles);
69
+ filelist.files.push(...postFiles);
71
70
  }
72
71
 
73
- return files;
72
+ return filelist;
74
73
  }
75
74
 
76
- async function parseUser(url: string): Promise<CoomerUser> {
75
+ async function parseUser(url: string): Promise<CoomerAPIUser> {
77
76
  const [_, domain, service, id] = url.match(
78
77
  /(https:\/\/\w+\.\w+)\/(\w+)\/user\/([\w|.|-]+)/,
79
78
  ) as RegExpMatchArray;
80
79
  if (!domain || !service || !id) console.error('Invalid URL', url);
81
80
 
82
- const { name } = await getUserProfileAPI({ domain, service, id });
81
+ const { name } = await getUserProfileData({ domain, service, id });
83
82
 
84
83
  return { domain, service, id, name };
85
84
  }
86
85
 
87
- export async function getCoomerData(url: string, mediaType: MediaType): Promise<ApiResult> {
86
+ export async function getCoomerData(url: string): Promise<CoomerFileList> {
88
87
  setGlobalHeaders({ accept: 'text/css' });
89
88
  const user = await parseUser(url);
90
- const dirName = `${user.name}-${user.service}`;
91
- const files = await getUserFiles(user, mediaType);
92
- return { dirName, files };
89
+ const filelist = await getUserFiles(user);
90
+ filelist.dirName = `${user.name}-${user.service}`;
91
+ return filelist;
93
92
  }
package/src/api/gofile.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { fetch } from 'undici';
2
- import type { ApiResult, File, MediaType } from '../types/index.js';
3
- import { setGlobalHeaders, testMediaType } from '../utils/index.js';
2
+ import { CoomerFile, CoomerFileList } from '../utils/file.js';
3
+ import { setGlobalHeaders } from '../utils/index.js';
4
4
 
5
5
  type GoFileAPIToken = { status: string; data: { token: string } };
6
6
  type GoFileAPIFilelist = { data: { children: { link: string; name: string }[] } };
@@ -26,7 +26,11 @@ async function getWebsiteToken() {
26
26
  throw new Error('cannot get wt');
27
27
  }
28
28
 
29
- async function getFolderFiles(id: string, token: string, websiteToken: string): Promise<File[]> {
29
+ async function getFolderFiles(
30
+ id: string,
31
+ token: string,
32
+ websiteToken: string,
33
+ ): Promise<CoomerFileList> {
30
34
  const url = `https://api.gofile.io/contents/${id}?wt=${websiteToken}&cache=true}`;
31
35
  const response = await fetch(url, {
32
36
  headers: {
@@ -39,26 +43,26 @@ async function getFolderFiles(id: string, token: string, websiteToken: string):
39
43
  }
40
44
 
41
45
  const data = (await response.json()) as GoFileAPIFilelist;
42
- const files = Object.values(data.data.children).map((f) => ({
43
- url: f.link,
44
- name: f.name,
45
- }));
46
+ const files = Object.values(data.data.children).map((f) =>
47
+ CoomerFile.from({
48
+ url: f.link,
49
+ name: f.name,
50
+ }),
51
+ );
46
52
 
47
- return files;
53
+ return new CoomerFileList(files);
48
54
  }
49
55
 
50
- export async function getGofileData(url: string, mediaType: MediaType): Promise<ApiResult> {
56
+ export async function getGofileData(url: string): Promise<CoomerFileList> {
51
57
  const id = url.match(/gofile.io\/d\/(\w+)/)?.[1] as string;
52
- const dirName = `gofile-${id}`;
53
58
 
54
59
  const token = await getToken();
55
60
  const websiteToken = await getWebsiteToken();
56
61
 
57
- const files = (await getFolderFiles(id, token, websiteToken)).filter((f) =>
58
- testMediaType(f.name, mediaType),
59
- );
62
+ const filelist = await getFolderFiles(id, token, websiteToken);
63
+ filelist.dirName = `gofile-${id}`;
60
64
 
61
65
  setGlobalHeaders({ Cookie: `accountToken=${token}` });
62
66
 
63
- return { dirName, files };
67
+ return filelist;
64
68
  }
package/src/api/index.ts CHANGED
@@ -1,28 +1,28 @@
1
- import type { ApiResult, MediaType } from '../types';
1
+ import type { CoomerFileList } from '../utils/file';
2
2
  import { getBunkrData } from './bunkr';
3
3
  import { getCoomerData } from './coomer-api';
4
4
  import { getGofileData } from './gofile';
5
5
  import { getRedditData } from './nsfw.xxx';
6
6
  import { getPlainFileData } from './plain-curl';
7
7
 
8
- export async function apiHandler(
9
- url: string,
10
- mediaType: MediaType,
11
- ): Promise<ApiResult | undefined> {
12
- if (/^u\/\w+$/.test(url.trim())) {
13
- return getRedditData(url, mediaType);
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);
14
13
  }
15
- if (/coomer|kemono/.test(url)) {
16
- return getCoomerData(url, mediaType);
14
+ if (/coomer|kemono/.test(url.origin)) {
15
+ return getCoomerData(url.href);
17
16
  }
18
- if (/bunkr/.test(url)) {
19
- return getBunkrData(url, mediaType);
17
+ if (/bunkr/.test(url.origin)) {
18
+ return getBunkrData(url.href);
20
19
  }
21
- if (/gofile\.io/.test(url)) {
22
- return getGofileData(url, mediaType);
20
+ if (/gofile\.io/.test(url.origin)) {
21
+ return getGofileData(url.href);
23
22
  }
24
- if (/\.\w+/.test(url.split('/').pop() as string)) {
25
- return getPlainFileData(url);
23
+ if (/\.\w+/.test(url.pathname)) {
24
+ return getPlainFileData(url.href);
26
25
  }
27
- console.error('Wrong URL.');
26
+
27
+ throw Error('Invalid URL');
28
28
  }
@@ -1,7 +1,6 @@
1
1
  import * as cheerio from 'cheerio';
2
2
  import { fetch } from 'undici';
3
- import type { ApiResult, File, MediaType } from '../types/index.js';
4
- import { testMediaType } from '../utils/index.js';
3
+ import { CoomerFile, CoomerFileList } from '../utils/file';
5
4
 
6
5
  async function getUserPage(user: string, offset: number) {
7
6
  const url = `https://nsfw.xxx/page/${offset}?nsfw[]=0&types[]=image&types[]=video&types[]=gallery&slider=1&jsload=1&user=${user}&_=${Date.now()}`;
@@ -26,9 +25,9 @@ async function getUserPosts(user: string): Promise<string[]> {
26
25
  return posts;
27
26
  }
28
27
 
29
- async function getPostsData(posts: string[], mediaType: MediaType): Promise<File[]> {
28
+ async function getPostsData(posts: string[]): Promise<CoomerFileList> {
30
29
  console.log('Fetching posts data...');
31
- const data = [];
30
+ const filelist = new CoomerFileList();
32
31
  for (const post of posts) {
33
32
  const page = await fetch(post).then((r) => r.text());
34
33
  const $ = cheerio.load(page);
@@ -46,16 +45,16 @@ async function getPostsData(posts: string[], mediaType: MediaType): Promise<File
46
45
  const ext = src.split('.').pop();
47
46
  const name = `${slug}-${date}.${ext}`;
48
47
 
49
- data.push({ name, url: src });
48
+ filelist.files.push(CoomerFile.from({ name, url: src }));
50
49
  }
51
50
 
52
- return data.filter((f) => testMediaType(f.name, mediaType));
51
+ return filelist;
53
52
  }
54
53
 
55
- export async function getRedditData(url: string, mediaType: MediaType): Promise<ApiResult> {
54
+ export async function getRedditData(url: string): Promise<CoomerFileList> {
56
55
  const user = url.match(/u\/(\w+)/)?.[1] as string;
57
56
  const posts = await getUserPosts(user);
58
- const files = await getPostsData(posts, mediaType);
59
- const dirName = `${user}-reddit`;
60
- return { dirName, files };
57
+ const filelist = await getPostsData(posts);
58
+ filelist.dirName = `${user}-reddit`;
59
+ return filelist;
61
60
  }
@@ -1,13 +1,9 @@
1
- import type { ApiResult } from '../types';
1
+ import { CoomerFile, CoomerFileList } from '../utils/file';
2
2
 
3
- export async function getPlainFileData(url: string): Promise<ApiResult> {
4
- return {
5
- dirName: '',
6
- files: [
7
- {
8
- name: url.split('/').pop() as string,
9
- url,
10
- },
11
- ],
12
- };
3
+ export async function getPlainFileData(url: string): Promise<CoomerFileList> {
4
+ const name = url.split('/').pop() as string;
5
+ const file = CoomerFile.from({ name, url });
6
+ const filelist = new CoomerFileList([file]);
7
+ filelist.dirName = '';
8
+ return filelist;
13
9
  }
@@ -1,16 +1,13 @@
1
1
  import yargs from 'yargs';
2
2
  import { hideBin } from 'yargs/helpers';
3
3
 
4
- export type ArgumentHandlerResult = {
5
- [x: string]: unknown;
4
+ type ArgumentHandlerResult = {
6
5
  url: string;
7
6
  dir: string;
8
7
  media: string;
9
8
  include: string;
10
9
  exclude: string;
11
10
  skip: number;
12
- _: (string | number)[];
13
- $0: string;
14
11
  };
15
12
 
16
13
  export function argumentHander(): ArgumentHandlerResult {
package/src/index.ts CHANGED
@@ -1,36 +1,34 @@
1
1
  #!/usr/bin/env node
2
- import os from 'node:os';
3
- import path from 'node:path';
4
2
  import process from 'node:process';
5
3
  import { apiHandler } from './api';
6
4
  import { argumentHander } from './args-handler';
7
- import type { ApiResult, MediaType } from './types';
8
- import { createMultibar, downloadFiles, filterKeywords, setGlobalHeaders } from './utils';
5
+ import { createMultibar, Downloader, setGlobalHeaders } from './utils';
9
6
 
10
7
  async function run() {
11
8
  const { url, dir, media, include, exclude, skip } = argumentHander();
12
9
 
13
- const { dirName, files } = (await apiHandler(url, media as MediaType)) as ApiResult;
10
+ const filelist = await apiHandler(url);
14
11
 
15
- const downloadDir =
16
- dir === './' ? path.resolve(dir, dirName) : path.join(os.homedir(), path.join(dir, dirName));
17
-
18
- const filteredFiles = filterKeywords(files.slice(skip), include, exclude);
12
+ const found = filelist.files.length;
13
+ filelist.setDirPath(dir);
14
+ filelist.skip(skip);
15
+ filelist.filterByText(include, exclude);
16
+ filelist.filterByMediaType(media);
19
17
 
20
18
  console.table([
21
19
  {
22
- found: files.length,
20
+ found,
23
21
  skip,
24
- filtered: files.length - filteredFiles.length - skip,
25
- folder: downloadDir,
22
+ filtered: found - filelist.files.length,
23
+ folder: filelist.dirPath,
26
24
  },
27
25
  ]);
28
26
 
29
27
  setGlobalHeaders({ Referer: url });
30
28
 
31
- createMultibar();
32
-
33
- await downloadFiles(filteredFiles, downloadDir);
29
+ const downloader = new Downloader();
30
+ createMultibar(downloader);
31
+ await downloader.downloadFiles(filelist);
34
32
 
35
33
  process.kill(process.pid, 'SIGINT');
36
34
  }
@@ -1,23 +1 @@
1
- export type File = {
2
- name: string;
3
- url: string;
4
- filepath?: string;
5
- size?: number;
6
- downloaded?: number;
7
- content?: string;
8
- };
9
-
10
- export type DownloaderSubject = {
11
- type: string;
12
- file?: File;
13
- index?: number;
14
- filesCount?: number;
15
- };
16
-
17
- export type ApiResult = {
18
- files: File[];
19
- // should merge into filepaths?
20
- dirName: string;
21
- };
22
-
23
1
  export type MediaType = 'video' | 'image' | 'all';
@@ -1,102 +1,108 @@
1
1
  import fs from 'node:fs';
2
- import path from 'node:path';
3
2
  import { Readable, Transform } from 'node:stream';
4
3
  import { pipeline } from 'node:stream/promises';
5
4
  import { Subject } from 'rxjs';
6
5
  import { tryFixCoomerUrl } from '../api/coomer-api';
7
- import type { DownloaderSubject, File } from '../types';
8
- import { getFileSize, mkdir } from './files';
6
+ import type { CoomerFile, CoomerFileList } from './file';
7
+ import { getFileSize, mkdir } from './io';
9
8
  import { PromiseRetry } from './promise';
10
9
  import { fetchByteRange } from './requests';
11
10
  import { Timer } from './timer';
12
11
 
13
- export const subject = new Subject<DownloaderSubject>();
14
-
15
- const CHUNK_TIMEOUT = 30_000;
16
- const CHUNK_FETCH_RETRIES = 5;
17
- const FETCH_RETRIES = 7;
18
-
19
- async function fetchStream(file: File, stream: Readable): Promise<void> {
20
- const { timer, signal } = Timer.withSignal(CHUNK_TIMEOUT, 'CHUNK_TIMEOUT');
21
-
22
- const fileStream = fs.createWriteStream(file.filepath as string, { flags: 'a' });
23
-
24
- const progressStream = new Transform({
25
- transform(chunk, _encoding, callback) {
26
- this.push(chunk);
27
- file.downloaded += chunk.length;
28
- timer.reset();
29
- subject.next({ type: 'CHUNK_DOWNLOADING_UPDATE', file });
30
- callback();
31
- },
32
- });
33
-
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 });
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
+ }
41
51
  }
42
- }
43
52
 
44
- async function downloadFile(file: File): Promise<void> {
45
- file.downloaded = await getFileSize(file.filepath as string);
53
+ async downloadFile(file: CoomerFile): Promise<void> {
54
+ file.downloaded = await getFileSize(file.filepath as string);
46
55
 
47
- const response = await fetchByteRange(file.url, file.downloaded);
56
+ const response = await fetchByteRange(file.url, file.downloaded);
48
57
 
49
- if (!response?.ok && response?.status !== 416) {
50
- throw new Error(`HTTP error! status: ${response?.status}`);
51
- }
58
+ if (!response?.ok && response?.status !== 416) {
59
+ throw new Error(`HTTP error! status: ${response?.status}`);
60
+ }
52
61
 
53
- const contentLength = response.headers.get('Content-Length') as string;
62
+ const contentLength = response.headers.get('Content-Length') as string;
54
63
 
55
- if (!contentLength && file.downloaded > 0) {
56
- return;
57
- }
64
+ if (!contentLength && file.downloaded > 0) return;
58
65
 
59
- const restFileSize = parseInt(contentLength);
60
- file.size = restFileSize + file.downloaded;
66
+ const restFileSize = parseInt(contentLength);
67
+ file.size = restFileSize + file.downloaded;
61
68
 
62
- if (file.size > file.downloaded && response.body) {
63
- const stream = Readable.fromWeb(response.body);
64
- const sizeOld = file.downloaded;
69
+ if (file.size > file.downloaded && response.body) {
70
+ const stream = Readable.fromWeb(response.body);
71
+ const sizeOld = file.downloaded;
65
72
 
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));
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' });
74
84
  }
75
85
 
76
- subject.next({ type: 'FILE_DOWNLOADING_END' });
77
- }
86
+ async downloadFiles(filelist: CoomerFileList): Promise<void> {
87
+ mkdir(filelist.dirPath as string);
78
88
 
79
- export async function downloadFiles(data: File[], downloadDir: string): Promise<void> {
80
- mkdir(downloadDir);
89
+ this.subject.next({ type: 'FILES_DOWNLOADING_START', filesCount: filelist.files.length });
81
90
 
82
- subject.next({ type: 'FILES_DOWNLOADING_START', filesCount: data.length });
91
+ for (const file of filelist.files) {
92
+ this.subject.next({ type: 'FILE_DOWNLOADING_START' });
83
93
 
84
- for (const [_, file] of data.entries()) {
85
- file.filepath = path.join(downloadDir, file.name);
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));
86
102
 
87
- subject.next({ type: 'FILE_DOWNLOADING_START' });
103
+ this.subject.next({ type: 'FILE_DOWNLOADING_END' });
104
+ }
88
105
 
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' });
106
+ this.subject.next({ type: 'FILES_DOWNLOADING_END' });
99
107
  }
100
-
101
- subject.next({ type: 'FILES_DOWNLOADING_END' });
102
108
  }