simple-photo-gallery 0.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 +349 -0
- package/dist/src/index.js +29 -0
- package/dist/src/modules/build/index.js +56 -0
- package/dist/src/modules/build/types/index.js +1 -0
- package/dist/src/modules/build/utils/index.js +7 -0
- package/dist/src/modules/init/const/index.js +4 -0
- package/dist/src/modules/init/index.js +156 -0
- package/dist/src/modules/init/types/index.js +1 -0
- package/dist/src/modules/init/utils/index.js +93 -0
- package/dist/src/modules/thumbnails/index.js +98 -0
- package/dist/src/modules/thumbnails/types/index.js +1 -0
- package/dist/src/modules/thumbnails/utils/index.js +127 -0
- package/dist/src/types/index.js +35 -0
- package/dist/src/utils/index.js +34 -0
- package/dist/tests/cli.test.js +170 -0
- package/package.json +61 -0
- package/src/index.ts +50 -0
- package/src/modules/build/index.ts +68 -0
- package/src/modules/build/types/index.ts +4 -0
- package/src/modules/build/utils/index.ts +9 -0
- package/src/modules/init/const/index.ts +5 -0
- package/src/modules/init/index.ts +193 -0
- package/src/modules/init/types/index.ts +16 -0
- package/src/modules/init/types/node-ffprobe.d.ts +17 -0
- package/src/modules/init/utils/index.ts +98 -0
- package/src/modules/thumbnails/index.ts +121 -0
- package/src/modules/thumbnails/types/index.ts +5 -0
- package/src/modules/thumbnails/utils/index.ts +162 -0
- package/src/types/index.ts +46 -0
- package/src/utils/index.ts +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
# Simple Photo Gallery CLI
|
|
2
|
+
|
|
3
|
+
A command-line tool for creating beautiful photo galleries from your image and video collections. This CLI helps you scan directories, generate thumbnails, and set up Astro-based gallery websites.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 📸 **Media Scanning**: Automatically scan directories for images and videos
|
|
8
|
+
- 🖼️ **Thumbnail Generation**: Create optimized thumbnails for fast loading
|
|
9
|
+
- 🎥 **Video Support**: Handle video files with ffmpeg integration
|
|
10
|
+
- 📱 **Responsive Design**: Generate galleries that work on all devices
|
|
11
|
+
- ⚡ **Fast Performance**: Optimized thumbnails and lazy loading
|
|
12
|
+
- 🔧 **Astro Integration**: Seamless setup with Astro static site generator
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
### Prerequisites
|
|
17
|
+
|
|
18
|
+
- Node.js 18+
|
|
19
|
+
- npm or yarn
|
|
20
|
+
- ffmpeg (for video processing)
|
|
21
|
+
|
|
22
|
+
### Install ffmpeg
|
|
23
|
+
|
|
24
|
+
**macOS:**
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
brew install ffmpeg
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Ubuntu/Debian:**
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
sudo apt update
|
|
34
|
+
sudo apt install ffmpeg
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Windows:**
|
|
38
|
+
Download from [ffmpeg.org](https://ffmpeg.org/download.html)
|
|
39
|
+
|
|
40
|
+
### Install the CLI
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Clone the repository
|
|
44
|
+
git clone <repository-url>
|
|
45
|
+
cd simple-photo-gallery-public/cli
|
|
46
|
+
|
|
47
|
+
# Install dependencies
|
|
48
|
+
npm install
|
|
49
|
+
|
|
50
|
+
# Build the CLI
|
|
51
|
+
npm run build
|
|
52
|
+
|
|
53
|
+
# Link globally (optional)
|
|
54
|
+
npm link
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Commands
|
|
58
|
+
|
|
59
|
+
### `gallery init`
|
|
60
|
+
|
|
61
|
+
Scan a directory for images and videos to create a `gallery.json` file.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
gallery init [options]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Options:**
|
|
68
|
+
|
|
69
|
+
- `-p, --path <path>` - Path to scan for media files (default: current directory)
|
|
70
|
+
- `-o, --output <path>` - Output directory for gallery.json
|
|
71
|
+
- `-r, --recursive` - Scan subdirectories recursively
|
|
72
|
+
|
|
73
|
+
**Examples:**
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Scan current directory
|
|
77
|
+
gallery init
|
|
78
|
+
|
|
79
|
+
# Scan specific directory
|
|
80
|
+
gallery init --path /path/to/photos
|
|
81
|
+
|
|
82
|
+
# Scan recursively
|
|
83
|
+
gallery init --path /path/to/photos --recursive
|
|
84
|
+
|
|
85
|
+
# Specify output directory
|
|
86
|
+
gallery init --path /path/to/photos --output /path/to/output
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### `gallery thumbnails`
|
|
90
|
+
|
|
91
|
+
Generate thumbnails for all media files in the gallery.
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
gallery thumbnails [options]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Options:**
|
|
98
|
+
|
|
99
|
+
- `-p, --path <path>` - Path containing .simple-photo-gallery folder (default: current directory)
|
|
100
|
+
- `-s, --size <size>` - Thumbnail height in pixels (default: 200)
|
|
101
|
+
|
|
102
|
+
**Examples:**
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# Generate thumbnails with default size
|
|
106
|
+
gallery thumbnails
|
|
107
|
+
|
|
108
|
+
# Generate thumbnails with custom size
|
|
109
|
+
gallery thumbnails --size 300
|
|
110
|
+
|
|
111
|
+
# Specify gallery path
|
|
112
|
+
gallery thumbnails --path /path/to/gallery
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### `gallery build`
|
|
116
|
+
|
|
117
|
+
Configure Astro template to work with external image directories and build galleries.
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
gallery build [options]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Options:**
|
|
124
|
+
|
|
125
|
+
- `-i, --images-path <path>` - Path to images directory (required)
|
|
126
|
+
- `-r, --recursive` - Scan subdirectories recursively for gallery/gallery.json files
|
|
127
|
+
|
|
128
|
+
**Examples:**
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# Setup template for a single gallery directory
|
|
132
|
+
gallery build --images-path ../my-photos
|
|
133
|
+
|
|
134
|
+
# Setup template recursively for multiple gallery directories
|
|
135
|
+
gallery build --images-path ../my-photos --recursive
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Complete Workflow Examples
|
|
139
|
+
|
|
140
|
+
### Basic Gallery Creation
|
|
141
|
+
|
|
142
|
+
1. **Prepare your photos directory:**
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
mkdir my-photos
|
|
146
|
+
# Copy your photos and videos to my-photos/
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
2. **Scan for media files:**
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
cd my-photos
|
|
153
|
+
gallery init
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
3. **Generate thumbnails:**
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
gallery thumbnails --size 250
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
4. **Set up Astro template:**
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
cd ../template
|
|
166
|
+
gallery build
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
5. **Build and serve:**
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
npm run dev
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Advanced Workflow with Multiple Sections
|
|
176
|
+
|
|
177
|
+
1. **Organize photos in subdirectories:**
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
my-photos/
|
|
181
|
+
├── vacation/
|
|
182
|
+
│ ├── beach.jpg
|
|
183
|
+
│ ├── mountains.jpg
|
|
184
|
+
│ └── sunset.mp4
|
|
185
|
+
├── family/
|
|
186
|
+
│ ├── birthday.jpg
|
|
187
|
+
│ └── christmas.jpg
|
|
188
|
+
└── events/
|
|
189
|
+
├── wedding.jpg
|
|
190
|
+
└── graduation.jpg
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
2. **Scan with recursive option:**
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
cd my-photos
|
|
197
|
+
gallery init --recursive
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
3. **Edit gallery.json to create sections:**
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
{
|
|
204
|
+
"title": "My Photo Collection",
|
|
205
|
+
"description": "A collection of my favorite moments",
|
|
206
|
+
"headerImage": "vacation/beach.jpg",
|
|
207
|
+
"metadata": { "ogUrl": "" },
|
|
208
|
+
"sections": [
|
|
209
|
+
{
|
|
210
|
+
"title": "Vacation Memories",
|
|
211
|
+
"description": "Amazing trips and adventures",
|
|
212
|
+
"images": [
|
|
213
|
+
// vacation images will be here
|
|
214
|
+
]
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"title": "Family Moments",
|
|
218
|
+
"description": "Precious family memories",
|
|
219
|
+
"images": [
|
|
220
|
+
// family images will be here
|
|
221
|
+
]
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
"title": "Special Events",
|
|
225
|
+
"description": "Important life events",
|
|
226
|
+
"images": [
|
|
227
|
+
// events images will be here
|
|
228
|
+
]
|
|
229
|
+
}
|
|
230
|
+
]
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
4. **Generate thumbnails:**
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
gallery thumbnails --size 300 --recursive
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
5. **Set up and build:**
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
cd ../template
|
|
244
|
+
gallery build --recursive
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## File Structure
|
|
248
|
+
|
|
249
|
+
After running the CLI, your project will have this structure:
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
my-photos/
|
|
253
|
+
├── .simple-photo-gallery/
|
|
254
|
+
│ ├── gallery.json # Gallery metadata and file list
|
|
255
|
+
│ └── thumbnails/ # Generated thumbnails
|
|
256
|
+
│ ├── image1.jpg
|
|
257
|
+
│ ├── image2.jpg
|
|
258
|
+
│ └── video1.jpg
|
|
259
|
+
├── photo1.jpg
|
|
260
|
+
├── photo2.jpg
|
|
261
|
+
└── video1.mp4
|
|
262
|
+
|
|
263
|
+
template/
|
|
264
|
+
├── astro.config.ts # Modified Astro config
|
|
265
|
+
├── gallery.json # Copied gallery data
|
|
266
|
+
└── ... (other Astro files)
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Supported Media Formats
|
|
270
|
+
|
|
271
|
+
**Images:**
|
|
272
|
+
|
|
273
|
+
- JPEG (.jpg, .jpeg)
|
|
274
|
+
- PNG (.png)
|
|
275
|
+
- WebP (.webp)
|
|
276
|
+
- GIF (.gif)
|
|
277
|
+
- TIFF (.tiff, .tif)
|
|
278
|
+
|
|
279
|
+
**Videos:**
|
|
280
|
+
|
|
281
|
+
- MP4 (.mp4)
|
|
282
|
+
- MOV (.mov)
|
|
283
|
+
- AVI (.avi)
|
|
284
|
+
- WebM (.webm)
|
|
285
|
+
- MKV (.mkv)
|
|
286
|
+
|
|
287
|
+
_Note: Video processing requires ffmpeg to be installed and available in your PATH._
|
|
288
|
+
|
|
289
|
+
## Troubleshooting
|
|
290
|
+
|
|
291
|
+
### Common Issues
|
|
292
|
+
|
|
293
|
+
**"ffprobe not found" error:**
|
|
294
|
+
|
|
295
|
+
- Install ffmpeg: `brew install ffmpeg` (macOS) or `sudo apt install ffmpeg` (Ubuntu)
|
|
296
|
+
- Ensure ffmpeg is in your PATH
|
|
297
|
+
|
|
298
|
+
**"Gallery not found" error:**
|
|
299
|
+
|
|
300
|
+
- Run `gallery init` first to create the gallery.json file
|
|
301
|
+
- Check that the path to .simple-photo-gallery folder is correct
|
|
302
|
+
|
|
303
|
+
**Thumbnail generation fails:**
|
|
304
|
+
|
|
305
|
+
- Check file permissions
|
|
306
|
+
- Ensure sufficient disk space
|
|
307
|
+
- Verify media files are not corrupted
|
|
308
|
+
|
|
309
|
+
### Getting Help
|
|
310
|
+
|
|
311
|
+
- Check that all required dependencies are installed
|
|
312
|
+
- Verify file paths are correct and accessible
|
|
313
|
+
- Ensure ffmpeg is properly installed for video processing
|
|
314
|
+
- Review the generated gallery.json file for any issues
|
|
315
|
+
|
|
316
|
+
## Development
|
|
317
|
+
|
|
318
|
+
### Building from Source
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
# Install dependencies
|
|
322
|
+
npm install
|
|
323
|
+
|
|
324
|
+
# Build the CLI
|
|
325
|
+
npm run build
|
|
326
|
+
|
|
327
|
+
# Run in development mode
|
|
328
|
+
npm run gallery
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Code Quality
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
# Check TypeScript
|
|
335
|
+
npm run check
|
|
336
|
+
|
|
337
|
+
# Lint code
|
|
338
|
+
npm run lint
|
|
339
|
+
|
|
340
|
+
# Fix linting issues
|
|
341
|
+
npm run lint:fix
|
|
342
|
+
|
|
343
|
+
# Format code
|
|
344
|
+
npm run format
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## License
|
|
348
|
+
|
|
349
|
+
MIT License - see LICENSE file for details.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { build } from './modules/build';
|
|
5
|
+
import { init } from './modules/init';
|
|
6
|
+
import { thumbnails } from './modules/thumbnails';
|
|
7
|
+
const program = new Command();
|
|
8
|
+
program.name('gallery').description('Simple Photo Gallery CLI').version('0.0.1');
|
|
9
|
+
program
|
|
10
|
+
.command('init')
|
|
11
|
+
.description('Initialize a gallery by scaning a folder for images and videos')
|
|
12
|
+
.option('-p, --path <path>', 'Path where the gallery should be initialized. Default: current working directory', process.cwd())
|
|
13
|
+
.option('-o, --output <path>', 'Output directory for the gallery.json file', '')
|
|
14
|
+
.option('-r, --recursive', 'Scan subdirectories recursively', false)
|
|
15
|
+
.action(init);
|
|
16
|
+
program
|
|
17
|
+
.command('thumbnails')
|
|
18
|
+
.description('Create thumbnails for all media files in the gallery')
|
|
19
|
+
.option('-p, --path <path>', 'Path to the folder containing the gallery.json file. Default: current working directory', process.cwd())
|
|
20
|
+
.option('-s, --size <size>', 'Thumbnail height in pixels', '200')
|
|
21
|
+
.option('-r, --recursive', 'Scan subdirectories recursively', false)
|
|
22
|
+
.action(thumbnails);
|
|
23
|
+
program
|
|
24
|
+
.command('build')
|
|
25
|
+
.description('Build the HTML gallery in the specified directory')
|
|
26
|
+
.option('-p, --path <path>', 'Path to the folder containing the gallery.json file. Default: current working directory', process.cwd())
|
|
27
|
+
.option('-r, --recursive', 'Scan subdirectories recursively', false)
|
|
28
|
+
.action(build);
|
|
29
|
+
program.parse();
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import { resolveFromCurrentDir } from './utils';
|
|
6
|
+
import { findGalleries } from '../../utils';
|
|
7
|
+
function buildGallery(galleryDir, templateDir) {
|
|
8
|
+
// Make sure the gallery.json file exists
|
|
9
|
+
const galleryJsonPath = path.join(galleryDir, 'gallery', 'gallery.json');
|
|
10
|
+
if (!fs.existsSync(galleryJsonPath)) {
|
|
11
|
+
console.log(`No gallery/gallery.json found in ${galleryDir}`);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
// Build the template
|
|
15
|
+
const originalEnv = { ...process.env };
|
|
16
|
+
try {
|
|
17
|
+
// Set the environment variable for the gallery.json path that will be used by the template
|
|
18
|
+
process.env.GALLERY_JSON_PATH = galleryJsonPath;
|
|
19
|
+
process.env.GALLERY_OUTPUT_DIR = path.join(galleryDir, 'gallery');
|
|
20
|
+
execSync('npm run build', { cwd: templateDir, stdio: 'inherit' });
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
console.error(error);
|
|
24
|
+
console.error(`Build failed for ${galleryDir}`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
// Restore original environment and gallery.json
|
|
29
|
+
process.env = originalEnv;
|
|
30
|
+
}
|
|
31
|
+
// Copy the build output to the output directory
|
|
32
|
+
const outputDir = path.join(galleryDir, 'gallery');
|
|
33
|
+
const buildDir = path.join(outputDir, '_build');
|
|
34
|
+
fs.cpSync(buildDir, outputDir, { recursive: true });
|
|
35
|
+
// Move the index.html to the gallery directory
|
|
36
|
+
fs.copyFileSync(path.join(outputDir, 'index.html'), path.join(galleryDir, 'index.html'));
|
|
37
|
+
fs.rmSync(path.join(outputDir, 'index.html'));
|
|
38
|
+
// Clean up the _build directory
|
|
39
|
+
console.log('Cleaning up build directory...');
|
|
40
|
+
fs.rmSync(buildDir, { recursive: true, force: true });
|
|
41
|
+
}
|
|
42
|
+
export async function build(options) {
|
|
43
|
+
// Get the template directory
|
|
44
|
+
const templateDir = path.join(resolveFromCurrentDir('../../../../../'), 'template');
|
|
45
|
+
// Find all gallery directories
|
|
46
|
+
const galleryDirs = findGalleries(options.path, options.recursive);
|
|
47
|
+
// If no galleries are found, exit
|
|
48
|
+
if (galleryDirs.length === 0) {
|
|
49
|
+
console.log('No gallery/gallery.json files found.');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Process each gallery
|
|
53
|
+
for (const dir of galleryDirs) {
|
|
54
|
+
buildGallery(dir, templateDir);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
// __dirname workaround for ESM modules
|
|
3
|
+
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
|
4
|
+
// Helper function to resolve paths relative to current file
|
|
5
|
+
export const resolveFromCurrentDir = (...segments) => {
|
|
6
|
+
return path.resolve(__dirname, ...segments);
|
|
7
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Image extensions
|
|
2
|
+
export const IMAGE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.tiff', '.tif', '.svg']);
|
|
3
|
+
// Video extensions
|
|
4
|
+
export const VIDEO_EXTENSIONS = new Set(['.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm', '.mkv', '.m4v', '.3gp']);
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { capitalizeTitle, getImageMetadata, getVideoDimensions, isMediaFile } from './utils';
|
|
4
|
+
async function scanDirectory(dirPath) {
|
|
5
|
+
const mediaFiles = [];
|
|
6
|
+
try {
|
|
7
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
8
|
+
for (const entry of entries) {
|
|
9
|
+
if (entry.isFile()) {
|
|
10
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
11
|
+
const mediaType = isMediaFile(entry.name);
|
|
12
|
+
if (mediaType) {
|
|
13
|
+
console.log(`Processing ${mediaType}: ${entry.name}`);
|
|
14
|
+
let metadata = { width: 0, height: 0 };
|
|
15
|
+
try {
|
|
16
|
+
if (mediaType === 'image') {
|
|
17
|
+
metadata = await getImageMetadata(fullPath);
|
|
18
|
+
}
|
|
19
|
+
else if (mediaType === 'video') {
|
|
20
|
+
try {
|
|
21
|
+
const videoDimensions = await getVideoDimensions(fullPath);
|
|
22
|
+
metadata = { ...videoDimensions };
|
|
23
|
+
}
|
|
24
|
+
catch (videoError) {
|
|
25
|
+
if (typeof videoError === 'object' &&
|
|
26
|
+
videoError !== null &&
|
|
27
|
+
'message' in videoError &&
|
|
28
|
+
typeof videoError.message === 'string' &&
|
|
29
|
+
(videoError.message.includes('ffprobe') ||
|
|
30
|
+
videoError.message.includes('ffmpeg'))) {
|
|
31
|
+
console.error(`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}`);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
console.error(`Error processing video ${entry.name}:`, videoError);
|
|
35
|
+
}
|
|
36
|
+
continue; // Skip this file
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (mediaError) {
|
|
41
|
+
console.error(`Error processing file ${entry.name}:`, mediaError);
|
|
42
|
+
continue; // Skip this file
|
|
43
|
+
}
|
|
44
|
+
const mediaFile = {
|
|
45
|
+
type: mediaType,
|
|
46
|
+
path: fullPath,
|
|
47
|
+
width: metadata.width,
|
|
48
|
+
height: metadata.height,
|
|
49
|
+
};
|
|
50
|
+
if (metadata.description) {
|
|
51
|
+
mediaFile.alt = metadata.description;
|
|
52
|
+
}
|
|
53
|
+
mediaFiles.push(mediaFile);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
console.error(`Error scanning directory ${dirPath}:`, error);
|
|
60
|
+
}
|
|
61
|
+
return mediaFiles;
|
|
62
|
+
}
|
|
63
|
+
async function createGalleryJson(mediaFiles, galleryJsonPath, subGalleries = []) {
|
|
64
|
+
const galleryDir = path.dirname(galleryJsonPath);
|
|
65
|
+
// Convert media file paths to be relative to gallery.json
|
|
66
|
+
const relativeMediaFiles = mediaFiles.map((file) => ({
|
|
67
|
+
...file,
|
|
68
|
+
path: path.relative(galleryDir, file.path),
|
|
69
|
+
}));
|
|
70
|
+
// Convert subGallery header image paths to be relative to gallery.json
|
|
71
|
+
const relativeSubGalleries = subGalleries.map((subGallery) => ({
|
|
72
|
+
...subGallery,
|
|
73
|
+
headerImage: subGallery.headerImage ? path.relative(galleryDir, subGallery.headerImage) : '',
|
|
74
|
+
}));
|
|
75
|
+
const galleryData = {
|
|
76
|
+
title: 'My Gallery',
|
|
77
|
+
description: 'My gallery with fantastic photos.',
|
|
78
|
+
headerImage: relativeMediaFiles[0]?.path || '',
|
|
79
|
+
metadata: { ogUrl: '' },
|
|
80
|
+
sections: [
|
|
81
|
+
{
|
|
82
|
+
images: relativeMediaFiles,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
subGalleries: {
|
|
86
|
+
title: 'Sub Galleries',
|
|
87
|
+
galleries: relativeSubGalleries,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
await fs.writeFile(galleryJsonPath, JSON.stringify(galleryData, null, 2));
|
|
91
|
+
}
|
|
92
|
+
async function processDirectory(dirPath, options) {
|
|
93
|
+
let totalFiles = 0;
|
|
94
|
+
const subGalleries = [];
|
|
95
|
+
// Scan current directory for media files
|
|
96
|
+
const mediaFiles = await scanDirectory(dirPath);
|
|
97
|
+
totalFiles += mediaFiles.length;
|
|
98
|
+
// Process subdirectories only if recursive mode is enabled
|
|
99
|
+
if (options.recursive) {
|
|
100
|
+
try {
|
|
101
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
if (entry.isDirectory() && entry.name !== 'gallery') {
|
|
104
|
+
const subDirPath = path.join(dirPath, entry.name);
|
|
105
|
+
const result = await processDirectory(subDirPath, options);
|
|
106
|
+
totalFiles += result.totalFiles;
|
|
107
|
+
// If the subdirectory had media files, add it as a subGallery
|
|
108
|
+
if (result.subGallery) {
|
|
109
|
+
subGalleries.push(result.subGallery);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
console.error(`Error reading directory ${dirPath}:`, error);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Create gallery.json if there are media files or subGalleries
|
|
119
|
+
if (mediaFiles.length > 0 || subGalleries.length > 0) {
|
|
120
|
+
const outputPath = options.output
|
|
121
|
+
? path.resolve(options.output, path.relative(path.resolve(options.path), dirPath), 'gallery')
|
|
122
|
+
: path.join(dirPath, 'gallery');
|
|
123
|
+
const galleryJsonPath = path.join(outputPath, 'gallery.json');
|
|
124
|
+
// Create output directory
|
|
125
|
+
await fs.mkdir(outputPath, { recursive: true });
|
|
126
|
+
// Create gallery.json for this directory
|
|
127
|
+
await createGalleryJson(mediaFiles, galleryJsonPath, subGalleries);
|
|
128
|
+
console.log(`Gallery JSON created at: ${galleryJsonPath} (${mediaFiles.length} files, ${subGalleries.length} subgalleries)`);
|
|
129
|
+
}
|
|
130
|
+
// Return result with suGgallery info if this directory has media files
|
|
131
|
+
const result = { totalFiles };
|
|
132
|
+
if (mediaFiles.length > 0 || subGalleries.length > 0) {
|
|
133
|
+
const dirName = path.basename(dirPath);
|
|
134
|
+
result.subGallery = {
|
|
135
|
+
title: capitalizeTitle(dirName),
|
|
136
|
+
headerImage: mediaFiles[0]?.path || '',
|
|
137
|
+
path: path.join('..', dirName),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
export async function init(options) {
|
|
143
|
+
const scanPath = path.resolve(options.path);
|
|
144
|
+
console.log(`Scanning directory: ${scanPath}`);
|
|
145
|
+
console.log(`Recursive: ${options.recursive}`);
|
|
146
|
+
try {
|
|
147
|
+
// Ensure scan path exists
|
|
148
|
+
await fs.access(scanPath);
|
|
149
|
+
// Process the directory tree with the specified recursion setting
|
|
150
|
+
const result = await processDirectory(scanPath, options);
|
|
151
|
+
console.log(`Total files processed: ${result.totalFiles}`);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
throw new Error(`Error during scan: ${error}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import exifReader from 'exif-reader';
|
|
4
|
+
import ffprobe from 'node-ffprobe';
|
|
5
|
+
import sharp from 'sharp';
|
|
6
|
+
import { IMAGE_EXTENSIONS, VIDEO_EXTENSIONS } from '../const';
|
|
7
|
+
export async function getImageMetadata(filePath) {
|
|
8
|
+
try {
|
|
9
|
+
const metadata = await sharp(filePath).metadata();
|
|
10
|
+
let description;
|
|
11
|
+
// Extract description from EXIF data
|
|
12
|
+
if (metadata.exif) {
|
|
13
|
+
try {
|
|
14
|
+
const exifData = exifReader(metadata.exif);
|
|
15
|
+
if (exifData.Image?.ImageDescription) {
|
|
16
|
+
description = exifData.Image.ImageDescription;
|
|
17
|
+
}
|
|
18
|
+
else if (exifData.Image?.Description) {
|
|
19
|
+
description = exifData.Image.Description.toString();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// EXIF parsing failed, but that's OK
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
width: metadata.width || 0,
|
|
28
|
+
height: metadata.height || 0,
|
|
29
|
+
description,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
console.warn(`Warning: Could not get metadata for image ${filePath}:`, error);
|
|
34
|
+
return { width: 0, height: 0 };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function getVideoDimensions(filePath) {
|
|
38
|
+
try {
|
|
39
|
+
const data = await ffprobe(filePath);
|
|
40
|
+
const videoStream = data.streams.find((stream) => stream.codec_type === 'video');
|
|
41
|
+
if (videoStream) {
|
|
42
|
+
return {
|
|
43
|
+
width: videoStream.width || 0,
|
|
44
|
+
height: videoStream.height || 0,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return { width: 0, height: 0 };
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
console.warn(`Warning: Could not get dimensions for video ${filePath}:`, error);
|
|
51
|
+
return { width: 0, height: 0 };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function isMediaFile(fileName) {
|
|
55
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
56
|
+
if (IMAGE_EXTENSIONS.has(ext))
|
|
57
|
+
return 'image';
|
|
58
|
+
if (VIDEO_EXTENSIONS.has(ext))
|
|
59
|
+
return 'video';
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
export function capitalizeTitle(folderName) {
|
|
63
|
+
return folderName
|
|
64
|
+
.replace('-', ' ')
|
|
65
|
+
.replace('_', ' ')
|
|
66
|
+
.split(' ')
|
|
67
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
68
|
+
.join(' ');
|
|
69
|
+
}
|
|
70
|
+
export async function hasMediaFiles(dirPath) {
|
|
71
|
+
try {
|
|
72
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
73
|
+
// Check for media files in current directory
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
if (entry.isFile() && isMediaFile(entry.name)) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Check subdirectories recursively
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
if (entry.isDirectory() && entry.name !== 'gallery') {
|
|
82
|
+
const subDirPath = path.join(dirPath, entry.name);
|
|
83
|
+
if (await hasMediaFiles(subDirPath)) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
console.error(`Error checking for media files in ${dirPath}:`, error);
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|