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 +87 -90
- package/package.json +1 -1
- package/src/api/bunkr.ts +5 -2
- package/src/api/coomer-api.ts +2 -1
- package/src/api/gofile.ts +2 -1
- package/src/api/index.ts +1 -1
- package/src/api/nsfw.xxx.ts +4 -2
- package/src/api/plain-curl.ts +2 -1
- package/src/cli/ui/app.tsx +11 -10
- package/src/cli/ui/hooks/downloader.ts +24 -14
- package/src/services/downloader.ts +2 -1
- package/src/services/file.ts +2 -86
- package/src/services/filelist.ts +86 -0
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/
|
|
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.
|
|
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(
|
|
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.
|
|
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 {
|
|
658
|
+
import { useRef, useSyncExternalStore } from "react";
|
|
671
659
|
var useDownloaderHook = () => {
|
|
672
660
|
const downloader = useInkStore((state) => state.downloader);
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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
|
-
|
|
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
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
|
|
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) =>
|
|
22
|
+
.map((byte, i) =>
|
|
23
|
+
String.fromCharCode(byte ^ secretKeyBuffer[i % secretKeyBuffer.length]),
|
|
24
|
+
)
|
|
22
25
|
.join('');
|
|
23
26
|
}
|
|
24
27
|
|
package/src/api/coomer-api.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { CoomerFile
|
|
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
|
|
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
package/src/api/nsfw.xxx.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as cheerio from 'cheerio';
|
|
2
2
|
import { fetch } from 'undici';
|
|
3
|
-
import { CoomerFile
|
|
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 =
|
|
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}`;
|
package/src/api/plain-curl.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { CoomerFile
|
|
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;
|
package/src/cli/ui/app.tsx
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import { Box } from 'ink';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { CoomerFileList } from '../../services/
|
|
4
|
-
import {
|
|
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
|
-
{!
|
|
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
|
|
34
|
+
{filelist?.getActiveFiles().map((file) => {
|
|
34
35
|
return <FileBox file={file} key={file.name} />;
|
|
35
36
|
})}
|
|
36
37
|
</>
|
|
@@ -1,21 +1,31 @@
|
|
|
1
|
-
import {
|
|
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
|
|
7
|
+
const versionRef = useRef(0);
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
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>();
|
package/src/services/file.ts
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
import
|
|
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
|
|
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
|
+
}
|