simple-photo-gallery 2.0.9 → 2.0.10-rc.1
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 +532 -336
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -3,74 +3,19 @@ import process3, { stdout } from 'process';
|
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import { LogLevels, createConsola } from 'consola';
|
|
5
5
|
import { execSync, spawn } from 'child_process';
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import { z } from 'zod';
|
|
6
|
+
import fs8, { promises } from 'fs';
|
|
7
|
+
import path7 from 'path';
|
|
9
8
|
import { Buffer } from 'buffer';
|
|
10
9
|
import sharp2 from 'sharp';
|
|
11
|
-
import ExifReader from 'exifreader';
|
|
12
10
|
import { encode } from 'blurhash';
|
|
11
|
+
import ExifReader from 'exifreader';
|
|
12
|
+
import { z } from 'zod';
|
|
13
13
|
import ffprobe from 'node-ffprobe';
|
|
14
14
|
import os from 'os';
|
|
15
15
|
import Conf from 'conf';
|
|
16
16
|
import axios from 'axios';
|
|
17
17
|
import { compareSemVer, parseSemVer } from 'semver-parser';
|
|
18
18
|
|
|
19
|
-
var ThumbnailSchema = z.object({
|
|
20
|
-
path: z.string(),
|
|
21
|
-
pathRetina: z.string(),
|
|
22
|
-
width: z.number(),
|
|
23
|
-
height: z.number(),
|
|
24
|
-
blurHash: z.string().optional()
|
|
25
|
-
});
|
|
26
|
-
var MediaFileSchema = z.object({
|
|
27
|
-
type: z.enum(["image", "video"]),
|
|
28
|
-
path: z.string(),
|
|
29
|
-
alt: z.string().optional(),
|
|
30
|
-
width: z.number(),
|
|
31
|
-
height: z.number(),
|
|
32
|
-
thumbnail: ThumbnailSchema.optional(),
|
|
33
|
-
lastMediaTimestamp: z.string().optional()
|
|
34
|
-
});
|
|
35
|
-
var GallerySectionSchema = z.object({
|
|
36
|
-
title: z.string().optional(),
|
|
37
|
-
description: z.string().optional(),
|
|
38
|
-
images: z.array(MediaFileSchema)
|
|
39
|
-
});
|
|
40
|
-
var SubGallerySchema = z.object({
|
|
41
|
-
title: z.string(),
|
|
42
|
-
headerImage: z.string(),
|
|
43
|
-
path: z.string()
|
|
44
|
-
});
|
|
45
|
-
var GalleryMetadataSchema = z.object({
|
|
46
|
-
image: z.string().optional(),
|
|
47
|
-
imageWidth: z.number().optional(),
|
|
48
|
-
imageHeight: z.number().optional(),
|
|
49
|
-
ogUrl: z.string().optional(),
|
|
50
|
-
ogType: z.string().optional(),
|
|
51
|
-
ogSiteName: z.string().optional(),
|
|
52
|
-
twitterSite: z.string().optional(),
|
|
53
|
-
twitterCreator: z.string().optional(),
|
|
54
|
-
author: z.string().optional(),
|
|
55
|
-
keywords: z.string().optional(),
|
|
56
|
-
canonicalUrl: z.string().optional(),
|
|
57
|
-
language: z.string().optional(),
|
|
58
|
-
robots: z.string().optional()
|
|
59
|
-
});
|
|
60
|
-
var GalleryDataSchema = z.object({
|
|
61
|
-
title: z.string(),
|
|
62
|
-
description: z.string(),
|
|
63
|
-
url: z.string().optional(),
|
|
64
|
-
headerImage: z.string(),
|
|
65
|
-
thumbnailSize: z.number().optional(),
|
|
66
|
-
metadata: GalleryMetadataSchema,
|
|
67
|
-
galleryOutputPath: z.string().optional(),
|
|
68
|
-
mediaBaseUrl: z.string().optional(),
|
|
69
|
-
analyticsScript: z.string().optional(),
|
|
70
|
-
sections: z.array(GallerySectionSchema),
|
|
71
|
-
subGalleries: z.object({ title: z.string(), galleries: z.array(SubGallerySchema) })
|
|
72
|
-
});
|
|
73
|
-
|
|
74
19
|
// src/config/index.ts
|
|
75
20
|
var DEFAULT_THUMBNAIL_SIZE = 300;
|
|
76
21
|
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".gif", ".webp", ".tiff", ".tif", ".svg", ".avif"]);
|
|
@@ -138,13 +83,22 @@ async function createImageThumbnails(image, metadata, outputPath, outputPathReti
|
|
|
138
83
|
return { width, height };
|
|
139
84
|
}
|
|
140
85
|
|
|
86
|
+
// src/utils/blurhash.ts
|
|
87
|
+
async function generateBlurHash(imagePath, componentX = 4, componentY = 3) {
|
|
88
|
+
const image = await loadImage(imagePath);
|
|
89
|
+
const { data, info } = await image.resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
90
|
+
const pixels = new Uint8ClampedArray(data.buffer);
|
|
91
|
+
return encode(pixels, info.width, info.height, componentX, componentY);
|
|
92
|
+
}
|
|
93
|
+
|
|
141
94
|
// src/modules/build/utils/index.ts
|
|
142
|
-
|
|
95
|
+
path7.dirname(new URL(import.meta.url).pathname);
|
|
143
96
|
async function createGallerySocialMediaCardImage(headerPhotoPath, title, ouputPath, ui) {
|
|
144
97
|
ui.start(`Creating social media card image`);
|
|
145
|
-
|
|
98
|
+
const headerBasename = path7.basename(headerPhotoPath, path7.extname(headerPhotoPath));
|
|
99
|
+
if (fs8.existsSync(ouputPath)) {
|
|
146
100
|
ui.success(`Social media card image already exists`);
|
|
147
|
-
return;
|
|
101
|
+
return headerBasename;
|
|
148
102
|
}
|
|
149
103
|
const image = await loadImage(headerPhotoPath);
|
|
150
104
|
const resizedImageBuffer = await image.resize(1200, 631, { fit: "cover" }).jpeg({ quality: 90 }).toBuffer();
|
|
@@ -164,76 +118,115 @@ async function createGallerySocialMediaCardImage(headerPhotoPath, title, ouputPa
|
|
|
164
118
|
const finalImageBuffer = await sharp2(resizedImageBuffer).composite([{ input: Buffer.from(svgText), top: 0, left: 0 }]).jpeg({ quality: 90 }).toBuffer();
|
|
165
119
|
await sharp2(finalImageBuffer).toFile(outputPath);
|
|
166
120
|
ui.success(`Created social media card image successfully`);
|
|
121
|
+
return headerBasename;
|
|
167
122
|
}
|
|
168
123
|
async function createOptimizedHeaderImage(headerPhotoPath, outputFolder, ui) {
|
|
169
124
|
ui.start(`Creating optimized header images`);
|
|
170
125
|
const image = await loadImage(headerPhotoPath);
|
|
126
|
+
const headerBasename = path7.basename(headerPhotoPath, path7.extname(headerPhotoPath));
|
|
127
|
+
const generatedFiles = [];
|
|
128
|
+
ui.debug("Generating blurhash for header image");
|
|
129
|
+
const blurHash = await generateBlurHash(headerPhotoPath);
|
|
171
130
|
const landscapeYFactor = 3 / 4;
|
|
172
131
|
for (const width of HEADER_IMAGE_LANDSCAPE_WIDTHS) {
|
|
173
132
|
ui.debug(`Creating landscape header image ${width}`);
|
|
174
|
-
|
|
133
|
+
const avifFilename = `${headerBasename}_landscape_${width}.avif`;
|
|
134
|
+
const jpgFilename = `${headerBasename}_landscape_${width}.jpg`;
|
|
135
|
+
if (fs8.existsSync(path7.join(outputFolder, avifFilename))) {
|
|
175
136
|
ui.debug(`Landscape header image ${width} AVIF already exists`);
|
|
176
137
|
} else {
|
|
177
138
|
await cropAndResizeImage(
|
|
178
139
|
image.clone(),
|
|
179
|
-
|
|
140
|
+
path7.join(outputFolder, avifFilename),
|
|
180
141
|
width,
|
|
181
142
|
width * landscapeYFactor,
|
|
182
143
|
"avif"
|
|
183
144
|
);
|
|
184
145
|
}
|
|
185
|
-
|
|
146
|
+
generatedFiles.push(avifFilename);
|
|
147
|
+
if (fs8.existsSync(path7.join(outputFolder, jpgFilename))) {
|
|
186
148
|
ui.debug(`Landscape header image ${width} JPG already exists`);
|
|
187
149
|
} else {
|
|
188
|
-
await cropAndResizeImage(
|
|
189
|
-
image.clone(),
|
|
190
|
-
path4.join(outputFolder, `header_landscape_${width}.jpg`),
|
|
191
|
-
width,
|
|
192
|
-
width * landscapeYFactor,
|
|
193
|
-
"jpg"
|
|
194
|
-
);
|
|
150
|
+
await cropAndResizeImage(image.clone(), path7.join(outputFolder, jpgFilename), width, width * landscapeYFactor, "jpg");
|
|
195
151
|
}
|
|
152
|
+
generatedFiles.push(jpgFilename);
|
|
196
153
|
}
|
|
197
154
|
const portraitYFactor = 4 / 3;
|
|
198
155
|
for (const width of HEADER_IMAGE_PORTRAIT_WIDTHS) {
|
|
199
156
|
ui.debug(`Creating portrait header image ${width}`);
|
|
200
|
-
|
|
157
|
+
const avifFilename = `${headerBasename}_portrait_${width}.avif`;
|
|
158
|
+
const jpgFilename = `${headerBasename}_portrait_${width}.jpg`;
|
|
159
|
+
if (fs8.existsSync(path7.join(outputFolder, avifFilename))) {
|
|
201
160
|
ui.debug(`Portrait header image ${width} AVIF already exists`);
|
|
202
161
|
} else {
|
|
203
|
-
await cropAndResizeImage(
|
|
204
|
-
image.clone(),
|
|
205
|
-
path4.join(outputFolder, `header_portrait_${width}.avif`),
|
|
206
|
-
width,
|
|
207
|
-
width * portraitYFactor,
|
|
208
|
-
"avif"
|
|
209
|
-
);
|
|
162
|
+
await cropAndResizeImage(image.clone(), path7.join(outputFolder, avifFilename), width, width * portraitYFactor, "avif");
|
|
210
163
|
}
|
|
211
|
-
|
|
164
|
+
generatedFiles.push(avifFilename);
|
|
165
|
+
if (fs8.existsSync(path7.join(outputFolder, jpgFilename))) {
|
|
212
166
|
ui.debug(`Portrait header image ${width} JPG already exists`);
|
|
213
167
|
} else {
|
|
214
|
-
await cropAndResizeImage(
|
|
215
|
-
image.clone(),
|
|
216
|
-
path4.join(outputFolder, `header_portrait_${width}.jpg`),
|
|
217
|
-
width,
|
|
218
|
-
width * portraitYFactor,
|
|
219
|
-
"jpg"
|
|
220
|
-
);
|
|
168
|
+
await cropAndResizeImage(image.clone(), path7.join(outputFolder, jpgFilename), width, width * portraitYFactor, "jpg");
|
|
221
169
|
}
|
|
170
|
+
generatedFiles.push(jpgFilename);
|
|
222
171
|
}
|
|
223
172
|
ui.success(`Created optimized header image successfully`);
|
|
173
|
+
return { headerBasename, generatedFiles, blurHash };
|
|
174
|
+
}
|
|
175
|
+
function hasOldHeaderImages(outputFolder, currentHeaderBasename) {
|
|
176
|
+
if (!fs8.existsSync(outputFolder)) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
const files = fs8.readdirSync(outputFolder);
|
|
180
|
+
for (const file of files) {
|
|
181
|
+
const landscapeMatch = file.match(/^(.+)_landscape_\d+\.(avif|jpg)$/);
|
|
182
|
+
const portraitMatch = file.match(/^(.+)_portrait_\d+\.(avif|jpg)$/);
|
|
183
|
+
if (landscapeMatch && landscapeMatch[1] !== currentHeaderBasename || portraitMatch && portraitMatch[1] !== currentHeaderBasename) {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
function cleanupOldHeaderImages(outputFolder, currentHeaderBasename, ui) {
|
|
190
|
+
ui.start(`Cleaning up old header images`);
|
|
191
|
+
if (!fs8.existsSync(outputFolder)) {
|
|
192
|
+
ui.debug(`Output folder ${outputFolder} does not exist, skipping cleanup`);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const files = fs8.readdirSync(outputFolder);
|
|
196
|
+
let deletedCount = 0;
|
|
197
|
+
for (const file of files) {
|
|
198
|
+
const landscapeMatch = file.match(/^(.+)_landscape_\d+\.(avif|jpg)$/);
|
|
199
|
+
const portraitMatch = file.match(/^(.+)_portrait_\d+\.(avif|jpg)$/);
|
|
200
|
+
if (landscapeMatch && landscapeMatch[1] !== currentHeaderBasename) {
|
|
201
|
+
const filePath = path7.join(outputFolder, file);
|
|
202
|
+
ui.debug(`Deleting old landscape header image: ${file}`);
|
|
203
|
+
fs8.unlinkSync(filePath);
|
|
204
|
+
deletedCount++;
|
|
205
|
+
} else if (portraitMatch && portraitMatch[1] !== currentHeaderBasename) {
|
|
206
|
+
const filePath = path7.join(outputFolder, file);
|
|
207
|
+
ui.debug(`Deleting old portrait header image: ${file}`);
|
|
208
|
+
fs8.unlinkSync(filePath);
|
|
209
|
+
deletedCount++;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (deletedCount > 0) {
|
|
213
|
+
ui.success(`Deleted ${deletedCount} old header image(s)`);
|
|
214
|
+
} else {
|
|
215
|
+
ui.debug(`No old header images to clean up`);
|
|
216
|
+
}
|
|
224
217
|
}
|
|
225
218
|
function findGalleries(basePath, recursive) {
|
|
226
219
|
const galleryDirs = [];
|
|
227
|
-
const galleryJsonPath =
|
|
228
|
-
if (
|
|
220
|
+
const galleryJsonPath = path7.join(basePath, "gallery", "gallery.json");
|
|
221
|
+
if (fs8.existsSync(galleryJsonPath)) {
|
|
229
222
|
galleryDirs.push(basePath);
|
|
230
223
|
}
|
|
231
224
|
if (recursive) {
|
|
232
225
|
try {
|
|
233
|
-
const entries =
|
|
226
|
+
const entries = fs8.readdirSync(basePath, { withFileTypes: true });
|
|
234
227
|
for (const entry of entries) {
|
|
235
228
|
if (entry.isDirectory() && entry.name !== "gallery") {
|
|
236
|
-
const subPath =
|
|
229
|
+
const subPath = path7.join(basePath, entry.name);
|
|
237
230
|
const subResults = findGalleries(subPath, recursive);
|
|
238
231
|
galleryDirs.push(...subResults);
|
|
239
232
|
}
|
|
@@ -261,16 +254,335 @@ function parseTelemetryOption(value) {
|
|
|
261
254
|
}
|
|
262
255
|
return value;
|
|
263
256
|
}
|
|
257
|
+
var ThumbnailSchema = z.object({
|
|
258
|
+
path: z.string(),
|
|
259
|
+
pathRetina: z.string(),
|
|
260
|
+
width: z.number(),
|
|
261
|
+
height: z.number(),
|
|
262
|
+
blurHash: z.string().optional()
|
|
263
|
+
});
|
|
264
|
+
var MediaFileSchema = z.object({
|
|
265
|
+
type: z.enum(["image", "video"]),
|
|
266
|
+
filename: z.string(),
|
|
267
|
+
alt: z.string().optional(),
|
|
268
|
+
width: z.number(),
|
|
269
|
+
height: z.number(),
|
|
270
|
+
thumbnail: ThumbnailSchema.optional(),
|
|
271
|
+
lastMediaTimestamp: z.string().optional()
|
|
272
|
+
});
|
|
273
|
+
var MediaFileDeprecatedSchema = z.object({
|
|
274
|
+
type: z.enum(["image", "video"]),
|
|
275
|
+
path: z.string(),
|
|
276
|
+
alt: z.string().optional(),
|
|
277
|
+
width: z.number(),
|
|
278
|
+
height: z.number(),
|
|
279
|
+
thumbnail: ThumbnailSchema.optional(),
|
|
280
|
+
lastMediaTimestamp: z.string().optional()
|
|
281
|
+
});
|
|
282
|
+
var GallerySectionSchema = z.object({
|
|
283
|
+
title: z.string().optional(),
|
|
284
|
+
description: z.string().optional(),
|
|
285
|
+
images: z.array(MediaFileSchema)
|
|
286
|
+
});
|
|
287
|
+
var GallerySectionDeprecatedSchema = z.object({
|
|
288
|
+
title: z.string().optional(),
|
|
289
|
+
description: z.string().optional(),
|
|
290
|
+
images: z.array(MediaFileDeprecatedSchema)
|
|
291
|
+
});
|
|
292
|
+
var SubGallerySchema = z.object({
|
|
293
|
+
title: z.string(),
|
|
294
|
+
headerImage: z.string(),
|
|
295
|
+
path: z.string()
|
|
296
|
+
});
|
|
297
|
+
var GalleryMetadataSchema = z.object({
|
|
298
|
+
image: z.string().optional(),
|
|
299
|
+
imageWidth: z.number().optional(),
|
|
300
|
+
imageHeight: z.number().optional(),
|
|
301
|
+
ogUrl: z.string().optional(),
|
|
302
|
+
ogType: z.string().optional(),
|
|
303
|
+
ogSiteName: z.string().optional(),
|
|
304
|
+
twitterSite: z.string().optional(),
|
|
305
|
+
twitterCreator: z.string().optional(),
|
|
306
|
+
author: z.string().optional(),
|
|
307
|
+
keywords: z.string().optional(),
|
|
308
|
+
canonicalUrl: z.string().optional(),
|
|
309
|
+
language: z.string().optional(),
|
|
310
|
+
robots: z.string().optional()
|
|
311
|
+
});
|
|
312
|
+
var GalleryDataSchema = z.object({
|
|
313
|
+
title: z.string(),
|
|
314
|
+
description: z.string(),
|
|
315
|
+
mediaBasePath: z.string().optional(),
|
|
316
|
+
url: z.string().optional(),
|
|
317
|
+
headerImage: z.string(),
|
|
318
|
+
headerImageBlurHash: z.string().optional(),
|
|
319
|
+
thumbnailSize: z.number().optional(),
|
|
320
|
+
metadata: GalleryMetadataSchema,
|
|
321
|
+
mediaBaseUrl: z.string().optional(),
|
|
322
|
+
analyticsScript: z.string().optional(),
|
|
323
|
+
sections: z.array(GallerySectionSchema),
|
|
324
|
+
subGalleries: z.object({ title: z.string(), galleries: z.array(SubGallerySchema) })
|
|
325
|
+
});
|
|
326
|
+
var GalleryDataDeprecatedSchema = z.object({
|
|
327
|
+
title: z.string(),
|
|
328
|
+
description: z.string(),
|
|
329
|
+
url: z.string().optional(),
|
|
330
|
+
headerImage: z.string(),
|
|
331
|
+
thumbnailSize: z.number().optional(),
|
|
332
|
+
metadata: GalleryMetadataSchema,
|
|
333
|
+
mediaBaseUrl: z.string().optional(),
|
|
334
|
+
analyticsScript: z.string().optional(),
|
|
335
|
+
sections: z.array(GallerySectionDeprecatedSchema),
|
|
336
|
+
subGalleries: z.object({ title: z.string(), galleries: z.array(SubGallerySchema) })
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// src/utils/gallery.ts
|
|
340
|
+
function parseGalleryJson(galleryJsonPath, ui) {
|
|
341
|
+
let galleryContent;
|
|
342
|
+
try {
|
|
343
|
+
galleryContent = fs8.readFileSync(galleryJsonPath, "utf8");
|
|
344
|
+
} catch (error) {
|
|
345
|
+
ui.error("Error parsing gallery.json: file not found");
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
348
|
+
let galleryJSON;
|
|
349
|
+
try {
|
|
350
|
+
galleryJSON = JSON.parse(galleryContent);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
ui.error("Error parsing gallery.json: invalid JSON");
|
|
353
|
+
throw error;
|
|
354
|
+
}
|
|
355
|
+
try {
|
|
356
|
+
return GalleryDataSchema.parse(galleryJSON);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
try {
|
|
359
|
+
const deprecatedGalleryData = GalleryDataDeprecatedSchema.parse(galleryJSON);
|
|
360
|
+
return migrateGalleryJson(deprecatedGalleryData, galleryJsonPath, ui);
|
|
361
|
+
} catch {
|
|
362
|
+
ui.error("Error parsing gallery.json: invalid gallery data");
|
|
363
|
+
throw error;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function migrateGalleryJson(deprecatedGalleryData, galleryJsonPath, ui) {
|
|
368
|
+
ui.start("Old gallery.json format detected. Migrating gallery.json to the new data format.");
|
|
369
|
+
let mediaBasePath;
|
|
370
|
+
const imagePath = deprecatedGalleryData.sections[0].images[0].path;
|
|
371
|
+
if (imagePath && imagePath !== path7.join("..", path7.basename(imagePath))) {
|
|
372
|
+
mediaBasePath = path7.resolve(path7.join(path7.dirname(galleryJsonPath)), path7.dirname(imagePath));
|
|
373
|
+
}
|
|
374
|
+
const sections = deprecatedGalleryData.sections.map((section) => ({
|
|
375
|
+
...section,
|
|
376
|
+
images: section.images.map((image) => ({
|
|
377
|
+
...image,
|
|
378
|
+
path: void 0,
|
|
379
|
+
filename: path7.basename(image.path)
|
|
380
|
+
}))
|
|
381
|
+
}));
|
|
382
|
+
const galleryData = {
|
|
383
|
+
...deprecatedGalleryData,
|
|
384
|
+
headerImage: path7.basename(deprecatedGalleryData.headerImage),
|
|
385
|
+
sections,
|
|
386
|
+
mediaBasePath
|
|
387
|
+
};
|
|
388
|
+
ui.debug("Backing up old gallery.json file");
|
|
389
|
+
fs8.copyFileSync(galleryJsonPath, `${galleryJsonPath}.old`);
|
|
390
|
+
ui.debug("Writing gallery data to gallery.json file");
|
|
391
|
+
fs8.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
|
|
392
|
+
ui.success("Gallery data migrated to the new data format successfully.");
|
|
393
|
+
return galleryData;
|
|
394
|
+
}
|
|
395
|
+
function getMediaFileType(fileName) {
|
|
396
|
+
const ext = path7.extname(fileName).toLowerCase();
|
|
397
|
+
if (IMAGE_EXTENSIONS.has(ext)) return "image";
|
|
398
|
+
if (VIDEO_EXTENSIONS.has(ext)) return "video";
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
function capitalizeTitle(folderName) {
|
|
402
|
+
return folderName.replace("-", " ").replace("_", " ").split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// src/modules/init/index.ts
|
|
406
|
+
async function scanDirectory(dirPath, ui) {
|
|
407
|
+
const mediaFiles = [];
|
|
408
|
+
const subGalleryDirectories = [];
|
|
409
|
+
try {
|
|
410
|
+
const entries = await promises.readdir(dirPath, { withFileTypes: true });
|
|
411
|
+
for (const entry of entries) {
|
|
412
|
+
if (entry.isFile()) {
|
|
413
|
+
const mediaType = getMediaFileType(entry.name);
|
|
414
|
+
if (mediaType) {
|
|
415
|
+
const mediaFile = {
|
|
416
|
+
type: mediaType,
|
|
417
|
+
filename: entry.name,
|
|
418
|
+
width: 0,
|
|
419
|
+
height: 0
|
|
420
|
+
};
|
|
421
|
+
mediaFiles.push(mediaFile);
|
|
422
|
+
}
|
|
423
|
+
} else if (entry.isDirectory() && entry.name !== "gallery") {
|
|
424
|
+
subGalleryDirectories.push(path7.join(dirPath, entry.name));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
} catch (error) {
|
|
428
|
+
if (error instanceof Error && error.message.includes("ENOENT")) {
|
|
429
|
+
ui.error(`Directory does not exist: ${dirPath}`);
|
|
430
|
+
} else if (error instanceof Error && error.message.includes("ENOTDIR")) {
|
|
431
|
+
ui.error(`Path is not a directory: ${dirPath}`);
|
|
432
|
+
} else {
|
|
433
|
+
ui.error(`Error scanning directory ${dirPath}:`, error);
|
|
434
|
+
}
|
|
435
|
+
throw error;
|
|
436
|
+
}
|
|
437
|
+
return { mediaFiles, subGalleryDirectories };
|
|
438
|
+
}
|
|
439
|
+
async function getGallerySettingsFromUser(galleryName, defaultImage, ui) {
|
|
440
|
+
ui.info(`Enter gallery settings for the gallery in folder "${galleryName}"`);
|
|
441
|
+
const title = await ui.prompt("Enter gallery title", { type: "text", default: "My Gallery", placeholder: "My Gallery" });
|
|
442
|
+
const description = await ui.prompt("Enter gallery description", {
|
|
443
|
+
type: "text",
|
|
444
|
+
default: "My gallery with fantastic photos.",
|
|
445
|
+
placeholder: "My gallery with fantastic photos."
|
|
446
|
+
});
|
|
447
|
+
const url = await ui.prompt("Enter the URL where the gallery will be hosted (important for social media image)", {
|
|
448
|
+
type: "text",
|
|
449
|
+
default: "",
|
|
450
|
+
placeholder: ""
|
|
451
|
+
});
|
|
452
|
+
const headerImage = await ui.prompt("Enter the name of the header image", {
|
|
453
|
+
type: "text",
|
|
454
|
+
default: defaultImage,
|
|
455
|
+
placeholder: defaultImage
|
|
456
|
+
});
|
|
457
|
+
return { title, description, url, headerImage };
|
|
458
|
+
}
|
|
459
|
+
async function createGalleryJson(mediaFiles, galleryJsonPath, scanPath, subGalleries = [], useDefaultSettings, ui) {
|
|
460
|
+
const galleryDir = path7.dirname(galleryJsonPath);
|
|
461
|
+
const isSameLocation = path7.relative(scanPath, path7.join(galleryDir, "..")) === "";
|
|
462
|
+
const mediaBasePath = isSameLocation ? void 0 : scanPath;
|
|
463
|
+
const relativeSubGalleries = subGalleries.map((subGallery) => ({
|
|
464
|
+
...subGallery,
|
|
465
|
+
headerImage: subGallery.headerImage ? path7.relative(galleryDir, subGallery.headerImage) : ""
|
|
466
|
+
}));
|
|
467
|
+
let galleryData = {
|
|
468
|
+
title: "My Gallery",
|
|
469
|
+
description: "My gallery with fantastic photos.",
|
|
470
|
+
headerImage: mediaFiles[0]?.filename || "",
|
|
471
|
+
mediaBasePath,
|
|
472
|
+
metadata: {},
|
|
473
|
+
sections: [
|
|
474
|
+
{
|
|
475
|
+
images: mediaFiles
|
|
476
|
+
}
|
|
477
|
+
],
|
|
478
|
+
subGalleries: {
|
|
479
|
+
title: "Sub Galleries",
|
|
480
|
+
galleries: relativeSubGalleries
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
if (!useDefaultSettings) {
|
|
484
|
+
galleryData = {
|
|
485
|
+
...galleryData,
|
|
486
|
+
...await getGallerySettingsFromUser(
|
|
487
|
+
path7.basename(path7.join(galleryDir, "..")),
|
|
488
|
+
path7.basename(mediaFiles[0]?.filename || ""),
|
|
489
|
+
ui
|
|
490
|
+
)
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
await promises.writeFile(galleryJsonPath, JSON.stringify(galleryData, null, 2));
|
|
494
|
+
}
|
|
495
|
+
async function galleryExists(outputPath) {
|
|
496
|
+
const galleryPath = path7.join(outputPath, "gallery");
|
|
497
|
+
const galleryJsonPath = path7.join(galleryPath, "gallery.json");
|
|
498
|
+
try {
|
|
499
|
+
await promises.access(galleryJsonPath);
|
|
500
|
+
return true;
|
|
501
|
+
} catch {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async function processDirectory(scanPath, outputPath, recursive, useDefaultSettings, force, ui) {
|
|
506
|
+
ui.start(`Scanning ${scanPath}`);
|
|
507
|
+
let totalFiles = 0;
|
|
508
|
+
let totalGalleries = 1;
|
|
509
|
+
const subGalleries = [];
|
|
510
|
+
const { mediaFiles, subGalleryDirectories } = await scanDirectory(scanPath, ui);
|
|
511
|
+
totalFiles += mediaFiles.length;
|
|
512
|
+
if (recursive) {
|
|
513
|
+
for (const subGalleryDir of subGalleryDirectories) {
|
|
514
|
+
const result2 = await processDirectory(
|
|
515
|
+
subGalleryDir,
|
|
516
|
+
path7.join(outputPath, path7.basename(subGalleryDir)),
|
|
517
|
+
recursive,
|
|
518
|
+
useDefaultSettings,
|
|
519
|
+
force,
|
|
520
|
+
ui
|
|
521
|
+
);
|
|
522
|
+
totalFiles += result2.totalFiles;
|
|
523
|
+
totalGalleries += result2.totalGalleries;
|
|
524
|
+
if (result2.subGallery) {
|
|
525
|
+
subGalleries.push(result2.subGallery);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (mediaFiles.length > 0 || subGalleries.length > 0) {
|
|
530
|
+
const galleryPath = path7.join(outputPath, "gallery");
|
|
531
|
+
const galleryJsonPath = path7.join(galleryPath, "gallery.json");
|
|
532
|
+
const exists = await galleryExists(outputPath);
|
|
533
|
+
if (exists && !force) {
|
|
534
|
+
const shouldOverride = await ui.prompt(`Gallery already exists at ${galleryJsonPath}. Do you want to override it?`, {
|
|
535
|
+
type: "confirm",
|
|
536
|
+
default: false
|
|
537
|
+
});
|
|
538
|
+
if (!shouldOverride) {
|
|
539
|
+
ui.info("Skipping gallery creation");
|
|
540
|
+
return { totalFiles: 0, totalGalleries: 0 };
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
try {
|
|
544
|
+
await promises.mkdir(galleryPath, { recursive: true });
|
|
545
|
+
await createGalleryJson(mediaFiles, galleryJsonPath, scanPath, subGalleries, useDefaultSettings, ui);
|
|
546
|
+
ui.success(
|
|
547
|
+
`Create gallery with ${mediaFiles.length} files and ${subGalleries.length} subgalleries at: ${galleryJsonPath}`
|
|
548
|
+
);
|
|
549
|
+
} catch (error) {
|
|
550
|
+
ui.error(`Error creating gallery.json at ${galleryJsonPath}`);
|
|
551
|
+
throw error;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
const result = { totalFiles, totalGalleries };
|
|
555
|
+
if (mediaFiles.length > 0 || subGalleries.length > 0) {
|
|
556
|
+
const dirName = path7.basename(scanPath);
|
|
557
|
+
result.subGallery = {
|
|
558
|
+
title: capitalizeTitle(dirName),
|
|
559
|
+
headerImage: mediaFiles[0]?.filename || "",
|
|
560
|
+
path: path7.join("..", dirName)
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
return result;
|
|
564
|
+
}
|
|
565
|
+
async function init(options, ui) {
|
|
566
|
+
try {
|
|
567
|
+
const scanPath = path7.resolve(options.photos);
|
|
568
|
+
const outputPath = options.gallery ? path7.resolve(options.gallery) : scanPath;
|
|
569
|
+
const result = await processDirectory(scanPath, outputPath, options.recursive, options.default, options.force, ui);
|
|
570
|
+
ui.box(
|
|
571
|
+
`Created ${result.totalGalleries} ${result.totalGalleries === 1 ? "gallery" : "galleries"} with ${result.totalFiles} media ${result.totalFiles === 1 ? "file" : "files"}`
|
|
572
|
+
);
|
|
573
|
+
return {
|
|
574
|
+
processedMediaCount: result.totalFiles,
|
|
575
|
+
processedGalleryCount: result.totalGalleries
|
|
576
|
+
};
|
|
577
|
+
} catch (error) {
|
|
578
|
+
ui.error("Error initializing gallery");
|
|
579
|
+
throw error;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
264
582
|
async function getFileMtime(filePath) {
|
|
265
583
|
const stats = await promises.stat(filePath);
|
|
266
584
|
return stats.mtime;
|
|
267
585
|
}
|
|
268
|
-
async function generateBlurHash(imagePath, componentX = 4, componentY = 3) {
|
|
269
|
-
const image = await loadImage(imagePath);
|
|
270
|
-
const { data, info } = await image.resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
271
|
-
const pixels = new Uint8ClampedArray(data.buffer);
|
|
272
|
-
return encode(pixels, info.width, info.height, componentX, componentY);
|
|
273
|
-
}
|
|
274
586
|
async function getVideoDimensions(filePath) {
|
|
275
587
|
const data = await ffprobe(filePath);
|
|
276
588
|
const videoStream = data.streams.find((stream) => stream.codec_type === "video");
|
|
@@ -331,7 +643,7 @@ async function createVideoThumbnails(inputPath, videoDimensions, outputPath, out
|
|
|
331
643
|
// src/modules/thumbnails/index.ts
|
|
332
644
|
async function processImage(imagePath, thumbnailPath, thumbnailPathRetina, thumbnailSize, lastMediaTimestamp) {
|
|
333
645
|
const fileMtime = await getFileMtime(imagePath);
|
|
334
|
-
if (lastMediaTimestamp && fileMtime <= lastMediaTimestamp &&
|
|
646
|
+
if (lastMediaTimestamp && fileMtime <= lastMediaTimestamp && fs8.existsSync(thumbnailPath)) {
|
|
335
647
|
return void 0;
|
|
336
648
|
}
|
|
337
649
|
const { image, metadata } = await loadImageWithMetadata(imagePath);
|
|
@@ -353,7 +665,7 @@ async function processImage(imagePath, thumbnailPath, thumbnailPathRetina, thumb
|
|
|
353
665
|
const blurHash = await generateBlurHash(thumbnailPath);
|
|
354
666
|
return {
|
|
355
667
|
type: "image",
|
|
356
|
-
|
|
668
|
+
filename: path7.basename(imagePath),
|
|
357
669
|
alt: description,
|
|
358
670
|
width: imageDimensions.width,
|
|
359
671
|
height: imageDimensions.height,
|
|
@@ -369,7 +681,7 @@ async function processImage(imagePath, thumbnailPath, thumbnailPathRetina, thumb
|
|
|
369
681
|
}
|
|
370
682
|
async function processVideo(videoPath, thumbnailPath, thumbnailPathRetina, thumbnailSize, verbose, lastMediaTimestamp) {
|
|
371
683
|
const fileMtime = await getFileMtime(videoPath);
|
|
372
|
-
if (lastMediaTimestamp && fileMtime <= lastMediaTimestamp &&
|
|
684
|
+
if (lastMediaTimestamp && fileMtime <= lastMediaTimestamp && fs8.existsSync(thumbnailPath)) {
|
|
373
685
|
return void 0;
|
|
374
686
|
}
|
|
375
687
|
const videoDimensions = await getVideoDimensions(videoPath);
|
|
@@ -384,7 +696,7 @@ async function processVideo(videoPath, thumbnailPath, thumbnailPathRetina, thumb
|
|
|
384
696
|
const blurHash = await generateBlurHash(thumbnailPath);
|
|
385
697
|
return {
|
|
386
698
|
type: "video",
|
|
387
|
-
|
|
699
|
+
filename: path7.basename(videoPath),
|
|
388
700
|
alt: void 0,
|
|
389
701
|
width: videoDimensions.width,
|
|
390
702
|
height: videoDimensions.height,
|
|
@@ -398,24 +710,24 @@ async function processVideo(videoPath, thumbnailPath, thumbnailPathRetina, thumb
|
|
|
398
710
|
lastMediaTimestamp: fileMtime.toISOString()
|
|
399
711
|
};
|
|
400
712
|
}
|
|
401
|
-
async function processMediaFile(mediaFile, galleryDir, thumbnailsPath, thumbnailSize, ui) {
|
|
713
|
+
async function processMediaFile(mediaFile, mediaBasePath, galleryDir, thumbnailsPath, thumbnailSize, ui) {
|
|
402
714
|
try {
|
|
403
|
-
const
|
|
404
|
-
const
|
|
405
|
-
const
|
|
406
|
-
const
|
|
715
|
+
const filePath = path7.resolve(path7.join(mediaBasePath, mediaFile.filename));
|
|
716
|
+
const fileName = mediaFile.filename;
|
|
717
|
+
const fileNameWithoutExt = path7.parse(fileName).name;
|
|
718
|
+
const galleryJsonDir = path7.join(galleryDir, "gallery");
|
|
407
719
|
const thumbnailFileName = `${fileNameWithoutExt}.avif`;
|
|
408
|
-
const thumbnailPath =
|
|
720
|
+
const thumbnailPath = path7.join(thumbnailsPath, thumbnailFileName);
|
|
409
721
|
const thumbnailPathRetina = thumbnailPath.replace(".avif", "@2x.avif");
|
|
410
|
-
const relativeThumbnailPath =
|
|
411
|
-
const relativeThumbnailRetinaPath =
|
|
722
|
+
const relativeThumbnailPath = path7.relative(galleryJsonDir, thumbnailPath);
|
|
723
|
+
const relativeThumbnailRetinaPath = path7.relative(galleryJsonDir, thumbnailPathRetina);
|
|
412
724
|
const lastMediaTimestamp = mediaFile.lastMediaTimestamp ? new Date(mediaFile.lastMediaTimestamp) : void 0;
|
|
413
725
|
const verbose = ui.level === LogLevels.debug;
|
|
414
726
|
ui.debug(` Processing ${mediaFile.type}: ${fileName}`);
|
|
415
727
|
const updatedMediaFile = await (mediaFile.type === "image" ? processImage(filePath, thumbnailPath, thumbnailPathRetina, thumbnailSize, lastMediaTimestamp) : processVideo(filePath, thumbnailPath, thumbnailPathRetina, thumbnailSize, verbose, lastMediaTimestamp));
|
|
416
728
|
if (!updatedMediaFile) {
|
|
417
729
|
ui.debug(` Skipping ${fileName} because it has already been processed`);
|
|
418
|
-
if (mediaFile.thumbnail && !mediaFile.thumbnail.blurHash &&
|
|
730
|
+
if (mediaFile.thumbnail && !mediaFile.thumbnail.blurHash && fs8.existsSync(thumbnailPath)) {
|
|
419
731
|
try {
|
|
420
732
|
const blurHash = await generateBlurHash(thumbnailPath);
|
|
421
733
|
return {
|
|
@@ -431,34 +743,41 @@ async function processMediaFile(mediaFile, galleryDir, thumbnailsPath, thumbnail
|
|
|
431
743
|
}
|
|
432
744
|
return mediaFile;
|
|
433
745
|
}
|
|
434
|
-
updatedMediaFile.
|
|
746
|
+
updatedMediaFile.filename = mediaFile.filename;
|
|
435
747
|
if (updatedMediaFile.thumbnail) {
|
|
436
748
|
updatedMediaFile.thumbnail.path = relativeThumbnailPath;
|
|
437
749
|
updatedMediaFile.thumbnail.pathRetina = relativeThumbnailRetinaPath;
|
|
438
750
|
}
|
|
439
751
|
return updatedMediaFile;
|
|
440
752
|
} catch (error) {
|
|
441
|
-
handleFileProcessingError(error,
|
|
442
|
-
return mediaFile;
|
|
753
|
+
handleFileProcessingError(error, mediaFile.filename, ui);
|
|
754
|
+
return { ...mediaFile, thumbnail: void 0 };
|
|
443
755
|
}
|
|
444
756
|
}
|
|
445
757
|
async function processGalleryThumbnails(galleryDir, ui) {
|
|
446
|
-
const galleryJsonPath =
|
|
447
|
-
const thumbnailsPath =
|
|
758
|
+
const galleryJsonPath = path7.join(galleryDir, "gallery", "gallery.json");
|
|
759
|
+
const thumbnailsPath = path7.join(galleryDir, "gallery", "images");
|
|
448
760
|
ui.start(`Creating thumbnails: ${galleryDir}`);
|
|
449
761
|
try {
|
|
450
|
-
|
|
451
|
-
const
|
|
452
|
-
const galleryData = GalleryDataSchema.parse(JSON.parse(galleryContent));
|
|
762
|
+
fs8.mkdirSync(thumbnailsPath, { recursive: true });
|
|
763
|
+
const galleryData = parseGalleryJson(galleryJsonPath, ui);
|
|
453
764
|
const thumbnailSize = galleryData.thumbnailSize || DEFAULT_THUMBNAIL_SIZE;
|
|
765
|
+
const mediaBasePath = galleryData.mediaBasePath ?? path7.join(galleryDir);
|
|
454
766
|
let processedCount = 0;
|
|
455
767
|
for (const section of galleryData.sections) {
|
|
456
768
|
for (const [index, mediaFile] of section.images.entries()) {
|
|
457
|
-
section.images[index] = await processMediaFile(
|
|
769
|
+
section.images[index] = await processMediaFile(
|
|
770
|
+
mediaFile,
|
|
771
|
+
mediaBasePath,
|
|
772
|
+
galleryDir,
|
|
773
|
+
thumbnailsPath,
|
|
774
|
+
thumbnailSize,
|
|
775
|
+
ui
|
|
776
|
+
);
|
|
458
777
|
}
|
|
459
778
|
processedCount += section.images.length;
|
|
460
779
|
}
|
|
461
|
-
|
|
780
|
+
fs8.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
|
|
462
781
|
ui.success(`Created thumbnails for ${processedCount} media files`);
|
|
463
782
|
return processedCount;
|
|
464
783
|
} catch (error) {
|
|
@@ -493,68 +812,113 @@ async function thumbnails(options, ui) {
|
|
|
493
812
|
}
|
|
494
813
|
|
|
495
814
|
// src/modules/build/index.ts
|
|
496
|
-
function checkFileIsOneFolderUp(filePath) {
|
|
497
|
-
const normalizedPath = path4.normalize(filePath);
|
|
498
|
-
const pathParts = normalizedPath.split(path4.sep);
|
|
499
|
-
return pathParts.length === 2 && pathParts[0] === "..";
|
|
500
|
-
}
|
|
501
815
|
function copyPhotos(galleryData, galleryDir, ui) {
|
|
502
816
|
for (const section of galleryData.sections) {
|
|
503
817
|
for (const image of section.images) {
|
|
504
|
-
if (
|
|
505
|
-
const sourcePath =
|
|
506
|
-
const
|
|
507
|
-
const destPath = path4.join(galleryDir, fileName);
|
|
818
|
+
if (galleryData.mediaBasePath) {
|
|
819
|
+
const sourcePath = path7.join(galleryData.mediaBasePath, image.filename);
|
|
820
|
+
const destPath = path7.join(galleryDir, image.filename);
|
|
508
821
|
ui.debug(`Copying photo to ${destPath}`);
|
|
509
|
-
|
|
822
|
+
fs8.copyFileSync(sourcePath, destPath);
|
|
510
823
|
}
|
|
511
824
|
}
|
|
512
825
|
}
|
|
513
826
|
}
|
|
827
|
+
async function scanAndAppendNewFiles(galleryDir, galleryJsonPath, galleryData, ui) {
|
|
828
|
+
const scanPath = galleryData.mediaBasePath || galleryDir;
|
|
829
|
+
ui.debug(`Scanning ${scanPath} for new media files`);
|
|
830
|
+
let scanResult;
|
|
831
|
+
try {
|
|
832
|
+
scanResult = await scanDirectory(scanPath, ui);
|
|
833
|
+
} catch {
|
|
834
|
+
ui.debug(`Could not scan directory ${scanPath}`);
|
|
835
|
+
return galleryData;
|
|
836
|
+
}
|
|
837
|
+
const existingFilenames = new Set(
|
|
838
|
+
galleryData.sections.flatMap((section) => section.images.map((image) => image.filename))
|
|
839
|
+
);
|
|
840
|
+
const newMediaFiles = scanResult.mediaFiles.filter((file) => !existingFilenames.has(file.filename));
|
|
841
|
+
if (newMediaFiles.length > 0) {
|
|
842
|
+
ui.info(`Found ${newMediaFiles.length} new media ${newMediaFiles.length === 1 ? "file" : "files"}`);
|
|
843
|
+
if (galleryData.sections.length === 0) {
|
|
844
|
+
galleryData.sections.push({ images: [] });
|
|
845
|
+
}
|
|
846
|
+
const lastSectionIndex = galleryData.sections.length - 1;
|
|
847
|
+
const lastSection = galleryData.sections[lastSectionIndex];
|
|
848
|
+
lastSection.images.push(...newMediaFiles);
|
|
849
|
+
ui.debug("Updating gallery.json with new files");
|
|
850
|
+
fs8.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
|
|
851
|
+
ui.success(`Added ${newMediaFiles.length} new ${newMediaFiles.length === 1 ? "file" : "files"} to gallery.json`);
|
|
852
|
+
} else {
|
|
853
|
+
ui.debug("No new media files found");
|
|
854
|
+
}
|
|
855
|
+
return galleryData;
|
|
856
|
+
}
|
|
514
857
|
async function buildGallery(galleryDir, templateDir, ui, baseUrl) {
|
|
515
858
|
ui.start(`Building gallery ${galleryDir}`);
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
const
|
|
520
|
-
const
|
|
521
|
-
const
|
|
859
|
+
const galleryJsonPath = path7.join(galleryDir, "gallery", "gallery.json");
|
|
860
|
+
let galleryData = parseGalleryJson(galleryJsonPath, ui);
|
|
861
|
+
galleryData = await scanAndAppendNewFiles(galleryDir, galleryJsonPath, galleryData, ui);
|
|
862
|
+
const socialMediaCardImagePath = path7.join(galleryDir, "gallery", "images", "social-media-card.jpg");
|
|
863
|
+
const mediaBasePath = galleryData.mediaBasePath;
|
|
864
|
+
const mediaBaseUrl = baseUrl || galleryData.mediaBaseUrl;
|
|
865
|
+
const headerImagePath = mediaBasePath ? path7.join(mediaBasePath, galleryData.headerImage) : path7.resolve(galleryDir, galleryData.headerImage);
|
|
866
|
+
const imagesFolder = path7.join(galleryDir, "gallery", "images");
|
|
867
|
+
const currentHeaderBasename = path7.basename(headerImagePath, path7.extname(headerImagePath));
|
|
868
|
+
if (!fs8.existsSync(imagesFolder)) {
|
|
869
|
+
fs8.mkdirSync(imagesFolder, { recursive: true });
|
|
870
|
+
}
|
|
871
|
+
const headerImageChanged = hasOldHeaderImages(imagesFolder, currentHeaderBasename);
|
|
872
|
+
if (headerImageChanged) {
|
|
873
|
+
ui.info("Header image changed, cleaning up old assets");
|
|
874
|
+
cleanupOldHeaderImages(imagesFolder, currentHeaderBasename, ui);
|
|
875
|
+
if (fs8.existsSync(socialMediaCardImagePath)) {
|
|
876
|
+
fs8.unlinkSync(socialMediaCardImagePath);
|
|
877
|
+
ui.debug("Deleted old social media card");
|
|
878
|
+
}
|
|
879
|
+
}
|
|
522
880
|
await createGallerySocialMediaCardImage(headerImagePath, galleryData.title, socialMediaCardImagePath, ui);
|
|
523
|
-
galleryData.metadata.image = galleryData.metadata.image || `${galleryData.url || ""}/${
|
|
524
|
-
|
|
525
|
-
await createOptimizedHeaderImage(headerImagePath,
|
|
526
|
-
if (
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
);
|
|
530
|
-
|
|
881
|
+
galleryData.metadata.image = galleryData.metadata.image || `${galleryData.url || ""}/${path7.relative(galleryDir, socialMediaCardImagePath)}`;
|
|
882
|
+
fs8.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
|
|
883
|
+
const { blurHash } = await createOptimizedHeaderImage(headerImagePath, imagesFolder, ui);
|
|
884
|
+
if (galleryData.headerImageBlurHash !== blurHash) {
|
|
885
|
+
ui.debug("Updating gallery.json with header image blurhash");
|
|
886
|
+
galleryData.headerImageBlurHash = blurHash;
|
|
887
|
+
fs8.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
|
|
888
|
+
}
|
|
889
|
+
if (!mediaBaseUrl && mediaBasePath) {
|
|
890
|
+
const shouldCopyPhotos = await ui.prompt("All photos need to be copied. Are you sure you want to continue?", {
|
|
891
|
+
type: "confirm"
|
|
892
|
+
});
|
|
893
|
+
if (shouldCopyPhotos) {
|
|
531
894
|
ui.debug("Copying photos");
|
|
532
895
|
copyPhotos(galleryData, galleryDir, ui);
|
|
533
896
|
}
|
|
534
897
|
}
|
|
535
|
-
if (
|
|
898
|
+
if (mediaBaseUrl && galleryData.mediaBaseUrl !== mediaBaseUrl) {
|
|
536
899
|
ui.debug("Updating gallery.json with baseUrl");
|
|
537
|
-
galleryData.mediaBaseUrl =
|
|
538
|
-
|
|
900
|
+
galleryData.mediaBaseUrl = mediaBaseUrl;
|
|
901
|
+
fs8.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
|
|
539
902
|
}
|
|
903
|
+
await processGalleryThumbnails(galleryDir, ui);
|
|
540
904
|
ui.debug("Building gallery from template");
|
|
541
905
|
try {
|
|
542
906
|
process3.env.GALLERY_JSON_PATH = galleryJsonPath;
|
|
543
|
-
process3.env.GALLERY_OUTPUT_DIR =
|
|
907
|
+
process3.env.GALLERY_OUTPUT_DIR = path7.join(galleryDir, "gallery");
|
|
544
908
|
execSync("npx astro build", { cwd: templateDir, stdio: ui.level === LogLevels.debug ? "inherit" : "ignore" });
|
|
545
909
|
} catch (error) {
|
|
546
910
|
ui.error(`Build failed for ${galleryDir}`);
|
|
547
911
|
throw error;
|
|
548
912
|
}
|
|
549
|
-
const outputDir =
|
|
550
|
-
const buildDir =
|
|
913
|
+
const outputDir = path7.join(galleryDir, "gallery");
|
|
914
|
+
const buildDir = path7.join(outputDir, "_build");
|
|
551
915
|
ui.debug(`Copying build output to ${outputDir}`);
|
|
552
|
-
|
|
916
|
+
fs8.cpSync(buildDir, outputDir, { recursive: true });
|
|
553
917
|
ui.debug("Moving index.html to gallery directory");
|
|
554
|
-
|
|
555
|
-
|
|
918
|
+
fs8.copyFileSync(path7.join(outputDir, "index.html"), path7.join(galleryDir, "index.html"));
|
|
919
|
+
fs8.rmSync(path7.join(outputDir, "index.html"));
|
|
556
920
|
ui.debug("Cleaning up build directory");
|
|
557
|
-
|
|
921
|
+
fs8.rmSync(buildDir, { recursive: true, force: true });
|
|
558
922
|
ui.success(`Gallery built successfully`);
|
|
559
923
|
}
|
|
560
924
|
async function build(options, ui) {
|
|
@@ -565,11 +929,11 @@ async function build(options, ui) {
|
|
|
565
929
|
return { processedGalleryCount: 0 };
|
|
566
930
|
}
|
|
567
931
|
const themePath = await import.meta.resolve("@simple-photo-gallery/theme-modern/package.json");
|
|
568
|
-
const themeDir =
|
|
932
|
+
const themeDir = path7.dirname(new URL(themePath).pathname);
|
|
569
933
|
let totalGalleries = 0;
|
|
570
934
|
for (const dir of galleryDirs) {
|
|
571
|
-
const baseUrl = options.baseUrl ? `${options.baseUrl}${
|
|
572
|
-
await buildGallery(
|
|
935
|
+
const baseUrl = options.baseUrl ? `${options.baseUrl}${path7.relative(options.gallery, dir)}` : void 0;
|
|
936
|
+
await buildGallery(path7.resolve(dir), themeDir, ui, baseUrl);
|
|
573
937
|
++totalGalleries;
|
|
574
938
|
}
|
|
575
939
|
ui.box(`Built ${totalGalleries} ${totalGalleries === 1 ? "gallery" : "galleries"} successfully`);
|
|
@@ -585,20 +949,20 @@ async function build(options, ui) {
|
|
|
585
949
|
}
|
|
586
950
|
async function cleanGallery(galleryDir, ui) {
|
|
587
951
|
let filesRemoved = 0;
|
|
588
|
-
const indexHtmlPath =
|
|
589
|
-
if (
|
|
952
|
+
const indexHtmlPath = path7.join(galleryDir, "index.html");
|
|
953
|
+
if (fs8.existsSync(indexHtmlPath)) {
|
|
590
954
|
try {
|
|
591
|
-
|
|
955
|
+
fs8.rmSync(indexHtmlPath);
|
|
592
956
|
ui.debug(`Removed: ${indexHtmlPath}`);
|
|
593
957
|
filesRemoved++;
|
|
594
958
|
} catch (error) {
|
|
595
959
|
ui.warn(`Failed to remove index.html: ${error}`);
|
|
596
960
|
}
|
|
597
961
|
}
|
|
598
|
-
const galleryPath =
|
|
599
|
-
if (
|
|
962
|
+
const galleryPath = path7.join(galleryDir, "gallery");
|
|
963
|
+
if (fs8.existsSync(galleryPath)) {
|
|
600
964
|
try {
|
|
601
|
-
|
|
965
|
+
fs8.rmSync(galleryPath, { recursive: true, force: true });
|
|
602
966
|
ui.debug(`Removed directory: ${galleryPath}`);
|
|
603
967
|
filesRemoved++;
|
|
604
968
|
} catch (error) {
|
|
@@ -614,8 +978,8 @@ async function cleanGallery(galleryDir, ui) {
|
|
|
614
978
|
}
|
|
615
979
|
async function clean(options, ui) {
|
|
616
980
|
try {
|
|
617
|
-
const basePath =
|
|
618
|
-
if (!
|
|
981
|
+
const basePath = path7.resolve(options.gallery);
|
|
982
|
+
if (!fs8.existsSync(basePath)) {
|
|
619
983
|
ui.error(`Directory does not exist: ${basePath}`);
|
|
620
984
|
return { processedGalleryCount: 0 };
|
|
621
985
|
}
|
|
@@ -634,174 +998,6 @@ async function clean(options, ui) {
|
|
|
634
998
|
throw error;
|
|
635
999
|
}
|
|
636
1000
|
}
|
|
637
|
-
function getMediaFileType(fileName) {
|
|
638
|
-
const ext = path4.extname(fileName).toLowerCase();
|
|
639
|
-
if (IMAGE_EXTENSIONS.has(ext)) return "image";
|
|
640
|
-
if (VIDEO_EXTENSIONS.has(ext)) return "video";
|
|
641
|
-
return null;
|
|
642
|
-
}
|
|
643
|
-
function capitalizeTitle(folderName) {
|
|
644
|
-
return folderName.replace("-", " ").replace("_", " ").split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
// src/modules/init/index.ts
|
|
648
|
-
async function scanDirectory(dirPath, ui) {
|
|
649
|
-
const mediaFiles = [];
|
|
650
|
-
const subGalleryDirectories = [];
|
|
651
|
-
try {
|
|
652
|
-
const entries = await promises.readdir(dirPath, { withFileTypes: true });
|
|
653
|
-
for (const entry of entries) {
|
|
654
|
-
if (entry.isFile()) {
|
|
655
|
-
const fullPath = path4.join(dirPath, entry.name);
|
|
656
|
-
const mediaType = getMediaFileType(entry.name);
|
|
657
|
-
if (mediaType) {
|
|
658
|
-
const mediaFile = {
|
|
659
|
-
type: mediaType,
|
|
660
|
-
path: fullPath,
|
|
661
|
-
width: 0,
|
|
662
|
-
height: 0
|
|
663
|
-
};
|
|
664
|
-
mediaFiles.push(mediaFile);
|
|
665
|
-
}
|
|
666
|
-
} else if (entry.isDirectory() && entry.name !== "gallery") {
|
|
667
|
-
subGalleryDirectories.push(path4.join(dirPath, entry.name));
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
} catch (error) {
|
|
671
|
-
if (error instanceof Error && error.message.includes("ENOENT")) {
|
|
672
|
-
ui.error(`Directory does not exist: ${dirPath}`);
|
|
673
|
-
} else if (error instanceof Error && error.message.includes("ENOTDIR")) {
|
|
674
|
-
ui.error(`Path is not a directory: ${dirPath}`);
|
|
675
|
-
} else {
|
|
676
|
-
ui.error(`Error scanning directory ${dirPath}:`, error);
|
|
677
|
-
}
|
|
678
|
-
throw error;
|
|
679
|
-
}
|
|
680
|
-
return { mediaFiles, subGalleryDirectories };
|
|
681
|
-
}
|
|
682
|
-
async function getGallerySettingsFromUser(galleryName, defaultImage, ui) {
|
|
683
|
-
ui.info(`Enter gallery settings for the gallery in folder "${galleryName}"`);
|
|
684
|
-
const title = await ui.prompt("Enter gallery title", { type: "text", default: "My Gallery", placeholder: "My Gallery" });
|
|
685
|
-
const description = await ui.prompt("Enter gallery description", {
|
|
686
|
-
type: "text",
|
|
687
|
-
default: "My gallery with fantastic photos.",
|
|
688
|
-
placeholder: "My gallery with fantastic photos."
|
|
689
|
-
});
|
|
690
|
-
const url = await ui.prompt("Enter the URL where the gallery will be hosted (important for social media image)", {
|
|
691
|
-
type: "text",
|
|
692
|
-
default: "",
|
|
693
|
-
placeholder: ""
|
|
694
|
-
});
|
|
695
|
-
const headerImageName = await ui.prompt("Enter the name of the header image", {
|
|
696
|
-
type: "text",
|
|
697
|
-
default: defaultImage,
|
|
698
|
-
placeholder: defaultImage
|
|
699
|
-
});
|
|
700
|
-
const headerImage = path4.join("..", headerImageName);
|
|
701
|
-
return { title, description, url, headerImage };
|
|
702
|
-
}
|
|
703
|
-
async function createGalleryJson(mediaFiles, galleryJsonPath, subGalleries = [], useDefaultSettings, ui) {
|
|
704
|
-
const galleryDir = path4.dirname(galleryJsonPath);
|
|
705
|
-
const relativeMediaFiles = mediaFiles.map((file) => ({
|
|
706
|
-
...file,
|
|
707
|
-
path: path4.relative(galleryDir, file.path)
|
|
708
|
-
}));
|
|
709
|
-
const relativeSubGalleries = subGalleries.map((subGallery) => ({
|
|
710
|
-
...subGallery,
|
|
711
|
-
headerImage: subGallery.headerImage ? path4.relative(galleryDir, subGallery.headerImage) : ""
|
|
712
|
-
}));
|
|
713
|
-
let galleryData = {
|
|
714
|
-
title: "My Gallery",
|
|
715
|
-
description: "My gallery with fantastic photos.",
|
|
716
|
-
headerImage: relativeMediaFiles[0]?.path || "",
|
|
717
|
-
metadata: {},
|
|
718
|
-
sections: [
|
|
719
|
-
{
|
|
720
|
-
images: relativeMediaFiles
|
|
721
|
-
}
|
|
722
|
-
],
|
|
723
|
-
subGalleries: {
|
|
724
|
-
title: "Sub Galleries",
|
|
725
|
-
galleries: relativeSubGalleries
|
|
726
|
-
}
|
|
727
|
-
};
|
|
728
|
-
if (!useDefaultSettings) {
|
|
729
|
-
galleryData = {
|
|
730
|
-
...galleryData,
|
|
731
|
-
...await getGallerySettingsFromUser(
|
|
732
|
-
path4.basename(path4.join(galleryDir, "..")),
|
|
733
|
-
path4.basename(mediaFiles[0]?.path || ""),
|
|
734
|
-
ui
|
|
735
|
-
)
|
|
736
|
-
};
|
|
737
|
-
}
|
|
738
|
-
await promises.writeFile(galleryJsonPath, JSON.stringify(galleryData, null, 2));
|
|
739
|
-
}
|
|
740
|
-
async function processDirectory(scanPath, outputPath, recursive, useDefaultSettings, ui) {
|
|
741
|
-
ui.start(`Scanning ${scanPath}`);
|
|
742
|
-
let totalFiles = 0;
|
|
743
|
-
let totalGalleries = 1;
|
|
744
|
-
const subGalleries = [];
|
|
745
|
-
const { mediaFiles, subGalleryDirectories } = await scanDirectory(scanPath, ui);
|
|
746
|
-
totalFiles += mediaFiles.length;
|
|
747
|
-
if (recursive) {
|
|
748
|
-
for (const subGalleryDir of subGalleryDirectories) {
|
|
749
|
-
const result2 = await processDirectory(
|
|
750
|
-
subGalleryDir,
|
|
751
|
-
path4.join(outputPath, path4.basename(subGalleryDir)),
|
|
752
|
-
recursive,
|
|
753
|
-
useDefaultSettings,
|
|
754
|
-
ui
|
|
755
|
-
);
|
|
756
|
-
totalFiles += result2.totalFiles;
|
|
757
|
-
totalGalleries += result2.totalGalleries;
|
|
758
|
-
if (result2.subGallery) {
|
|
759
|
-
subGalleries.push(result2.subGallery);
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
if (mediaFiles.length > 0 || subGalleries.length > 0) {
|
|
764
|
-
const galleryPath = path4.join(outputPath, "gallery");
|
|
765
|
-
const galleryJsonPath = path4.join(galleryPath, "gallery.json");
|
|
766
|
-
try {
|
|
767
|
-
await promises.mkdir(galleryPath, { recursive: true });
|
|
768
|
-
await createGalleryJson(mediaFiles, galleryJsonPath, subGalleries, useDefaultSettings, ui);
|
|
769
|
-
ui.success(
|
|
770
|
-
`Create gallery with ${mediaFiles.length} files and ${subGalleries.length} subgalleries at: ${galleryJsonPath}`
|
|
771
|
-
);
|
|
772
|
-
} catch (error) {
|
|
773
|
-
ui.error(`Error creating gallery.json at ${galleryJsonPath}`);
|
|
774
|
-
throw error;
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
const result = { totalFiles, totalGalleries };
|
|
778
|
-
if (mediaFiles.length > 0 || subGalleries.length > 0) {
|
|
779
|
-
const dirName = path4.basename(scanPath);
|
|
780
|
-
result.subGallery = {
|
|
781
|
-
title: capitalizeTitle(dirName),
|
|
782
|
-
headerImage: mediaFiles[0]?.path || "",
|
|
783
|
-
path: path4.join("..", dirName)
|
|
784
|
-
};
|
|
785
|
-
}
|
|
786
|
-
return result;
|
|
787
|
-
}
|
|
788
|
-
async function init(options, ui) {
|
|
789
|
-
try {
|
|
790
|
-
const scanPath = path4.resolve(options.photos);
|
|
791
|
-
const outputPath = options.gallery ? path4.resolve(options.gallery) : scanPath;
|
|
792
|
-
const result = await processDirectory(scanPath, outputPath, options.recursive, options.default, ui);
|
|
793
|
-
ui.box(
|
|
794
|
-
`Created ${result.totalGalleries} ${result.totalGalleries === 1 ? "gallery" : "galleries"} with ${result.totalFiles} media ${result.totalFiles === 1 ? "file" : "files"}`
|
|
795
|
-
);
|
|
796
|
-
return {
|
|
797
|
-
processedMediaCount: result.totalFiles,
|
|
798
|
-
processedGalleryCount: result.totalGalleries
|
|
799
|
-
};
|
|
800
|
-
} catch (error) {
|
|
801
|
-
ui.error("Error initializing gallery");
|
|
802
|
-
throw error;
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
1001
|
|
|
806
1002
|
// src/modules/telemetry/index.ts
|
|
807
1003
|
async function telemetry(options, ui, telemetryService2) {
|
|
@@ -1040,7 +1236,7 @@ async function waitForUpdateCheck(checkPromise) {
|
|
|
1040
1236
|
// package.json
|
|
1041
1237
|
var package_default = {
|
|
1042
1238
|
name: "simple-photo-gallery",
|
|
1043
|
-
version: "2.0.
|
|
1239
|
+
version: "2.0.10-rc.1"};
|
|
1044
1240
|
|
|
1045
1241
|
// src/index.ts
|
|
1046
1242
|
var program = new Command();
|
|
@@ -1108,7 +1304,7 @@ program.command("init").description("Initialize a gallery by scaning a folder fo
|
|
|
1108
1304
|
).option(
|
|
1109
1305
|
"-g, --gallery <path>",
|
|
1110
1306
|
"Path to the directory where the gallery will be initialized. Default: same directory as the photos folder"
|
|
1111
|
-
).option("-r, --recursive", "Recursively create galleries from all photos subdirectories", false).option("-d, --default", "Use default gallery settings instead of asking the user", false).action(withCommandContext((options, ui) => init(options, ui)));
|
|
1307
|
+
).option("-r, --recursive", "Recursively create galleries from all photos subdirectories", false).option("-d, --default", "Use default gallery settings instead of asking the user", false).option("-f, --force", "Force override existing galleries without asking", false).action(withCommandContext((options, ui) => init(options, ui)));
|
|
1112
1308
|
program.command("thumbnails").description("Create thumbnails for all media files in the gallery").option("-g, --gallery <path>", "Path to the directory of the gallery. Default: current working directory", process3.cwd()).option("-r, --recursive", "Scan subdirectories recursively", false).action(withCommandContext((options, ui) => thumbnails(options, ui)));
|
|
1113
1309
|
program.command("build").description("Build the HTML gallery in the specified directory").option("-g, --gallery <path>", "Path to the directory of the gallery. Default: current working directory", process3.cwd()).option("-r, --recursive", "Scan subdirectories recursively", false).option("-b, --base-url <url>", "Base URL where the photos are hosted").action(withCommandContext((options, ui) => build(options, ui)));
|
|
1114
1310
|
program.command("clean").description("Remove all gallery files and folders (index.html, gallery/)").option("-g, --gallery <path>", "Path to the directory of the gallery. Default: current working directory", process3.cwd()).option("-r, --recursive", "Clean subdirectories recursively", false).action(withCommandContext((options, ui) => clean(options, ui)));
|