animepic-utils 0.0.9 → 0.1.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.
@@ -7,4 +7,17 @@ export default class DownloadUtils {
7
7
  * Download any missing pixiv images.
8
8
  */
9
9
  static downloadMissingPixiv: (folder: string, pixivRefreshToken?: string) => Promise<void>;
10
+ /**
11
+ * Filter out duplicates.
12
+ */
13
+ static filterDuplicates: (folder: string) => Promise<void>;
14
+ private static normalizeFilename;
15
+ /**
16
+ * Process emojis.
17
+ */
18
+ static emojiProcessing: (folder: string) => Promise<void>;
19
+ /**
20
+ * Move images that do not have a very square aspect ratio.
21
+ */
22
+ static moveNonSquare: (folder: string, tolerance?: number) => Promise<void>;
10
23
  }
@@ -6,6 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const fs_1 = __importDefault(require("fs"));
7
7
  const path_1 = __importDefault(require("path"));
8
8
  const pixiv_ts_1 = __importDefault(require("pixiv.ts"));
9
+ const sharp_phash_1 = __importDefault(require("sharp-phash"));
10
+ const distance_1 = __importDefault(require("sharp-phash/distance"));
11
+ const sharp_1 = __importDefault(require("sharp"));
9
12
  const image_utils_1 = __importDefault(require("./image-utils"));
10
13
  class DownloadUtils {
11
14
  /**
@@ -143,5 +146,132 @@ class DownloadUtils {
143
146
  i++;
144
147
  }
145
148
  };
149
+ /**
150
+ * Filter out duplicates.
151
+ */
152
+ static filterDuplicates = async (folder) => {
153
+ const originalFolder = path_1.default.join(folder, "original");
154
+ const dupeFolder = path_1.default.join(folder, "duplicates");
155
+ if (!fs_1.default.existsSync(originalFolder))
156
+ fs_1.default.mkdirSync(originalFolder);
157
+ image_utils_1.default.moveImages(folder, originalFolder);
158
+ const binaryToHex = (bin) => {
159
+ return bin.match(/.{4}/g)?.reduce(function (acc, i) {
160
+ return acc + parseInt(i, 2).toString(16).toUpperCase();
161
+ }, "") || "";
162
+ };
163
+ const files = fs_1.default.readdirSync(originalFolder)
164
+ .filter((f) => f !== ".DS_Store")
165
+ .sort(new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare);
166
+ console.log("Generating hash map...");
167
+ let hashMap = {};
168
+ let i = 1;
169
+ for (const file of files) {
170
+ console.log(`${i}/${files.length}`);
171
+ let buffer = fs_1.default.readFileSync(path_1.default.join(originalFolder, file));
172
+ const binary = await (0, sharp_phash_1.default)(buffer);
173
+ hashMap[file] = binaryToHex(binary);
174
+ i++;
175
+ }
176
+ console.log("Checking for duplicates...");
177
+ const processed = new Set();
178
+ for (let i = 0; i < files.length; i++) {
179
+ const fileA = files[i];
180
+ if (processed.has(fileA))
181
+ continue;
182
+ let folderKey = path_1.default.basename(fileA, path_1.default.extname(fileA));
183
+ let shouldMove = false;
184
+ for (let j = i + 1; j < files.length; j++) {
185
+ const fileB = files[j];
186
+ if (processed.has(fileB))
187
+ continue;
188
+ if ((0, distance_1.default)(hashMap[fileA], hashMap[fileB]) < 6) {
189
+ let destFolder = path_1.default.join(dupeFolder, folderKey);
190
+ if (!fs_1.default.existsSync(destFolder))
191
+ fs_1.default.mkdirSync(destFolder, { recursive: true });
192
+ let src = path_1.default.join(originalFolder, fileB);
193
+ let dest = path_1.default.join(destFolder, fileB);
194
+ fs_1.default.renameSync(src, dest);
195
+ processed.add(fileB);
196
+ shouldMove = true;
197
+ }
198
+ }
199
+ if (shouldMove) {
200
+ let destFolder = path_1.default.join(dupeFolder, folderKey);
201
+ if (!fs_1.default.existsSync(destFolder))
202
+ fs_1.default.mkdirSync(destFolder, { recursive: true });
203
+ let src = path_1.default.join(originalFolder, fileA);
204
+ let dest = path_1.default.join(destFolder, fileA);
205
+ fs_1.default.renameSync(src, dest);
206
+ }
207
+ processed.add(fileA);
208
+ }
209
+ };
210
+ static normalizeFilename = (input) => {
211
+ let { name, ext } = path_1.default.parse(input);
212
+ name = name.replace(/\(.*?\)/g, "").trim();
213
+ name = name.replace(/^[\d_]+/, "").trim();
214
+ const isCamelCase = /^[a-z]+(?:[A-Z][a-z0-9]*)*$/.test(name);
215
+ if (isCamelCase)
216
+ return `${name}${ext}`;
217
+ const words = name
218
+ .replace(/[^a-zA-Z0-9]+/g, " ").trim()
219
+ .split(/\s+/).filter(Boolean);
220
+ if (words.length === 0)
221
+ return `${name}${ext}`;
222
+ const camel = words.map((w, i) => i === 0 ? w.toLowerCase()
223
+ : w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
224
+ return `${camel || name}${ext}`;
225
+ };
226
+ /**
227
+ * Process emojis.
228
+ */
229
+ static emojiProcessing = async (folder) => {
230
+ const originalFolder = path_1.default.join(folder, "original");
231
+ const processedFolder = path_1.default.join(folder, "processed");
232
+ if (!fs_1.default.existsSync(originalFolder))
233
+ fs_1.default.mkdirSync(originalFolder);
234
+ if (!fs_1.default.existsSync(processedFolder))
235
+ fs_1.default.mkdirSync(processedFolder);
236
+ image_utils_1.default.moveImages(folder, originalFolder);
237
+ image_utils_1.default.copyImages(originalFolder, processedFolder);
238
+ console.log("Processing images...");
239
+ await image_utils_1.default.processImages(processedFolder, async (file) => image_utils_1.default.resizeImage(file, 200), async (file) => {
240
+ let newName = this.normalizeFilename(path_1.default.basename(file));
241
+ let newDest = path_1.default.join(path_1.default.dirname(file), newName);
242
+ fs_1.default.renameSync(file, newDest);
243
+ return newDest;
244
+ });
245
+ };
246
+ /**
247
+ * Move images that do not have a very square aspect ratio.
248
+ */
249
+ static moveNonSquare = async (folder, tolerance = 0.1) => {
250
+ const originalFolder = path_1.default.join(folder, "original");
251
+ const nonSquareFolder = path_1.default.join(folder, "nosquare");
252
+ if (!fs_1.default.existsSync(originalFolder))
253
+ fs_1.default.mkdirSync(originalFolder);
254
+ image_utils_1.default.moveImages(folder, originalFolder);
255
+ const files = fs_1.default.readdirSync(originalFolder)
256
+ .filter((f) => f !== ".DS_Store")
257
+ .sort(new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare);
258
+ let i = 1;
259
+ for (const file of files) {
260
+ console.log(`${i}/${files.length}`);
261
+ const filePath = path_1.default.join(originalFolder, file);
262
+ const meta = await (0, sharp_1.default)(filePath).metadata();
263
+ if (!meta.width || !meta.height)
264
+ continue;
265
+ const aspect = meta.width / meta.height;
266
+ const lower = 1 - tolerance;
267
+ const upper = 1 + tolerance;
268
+ if (aspect < lower || aspect > upper) {
269
+ if (!fs_1.default.existsSync(nonSquareFolder))
270
+ fs_1.default.mkdirSync(nonSquareFolder);
271
+ fs_1.default.renameSync(filePath, path_1.default.join(nonSquareFolder, file));
272
+ }
273
+ i++;
274
+ }
275
+ };
146
276
  }
147
277
  exports.default = DownloadUtils;
package/download-utils.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import fs from "fs"
2
2
  import path from "path"
3
3
  import Pixiv from "pixiv.ts"
4
+ import phash from "sharp-phash"
5
+ import dist from "sharp-phash/distance"
6
+ import sharp from "sharp"
4
7
  import imageUtils from "./image-utils"
5
8
 
6
9
  export default class DownloadUtils {
@@ -132,4 +135,148 @@ export default class DownloadUtils {
132
135
  i++
133
136
  }
134
137
  }
138
+
139
+ /**
140
+ * Filter out duplicates.
141
+ */
142
+ public static filterDuplicates = async (folder: string) => {
143
+ const originalFolder = path.join(folder, "original")
144
+ const dupeFolder = path.join(folder, "duplicates")
145
+ if (!fs.existsSync(originalFolder)) fs.mkdirSync(originalFolder)
146
+
147
+ imageUtils.moveImages(folder, originalFolder)
148
+
149
+ const binaryToHex = (bin: string) => {
150
+ return bin.match(/.{4}/g)?.reduce(function(acc, i) {
151
+ return acc + parseInt(i, 2).toString(16).toUpperCase()
152
+ }, "") || ""
153
+ }
154
+
155
+ const files = fs.readdirSync(originalFolder)
156
+ .filter((f) => f !== ".DS_Store")
157
+ .sort(new Intl.Collator(undefined, {numeric: true, sensitivity: "base"}).compare)
158
+
159
+ console.log("Generating hash map...")
160
+
161
+ let hashMap = {} as {[key: string]: string}
162
+ let i = 1
163
+ for (const file of files) {
164
+ console.log(`${i}/${files.length}`)
165
+ let buffer = fs.readFileSync(path.join(originalFolder, file))
166
+ const binary = await phash(buffer)
167
+ hashMap[file] = binaryToHex(binary)
168
+ i++
169
+ }
170
+
171
+ console.log("Checking for duplicates...")
172
+
173
+ const processed = new Set<string>()
174
+
175
+ for (let i = 0; i < files.length; i++) {
176
+ const fileA = files[i]
177
+ if (processed.has(fileA)) continue
178
+
179
+ let folderKey = path.basename(fileA, path.extname(fileA))
180
+ let shouldMove = false
181
+ for (let j = i + 1; j < files.length; j++) {
182
+ const fileB = files[j]
183
+ if (processed.has(fileB)) continue
184
+
185
+ if (dist(hashMap[fileA], hashMap[fileB]) < 6) {
186
+ let destFolder = path.join(dupeFolder, folderKey)
187
+ if (!fs.existsSync(destFolder)) fs.mkdirSync(destFolder, {recursive: true})
188
+ let src = path.join(originalFolder, fileB)
189
+ let dest = path.join(destFolder, fileB)
190
+ fs.renameSync(src, dest)
191
+ processed.add(fileB)
192
+ shouldMove = true
193
+ }
194
+ }
195
+ if (shouldMove) {
196
+ let destFolder = path.join(dupeFolder, folderKey)
197
+ if (!fs.existsSync(destFolder)) fs.mkdirSync(destFolder, {recursive: true})
198
+ let src = path.join(originalFolder, fileA)
199
+ let dest = path.join(destFolder, fileA)
200
+ fs.renameSync(src, dest)
201
+ }
202
+ processed.add(fileA)
203
+ }
204
+ }
205
+
206
+ private static normalizeFilename = (input: string) => {
207
+ let {name, ext} = path.parse(input)
208
+ name = name.replace(/\(.*?\)/g, "").trim()
209
+ name = name.replace(/^[\d_]+/, "").trim()
210
+
211
+ const isCamelCase = /^[a-z]+(?:[A-Z][a-z0-9]*)*$/.test(name)
212
+ if (isCamelCase) return `${name}${ext}`
213
+
214
+ const words = name
215
+ .replace(/[^a-zA-Z0-9]+/g, " ").trim()
216
+ .split(/\s+/).filter(Boolean)
217
+
218
+ if (words.length === 0) return `${name}${ext}`
219
+
220
+ const camel = words.map((w, i) => i === 0 ? w.toLowerCase()
221
+ : w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("")
222
+
223
+ return `${camel || name}${ext}`
224
+ }
225
+
226
+ /**
227
+ * Process emojis.
228
+ */
229
+ public static emojiProcessing = async (folder: string) => {
230
+ const originalFolder = path.join(folder, "original")
231
+ const processedFolder = path.join(folder, "processed")
232
+ if (!fs.existsSync(originalFolder)) fs.mkdirSync(originalFolder)
233
+ if (!fs.existsSync(processedFolder)) fs.mkdirSync(processedFolder)
234
+
235
+ imageUtils.moveImages(folder, originalFolder)
236
+ imageUtils.copyImages(originalFolder, processedFolder)
237
+
238
+ console.log("Processing images...")
239
+ await imageUtils.processImages(processedFolder,
240
+ async (file) => imageUtils.resizeImage(file, 200),
241
+ async (file) => {
242
+ let newName = this.normalizeFilename(path.basename(file))
243
+ let newDest = path.join(path.dirname(file), newName)
244
+ fs.renameSync(file, newDest)
245
+ return newDest
246
+ }
247
+ )
248
+ }
249
+
250
+ /**
251
+ * Move images that do not have a very square aspect ratio.
252
+ */
253
+ public static moveNonSquare = async (folder: string, tolerance = 0.1) => {
254
+ const originalFolder = path.join(folder, "original")
255
+ const nonSquareFolder = path.join(folder, "nosquare")
256
+ if (!fs.existsSync(originalFolder)) fs.mkdirSync(originalFolder)
257
+
258
+ imageUtils.moveImages(folder, originalFolder)
259
+
260
+ const files = fs.readdirSync(originalFolder)
261
+ .filter((f) => f !== ".DS_Store")
262
+ .sort(new Intl.Collator(undefined, {numeric: true, sensitivity: "base"}).compare)
263
+
264
+ let i = 1
265
+ for (const file of files) {
266
+ console.log(`${i}/${files.length}`)
267
+ const filePath = path.join(originalFolder, file)
268
+ const meta = await sharp(filePath).metadata()
269
+ if (!meta.width || !meta.height) continue
270
+
271
+ const aspect = meta.width / meta.height
272
+ const lower = 1 - tolerance
273
+ const upper = 1 + tolerance
274
+
275
+ if (aspect < lower || aspect > upper) {
276
+ if (!fs.existsSync(nonSquareFolder)) fs.mkdirSync(nonSquareFolder)
277
+ fs.renameSync(filePath, path.join(nonSquareFolder, file))
278
+ }
279
+ i++
280
+ }
281
+ }
135
282
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "animepic-utils",
3
- "version": "0.0.9",
3
+ "version": "0.1.0",
4
4
  "description": "Node.js utils for processing anime images",
5
5
  "main": "dist/utils.js",
6
6
  "types": "dist/utils.d.ts",
@@ -29,6 +29,7 @@
29
29
  "jszip": "^3.10.1",
30
30
  "pixiv.ts": "^0.8.7",
31
31
  "sharp": "^0.34.4",
32
+ "sharp-phash": "^2.2.0",
32
33
  "waifu2x": "^1.5.1"
33
34
  }
34
35
  }
package/title.png CHANGED
Binary file