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 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 as fetch2 } from "undici";
8
+ import { fetch } from "undici";
11
9
 
12
- // src/utils/downloader.ts
13
- import fs2 from "node:fs";
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/api/coomer-api.ts
20
- var SERVERS = ["n1", "n2", "n3", "n4"];
21
- function tryFixCoomerUrl(url, attempts) {
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
- async function getUserProfileAPI(user) {
34
- const url = `${user.domain}/api/v1/${user.service}/user/${user.id}/profile`;
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
- async function getUserPostsAPI(user, offset) {
39
- const url = `${user.domain}/api/v1/${user.service}/user/${user.id}/posts?o=${offset}`;
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
- async function getUserFiles(user, mediaType) {
44
- const userPosts = [];
45
- const offset = 50;
46
- for (let i = 0; i < 1e3; i++) {
47
- const posts = await getUserPostsAPI(user, i * offset);
48
- userPosts.push(...posts);
49
- if (posts.length < 50) break;
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
- const files = [];
52
- for (const p of userPosts) {
53
- const title = p.title.match(/\w+/g)?.join(" ") || "";
54
- const content = p.content;
55
- const date = p.published.replace(/T/, " ");
56
- const datentitle = `${date} ${title}`.trim();
57
- const postFiles = [...p.attachments, p.file].filter((f) => f.path).filter((f) => testMediaType(f.name, mediaType)).map((f, i) => {
58
- const ext = f.name.split(".").pop();
59
- const name = `${datentitle} ${i + 1}.${ext}`;
60
- const url = `${user.domain}/${f.path}`;
61
- return { name, url, content };
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
- files.push(...postFiles);
74
+ return this;
64
75
  }
65
- return files;
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
- async function parseUser(url) {
68
- const [_, domain, service, id] = url.match(
69
- /(https:\/\/\w+\.\w+)\/(\w+)\/user\/([\w|.|-]+)/
70
- );
71
- if (!domain || !service || !id) console.error("Invalid URL", url);
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 getCoomerData(url, mediaType) {
76
- setGlobalHeaders({ accept: "text/css" });
77
- const user = await parseUser(url);
78
- const dirName = `${user.name}-${user.service}`;
79
- const files = await getUserFiles(user, mediaType);
80
- return { dirName, files };
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/files.ts
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, getGlobalDispatcher, interceptors, setGlobalDispatcher } from "undici";
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 fetch_(url) {
222
+ function fetchWithGlobalHeader(url) {
118
223
  const requestHeaders = new Headers(HeadersDefault);
119
- return fetch(url, { headers: requestHeaders });
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 fetch(url, { headers: requestHeaders });
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(this.timeoutCallback, this.timeout);
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 subject = new Subject();
155
- var CHUNK_TIMEOUT = 3e4;
156
- var DOWNLOAD_ATTEMPTS = 7;
157
- async function downloadStream(file, stream) {
158
- subject.next({ type: "CHUNK_DOWNLOADING_STARTED", file });
159
- const fileStream = fs2.createWriteStream(file.filepath, { flags: "a" });
160
- const controller = new AbortController();
161
- const timer = new Timer(CHUNK_TIMEOUT, () => {
162
- controller.abort("Stream is stuck.");
163
- }).start();
164
- const progressStream = new Transform({
165
- transform(chunk, _encoding, callback) {
166
- this.push(chunk);
167
- file.downloaded += chunk.length;
168
- timer.reset();
169
- subject.next({ type: "CHUNK_DOWNLOADING_UPDATE", file });
170
- callback();
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
- throw error;
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).catch(
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
- await downloadStream(file, stream);
203
- }
204
- } catch (error) {
205
- if (downloadedOld < (file.downloaded || 0)) {
206
- attempts = DOWNLOAD_ATTEMPTS;
207
- }
208
- if (attempts < 1) {
209
- console.error(file.url);
210
- console.error(error);
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
- async function downloadFiles(data, downloadDir) {
217
- mkdir(downloadDir);
218
- subject.next({ type: "FILES_DOWNLOADING_STARTED", filesCount: data.length });
219
- for (const [index, file] of data.entries()) {
220
- file.filepath = path.join(downloadDir, file.name);
221
- subject.next({ type: "FILE_DOWNLOADING_STARTED", index });
222
- await downloadFile(file);
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
- subject.next({ type: "FILES_DOWNLOADING_STARTED" });
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
- subject.subscribe({
271
- next: ({ type, filesCount, index, file }) => {
374
+ let index = 0;
375
+ downloader.subject.subscribe({
376
+ next: ({ type, filesCount, file }) => {
272
377
  switch (type) {
273
- case "FILES_DOWNLOADING_STARTED":
378
+ case "FILES_DOWNLOADING_START":
274
379
  bar?.stop();
275
380
  bar = multibar.create(filesCount, 0);
276
381
  break;
277
- case "FILES_DOWNLOADING_FINISHED":
382
+ case "FILES_DOWNLOADING_END":
278
383
  bar?.stop();
279
384
  break;
280
- case "FILE_DOWNLOADING_STARTED":
281
- bar?.update(index + 1, { filename: "Downloaded files", size: "" });
385
+ case "FILE_DOWNLOADING_START":
386
+ bar?.update(++index, { filename: "Downloaded files", size: "" });
282
387
  break;
283
- case "CHUNK_DOWNLOADING_STARTED":
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/bunkr.ts
305
- async function getEncryptionData(slug) {
306
- const response = await fetch2("https://bunkr.cr/api/vs", {
307
- method: "POST",
308
- headers: { "Content-Type": "application/json" },
309
- body: JSON.stringify({ slug })
310
- });
311
- return await response.json();
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 decryptEncryptedUrl(encryptionData) {
314
- const secretKey = `SECRET_KEY_${Math.floor(encryptionData.timestamp / 3600)}`;
315
- const encryptedUrlBuffer = Buffer.from(encryptionData.url, "base64");
316
- const secretKeyBuffer = Buffer.from(secretKey, "utf-8");
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 getFileData(url, name) {
320
- const slug = url.split("/").pop();
321
- const encryptionData = await getEncryptionData(slug);
322
- const src = decryptEncryptedUrl(encryptionData);
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 getGalleryFiles(url, mediaType) {
326
- const data = [];
327
- const page = await fetch2(url).then((r) => r.text());
328
- const $ = cheerio.load(page);
329
- const title = $("title").text();
330
- const url_ = new URL(url);
331
- if (url_.pathname.startsWith("/f/")) {
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 fileNames = Array.from($("div[title]").map((_, e) => $(e).attr("title")));
338
- const files = Array.from($("a").map((_, e) => $(e).attr("href"))).filter((a) => /\/f\/\w+/.test(a)).map((a, i) => ({
339
- url: `${url_.origin}${a}`,
340
- name: fileNames[i] || url.split("/").pop()
341
- }));
342
- for (const { name, url: url2 } of files) {
343
- const res = await getFileData(url2, name);
344
- data.push(res);
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 { title, files: data.filter((f) => testMediaType(f.name, mediaType)) };
458
+ return filelist;
347
459
  }
348
- async function getBunkrData(url, mediaType) {
349
- const { files, title } = await getGalleryFiles(url, mediaType);
350
- const dirName = `${title.split("|")[0].trim()}-bunkr`;
351
- return { dirName, files };
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((f) => ({
387
- url: f.link,
388
- name: f.name
389
- }));
390
- return files;
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, mediaType) {
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 files = (await getFolderFiles(id, token, websiteToken)).filter(
398
- (f) => testMediaType(f.name, mediaType)
399
- );
520
+ const filelist = await getFolderFiles(id, token, websiteToken);
521
+ filelist.dirName = `gofile-${id}`;
400
522
  setGlobalHeaders({ Cookie: `accountToken=${token}` });
401
- return { dirName, files };
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, mediaType) {
545
+ async function getPostsData(posts) {
424
546
  console.log("Fetching posts data...");
425
- const data = [];
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
- data.push({ name, url: src });
557
+ filelist.files.push(CoomerFile.from({ name, url: src }));
436
558
  }
437
- return data.filter((f) => testMediaType(f.name, mediaType));
559
+ return filelist;
438
560
  }
439
- async function getRedditData(url, mediaType) {
561
+ async function getRedditData(url) {
440
562
  const user = url.match(/u\/(\w+)/)?.[1];
441
563
  const posts = await getUserPosts(user);
442
- const files = await getPostsData(posts, mediaType);
443
- const dirName = `${user}-reddit`;
444
- return { dirName, files };
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
- return {
450
- dirName: "",
451
- files: [
452
- {
453
- name: url.split("/").pop(),
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(url, mediaType) {
462
- if (/^u\/\w+$/.test(url.trim())) {
463
- return getRedditData(url, mediaType);
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, mediaType);
584
+ if (/coomer|kemono/.test(url.origin)) {
585
+ return getCoomerData(url.href);
467
586
  }
468
- if (/bunkr/.test(url)) {
469
- return getBunkrData(url, mediaType);
587
+ if (/bunkr/.test(url.origin)) {
588
+ return getBunkrData(url.href);
470
589
  }
471
- if (/gofile\.io/.test(url)) {
472
- return getGofileData(url, mediaType);
590
+ if (/gofile\.io/.test(url.origin)) {
591
+ return getGofileData(url.href);
473
592
  }
474
- if (/\.\w+/.test(url.split("/").pop())) {
475
- return getPlainFileData(url);
593
+ if (/\.\w+/.test(url.pathname)) {
594
+ return getPlainFileData(url.href);
476
595
  }
477
- console.error("Wrong URL.");
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 { dirName, files } = await apiHandler(url, media);
517
- const downloadDir = dir === "./" ? path2.resolve(dir, dirName) : path2.join(os.homedir(), path2.join(dir, dirName));
518
- const filteredFiles = filterKeywords(files, include, exclude).slice(skip);
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: files.length,
643
+ found,
522
644
  skip,
523
- filtered: files.length - filteredFiles.length - skip,
524
- folder: downloadDir
645
+ filtered: found - filelist.files.length,
646
+ folder: filelist.dirPath
525
647
  }
526
648
  ]);
527
649
  setGlobalHeaders({ Referer: url });
528
- createMultibar();
529
- await downloadFiles(filteredFiles, downloadDir);
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();