simple-photo-gallery 0.0.4 → 0.0.5
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.d.ts +1 -0
- package/dist/index.js +497 -0
- package/dist/index.js.map +1 -0
- package/package.json +7 -7
- package/dist/src/index.js +0 -29
- package/dist/src/modules/build/index.js +0 -57
- package/dist/src/modules/build/types/index.js +0 -1
- package/dist/src/modules/build/utils/index.js +0 -7
- package/dist/src/modules/init/const/index.js +0 -4
- package/dist/src/modules/init/index.js +0 -156
- package/dist/src/modules/init/types/index.js +0 -1
- package/dist/src/modules/init/utils/index.js +0 -93
- package/dist/src/modules/thumbnails/index.js +0 -98
- package/dist/src/modules/thumbnails/types/index.js +0 -1
- package/dist/src/modules/thumbnails/utils/index.js +0 -127
- package/dist/src/types/index.js +0 -35
- package/dist/src/utils/index.js +0 -34
- package/dist/tests/gallery.test.js +0 -170
- package/src/index.ts +0 -50
- package/src/modules/build/index.ts +0 -68
- package/src/modules/build/types/index.ts +0 -4
- package/src/modules/build/utils/index.ts +0 -9
- package/src/modules/init/const/index.ts +0 -5
- package/src/modules/init/index.ts +0 -193
- package/src/modules/init/types/index.ts +0 -16
- package/src/modules/init/types/node-ffprobe.d.ts +0 -17
- package/src/modules/init/utils/index.ts +0 -98
- package/src/modules/thumbnails/index.ts +0 -121
- package/src/modules/thumbnails/types/index.ts +0 -5
- package/src/modules/thumbnails/utils/index.ts +0 -162
- package/src/types/index.ts +0 -46
- package/src/utils/index.ts +0 -37
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
import { createImageThumbnail, createVideoThumbnail } from './utils';
|
|
5
|
-
|
|
6
|
-
import { GalleryDataSchema, type MediaFile } from '../../types';
|
|
7
|
-
import { findGalleries } from '../../utils';
|
|
8
|
-
|
|
9
|
-
import type { ThumbnailOptions } from './types';
|
|
10
|
-
const processGallery = async (galleryDir: string, size: number): Promise<number> => {
|
|
11
|
-
const galleryJsonPath = path.join(galleryDir, 'gallery', 'gallery.json');
|
|
12
|
-
const thumbnailsPath = path.join(galleryDir, 'gallery', 'thumbnails');
|
|
13
|
-
|
|
14
|
-
console.log(`\nProcessing gallery in: ${galleryDir}`);
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
// Ensure thumbnails directory exists
|
|
18
|
-
fs.mkdirSync(thumbnailsPath, { recursive: true });
|
|
19
|
-
|
|
20
|
-
// Read gallery.json
|
|
21
|
-
const galleryContent = fs.readFileSync(galleryJsonPath, 'utf8');
|
|
22
|
-
const galleryData = GalleryDataSchema.parse(JSON.parse(galleryContent));
|
|
23
|
-
|
|
24
|
-
// Process all sections and their images
|
|
25
|
-
let processedCount = 0;
|
|
26
|
-
for (const section of galleryData.sections) {
|
|
27
|
-
for (const [index, mediaFile] of section.images.entries()) {
|
|
28
|
-
section.images[index] = await processMediaFile(mediaFile, galleryDir, thumbnailsPath, size);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
processedCount += section.images.length;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Write updated gallery.json
|
|
35
|
-
fs.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
|
|
36
|
-
|
|
37
|
-
return processedCount;
|
|
38
|
-
} catch (error) {
|
|
39
|
-
console.error(`Error creating thumbnails for ${galleryDir}:`, error);
|
|
40
|
-
return 0;
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
async function processMediaFile(
|
|
45
|
-
mediaFile: MediaFile,
|
|
46
|
-
galleryDir: string,
|
|
47
|
-
thumbnailsPath: string,
|
|
48
|
-
size: number,
|
|
49
|
-
): Promise<MediaFile> {
|
|
50
|
-
try {
|
|
51
|
-
// Resolve the path relative to the gallery.json file location, not the gallery directory
|
|
52
|
-
const galleryJsonDir = path.join(galleryDir, 'gallery');
|
|
53
|
-
const filePath = path.resolve(path.join(galleryJsonDir, mediaFile.path));
|
|
54
|
-
|
|
55
|
-
const fileName = path.basename(filePath);
|
|
56
|
-
const fileNameWithoutExt = path.parse(fileName).name;
|
|
57
|
-
const thumbnailFileName = `${fileNameWithoutExt}.jpg`;
|
|
58
|
-
const thumbnailPath = path.join(thumbnailsPath, thumbnailFileName);
|
|
59
|
-
const relativeThumbnailPath = path.relative(galleryJsonDir, thumbnailPath);
|
|
60
|
-
|
|
61
|
-
console.log(`Processing ${mediaFile.type}: ${fileName}`);
|
|
62
|
-
|
|
63
|
-
let thumbnailDimensions: { width: number; height: number };
|
|
64
|
-
|
|
65
|
-
if (mediaFile.type === 'image') {
|
|
66
|
-
thumbnailDimensions = await createImageThumbnail(filePath, thumbnailPath, size);
|
|
67
|
-
} else if (mediaFile.type === 'video') {
|
|
68
|
-
thumbnailDimensions = await createVideoThumbnail(filePath, thumbnailPath, size);
|
|
69
|
-
} else {
|
|
70
|
-
console.warn(`Unknown media type: ${mediaFile.type}, skipping...`);
|
|
71
|
-
return mediaFile;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Update media file with thumbnail information
|
|
75
|
-
const updatedMediaFile: MediaFile = {
|
|
76
|
-
...mediaFile,
|
|
77
|
-
thumbnail: {
|
|
78
|
-
path: relativeThumbnailPath,
|
|
79
|
-
width: thumbnailDimensions.width,
|
|
80
|
-
height: thumbnailDimensions.height,
|
|
81
|
-
},
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
return updatedMediaFile;
|
|
85
|
-
} catch (error) {
|
|
86
|
-
if (error instanceof Error && error.message === 'FFMPEG_NOT_AVAILABLE') {
|
|
87
|
-
console.warn(`⚠ Skipping video thumbnail (ffmpeg not available): ${path.basename(mediaFile.path)}`);
|
|
88
|
-
} else {
|
|
89
|
-
console.error(`Error processing ${mediaFile.path}:`, error);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return mediaFile;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export async function thumbnails(options: ThumbnailOptions): Promise<void> {
|
|
97
|
-
const size = Number.parseInt(options.size);
|
|
98
|
-
|
|
99
|
-
// Find all gallery directories
|
|
100
|
-
const galleryDirs = findGalleries(options.path, options.recursive);
|
|
101
|
-
|
|
102
|
-
// If no galleries are found, exit
|
|
103
|
-
if (galleryDirs.length === 0) {
|
|
104
|
-
console.log('No gallery/gallery.json files found.');
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Process each gallery directory
|
|
109
|
-
let totalProcessed = 0;
|
|
110
|
-
for (const galleryDir of galleryDirs) {
|
|
111
|
-
const processed = await processGallery(galleryDir, size);
|
|
112
|
-
totalProcessed += processed;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Log processing stats
|
|
116
|
-
if (options.recursive) {
|
|
117
|
-
console.log(`Completed processing ${totalProcessed} total media files across ${galleryDirs.length} galleries.`);
|
|
118
|
-
} else {
|
|
119
|
-
console.log(`Completed processing ${totalProcessed} media files.`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import { promises as fs } from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
import ffprobe from 'node-ffprobe';
|
|
6
|
-
import sharp from 'sharp';
|
|
7
|
-
|
|
8
|
-
import type { Buffer } from 'node:buffer';
|
|
9
|
-
|
|
10
|
-
// Check if ffmpeg is available
|
|
11
|
-
async function checkFfmpegAvailability(): Promise<boolean> {
|
|
12
|
-
return new Promise((resolve) => {
|
|
13
|
-
const ffmpeg = spawn('ffmpeg', ['-version']);
|
|
14
|
-
|
|
15
|
-
ffmpeg.on('close', (code) => {
|
|
16
|
-
resolve(code === 0);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
ffmpeg.on('error', () => {
|
|
20
|
-
resolve(false);
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Utility function to resize and save thumbnail
|
|
26
|
-
async function resizeAndSaveThumbnail(image: sharp.Sharp, outputPath: string, width: number, height: number): Promise<void> {
|
|
27
|
-
await image.resize(width, height, { withoutEnlargement: true }).jpeg({ quality: 90 }).toFile(outputPath);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export async function createImageThumbnail(
|
|
31
|
-
inputPath: string,
|
|
32
|
-
outputPath: string,
|
|
33
|
-
height: number,
|
|
34
|
-
): Promise<{ width: number; height: number }> {
|
|
35
|
-
try {
|
|
36
|
-
// Check if input file exists
|
|
37
|
-
await fs.access(inputPath);
|
|
38
|
-
|
|
39
|
-
// Create thumbnail using sharp
|
|
40
|
-
const image = sharp(inputPath);
|
|
41
|
-
const metadata = await image.metadata();
|
|
42
|
-
|
|
43
|
-
const originalWidth = metadata.width || 0;
|
|
44
|
-
const originalHeight = metadata.height || 0;
|
|
45
|
-
|
|
46
|
-
if (originalWidth === 0 || originalHeight === 0) {
|
|
47
|
-
throw new Error('Invalid image dimensions');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Calculate width maintaining aspect ratio
|
|
51
|
-
const aspectRatio = originalWidth / originalHeight;
|
|
52
|
-
const width = Math.round(height * aspectRatio);
|
|
53
|
-
|
|
54
|
-
await resizeAndSaveThumbnail(image, outputPath, width, height);
|
|
55
|
-
|
|
56
|
-
return { width, height };
|
|
57
|
-
} catch (error) {
|
|
58
|
-
throw new Error(`Failed to create image thumbnail for ${inputPath}: ${error}`);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export async function createVideoThumbnail(
|
|
63
|
-
inputPath: string,
|
|
64
|
-
outputPath: string,
|
|
65
|
-
height: number,
|
|
66
|
-
): Promise<{ width: number; height: number }> {
|
|
67
|
-
try {
|
|
68
|
-
// Check if input file exists
|
|
69
|
-
await fs.access(inputPath);
|
|
70
|
-
|
|
71
|
-
// Check if ffmpeg is available
|
|
72
|
-
const ffmpegAvailable = await checkFfmpegAvailability();
|
|
73
|
-
if (!ffmpegAvailable) {
|
|
74
|
-
console.warn(`Warning: ffmpeg is not available. Skipping thumbnail creation for video: ${path.basename(inputPath)}`);
|
|
75
|
-
throw new Error('FFMPEG_NOT_AVAILABLE');
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Get video metadata using ffprobe
|
|
79
|
-
const videoData = await ffprobe(inputPath);
|
|
80
|
-
const videoStream = videoData.streams.find((stream) => stream.codec_type === 'video');
|
|
81
|
-
|
|
82
|
-
if (!videoStream) {
|
|
83
|
-
throw new Error('No video stream found');
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const originalWidth = videoStream.width || 0;
|
|
87
|
-
const originalHeight = videoStream.height || 0;
|
|
88
|
-
|
|
89
|
-
if (originalWidth === 0 || originalHeight === 0) {
|
|
90
|
-
throw new Error('Invalid video dimensions');
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Calculate width maintaining aspect ratio
|
|
94
|
-
const aspectRatio = originalWidth / originalHeight;
|
|
95
|
-
const width = Math.round(height * aspectRatio);
|
|
96
|
-
|
|
97
|
-
// Use ffmpeg to extract first frame as a temporary file, then process with sharp
|
|
98
|
-
const tempFramePath = `${outputPath}.temp.png`;
|
|
99
|
-
|
|
100
|
-
return new Promise((resolve, reject) => {
|
|
101
|
-
// Extract first frame using ffmpeg
|
|
102
|
-
const ffmpeg = spawn('ffmpeg', [
|
|
103
|
-
'-i',
|
|
104
|
-
inputPath,
|
|
105
|
-
'-vframes',
|
|
106
|
-
'1',
|
|
107
|
-
'-y', // Overwrite output file
|
|
108
|
-
tempFramePath,
|
|
109
|
-
]);
|
|
110
|
-
|
|
111
|
-
ffmpeg.stderr.on('data', (data: Buffer) => {
|
|
112
|
-
// FFmpeg writes normal output to stderr, so we don't treat this as an error
|
|
113
|
-
console.debug(`ffmpeg: ${data.toString()}`);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
ffmpeg.on('close', async (code: number) => {
|
|
117
|
-
if (code === 0) {
|
|
118
|
-
try {
|
|
119
|
-
// Process the extracted frame with sharp
|
|
120
|
-
const frameImage = sharp(tempFramePath);
|
|
121
|
-
await resizeAndSaveThumbnail(frameImage, outputPath, width, height);
|
|
122
|
-
|
|
123
|
-
// Clean up temporary file
|
|
124
|
-
try {
|
|
125
|
-
await fs.unlink(tempFramePath);
|
|
126
|
-
} catch {
|
|
127
|
-
// Ignore cleanup errors
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
resolve({ width, height });
|
|
131
|
-
} catch (sharpError) {
|
|
132
|
-
reject(new Error(`Failed to process extracted frame: ${sharpError}`));
|
|
133
|
-
}
|
|
134
|
-
} else {
|
|
135
|
-
reject(new Error(`ffmpeg exited with code ${code}`));
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
ffmpeg.on('error', (error: Error) => {
|
|
140
|
-
reject(new Error(`Failed to start ffmpeg: ${error.message}`));
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
} catch (error) {
|
|
144
|
-
if (error instanceof Error && error.message === 'FFMPEG_NOT_AVAILABLE') {
|
|
145
|
-
// Re-throw the specific error for graceful handling upstream
|
|
146
|
-
throw error;
|
|
147
|
-
}
|
|
148
|
-
if (
|
|
149
|
-
typeof error === 'object' &&
|
|
150
|
-
error !== null &&
|
|
151
|
-
'message' in error &&
|
|
152
|
-
typeof (error as { message: string }).message === 'string' &&
|
|
153
|
-
((error as { message: string }).message.includes('ffmpeg') ||
|
|
154
|
-
(error as { message: string }).message.includes('ffprobe'))
|
|
155
|
-
) {
|
|
156
|
-
throw new Error(
|
|
157
|
-
`Error: ffmpeg is required to process videos. Please install ffmpeg and ensure it is available in your PATH. Failed to process: ${path.basename(inputPath)}`,
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
throw new Error(`Failed to create video thumbnail for ${inputPath}: ${error}`);
|
|
161
|
-
}
|
|
162
|
-
}
|
package/src/types/index.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
export const ThumbnailSchema = z.object({
|
|
4
|
-
path: z.string(),
|
|
5
|
-
width: z.number(),
|
|
6
|
-
height: z.number(),
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
export const MediaFileSchema = z.object({
|
|
10
|
-
type: z.enum(['image', 'video']),
|
|
11
|
-
path: z.string(),
|
|
12
|
-
alt: z.string().optional(),
|
|
13
|
-
width: z.number(),
|
|
14
|
-
height: z.number(),
|
|
15
|
-
thumbnail: ThumbnailSchema.optional(),
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
export const GallerySectionSchema = z.object({
|
|
19
|
-
title: z.string().optional(),
|
|
20
|
-
description: z.string().optional(),
|
|
21
|
-
images: z.array(MediaFileSchema),
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
export const SubGallerySchema = z.object({
|
|
25
|
-
title: z.string(),
|
|
26
|
-
headerImage: z.string(),
|
|
27
|
-
path: z.string(),
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
export const GalleryDataSchema = z.object({
|
|
31
|
-
title: z.string(),
|
|
32
|
-
description: z.string(),
|
|
33
|
-
headerImage: z.string(),
|
|
34
|
-
metadata: z.object({
|
|
35
|
-
ogUrl: z.string(),
|
|
36
|
-
}),
|
|
37
|
-
galleryOutputPath: z.string().optional(),
|
|
38
|
-
sections: z.array(GallerySectionSchema),
|
|
39
|
-
subGalleries: z.object({ title: z.string(), galleries: z.array(SubGallerySchema) }),
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
export type Thumbnail = z.infer<typeof ThumbnailSchema>;
|
|
43
|
-
export type MediaFile = z.infer<typeof MediaFileSchema>;
|
|
44
|
-
export type GallerySection = z.infer<typeof GallerySectionSchema>;
|
|
45
|
-
export type SubGallery = z.infer<typeof SubGallerySchema>;
|
|
46
|
-
export type GalleryData = z.infer<typeof GalleryDataSchema>;
|
package/src/utils/index.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Finds all gallery directories that contain a gallery/gallery.json file.
|
|
6
|
-
*
|
|
7
|
-
* @param basePath - The base directory to search from
|
|
8
|
-
* @param recursive - Whether to search subdirectories recursively
|
|
9
|
-
* @returns Array of paths to directories containing gallery/gallery.json files
|
|
10
|
-
*/
|
|
11
|
-
export const findGalleries = (basePath: string, recursive: boolean): string[] => {
|
|
12
|
-
const galleryDirs: string[] = [];
|
|
13
|
-
|
|
14
|
-
// Check basePath itself
|
|
15
|
-
const galleryJsonPath = path.join(basePath, 'gallery', 'gallery.json');
|
|
16
|
-
if (fs.existsSync(galleryJsonPath)) {
|
|
17
|
-
galleryDirs.push(basePath);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// If recursive, search all subdirectories
|
|
21
|
-
if (recursive) {
|
|
22
|
-
try {
|
|
23
|
-
const entries = fs.readdirSync(basePath, { withFileTypes: true });
|
|
24
|
-
for (const entry of entries) {
|
|
25
|
-
if (entry.isDirectory() && entry.name !== 'gallery') {
|
|
26
|
-
const subPath = path.join(basePath, entry.name);
|
|
27
|
-
const subResults = findGalleries(subPath, recursive);
|
|
28
|
-
galleryDirs.push(...subResults);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
} catch {
|
|
32
|
-
// Silently ignore errors when reading directories
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return galleryDirs;
|
|
37
|
-
};
|