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.
- package/dist/image-utils.d.ts +7 -3
- package/dist/image-utils.js +134 -51
- package/dist/pics.js +5 -2
- package/image-utils.ts +123 -45
- package/package.json +2 -1
- package/pics.ts +6 -2
- package/pixivauth.py +115 -0
- package/readme.md +27 -4
package/dist/image-utils.d.ts
CHANGED
|
@@ -74,10 +74,14 @@ export default class ImageUtils {
|
|
|
74
74
|
/**
|
|
75
75
|
* Reverse searches the image to find danbooru post.
|
|
76
76
|
*/
|
|
77
|
-
static
|
|
77
|
+
static reverseImageSearch: (filepath: string, minSimilarity?: number) => Promise<any>;
|
|
78
78
|
/**
|
|
79
|
-
* Attempts to recover arbitrarily named posts from
|
|
79
|
+
* Attempts to recover arbitrarily named posts from pixiv, or danbooru as fallback.
|
|
80
80
|
*/
|
|
81
|
-
static
|
|
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 {};
|
package/dist/image-utils.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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.
|
|
357
|
-
if (!result
|
|
358
|
-
result
|
|
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
|
|
362
|
+
* Attempts to recover arbitrarily named posts from pixiv, or danbooru as fallback.
|
|
364
363
|
*/
|
|
365
|
-
static
|
|
366
|
-
const
|
|
367
|
-
const
|
|
368
|
-
const
|
|
369
|
-
const
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
378
|
-
let
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
if (
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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(
|
|
406
|
-
fs_1.default.mkdirSync(
|
|
407
|
-
let dest = path_1.default.join(
|
|
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(
|
|
412
|
-
fs_1.default.mkdirSync(
|
|
413
|
-
let dest = path_1.default.join(
|
|
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(
|
|
418
|
-
fs_1.default.mkdirSync(
|
|
419
|
-
let dest = path_1.default.join(
|
|
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(
|
|
427
|
-
fs_1.default.mkdirSync(
|
|
428
|
-
let src = path_1.default.join(
|
|
429
|
-
let dest = path_1.default.join(
|
|
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
|
-
|
|
10
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
365
|
-
if (!result
|
|
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
|
|
369
|
+
* Attempts to recover arbitrarily named posts from pixiv, or danbooru as fallback.
|
|
372
370
|
*/
|
|
373
|
-
public static
|
|
374
|
-
const
|
|
375
|
-
const
|
|
376
|
-
const
|
|
377
|
-
const
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
387
|
-
let
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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(
|
|
410
|
-
let dest = path.join(
|
|
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(
|
|
414
|
-
let dest = path.join(
|
|
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(
|
|
418
|
-
let dest = path.join(
|
|
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(
|
|
425
|
-
let src = path.join(
|
|
426
|
-
let dest = path.join(
|
|
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.
|
|
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
|
-
|
|
6
|
-
|
|
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 `
|
|
41
|
-
the original
|
|
42
|
-
will be put
|
|
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.
|
|
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
|