« get me outta code hell

--clear-thumbs utility - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/gen-thumbs.js
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2023-03-04 10:13:48 -0400
committer(quasar) nebula <qznebula@protonmail.com>2023-03-04 10:13:48 -0400
commit79b233cab5853b50717ffb281247485e26101ef0 (patch)
tree60a2fb5d21707f5b158d798152e005701eb678e2 /src/gen-thumbs.js
parent12e49429e0de38a1891a0b5ead653570ecd0e23b (diff)
--clear-thumbs utility
Diffstat (limited to 'src/gen-thumbs.js')
-rw-r--r--src/gen-thumbs.js139
1 files changed, 117 insertions, 22 deletions
diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js
index 64f1f27..26ac035 100644
--- a/src/gen-thumbs.js
+++ b/src/gen-thumbs.js
@@ -89,11 +89,17 @@ import {spawn} from 'child_process';
 import {createHash} from 'crypto';
 import * as path from 'path';
 
-import {readFile, writeFile} from 'fs/promises'; // Whatcha know! Nice.
+import {
+  readFile,
+  stat,
+  unlink,
+  writeFile,
+} from 'fs/promises'; // Whatcha know! Nice.
 
 import {createReadStream} from 'fs'; // Still gotta import from 8oth tho, for createReadStream.
 
 import {
+  fileIssue,
   logError,
   logInfo,
   logWarn,
@@ -110,6 +116,8 @@ import {
 
 import {delay, queue} from './util/sugar.js';
 
+export const defaultMagickThreads = 8;
+
 function readFileMD5(filePath) {
   return new Promise((resolve, reject) => {
     const md5 = createHash('md5');
@@ -189,8 +197,95 @@ function generateImageThumbnails(filePath, {spawnConvert}) {
         promisifyProcess(convert('.' + ext, details), false)));
 }
 
+export async function clearThumbs(mediaPath, {
+  queueSize = 0,
+} = {}) {
+  if (!mediaPath) {
+    throw new Error('Expected mediaPath to be passed');
+  }
+
+  logInfo`Looking for thumbnails to clear out...`;
+
+  const thumbFiles = await traverse(mediaPath, {
+    filterFile: file => isThumb(file),
+    filterDir: name => name !== '.git',
+  });
+
+  if (thumbFiles.length) {
+    // Double-check files. Since we're unlinking (deleting) files,
+    // we're better off safe than sorry!
+    const thumbtacks = Object.keys(thumbnailSpec);
+    const unsafeFiles = thumbFiles.filter(file => {
+      if (path.extname(file) !== '.jpg') return true;
+      if (thumbtacks.every(tack => !file.includes(tack))) return true;
+      return false;
+    });
+
+    if (unsafeFiles.length > 0) {
+      logError`Detected files which we thought were safe, but don't actually seem to be thumbnails!`;
+      logError`List of files that were invalid: ${`(Please remove any personal files before reporting)`}`;
+      for (const file of unsafeFiles) {
+        console.error(file);
+      }
+      fileIssue();
+      return;
+    }
+
+    logInfo`Clearing out ${thumbFiles.length} thumbs.`;
+
+    const errored = [];
+
+    await progressPromiseAll(`Removing thumbnail files`, queue(
+      thumbFiles.map(file => async () => {
+        try {
+          await unlink(path.join(mediaPath, file));
+        } catch (error) {
+          if (error.code !== 'ENOENT') {
+            errored.push(file);
+          }
+        }
+      }),
+      queueSize));
+
+    if (errored.length) {
+      logError`Couldn't remove these paths (${errored.length}):`;
+      for (const file of errored) {
+        console.error(file);
+      }
+      logError`Check for permission errors?`;
+    } else {
+      logInfo`Successfully deleted all ${thumbFiles.length} thumbnail files!`;
+    }
+  } else {
+    logInfo`Didn't find any thumbs in media directory.`;
+    logInfo`${mediaPath}`;
+  }
+
+  let cacheExists = false;
+  try {
+    await stat(path.join(mediaPath, CACHE_FILE));
+    cacheExists = true;
+  } catch (error) {
+    if (error.code === 'ENOENT') {
+      logInfo`Cache file already missing, nothing to remove there.`;
+    } else {
+      logWarn`Failed to access cache file. Check its permissions?`;
+    }
+  }
+
+  if (cacheExists) {
+    try {
+      unlink(path.join(mediaPath, CACHE_FILE));
+      logInfo`Removed thumbnail cache file.`;
+    } catch (error) {
+      logWarn`Failed to remove cache file. Check its permissions?`;
+    }
+  }
+}
+
 export default async function genThumbs(mediaPath, {
   queueSize = 0,
+  magickThreads = defaultMagickThreads,
   quiet = false,
 } = {}) {
   if (!mediaPath) {
@@ -226,6 +321,8 @@ export default async function genThumbs(mediaPath, {
     logInfo`Found ImageMagick binary: ${convertInfo}`;
   }
 
+  quietInfo`Running up to ${magickThreads + ' magick threads'} simultaneously.`;
+
   let cache,
     firstRun = false;
   try {
@@ -306,32 +403,28 @@ export default async function genThumbs(mediaPath, {
     return true;
   }
 
+  logInfo`Generating thumbnails for ${entriesToGenerate.length} media files.`;
+  if (entriesToGenerate.length > 250) {
+    logInfo`Go get a latte - this could take a while!`;
+  }
+
   const failed = [];
   const succeeded = [];
   const writeMessageFn = () =>
     `Writing image thumbnails. [failed: ${failed.length}]`;
 
-  // This is actually sort of a lie, 8ecause we aren't doing synchronicity.
-  // (We pass queueSize = 1 to queue().) 8ut we still use progressPromiseAll,
-  // 'cuz the progress indic8tor is very cool and good.
-  await progressPromiseAll(
-    writeMessageFn,
+  await progressPromiseAll(writeMessageFn,
     queue(
-      entriesToGenerate.map(
-        ([filePath, md5]) =>
-          () =>
-            generateImageThumbnails(path.join(mediaPath, filePath), {spawnConvert}).then(
-              () => {
-                updatedCache[filePath] = md5;
-                succeeded.push(filePath);
-              },
-              (error) => {
-                failed.push([filePath, error]);
-              }
-            )
-      )
-    )
-  );
+      entriesToGenerate.map(([filePath, md5]) => () =>
+        generateImageThumbnails(path.join(mediaPath, filePath), {spawnConvert}).then(
+          () => {
+            updatedCache[filePath] = md5;
+            succeeded.push(filePath);
+          },
+          error => {
+            failed.push([filePath, error]);
+          })),
+      magickThreads));
 
   if (failed.length > 0) {
     for (const [path, error] of failed) {
@@ -359,7 +452,7 @@ export default async function genThumbs(mediaPath, {
 }
 
 export function isThumb(file) {
-  const thumbnailLabel = file.match(/\.([^.]+)\.[^.]+$/)?.[1];
+  const thumbnailLabel = file.match(/\.([^.]+)\.jpg$/)?.[1];
   return Object.keys(thumbnailSpec).includes(thumbnailLabel);
 }
 
@@ -369,6 +462,7 @@ if (isMain(import.meta.url)) {
       'media-path': {
         type: 'value',
       },
+
       'queue-size': {
         type: 'value',
         validate(size) {
@@ -377,6 +471,7 @@ if (isMain(import.meta.url)) {
           return true;
         },
       },
+
       queue: {alias: 'queue-size'},
     });