animepic-utils 0.0.4 → 0.0.6

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.
@@ -74,10 +74,14 @@ export default class ImageUtils {
74
74
  /**
75
75
  * Reverse searches the image to find danbooru post.
76
76
  */
77
- static reverseDanbooruSearch: (filepath: string, minSimilarity?: number) => Promise<any[]>;
77
+ static reverseImageSearch: (filepath: string, minSimilarity?: number) => Promise<any>;
78
78
  /**
79
- * Attempts to recover arbitrarily named posts from danbooru, if they exist.
79
+ * Attempts to recover arbitrarily named posts from pixiv, or danbooru as fallback.
80
80
  */
81
- static recoverFromDanbooru: (folder: string) => Promise<void>;
81
+ static recoverFromPixiv: (folder: string, pixivRefreshToken?: string) => Promise<void>;
82
+ /**
83
+ * Attempts to filter AI images on a folder containing images from pixiv.
84
+ */
85
+ static filterAIImages: (folder: string, pixivRefreshToken?: string) => Promise<void>;
82
86
  }
83
87
  export {};
@@ -40,6 +40,7 @@ const fs_1 = __importDefault(require("fs"));
40
40
  const path_1 = __importDefault(require("path"));
41
41
  const sharp_1 = __importDefault(require("sharp"));
42
42
  const waifu2x_1 = __importDefault(require("waifu2x"));
43
+ const pixiv_ts_1 = __importDefault(require("pixiv.ts"));
43
44
  const cheerio = __importStar(require("cheerio"));
44
45
  class ImageUtils {
45
46
  /**
@@ -295,18 +296,17 @@ class ImageUtils {
295
296
  /**
296
297
  * Reverse searches the image to find danbooru post.
297
298
  */
298
- static reverseDanbooruSearch = async (filepath, minSimilarity = 75) => {
299
+ static reverseImageSearch = async (filepath, minSimilarity = 75) => {
299
300
  const buffer = new Uint8Array(fs_1.default.readFileSync(filepath)).buffer;
300
301
  const form = new FormData();
301
302
  form.append("file", new Blob([buffer], { type: "image/png" }));
302
303
  const html = await fetch("https://iqdb.org/", { method: "POST", body: form }).then((r) => r.text());
303
304
  const $ = cheerio.load(html);
304
- let result = [];
305
+ let result = {};
305
306
  let downloadLinks = [];
306
307
  let promises = [];
307
308
  const appendDanbooru = async (link) => {
308
- const json = await fetch(`${link}.json`).then((r) => r.json());
309
- result.push(json);
309
+ result = await fetch(`${link}.json`).then((r) => r.json());
310
310
  };
311
311
  const appendZerochanDownload = async (link) => {
312
312
  const json = await fetch(`${link}?json`).then((r) => r.json());
@@ -323,7 +323,6 @@ class ImageUtils {
323
323
  downloadLinks.push(result[0]?.file_url);
324
324
  };
325
325
  const appendYandereDownload = async (link) => {
326
- console.log(link.match(/\d+/)?.[0]);
327
326
  const result = await fetch(`https://yande.re/post.json?tags=id:${link.match(/\d+/)?.[0]}`).then((r) => r.json());
328
327
  downloadLinks.push(result[0]?.file_url);
329
328
  };
@@ -353,82 +352,166 @@ class ImageUtils {
353
352
  }
354
353
  });
355
354
  await Promise.allSettled(promises);
356
- if (result.length === 1) {
357
- if (!result[0].file_url)
358
- result[0].file_url = downloadLinks[0];
355
+ if (result.id) {
356
+ if (!result.file_url)
357
+ result.file_url = downloadLinks[0];
359
358
  }
360
359
  return result;
361
360
  };
362
361
  /**
363
- * Attempts to recover arbitrarily named posts from danbooru, if they exist.
362
+ * Attempts to recover arbitrarily named posts from pixiv, or danbooru as fallback.
364
363
  */
365
- static recoverFromDanbooru = async (folder) => {
366
- const original = path_1.default.join(folder, "original");
367
- const pixiv = path_1.default.join(folder, "pixiv");
368
- const twitter = path_1.default.join(folder, "twitter");
369
- const comic = path_1.default.join(folder, "comic");
370
- const unrecoverable = path_1.default.join(folder, "unrecoverable");
371
- if (!fs_1.default.existsSync(original))
372
- fs_1.default.mkdirSync(original);
373
- this.moveImages(folder, original);
374
- const files = fs_1.default.readdirSync(original).filter((f) => f !== ".DS_Store")
364
+ static recoverFromPixiv = async (folder, pixivRefreshToken) => {
365
+ const pixiv = await pixiv_ts_1.default.refreshLogin(pixivRefreshToken);
366
+ const originalFolder = path_1.default.join(folder, "original");
367
+ const pixivFolder = path_1.default.join(folder, "pixiv");
368
+ const twitterFolder = path_1.default.join(folder, "twitter");
369
+ const otherFolder = path_1.default.join(folder, "other");
370
+ const comicFolder = path_1.default.join(folder, "comic");
371
+ const unrecoverableFolder = path_1.default.join(folder, "unrecoverable");
372
+ if (!fs_1.default.existsSync(originalFolder))
373
+ fs_1.default.mkdirSync(originalFolder);
374
+ this.moveImages(folder, originalFolder);
375
+ const files = fs_1.default.readdirSync(originalFolder).filter((f) => f !== ".DS_Store")
375
376
  .sort(new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare);
377
+ let i = 1;
376
378
  for (const file of files) {
377
- const pixivID = file.match(/\d+/)?.[0];
378
- let result = [];
379
- if (pixivID)
380
- result = await fetch(`https://danbooru.donmai.us/posts.json?tags=pixiv_id%3A${pixivID}&limit=1000`).then((r) => r.json());
381
- if (!result.length)
382
- result = await this.reverseDanbooruSearch(path_1.default.join(original, file));
383
- if (result.length) {
384
- let isComic = false;
385
- for (const json of result) {
379
+ console.log(`${i}/${files.length} -> ${file}`);
380
+ let pixivID = file.match(/^\d{5,}(?=$|_)/)?.[0];
381
+ let danbooruPosts = [];
382
+ let isComic = false;
383
+ if (pixivID) {
384
+ danbooruPosts = await fetch(`https://danbooru.donmai.us/posts.json?tags=pixiv_id%3A${pixivID}&limit=1000`).then((r) => r.json());
385
+ }
386
+ else {
387
+ const danbooruPost = await this.reverseImageSearch(path_1.default.join(originalFolder, file));
388
+ if (Object.keys(danbooruPost).length)
389
+ danbooruPosts = [danbooruPost];
390
+ if (danbooruPosts[0]?.source.includes("pximg.net") || danbooruPosts[0]?.source.includes("pixiv.net")) {
391
+ pixivID = path_1.default.basename(danbooruPosts[0].source).match(/\d+/)?.[0];
392
+ }
393
+ }
394
+ if (danbooruPosts.length) {
395
+ for (const json of danbooruPosts) {
386
396
  if (json.tag_string.includes("comic"))
387
397
  isComic = true;
388
398
  }
389
- for (const json of result) {
399
+ }
400
+ try {
401
+ if (pixivID) {
402
+ let illust = await pixiv.illust.get(pixivID);
403
+ if (illust.width === 100 && illust.height === 100 && path_1.default.basename(illust.image_urls.medium)
404
+ .includes("limit_unknown"))
405
+ throw new Error("bad illust");
406
+ let multiFolder = isComic ? comicFolder : pixivFolder;
407
+ await pixiv.util.downloadIllust(illust, pixivFolder, "original", multiFolder);
408
+ i++;
409
+ continue;
410
+ }
411
+ }
412
+ catch { }
413
+ if (danbooruPosts.length) {
414
+ for (const json of danbooruPosts) {
390
415
  let filename = path_1.default.basename(json.source);
391
- if (json.source.includes("twitter.com") || json.source.includes("x.com")) {
416
+ if (!filename.includes("."))
417
+ filename += ".png";
418
+ if (json.source.includes("pximg.net") || json.source.includes("pixiv.net")) {
419
+ filename = path_1.default.basename(json.source);
420
+ }
421
+ else if (json.source.includes("twitter.com") || json.source.includes("x.com")) {
392
422
  filename = `twitter_${filename}.${json.file_ext}`;
393
423
  }
394
424
  const downloadLink = json.file_url;
395
- if (!downloadLink) {
396
- if (!fs_1.default.existsSync(unrecoverable))
397
- fs_1.default.mkdirSync(unrecoverable);
398
- let src = path_1.default.join(original, file);
399
- let dest = path_1.default.join(unrecoverable, file);
400
- fs_1.default.copyFileSync(src, dest);
401
- }
402
- else {
425
+ if (downloadLink) {
403
426
  const buffer = await fetch(downloadLink).then((r) => r.arrayBuffer());
404
427
  if (json.source.includes("twitter.com") || json.source.includes("x.com")) {
405
- if (!fs_1.default.existsSync(twitter))
406
- fs_1.default.mkdirSync(twitter);
407
- let dest = path_1.default.join(twitter, filename);
428
+ if (!fs_1.default.existsSync(twitterFolder))
429
+ fs_1.default.mkdirSync(twitterFolder);
430
+ let dest = path_1.default.join(twitterFolder, filename);
408
431
  fs_1.default.writeFileSync(dest, new Uint8Array(buffer));
409
432
  }
410
433
  else if (isComic) {
411
- if (!fs_1.default.existsSync(comic))
412
- fs_1.default.mkdirSync(comic);
413
- let dest = path_1.default.join(comic, filename);
434
+ if (!fs_1.default.existsSync(comicFolder))
435
+ fs_1.default.mkdirSync(comicFolder);
436
+ let dest = path_1.default.join(comicFolder, filename);
437
+ fs_1.default.writeFileSync(dest, new Uint8Array(buffer));
438
+ }
439
+ else if (json.source.includes("pximg.net") || json.source.includes("pixiv.net")) {
440
+ if (!fs_1.default.existsSync(pixivFolder))
441
+ fs_1.default.mkdirSync(pixivFolder);
442
+ let dest = path_1.default.join(pixivFolder, filename);
414
443
  fs_1.default.writeFileSync(dest, new Uint8Array(buffer));
415
444
  }
416
445
  else {
417
- if (!fs_1.default.existsSync(pixiv))
418
- fs_1.default.mkdirSync(pixiv);
419
- let dest = path_1.default.join(pixiv, filename);
446
+ if (!fs_1.default.existsSync(otherFolder))
447
+ fs_1.default.mkdirSync(otherFolder);
448
+ let dest = path_1.default.join(otherFolder, filename);
420
449
  fs_1.default.writeFileSync(dest, new Uint8Array(buffer));
421
450
  }
422
451
  }
452
+ else {
453
+ if (!fs_1.default.existsSync(unrecoverableFolder))
454
+ fs_1.default.mkdirSync(unrecoverableFolder);
455
+ let src = path_1.default.join(originalFolder, file);
456
+ let dest = path_1.default.join(unrecoverableFolder, file);
457
+ fs_1.default.copyFileSync(src, dest);
458
+ }
423
459
  }
424
460
  }
425
461
  else {
426
- if (!fs_1.default.existsSync(unrecoverable))
427
- fs_1.default.mkdirSync(unrecoverable);
428
- let src = path_1.default.join(original, file);
429
- let dest = path_1.default.join(unrecoverable, file);
462
+ if (!fs_1.default.existsSync(unrecoverableFolder))
463
+ fs_1.default.mkdirSync(unrecoverableFolder);
464
+ let src = path_1.default.join(originalFolder, file);
465
+ let dest = path_1.default.join(unrecoverableFolder, file);
430
466
  fs_1.default.copyFileSync(src, dest);
431
467
  }
468
+ i++;
469
+ }
470
+ };
471
+ /**
472
+ * Attempts to filter AI images on a folder containing images from pixiv.
473
+ */
474
+ static filterAIImages = async (folder, pixivRefreshToken) => {
475
+ const pixiv = await pixiv_ts_1.default.refreshLogin(pixivRefreshToken);
476
+ const originalFolder = path_1.default.join(folder, "original");
477
+ const aiFolder = path_1.default.join(folder, "ai");
478
+ const errorFolder = path_1.default.join(folder, "error");
479
+ if (!fs_1.default.existsSync(originalFolder))
480
+ fs_1.default.mkdirSync(originalFolder);
481
+ this.moveImages(folder, originalFolder);
482
+ const files = fs_1.default.readdirSync(originalFolder).filter((f) => f !== ".DS_Store")
483
+ .sort(new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare);
484
+ let i = 1;
485
+ for (const file of files) {
486
+ console.log(`${i}/${files.length} -> ${file}`);
487
+ let pixivID = file.match(/^\d{5,}(?=\.|_)/)?.[0];
488
+ if (pixivID) {
489
+ try {
490
+ let illust = await pixiv.illust.get(pixivID);
491
+ if (pixiv.util.isAI(illust)) {
492
+ if (!fs_1.default.existsSync(aiFolder))
493
+ fs_1.default.mkdirSync(aiFolder);
494
+ let src = path_1.default.join(originalFolder, file);
495
+ let dest = path_1.default.join(aiFolder, file);
496
+ fs_1.default.renameSync(src, dest);
497
+ }
498
+ }
499
+ catch {
500
+ if (!fs_1.default.existsSync(errorFolder))
501
+ fs_1.default.mkdirSync(errorFolder);
502
+ let src = path_1.default.join(originalFolder, file);
503
+ let dest = path_1.default.join(errorFolder, file);
504
+ fs_1.default.renameSync(src, dest);
505
+ }
506
+ }
507
+ else {
508
+ if (!fs_1.default.existsSync(errorFolder))
509
+ fs_1.default.mkdirSync(errorFolder);
510
+ let src = path_1.default.join(originalFolder, file);
511
+ let dest = path_1.default.join(errorFolder, file);
512
+ fs_1.default.renameSync(src, dest);
513
+ }
514
+ i++;
432
515
  }
433
516
  };
434
517
  }
package/dist/pics.js CHANGED
@@ -6,8 +6,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  require("dotenv/config");
7
7
  const image_utils_1 = __importDefault(require("./image-utils"));
8
8
  const start = async () => {
9
- // await imageUtils.recoverFromDanbooru(process.env.FOLDER!)
10
- await image_utils_1.default.moepicsProcess(process.env.FOLDER);
9
+ let pixivKey = process.env.PIXIV_REFRESH_TOKEN;
10
+ let folder = process.env.FOLDER;
11
+ // await imageUtils.recoverFromPixiv(folder, pixivKey)
12
+ await image_utils_1.default.filterAIImages(folder, pixivKey);
13
+ // await imageUtils.moepicsProcess(process.env.FOLDER!)
11
14
  // imageUtils.changeQualifiers(process.env.FOLDER!, "c")
12
15
  };
13
16
  start();
package/image-utils.ts CHANGED
@@ -2,6 +2,7 @@ import fs from "fs"
2
2
  import path from "path"
3
3
  import sharp from "sharp"
4
4
  import waifu2x, {Waifu2xOptions} from "waifu2x"
5
+ import Pixiv, { PixivIllust } from "pixiv.ts"
5
6
  import * as cheerio from "cheerio"
6
7
 
7
8
  type Formats = "jpg" | "png" | "webp" | "avif" | "jxl"
@@ -300,7 +301,7 @@ export default class ImageUtils {
300
301
  /**
301
302
  * Reverse searches the image to find danbooru post.
302
303
  */
303
- public static reverseDanbooruSearch = async (filepath: string, minSimilarity = 75) => {
304
+ public static reverseImageSearch = async (filepath: string, minSimilarity = 75) => {
304
305
  const buffer = new Uint8Array(fs.readFileSync(filepath)).buffer
305
306
 
306
307
  const form = new FormData()
@@ -309,15 +310,13 @@ export default class ImageUtils {
309
310
  const html = await fetch("https://iqdb.org/", {method: "POST", body: form}).then((r) => r.text())
310
311
  const $ = cheerio.load(html)
311
312
 
312
- let result = [] as any[]
313
+ let result = {} as any
313
314
  let downloadLinks = [] as string[]
314
315
  let promises = [] as Promise<void>[]
315
316
 
316
317
  const appendDanbooru = async (link: string) => {
317
- const json = await fetch(`${link}.json`).then((r) => r.json())
318
- result.push(json)
318
+ result = await fetch(`${link}.json`).then((r) => r.json())
319
319
  }
320
-
321
320
  const appendZerochanDownload = async (link: string) => {
322
321
  const json = await fetch(`${link}?json`).then((r) => r.json())
323
322
  downloadLinks.push(json.full)
@@ -333,7 +332,6 @@ export default class ImageUtils {
333
332
  downloadLinks.push(result[0]?.file_url)
334
333
  }
335
334
  const appendYandereDownload = async (link: string) => {
336
- console.log(link.match(/\d+/)?.[0])
337
335
  const result = await fetch(`https://yande.re/post.json?tags=id:${link.match(/\d+/)?.[0]}`).then((r) => r.json())
338
336
  downloadLinks.push(result[0]?.file_url)
339
337
  }
@@ -361,71 +359,151 @@ export default class ImageUtils {
361
359
  })
362
360
 
363
361
  await Promise.allSettled(promises)
364
- if (result.length === 1) {
365
- if (!result[0].file_url) result[0].file_url = downloadLinks[0]
362
+ if (result.id) {
363
+ if (!result.file_url) result.file_url = downloadLinks[0]
366
364
  }
367
365
  return result
368
366
  }
369
367
 
370
368
  /**
371
- * Attempts to recover arbitrarily named posts from danbooru, if they exist.
369
+ * Attempts to recover arbitrarily named posts from pixiv, or danbooru as fallback.
372
370
  */
373
- public static recoverFromDanbooru = async (folder: string) => {
374
- const original = path.join(folder, "original")
375
- const pixiv = path.join(folder, "pixiv")
376
- const twitter = path.join(folder, "twitter")
377
- const comic = path.join(folder, "comic")
378
- const unrecoverable = path.join(folder, "unrecoverable")
379
- if (!fs.existsSync(original)) fs.mkdirSync(original)
380
-
381
- this.moveImages(folder, original)
382
-
383
- const files = fs.readdirSync(original).filter((f) => f !== ".DS_Store")
371
+ public static recoverFromPixiv = async (folder: string, pixivRefreshToken?: string) => {
372
+ const pixiv = await Pixiv.refreshLogin(pixivRefreshToken!)
373
+ const originalFolder = path.join(folder, "original")
374
+ const pixivFolder = path.join(folder, "pixiv")
375
+ const twitterFolder = path.join(folder, "twitter")
376
+ const otherFolder = path.join(folder, "other")
377
+ const comicFolder = path.join(folder, "comic")
378
+ const unrecoverableFolder = path.join(folder, "unrecoverable")
379
+ if (!fs.existsSync(originalFolder)) fs.mkdirSync(originalFolder)
380
+
381
+ this.moveImages(folder, originalFolder)
382
+
383
+ const files = fs.readdirSync(originalFolder).filter((f) => f !== ".DS_Store")
384
384
  .sort(new Intl.Collator(undefined, {numeric: true, sensitivity: "base"}).compare)
385
+
386
+ let i = 1
385
387
  for (const file of files) {
386
- const pixivID = file.match(/\d+/)?.[0]
387
- let result = [] as any[]
388
- if (pixivID) result = await fetch(`https://danbooru.donmai.us/posts.json?tags=pixiv_id%3A${pixivID}&limit=1000`).then((r) => r.json())
389
- if (!result.length) result = await this.reverseDanbooruSearch(path.join(original, file))
390
- if (result.length) {
391
- let isComic = false
392
- for (const json of result) {
388
+ console.log(`${i}/${files.length} -> ${file}`)
389
+ let pixivID = file.match(/^\d{5,}(?=$|_)/)?.[0]
390
+ let danbooruPosts: any[] = []
391
+ let isComic = false
392
+
393
+ if (pixivID) {
394
+ danbooruPosts = await fetch(`https://danbooru.donmai.us/posts.json?tags=pixiv_id%3A${pixivID}&limit=1000`).then((r) => r.json())
395
+ } else {
396
+ const danbooruPost = await this.reverseImageSearch(path.join(originalFolder, file))
397
+ if (Object.keys(danbooruPost).length) danbooruPosts = [danbooruPost]
398
+ if (danbooruPosts[0]?.source.includes("pximg.net") || danbooruPosts[0]?.source.includes("pixiv.net")) {
399
+ pixivID = path.basename(danbooruPosts[0].source).match(/\d+/)?.[0]
400
+ }
401
+ }
402
+
403
+ if (danbooruPosts.length) {
404
+ for (const json of danbooruPosts) {
393
405
  if (json.tag_string.includes("comic")) isComic = true
394
406
  }
395
- for (const json of result) {
407
+ }
408
+
409
+ try {
410
+ if (pixivID) {
411
+ let illust = await pixiv.illust.get(pixivID)
412
+ if (illust.width === 100 && illust.height === 100 && path.basename(illust.image_urls.medium)
413
+ .includes("limit_unknown")) throw new Error("bad illust")
414
+ let multiFolder = isComic ? comicFolder : pixivFolder
415
+ await pixiv.util.downloadIllust(illust, pixivFolder, "original", multiFolder)
416
+ i++
417
+ continue
418
+ }
419
+ } catch {}
420
+
421
+ if (danbooruPosts.length) {
422
+ for (const json of danbooruPosts) {
396
423
  let filename = path.basename(json.source)
397
- if (json.source.includes("twitter.com") || json.source.includes("x.com")) {
424
+ if (!filename.includes(".")) filename += ".png"
425
+ if (json.source.includes("pximg.net") || json.source.includes("pixiv.net")) {
426
+ filename = path.basename(json.source)
427
+ } else if (json.source.includes("twitter.com") || json.source.includes("x.com")) {
398
428
  filename = `twitter_${filename}.${json.file_ext}`
399
429
  }
400
430
  const downloadLink = json.file_url
401
- if (!downloadLink) {
402
- if (!fs.existsSync(unrecoverable)) fs.mkdirSync(unrecoverable)
403
- let src = path.join(original, file)
404
- let dest = path.join(unrecoverable, file)
405
- fs.copyFileSync(src, dest)
406
- } else {
431
+ if (downloadLink) {
407
432
  const buffer = await fetch(downloadLink).then((r) => r.arrayBuffer())
408
433
  if (json.source.includes("twitter.com") || json.source.includes("x.com")) {
409
- if (!fs.existsSync(twitter)) fs.mkdirSync(twitter)
410
- let dest = path.join(twitter, filename)
434
+ if (!fs.existsSync(twitterFolder)) fs.mkdirSync(twitterFolder)
435
+ let dest = path.join(twitterFolder, filename)
411
436
  fs.writeFileSync(dest, new Uint8Array(buffer))
412
437
  } else if (isComic) {
413
- if (!fs.existsSync(comic)) fs.mkdirSync(comic)
414
- let dest = path.join(comic, filename)
438
+ if (!fs.existsSync(comicFolder)) fs.mkdirSync(comicFolder)
439
+ let dest = path.join(comicFolder, filename)
440
+ fs.writeFileSync(dest, new Uint8Array(buffer))
441
+ } else if (json.source.includes("pximg.net") || json.source.includes("pixiv.net")) {
442
+ if (!fs.existsSync(pixivFolder)) fs.mkdirSync(pixivFolder)
443
+ let dest = path.join(pixivFolder, filename)
415
444
  fs.writeFileSync(dest, new Uint8Array(buffer))
416
- } else {
417
- if (!fs.existsSync(pixiv)) fs.mkdirSync(pixiv)
418
- let dest = path.join(pixiv, filename)
445
+ } else {
446
+ if (!fs.existsSync(otherFolder)) fs.mkdirSync(otherFolder)
447
+ let dest = path.join(otherFolder, filename)
419
448
  fs.writeFileSync(dest, new Uint8Array(buffer))
420
449
  }
450
+ } else {
451
+ if (!fs.existsSync(unrecoverableFolder)) fs.mkdirSync(unrecoverableFolder)
452
+ let src = path.join(originalFolder, file)
453
+ let dest = path.join(unrecoverableFolder, file)
454
+ fs.copyFileSync(src, dest)
421
455
  }
422
456
  }
423
457
  } else {
424
- if (!fs.existsSync(unrecoverable)) fs.mkdirSync(unrecoverable)
425
- let src = path.join(original, file)
426
- let dest = path.join(unrecoverable, file)
458
+ if (!fs.existsSync(unrecoverableFolder)) fs.mkdirSync(unrecoverableFolder)
459
+ let src = path.join(originalFolder, file)
460
+ let dest = path.join(unrecoverableFolder, file)
427
461
  fs.copyFileSync(src, dest)
428
462
  }
463
+ i++
464
+ }
465
+ }
466
+
467
+ /**
468
+ * Attempts to filter AI images on a folder containing images from pixiv.
469
+ */
470
+ public static filterAIImages = async (folder: string, pixivRefreshToken?: string) => {
471
+ const pixiv = await Pixiv.refreshLogin(pixivRefreshToken!)
472
+ const originalFolder = path.join(folder, "original")
473
+ const aiFolder = path.join(folder, "ai")
474
+ const errorFolder = path.join(folder, "error")
475
+ if (!fs.existsSync(originalFolder)) fs.mkdirSync(originalFolder)
476
+
477
+ this.moveImages(folder, originalFolder)
478
+
479
+ const files = fs.readdirSync(originalFolder).filter((f) => f !== ".DS_Store")
480
+ .sort(new Intl.Collator(undefined, {numeric: true, sensitivity: "base"}).compare)
481
+ let i = 1
482
+ for (const file of files) {
483
+ console.log(`${i}/${files.length} -> ${file}`)
484
+ let pixivID = file.match(/^\d{5,}(?=\.|_)/)?.[0]
485
+ if (pixivID) {
486
+ try {
487
+ let illust = await pixiv.illust.get(pixivID)
488
+ if (pixiv.util.isAI(illust)) {
489
+ if (!fs.existsSync(aiFolder)) fs.mkdirSync(aiFolder)
490
+ let src = path.join(originalFolder, file)
491
+ let dest = path.join(aiFolder, file)
492
+ fs.renameSync(src, dest)
493
+ }
494
+ } catch {
495
+ if (!fs.existsSync(errorFolder)) fs.mkdirSync(errorFolder)
496
+ let src = path.join(originalFolder, file)
497
+ let dest = path.join(errorFolder, file)
498
+ fs.renameSync(src, dest)
499
+ }
500
+ } else {
501
+ if (!fs.existsSync(errorFolder)) fs.mkdirSync(errorFolder)
502
+ let src = path.join(originalFolder, file)
503
+ let dest = path.join(errorFolder, file)
504
+ fs.renameSync(src, dest)
505
+ }
506
+ i++
429
507
  }
430
508
  }
431
509
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "animepic-utils",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Node.js utils for processing anime images",
5
5
  "main": "dist/image-utils.js",
6
6
  "types": "dist/image-utils.d.ts",
@@ -24,6 +24,7 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "cheerio": "^1.1.2",
27
+ "pixiv.ts": "^0.7.9",
27
28
  "sharp": "^0.34.4",
28
29
  "waifu2x": "^1.5.1"
29
30
  }
package/pics.ts CHANGED
@@ -2,8 +2,12 @@ import "dotenv/config"
2
2
  import imageUtils from "./image-utils"
3
3
 
4
4
  const start = async () => {
5
- // await imageUtils.recoverFromDanbooru(process.env.FOLDER!)
6
- await imageUtils.moepicsProcess(process.env.FOLDER!)
5
+ let pixivKey = process.env.PIXIV_REFRESH_TOKEN!
6
+ let folder = process.env.FOLDER!
7
+
8
+ // await imageUtils.recoverFromPixiv(folder, pixivKey)
9
+ await imageUtils.filterAIImages(folder, pixivKey)
10
+ // await imageUtils.moepicsProcess(process.env.FOLDER!)
7
11
  // imageUtils.changeQualifiers(process.env.FOLDER!, "c")
8
12
  }
9
13
 
package/pixivauth.py ADDED
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env python
2
+
3
+ from argparse import ArgumentParser
4
+ from base64 import urlsafe_b64encode
5
+ from hashlib import sha256
6
+ from pprint import pprint
7
+ from secrets import token_urlsafe
8
+ from sys import exit
9
+ from urllib.parse import urlencode
10
+ from webbrowser import open as open_url
11
+
12
+ import requests
13
+
14
+ # Latest app version can be found using GET /v1/application-info/android
15
+ USER_AGENT = "PixivAndroidApp/5.0.234 (Android 11; Pixel 5)"
16
+ REDIRECT_URI = "https://app-api.pixiv.net/web/v1/users/auth/pixiv/callback"
17
+ LOGIN_URL = "https://app-api.pixiv.net/web/v1/login"
18
+ AUTH_TOKEN_URL = "https://oauth.secure.pixiv.net/auth/token"
19
+ CLIENT_ID = "MOBrBDS8blbauoSck0ZfDbtuzpyT"
20
+ CLIENT_SECRET = "lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj"
21
+
22
+
23
+ def s256(data):
24
+ """S256 transformation method."""
25
+
26
+ return urlsafe_b64encode(sha256(data).digest()).rstrip(b"=").decode("ascii")
27
+
28
+
29
+ def oauth_pkce(transform):
30
+ """Proof Key for Code Exchange by OAuth Public Clients (RFC7636)."""
31
+
32
+ code_verifier = token_urlsafe(32)
33
+ code_challenge = transform(code_verifier.encode("ascii"))
34
+
35
+ return code_verifier, code_challenge
36
+
37
+
38
+ def print_auth_token_response(response):
39
+ data = response.json()
40
+
41
+ try:
42
+ access_token = data["access_token"]
43
+ refresh_token = data["refresh_token"]
44
+ except KeyError:
45
+ print("error:")
46
+ pprint(data)
47
+ exit(1)
48
+
49
+ print("access_token:", access_token)
50
+ print("refresh_token:", refresh_token)
51
+ print("expires_in:", data.get("expires_in", 0))
52
+
53
+
54
+ def login():
55
+ code_verifier, code_challenge = oauth_pkce(s256)
56
+ login_params = {
57
+ "code_challenge": code_challenge,
58
+ "code_challenge_method": "S256",
59
+ "client": "pixiv-android",
60
+ }
61
+
62
+ open_url(f"{LOGIN_URL}?{urlencode(login_params)}")
63
+
64
+ try:
65
+ code = input("code: ").strip()
66
+ except (EOFError, KeyboardInterrupt):
67
+ return
68
+
69
+ response = requests.post(
70
+ AUTH_TOKEN_URL,
71
+ data={
72
+ "client_id": CLIENT_ID,
73
+ "client_secret": CLIENT_SECRET,
74
+ "code": code,
75
+ "code_verifier": code_verifier,
76
+ "grant_type": "authorization_code",
77
+ "include_policy": "true",
78
+ "redirect_uri": REDIRECT_URI,
79
+ },
80
+ headers={"User-Agent": USER_AGENT},
81
+ )
82
+
83
+ print_auth_token_response(response)
84
+
85
+
86
+ def refresh(refresh_token):
87
+ response = requests.post(
88
+ AUTH_TOKEN_URL,
89
+ data={
90
+ "client_id": CLIENT_ID,
91
+ "client_secret": CLIENT_SECRET,
92
+ "grant_type": "refresh_token",
93
+ "include_policy": "true",
94
+ "refresh_token": refresh_token,
95
+ },
96
+ headers={"User-Agent": USER_AGENT},
97
+ )
98
+ print_auth_token_response(response)
99
+
100
+
101
+ def main():
102
+ parser = ArgumentParser()
103
+ subparsers = parser.add_subparsers()
104
+ parser.set_defaults(func=lambda _: parser.print_usage())
105
+ login_parser = subparsers.add_parser("login")
106
+ login_parser.set_defaults(func=lambda _: login())
107
+ refresh_parser = subparsers.add_parser("refresh")
108
+ refresh_parser.add_argument("refresh_token")
109
+ refresh_parser.set_defaults(func=lambda ns: refresh(ns.refresh_token))
110
+ args = parser.parse_args()
111
+ args.func(args)
112
+
113
+
114
+ if __name__ == "__main__":
115
+ main()
package/readme.md CHANGED
@@ -9,6 +9,11 @@
9
9
 
10
10
  Some utilities for processing anime images. (However I guess it'll work for any images).
11
11
 
12
+ ### Pixiv Refresh Token
13
+
14
+ To obtain your pixiv refresh token for the methods that require it, you can use the pixivauth.py script
15
+ from PixivPy. I included it in the base of the github repository.
16
+
12
17
  The primary function is `processImages` that accepts a folder of images, and then a variable
13
18
  amount of processing functions that will be applied to every image in the folder. The processing functions
14
19
  should take the current file parameter and return the path to the output, this is then fed back as the
@@ -37,14 +42,32 @@ await imageUtils.fixFileExtensions(folder)
37
42
 
38
43
  ### Anime specific
39
44
 
40
- The function `recoverFromDanbooru` takes a folder of arbitrarily named images and attempts to recover
41
- the original pixiv/twitter files from the largest mirror site, danbooru. Files which are unrecoverable
42
- will be put in an "unrecoverable" folder, usually because it wasn't found on danbooru.
45
+ The function `recoverFromPixiv` takes a folder of arbitrarily named images and attempts to recover
46
+ the original images from pixiv, or danbooru as a fallback. Files which are unrecoverable
47
+ will be put into an "unrecoverable" folder, usually because it was deleted and not archived anywhere.
48
+
49
+ ```ts
50
+ import imageUtils from "animepic-utils"
51
+
52
+ await imageUtils.recoverFromPixiv(folder, process.env.PIXIV_REFRESH_TOKEN)
53
+ ```
54
+
55
+ The `reverseImageSearch` function reverse searches the image on iqdb and returns the found danbooru post,
56
+ since usually this is the site with the most info.
57
+
58
+ ```ts
59
+ import imageUtils from "animepic-utils"
60
+
61
+ await imageUtils.reverseImageSearch(imagePath)
62
+ ```
63
+
64
+ The `filterAIImages` function attempts to filter out AI images from a folder containing pixiv images. If
65
+ you have images that aren't on pixiv, you will have to use your best judgement...
43
66
 
44
67
  ```ts
45
68
  import imageUtils from "animepic-utils"
46
69
 
47
- await imageUtils.recoverFromDanbooru(folder)
70
+ await imageUtils.filterAIImages(folder, process.env.PIXIV_REFRESH_TOKEN)
48
71
  ```
49
72
 
50
73
  The function `moepicsProcess` takes a folder of anime images and will generate the compressed