coomer-downloader 3.4.0 → 3.4.1

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
@@ -7,24 +7,67 @@ import process2 from "node:process";
7
7
  import * as cheerio from "cheerio";
8
8
  import { fetch } from "undici";
9
9
 
10
+ // src/utils/io.ts
11
+ import { createHash } from "node:crypto";
12
+ import fs from "node:fs";
13
+ import { access, constants, unlink } from "node:fs/promises";
14
+ import { pipeline } from "node:stream/promises";
15
+ async function getFileSize(filepath) {
16
+ let size = 0;
17
+ if (fs.existsSync(filepath)) {
18
+ size = (await fs.promises.stat(filepath)).size || 0;
19
+ }
20
+ return size;
21
+ }
22
+ async function getFileHash(filepath) {
23
+ const hash = createHash("sha256");
24
+ const filestream = fs.createReadStream(filepath);
25
+ await pipeline(filestream, hash);
26
+ return hash.digest("hex");
27
+ }
28
+ function mkdir(filepath) {
29
+ if (!fs.existsSync(filepath)) {
30
+ fs.mkdirSync(filepath, { recursive: true });
31
+ }
32
+ }
33
+ async function deleteFile(path2) {
34
+ await access(path2, constants.F_OK);
35
+ await unlink(path2);
36
+ }
37
+ function sanitizeFilename(name) {
38
+ if (!name) return name;
39
+ return name.replace(/[<>:"/\\|?*\x00-\x1F]/g, "-").replace(/\s+/g, " ").trim().replace(/[.]+$/, "");
40
+ }
41
+
10
42
  // src/services/file.ts
43
+ var CoomerFile = class _CoomerFile {
44
+ constructor(name, url, filepath = "", size, downloaded = 0, content) {
45
+ this.name = name;
46
+ this.url = url;
47
+ this.filepath = filepath;
48
+ this.size = size;
49
+ this.downloaded = downloaded;
50
+ this.content = content;
51
+ }
52
+ active = false;
53
+ hash;
54
+ async calcDownloadedSize() {
55
+ this.downloaded = await getFileSize(this.filepath);
56
+ return this;
57
+ }
58
+ get textContent() {
59
+ const text = `${this.name || ""} ${this.content || ""}`.toLowerCase();
60
+ return text;
61
+ }
62
+ static from(f) {
63
+ return new _CoomerFile(f.name, f.url, f.filepath, f.size, f.downloaded, f.content);
64
+ }
65
+ };
66
+
67
+ // src/services/filelist.ts
11
68
  import os from "node:os";
12
69
  import path from "node:path";
13
70
 
14
- // src/logger/index.ts
15
- import pino from "pino";
16
- var logger = pino(
17
- {
18
- level: "debug"
19
- },
20
- pino.destination({
21
- dest: "./debug.log",
22
- append: false,
23
- sync: true
24
- })
25
- );
26
- var logger_default = logger;
27
-
28
71
  // src/utils/duplicates.ts
29
72
  function collectUniquesAndDuplicatesBy(xs, k) {
30
73
  const seen = /* @__PURE__ */ new Set();
@@ -70,38 +113,6 @@ function parseSizeValue(s) {
70
113
  return Math.floor(val * mult);
71
114
  }
72
115
 
73
- // src/utils/io.ts
74
- import { createHash } from "node:crypto";
75
- import fs from "node:fs";
76
- import { access, constants, unlink } from "node:fs/promises";
77
- import { pipeline } from "node:stream/promises";
78
- async function getFileSize(filepath) {
79
- let size = 0;
80
- if (fs.existsSync(filepath)) {
81
- size = (await fs.promises.stat(filepath)).size || 0;
82
- }
83
- return size;
84
- }
85
- async function getFileHash(filepath) {
86
- const hash = createHash("sha256");
87
- const filestream = fs.createReadStream(filepath);
88
- await pipeline(filestream, hash);
89
- return hash.digest("hex");
90
- }
91
- function mkdir(filepath) {
92
- if (!fs.existsSync(filepath)) {
93
- fs.mkdirSync(filepath, { recursive: true });
94
- }
95
- }
96
- async function deleteFile(path2) {
97
- await access(path2, constants.F_OK);
98
- await unlink(path2);
99
- }
100
- function sanitizeFilename(name) {
101
- if (!name) return name;
102
- return name.replace(/[<>"/\\|?*\x00-\x1F]/g, "-").replace(/\s+/g, " ").trim().replace(/[.]+$/, "");
103
- }
104
-
105
116
  // src/utils/mediatypes.ts
106
117
  function isImage(name) {
107
118
  return /\.(jpg|jpeg|png|gif|bmp|tiff|webp|avif)$/i.test(name);
@@ -113,30 +124,7 @@ function testMediaType(name, type) {
113
124
  return type === "image" ? isImage(name) : isVideo(name);
114
125
  }
115
126
 
116
- // src/services/file.ts
117
- var CoomerFile = class _CoomerFile {
118
- constructor(name, url, filepath = "", size, downloaded = 0, content) {
119
- this.name = name;
120
- this.url = url;
121
- this.filepath = filepath;
122
- this.size = size;
123
- this.downloaded = downloaded;
124
- this.content = content;
125
- }
126
- active = false;
127
- hash;
128
- async getDownloadedSize() {
129
- this.downloaded = await getFileSize(this.filepath);
130
- return this;
131
- }
132
- get textContent() {
133
- const text = `${this.name || ""} ${this.content || ""}`.toLowerCase();
134
- return text;
135
- }
136
- static from(f) {
137
- return new _CoomerFile(f.name, f.url, f.filepath, f.size, f.downloaded, f.content);
138
- }
139
- };
127
+ // src/services/filelist.ts
140
128
  var CoomerFileList = class {
141
129
  constructor(files = []) {
142
130
  this.files = files;
@@ -172,7 +160,7 @@ var CoomerFileList = class {
172
160
  }
173
161
  async calculateFileSizes() {
174
162
  for (const file of this.files) {
175
- await file.getDownloadedSize();
163
+ await file.calcDownloadedSize();
176
164
  }
177
165
  return this;
178
166
  }
@@ -187,8 +175,6 @@ var CoomerFileList = class {
187
175
  file.hash = await getFileHash(file.filepath);
188
176
  }
189
177
  const { duplicates } = collectUniquesAndDuplicatesBy(this.files, "hash");
190
- console.log({ duplicates });
191
- logger_default.debug(`duplicates: ${JSON.stringify(duplicates)}`);
192
178
  duplicates.forEach((f) => {
193
179
  deleteFile(f.filepath);
194
180
  });
@@ -212,7 +198,9 @@ function decryptEncryptedUrl(encryptionData) {
212
198
  const secretKey = `SECRET_KEY_${Math.floor(encryptionData.timestamp / 3600)}`;
213
199
  const encryptedUrlBuffer = Buffer.from(encryptionData.url, "base64");
214
200
  const secretKeyBuffer = Buffer.from(secretKey, "utf-8");
215
- return Array.from(encryptedUrlBuffer).map((byte, i) => String.fromCharCode(byte ^ secretKeyBuffer[i % secretKeyBuffer.length])).join("");
201
+ return Array.from(encryptedUrlBuffer).map(
202
+ (byte, i) => String.fromCharCode(byte ^ secretKeyBuffer[i % secretKeyBuffer.length])
203
+ ).join("");
216
204
  }
217
205
  async function getFileData(url, name) {
218
206
  const slug = url.split("/").pop();
@@ -659,7 +647,7 @@ import { Box as Box6, Spacer as Spacer2, Text as Text6 } from "ink";
659
647
  import React7 from "react";
660
648
 
661
649
  // package.json
662
- var version = "3.4.0";
650
+ var version = "3.4.1";
663
651
 
664
652
  // src/cli/ui/components/titlebar.tsx
665
653
  function TitleBar() {
@@ -667,18 +655,30 @@ function TitleBar() {
667
655
  }
668
656
 
669
657
  // src/cli/ui/hooks/downloader.ts
670
- import { useEffect as useEffect2, useState as useState2 } from "react";
658
+ import { useRef, useSyncExternalStore } from "react";
671
659
  var useDownloaderHook = () => {
672
660
  const downloader = useInkStore((state) => state.downloader);
673
- const filelist = downloader?.filelist;
674
- const [_, setHelper] = useState2(0);
675
- useEffect2(() => {
676
- downloader?.subject.subscribe(({ type }) => {
677
- if (type === "FILE_DOWNLOADING_START" || type === "FILE_DOWNLOADING_END" || type === "CHUNK_DOWNLOADING_UPDATE") {
678
- setHelper(Date.now());
679
- }
680
- });
681
- });
661
+ const versionRef = useRef(0);
662
+ useSyncExternalStore(
663
+ (onStoreChange) => {
664
+ if (!downloader) return () => {
665
+ };
666
+ const sub = downloader.subject.subscribe(({ type }) => {
667
+ const targets = [
668
+ "FILE_DOWNLOADING_START",
669
+ "FILE_DOWNLOADING_END",
670
+ "CHUNK_DOWNLOADING_UPDATE"
671
+ ];
672
+ if (targets.includes(type)) {
673
+ versionRef.current++;
674
+ onStoreChange();
675
+ }
676
+ });
677
+ return () => sub.unsubscribe();
678
+ },
679
+ () => versionRef.current
680
+ );
681
+ return downloader?.filelist;
682
682
  };
683
683
 
684
684
  // src/cli/ui/hooks/input.ts
@@ -699,11 +699,8 @@ var useInputHook = () => {
699
699
  // src/cli/ui/app.tsx
700
700
  function App() {
701
701
  useInputHook();
702
- useDownloaderHook();
703
- const downloader = useInkStore((state) => state.downloader);
704
- const filelist = downloader?.filelist;
705
- const isFilelist = filelist instanceof CoomerFileList;
706
- return /* @__PURE__ */ React8.createElement(Box7, { borderStyle: "single", flexDirection: "column", borderColor: "blue", width: 80 }, /* @__PURE__ */ React8.createElement(TitleBar, null), !isFilelist ? /* @__PURE__ */ React8.createElement(Loading, null) : /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(Box7, null, /* @__PURE__ */ React8.createElement(Box7, null, /* @__PURE__ */ React8.createElement(FileListStateBox, { filelist })), /* @__PURE__ */ React8.createElement(Box7, { flexBasis: 30 }, /* @__PURE__ */ React8.createElement(KeyboardControlsInfo, null))), filelist.getActiveFiles().map((file) => {
702
+ const filelist = useDownloaderHook();
703
+ return /* @__PURE__ */ React8.createElement(Box7, { borderStyle: "single", flexDirection: "column", borderColor: "blue", width: 80 }, /* @__PURE__ */ React8.createElement(TitleBar, null), !(filelist instanceof CoomerFileList) ? /* @__PURE__ */ React8.createElement(Loading, null) : /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(Box7, null, /* @__PURE__ */ React8.createElement(Box7, null, /* @__PURE__ */ React8.createElement(FileListStateBox, { filelist })), /* @__PURE__ */ React8.createElement(Box7, { flexBasis: 30 }, /* @__PURE__ */ React8.createElement(KeyboardControlsInfo, null))), filelist?.getActiveFiles().map((file) => {
707
704
  return /* @__PURE__ */ React8.createElement(FileBox, { file, key: file.name });
708
705
  })));
709
706
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coomer-downloader",
3
- "version": "3.4.0",
3
+ "version": "3.4.1",
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,6 +1,7 @@
1
1
  import * as cheerio from 'cheerio';
2
2
  import { fetch } from 'undici';
3
- import { CoomerFile, CoomerFileList } from '../services/file';
3
+ import { CoomerFile } from '../services/file';
4
+ import { CoomerFileList } from '../services/filelist';
4
5
 
5
6
  type EncData = { url: string; timestamp: number };
6
7
 
@@ -18,7 +19,9 @@ function decryptEncryptedUrl(encryptionData: EncData) {
18
19
  const encryptedUrlBuffer = Buffer.from(encryptionData.url, 'base64');
19
20
  const secretKeyBuffer = Buffer.from(secretKey, 'utf-8');
20
21
  return Array.from(encryptedUrlBuffer)
21
- .map((byte, i) => String.fromCharCode(byte ^ secretKeyBuffer[i % secretKeyBuffer.length]))
22
+ .map((byte, i) =>
23
+ String.fromCharCode(byte ^ secretKeyBuffer[i % secretKeyBuffer.length]),
24
+ )
22
25
  .join('');
23
26
  }
24
27
 
@@ -1,4 +1,5 @@
1
- import { CoomerFile, CoomerFileList } from '../services/file';
1
+ import { CoomerFile } from '../services/file';
2
+ import { CoomerFileList } from '../services/filelist';
2
3
  import { isImage } from '../utils/mediatypes';
3
4
  import { fetchWithGlobalHeader, setGlobalHeaders } from '../utils/requests';
4
5
 
package/src/api/gofile.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { fetch } from 'undici';
2
- import { CoomerFile, CoomerFileList } from '../services/file';
2
+ import { CoomerFile } from '../services/file';
3
+ import { CoomerFileList } from '../services/filelist';
3
4
  import { setGlobalHeaders } from '../utils/requests';
4
5
 
5
6
  type GoFileAPIToken = { status: string; data: { token: string } };
package/src/api/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { CoomerFileList } from '../services/file';
1
+ import type { CoomerFileList } from '../services/filelist';
2
2
  import { getBunkrData } from './bunkr';
3
3
  import { getCoomerData } from './coomer-api';
4
4
  import { getGofileData } from './gofile';
@@ -1,6 +1,7 @@
1
1
  import * as cheerio from 'cheerio';
2
2
  import { fetch } from 'undici';
3
- import { CoomerFile, CoomerFileList } from '../services/file';
3
+ import { CoomerFile } from '../services/file';
4
+ import { CoomerFileList } from '../services/filelist';
4
5
 
5
6
  async function getUserPage(user: string, offset: number) {
6
7
  const url = `https://nsfw.xxx/page/${offset}?nsfw[]=0&types[]=image&types[]=video&types[]=gallery&slider=1&jsload=1&user=${user}&_=${Date.now()}`;
@@ -38,7 +39,8 @@ async function getPostsData(posts: string[]): Promise<CoomerFileList> {
38
39
  if (!src) continue;
39
40
 
40
41
  const slug = post.split('post/')[1].split('?')[0];
41
- const date = $('.sh-section .sh-section__passed').first().text().replace(/ /g, '-') || '';
42
+ const date =
43
+ $('.sh-section .sh-section__passed').first().text().replace(/ /g, '-') || '';
42
44
 
43
45
  const ext = src.split('.').pop();
44
46
  const name = `${slug}-${date}.${ext}`;
@@ -1,4 +1,5 @@
1
- import { CoomerFile, CoomerFileList } from '../services/file';
1
+ import { CoomerFile } from '../services/file';
2
+ import { CoomerFileList } from '../services/filelist';
2
3
 
3
4
  export async function getPlainFileData(url: string): Promise<CoomerFileList> {
4
5
  const name = url.split('/').pop() as string;
@@ -1,23 +1,24 @@
1
1
  import { Box } from 'ink';
2
2
  import React from 'react';
3
- import { CoomerFileList } from '../../services/file';
4
- import { FileBox, FileListStateBox, KeyboardControlsInfo, Loading, TitleBar } from './components';
3
+ import { CoomerFileList } from '../../services/filelist';
4
+ import {
5
+ FileBox,
6
+ FileListStateBox,
7
+ KeyboardControlsInfo,
8
+ Loading,
9
+ TitleBar,
10
+ } from './components';
5
11
  import { useDownloaderHook } from './hooks/downloader';
6
12
  import { useInputHook } from './hooks/input';
7
- import { useInkStore } from './store';
8
13
 
9
14
  export function App() {
10
15
  useInputHook();
11
- useDownloaderHook();
12
-
13
- const downloader = useInkStore((state) => state.downloader);
14
- const filelist = downloader?.filelist;
15
- const isFilelist = filelist instanceof CoomerFileList;
16
+ const filelist = useDownloaderHook();
16
17
 
17
18
  return (
18
19
  <Box borderStyle="single" flexDirection="column" borderColor="blue" width={80}>
19
20
  <TitleBar />
20
- {!isFilelist ? (
21
+ {!(filelist instanceof CoomerFileList) ? (
21
22
  <Loading />
22
23
  ) : (
23
24
  <>
@@ -30,7 +31,7 @@ export function App() {
30
31
  </Box>
31
32
  </Box>
32
33
 
33
- {filelist.getActiveFiles().map((file) => {
34
+ {filelist?.getActiveFiles().map((file) => {
34
35
  return <FileBox file={file} key={file.name} />;
35
36
  })}
36
37
  </>
@@ -1,21 +1,31 @@
1
- import { useEffect, useState } from 'react';
1
+ import { useRef, useSyncExternalStore } from 'react';
2
2
  import { useInkStore } from '../store';
3
3
 
4
4
  export const useDownloaderHook = () => {
5
5
  const downloader = useInkStore((state) => state.downloader);
6
- const filelist = downloader?.filelist;
7
6
 
8
- const [_, setHelper] = useState(0);
7
+ const versionRef = useRef(0);
9
8
 
10
- useEffect(() => {
11
- downloader?.subject.subscribe(({ type }) => {
12
- if (
13
- type === 'FILE_DOWNLOADING_START' ||
14
- type === 'FILE_DOWNLOADING_END' ||
15
- type === 'CHUNK_DOWNLOADING_UPDATE'
16
- ) {
17
- setHelper(Date.now());
18
- }
19
- });
20
- });
9
+ useSyncExternalStore(
10
+ (onStoreChange) => {
11
+ if (!downloader) return () => {};
12
+
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)) {
21
+ versionRef.current++;
22
+ onStoreChange();
23
+ }
24
+ });
25
+ return () => sub.unsubscribe();
26
+ },
27
+ () => versionRef.current,
28
+ );
29
+
30
+ return downloader?.filelist;
21
31
  };
@@ -8,7 +8,8 @@ import { deleteFile, getFileSize, mkdir } from '../utils/io';
8
8
  import { sleep } from '../utils/promise';
9
9
  import { fetchByteRange } from '../utils/requests';
10
10
  import { Timer } from '../utils/timer';
11
- import type { CoomerFile, CoomerFileList } from './file';
11
+ import type { CoomerFile } from './file';
12
+ import type { CoomerFileList } from './filelist';
12
13
 
13
14
  export class Downloader {
14
15
  public subject = new Subject<DownloaderSubject>();
@@ -1,11 +1,4 @@
1
- import os from 'node:os';
2
- import path from 'node:path';
3
- import logger from '../logger';
4
- import type { MediaType } from '../types';
5
- import { collectUniquesAndDuplicatesBy, removeDuplicatesBy } from '../utils/duplicates';
6
- import { filterString } from '../utils/filters';
7
- import { deleteFile, getFileHash, getFileSize, sanitizeFilename } from '../utils/io';
8
- import { testMediaType } from '../utils/mediatypes';
1
+ import { getFileSize } from '../utils/io';
9
2
 
10
3
  export class CoomerFile {
11
4
  public active = false;
@@ -20,7 +13,7 @@ export class CoomerFile {
20
13
  public content?: string,
21
14
  ) {}
22
15
 
23
- public async getDownloadedSize() {
16
+ public async calcDownloadedSize() {
24
17
  this.downloaded = await getFileSize(this.filepath as string);
25
18
  return this;
26
19
  }
@@ -34,80 +27,3 @@ export class CoomerFile {
34
27
  return new CoomerFile(f.name, f.url, f.filepath, f.size, f.downloaded, f.content);
35
28
  }
36
29
  }
37
-
38
- export class CoomerFileList {
39
- public dirPath?: string;
40
- public dirName?: string;
41
-
42
- constructor(public files: CoomerFile[] = []) {}
43
-
44
- public setDirPath(dir: string, dirName?: string) {
45
- dirName = dirName || (this.dirName as string);
46
-
47
- if (dir === './') {
48
- this.dirPath = path.resolve(dir, dirName);
49
- } else {
50
- this.dirPath = path.join(os.homedir(), path.join(dir, dirName));
51
- }
52
-
53
- this.files.forEach((file) => {
54
- const safeName = sanitizeFilename(file.name) || file.name;
55
- file.filepath = path.join(this.dirPath as string, safeName);
56
- });
57
-
58
- return this;
59
- }
60
-
61
- public filterByText(include: string, exclude: string) {
62
- this.files = this.files.filter((f) => filterString(f.textContent, include, exclude));
63
- return this;
64
- }
65
-
66
- public filterByMediaType(media?: string) {
67
- if (media) {
68
- this.files = this.files.filter((f) => testMediaType(f.name, media as MediaType));
69
- }
70
- return this;
71
- }
72
-
73
- public skip(n: number) {
74
- this.files = this.files.slice(n);
75
- return this;
76
- }
77
-
78
- public async calculateFileSizes() {
79
- for (const file of this.files) {
80
- await file.getDownloadedSize();
81
- }
82
- return this;
83
- }
84
-
85
- public getActiveFiles() {
86
- return this.files.filter((f) => f.active);
87
- }
88
-
89
- public getDownloaded() {
90
- return this.files.filter((f) => f.size && f.size <= f.downloaded);
91
- }
92
-
93
- public async removeDuplicatesByHash() {
94
- for (const file of this.files) {
95
- file.hash = await getFileHash(file.filepath);
96
- }
97
-
98
- const { duplicates } = collectUniquesAndDuplicatesBy(this.files, 'hash');
99
-
100
- console.log({ duplicates });
101
-
102
- logger.debug(`duplicates: ${JSON.stringify(duplicates)}`);
103
-
104
- duplicates.forEach((f) => {
105
- deleteFile(f.filepath);
106
- });
107
- }
108
-
109
- public removeURLDuplicates() {
110
- this.files = removeDuplicatesBy(this.files, 'url');
111
- return this;
112
- }
113
- }
@@ -0,0 +1,86 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import logger from '../logger';
4
+ import type { MediaType } from '../types';
5
+ import { collectUniquesAndDuplicatesBy, removeDuplicatesBy } from '../utils/duplicates';
6
+ import { filterString } from '../utils/filters';
7
+ import { deleteFile, getFileHash, sanitizeFilename } from '../utils/io';
8
+ import { testMediaType } from '../utils/mediatypes';
9
+ import type { CoomerFile } from './file';
10
+
11
+ export class CoomerFileList {
12
+ public dirPath?: string;
13
+ public dirName?: string;
14
+
15
+ constructor(public files: CoomerFile[] = []) {}
16
+
17
+ public setDirPath(dir: string, dirName?: string) {
18
+ dirName = dirName || (this.dirName as string);
19
+
20
+ if (dir === './') {
21
+ this.dirPath = path.resolve(dir, dirName);
22
+ } else {
23
+ this.dirPath = path.join(os.homedir(), path.join(dir, dirName));
24
+ }
25
+
26
+ this.files.forEach((file) => {
27
+ const safeName = sanitizeFilename(file.name) || file.name;
28
+ file.filepath = path.join(this.dirPath as string, safeName);
29
+ });
30
+
31
+ return this;
32
+ }
33
+
34
+ public filterByText(include: string, exclude: string) {
35
+ this.files = this.files.filter((f) => filterString(f.textContent, include, exclude));
36
+ return this;
37
+ }
38
+
39
+ public filterByMediaType(media?: string) {
40
+ if (media) {
41
+ this.files = this.files.filter((f) => testMediaType(f.name, media as MediaType));
42
+ }
43
+ return this;
44
+ }
45
+
46
+ public skip(n: number) {
47
+ this.files = this.files.slice(n);
48
+ return this;
49
+ }
50
+
51
+ public async calculateFileSizes() {
52
+ for (const file of this.files) {
53
+ await file.calcDownloadedSize();
54
+ }
55
+ return this;
56
+ }
57
+
58
+ public getActiveFiles() {
59
+ return this.files.filter((f) => f.active);
60
+ }
61
+
62
+ public getDownloaded() {
63
+ return this.files.filter((f) => f.size && f.size <= f.downloaded);
64
+ }
65
+
66
+ public async removeDuplicatesByHash() {
67
+ for (const file of this.files) {
68
+ file.hash = await getFileHash(file.filepath);
69
+ }
70
+
71
+ const { duplicates } = collectUniquesAndDuplicatesBy(this.files, 'hash');
72
+
73
+ // console.log({ duplicates });
74
+
75
+ // logger.debug(`duplicates: ${JSON.stringify(duplicates)}`);
76
+
77
+ duplicates.forEach((f) => {
78
+ deleteFile(f.filepath);
79
+ });
80
+ }
81
+
82
+ public removeURLDuplicates() {
83
+ this.files = removeDuplicatesBy(this.files, 'url');
84
+ return this;
85
+ }
86
+ }