simple-photo-gallery 0.0.8 → 2.0.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/README.md +53 -322
- package/dist/index.js +585 -372
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,23 +1,97 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
2
|
+
import process2 from 'process';
|
|
3
3
|
import { Command } from 'commander';
|
|
4
|
+
import { LogLevels, createConsola } from 'consola';
|
|
4
5
|
import { execSync, spawn } from 'child_process';
|
|
5
|
-
import
|
|
6
|
+
import fs4, { promises } from 'fs';
|
|
6
7
|
import path4 from 'path';
|
|
8
|
+
import { Buffer } from 'buffer';
|
|
9
|
+
import sharp from 'sharp';
|
|
10
|
+
import { z } from 'zod';
|
|
7
11
|
import exifReader from 'exif-reader';
|
|
8
12
|
import ffprobe from 'node-ffprobe';
|
|
9
|
-
import sharp2 from 'sharp';
|
|
10
|
-
import { z } from 'zod';
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
path4.dirname(new URL(import.meta.url).pathname);
|
|
15
|
+
async function createGallerySocialMediaCardImage(headerPhotoPath, title, ouputPath, ui) {
|
|
16
|
+
ui.start(`Creating social media card image`);
|
|
17
|
+
const resizedImageBuffer = await sharp(headerPhotoPath).resize(1200, 631, { fit: "cover" }).jpeg({ quality: 90 }).toBuffer();
|
|
18
|
+
const outputPath = ouputPath;
|
|
19
|
+
await sharp(resizedImageBuffer).toFile(outputPath);
|
|
20
|
+
const svgText = `
|
|
21
|
+
<svg width="1200" height="631" xmlns="http://www.w3.org/2000/svg">
|
|
22
|
+
<defs>
|
|
23
|
+
<style>
|
|
24
|
+
.title { font-family: Arial, sans-serif; font-size: 96px; font-weight: bold; fill: white; text-anchor: middle; }
|
|
25
|
+
.description { font-family: Arial, sans-serif; font-size: 48px; fill: white; text-anchor: middle; }
|
|
26
|
+
</style>
|
|
27
|
+
</defs>
|
|
28
|
+
<text x="600" y="250" class="title">${title}</text>
|
|
29
|
+
</svg>
|
|
30
|
+
`;
|
|
31
|
+
const finalImageBuffer = await sharp(resizedImageBuffer).composite([{ input: Buffer.from(svgText), top: 0, left: 0 }]).jpeg({ quality: 90 }).toBuffer();
|
|
32
|
+
await sharp(finalImageBuffer).toFile(outputPath);
|
|
33
|
+
ui.success(`Created social media card image successfully`);
|
|
34
|
+
}
|
|
35
|
+
var ThumbnailSchema = z.object({
|
|
36
|
+
path: z.string(),
|
|
37
|
+
pathRetina: z.string(),
|
|
38
|
+
width: z.number(),
|
|
39
|
+
height: z.number()
|
|
40
|
+
});
|
|
41
|
+
var MediaFileSchema = z.object({
|
|
42
|
+
type: z.enum(["image", "video"]),
|
|
43
|
+
path: z.string(),
|
|
44
|
+
alt: z.string().optional(),
|
|
45
|
+
width: z.number(),
|
|
46
|
+
height: z.number(),
|
|
47
|
+
thumbnail: ThumbnailSchema.optional(),
|
|
48
|
+
lastMediaTimestamp: z.string().optional()
|
|
49
|
+
});
|
|
50
|
+
var GallerySectionSchema = z.object({
|
|
51
|
+
title: z.string().optional(),
|
|
52
|
+
description: z.string().optional(),
|
|
53
|
+
images: z.array(MediaFileSchema)
|
|
54
|
+
});
|
|
55
|
+
var SubGallerySchema = z.object({
|
|
56
|
+
title: z.string(),
|
|
57
|
+
headerImage: z.string(),
|
|
58
|
+
path: z.string()
|
|
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: z.object({
|
|
67
|
+
image: z.string().optional(),
|
|
68
|
+
imageWidth: z.number().optional(),
|
|
69
|
+
imageHeight: z.number().optional(),
|
|
70
|
+
ogUrl: z.string().optional(),
|
|
71
|
+
ogType: z.string().optional(),
|
|
72
|
+
ogSiteName: z.string().optional(),
|
|
73
|
+
twitterSite: z.string().optional(),
|
|
74
|
+
twitterCreator: z.string().optional(),
|
|
75
|
+
author: z.string().optional(),
|
|
76
|
+
keywords: z.string().optional(),
|
|
77
|
+
canonicalUrl: z.string().optional(),
|
|
78
|
+
language: z.string().optional(),
|
|
79
|
+
robots: z.string().optional()
|
|
80
|
+
}),
|
|
81
|
+
galleryOutputPath: z.string().optional(),
|
|
82
|
+
mediaBaseUrl: z.string().optional(),
|
|
83
|
+
sections: z.array(GallerySectionSchema),
|
|
84
|
+
subGalleries: z.object({ title: z.string(), galleries: z.array(SubGallerySchema) })
|
|
85
|
+
});
|
|
86
|
+
function findGalleries(basePath, recursive) {
|
|
13
87
|
const galleryDirs = [];
|
|
14
88
|
const galleryJsonPath = path4.join(basePath, "gallery", "gallery.json");
|
|
15
|
-
if (
|
|
89
|
+
if (fs4.existsSync(galleryJsonPath)) {
|
|
16
90
|
galleryDirs.push(basePath);
|
|
17
91
|
}
|
|
18
92
|
if (recursive) {
|
|
19
93
|
try {
|
|
20
|
-
const entries =
|
|
94
|
+
const entries = fs4.readdirSync(basePath, { withFileTypes: true });
|
|
21
95
|
for (const entry of entries) {
|
|
22
96
|
if (entry.isDirectory() && entry.name !== "gallery") {
|
|
23
97
|
const subPath = path4.join(basePath, entry.name);
|
|
@@ -29,95 +103,408 @@ var findGalleries = (basePath, recursive) => {
|
|
|
29
103
|
}
|
|
30
104
|
}
|
|
31
105
|
return galleryDirs;
|
|
32
|
-
}
|
|
106
|
+
}
|
|
107
|
+
function handleFileProcessingError(error, filename, ui) {
|
|
108
|
+
if (error instanceof Error && (error.message.includes("ffprobe") || error.message.includes("ffmpeg"))) {
|
|
109
|
+
ui.warn(
|
|
110
|
+
`Error processing ${filename}: ffprobe (part of ffmpeg) is required to process videos. Please install ffmpeg and ensure it is available in your PATH`
|
|
111
|
+
);
|
|
112
|
+
} else if (error instanceof Error && error.message.includes("unsupported image format")) {
|
|
113
|
+
ui.warn(`Error processing ${filename}: unsupported image format`);
|
|
114
|
+
} else {
|
|
115
|
+
ui.warn(`Error processing ${filename}`);
|
|
116
|
+
}
|
|
117
|
+
ui.debug(error);
|
|
118
|
+
}
|
|
119
|
+
async function getFileMtime(filePath) {
|
|
120
|
+
const stats = await promises.stat(filePath);
|
|
121
|
+
return stats.mtime;
|
|
122
|
+
}
|
|
123
|
+
async function resizeAndSaveThumbnail(image, outputPath, width, height) {
|
|
124
|
+
await image.resize(width, height, { withoutEnlargement: true }).jpeg({ quality: 90 }).toFile(outputPath);
|
|
125
|
+
}
|
|
126
|
+
async function getImageDescription(metadata) {
|
|
127
|
+
let description;
|
|
128
|
+
if (metadata.exif) {
|
|
129
|
+
try {
|
|
130
|
+
const exifData = exifReader(metadata.exif);
|
|
131
|
+
if (exifData.Image?.ImageDescription) {
|
|
132
|
+
description = exifData.Image.ImageDescription.toString();
|
|
133
|
+
} else if (exifData.Image?.Description) {
|
|
134
|
+
description = exifData.Image.Description.toString();
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return description;
|
|
140
|
+
}
|
|
141
|
+
async function createImageThumbnails(image, metadata, outputPath, outputPathRetina, height) {
|
|
142
|
+
const originalWidth = metadata.width || 0;
|
|
143
|
+
const originalHeight = metadata.height || 0;
|
|
144
|
+
if (originalWidth === 0 || originalHeight === 0) {
|
|
145
|
+
throw new Error("Invalid image dimensions");
|
|
146
|
+
}
|
|
147
|
+
const aspectRatio = originalWidth / originalHeight;
|
|
148
|
+
const width = Math.round(height * aspectRatio);
|
|
149
|
+
await resizeAndSaveThumbnail(image, outputPath, width, height);
|
|
150
|
+
await resizeAndSaveThumbnail(image, outputPathRetina, width * 2, height * 2);
|
|
151
|
+
return { width, height };
|
|
152
|
+
}
|
|
153
|
+
async function getVideoDimensions(filePath) {
|
|
154
|
+
const data = await ffprobe(filePath);
|
|
155
|
+
const videoStream = data.streams.find((stream) => stream.codec_type === "video");
|
|
156
|
+
if (!videoStream) {
|
|
157
|
+
throw new Error("No video stream found");
|
|
158
|
+
}
|
|
159
|
+
const dimensions = {
|
|
160
|
+
width: videoStream.width || 0,
|
|
161
|
+
height: videoStream.height || 0
|
|
162
|
+
};
|
|
163
|
+
if (dimensions.width === 0 || dimensions.height === 0) {
|
|
164
|
+
throw new Error("Invalid video dimensions");
|
|
165
|
+
}
|
|
166
|
+
return dimensions;
|
|
167
|
+
}
|
|
168
|
+
async function createVideoThumbnails(inputPath, videoDimensions, outputPath, outputPathRetina, height, verbose = false) {
|
|
169
|
+
const aspectRatio = videoDimensions.width / videoDimensions.height;
|
|
170
|
+
const width = Math.round(height * aspectRatio);
|
|
171
|
+
const tempFramePath = `${outputPath}.temp.png`;
|
|
172
|
+
return new Promise((resolve, reject) => {
|
|
173
|
+
const ffmpeg = spawn("ffmpeg", [
|
|
174
|
+
"-i",
|
|
175
|
+
inputPath,
|
|
176
|
+
"-vframes",
|
|
177
|
+
"1",
|
|
178
|
+
"-y",
|
|
179
|
+
"-loglevel",
|
|
180
|
+
verbose ? "error" : "quiet",
|
|
181
|
+
tempFramePath
|
|
182
|
+
]);
|
|
183
|
+
ffmpeg.stderr.on("data", (data) => {
|
|
184
|
+
console.log(`ffmpeg: ${data.toString()}`);
|
|
185
|
+
});
|
|
186
|
+
ffmpeg.on("close", async (code) => {
|
|
187
|
+
if (code === 0) {
|
|
188
|
+
try {
|
|
189
|
+
const frameImage = sharp(tempFramePath);
|
|
190
|
+
await resizeAndSaveThumbnail(frameImage, outputPath, width, height);
|
|
191
|
+
await resizeAndSaveThumbnail(frameImage, outputPathRetina, width * 2, height * 2);
|
|
192
|
+
try {
|
|
193
|
+
await promises.unlink(tempFramePath);
|
|
194
|
+
} catch {
|
|
195
|
+
}
|
|
196
|
+
resolve({ width, height });
|
|
197
|
+
} catch (sharpError) {
|
|
198
|
+
reject(new Error(`Failed to process extracted frame: ${sharpError}`));
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
reject(new Error(`ffmpeg exited with code ${code}`));
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
ffmpeg.on("error", (error) => {
|
|
205
|
+
reject(new Error(`Failed to start ffmpeg: ${error.message}`));
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
}
|
|
33
209
|
|
|
34
|
-
// src/
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
210
|
+
// src/config/index.ts
|
|
211
|
+
var DEFAULT_THUMBNAIL_SIZE = 300;
|
|
212
|
+
|
|
213
|
+
// src/modules/thumbnails/index.ts
|
|
214
|
+
async function processImage(imagePath, thumbnailPath, thumbnailPathRetina, thumbnailSize, lastMediaTimestamp) {
|
|
215
|
+
const fileMtime = await getFileMtime(imagePath);
|
|
216
|
+
if (lastMediaTimestamp && fileMtime <= lastMediaTimestamp && fs4.existsSync(thumbnailPath)) {
|
|
217
|
+
return void 0;
|
|
218
|
+
}
|
|
219
|
+
const image = sharp(imagePath);
|
|
220
|
+
const metadata = await image.metadata();
|
|
221
|
+
const imageDimensions = {
|
|
222
|
+
width: metadata.width || 0,
|
|
223
|
+
height: metadata.height || 0
|
|
224
|
+
};
|
|
225
|
+
if (imageDimensions.width === 0 || imageDimensions.height === 0) {
|
|
226
|
+
throw new Error("Invalid image dimensions");
|
|
40
227
|
}
|
|
41
|
-
const
|
|
228
|
+
const description = await getImageDescription(metadata);
|
|
229
|
+
const thumbnailDimensions = await createImageThumbnails(
|
|
230
|
+
image,
|
|
231
|
+
metadata,
|
|
232
|
+
thumbnailPath,
|
|
233
|
+
thumbnailPathRetina,
|
|
234
|
+
thumbnailSize
|
|
235
|
+
);
|
|
236
|
+
return {
|
|
237
|
+
type: "image",
|
|
238
|
+
path: imagePath,
|
|
239
|
+
alt: description,
|
|
240
|
+
width: imageDimensions.width,
|
|
241
|
+
height: imageDimensions.height,
|
|
242
|
+
thumbnail: {
|
|
243
|
+
path: thumbnailPath,
|
|
244
|
+
pathRetina: thumbnailPathRetina,
|
|
245
|
+
width: thumbnailDimensions.width,
|
|
246
|
+
height: thumbnailDimensions.height
|
|
247
|
+
},
|
|
248
|
+
lastMediaTimestamp: fileMtime.toISOString()
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
async function processVideo(videoPath, thumbnailPath, thumbnailPathRetina, thumbnailSize, verbose, lastMediaTimestamp) {
|
|
252
|
+
const fileMtime = await getFileMtime(videoPath);
|
|
253
|
+
if (lastMediaTimestamp && fileMtime <= lastMediaTimestamp && fs4.existsSync(thumbnailPath)) {
|
|
254
|
+
return void 0;
|
|
255
|
+
}
|
|
256
|
+
const videoDimensions = await getVideoDimensions(videoPath);
|
|
257
|
+
const thumbnailDimensions = await createVideoThumbnails(
|
|
258
|
+
videoPath,
|
|
259
|
+
videoDimensions,
|
|
260
|
+
thumbnailPath,
|
|
261
|
+
thumbnailPathRetina,
|
|
262
|
+
thumbnailSize,
|
|
263
|
+
verbose
|
|
264
|
+
);
|
|
265
|
+
return {
|
|
266
|
+
type: "video",
|
|
267
|
+
path: videoPath,
|
|
268
|
+
alt: void 0,
|
|
269
|
+
width: videoDimensions.width,
|
|
270
|
+
height: videoDimensions.height,
|
|
271
|
+
thumbnail: {
|
|
272
|
+
path: thumbnailPath,
|
|
273
|
+
pathRetina: thumbnailPathRetina,
|
|
274
|
+
width: thumbnailDimensions.width,
|
|
275
|
+
height: thumbnailDimensions.height
|
|
276
|
+
},
|
|
277
|
+
lastMediaTimestamp: fileMtime.toISOString()
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
async function processMediaFile(mediaFile, galleryDir, thumbnailsPath, thumbnailSize, ui) {
|
|
42
281
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
282
|
+
const galleryJsonDir = path4.join(galleryDir, "gallery");
|
|
283
|
+
const filePath = path4.resolve(path4.join(galleryJsonDir, mediaFile.path));
|
|
284
|
+
const fileName = path4.basename(filePath);
|
|
285
|
+
const fileNameWithoutExt = path4.parse(fileName).name;
|
|
286
|
+
const thumbnailFileName = `${fileNameWithoutExt}.jpg`;
|
|
287
|
+
const thumbnailPath = path4.join(thumbnailsPath, thumbnailFileName);
|
|
288
|
+
const thumbnailPathRetina = thumbnailPath.replace(".jpg", "@2x.jpg");
|
|
289
|
+
const relativeThumbnailPath = path4.relative(galleryJsonDir, thumbnailPath);
|
|
290
|
+
const relativeThumbnailRetinaPath = path4.relative(galleryJsonDir, thumbnailPathRetina);
|
|
291
|
+
const lastMediaTimestamp = mediaFile.lastMediaTimestamp ? new Date(mediaFile.lastMediaTimestamp) : void 0;
|
|
292
|
+
const verbose = ui.level === LogLevels.debug;
|
|
293
|
+
ui.debug(` Processing ${mediaFile.type}: ${fileName}`);
|
|
294
|
+
const updatedMediaFile = await (mediaFile.type === "image" ? processImage(filePath, thumbnailPath, thumbnailPathRetina, thumbnailSize, lastMediaTimestamp) : processVideo(filePath, thumbnailPath, thumbnailPathRetina, thumbnailSize, verbose, lastMediaTimestamp));
|
|
295
|
+
if (!updatedMediaFile) {
|
|
296
|
+
ui.debug(` Skipping ${fileName} because it has already been processed`);
|
|
297
|
+
return mediaFile;
|
|
298
|
+
}
|
|
299
|
+
updatedMediaFile.path = mediaFile.path;
|
|
300
|
+
if (updatedMediaFile.thumbnail) {
|
|
301
|
+
updatedMediaFile.thumbnail.path = relativeThumbnailPath;
|
|
302
|
+
updatedMediaFile.thumbnail.pathRetina = relativeThumbnailRetinaPath;
|
|
303
|
+
}
|
|
304
|
+
return updatedMediaFile;
|
|
46
305
|
} catch (error) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return;
|
|
50
|
-
} finally {
|
|
51
|
-
process.env = originalEnv;
|
|
306
|
+
handleFileProcessingError(error, path4.basename(mediaFile.path), ui);
|
|
307
|
+
return mediaFile;
|
|
52
308
|
}
|
|
53
|
-
const outputDir = path4.join(galleryDir, "gallery");
|
|
54
|
-
const buildDir = path4.join(outputDir, "_build");
|
|
55
|
-
fs2.cpSync(buildDir, outputDir, { recursive: true });
|
|
56
|
-
fs2.copyFileSync(path4.join(outputDir, "index.html"), path4.join(galleryDir, "index.html"));
|
|
57
|
-
fs2.rmSync(path4.join(outputDir, "index.html"));
|
|
58
|
-
console.log("Cleaning up build directory...");
|
|
59
|
-
fs2.rmSync(buildDir, { recursive: true, force: true });
|
|
60
309
|
}
|
|
61
|
-
async function
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
310
|
+
async function processGalleryThumbnails(galleryDir, ui) {
|
|
311
|
+
const galleryJsonPath = path4.join(galleryDir, "gallery", "gallery.json");
|
|
312
|
+
const thumbnailsPath = path4.join(galleryDir, "gallery", "thumbnails");
|
|
313
|
+
ui.start(`Creating thumbnails: ${galleryDir}`);
|
|
314
|
+
try {
|
|
315
|
+
fs4.mkdirSync(thumbnailsPath, { recursive: true });
|
|
316
|
+
const galleryContent = fs4.readFileSync(galleryJsonPath, "utf8");
|
|
317
|
+
const galleryData = GalleryDataSchema.parse(JSON.parse(galleryContent));
|
|
318
|
+
const thumbnailSize = galleryData.thumbnailSize || DEFAULT_THUMBNAIL_SIZE;
|
|
319
|
+
let processedCount = 0;
|
|
320
|
+
for (const section of galleryData.sections) {
|
|
321
|
+
for (const [index, mediaFile] of section.images.entries()) {
|
|
322
|
+
section.images[index] = await processMediaFile(mediaFile, galleryDir, thumbnailsPath, thumbnailSize, ui);
|
|
323
|
+
}
|
|
324
|
+
processedCount += section.images.length;
|
|
325
|
+
}
|
|
326
|
+
fs4.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
|
|
327
|
+
ui.success(`Created thumbnails for ${processedCount} media files`);
|
|
328
|
+
return processedCount;
|
|
329
|
+
} catch (error) {
|
|
330
|
+
ui.error(`Error creating thumbnails for ${galleryDir}`);
|
|
331
|
+
throw error;
|
|
68
332
|
}
|
|
69
|
-
|
|
70
|
-
|
|
333
|
+
}
|
|
334
|
+
async function thumbnails(options, ui) {
|
|
335
|
+
try {
|
|
336
|
+
const galleryDirs = findGalleries(options.gallery, options.recursive);
|
|
337
|
+
if (galleryDirs.length === 0) {
|
|
338
|
+
ui.error("No galleries found.");
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
let totalGalleries = 0;
|
|
342
|
+
let totalProcessed = 0;
|
|
343
|
+
for (const galleryDir of galleryDirs) {
|
|
344
|
+
const processed = await processGalleryThumbnails(galleryDir, ui);
|
|
345
|
+
if (processed > 0) {
|
|
346
|
+
++totalGalleries;
|
|
347
|
+
totalProcessed += processed;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
ui.box(
|
|
351
|
+
`Created thumbnails for ${totalGalleries} ${totalGalleries === 1 ? "gallery" : "galleries"} with ${totalProcessed} media ${totalProcessed === 1 ? "file" : "files"}`
|
|
352
|
+
);
|
|
353
|
+
} catch (error) {
|
|
354
|
+
ui.error("Error creating thumbnails");
|
|
355
|
+
throw error;
|
|
71
356
|
}
|
|
72
357
|
}
|
|
73
358
|
|
|
74
|
-
// src/modules/
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
description = exifData.Image.Description.toString();
|
|
90
|
-
}
|
|
91
|
-
} catch {
|
|
359
|
+
// src/modules/build/index.ts
|
|
360
|
+
function checkFileIsOneFolderUp(filePath) {
|
|
361
|
+
const normalizedPath = path4.normalize(filePath);
|
|
362
|
+
const pathParts = normalizedPath.split(path4.sep);
|
|
363
|
+
return pathParts.length === 2 && pathParts[0] === "..";
|
|
364
|
+
}
|
|
365
|
+
function copyPhotos(galleryData, galleryDir, ui) {
|
|
366
|
+
for (const section of galleryData.sections) {
|
|
367
|
+
for (const image of section.images) {
|
|
368
|
+
if (!checkFileIsOneFolderUp(image.path)) {
|
|
369
|
+
const sourcePath = path4.join(galleryDir, "gallery", image.path);
|
|
370
|
+
const fileName = path4.basename(image.path);
|
|
371
|
+
const destPath = path4.join(galleryDir, fileName);
|
|
372
|
+
ui.debug(`Copying photo to ${destPath}`);
|
|
373
|
+
fs4.copyFileSync(sourcePath, destPath);
|
|
92
374
|
}
|
|
93
375
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
async function buildGallery(galleryDir, templateDir, ui, baseUrl) {
|
|
379
|
+
ui.start(`Building gallery ${galleryDir}`);
|
|
380
|
+
await processGalleryThumbnails(galleryDir, ui);
|
|
381
|
+
const galleryJsonPath = path4.join(galleryDir, "gallery", "gallery.json");
|
|
382
|
+
const galleryContent = fs4.readFileSync(galleryJsonPath, "utf8");
|
|
383
|
+
const galleryData = GalleryDataSchema.parse(JSON.parse(galleryContent));
|
|
384
|
+
const socialMediaCardImagePath = path4.join(galleryDir, "gallery", "thumbnails", "social-media-card.jpg");
|
|
385
|
+
await createGallerySocialMediaCardImage(
|
|
386
|
+
path4.resolve(path4.join(galleryDir, "gallery"), galleryData.headerImage),
|
|
387
|
+
galleryData.title,
|
|
388
|
+
socialMediaCardImagePath,
|
|
389
|
+
ui
|
|
390
|
+
);
|
|
391
|
+
galleryData.metadata.image = galleryData.metadata.image || `${galleryData.url || ""}/${path4.relative(galleryDir, socialMediaCardImagePath)}`;
|
|
392
|
+
fs4.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
|
|
393
|
+
if (!baseUrl) {
|
|
394
|
+
const shouldCopyPhotos = galleryData.sections.some(
|
|
395
|
+
(section) => section.images.some((image) => !checkFileIsOneFolderUp(image.path))
|
|
396
|
+
);
|
|
397
|
+
if (shouldCopyPhotos && await ui.prompt("All photos need to be copied. Are you sure you want to continue?", { type: "confirm" })) {
|
|
398
|
+
ui.debug("Copying photos");
|
|
399
|
+
copyPhotos(galleryData, galleryDir, ui);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (baseUrl) {
|
|
403
|
+
ui.debug("Updating gallery.json with baseUrl");
|
|
404
|
+
galleryData.mediaBaseUrl = baseUrl;
|
|
405
|
+
fs4.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
|
|
406
|
+
}
|
|
407
|
+
ui.debug("Building gallery form template");
|
|
408
|
+
try {
|
|
409
|
+
process2.env.GALLERY_JSON_PATH = galleryJsonPath;
|
|
410
|
+
process2.env.GALLERY_OUTPUT_DIR = path4.join(galleryDir, "gallery");
|
|
411
|
+
execSync("npx astro build", { cwd: templateDir, stdio: ui.level === LogLevels.debug ? "inherit" : "ignore" });
|
|
99
412
|
} catch (error) {
|
|
100
|
-
|
|
101
|
-
|
|
413
|
+
ui.error(`Build failed for ${galleryDir}`);
|
|
414
|
+
throw error;
|
|
102
415
|
}
|
|
416
|
+
const outputDir = path4.join(galleryDir, "gallery");
|
|
417
|
+
const buildDir = path4.join(outputDir, "_build");
|
|
418
|
+
ui.debug(`Copying build output to ${outputDir}`);
|
|
419
|
+
fs4.cpSync(buildDir, outputDir, { recursive: true });
|
|
420
|
+
ui.debug("Moving index.html to gallery directory");
|
|
421
|
+
fs4.copyFileSync(path4.join(outputDir, "index.html"), path4.join(galleryDir, "index.html"));
|
|
422
|
+
fs4.rmSync(path4.join(outputDir, "index.html"));
|
|
423
|
+
ui.debug("Cleaning up build directory");
|
|
424
|
+
fs4.rmSync(buildDir, { recursive: true, force: true });
|
|
425
|
+
ui.success(`Gallery built successfully`);
|
|
103
426
|
}
|
|
104
|
-
async function
|
|
427
|
+
async function build(options, ui) {
|
|
428
|
+
try {
|
|
429
|
+
const galleryDirs = findGalleries(options.gallery, options.recursive);
|
|
430
|
+
if (galleryDirs.length === 0) {
|
|
431
|
+
ui.error("No galleries found.");
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const themePath = await import.meta.resolve("@simple-photo-gallery/theme-modern/package.json");
|
|
435
|
+
const themeDir = path4.dirname(new URL(themePath).pathname);
|
|
436
|
+
let totalGalleries = 0;
|
|
437
|
+
for (const dir of galleryDirs) {
|
|
438
|
+
const baseUrl = options.baseUrl ? `${options.baseUrl}${path4.relative(options.gallery, dir)}` : void 0;
|
|
439
|
+
await buildGallery(path4.resolve(dir), themeDir, ui, baseUrl);
|
|
440
|
+
++totalGalleries;
|
|
441
|
+
}
|
|
442
|
+
ui.box(`Built ${totalGalleries} ${totalGalleries === 1 ? "gallery" : "galleries"} successfully`);
|
|
443
|
+
} catch (error) {
|
|
444
|
+
if (error instanceof Error && error.message.includes("Cannot find package")) {
|
|
445
|
+
ui.error("Theme package not found: @simple-photo-gallery/theme-modern/package.json");
|
|
446
|
+
} else {
|
|
447
|
+
ui.error("Error building gallery");
|
|
448
|
+
}
|
|
449
|
+
throw error;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
async function cleanGallery(galleryDir, ui) {
|
|
453
|
+
let filesRemoved = 0;
|
|
454
|
+
const indexHtmlPath = path4.join(galleryDir, "index.html");
|
|
455
|
+
if (fs4.existsSync(indexHtmlPath)) {
|
|
456
|
+
try {
|
|
457
|
+
fs4.rmSync(indexHtmlPath);
|
|
458
|
+
ui.debug(`Removed: ${indexHtmlPath}`);
|
|
459
|
+
filesRemoved++;
|
|
460
|
+
} catch (error) {
|
|
461
|
+
ui.warn(`Failed to remove index.html: ${error}`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const galleryPath = path4.join(galleryDir, "gallery");
|
|
465
|
+
if (fs4.existsSync(galleryPath)) {
|
|
466
|
+
try {
|
|
467
|
+
fs4.rmSync(galleryPath, { recursive: true, force: true });
|
|
468
|
+
ui.debug(`Removed directory: ${galleryPath}`);
|
|
469
|
+
filesRemoved++;
|
|
470
|
+
} catch (error) {
|
|
471
|
+
ui.warn(`Failed to remove gallery directory: ${error}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (filesRemoved > 0) {
|
|
475
|
+
ui.success(`Cleaned gallery at: ${galleryDir}`);
|
|
476
|
+
} else {
|
|
477
|
+
ui.info(`No gallery files found at: ${galleryDir}`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
async function clean(options, ui) {
|
|
105
481
|
try {
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
482
|
+
const basePath = path4.resolve(options.gallery);
|
|
483
|
+
if (!fs4.existsSync(basePath)) {
|
|
484
|
+
ui.error(`Directory does not exist: ${basePath}`);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const galleryDirs = findGalleries(basePath, options.recursive);
|
|
488
|
+
if (galleryDirs.length === 0) {
|
|
489
|
+
ui.info("No galleries found to clean.");
|
|
490
|
+
return;
|
|
113
491
|
}
|
|
114
|
-
|
|
492
|
+
for (const dir of galleryDirs) {
|
|
493
|
+
await cleanGallery(dir, ui);
|
|
494
|
+
}
|
|
495
|
+
ui.box(`Successfully cleaned ${galleryDirs.length} ${galleryDirs.length === 1 ? "gallery" : "galleries"}`);
|
|
115
496
|
} catch (error) {
|
|
116
|
-
|
|
117
|
-
|
|
497
|
+
ui.error("Error cleaning galleries");
|
|
498
|
+
throw error;
|
|
118
499
|
}
|
|
119
500
|
}
|
|
120
|
-
|
|
501
|
+
|
|
502
|
+
// src/modules/init/const/index.ts
|
|
503
|
+
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tiff", ".tif", ".svg"]);
|
|
504
|
+
var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([".mp4", ".avi", ".mov", ".wmv", ".flv", ".webm", ".mkv", ".m4v", ".3gp"]);
|
|
505
|
+
|
|
506
|
+
// src/modules/init/utils/index.ts
|
|
507
|
+
function getMediaFileType(fileName) {
|
|
121
508
|
const ext = path4.extname(fileName).toLowerCase();
|
|
122
509
|
if (IMAGE_EXTENSIONS.has(ext)) return "image";
|
|
123
510
|
if (VIDEO_EXTENSIONS.has(ext)) return "video";
|
|
@@ -128,58 +515,62 @@ function capitalizeTitle(folderName) {
|
|
|
128
515
|
}
|
|
129
516
|
|
|
130
517
|
// src/modules/init/index.ts
|
|
131
|
-
async function scanDirectory(dirPath) {
|
|
518
|
+
async function scanDirectory(dirPath, ui) {
|
|
132
519
|
const mediaFiles = [];
|
|
520
|
+
const subGalleryDirectories = [];
|
|
133
521
|
try {
|
|
134
522
|
const entries = await promises.readdir(dirPath, { withFileTypes: true });
|
|
135
523
|
for (const entry of entries) {
|
|
136
524
|
if (entry.isFile()) {
|
|
137
525
|
const fullPath = path4.join(dirPath, entry.name);
|
|
138
|
-
const mediaType =
|
|
526
|
+
const mediaType = getMediaFileType(entry.name);
|
|
139
527
|
if (mediaType) {
|
|
140
|
-
console.log(`Processing ${mediaType}: ${entry.name}`);
|
|
141
|
-
let metadata = { width: 0, height: 0 };
|
|
142
|
-
try {
|
|
143
|
-
if (mediaType === "image") {
|
|
144
|
-
metadata = await getImageMetadata(fullPath);
|
|
145
|
-
} else if (mediaType === "video") {
|
|
146
|
-
try {
|
|
147
|
-
const videoDimensions = await getVideoDimensions(fullPath);
|
|
148
|
-
metadata = { ...videoDimensions };
|
|
149
|
-
} catch (videoError) {
|
|
150
|
-
if (typeof videoError === "object" && videoError !== null && "message" in videoError && typeof videoError.message === "string" && (videoError.message.includes("ffprobe") || videoError.message.includes("ffmpeg"))) {
|
|
151
|
-
console.error(
|
|
152
|
-
`Error: ffprobe (part of ffmpeg) is required to process videos. Please install ffmpeg and ensure it is available in your PATH. Skipping video: ${entry.name}`
|
|
153
|
-
);
|
|
154
|
-
} else {
|
|
155
|
-
console.error(`Error processing video ${entry.name}:`, videoError);
|
|
156
|
-
}
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
} catch (mediaError) {
|
|
161
|
-
console.error(`Error processing file ${entry.name}:`, mediaError);
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
528
|
const mediaFile = {
|
|
165
529
|
type: mediaType,
|
|
166
530
|
path: fullPath,
|
|
167
|
-
width:
|
|
168
|
-
height:
|
|
531
|
+
width: 0,
|
|
532
|
+
height: 0
|
|
169
533
|
};
|
|
170
|
-
if (metadata.description) {
|
|
171
|
-
mediaFile.alt = metadata.description;
|
|
172
|
-
}
|
|
173
534
|
mediaFiles.push(mediaFile);
|
|
174
535
|
}
|
|
536
|
+
} else if (entry.isDirectory() && entry.name !== "gallery") {
|
|
537
|
+
subGalleryDirectories.push(path4.join(dirPath, entry.name));
|
|
175
538
|
}
|
|
176
539
|
}
|
|
177
540
|
} catch (error) {
|
|
178
|
-
|
|
541
|
+
if (error instanceof Error && error.message.includes("ENOENT")) {
|
|
542
|
+
ui.error(`Directory does not exist: ${dirPath}`);
|
|
543
|
+
} else if (error instanceof Error && error.message.includes("ENOTDIR")) {
|
|
544
|
+
ui.error(`Path is not a directory: ${dirPath}`);
|
|
545
|
+
} else {
|
|
546
|
+
ui.error(`Error scanning directory ${dirPath}:`, error);
|
|
547
|
+
}
|
|
548
|
+
throw error;
|
|
179
549
|
}
|
|
180
|
-
return mediaFiles;
|
|
550
|
+
return { mediaFiles, subGalleryDirectories };
|
|
551
|
+
}
|
|
552
|
+
async function getGallerySettingsFromUser(galleryName, defaultImage, ui) {
|
|
553
|
+
ui.info(`Enter gallery settings for the gallery in folder "${galleryName}"`);
|
|
554
|
+
const title = await ui.prompt("Enter gallery title", { type: "text", default: "My Gallery", placeholder: "My Gallery" });
|
|
555
|
+
const description = await ui.prompt("Enter gallery description", {
|
|
556
|
+
type: "text",
|
|
557
|
+
default: "My gallery with fantastic photos.",
|
|
558
|
+
placeholder: "My gallery with fantastic photos."
|
|
559
|
+
});
|
|
560
|
+
const url = await ui.prompt("Enter the URL where the gallery will be hosted (important for social media image)", {
|
|
561
|
+
type: "text",
|
|
562
|
+
default: "",
|
|
563
|
+
placeholder: ""
|
|
564
|
+
});
|
|
565
|
+
const headerImageName = await ui.prompt("Enter the name of the header image", {
|
|
566
|
+
type: "text",
|
|
567
|
+
default: defaultImage,
|
|
568
|
+
placeholder: defaultImage
|
|
569
|
+
});
|
|
570
|
+
const headerImage = path4.join("..", headerImageName);
|
|
571
|
+
return { title, description, url, headerImage };
|
|
181
572
|
}
|
|
182
|
-
async function createGalleryJson(mediaFiles, galleryJsonPath, subGalleries = []) {
|
|
573
|
+
async function createGalleryJson(mediaFiles, galleryJsonPath, subGalleries = [], useDefaultSettings, ui) {
|
|
183
574
|
const galleryDir = path4.dirname(galleryJsonPath);
|
|
184
575
|
const relativeMediaFiles = mediaFiles.map((file) => ({
|
|
185
576
|
...file,
|
|
@@ -189,11 +580,11 @@ async function createGalleryJson(mediaFiles, galleryJsonPath, subGalleries = [])
|
|
|
189
580
|
...subGallery,
|
|
190
581
|
headerImage: subGallery.headerImage ? path4.relative(galleryDir, subGallery.headerImage) : ""
|
|
191
582
|
}));
|
|
192
|
-
|
|
583
|
+
let galleryData = {
|
|
193
584
|
title: "My Gallery",
|
|
194
585
|
description: "My gallery with fantastic photos.",
|
|
195
586
|
headerImage: relativeMediaFiles[0]?.path || "",
|
|
196
|
-
metadata: {
|
|
587
|
+
metadata: {},
|
|
197
588
|
sections: [
|
|
198
589
|
{
|
|
199
590
|
images: relativeMediaFiles
|
|
@@ -204,42 +595,58 @@ async function createGalleryJson(mediaFiles, galleryJsonPath, subGalleries = [])
|
|
|
204
595
|
galleries: relativeSubGalleries
|
|
205
596
|
}
|
|
206
597
|
};
|
|
598
|
+
if (!useDefaultSettings) {
|
|
599
|
+
galleryData = {
|
|
600
|
+
...galleryData,
|
|
601
|
+
...await getGallerySettingsFromUser(
|
|
602
|
+
path4.basename(path4.join(galleryDir, "..")),
|
|
603
|
+
path4.basename(mediaFiles[0]?.path || ""),
|
|
604
|
+
ui
|
|
605
|
+
)
|
|
606
|
+
};
|
|
607
|
+
}
|
|
207
608
|
await promises.writeFile(galleryJsonPath, JSON.stringify(galleryData, null, 2));
|
|
208
609
|
}
|
|
209
|
-
async function processDirectory(
|
|
610
|
+
async function processDirectory(scanPath, outputPath, recursive, useDefaultSettings, ui) {
|
|
611
|
+
ui.start(`Scanning ${scanPath}`);
|
|
210
612
|
let totalFiles = 0;
|
|
613
|
+
let totalGalleries = 1;
|
|
211
614
|
const subGalleries = [];
|
|
212
|
-
const mediaFiles = await scanDirectory(
|
|
615
|
+
const { mediaFiles, subGalleryDirectories } = await scanDirectory(scanPath, ui);
|
|
213
616
|
totalFiles += mediaFiles.length;
|
|
214
|
-
if (
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
617
|
+
if (recursive) {
|
|
618
|
+
for (const subGalleryDir of subGalleryDirectories) {
|
|
619
|
+
const result2 = await processDirectory(
|
|
620
|
+
subGalleryDir,
|
|
621
|
+
path4.join(outputPath, path4.basename(subGalleryDir)),
|
|
622
|
+
recursive,
|
|
623
|
+
useDefaultSettings,
|
|
624
|
+
ui
|
|
625
|
+
);
|
|
626
|
+
totalFiles += result2.totalFiles;
|
|
627
|
+
totalGalleries += result2.totalGalleries;
|
|
628
|
+
if (result2.subGallery) {
|
|
629
|
+
subGalleries.push(result2.subGallery);
|
|
226
630
|
}
|
|
227
|
-
} catch (error) {
|
|
228
|
-
console.error(`Error reading directory ${dirPath}:`, error);
|
|
229
631
|
}
|
|
230
632
|
}
|
|
231
633
|
if (mediaFiles.length > 0 || subGalleries.length > 0) {
|
|
232
|
-
const
|
|
233
|
-
const galleryJsonPath = path4.join(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
634
|
+
const galleryPath = path4.join(outputPath, "gallery");
|
|
635
|
+
const galleryJsonPath = path4.join(galleryPath, "gallery.json");
|
|
636
|
+
try {
|
|
637
|
+
await promises.mkdir(galleryPath, { recursive: true });
|
|
638
|
+
await createGalleryJson(mediaFiles, galleryJsonPath, subGalleries, useDefaultSettings, ui);
|
|
639
|
+
ui.success(
|
|
640
|
+
`Create gallery with ${mediaFiles.length} files and ${subGalleries.length} subgalleries at: ${galleryJsonPath}`
|
|
641
|
+
);
|
|
642
|
+
} catch (error) {
|
|
643
|
+
ui.error(`Error creating gallery.json at ${galleryJsonPath}`);
|
|
644
|
+
throw error;
|
|
645
|
+
}
|
|
239
646
|
}
|
|
240
|
-
const result = { totalFiles };
|
|
647
|
+
const result = { totalFiles, totalGalleries };
|
|
241
648
|
if (mediaFiles.length > 0 || subGalleries.length > 0) {
|
|
242
|
-
const dirName = path4.basename(
|
|
649
|
+
const dirName = path4.basename(scanPath);
|
|
243
650
|
result.subGallery = {
|
|
244
651
|
title: capitalizeTitle(dirName),
|
|
245
652
|
headerImage: mediaFiles[0]?.path || "",
|
|
@@ -248,250 +655,56 @@ async function processDirectory(dirPath, options) {
|
|
|
248
655
|
}
|
|
249
656
|
return result;
|
|
250
657
|
}
|
|
251
|
-
async function init(options) {
|
|
252
|
-
const scanPath = path4.resolve(options.path);
|
|
253
|
-
console.log(`Scanning directory: ${scanPath}`);
|
|
254
|
-
console.log(`Recursive: ${options.recursive}`);
|
|
255
|
-
try {
|
|
256
|
-
await promises.access(scanPath);
|
|
257
|
-
const result = await processDirectory(scanPath, options);
|
|
258
|
-
console.log(`Total files processed: ${result.totalFiles}`);
|
|
259
|
-
} catch (error) {
|
|
260
|
-
throw new Error(`Error during scan: ${error}`);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
async function checkFfmpegAvailability() {
|
|
264
|
-
return new Promise((resolve) => {
|
|
265
|
-
const ffmpeg = spawn("ffmpeg", ["-version"]);
|
|
266
|
-
ffmpeg.on("close", (code) => {
|
|
267
|
-
resolve(code === 0);
|
|
268
|
-
});
|
|
269
|
-
ffmpeg.on("error", () => {
|
|
270
|
-
resolve(false);
|
|
271
|
-
});
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
async function resizeAndSaveThumbnail(image, outputPath, width, height) {
|
|
275
|
-
await image.resize(width, height, { withoutEnlargement: true }).jpeg({ quality: 90 }).toFile(outputPath);
|
|
276
|
-
}
|
|
277
|
-
async function createImageThumbnail(inputPath, outputPath, height) {
|
|
278
|
-
try {
|
|
279
|
-
await promises.access(inputPath);
|
|
280
|
-
const image = sharp2(inputPath);
|
|
281
|
-
const metadata = await image.metadata();
|
|
282
|
-
const originalWidth = metadata.width || 0;
|
|
283
|
-
const originalHeight = metadata.height || 0;
|
|
284
|
-
if (originalWidth === 0 || originalHeight === 0) {
|
|
285
|
-
throw new Error("Invalid image dimensions");
|
|
286
|
-
}
|
|
287
|
-
const aspectRatio = originalWidth / originalHeight;
|
|
288
|
-
const width = Math.round(height * aspectRatio);
|
|
289
|
-
await resizeAndSaveThumbnail(image, outputPath, width, height);
|
|
290
|
-
return { width, height };
|
|
291
|
-
} catch (error) {
|
|
292
|
-
throw new Error(`Failed to create image thumbnail for ${inputPath}: ${error}`);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
async function createVideoThumbnail(inputPath, outputPath, height) {
|
|
658
|
+
async function init(options, ui) {
|
|
296
659
|
try {
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const videoData = await ffprobe(inputPath);
|
|
304
|
-
const videoStream = videoData.streams.find((stream) => stream.codec_type === "video");
|
|
305
|
-
if (!videoStream) {
|
|
306
|
-
throw new Error("No video stream found");
|
|
307
|
-
}
|
|
308
|
-
const originalWidth = videoStream.width || 0;
|
|
309
|
-
const originalHeight = videoStream.height || 0;
|
|
310
|
-
if (originalWidth === 0 || originalHeight === 0) {
|
|
311
|
-
throw new Error("Invalid video dimensions");
|
|
312
|
-
}
|
|
313
|
-
const aspectRatio = originalWidth / originalHeight;
|
|
314
|
-
const width = Math.round(height * aspectRatio);
|
|
315
|
-
const tempFramePath = `${outputPath}.temp.png`;
|
|
316
|
-
return new Promise((resolve, reject) => {
|
|
317
|
-
const ffmpeg = spawn("ffmpeg", [
|
|
318
|
-
"-i",
|
|
319
|
-
inputPath,
|
|
320
|
-
"-vframes",
|
|
321
|
-
"1",
|
|
322
|
-
"-y",
|
|
323
|
-
// Overwrite output file
|
|
324
|
-
tempFramePath
|
|
325
|
-
]);
|
|
326
|
-
ffmpeg.stderr.on("data", (data) => {
|
|
327
|
-
console.debug(`ffmpeg: ${data.toString()}`);
|
|
328
|
-
});
|
|
329
|
-
ffmpeg.on("close", async (code) => {
|
|
330
|
-
if (code === 0) {
|
|
331
|
-
try {
|
|
332
|
-
const frameImage = sharp2(tempFramePath);
|
|
333
|
-
await resizeAndSaveThumbnail(frameImage, outputPath, width, height);
|
|
334
|
-
try {
|
|
335
|
-
await promises.unlink(tempFramePath);
|
|
336
|
-
} catch {
|
|
337
|
-
}
|
|
338
|
-
resolve({ width, height });
|
|
339
|
-
} catch (sharpError) {
|
|
340
|
-
reject(new Error(`Failed to process extracted frame: ${sharpError}`));
|
|
341
|
-
}
|
|
342
|
-
} else {
|
|
343
|
-
reject(new Error(`ffmpeg exited with code ${code}`));
|
|
344
|
-
}
|
|
345
|
-
});
|
|
346
|
-
ffmpeg.on("error", (error) => {
|
|
347
|
-
reject(new Error(`Failed to start ffmpeg: ${error.message}`));
|
|
348
|
-
});
|
|
349
|
-
});
|
|
660
|
+
const scanPath = path4.resolve(options.photos);
|
|
661
|
+
const outputPath = options.gallery ? path4.resolve(options.gallery) : scanPath;
|
|
662
|
+
const result = await processDirectory(scanPath, outputPath, options.recursive, options.default, ui);
|
|
663
|
+
ui.box(
|
|
664
|
+
`Created ${result.totalGalleries} ${result.totalGalleries === 1 ? "gallery" : "galleries"} with ${result.totalFiles} media ${result.totalFiles === 1 ? "file" : "files"}`
|
|
665
|
+
);
|
|
350
666
|
} catch (error) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
}
|
|
354
|
-
if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string" && (error.message.includes("ffmpeg") || error.message.includes("ffprobe"))) {
|
|
355
|
-
throw new Error(
|
|
356
|
-
`Error: ffmpeg is required to process videos. Please install ffmpeg and ensure it is available in your PATH. Failed to process: ${path4.basename(inputPath)}`
|
|
357
|
-
);
|
|
358
|
-
}
|
|
359
|
-
throw new Error(`Failed to create video thumbnail for ${inputPath}: ${error}`);
|
|
667
|
+
ui.error("Error initializing gallery");
|
|
668
|
+
throw error;
|
|
360
669
|
}
|
|
361
670
|
}
|
|
362
|
-
var ThumbnailSchema = z.object({
|
|
363
|
-
path: z.string(),
|
|
364
|
-
width: z.number(),
|
|
365
|
-
height: z.number()
|
|
366
|
-
});
|
|
367
|
-
var MediaFileSchema = z.object({
|
|
368
|
-
type: z.enum(["image", "video"]),
|
|
369
|
-
path: z.string(),
|
|
370
|
-
alt: z.string().optional(),
|
|
371
|
-
width: z.number(),
|
|
372
|
-
height: z.number(),
|
|
373
|
-
thumbnail: ThumbnailSchema.optional()
|
|
374
|
-
});
|
|
375
|
-
var GallerySectionSchema = z.object({
|
|
376
|
-
title: z.string().optional(),
|
|
377
|
-
description: z.string().optional(),
|
|
378
|
-
images: z.array(MediaFileSchema)
|
|
379
|
-
});
|
|
380
|
-
var SubGallerySchema = z.object({
|
|
381
|
-
title: z.string(),
|
|
382
|
-
headerImage: z.string(),
|
|
383
|
-
path: z.string()
|
|
384
|
-
});
|
|
385
|
-
var GalleryDataSchema = z.object({
|
|
386
|
-
title: z.string(),
|
|
387
|
-
description: z.string(),
|
|
388
|
-
headerImage: z.string(),
|
|
389
|
-
metadata: z.object({
|
|
390
|
-
ogUrl: z.string()
|
|
391
|
-
}),
|
|
392
|
-
galleryOutputPath: z.string().optional(),
|
|
393
|
-
sections: z.array(GallerySectionSchema),
|
|
394
|
-
subGalleries: z.object({ title: z.string(), galleries: z.array(SubGallerySchema) })
|
|
395
|
-
});
|
|
396
671
|
|
|
397
|
-
// src/
|
|
398
|
-
var
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
const galleryData = GalleryDataSchema.parse(JSON.parse(galleryContent));
|
|
407
|
-
let processedCount = 0;
|
|
408
|
-
for (const section of galleryData.sections) {
|
|
409
|
-
for (const [index, mediaFile] of section.images.entries()) {
|
|
410
|
-
section.images[index] = await processMediaFile(mediaFile, galleryDir, thumbnailsPath, size);
|
|
411
|
-
}
|
|
412
|
-
processedCount += section.images.length;
|
|
413
|
-
}
|
|
414
|
-
fs2.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
|
|
415
|
-
return processedCount;
|
|
416
|
-
} catch (error) {
|
|
417
|
-
console.error(`Error creating thumbnails for ${galleryDir}:`, error);
|
|
418
|
-
return 0;
|
|
419
|
-
}
|
|
420
|
-
};
|
|
421
|
-
async function processMediaFile(mediaFile, galleryDir, thumbnailsPath, size) {
|
|
422
|
-
try {
|
|
423
|
-
const galleryJsonDir = path4.join(galleryDir, "gallery");
|
|
424
|
-
const filePath = path4.resolve(path4.join(galleryJsonDir, mediaFile.path));
|
|
425
|
-
const fileName = path4.basename(filePath);
|
|
426
|
-
const fileNameWithoutExt = path4.parse(fileName).name;
|
|
427
|
-
const thumbnailFileName = `${fileNameWithoutExt}.jpg`;
|
|
428
|
-
const thumbnailPath = path4.join(thumbnailsPath, thumbnailFileName);
|
|
429
|
-
const relativeThumbnailPath = path4.relative(galleryJsonDir, thumbnailPath);
|
|
430
|
-
console.log(`Processing ${mediaFile.type}: ${fileName}`);
|
|
431
|
-
let thumbnailDimensions;
|
|
432
|
-
if (mediaFile.type === "image") {
|
|
433
|
-
thumbnailDimensions = await createImageThumbnail(filePath, thumbnailPath, size);
|
|
434
|
-
} else if (mediaFile.type === "video") {
|
|
435
|
-
thumbnailDimensions = await createVideoThumbnail(filePath, thumbnailPath, size);
|
|
436
|
-
} else {
|
|
437
|
-
console.warn(`Unknown media type: ${mediaFile.type}, skipping...`);
|
|
438
|
-
return mediaFile;
|
|
439
|
-
}
|
|
440
|
-
const updatedMediaFile = {
|
|
441
|
-
...mediaFile,
|
|
442
|
-
thumbnail: {
|
|
443
|
-
path: relativeThumbnailPath,
|
|
444
|
-
width: thumbnailDimensions.width,
|
|
445
|
-
height: thumbnailDimensions.height
|
|
446
|
-
}
|
|
447
|
-
};
|
|
448
|
-
return updatedMediaFile;
|
|
449
|
-
} catch (error) {
|
|
450
|
-
if (error instanceof Error && error.message === "FFMPEG_NOT_AVAILABLE") {
|
|
451
|
-
console.warn(`\u26A0 Skipping video thumbnail (ffmpeg not available): ${path4.basename(mediaFile.path)}`);
|
|
452
|
-
} else {
|
|
453
|
-
console.error(`Error processing ${mediaFile.path}:`, error);
|
|
454
|
-
}
|
|
455
|
-
return mediaFile;
|
|
672
|
+
// src/index.ts
|
|
673
|
+
var program = new Command();
|
|
674
|
+
program.name("gallery").description("Simple Photo Gallery CLI").version("0.0.1").option("-v, --verbose", "Verbose output (debug level)", false).option("-q, --quiet", "Minimal output (only warnings/errors)", false).showHelpAfterError(true);
|
|
675
|
+
function createConsolaUI(globalOpts) {
|
|
676
|
+
let level = LogLevels.info;
|
|
677
|
+
if (globalOpts.quiet) {
|
|
678
|
+
level = LogLevels.warn;
|
|
679
|
+
} else if (globalOpts.verbose) {
|
|
680
|
+
level = LogLevels.debug;
|
|
456
681
|
}
|
|
682
|
+
return createConsola({
|
|
683
|
+
level
|
|
684
|
+
}).withTag("simple-photo-gallery");
|
|
457
685
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
totalProcessed += processed;
|
|
469
|
-
}
|
|
470
|
-
if (options.recursive) {
|
|
471
|
-
console.log(`Completed processing ${totalProcessed} total media files across ${galleryDirs.length} galleries.`);
|
|
472
|
-
} else {
|
|
473
|
-
console.log(`Completed processing ${totalProcessed} media files.`);
|
|
474
|
-
}
|
|
686
|
+
function withConsolaUI(handler) {
|
|
687
|
+
return async (opts) => {
|
|
688
|
+
const ui = createConsolaUI(program.opts());
|
|
689
|
+
try {
|
|
690
|
+
await handler(opts, ui);
|
|
691
|
+
} catch (error) {
|
|
692
|
+
ui.debug(error);
|
|
693
|
+
process2.exitCode = 1;
|
|
694
|
+
}
|
|
695
|
+
};
|
|
475
696
|
}
|
|
476
|
-
|
|
477
|
-
// src/index.ts
|
|
478
|
-
var program = new Command();
|
|
479
|
-
program.name("gallery").description("Simple Photo Gallery CLI").version("0.0.1");
|
|
480
697
|
program.command("init").description("Initialize a gallery by scaning a folder for images and videos").option(
|
|
481
|
-
"-p, --
|
|
482
|
-
"Path where the
|
|
483
|
-
|
|
484
|
-
).option(
|
|
485
|
-
|
|
486
|
-
"
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
).option("-
|
|
490
|
-
program.command("
|
|
491
|
-
"-p, --path <path>",
|
|
492
|
-
"Path to the folder containing the gallery.json file. Default: current working directory",
|
|
493
|
-
process.cwd()
|
|
494
|
-
).option("-r, --recursive", "Scan subdirectories recursively", false).action(build);
|
|
698
|
+
"-p, --photos <path>",
|
|
699
|
+
"Path to the folder where the photos are stored. Default: current working directory",
|
|
700
|
+
process2.cwd()
|
|
701
|
+
).option(
|
|
702
|
+
"-g, --gallery <path>",
|
|
703
|
+
"Path to the directory where the gallery will be initialized. Default: same directory as the photos folder"
|
|
704
|
+
).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(withConsolaUI(init));
|
|
705
|
+
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", process2.cwd()).option("-r, --recursive", "Scan subdirectories recursively", false).action(withConsolaUI(thumbnails));
|
|
706
|
+
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", process2.cwd()).option("-r, --recursive", "Scan subdirectories recursively", false).option("-b, --base-url <url>", "Base URL where the photos are hosted").action(withConsolaUI(build));
|
|
707
|
+
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", process2.cwd()).option("-r, --recursive", "Clean subdirectories recursively", false).action(withConsolaUI(clean));
|
|
495
708
|
program.parse();
|
|
496
709
|
//# sourceMappingURL=index.js.map
|
|
497
710
|
//# sourceMappingURL=index.js.map
|