simple-photo-gallery 2.0.11-rc.1 → 2.0.11-rc.2
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.cjs +1257 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.js +2 -86
- package/dist/index.js.map +1 -1
- package/dist/lib/index.cjs +152 -0
- package/dist/lib/index.cjs.map +1 -0
- package/dist/lib/index.d.cts +92 -0
- package/dist/lib/index.d.ts +92 -0
- package/dist/lib/index.js +136 -0
- package/dist/lib/index.js.map +1 -0
- package/package.json +14 -2
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1257 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var process3 = require('process');
|
|
5
|
+
var commander = require('commander');
|
|
6
|
+
var consola = require('consola');
|
|
7
|
+
var child_process = require('child_process');
|
|
8
|
+
var fs8 = require('fs');
|
|
9
|
+
var path7 = require('path');
|
|
10
|
+
var buffer = require('buffer');
|
|
11
|
+
var sharp2 = require('sharp');
|
|
12
|
+
var blurhash = require('blurhash');
|
|
13
|
+
var ExifReader = require('exifreader');
|
|
14
|
+
var gallery = require('@simple-photo-gallery/common/src/gallery');
|
|
15
|
+
var ffprobe = require('node-ffprobe');
|
|
16
|
+
var os = require('os');
|
|
17
|
+
var Conf = require('conf');
|
|
18
|
+
var axios = require('axios');
|
|
19
|
+
var semverParser = require('semver-parser');
|
|
20
|
+
|
|
21
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
22
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
23
|
+
|
|
24
|
+
var process3__default = /*#__PURE__*/_interopDefault(process3);
|
|
25
|
+
var fs8__default = /*#__PURE__*/_interopDefault(fs8);
|
|
26
|
+
var path7__default = /*#__PURE__*/_interopDefault(path7);
|
|
27
|
+
var sharp2__default = /*#__PURE__*/_interopDefault(sharp2);
|
|
28
|
+
var ExifReader__default = /*#__PURE__*/_interopDefault(ExifReader);
|
|
29
|
+
var ffprobe__default = /*#__PURE__*/_interopDefault(ffprobe);
|
|
30
|
+
var os__default = /*#__PURE__*/_interopDefault(os);
|
|
31
|
+
var Conf__default = /*#__PURE__*/_interopDefault(Conf);
|
|
32
|
+
var axios__default = /*#__PURE__*/_interopDefault(axios);
|
|
33
|
+
|
|
34
|
+
// src/config/index.ts
|
|
35
|
+
var DEFAULT_THUMBNAIL_SIZE = 300;
|
|
36
|
+
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".gif", ".webp", ".tiff", ".tif", ".svg", ".avif"]);
|
|
37
|
+
var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([".mp4", ".avi", ".mov", ".wmv", ".flv", ".webm", ".mkv", ".m4v", ".3gp"]);
|
|
38
|
+
var HEADER_IMAGE_LANDSCAPE_WIDTHS = [3840, 2560, 1920, 1280, 960, 640];
|
|
39
|
+
var HEADER_IMAGE_PORTRAIT_WIDTHS = [1080, 720, 480, 360];
|
|
40
|
+
async function loadImage(imagePath) {
|
|
41
|
+
return sharp2__default.default(imagePath).rotate();
|
|
42
|
+
}
|
|
43
|
+
async function loadImageWithMetadata(imagePath) {
|
|
44
|
+
const image = sharp2__default.default(imagePath);
|
|
45
|
+
const metadata = await image.metadata();
|
|
46
|
+
image.rotate();
|
|
47
|
+
const needsDimensionSwap = metadata.orientation && metadata.orientation >= 5 && metadata.orientation <= 8;
|
|
48
|
+
if (needsDimensionSwap) {
|
|
49
|
+
const originalWidth = metadata.width;
|
|
50
|
+
metadata.width = metadata.height;
|
|
51
|
+
metadata.height = originalWidth;
|
|
52
|
+
}
|
|
53
|
+
return { image, metadata };
|
|
54
|
+
}
|
|
55
|
+
async function resizeImage(image, outputPath, width, height, format = "avif") {
|
|
56
|
+
await image.resize(width, height, { withoutEnlargement: true }).toFormat(format).toFile(outputPath);
|
|
57
|
+
}
|
|
58
|
+
async function cropAndResizeImage(image, outputPath, width, height, format = "avif") {
|
|
59
|
+
await image.resize(width, height, {
|
|
60
|
+
fit: "cover",
|
|
61
|
+
withoutEnlargement: true
|
|
62
|
+
}).toFormat(format).toFile(outputPath);
|
|
63
|
+
}
|
|
64
|
+
async function getImageDescription(imagePath) {
|
|
65
|
+
try {
|
|
66
|
+
const tags = await ExifReader__default.default.load(imagePath);
|
|
67
|
+
if (tags.description?.description) return tags.description.description;
|
|
68
|
+
if (tags.ImageDescription?.description) return tags.ImageDescription.description;
|
|
69
|
+
if (tags.UserComment && typeof tags.UserComment === "object" && tags.UserComment !== null && "description" in tags.UserComment) {
|
|
70
|
+
return tags.UserComment.description;
|
|
71
|
+
}
|
|
72
|
+
if (tags.ExtDescrAccessibility?.description) return tags.ExtDescrAccessibility.description;
|
|
73
|
+
if (tags["Caption/Abstract"]?.description) return tags["Caption/Abstract"].description;
|
|
74
|
+
if (tags.XPTitle?.description) return tags.XPTitle.description;
|
|
75
|
+
if (tags.XPComment?.description) return tags.XPComment.description;
|
|
76
|
+
} catch {
|
|
77
|
+
return void 0;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function createImageThumbnails(image, metadata, outputPath, outputPathRetina, size) {
|
|
81
|
+
const originalWidth = metadata.width || 0;
|
|
82
|
+
const originalHeight = metadata.height || 0;
|
|
83
|
+
if (originalWidth === 0 || originalHeight === 0) {
|
|
84
|
+
throw new Error("Invalid image dimensions");
|
|
85
|
+
}
|
|
86
|
+
const aspectRatio = originalWidth / originalHeight;
|
|
87
|
+
let width;
|
|
88
|
+
let height;
|
|
89
|
+
if (originalWidth > originalHeight) {
|
|
90
|
+
width = size;
|
|
91
|
+
height = Math.round(size / aspectRatio);
|
|
92
|
+
} else {
|
|
93
|
+
width = Math.round(size * aspectRatio);
|
|
94
|
+
height = size;
|
|
95
|
+
}
|
|
96
|
+
await resizeImage(image, outputPath, width, height);
|
|
97
|
+
await resizeImage(image, outputPathRetina, width * 2, height * 2);
|
|
98
|
+
return { width, height };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/utils/blurhash.ts
|
|
102
|
+
async function generateBlurHash(imagePath, componentX = 4, componentY = 3) {
|
|
103
|
+
const image = await loadImage(imagePath);
|
|
104
|
+
const { data, info } = await image.resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
105
|
+
const pixels = new Uint8ClampedArray(data.buffer);
|
|
106
|
+
return blurhash.encode(pixels, info.width, info.height, componentX, componentY);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/modules/build/utils/index.ts
|
|
110
|
+
path7__default.default.dirname(new URL((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))).pathname);
|
|
111
|
+
async function createGallerySocialMediaCardImage(headerPhotoPath, title, ouputPath, ui) {
|
|
112
|
+
ui.start(`Creating social media card image`);
|
|
113
|
+
const headerBasename = path7__default.default.basename(headerPhotoPath, path7__default.default.extname(headerPhotoPath));
|
|
114
|
+
if (fs8__default.default.existsSync(ouputPath)) {
|
|
115
|
+
ui.success(`Social media card image already exists`);
|
|
116
|
+
return headerBasename;
|
|
117
|
+
}
|
|
118
|
+
const image = await loadImage(headerPhotoPath);
|
|
119
|
+
const resizedImageBuffer = await image.resize(1200, 631, { fit: "cover" }).jpeg({ quality: 90 }).toBuffer();
|
|
120
|
+
const outputPath = ouputPath;
|
|
121
|
+
await sharp2__default.default(resizedImageBuffer).toFile(outputPath);
|
|
122
|
+
const svgText = `
|
|
123
|
+
<svg width="1200" height="631" xmlns="http://www.w3.org/2000/svg">
|
|
124
|
+
<defs>
|
|
125
|
+
<style>
|
|
126
|
+
.title { font-family: Arial, sans-serif; font-size: 96px; font-weight: bold; fill: white; stroke: black; stroke-width: 5; paint-order: stroke; text-anchor: middle; }
|
|
127
|
+
</style>
|
|
128
|
+
</defs>
|
|
129
|
+
<text x="600" y="250" class="title">${title}</text>
|
|
130
|
+
</svg>
|
|
131
|
+
`;
|
|
132
|
+
const finalImageBuffer = await sharp2__default.default(resizedImageBuffer).composite([{ input: buffer.Buffer.from(svgText), top: 0, left: 0 }]).jpeg({ quality: 90 }).toBuffer();
|
|
133
|
+
await sharp2__default.default(finalImageBuffer).toFile(outputPath);
|
|
134
|
+
ui.success(`Created social media card image successfully`);
|
|
135
|
+
return headerBasename;
|
|
136
|
+
}
|
|
137
|
+
async function createOptimizedHeaderImage(headerPhotoPath, outputFolder, ui) {
|
|
138
|
+
ui.start(`Creating optimized header images`);
|
|
139
|
+
const image = await loadImage(headerPhotoPath);
|
|
140
|
+
const headerBasename = path7__default.default.basename(headerPhotoPath, path7__default.default.extname(headerPhotoPath));
|
|
141
|
+
const generatedFiles = [];
|
|
142
|
+
ui.debug("Generating blurhash for header image");
|
|
143
|
+
const blurHash = await generateBlurHash(headerPhotoPath);
|
|
144
|
+
const landscapeYFactor = 3 / 4;
|
|
145
|
+
for (const width of HEADER_IMAGE_LANDSCAPE_WIDTHS) {
|
|
146
|
+
ui.debug(`Creating landscape header image ${width}`);
|
|
147
|
+
const avifFilename = `${headerBasename}_landscape_${width}.avif`;
|
|
148
|
+
const jpgFilename = `${headerBasename}_landscape_${width}.jpg`;
|
|
149
|
+
if (fs8__default.default.existsSync(path7__default.default.join(outputFolder, avifFilename))) {
|
|
150
|
+
ui.debug(`Landscape header image ${width} AVIF already exists`);
|
|
151
|
+
} else {
|
|
152
|
+
await cropAndResizeImage(
|
|
153
|
+
image.clone(),
|
|
154
|
+
path7__default.default.join(outputFolder, avifFilename),
|
|
155
|
+
width,
|
|
156
|
+
width * landscapeYFactor,
|
|
157
|
+
"avif"
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
generatedFiles.push(avifFilename);
|
|
161
|
+
if (fs8__default.default.existsSync(path7__default.default.join(outputFolder, jpgFilename))) {
|
|
162
|
+
ui.debug(`Landscape header image ${width} JPG already exists`);
|
|
163
|
+
} else {
|
|
164
|
+
await cropAndResizeImage(image.clone(), path7__default.default.join(outputFolder, jpgFilename), width, width * landscapeYFactor, "jpg");
|
|
165
|
+
}
|
|
166
|
+
generatedFiles.push(jpgFilename);
|
|
167
|
+
}
|
|
168
|
+
const portraitYFactor = 4 / 3;
|
|
169
|
+
for (const width of HEADER_IMAGE_PORTRAIT_WIDTHS) {
|
|
170
|
+
ui.debug(`Creating portrait header image ${width}`);
|
|
171
|
+
const avifFilename = `${headerBasename}_portrait_${width}.avif`;
|
|
172
|
+
const jpgFilename = `${headerBasename}_portrait_${width}.jpg`;
|
|
173
|
+
if (fs8__default.default.existsSync(path7__default.default.join(outputFolder, avifFilename))) {
|
|
174
|
+
ui.debug(`Portrait header image ${width} AVIF already exists`);
|
|
175
|
+
} else {
|
|
176
|
+
await cropAndResizeImage(image.clone(), path7__default.default.join(outputFolder, avifFilename), width, width * portraitYFactor, "avif");
|
|
177
|
+
}
|
|
178
|
+
generatedFiles.push(avifFilename);
|
|
179
|
+
if (fs8__default.default.existsSync(path7__default.default.join(outputFolder, jpgFilename))) {
|
|
180
|
+
ui.debug(`Portrait header image ${width} JPG already exists`);
|
|
181
|
+
} else {
|
|
182
|
+
await cropAndResizeImage(image.clone(), path7__default.default.join(outputFolder, jpgFilename), width, width * portraitYFactor, "jpg");
|
|
183
|
+
}
|
|
184
|
+
generatedFiles.push(jpgFilename);
|
|
185
|
+
}
|
|
186
|
+
ui.success(`Created optimized header image successfully`);
|
|
187
|
+
return { headerBasename, generatedFiles, blurHash };
|
|
188
|
+
}
|
|
189
|
+
function hasOldHeaderImages(outputFolder, currentHeaderBasename) {
|
|
190
|
+
if (!fs8__default.default.existsSync(outputFolder)) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
const files = fs8__default.default.readdirSync(outputFolder);
|
|
194
|
+
for (const file of files) {
|
|
195
|
+
const landscapeMatch = file.match(/^(.+)_landscape_\d+\.(avif|jpg)$/);
|
|
196
|
+
const portraitMatch = file.match(/^(.+)_portrait_\d+\.(avif|jpg)$/);
|
|
197
|
+
if (landscapeMatch && landscapeMatch[1] !== currentHeaderBasename || portraitMatch && portraitMatch[1] !== currentHeaderBasename) {
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
function cleanupOldHeaderImages(outputFolder, currentHeaderBasename, ui) {
|
|
204
|
+
ui.start(`Cleaning up old header images`);
|
|
205
|
+
if (!fs8__default.default.existsSync(outputFolder)) {
|
|
206
|
+
ui.debug(`Output folder ${outputFolder} does not exist, skipping cleanup`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const files = fs8__default.default.readdirSync(outputFolder);
|
|
210
|
+
let deletedCount = 0;
|
|
211
|
+
for (const file of files) {
|
|
212
|
+
const landscapeMatch = file.match(/^(.+)_landscape_\d+\.(avif|jpg)$/);
|
|
213
|
+
const portraitMatch = file.match(/^(.+)_portrait_\d+\.(avif|jpg)$/);
|
|
214
|
+
if (landscapeMatch && landscapeMatch[1] !== currentHeaderBasename) {
|
|
215
|
+
const filePath = path7__default.default.join(outputFolder, file);
|
|
216
|
+
ui.debug(`Deleting old landscape header image: ${file}`);
|
|
217
|
+
fs8__default.default.unlinkSync(filePath);
|
|
218
|
+
deletedCount++;
|
|
219
|
+
} else if (portraitMatch && portraitMatch[1] !== currentHeaderBasename) {
|
|
220
|
+
const filePath = path7__default.default.join(outputFolder, file);
|
|
221
|
+
ui.debug(`Deleting old portrait header image: ${file}`);
|
|
222
|
+
fs8__default.default.unlinkSync(filePath);
|
|
223
|
+
deletedCount++;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (deletedCount > 0) {
|
|
227
|
+
ui.success(`Deleted ${deletedCount} old header image(s)`);
|
|
228
|
+
} else {
|
|
229
|
+
ui.debug(`No old header images to clean up`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function findGalleries(basePath, recursive) {
|
|
233
|
+
const galleryDirs = [];
|
|
234
|
+
const galleryJsonPath = path7__default.default.join(basePath, "gallery", "gallery.json");
|
|
235
|
+
if (fs8__default.default.existsSync(galleryJsonPath)) {
|
|
236
|
+
galleryDirs.push(basePath);
|
|
237
|
+
}
|
|
238
|
+
if (recursive) {
|
|
239
|
+
try {
|
|
240
|
+
const entries = fs8__default.default.readdirSync(basePath, { withFileTypes: true });
|
|
241
|
+
for (const entry of entries) {
|
|
242
|
+
if (entry.isDirectory() && entry.name !== "gallery") {
|
|
243
|
+
const subPath = path7__default.default.join(basePath, entry.name);
|
|
244
|
+
const subResults = findGalleries(subPath, recursive);
|
|
245
|
+
galleryDirs.push(...subResults);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return galleryDirs;
|
|
252
|
+
}
|
|
253
|
+
function handleFileProcessingError(error, filename, ui) {
|
|
254
|
+
if (error instanceof Error && (error.message.includes("ffprobe") || error.message.includes("ffmpeg"))) {
|
|
255
|
+
ui.warn(
|
|
256
|
+
`Error processing ${filename}: ffprobe (part of ffmpeg) is required to process videos. Please install ffmpeg and ensure it is available in your PATH`
|
|
257
|
+
);
|
|
258
|
+
} else if (error instanceof Error && error.message.includes("unsupported image format")) {
|
|
259
|
+
ui.warn(`Error processing ${filename}: unsupported image format`);
|
|
260
|
+
} else {
|
|
261
|
+
ui.warn(`Error processing ${filename}`);
|
|
262
|
+
}
|
|
263
|
+
ui.debug(error);
|
|
264
|
+
}
|
|
265
|
+
function parseTelemetryOption(value) {
|
|
266
|
+
if (value !== "0" && value !== "1") {
|
|
267
|
+
throw new Error("Telemetry option must be either 0 or 1.");
|
|
268
|
+
}
|
|
269
|
+
return value;
|
|
270
|
+
}
|
|
271
|
+
function parseGalleryJson(galleryJsonPath, ui) {
|
|
272
|
+
let galleryContent;
|
|
273
|
+
try {
|
|
274
|
+
galleryContent = fs8__default.default.readFileSync(galleryJsonPath, "utf8");
|
|
275
|
+
} catch (error) {
|
|
276
|
+
ui.error("Error parsing gallery.json: file not found");
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
let galleryJSON;
|
|
280
|
+
try {
|
|
281
|
+
galleryJSON = JSON.parse(galleryContent);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
ui.error("Error parsing gallery.json: invalid JSON");
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
return gallery.GalleryDataSchema.parse(galleryJSON);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
try {
|
|
290
|
+
const deprecatedGalleryData = gallery.GalleryDataDeprecatedSchema.parse(galleryJSON);
|
|
291
|
+
return migrateGalleryJson(deprecatedGalleryData, galleryJsonPath, ui);
|
|
292
|
+
} catch {
|
|
293
|
+
ui.error("Error parsing gallery.json: invalid gallery data");
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function migrateGalleryJson(deprecatedGalleryData, galleryJsonPath, ui) {
|
|
299
|
+
ui.start("Old gallery.json format detected. Migrating gallery.json to the new data format.");
|
|
300
|
+
let mediaBasePath;
|
|
301
|
+
const imagePath = deprecatedGalleryData.sections[0].images[0].path;
|
|
302
|
+
if (imagePath && imagePath !== path7__default.default.join("..", path7__default.default.basename(imagePath))) {
|
|
303
|
+
mediaBasePath = path7__default.default.resolve(path7__default.default.join(path7__default.default.dirname(galleryJsonPath)), path7__default.default.dirname(imagePath));
|
|
304
|
+
}
|
|
305
|
+
const sections = deprecatedGalleryData.sections.map((section) => ({
|
|
306
|
+
...section,
|
|
307
|
+
images: section.images.map((image) => ({
|
|
308
|
+
...image,
|
|
309
|
+
path: void 0,
|
|
310
|
+
filename: path7__default.default.basename(image.path)
|
|
311
|
+
}))
|
|
312
|
+
}));
|
|
313
|
+
const galleryData = {
|
|
314
|
+
...deprecatedGalleryData,
|
|
315
|
+
headerImage: path7__default.default.basename(deprecatedGalleryData.headerImage),
|
|
316
|
+
sections,
|
|
317
|
+
mediaBasePath
|
|
318
|
+
};
|
|
319
|
+
ui.debug("Backing up old gallery.json file");
|
|
320
|
+
fs8__default.default.copyFileSync(galleryJsonPath, `${galleryJsonPath}.old`);
|
|
321
|
+
ui.debug("Writing gallery data to gallery.json file");
|
|
322
|
+
fs8__default.default.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
|
|
323
|
+
ui.success("Gallery data migrated to the new data format successfully.");
|
|
324
|
+
return galleryData;
|
|
325
|
+
}
|
|
326
|
+
function getMediaFileType(fileName) {
|
|
327
|
+
const ext = path7__default.default.extname(fileName).toLowerCase();
|
|
328
|
+
if (IMAGE_EXTENSIONS.has(ext)) return "image";
|
|
329
|
+
if (VIDEO_EXTENSIONS.has(ext)) return "video";
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
function capitalizeTitle(folderName) {
|
|
333
|
+
return folderName.replace("-", " ").replace("_", " ").split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// src/modules/init/index.ts
|
|
337
|
+
async function scanDirectory(dirPath, ui) {
|
|
338
|
+
const mediaFiles = [];
|
|
339
|
+
const subGalleryDirectories = [];
|
|
340
|
+
try {
|
|
341
|
+
const entries = await fs8.promises.readdir(dirPath, { withFileTypes: true });
|
|
342
|
+
for (const entry of entries) {
|
|
343
|
+
if (entry.isFile()) {
|
|
344
|
+
const mediaType = getMediaFileType(entry.name);
|
|
345
|
+
if (mediaType) {
|
|
346
|
+
const mediaFile = {
|
|
347
|
+
type: mediaType,
|
|
348
|
+
filename: entry.name,
|
|
349
|
+
width: 0,
|
|
350
|
+
height: 0
|
|
351
|
+
};
|
|
352
|
+
mediaFiles.push(mediaFile);
|
|
353
|
+
}
|
|
354
|
+
} else if (entry.isDirectory() && entry.name !== "gallery") {
|
|
355
|
+
subGalleryDirectories.push(path7__default.default.join(dirPath, entry.name));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
} catch (error) {
|
|
359
|
+
if (error instanceof Error && error.message.includes("ENOENT")) {
|
|
360
|
+
ui.error(`Directory does not exist: ${dirPath}`);
|
|
361
|
+
} else if (error instanceof Error && error.message.includes("ENOTDIR")) {
|
|
362
|
+
ui.error(`Path is not a directory: ${dirPath}`);
|
|
363
|
+
} else {
|
|
364
|
+
ui.error(`Error scanning directory ${dirPath}:`, error);
|
|
365
|
+
}
|
|
366
|
+
throw error;
|
|
367
|
+
}
|
|
368
|
+
return { mediaFiles, subGalleryDirectories };
|
|
369
|
+
}
|
|
370
|
+
async function getGallerySettingsFromUser(galleryName, defaultImage, ui) {
|
|
371
|
+
ui.info(`Enter gallery settings for the gallery in folder "${galleryName}"`);
|
|
372
|
+
const title = await ui.prompt("Enter gallery title", { type: "text", default: "My Gallery", placeholder: "My Gallery" });
|
|
373
|
+
const description = await ui.prompt("Enter gallery description", {
|
|
374
|
+
type: "text",
|
|
375
|
+
default: "My gallery with fantastic photos.",
|
|
376
|
+
placeholder: "My gallery with fantastic photos."
|
|
377
|
+
});
|
|
378
|
+
const url = await ui.prompt("Enter the URL where the gallery will be hosted (important for social media image)", {
|
|
379
|
+
type: "text",
|
|
380
|
+
default: "",
|
|
381
|
+
placeholder: ""
|
|
382
|
+
});
|
|
383
|
+
const headerImage = await ui.prompt("Enter the name of the header image", {
|
|
384
|
+
type: "text",
|
|
385
|
+
default: defaultImage,
|
|
386
|
+
placeholder: defaultImage
|
|
387
|
+
});
|
|
388
|
+
return { title, description, url, headerImage };
|
|
389
|
+
}
|
|
390
|
+
async function createGalleryJson(mediaFiles, galleryJsonPath, scanPath, subGalleries = [], useDefaultSettings, ui) {
|
|
391
|
+
const galleryDir = path7__default.default.dirname(galleryJsonPath);
|
|
392
|
+
const isSameLocation = path7__default.default.relative(scanPath, path7__default.default.join(galleryDir, "..")) === "";
|
|
393
|
+
const mediaBasePath = isSameLocation ? void 0 : scanPath;
|
|
394
|
+
const relativeSubGalleries = subGalleries.map((subGallery) => ({
|
|
395
|
+
...subGallery,
|
|
396
|
+
headerImage: subGallery.headerImage ? path7__default.default.relative(galleryDir, subGallery.headerImage) : ""
|
|
397
|
+
}));
|
|
398
|
+
let galleryData = {
|
|
399
|
+
title: "My Gallery",
|
|
400
|
+
description: "My gallery with fantastic photos.",
|
|
401
|
+
headerImage: mediaFiles[0]?.filename || "",
|
|
402
|
+
mediaBasePath,
|
|
403
|
+
metadata: {},
|
|
404
|
+
sections: [
|
|
405
|
+
{
|
|
406
|
+
images: mediaFiles
|
|
407
|
+
}
|
|
408
|
+
],
|
|
409
|
+
subGalleries: {
|
|
410
|
+
title: "Sub Galleries",
|
|
411
|
+
galleries: relativeSubGalleries
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
if (!useDefaultSettings) {
|
|
415
|
+
galleryData = {
|
|
416
|
+
...galleryData,
|
|
417
|
+
...await getGallerySettingsFromUser(
|
|
418
|
+
path7__default.default.basename(path7__default.default.join(galleryDir, "..")),
|
|
419
|
+
path7__default.default.basename(mediaFiles[0]?.filename || ""),
|
|
420
|
+
ui
|
|
421
|
+
)
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
await fs8.promises.writeFile(galleryJsonPath, JSON.stringify(galleryData, null, 2));
|
|
425
|
+
}
|
|
426
|
+
async function galleryExists(outputPath) {
|
|
427
|
+
const galleryPath = path7__default.default.join(outputPath, "gallery");
|
|
428
|
+
const galleryJsonPath = path7__default.default.join(galleryPath, "gallery.json");
|
|
429
|
+
try {
|
|
430
|
+
await fs8.promises.access(galleryJsonPath);
|
|
431
|
+
return true;
|
|
432
|
+
} catch {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async function processDirectory(scanPath, outputPath, recursive, useDefaultSettings, force, ui) {
|
|
437
|
+
ui.start(`Scanning ${scanPath}`);
|
|
438
|
+
let totalFiles = 0;
|
|
439
|
+
let totalGalleries = 1;
|
|
440
|
+
const subGalleries = [];
|
|
441
|
+
const { mediaFiles, subGalleryDirectories } = await scanDirectory(scanPath, ui);
|
|
442
|
+
totalFiles += mediaFiles.length;
|
|
443
|
+
if (recursive) {
|
|
444
|
+
for (const subGalleryDir of subGalleryDirectories) {
|
|
445
|
+
const result2 = await processDirectory(
|
|
446
|
+
subGalleryDir,
|
|
447
|
+
path7__default.default.join(outputPath, path7__default.default.basename(subGalleryDir)),
|
|
448
|
+
recursive,
|
|
449
|
+
useDefaultSettings,
|
|
450
|
+
force,
|
|
451
|
+
ui
|
|
452
|
+
);
|
|
453
|
+
totalFiles += result2.totalFiles;
|
|
454
|
+
totalGalleries += result2.totalGalleries;
|
|
455
|
+
if (result2.subGallery) {
|
|
456
|
+
subGalleries.push(result2.subGallery);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (mediaFiles.length > 0 || subGalleries.length > 0) {
|
|
461
|
+
const galleryPath = path7__default.default.join(outputPath, "gallery");
|
|
462
|
+
const galleryJsonPath = path7__default.default.join(galleryPath, "gallery.json");
|
|
463
|
+
const exists = await galleryExists(outputPath);
|
|
464
|
+
if (exists && !force) {
|
|
465
|
+
const shouldOverride = await ui.prompt(`Gallery already exists at ${galleryJsonPath}. Do you want to override it?`, {
|
|
466
|
+
type: "confirm",
|
|
467
|
+
default: false
|
|
468
|
+
});
|
|
469
|
+
if (!shouldOverride) {
|
|
470
|
+
ui.info("Skipping gallery creation");
|
|
471
|
+
return { totalFiles: 0, totalGalleries: 0 };
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
try {
|
|
475
|
+
await fs8.promises.mkdir(galleryPath, { recursive: true });
|
|
476
|
+
await createGalleryJson(mediaFiles, galleryJsonPath, scanPath, subGalleries, useDefaultSettings, ui);
|
|
477
|
+
ui.success(
|
|
478
|
+
`Create gallery with ${mediaFiles.length} files and ${subGalleries.length} subgalleries at: ${galleryJsonPath}`
|
|
479
|
+
);
|
|
480
|
+
} catch (error) {
|
|
481
|
+
ui.error(`Error creating gallery.json at ${galleryJsonPath}`);
|
|
482
|
+
throw error;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const result = { totalFiles, totalGalleries };
|
|
486
|
+
if (mediaFiles.length > 0 || subGalleries.length > 0) {
|
|
487
|
+
const dirName = path7__default.default.basename(scanPath);
|
|
488
|
+
result.subGallery = {
|
|
489
|
+
title: capitalizeTitle(dirName),
|
|
490
|
+
headerImage: mediaFiles[0]?.filename || "",
|
|
491
|
+
path: path7__default.default.join("..", dirName)
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
return result;
|
|
495
|
+
}
|
|
496
|
+
async function init(options, ui) {
|
|
497
|
+
try {
|
|
498
|
+
const scanPath = path7__default.default.resolve(options.photos);
|
|
499
|
+
const outputPath = options.gallery ? path7__default.default.resolve(options.gallery) : scanPath;
|
|
500
|
+
const result = await processDirectory(scanPath, outputPath, options.recursive, options.default, options.force, ui);
|
|
501
|
+
ui.box(
|
|
502
|
+
`Created ${result.totalGalleries} ${result.totalGalleries === 1 ? "gallery" : "galleries"} with ${result.totalFiles} media ${result.totalFiles === 1 ? "file" : "files"}`
|
|
503
|
+
);
|
|
504
|
+
return {
|
|
505
|
+
processedMediaCount: result.totalFiles,
|
|
506
|
+
processedGalleryCount: result.totalGalleries
|
|
507
|
+
};
|
|
508
|
+
} catch (error) {
|
|
509
|
+
ui.error("Error initializing gallery");
|
|
510
|
+
throw error;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
async function getFileMtime(filePath) {
|
|
514
|
+
const stats = await fs8.promises.stat(filePath);
|
|
515
|
+
return stats.mtime;
|
|
516
|
+
}
|
|
517
|
+
async function getVideoDimensions(filePath) {
|
|
518
|
+
const data = await ffprobe__default.default(filePath);
|
|
519
|
+
const videoStream = data.streams.find((stream) => stream.codec_type === "video");
|
|
520
|
+
if (!videoStream) {
|
|
521
|
+
throw new Error("No video stream found");
|
|
522
|
+
}
|
|
523
|
+
const dimensions = {
|
|
524
|
+
width: videoStream.width || 0,
|
|
525
|
+
height: videoStream.height || 0
|
|
526
|
+
};
|
|
527
|
+
if (dimensions.width === 0 || dimensions.height === 0) {
|
|
528
|
+
throw new Error("Invalid video dimensions");
|
|
529
|
+
}
|
|
530
|
+
return dimensions;
|
|
531
|
+
}
|
|
532
|
+
async function createVideoThumbnails(inputPath, videoDimensions, outputPath, outputPathRetina, height, verbose = false) {
|
|
533
|
+
const aspectRatio = videoDimensions.width / videoDimensions.height;
|
|
534
|
+
const width = Math.round(height * aspectRatio);
|
|
535
|
+
const tempFramePath = `${outputPath}.temp.png`;
|
|
536
|
+
return new Promise((resolve, reject) => {
|
|
537
|
+
const ffmpeg = child_process.spawn("ffmpeg", [
|
|
538
|
+
"-i",
|
|
539
|
+
inputPath,
|
|
540
|
+
"-vframes",
|
|
541
|
+
"1",
|
|
542
|
+
"-y",
|
|
543
|
+
"-loglevel",
|
|
544
|
+
verbose ? "error" : "quiet",
|
|
545
|
+
tempFramePath
|
|
546
|
+
]);
|
|
547
|
+
ffmpeg.stderr.on("data", (data) => {
|
|
548
|
+
console.log(`ffmpeg: ${data.toString()}`);
|
|
549
|
+
});
|
|
550
|
+
ffmpeg.on("close", async (code) => {
|
|
551
|
+
if (code === 0) {
|
|
552
|
+
try {
|
|
553
|
+
const frameImage = sharp2__default.default(tempFramePath);
|
|
554
|
+
await resizeImage(frameImage, outputPath, width, height);
|
|
555
|
+
await resizeImage(frameImage, outputPathRetina, width * 2, height * 2);
|
|
556
|
+
try {
|
|
557
|
+
await fs8.promises.unlink(tempFramePath);
|
|
558
|
+
} catch {
|
|
559
|
+
}
|
|
560
|
+
resolve({ width, height });
|
|
561
|
+
} catch (sharpError) {
|
|
562
|
+
reject(new Error(`Failed to process extracted frame: ${sharpError}`));
|
|
563
|
+
}
|
|
564
|
+
} else {
|
|
565
|
+
reject(new Error(`ffmpeg exited with code ${code}`));
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
ffmpeg.on("error", (error) => {
|
|
569
|
+
reject(new Error(`Failed to start ffmpeg: ${error.message}`));
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/modules/thumbnails/index.ts
|
|
575
|
+
async function processImage(imagePath, thumbnailPath, thumbnailPathRetina, thumbnailSize, lastMediaTimestamp) {
|
|
576
|
+
const fileMtime = await getFileMtime(imagePath);
|
|
577
|
+
if (lastMediaTimestamp && fileMtime <= lastMediaTimestamp && fs8__default.default.existsSync(thumbnailPath)) {
|
|
578
|
+
return void 0;
|
|
579
|
+
}
|
|
580
|
+
const { image, metadata } = await loadImageWithMetadata(imagePath);
|
|
581
|
+
const imageDimensions = {
|
|
582
|
+
width: metadata.width || 0,
|
|
583
|
+
height: metadata.height || 0
|
|
584
|
+
};
|
|
585
|
+
if (imageDimensions.width === 0 || imageDimensions.height === 0) {
|
|
586
|
+
throw new Error("Invalid image dimensions");
|
|
587
|
+
}
|
|
588
|
+
const description = await getImageDescription(imagePath);
|
|
589
|
+
const thumbnailDimensions = await createImageThumbnails(
|
|
590
|
+
image,
|
|
591
|
+
metadata,
|
|
592
|
+
thumbnailPath,
|
|
593
|
+
thumbnailPathRetina,
|
|
594
|
+
thumbnailSize
|
|
595
|
+
);
|
|
596
|
+
const blurHash = await generateBlurHash(thumbnailPath);
|
|
597
|
+
return {
|
|
598
|
+
type: "image",
|
|
599
|
+
filename: path7__default.default.basename(imagePath),
|
|
600
|
+
alt: description,
|
|
601
|
+
width: imageDimensions.width,
|
|
602
|
+
height: imageDimensions.height,
|
|
603
|
+
thumbnail: {
|
|
604
|
+
path: thumbnailPath,
|
|
605
|
+
pathRetina: thumbnailPathRetina,
|
|
606
|
+
width: thumbnailDimensions.width,
|
|
607
|
+
height: thumbnailDimensions.height,
|
|
608
|
+
blurHash
|
|
609
|
+
},
|
|
610
|
+
lastMediaTimestamp: fileMtime.toISOString()
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
async function processVideo(videoPath, thumbnailPath, thumbnailPathRetina, thumbnailSize, verbose, lastMediaTimestamp) {
|
|
614
|
+
const fileMtime = await getFileMtime(videoPath);
|
|
615
|
+
if (lastMediaTimestamp && fileMtime <= lastMediaTimestamp && fs8__default.default.existsSync(thumbnailPath)) {
|
|
616
|
+
return void 0;
|
|
617
|
+
}
|
|
618
|
+
const videoDimensions = await getVideoDimensions(videoPath);
|
|
619
|
+
const thumbnailDimensions = await createVideoThumbnails(
|
|
620
|
+
videoPath,
|
|
621
|
+
videoDimensions,
|
|
622
|
+
thumbnailPath,
|
|
623
|
+
thumbnailPathRetina,
|
|
624
|
+
thumbnailSize,
|
|
625
|
+
verbose
|
|
626
|
+
);
|
|
627
|
+
const blurHash = await generateBlurHash(thumbnailPath);
|
|
628
|
+
return {
|
|
629
|
+
type: "video",
|
|
630
|
+
filename: path7__default.default.basename(videoPath),
|
|
631
|
+
alt: void 0,
|
|
632
|
+
width: videoDimensions.width,
|
|
633
|
+
height: videoDimensions.height,
|
|
634
|
+
thumbnail: {
|
|
635
|
+
path: thumbnailPath,
|
|
636
|
+
pathRetina: thumbnailPathRetina,
|
|
637
|
+
width: thumbnailDimensions.width,
|
|
638
|
+
height: thumbnailDimensions.height,
|
|
639
|
+
blurHash
|
|
640
|
+
},
|
|
641
|
+
lastMediaTimestamp: fileMtime.toISOString()
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
async function processMediaFile(mediaFile, mediaBasePath, galleryDir, thumbnailsPath, thumbnailSize, ui) {
|
|
645
|
+
try {
|
|
646
|
+
const filePath = path7__default.default.resolve(path7__default.default.join(mediaBasePath, mediaFile.filename));
|
|
647
|
+
const fileName = mediaFile.filename;
|
|
648
|
+
const fileNameWithoutExt = path7__default.default.parse(fileName).name;
|
|
649
|
+
const thumbnailFileName = `${fileNameWithoutExt}.avif`;
|
|
650
|
+
const thumbnailPath = path7__default.default.join(thumbnailsPath, thumbnailFileName);
|
|
651
|
+
const thumbnailPathRetina = thumbnailPath.replace(".avif", "@2x.avif");
|
|
652
|
+
const lastMediaTimestamp = mediaFile.lastMediaTimestamp ? new Date(mediaFile.lastMediaTimestamp) : void 0;
|
|
653
|
+
const verbose = ui.level === consola.LogLevels.debug;
|
|
654
|
+
ui.debug(` Processing ${mediaFile.type}: ${fileName}`);
|
|
655
|
+
const updatedMediaFile = await (mediaFile.type === "image" ? processImage(filePath, thumbnailPath, thumbnailPathRetina, thumbnailSize, lastMediaTimestamp) : processVideo(filePath, thumbnailPath, thumbnailPathRetina, thumbnailSize, verbose, lastMediaTimestamp));
|
|
656
|
+
if (!updatedMediaFile) {
|
|
657
|
+
ui.debug(` Skipping ${fileName} because it has already been processed`);
|
|
658
|
+
if (mediaFile.thumbnail && !mediaFile.thumbnail.blurHash && fs8__default.default.existsSync(thumbnailPath)) {
|
|
659
|
+
try {
|
|
660
|
+
const blurHash = await generateBlurHash(thumbnailPath);
|
|
661
|
+
return {
|
|
662
|
+
...mediaFile,
|
|
663
|
+
thumbnail: {
|
|
664
|
+
...mediaFile.thumbnail,
|
|
665
|
+
blurHash
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
} catch (error) {
|
|
669
|
+
ui.debug(` Failed to generate BlurHash for ${fileName}:`, error);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return mediaFile;
|
|
673
|
+
}
|
|
674
|
+
updatedMediaFile.filename = mediaFile.filename;
|
|
675
|
+
if (updatedMediaFile.thumbnail) {
|
|
676
|
+
updatedMediaFile.thumbnail.path = path7__default.default.basename(thumbnailPath);
|
|
677
|
+
updatedMediaFile.thumbnail.pathRetina = path7__default.default.basename(thumbnailPathRetina);
|
|
678
|
+
}
|
|
679
|
+
return updatedMediaFile;
|
|
680
|
+
} catch (error) {
|
|
681
|
+
handleFileProcessingError(error, mediaFile.filename, ui);
|
|
682
|
+
return { ...mediaFile, thumbnail: void 0 };
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
async function processGalleryThumbnails(galleryDir, ui) {
|
|
686
|
+
const galleryJsonPath = path7__default.default.join(galleryDir, "gallery", "gallery.json");
|
|
687
|
+
const thumbnailsPath = path7__default.default.join(galleryDir, "gallery", "images");
|
|
688
|
+
ui.start(`Creating thumbnails: ${galleryDir}`);
|
|
689
|
+
try {
|
|
690
|
+
fs8__default.default.mkdirSync(thumbnailsPath, { recursive: true });
|
|
691
|
+
const galleryData = parseGalleryJson(galleryJsonPath, ui);
|
|
692
|
+
const thumbnailSize = galleryData.thumbnailSize || DEFAULT_THUMBNAIL_SIZE;
|
|
693
|
+
const mediaBasePath = galleryData.mediaBasePath ?? path7__default.default.join(galleryDir);
|
|
694
|
+
let processedCount = 0;
|
|
695
|
+
for (const section of galleryData.sections) {
|
|
696
|
+
for (const [index, mediaFile] of section.images.entries()) {
|
|
697
|
+
section.images[index] = await processMediaFile(
|
|
698
|
+
mediaFile,
|
|
699
|
+
mediaBasePath,
|
|
700
|
+
galleryDir,
|
|
701
|
+
thumbnailsPath,
|
|
702
|
+
thumbnailSize,
|
|
703
|
+
ui
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
processedCount += section.images.length;
|
|
707
|
+
}
|
|
708
|
+
fs8__default.default.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
|
|
709
|
+
ui.success(`Created thumbnails for ${processedCount} media files`);
|
|
710
|
+
return processedCount;
|
|
711
|
+
} catch (error) {
|
|
712
|
+
ui.error(`Error creating thumbnails for ${galleryDir}`);
|
|
713
|
+
throw error;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
async function thumbnails(options, ui) {
|
|
717
|
+
try {
|
|
718
|
+
const galleryDirs = findGalleries(options.gallery, options.recursive);
|
|
719
|
+
if (galleryDirs.length === 0) {
|
|
720
|
+
ui.error("No galleries found.");
|
|
721
|
+
return { processedGalleryCount: 0, processedMediaCount: 0 };
|
|
722
|
+
}
|
|
723
|
+
let totalGalleries = 0;
|
|
724
|
+
let totalProcessed = 0;
|
|
725
|
+
for (const galleryDir of galleryDirs) {
|
|
726
|
+
const processed = await processGalleryThumbnails(galleryDir, ui);
|
|
727
|
+
if (processed > 0) {
|
|
728
|
+
++totalGalleries;
|
|
729
|
+
totalProcessed += processed;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
ui.box(
|
|
733
|
+
`Created thumbnails for ${totalGalleries} ${totalGalleries === 1 ? "gallery" : "galleries"} with ${totalProcessed} media ${totalProcessed === 1 ? "file" : "files"}`
|
|
734
|
+
);
|
|
735
|
+
return { processedGalleryCount: totalGalleries, processedMediaCount: totalProcessed };
|
|
736
|
+
} catch (error) {
|
|
737
|
+
ui.error("Error creating thumbnails");
|
|
738
|
+
throw error;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// src/modules/build/index.ts
|
|
743
|
+
function copyPhotos(galleryData, galleryDir, ui) {
|
|
744
|
+
for (const section of galleryData.sections) {
|
|
745
|
+
for (const image of section.images) {
|
|
746
|
+
if (galleryData.mediaBasePath) {
|
|
747
|
+
const sourcePath = path7__default.default.join(galleryData.mediaBasePath, image.filename);
|
|
748
|
+
const destPath = path7__default.default.join(galleryDir, image.filename);
|
|
749
|
+
ui.debug(`Copying photo to ${destPath}`);
|
|
750
|
+
fs8__default.default.copyFileSync(sourcePath, destPath);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
async function scanAndAppendNewFiles(galleryDir, galleryJsonPath, galleryData, ui) {
|
|
756
|
+
const scanPath = galleryData.mediaBasePath || galleryDir;
|
|
757
|
+
ui.debug(`Scanning ${scanPath} for new media files`);
|
|
758
|
+
let scanResult;
|
|
759
|
+
try {
|
|
760
|
+
scanResult = await scanDirectory(scanPath, ui);
|
|
761
|
+
} catch {
|
|
762
|
+
ui.debug(`Could not scan directory ${scanPath}`);
|
|
763
|
+
return galleryData;
|
|
764
|
+
}
|
|
765
|
+
const existingFilenames = new Set(
|
|
766
|
+
galleryData.sections.flatMap((section) => section.images.map((image) => image.filename))
|
|
767
|
+
);
|
|
768
|
+
const newMediaFiles = scanResult.mediaFiles.filter((file) => !existingFilenames.has(file.filename));
|
|
769
|
+
if (newMediaFiles.length > 0) {
|
|
770
|
+
ui.info(`Found ${newMediaFiles.length} new media ${newMediaFiles.length === 1 ? "file" : "files"}`);
|
|
771
|
+
if (galleryData.sections.length === 0) {
|
|
772
|
+
galleryData.sections.push({ images: [] });
|
|
773
|
+
}
|
|
774
|
+
const lastSectionIndex = galleryData.sections.length - 1;
|
|
775
|
+
const lastSection = galleryData.sections[lastSectionIndex];
|
|
776
|
+
lastSection.images.push(...newMediaFiles);
|
|
777
|
+
ui.debug("Updating gallery.json with new files");
|
|
778
|
+
fs8__default.default.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
|
|
779
|
+
ui.success(`Added ${newMediaFiles.length} new ${newMediaFiles.length === 1 ? "file" : "files"} to gallery.json`);
|
|
780
|
+
} else {
|
|
781
|
+
ui.debug("No new media files found");
|
|
782
|
+
}
|
|
783
|
+
return galleryData;
|
|
784
|
+
}
|
|
785
|
+
async function buildGallery(galleryDir, templateDir, scan, shouldCreateThumbnails, ui, baseUrl, thumbsBaseUrl) {
|
|
786
|
+
ui.start(`Building gallery ${galleryDir}`);
|
|
787
|
+
const galleryJsonPath = path7__default.default.join(galleryDir, "gallery", "gallery.json");
|
|
788
|
+
let galleryData = parseGalleryJson(galleryJsonPath, ui);
|
|
789
|
+
if (scan) {
|
|
790
|
+
galleryData = await scanAndAppendNewFiles(galleryDir, galleryJsonPath, galleryData, ui);
|
|
791
|
+
}
|
|
792
|
+
const socialMediaCardImagePath = path7__default.default.join(galleryDir, "gallery", "images", "social-media-card.jpg");
|
|
793
|
+
const mediaBasePath = galleryData.mediaBasePath;
|
|
794
|
+
const mediaBaseUrl = baseUrl || galleryData.mediaBaseUrl;
|
|
795
|
+
const headerImagePath = mediaBasePath ? path7__default.default.join(mediaBasePath, galleryData.headerImage) : path7__default.default.resolve(galleryDir, galleryData.headerImage);
|
|
796
|
+
const imagesFolder = path7__default.default.join(galleryDir, "gallery", "images");
|
|
797
|
+
const currentHeaderBasename = path7__default.default.basename(headerImagePath, path7__default.default.extname(headerImagePath));
|
|
798
|
+
if (shouldCreateThumbnails) {
|
|
799
|
+
if (!fs8__default.default.existsSync(imagesFolder)) {
|
|
800
|
+
fs8__default.default.mkdirSync(imagesFolder, { recursive: true });
|
|
801
|
+
}
|
|
802
|
+
const headerImageChanged = hasOldHeaderImages(imagesFolder, currentHeaderBasename);
|
|
803
|
+
if (headerImageChanged) {
|
|
804
|
+
ui.info("Header image changed, cleaning up old assets");
|
|
805
|
+
cleanupOldHeaderImages(imagesFolder, currentHeaderBasename, ui);
|
|
806
|
+
if (fs8__default.default.existsSync(socialMediaCardImagePath)) {
|
|
807
|
+
fs8__default.default.unlinkSync(socialMediaCardImagePath);
|
|
808
|
+
ui.debug("Deleted old social media card");
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
await createGallerySocialMediaCardImage(headerImagePath, galleryData.title, socialMediaCardImagePath, ui);
|
|
812
|
+
const { blurHash } = await createOptimizedHeaderImage(headerImagePath, imagesFolder, ui);
|
|
813
|
+
if (galleryData.headerImageBlurHash !== blurHash) {
|
|
814
|
+
ui.debug("Updating gallery.json with header image blurhash");
|
|
815
|
+
galleryData.headerImageBlurHash = blurHash;
|
|
816
|
+
fs8__default.default.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
if (!mediaBaseUrl && mediaBasePath) {
|
|
820
|
+
const shouldCopyPhotos = await ui.prompt("All photos need to be copied. Are you sure you want to continue?", {
|
|
821
|
+
type: "confirm"
|
|
822
|
+
});
|
|
823
|
+
if (shouldCopyPhotos) {
|
|
824
|
+
ui.debug("Copying photos");
|
|
825
|
+
copyPhotos(galleryData, galleryDir, ui);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
if (mediaBaseUrl && galleryData.mediaBaseUrl !== mediaBaseUrl) {
|
|
829
|
+
ui.debug("Updating gallery.json with baseUrl");
|
|
830
|
+
galleryData.mediaBaseUrl = mediaBaseUrl;
|
|
831
|
+
fs8__default.default.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
|
|
832
|
+
}
|
|
833
|
+
if (thumbsBaseUrl && galleryData.thumbsBaseUrl !== thumbsBaseUrl) {
|
|
834
|
+
ui.debug("Updating gallery.json with thumbsBaseUrl");
|
|
835
|
+
galleryData.thumbsBaseUrl = thumbsBaseUrl;
|
|
836
|
+
fs8__default.default.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
|
|
837
|
+
}
|
|
838
|
+
if (!galleryData.metadata.image) {
|
|
839
|
+
ui.debug("Updating gallery.json with social media card URL");
|
|
840
|
+
galleryData.metadata.image = thumbsBaseUrl ? `${thumbsBaseUrl}/${path7__default.default.basename(socialMediaCardImagePath)}` : `${galleryData.url || ""}/${path7__default.default.relative(galleryDir, socialMediaCardImagePath)}`;
|
|
841
|
+
fs8__default.default.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
|
|
842
|
+
}
|
|
843
|
+
if (shouldCreateThumbnails) {
|
|
844
|
+
await processGalleryThumbnails(galleryDir, ui);
|
|
845
|
+
}
|
|
846
|
+
ui.debug("Building gallery from template");
|
|
847
|
+
try {
|
|
848
|
+
process3__default.default.env.GALLERY_JSON_PATH = galleryJsonPath;
|
|
849
|
+
process3__default.default.env.GALLERY_OUTPUT_DIR = path7__default.default.join(galleryDir, "gallery");
|
|
850
|
+
child_process.execSync("npx astro build", { cwd: templateDir, stdio: ui.level === consola.LogLevels.debug ? "inherit" : "ignore" });
|
|
851
|
+
} catch (error) {
|
|
852
|
+
ui.error(`Build failed for ${galleryDir}`);
|
|
853
|
+
throw error;
|
|
854
|
+
}
|
|
855
|
+
const outputDir = path7__default.default.join(galleryDir, "gallery");
|
|
856
|
+
const buildDir = path7__default.default.join(outputDir, "_build");
|
|
857
|
+
ui.debug(`Copying build output to ${outputDir}`);
|
|
858
|
+
fs8__default.default.cpSync(buildDir, outputDir, { recursive: true });
|
|
859
|
+
ui.debug("Moving index.html to gallery directory");
|
|
860
|
+
fs8__default.default.copyFileSync(path7__default.default.join(outputDir, "index.html"), path7__default.default.join(galleryDir, "index.html"));
|
|
861
|
+
fs8__default.default.rmSync(path7__default.default.join(outputDir, "index.html"));
|
|
862
|
+
ui.debug("Cleaning up build directory");
|
|
863
|
+
fs8__default.default.rmSync(buildDir, { recursive: true, force: true });
|
|
864
|
+
ui.success(`Gallery built successfully`);
|
|
865
|
+
}
|
|
866
|
+
async function build(options, ui) {
|
|
867
|
+
try {
|
|
868
|
+
const galleryDirs = findGalleries(options.gallery, options.recursive);
|
|
869
|
+
if (galleryDirs.length === 0) {
|
|
870
|
+
ui.error("No galleries found.");
|
|
871
|
+
return { processedGalleryCount: 0 };
|
|
872
|
+
}
|
|
873
|
+
const themePath = await undefined("@simple-photo-gallery/theme-modern/package.json");
|
|
874
|
+
const themeDir = path7__default.default.dirname(new URL(themePath).pathname);
|
|
875
|
+
let totalGalleries = 0;
|
|
876
|
+
for (const dir of galleryDirs) {
|
|
877
|
+
const baseUrl = options.baseUrl ? `${options.baseUrl}${path7__default.default.relative(options.gallery, dir)}` : void 0;
|
|
878
|
+
const thumbsBaseUrl = options.thumbsBaseUrl ? `${options.thumbsBaseUrl}${path7__default.default.relative(options.gallery, dir)}` : void 0;
|
|
879
|
+
await buildGallery(path7__default.default.resolve(dir), themeDir, options.scan, options.thumbnails, ui, baseUrl, thumbsBaseUrl);
|
|
880
|
+
++totalGalleries;
|
|
881
|
+
}
|
|
882
|
+
ui.box(`Built ${totalGalleries} ${totalGalleries === 1 ? "gallery" : "galleries"} successfully`);
|
|
883
|
+
return { processedGalleryCount: totalGalleries };
|
|
884
|
+
} catch (error) {
|
|
885
|
+
if (error instanceof Error && error.message.includes("Cannot find package")) {
|
|
886
|
+
ui.error("Theme package not found: @simple-photo-gallery/theme-modern/package.json");
|
|
887
|
+
} else {
|
|
888
|
+
ui.error("Error building gallery");
|
|
889
|
+
}
|
|
890
|
+
throw error;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
async function cleanGallery(galleryDir, ui) {
|
|
894
|
+
let filesRemoved = 0;
|
|
895
|
+
const indexHtmlPath = path7__default.default.join(galleryDir, "index.html");
|
|
896
|
+
if (fs8__default.default.existsSync(indexHtmlPath)) {
|
|
897
|
+
try {
|
|
898
|
+
fs8__default.default.rmSync(indexHtmlPath);
|
|
899
|
+
ui.debug(`Removed: ${indexHtmlPath}`);
|
|
900
|
+
filesRemoved++;
|
|
901
|
+
} catch (error) {
|
|
902
|
+
ui.warn(`Failed to remove index.html: ${error}`);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
const galleryPath = path7__default.default.join(galleryDir, "gallery");
|
|
906
|
+
if (fs8__default.default.existsSync(galleryPath)) {
|
|
907
|
+
try {
|
|
908
|
+
fs8__default.default.rmSync(galleryPath, { recursive: true, force: true });
|
|
909
|
+
ui.debug(`Removed directory: ${galleryPath}`);
|
|
910
|
+
filesRemoved++;
|
|
911
|
+
} catch (error) {
|
|
912
|
+
ui.warn(`Failed to remove gallery directory: ${error}`);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
if (filesRemoved > 0) {
|
|
916
|
+
ui.success(`Cleaned gallery at: ${galleryDir}`);
|
|
917
|
+
} else {
|
|
918
|
+
ui.info(`No gallery files found at: ${galleryDir}`);
|
|
919
|
+
}
|
|
920
|
+
return { processedGalleryCount: filesRemoved };
|
|
921
|
+
}
|
|
922
|
+
async function clean(options, ui) {
|
|
923
|
+
try {
|
|
924
|
+
const basePath = path7__default.default.resolve(options.gallery);
|
|
925
|
+
if (!fs8__default.default.existsSync(basePath)) {
|
|
926
|
+
ui.error(`Directory does not exist: ${basePath}`);
|
|
927
|
+
return { processedGalleryCount: 0 };
|
|
928
|
+
}
|
|
929
|
+
const galleryDirs = findGalleries(basePath, options.recursive);
|
|
930
|
+
if (galleryDirs.length === 0) {
|
|
931
|
+
ui.info("No galleries found to clean.");
|
|
932
|
+
return { processedGalleryCount: 0 };
|
|
933
|
+
}
|
|
934
|
+
for (const dir of galleryDirs) {
|
|
935
|
+
await cleanGallery(dir, ui);
|
|
936
|
+
}
|
|
937
|
+
ui.box(`Successfully cleaned ${galleryDirs.length} ${galleryDirs.length === 1 ? "gallery" : "galleries"}`);
|
|
938
|
+
return { processedGalleryCount: galleryDirs.length };
|
|
939
|
+
} catch (error) {
|
|
940
|
+
ui.error("Error cleaning galleries");
|
|
941
|
+
throw error;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// src/modules/telemetry/index.ts
|
|
946
|
+
async function telemetry(options, ui, telemetryService2) {
|
|
947
|
+
const updates = {};
|
|
948
|
+
if (options.state === void 0) {
|
|
949
|
+
const current = telemetryService2.getStoredPreference();
|
|
950
|
+
if (current === void 0) {
|
|
951
|
+
ui.info("Telemetry preference not set yet. It will be requested on next command run.");
|
|
952
|
+
updates.telemetryStatus = "unset";
|
|
953
|
+
} else {
|
|
954
|
+
ui.info(`Telemetry is currently ${current ? "enabled" : "disabled"}.`);
|
|
955
|
+
updates.telemetryEnabled = current;
|
|
956
|
+
}
|
|
957
|
+
} else {
|
|
958
|
+
telemetryService2.setStoredPreference(options.state === "1");
|
|
959
|
+
ui.success(`Anonymous telemetry ${options.state === "1" ? "enabled" : "disabled"}.`);
|
|
960
|
+
updates.telemetryEnabled = options.state === "1";
|
|
961
|
+
}
|
|
962
|
+
return updates;
|
|
963
|
+
}
|
|
964
|
+
var ApiTelemetryClient = class {
|
|
965
|
+
constructor() {
|
|
966
|
+
this.endpoint = "https://tools.simple.photo/api/telemetry";
|
|
967
|
+
}
|
|
968
|
+
async record(event) {
|
|
969
|
+
try {
|
|
970
|
+
axios__default.default.post(this.endpoint, event, {
|
|
971
|
+
headers: {
|
|
972
|
+
"content-type": "application/json",
|
|
973
|
+
"user-agent": `simple-photo-gallery/${event.packageVersion} (${process3__default.default.platform}; ${process3__default.default.arch})`
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
} catch {
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
};
|
|
980
|
+
var ConsoleTelemetryClient = class {
|
|
981
|
+
async record(event) {
|
|
982
|
+
const serialized = JSON.stringify(event, null, 2);
|
|
983
|
+
process3.stdout.write(`TELEMETRY EVENT: ${serialized}
|
|
984
|
+
`);
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
// src/modules/telemetry/service/index.ts
|
|
989
|
+
var CONFIG_KEY = "telemetryEnabled";
|
|
990
|
+
var TelemetryService = class {
|
|
991
|
+
constructor(packageName, packageVersion, ui) {
|
|
992
|
+
this.packageName = packageName;
|
|
993
|
+
this.packageVersion = packageVersion;
|
|
994
|
+
this.ui = ui;
|
|
995
|
+
this.config = new Conf__default.default({ projectName: packageName });
|
|
996
|
+
}
|
|
997
|
+
/** Returns the stored telemetry preference, if any. */
|
|
998
|
+
getStoredPreference() {
|
|
999
|
+
return this.config.get(CONFIG_KEY);
|
|
1000
|
+
}
|
|
1001
|
+
/** Updates the persisted telemetry preference. */
|
|
1002
|
+
setStoredPreference(enabled) {
|
|
1003
|
+
this.config.set(CONFIG_KEY, enabled);
|
|
1004
|
+
}
|
|
1005
|
+
/** Determines whether telemetry should be collected for this run. */
|
|
1006
|
+
async shouldCollectTelemetry(override) {
|
|
1007
|
+
if (override) {
|
|
1008
|
+
return override === "1";
|
|
1009
|
+
}
|
|
1010
|
+
if (process3__default.default.env.CI || process3__default.default.env.DO_NOT_TRACK) {
|
|
1011
|
+
return false;
|
|
1012
|
+
}
|
|
1013
|
+
if (process3__default.default.env.SPG_TELEMETRY) {
|
|
1014
|
+
return process3__default.default.env.SPG_TELEMETRY === "1";
|
|
1015
|
+
}
|
|
1016
|
+
const stored = this.getStoredPreference();
|
|
1017
|
+
if (stored === void 0) {
|
|
1018
|
+
const consent = await this.promptForConsent();
|
|
1019
|
+
this.setStoredPreference(consent);
|
|
1020
|
+
return consent;
|
|
1021
|
+
} else {
|
|
1022
|
+
return stored;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
/** Builds and dispatches a telemetry event when telemetry is enabled. */
|
|
1026
|
+
async emit(params) {
|
|
1027
|
+
const event = this.buildEvent(params);
|
|
1028
|
+
try {
|
|
1029
|
+
const client = await this.getClient();
|
|
1030
|
+
if (client) {
|
|
1031
|
+
await client.record(event);
|
|
1032
|
+
}
|
|
1033
|
+
} catch (error) {
|
|
1034
|
+
this.ui.debug("Error recording telemetry event", error);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
/** Builds a telemetry event. */
|
|
1038
|
+
buildEvent({
|
|
1039
|
+
command,
|
|
1040
|
+
argumentsProvided,
|
|
1041
|
+
globalOptionsProvided,
|
|
1042
|
+
metrics,
|
|
1043
|
+
success,
|
|
1044
|
+
error,
|
|
1045
|
+
startedAt
|
|
1046
|
+
}) {
|
|
1047
|
+
const now = Date.now();
|
|
1048
|
+
const event = {
|
|
1049
|
+
command,
|
|
1050
|
+
argumentsProvided,
|
|
1051
|
+
globalOptionsProvided,
|
|
1052
|
+
timestamp: new Date(now).toISOString(),
|
|
1053
|
+
durationMs: now - startedAt,
|
|
1054
|
+
packageName: this.packageName,
|
|
1055
|
+
packageVersion: this.packageVersion,
|
|
1056
|
+
nodeVersion: process3__default.default.version,
|
|
1057
|
+
osPlatform: os__default.default.platform(),
|
|
1058
|
+
osRelease: os__default.default.release(),
|
|
1059
|
+
osArch: os__default.default.arch(),
|
|
1060
|
+
result: success ? "success" : "error"
|
|
1061
|
+
};
|
|
1062
|
+
if (metrics && Object.keys(metrics).length > 0) {
|
|
1063
|
+
event.metrics = metrics;
|
|
1064
|
+
}
|
|
1065
|
+
if (!success && error) {
|
|
1066
|
+
event.errorName = error.name;
|
|
1067
|
+
event.errorMessage = error.message;
|
|
1068
|
+
}
|
|
1069
|
+
return event;
|
|
1070
|
+
}
|
|
1071
|
+
/** Prompts the user for consent to collect telemetry. */
|
|
1072
|
+
async promptForConsent() {
|
|
1073
|
+
this.ui.info(
|
|
1074
|
+
"Simple Photo Gallery collects anonymous usage telemetry to improve the CLI. No personal data or information about your photos is collected. For more information, see https://simple.photo/telemetry."
|
|
1075
|
+
);
|
|
1076
|
+
const answer = await this.ui.prompt("Would you like to enable telemetry?", {
|
|
1077
|
+
type: "confirm",
|
|
1078
|
+
initial: true
|
|
1079
|
+
});
|
|
1080
|
+
if (!answer) {
|
|
1081
|
+
this.ui.info('Anonymous telemetry disabled. You can re-enable it with "spg telemetry 1".');
|
|
1082
|
+
return false;
|
|
1083
|
+
}
|
|
1084
|
+
this.ui.success("Thank you! Telemetry will help us improve Simple Photo Gallery.");
|
|
1085
|
+
return true;
|
|
1086
|
+
}
|
|
1087
|
+
/** Returns the telemetry client. */
|
|
1088
|
+
getClient() {
|
|
1089
|
+
if (!this.client) {
|
|
1090
|
+
switch (process3__default.default.env.SPG_TELEMETRY_PROVIDER) {
|
|
1091
|
+
case "none": {
|
|
1092
|
+
this.client = void 0;
|
|
1093
|
+
break;
|
|
1094
|
+
}
|
|
1095
|
+
case "console": {
|
|
1096
|
+
this.client = new ConsoleTelemetryClient();
|
|
1097
|
+
break;
|
|
1098
|
+
}
|
|
1099
|
+
case "api": {
|
|
1100
|
+
this.client = new ApiTelemetryClient();
|
|
1101
|
+
break;
|
|
1102
|
+
}
|
|
1103
|
+
default: {
|
|
1104
|
+
this.client = new ApiTelemetryClient();
|
|
1105
|
+
break;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
return this.client;
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
var NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
1113
|
+
var CHECK_TIMEOUT_MS = 3e3;
|
|
1114
|
+
async function fetchLatestStableVersion(packageName) {
|
|
1115
|
+
try {
|
|
1116
|
+
const response = await axios__default.default.get(`${NPM_REGISTRY_URL}/${packageName}`, {
|
|
1117
|
+
timeout: CHECK_TIMEOUT_MS,
|
|
1118
|
+
headers: {
|
|
1119
|
+
Accept: "application/vnd.npm.install-v1+json"
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
const versions = Object.keys(response.data.versions);
|
|
1123
|
+
const stableVersions = versions.filter((version) => {
|
|
1124
|
+
try {
|
|
1125
|
+
const parsed = semverParser.parseSemVer(version);
|
|
1126
|
+
return !parsed.pre || parsed.pre.length === 0;
|
|
1127
|
+
} catch {
|
|
1128
|
+
return false;
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
if (stableVersions.length === 0) {
|
|
1132
|
+
return void 0;
|
|
1133
|
+
}
|
|
1134
|
+
stableVersions.sort(semverParser.compareSemVer);
|
|
1135
|
+
return stableVersions[stableVersions.length - 1];
|
|
1136
|
+
} catch {
|
|
1137
|
+
return void 0;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
function checkForUpdates(packageName, currentVersion) {
|
|
1141
|
+
return fetchLatestStableVersion(packageName).then((latestStableVersion) => {
|
|
1142
|
+
if (!latestStableVersion) {
|
|
1143
|
+
return null;
|
|
1144
|
+
}
|
|
1145
|
+
try {
|
|
1146
|
+
if (semverParser.compareSemVer(latestStableVersion, currentVersion) > 0) {
|
|
1147
|
+
return { currentVersion, latestVersion: latestStableVersion };
|
|
1148
|
+
}
|
|
1149
|
+
} catch {
|
|
1150
|
+
}
|
|
1151
|
+
return null;
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
function displayUpdateNotification(updateInfo, ui) {
|
|
1155
|
+
const { currentVersion, latestVersion } = updateInfo;
|
|
1156
|
+
const message = [
|
|
1157
|
+
`New version available: ${currentVersion} \u2192 ${latestVersion}`,
|
|
1158
|
+
"",
|
|
1159
|
+
"Run the following commands to update:",
|
|
1160
|
+
"",
|
|
1161
|
+
" npm install -g simple-photo-gallery@latest"
|
|
1162
|
+
].join("\n");
|
|
1163
|
+
ui.box(message);
|
|
1164
|
+
}
|
|
1165
|
+
async function waitForUpdateCheck(checkPromise) {
|
|
1166
|
+
try {
|
|
1167
|
+
let timeoutId;
|
|
1168
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
1169
|
+
timeoutId = setTimeout(() => resolve(null), 5e3);
|
|
1170
|
+
});
|
|
1171
|
+
const result = await Promise.race([checkPromise, timeoutPromise]);
|
|
1172
|
+
clearTimeout(timeoutId);
|
|
1173
|
+
return result;
|
|
1174
|
+
} catch {
|
|
1175
|
+
return null;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// package.json
|
|
1180
|
+
var package_default = {
|
|
1181
|
+
name: "simple-photo-gallery",
|
|
1182
|
+
version: "2.0.11-rc.2"};
|
|
1183
|
+
|
|
1184
|
+
// src/index.ts
|
|
1185
|
+
var program = new commander.Command();
|
|
1186
|
+
var telemetryService = new TelemetryService(package_default.name, package_default.version, createConsolaUI(program.opts()));
|
|
1187
|
+
program.name("gallery").description("Simple Photo Gallery CLI").version(package_default.version).option("-v, --verbose", "Verbose output (debug level)", false).option("-q, --quiet", "Minimal output (only warnings/errors)", false).option("--telemetry <state>", "Enable (1) or disable (0) telemetry for this command", parseTelemetryOption).showHelpAfterError(true);
|
|
1188
|
+
function createConsolaUI(globalOpts) {
|
|
1189
|
+
let level = consola.LogLevels.info;
|
|
1190
|
+
if (globalOpts.quiet) {
|
|
1191
|
+
level = consola.LogLevels.warn;
|
|
1192
|
+
} else if (globalOpts.verbose) {
|
|
1193
|
+
level = consola.LogLevels.debug;
|
|
1194
|
+
}
|
|
1195
|
+
return consola.createConsola({
|
|
1196
|
+
level
|
|
1197
|
+
}).withTag("simple-photo-gallery");
|
|
1198
|
+
}
|
|
1199
|
+
function collectCommandArguments(command) {
|
|
1200
|
+
return command.options.map((option) => ({ name: option.attributeName(), source: command.getOptionValueSource(option.attributeName()) })).filter((option) => option.source && option.source !== "default").map((option) => option.name);
|
|
1201
|
+
}
|
|
1202
|
+
function collectGlobalArguments() {
|
|
1203
|
+
return program.options.map((option) => ({ name: option.attributeName(), source: program.getOptionValueSource(option.attributeName()) })).filter((option) => option.source && option.source !== "default").map((option) => option.name);
|
|
1204
|
+
}
|
|
1205
|
+
function withCommandContext(handler) {
|
|
1206
|
+
return async (rawOpts, command) => {
|
|
1207
|
+
const { telemetry: telemetryOverride, ...commandOptions } = rawOpts;
|
|
1208
|
+
const ui = createConsolaUI(program.opts());
|
|
1209
|
+
const shouldCollectTelemetry = await telemetryService.shouldCollectTelemetry(telemetryOverride);
|
|
1210
|
+
const startedAt = Date.now();
|
|
1211
|
+
const updateCheckPromise = checkForUpdates(package_default.name, package_default.version);
|
|
1212
|
+
let success = false;
|
|
1213
|
+
let metrics;
|
|
1214
|
+
let errorInfo;
|
|
1215
|
+
try {
|
|
1216
|
+
const result = await handler(commandOptions, ui, command);
|
|
1217
|
+
if (result && Object.keys(result).length > 0) {
|
|
1218
|
+
metrics = result;
|
|
1219
|
+
}
|
|
1220
|
+
success = true;
|
|
1221
|
+
} catch (error) {
|
|
1222
|
+
ui.debug(error);
|
|
1223
|
+
errorInfo = error instanceof Error ? { name: error.name, message: error.message } : { name: "UnknownError", message: String(error) };
|
|
1224
|
+
process3__default.default.exitCode = 1;
|
|
1225
|
+
}
|
|
1226
|
+
const updateInfo = await waitForUpdateCheck(updateCheckPromise);
|
|
1227
|
+
if (updateInfo) {
|
|
1228
|
+
displayUpdateNotification(updateInfo, ui);
|
|
1229
|
+
}
|
|
1230
|
+
if (shouldCollectTelemetry) {
|
|
1231
|
+
await telemetryService.emit({
|
|
1232
|
+
command: command.name(),
|
|
1233
|
+
argumentsProvided: collectCommandArguments(command),
|
|
1234
|
+
globalOptionsProvided: collectGlobalArguments(),
|
|
1235
|
+
metrics,
|
|
1236
|
+
success,
|
|
1237
|
+
error: errorInfo,
|
|
1238
|
+
startedAt
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
program.command("init").description("Initialize a gallery by scaning a folder for images and videos").option(
|
|
1244
|
+
"-p, --photos <path>",
|
|
1245
|
+
"Path to the folder where the photos are stored. Default: current working directory",
|
|
1246
|
+
process3__default.default.cwd()
|
|
1247
|
+
).option(
|
|
1248
|
+
"-g, --gallery <path>",
|
|
1249
|
+
"Path to the directory where the gallery will be initialized. Default: same directory as the photos folder"
|
|
1250
|
+
).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)));
|
|
1251
|
+
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__default.default.cwd()).option("-r, --recursive", "Scan subdirectories recursively", false).action(withCommandContext((options, ui) => thumbnails(options, ui)));
|
|
1252
|
+
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__default.default.cwd()).option("-r, --recursive", "Scan subdirectories recursively", false).option("-b, --base-url <url>", "Base URL where the photos are hosted").option("-t, --thumbs-base-url <url>", "Base URL where the thumbnails are hosted").option("--no-thumbnails", "Skip creating thumbnails when building the gallery", true).option("--no-scan", "Do not scan for new photos when building the gallery", true).action(withCommandContext((options, ui) => build(options, ui)));
|
|
1253
|
+
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__default.default.cwd()).option("-r, --recursive", "Clean subdirectories recursively", false).action(withCommandContext((options, ui) => clean(options, ui)));
|
|
1254
|
+
program.command("telemetry").description("Manage anonymous telemetry preferences. Use 1 to enable, 0 to disable, or no argument to check status").option("-s, --state <state>", "Enable (1) or disable (0) telemetry", parseTelemetryOption).action(withCommandContext((options, ui) => telemetry(options, ui, telemetryService)));
|
|
1255
|
+
program.parse();
|
|
1256
|
+
//# sourceMappingURL=index.cjs.map
|
|
1257
|
+
//# sourceMappingURL=index.cjs.map
|