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/dist/index.js +365 -242
- package/package.json +1 -1
- package/src/api/bunkr.ts +16 -17
- package/src/api/coomer-api.ts +25 -26
- package/src/api/gofile.ts +18 -14
- package/src/api/index.ts +16 -16
- package/src/api/nsfw.xxx.ts +9 -10
- package/src/api/plain-curl.ts +7 -11
- package/src/args-handler.ts +1 -4
- package/src/index.ts +13 -14
- package/src/types/index.ts +0 -22
- package/src/utils/downloader.ts +76 -69
- package/src/utils/file.ts +75 -26
- package/src/utils/filters.ts +27 -16
- package/src/utils/index.ts +9 -4
- package/src/utils/multibar.ts +20 -21
- package/src/utils/promise.ts +53 -0
- package/src/utils/requests.ts +2 -16
- package/src/utils/strings.ts +1 -10
- package/src/utils/timer.ts +19 -1
- /package/src/utils/{files.ts → io.ts} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,86 +1,151 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import path2 from "node:path";
|
|
6
4
|
import process2 from "node:process";
|
|
7
5
|
|
|
8
6
|
// src/api/bunkr.ts
|
|
9
7
|
import * as cheerio from "cheerio";
|
|
10
|
-
import { fetch
|
|
8
|
+
import { fetch } from "undici";
|
|
11
9
|
|
|
12
|
-
// src/utils/
|
|
13
|
-
import
|
|
10
|
+
// src/utils/file.ts
|
|
11
|
+
import os from "node:os";
|
|
14
12
|
import path from "node:path";
|
|
15
|
-
import { Readable, Transform } from "node:stream";
|
|
16
|
-
import { pipeline } from "node:stream/promises";
|
|
17
|
-
import { Subject } from "rxjs";
|
|
18
13
|
|
|
19
|
-
// src/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (attempts < 2 && isImage(url)) {
|
|
23
|
-
return url.replace(/\/data\//, "/thumbnail/data/").replace(/n\d\./, "img.");
|
|
24
|
-
}
|
|
25
|
-
const server = url.match(/n\d\./)?.[0].slice(0, 2);
|
|
26
|
-
const i = SERVERS.indexOf(server);
|
|
27
|
-
if (i !== -1) {
|
|
28
|
-
const newServer = SERVERS[(i + 1) % SERVERS.length];
|
|
29
|
-
return url.replace(/n\d./, `${newServer}.`);
|
|
30
|
-
}
|
|
31
|
-
return url;
|
|
14
|
+
// src/utils/filters.ts
|
|
15
|
+
function isImage(name) {
|
|
16
|
+
return /\.(jpg|jpeg|png|gif|bmp|tiff|webp|avif)$/i.test(name);
|
|
32
17
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const result = await fetch_(url).then((r) => r.json());
|
|
36
|
-
return result;
|
|
18
|
+
function isVideo(name) {
|
|
19
|
+
return /\.(mp4|m4v|avi|mov|mkv|webm|flv|wmv|mpeg|mpg|3gp)$/i.test(name);
|
|
37
20
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const posts = await fetch_(url).then((r) => r.json());
|
|
41
|
-
return posts;
|
|
21
|
+
function testMediaType(name, type) {
|
|
22
|
+
return type === "all" ? true : type === "image" ? isImage(name) : isVideo(name);
|
|
42
23
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
24
|
+
function includesAllWords(str, words) {
|
|
25
|
+
if (!words.length) return true;
|
|
26
|
+
return words.every((w) => str.includes(w));
|
|
27
|
+
}
|
|
28
|
+
function includesNoWords(str, words) {
|
|
29
|
+
if (!words.length) return true;
|
|
30
|
+
return words.every((w) => !str.includes(w));
|
|
31
|
+
}
|
|
32
|
+
function parseQuery(query) {
|
|
33
|
+
return query.split(",").map((x) => x.toLowerCase().trim()).filter((_) => _);
|
|
34
|
+
}
|
|
35
|
+
function filterString(text, include, exclude) {
|
|
36
|
+
return includesAllWords(text, parseQuery(include)) && includesNoWords(text, parseQuery(exclude));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/utils/file.ts
|
|
40
|
+
var CoomerFile = class _CoomerFile {
|
|
41
|
+
constructor(name, url, filepath, size, downloaded, content) {
|
|
42
|
+
this.name = name;
|
|
43
|
+
this.url = url;
|
|
44
|
+
this.filepath = filepath;
|
|
45
|
+
this.size = size;
|
|
46
|
+
this.downloaded = downloaded;
|
|
47
|
+
this.content = content;
|
|
50
48
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
49
|
+
state = "pause";
|
|
50
|
+
get textContent() {
|
|
51
|
+
const text = `${this.name || ""} ${this.content || ""}`.toLowerCase();
|
|
52
|
+
return text;
|
|
53
|
+
}
|
|
54
|
+
static from(f) {
|
|
55
|
+
return new _CoomerFile(f.name, f.url, f.filepath, f.size, f.downloaded, f.content);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
var CoomerFileList = class {
|
|
59
|
+
constructor(files = []) {
|
|
60
|
+
this.files = files;
|
|
61
|
+
}
|
|
62
|
+
dirPath;
|
|
63
|
+
dirName;
|
|
64
|
+
setDirPath(dir, dirName) {
|
|
65
|
+
dirName = dirName || this.dirName;
|
|
66
|
+
if (dir === "./") {
|
|
67
|
+
this.dirPath = path.resolve(dir, dirName);
|
|
68
|
+
} else {
|
|
69
|
+
this.dirPath = path.join(os.homedir(), path.join(dir, dirName));
|
|
70
|
+
}
|
|
71
|
+
this.files.forEach((file) => {
|
|
72
|
+
file.filepath = path.join(this.dirPath, file.name);
|
|
62
73
|
});
|
|
63
|
-
|
|
74
|
+
return this;
|
|
64
75
|
}
|
|
65
|
-
|
|
76
|
+
filterByText(include, exclude) {
|
|
77
|
+
this.files = this.files.filter((f) => filterString(f.textContent, include, exclude));
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
filterByMediaType(media) {
|
|
81
|
+
if (media) {
|
|
82
|
+
this.files = this.files.filter((f) => testMediaType(f.name, media));
|
|
83
|
+
}
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
skip(n) {
|
|
87
|
+
this.files = this.files.slice(n);
|
|
88
|
+
return this;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// src/api/bunkr.ts
|
|
93
|
+
async function getEncryptionData(slug) {
|
|
94
|
+
const response = await fetch("https://bunkr.cr/api/vs", {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: { "Content-Type": "application/json" },
|
|
97
|
+
body: JSON.stringify({ slug })
|
|
98
|
+
});
|
|
99
|
+
return await response.json();
|
|
66
100
|
}
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
const { name } = await getUserProfileAPI({ domain, service, id });
|
|
73
|
-
return { domain, service, id, name };
|
|
101
|
+
function decryptEncryptedUrl(encryptionData) {
|
|
102
|
+
const secretKey = `SECRET_KEY_${Math.floor(encryptionData.timestamp / 3600)}`;
|
|
103
|
+
const encryptedUrlBuffer = Buffer.from(encryptionData.url, "base64");
|
|
104
|
+
const secretKeyBuffer = Buffer.from(secretKey, "utf-8");
|
|
105
|
+
return Array.from(encryptedUrlBuffer).map((byte, i) => String.fromCharCode(byte ^ secretKeyBuffer[i % secretKeyBuffer.length])).join("");
|
|
74
106
|
}
|
|
75
|
-
async function
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
107
|
+
async function getFileData(url, name) {
|
|
108
|
+
const slug = url.split("/").pop();
|
|
109
|
+
const encryptionData = await getEncryptionData(slug);
|
|
110
|
+
const src = decryptEncryptedUrl(encryptionData);
|
|
111
|
+
return CoomerFile.from({ name, url: src });
|
|
112
|
+
}
|
|
113
|
+
async function getGalleryFiles(url) {
|
|
114
|
+
const filelist = new CoomerFileList();
|
|
115
|
+
const page = await fetch(url).then((r) => r.text());
|
|
116
|
+
const $ = cheerio.load(page);
|
|
117
|
+
const dirName = $("title").text();
|
|
118
|
+
filelist.dirName = `${dirName.split("|")[0].trim()}-bunkr`;
|
|
119
|
+
const url_ = new URL(url);
|
|
120
|
+
if (url_.pathname.startsWith("/f/")) {
|
|
121
|
+
const fileName = $("h1").text();
|
|
122
|
+
const singleFile = await getFileData(url, fileName);
|
|
123
|
+
filelist.files.push(singleFile);
|
|
124
|
+
return filelist;
|
|
125
|
+
}
|
|
126
|
+
const fileNames = Array.from($("div[title]").map((_, e) => $(e).attr("title")));
|
|
127
|
+
const data = Array.from($("a").map((_, e) => $(e).attr("href"))).filter((a) => /\/f\/\w+/.test(a)).map((a, i) => ({
|
|
128
|
+
url: `${url_.origin}${a}`,
|
|
129
|
+
name: fileNames[i] || url.split("/").pop()
|
|
130
|
+
}));
|
|
131
|
+
for (const { name, url: url2 } of data) {
|
|
132
|
+
const res = await getFileData(url2, name);
|
|
133
|
+
filelist.files.push(res);
|
|
134
|
+
}
|
|
135
|
+
return filelist;
|
|
136
|
+
}
|
|
137
|
+
async function getBunkrData(url) {
|
|
138
|
+
const filelist = await getGalleryFiles(url);
|
|
139
|
+
return filelist;
|
|
81
140
|
}
|
|
82
141
|
|
|
83
|
-
// src/utils/
|
|
142
|
+
// src/utils/downloader.ts
|
|
143
|
+
import fs2 from "node:fs";
|
|
144
|
+
import { Readable, Transform } from "node:stream";
|
|
145
|
+
import { pipeline } from "node:stream/promises";
|
|
146
|
+
import { Subject } from "rxjs";
|
|
147
|
+
|
|
148
|
+
// src/utils/io.ts
|
|
84
149
|
import fs from "node:fs";
|
|
85
150
|
async function getFileSize(filepath) {
|
|
86
151
|
let size = 0;
|
|
@@ -95,10 +160,50 @@ function mkdir(filepath) {
|
|
|
95
160
|
}
|
|
96
161
|
}
|
|
97
162
|
|
|
163
|
+
// src/utils/promise.ts
|
|
164
|
+
async function sleep(time) {
|
|
165
|
+
return new Promise((resolve) => setTimeout(resolve, time));
|
|
166
|
+
}
|
|
167
|
+
var PromiseRetry = class _PromiseRetry {
|
|
168
|
+
retries;
|
|
169
|
+
delay;
|
|
170
|
+
callback;
|
|
171
|
+
constructor(options) {
|
|
172
|
+
this.retries = options.retries || 3;
|
|
173
|
+
this.delay = options.delay || 1e3;
|
|
174
|
+
this.callback = options.callback;
|
|
175
|
+
}
|
|
176
|
+
async execute(fn) {
|
|
177
|
+
let retries = this.retries;
|
|
178
|
+
while (true) {
|
|
179
|
+
try {
|
|
180
|
+
return await fn();
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (retries <= 0) {
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
if (this.callback) {
|
|
186
|
+
const res = this.callback(retries, error);
|
|
187
|
+
if (res) {
|
|
188
|
+
const { newRetries } = res;
|
|
189
|
+
if (newRetries === 0) throw error;
|
|
190
|
+
this.retries = newRetries || retries;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
await sleep(this.delay);
|
|
194
|
+
retries--;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
static create(options) {
|
|
199
|
+
return new _PromiseRetry(options);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
98
203
|
// src/utils/requests.ts
|
|
99
204
|
import { CookieAgent } from "http-cookie-agent/undici";
|
|
100
205
|
import { CookieJar } from "tough-cookie";
|
|
101
|
-
import { fetch
|
|
206
|
+
import { fetch as fetch2, interceptors, setGlobalDispatcher } from "undici";
|
|
102
207
|
function setCookieJarDispatcher() {
|
|
103
208
|
const jar = new CookieJar();
|
|
104
209
|
const agent = new CookieAgent({ cookies: { jar } }).compose(interceptors.retry()).compose(interceptors.redirect({ maxRedirections: 3 }));
|
|
@@ -114,18 +219,18 @@ function setGlobalHeaders(headers) {
|
|
|
114
219
|
HeadersDefault.set(k, headers[k]);
|
|
115
220
|
});
|
|
116
221
|
}
|
|
117
|
-
function
|
|
222
|
+
function fetchWithGlobalHeader(url) {
|
|
118
223
|
const requestHeaders = new Headers(HeadersDefault);
|
|
119
|
-
return
|
|
224
|
+
return fetch2(url, { headers: requestHeaders });
|
|
120
225
|
}
|
|
121
226
|
function fetchByteRange(url, downloadedSize) {
|
|
122
227
|
const requestHeaders = new Headers(HeadersDefault);
|
|
123
228
|
requestHeaders.set("Range", `bytes=${downloadedSize}-`);
|
|
124
|
-
return
|
|
229
|
+
return fetch2(url, { headers: requestHeaders });
|
|
125
230
|
}
|
|
126
231
|
|
|
127
232
|
// src/utils/timer.ts
|
|
128
|
-
var Timer = class {
|
|
233
|
+
var Timer = class _Timer {
|
|
129
234
|
constructor(timeout = 1e4, timeoutCallback) {
|
|
130
235
|
this.timeout = timeout;
|
|
131
236
|
this.timeoutCallback = timeoutCallback;
|
|
@@ -133,7 +238,10 @@ var Timer = class {
|
|
|
133
238
|
}
|
|
134
239
|
timer = void 0;
|
|
135
240
|
start() {
|
|
136
|
-
this.timer = setTimeout(
|
|
241
|
+
this.timer = setTimeout(() => {
|
|
242
|
+
this.stop();
|
|
243
|
+
this.timeoutCallback();
|
|
244
|
+
}, this.timeout);
|
|
137
245
|
return this;
|
|
138
246
|
}
|
|
139
247
|
stop() {
|
|
@@ -148,95 +256,91 @@ var Timer = class {
|
|
|
148
256
|
this.start();
|
|
149
257
|
return this;
|
|
150
258
|
}
|
|
259
|
+
static withSignal(timeout, message) {
|
|
260
|
+
const controller = new AbortController();
|
|
261
|
+
const callback = () => {
|
|
262
|
+
controller.abort(message);
|
|
263
|
+
};
|
|
264
|
+
const timer = new _Timer(timeout, callback).start();
|
|
265
|
+
return {
|
|
266
|
+
timer,
|
|
267
|
+
signal: controller.signal
|
|
268
|
+
};
|
|
269
|
+
}
|
|
151
270
|
};
|
|
152
271
|
|
|
153
272
|
// src/utils/downloader.ts
|
|
154
|
-
var
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
273
|
+
var Downloader = class {
|
|
274
|
+
constructor(chunkTimeout = 3e4, chunkFetchRetries = 5, fetchRetries = 7) {
|
|
275
|
+
this.chunkTimeout = chunkTimeout;
|
|
276
|
+
this.chunkFetchRetries = chunkFetchRetries;
|
|
277
|
+
this.fetchRetries = fetchRetries;
|
|
278
|
+
}
|
|
279
|
+
subject = new Subject();
|
|
280
|
+
async fetchStream(file, stream) {
|
|
281
|
+
const { subject, chunkTimeout } = this;
|
|
282
|
+
const { timer, signal } = Timer.withSignal(chunkTimeout, "chunkTimeout");
|
|
283
|
+
const fileStream = fs2.createWriteStream(file.filepath, { flags: "a" });
|
|
284
|
+
const progressStream = new Transform({
|
|
285
|
+
transform(chunk, _encoding, callback) {
|
|
286
|
+
this.push(chunk);
|
|
287
|
+
file.downloaded += chunk.length;
|
|
288
|
+
timer.reset();
|
|
289
|
+
subject.next({ type: "CHUNK_DOWNLOADING_UPDATE", file });
|
|
290
|
+
callback();
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
try {
|
|
294
|
+
subject.next({ type: "CHUNK_DOWNLOADING_START", file });
|
|
295
|
+
await pipeline(stream, progressStream, fileStream, { signal });
|
|
296
|
+
} catch (error) {
|
|
297
|
+
console.error(error.name === "AbortError" ? signal.reason : error);
|
|
298
|
+
} finally {
|
|
299
|
+
subject.next({ type: "CHUNK_DOWNLOADING_END", file });
|
|
171
300
|
}
|
|
172
|
-
});
|
|
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
301
|
}
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
async function downloadFile(file, attempts = DOWNLOAD_ATTEMPTS) {
|
|
185
|
-
const downloadedOld = file.downloaded || 0;
|
|
186
|
-
try {
|
|
302
|
+
async downloadFile(file) {
|
|
187
303
|
file.downloaded = await getFileSize(file.filepath);
|
|
188
|
-
const response = await fetchByteRange(file.url, file.downloaded)
|
|
189
|
-
(error) => handleFetchError(error, file, --attempts)
|
|
190
|
-
);
|
|
304
|
+
const response = await fetchByteRange(file.url, file.downloaded);
|
|
191
305
|
if (!response?.ok && response?.status !== 416) {
|
|
192
306
|
throw new Error(`HTTP error! status: ${response?.status}`);
|
|
193
307
|
}
|
|
194
308
|
const contentLength = response.headers.get("Content-Length");
|
|
195
|
-
if (!contentLength && file.downloaded > 0)
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
309
|
+
if (!contentLength && file.downloaded > 0) return;
|
|
198
310
|
const restFileSize = parseInt(contentLength);
|
|
199
311
|
file.size = restFileSize + file.downloaded;
|
|
200
312
|
if (file.size > file.downloaded && response.body) {
|
|
201
313
|
const stream = Readable.fromWeb(response.body);
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
} else {
|
|
212
|
-
await downloadFile(file, attempts - 1);
|
|
314
|
+
const sizeOld = file.downloaded;
|
|
315
|
+
await PromiseRetry.create({
|
|
316
|
+
retries: this.chunkFetchRetries,
|
|
317
|
+
callback: () => {
|
|
318
|
+
if (sizeOld !== file.downloaded) {
|
|
319
|
+
return { newRetries: 5 };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}).execute(async () => await this.fetchStream(file, stream));
|
|
213
323
|
}
|
|
324
|
+
this.subject.next({ type: "FILE_DOWNLOADING_END" });
|
|
214
325
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
326
|
+
async downloadFiles(filelist) {
|
|
327
|
+
mkdir(filelist.dirPath);
|
|
328
|
+
this.subject.next({ type: "FILES_DOWNLOADING_START", filesCount: filelist.files.length });
|
|
329
|
+
for (const file of filelist.files) {
|
|
330
|
+
this.subject.next({ type: "FILE_DOWNLOADING_START" });
|
|
331
|
+
await PromiseRetry.create({
|
|
332
|
+
retries: this.fetchRetries,
|
|
333
|
+
callback: (retries) => {
|
|
334
|
+
if (/coomer|kemono/.test(file.url)) {
|
|
335
|
+
file.url = tryFixCoomerUrl(file.url, retries);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}).execute(async () => await this.downloadFile(file));
|
|
339
|
+
this.subject.next({ type: "FILE_DOWNLOADING_END" });
|
|
340
|
+
}
|
|
341
|
+
this.subject.next({ type: "FILES_DOWNLOADING_END" });
|
|
223
342
|
}
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// src/utils/filters.ts
|
|
228
|
-
var isImage = (name) => /\.(jpg|jpeg|png|gif|bmp|tiff|webp|avif)$/i.test(name);
|
|
229
|
-
var isVideo = (name) => /\.(mp4|m4v|avi|mov|mkv|webm|flv|wmv|mpeg|mpg|3gp)$/i.test(name);
|
|
230
|
-
var testMediaType = (name, type) => type === "all" ? true : type === "image" ? isImage(name) : isVideo(name);
|
|
231
|
-
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
|
-
return files.filter((f) => {
|
|
236
|
-
const text = `${f.name || ""} ${f.content || ""}`.toLowerCase();
|
|
237
|
-
return isValid(text);
|
|
238
|
-
});
|
|
239
|
-
}
|
|
343
|
+
};
|
|
240
344
|
|
|
241
345
|
// src/utils/multibar.ts
|
|
242
346
|
import { MultiBar } from "cli-progress";
|
|
@@ -262,38 +366,42 @@ var config = {
|
|
|
262
366
|
hideCursor: true,
|
|
263
367
|
format: "{percentage}% | {filename} | {value}/{total}{size}"
|
|
264
368
|
};
|
|
265
|
-
function createMultibar() {
|
|
369
|
+
function createMultibar(downloader) {
|
|
266
370
|
const multibar = new MultiBar(config);
|
|
267
371
|
let bar;
|
|
268
372
|
let minibar;
|
|
269
373
|
let filename;
|
|
270
|
-
|
|
271
|
-
|
|
374
|
+
let index = 0;
|
|
375
|
+
downloader.subject.subscribe({
|
|
376
|
+
next: ({ type, filesCount, file }) => {
|
|
272
377
|
switch (type) {
|
|
273
|
-
case "
|
|
378
|
+
case "FILES_DOWNLOADING_START":
|
|
274
379
|
bar?.stop();
|
|
275
380
|
bar = multibar.create(filesCount, 0);
|
|
276
381
|
break;
|
|
277
|
-
case "
|
|
382
|
+
case "FILES_DOWNLOADING_END":
|
|
278
383
|
bar?.stop();
|
|
279
384
|
break;
|
|
280
|
-
case "
|
|
281
|
-
bar?.update(index
|
|
385
|
+
case "FILE_DOWNLOADING_START":
|
|
386
|
+
bar?.update(++index, { filename: "Downloaded files", size: "" });
|
|
282
387
|
break;
|
|
283
|
-
case "
|
|
388
|
+
case "FILE_DOWNLOADING_END":
|
|
389
|
+
multibar.remove(minibar);
|
|
390
|
+
break;
|
|
391
|
+
case "CHUNK_DOWNLOADING_START":
|
|
284
392
|
multibar?.remove(minibar);
|
|
285
393
|
filename = formatNameStdout(file?.filepath);
|
|
286
394
|
minibar = multibar.create(b2mb(file?.size), b2mb(file?.downloaded));
|
|
287
395
|
break;
|
|
288
|
-
case "FILE_DOWNLOADING_FINISHED":
|
|
289
|
-
multibar.remove(minibar);
|
|
290
|
-
break;
|
|
291
396
|
case "CHUNK_DOWNLOADING_UPDATE":
|
|
292
397
|
minibar?.update(b2mb(file?.downloaded), {
|
|
293
398
|
filename,
|
|
294
399
|
size: "mb"
|
|
295
400
|
});
|
|
296
401
|
break;
|
|
402
|
+
case "CHUNK_DOWNLOADING_END":
|
|
403
|
+
multibar?.remove(minibar);
|
|
404
|
+
break;
|
|
297
405
|
default:
|
|
298
406
|
break;
|
|
299
407
|
}
|
|
@@ -301,54 +409,68 @@ function createMultibar() {
|
|
|
301
409
|
});
|
|
302
410
|
}
|
|
303
411
|
|
|
304
|
-
// src/api/
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
412
|
+
// src/api/coomer-api.ts
|
|
413
|
+
var SERVERS = ["n1", "n2", "n3", "n4"];
|
|
414
|
+
function tryFixCoomerUrl(url, attempts) {
|
|
415
|
+
if (attempts < 2 && isImage(url)) {
|
|
416
|
+
return url.replace(/\/data\//, "/thumbnail/data/").replace(/n\d\./, "img.");
|
|
417
|
+
}
|
|
418
|
+
const server = url.match(/n\d\./)?.[0].slice(0, 2);
|
|
419
|
+
const i = SERVERS.indexOf(server);
|
|
420
|
+
if (i !== -1) {
|
|
421
|
+
const newServer = SERVERS[(i + 1) % SERVERS.length];
|
|
422
|
+
return url.replace(/n\d./, `${newServer}.`);
|
|
423
|
+
}
|
|
424
|
+
return url;
|
|
312
425
|
}
|
|
313
|
-
function
|
|
314
|
-
const
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
return Array.from(encryptedUrlBuffer).map((byte, i) => String.fromCharCode(byte ^ secretKeyBuffer[i % secretKeyBuffer.length])).join("");
|
|
426
|
+
async function getUserProfileData(user) {
|
|
427
|
+
const url = `${user.domain}/api/v1/${user.service}/user/${user.id}/profile`;
|
|
428
|
+
const result = await fetchWithGlobalHeader(url).then((r) => r.json());
|
|
429
|
+
return result;
|
|
318
430
|
}
|
|
319
|
-
async function
|
|
320
|
-
const
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
return { name, url: src };
|
|
431
|
+
async function getUserPostsAPI(user, offset) {
|
|
432
|
+
const url = `${user.domain}/api/v1/${user.service}/user/${user.id}/posts?o=${offset}`;
|
|
433
|
+
const posts = await fetchWithGlobalHeader(url).then((r) => r.json());
|
|
434
|
+
return posts;
|
|
324
435
|
}
|
|
325
|
-
async function
|
|
326
|
-
const
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const fileName = $("h1").text();
|
|
333
|
-
const singleFile = await getFileData(url, fileName);
|
|
334
|
-
data.push(singleFile);
|
|
335
|
-
return { title, files: data.filter((f) => testMediaType(f.name, mediaType)) };
|
|
436
|
+
async function getUserFiles(user) {
|
|
437
|
+
const userPosts = [];
|
|
438
|
+
const offset = 50;
|
|
439
|
+
for (let i = 0; i < 1e3; i++) {
|
|
440
|
+
const posts = await getUserPostsAPI(user, i * offset);
|
|
441
|
+
userPosts.push(...posts);
|
|
442
|
+
if (posts.length < 50) break;
|
|
336
443
|
}
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
|
|
444
|
+
const filelist = new CoomerFileList();
|
|
445
|
+
for (const p of userPosts) {
|
|
446
|
+
const title = p.title.match(/\w+/g)?.join(" ") || "";
|
|
447
|
+
const content = p.content;
|
|
448
|
+
const date = p.published.replace(/T/, " ");
|
|
449
|
+
const datentitle = `${date} ${title}`.trim();
|
|
450
|
+
const postFiles = [...p.attachments, p.file].filter((f) => f.path).map((f, i) => {
|
|
451
|
+
const ext = f.name.split(".").pop();
|
|
452
|
+
const name = `${datentitle} ${i + 1}.${ext}`;
|
|
453
|
+
const url = `${user.domain}/${f.path}`;
|
|
454
|
+
return CoomerFile.from({ name, url, content });
|
|
455
|
+
});
|
|
456
|
+
filelist.files.push(...postFiles);
|
|
345
457
|
}
|
|
346
|
-
return
|
|
458
|
+
return filelist;
|
|
347
459
|
}
|
|
348
|
-
async function
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
460
|
+
async function parseUser(url) {
|
|
461
|
+
const [_, domain, service, id] = url.match(
|
|
462
|
+
/(https:\/\/\w+\.\w+)\/(\w+)\/user\/([\w|.|-]+)/
|
|
463
|
+
);
|
|
464
|
+
if (!domain || !service || !id) console.error("Invalid URL", url);
|
|
465
|
+
const { name } = await getUserProfileData({ domain, service, id });
|
|
466
|
+
return { domain, service, id, name };
|
|
467
|
+
}
|
|
468
|
+
async function getCoomerData(url) {
|
|
469
|
+
setGlobalHeaders({ accept: "text/css" });
|
|
470
|
+
const user = await parseUser(url);
|
|
471
|
+
const filelist = await getUserFiles(user);
|
|
472
|
+
filelist.dirName = `${user.name}-${user.service}`;
|
|
473
|
+
return filelist;
|
|
352
474
|
}
|
|
353
475
|
|
|
354
476
|
// src/api/gofile.ts
|
|
@@ -383,22 +505,22 @@ async function getFolderFiles(id, token, websiteToken) {
|
|
|
383
505
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
384
506
|
}
|
|
385
507
|
const data = await response.json();
|
|
386
|
-
const files = Object.values(data.data.children).map(
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
508
|
+
const files = Object.values(data.data.children).map(
|
|
509
|
+
(f) => CoomerFile.from({
|
|
510
|
+
url: f.link,
|
|
511
|
+
name: f.name
|
|
512
|
+
})
|
|
513
|
+
);
|
|
514
|
+
return new CoomerFileList(files);
|
|
391
515
|
}
|
|
392
|
-
async function getGofileData(url
|
|
516
|
+
async function getGofileData(url) {
|
|
393
517
|
const id = url.match(/gofile.io\/d\/(\w+)/)?.[1];
|
|
394
|
-
const dirName = `gofile-${id}`;
|
|
395
518
|
const token = await getToken();
|
|
396
519
|
const websiteToken = await getWebsiteToken();
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
);
|
|
520
|
+
const filelist = await getFolderFiles(id, token, websiteToken);
|
|
521
|
+
filelist.dirName = `gofile-${id}`;
|
|
400
522
|
setGlobalHeaders({ Cookie: `accountToken=${token}` });
|
|
401
|
-
return
|
|
523
|
+
return filelist;
|
|
402
524
|
}
|
|
403
525
|
|
|
404
526
|
// src/api/nsfw.xxx.ts
|
|
@@ -420,9 +542,9 @@ async function getUserPosts(user) {
|
|
|
420
542
|
}
|
|
421
543
|
return posts;
|
|
422
544
|
}
|
|
423
|
-
async function getPostsData(posts
|
|
545
|
+
async function getPostsData(posts) {
|
|
424
546
|
console.log("Fetching posts data...");
|
|
425
|
-
const
|
|
547
|
+
const filelist = new CoomerFileList();
|
|
426
548
|
for (const post of posts) {
|
|
427
549
|
const page = await fetch4(post).then((r) => r.text());
|
|
428
550
|
const $ = cheerio2.load(page);
|
|
@@ -432,49 +554,46 @@ async function getPostsData(posts, mediaType) {
|
|
|
432
554
|
const date = $(".sh-section .sh-section__passed").first().text().replace(/ /g, "-") || "";
|
|
433
555
|
const ext = src.split(".").pop();
|
|
434
556
|
const name = `${slug}-${date}.${ext}`;
|
|
435
|
-
|
|
557
|
+
filelist.files.push(CoomerFile.from({ name, url: src }));
|
|
436
558
|
}
|
|
437
|
-
return
|
|
559
|
+
return filelist;
|
|
438
560
|
}
|
|
439
|
-
async function getRedditData(url
|
|
561
|
+
async function getRedditData(url) {
|
|
440
562
|
const user = url.match(/u\/(\w+)/)?.[1];
|
|
441
563
|
const posts = await getUserPosts(user);
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
return
|
|
564
|
+
const filelist = await getPostsData(posts);
|
|
565
|
+
filelist.dirName = `${user}-reddit`;
|
|
566
|
+
return filelist;
|
|
445
567
|
}
|
|
446
568
|
|
|
447
569
|
// src/api/plain-curl.ts
|
|
448
570
|
async function getPlainFileData(url) {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
url
|
|
455
|
-
}
|
|
456
|
-
]
|
|
457
|
-
};
|
|
571
|
+
const name = url.split("/").pop();
|
|
572
|
+
const file = CoomerFile.from({ name, url });
|
|
573
|
+
const filelist = new CoomerFileList([file]);
|
|
574
|
+
filelist.dirName = "";
|
|
575
|
+
return filelist;
|
|
458
576
|
}
|
|
459
577
|
|
|
460
578
|
// src/api/index.ts
|
|
461
|
-
async function apiHandler(
|
|
462
|
-
|
|
463
|
-
|
|
579
|
+
async function apiHandler(url_) {
|
|
580
|
+
const url = new URL(url_);
|
|
581
|
+
if (/^u\/\w+$/.test(url.origin)) {
|
|
582
|
+
return getRedditData(url.href);
|
|
464
583
|
}
|
|
465
|
-
if (/coomer|kemono/.test(url)) {
|
|
466
|
-
return getCoomerData(url
|
|
584
|
+
if (/coomer|kemono/.test(url.origin)) {
|
|
585
|
+
return getCoomerData(url.href);
|
|
467
586
|
}
|
|
468
|
-
if (/bunkr/.test(url)) {
|
|
469
|
-
return getBunkrData(url
|
|
587
|
+
if (/bunkr/.test(url.origin)) {
|
|
588
|
+
return getBunkrData(url.href);
|
|
470
589
|
}
|
|
471
|
-
if (/gofile\.io/.test(url)) {
|
|
472
|
-
return getGofileData(url
|
|
590
|
+
if (/gofile\.io/.test(url.origin)) {
|
|
591
|
+
return getGofileData(url.href);
|
|
473
592
|
}
|
|
474
|
-
if (/\.\w+/.test(url.
|
|
475
|
-
return getPlainFileData(url);
|
|
593
|
+
if (/\.\w+/.test(url.pathname)) {
|
|
594
|
+
return getPlainFileData(url.href);
|
|
476
595
|
}
|
|
477
|
-
|
|
596
|
+
throw Error("Invalid URL");
|
|
478
597
|
}
|
|
479
598
|
|
|
480
599
|
// src/args-handler.ts
|
|
@@ -513,20 +632,24 @@ function argumentHander() {
|
|
|
513
632
|
// src/index.ts
|
|
514
633
|
async function run() {
|
|
515
634
|
const { url, dir, media, include, exclude, skip } = argumentHander();
|
|
516
|
-
const
|
|
517
|
-
const
|
|
518
|
-
|
|
635
|
+
const filelist = await apiHandler(url);
|
|
636
|
+
const found = filelist.files.length;
|
|
637
|
+
filelist.setDirPath(dir);
|
|
638
|
+
filelist.skip(skip);
|
|
639
|
+
filelist.filterByText(include, exclude);
|
|
640
|
+
filelist.filterByMediaType(media);
|
|
519
641
|
console.table([
|
|
520
642
|
{
|
|
521
|
-
found
|
|
643
|
+
found,
|
|
522
644
|
skip,
|
|
523
|
-
filtered:
|
|
524
|
-
folder:
|
|
645
|
+
filtered: found - filelist.files.length,
|
|
646
|
+
folder: filelist.dirPath
|
|
525
647
|
}
|
|
526
648
|
]);
|
|
527
649
|
setGlobalHeaders({ Referer: url });
|
|
528
|
-
|
|
529
|
-
|
|
650
|
+
const downloader = new Downloader();
|
|
651
|
+
createMultibar(downloader);
|
|
652
|
+
await downloader.downloadFiles(filelist);
|
|
530
653
|
process2.kill(process2.pid, "SIGINT");
|
|
531
654
|
}
|
|
532
655
|
run();
|