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,170 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'node:child_process';
|
|
2
|
-
import { existsSync, rmSync, readFileSync, readdirSync } from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import process from 'node:process';
|
|
5
|
-
import { copySync } from 'fs-extra';
|
|
6
|
-
import { GalleryDataSchema, MediaFileSchema, ThumbnailSchema } from '../src/types';
|
|
7
|
-
const testDir = process.cwd();
|
|
8
|
-
const tsxPath = path.resolve(testDir, '..', 'node_modules', '.bin', 'tsx');
|
|
9
|
-
const cliPath = path.resolve(testDir, 'src', 'index.ts');
|
|
10
|
-
const singleFixturePath = path.resolve(testDir, 'tests', 'fixtures', 'single');
|
|
11
|
-
const singleTestPath = path.resolve(testDir, 'tests', 'fixtures', 'test', 'single');
|
|
12
|
-
const multiFixturePath = path.resolve(testDir, 'tests', 'fixtures', 'multi');
|
|
13
|
-
const multiTestPath = path.resolve(testDir, 'tests', 'fixtures', 'test', 'multi');
|
|
14
|
-
const MediaFileWithThumbnailSchema = MediaFileSchema.extend({
|
|
15
|
-
thumbnail: ThumbnailSchema,
|
|
16
|
-
});
|
|
17
|
-
// Helper functions for gallery validation
|
|
18
|
-
function validateGalleryStructure(galleryPath, expectedImageCount, expectedSubGalleryCount = 0) {
|
|
19
|
-
// Check that gallery.json exists
|
|
20
|
-
const galleryJsonPath = path.resolve(galleryPath, 'gallery.json');
|
|
21
|
-
expect(existsSync(galleryJsonPath)).toBe(true);
|
|
22
|
-
// Read and parse gallery.json
|
|
23
|
-
const galleryContent = readFileSync(galleryJsonPath, 'utf8');
|
|
24
|
-
const galleryData = JSON.parse(galleryContent);
|
|
25
|
-
// Validate with schema
|
|
26
|
-
expect(() => GalleryDataSchema.parse(galleryData)).not.toThrow();
|
|
27
|
-
const validatedData = GalleryDataSchema.parse(galleryData);
|
|
28
|
-
// Check image count
|
|
29
|
-
expect(validatedData.sections).toHaveLength(1);
|
|
30
|
-
expect(validatedData.sections[0].images).toHaveLength(expectedImageCount);
|
|
31
|
-
// Check subgallery count
|
|
32
|
-
expect(validatedData.subGalleries.galleries).toHaveLength(expectedSubGalleryCount);
|
|
33
|
-
return validatedData;
|
|
34
|
-
}
|
|
35
|
-
function validateThumbnails(galleryPath, expectedThumbnailCount) {
|
|
36
|
-
// Check that thumbnails directory exists
|
|
37
|
-
const thumbnailsPath = path.resolve(galleryPath, 'thumbnails');
|
|
38
|
-
expect(existsSync(thumbnailsPath)).toBe(true);
|
|
39
|
-
// Check thumbnail file count
|
|
40
|
-
const thumbnailFiles = readdirSync(thumbnailsPath);
|
|
41
|
-
expect(thumbnailFiles).toHaveLength(expectedThumbnailCount);
|
|
42
|
-
// Read and validate gallery.json with thumbnails
|
|
43
|
-
const galleryJsonPath = path.resolve(galleryPath, 'gallery.json');
|
|
44
|
-
const galleryContent = readFileSync(galleryJsonPath, 'utf8');
|
|
45
|
-
const galleryData = JSON.parse(galleryContent);
|
|
46
|
-
const validatedData = GalleryDataSchema.parse(galleryData);
|
|
47
|
-
// Validate all images have thumbnails
|
|
48
|
-
for (const image of validatedData.sections[0].images) {
|
|
49
|
-
expect(() => MediaFileWithThumbnailSchema.parse(image)).not.toThrow();
|
|
50
|
-
}
|
|
51
|
-
return validatedData;
|
|
52
|
-
}
|
|
53
|
-
function validateBuildOutput(testPath, galleryPath) {
|
|
54
|
-
// Check that index.html exists
|
|
55
|
-
const indexPath = path.resolve(testPath, 'index.html');
|
|
56
|
-
expect(existsSync(indexPath)).toBe(true);
|
|
57
|
-
// Check that gallery files still exist
|
|
58
|
-
const galleryJsonPath = path.resolve(galleryPath, 'gallery.json');
|
|
59
|
-
const galleryFiles = readdirSync(galleryPath);
|
|
60
|
-
expect(galleryFiles).toContain('thumbnails');
|
|
61
|
-
expect(galleryFiles).toContain('gallery.json');
|
|
62
|
-
// Validate gallery.json is still valid
|
|
63
|
-
const galleryContent = readFileSync(galleryJsonPath, 'utf8');
|
|
64
|
-
const galleryData = JSON.parse(galleryContent);
|
|
65
|
-
expect(() => GalleryDataSchema.parse(galleryData)).not.toThrow();
|
|
66
|
-
}
|
|
67
|
-
describe('Single-folder gallery', () => {
|
|
68
|
-
beforeAll(() => {
|
|
69
|
-
if (existsSync(singleTestPath)) {
|
|
70
|
-
rmSync(singleTestPath, { recursive: true, force: true });
|
|
71
|
-
}
|
|
72
|
-
copySync(singleFixturePath, singleTestPath);
|
|
73
|
-
});
|
|
74
|
-
afterAll(() => {
|
|
75
|
-
if (existsSync(singleTestPath)) {
|
|
76
|
-
rmSync(singleTestPath, { recursive: true, force: true });
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
describe('init command', () => {
|
|
80
|
-
test('should create gallery.json with correct structure and content', () => {
|
|
81
|
-
const galleryPath = path.resolve(singleTestPath, 'gallery');
|
|
82
|
-
// Run init command
|
|
83
|
-
execSync(`${tsxPath} ${cliPath} init --path ${singleTestPath}`);
|
|
84
|
-
// Validate gallery structure
|
|
85
|
-
validateGalleryStructure(galleryPath, 3, 0);
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
describe('thumbnails command', () => {
|
|
89
|
-
test('should create thumbnails for all images and update gallery.json', () => {
|
|
90
|
-
const galleryPath = path.resolve(singleTestPath, 'gallery');
|
|
91
|
-
// Run thumbnails command (init should have been run by previous test)
|
|
92
|
-
execSync(`${tsxPath} ${cliPath} thumbnails --path ${singleTestPath}`);
|
|
93
|
-
// Validate thumbnails using helper
|
|
94
|
-
validateThumbnails(galleryPath, 3);
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
describe('build command', () => {
|
|
98
|
-
test('should create static HTML files and gallery assets', () => {
|
|
99
|
-
const galleryPath = path.resolve(singleTestPath, 'gallery');
|
|
100
|
-
// Run build command (init and thumbnails should have been run by previous tests)
|
|
101
|
-
execSync(`${tsxPath} ${cliPath} build --path ${singleTestPath}`);
|
|
102
|
-
// Validate build output using helper
|
|
103
|
-
validateBuildOutput(singleTestPath, galleryPath);
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
describe('Multi-folder gallery', () => {
|
|
108
|
-
beforeAll(() => {
|
|
109
|
-
if (existsSync(multiTestPath)) {
|
|
110
|
-
rmSync(multiTestPath, { recursive: true, force: true });
|
|
111
|
-
}
|
|
112
|
-
copySync(multiFixturePath, multiTestPath);
|
|
113
|
-
});
|
|
114
|
-
afterAll(() => {
|
|
115
|
-
if (existsSync(multiTestPath)) {
|
|
116
|
-
rmSync(multiTestPath, { recursive: true, force: true });
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
describe('init command with recursive option', () => {
|
|
120
|
-
test('should create gallery.json files with subgalleries for multi-folder structure', () => {
|
|
121
|
-
// Run init command with recursive option
|
|
122
|
-
execSync(`${tsxPath} ${cliPath} init --path ${multiTestPath} -r`);
|
|
123
|
-
// Validate main gallery (3 root images + 2 subgalleries)
|
|
124
|
-
const mainGalleryPath = path.resolve(multiTestPath, 'gallery');
|
|
125
|
-
const mainValidatedData = validateGalleryStructure(mainGalleryPath, 3, 2);
|
|
126
|
-
// Check subgalleries metadata
|
|
127
|
-
expect(mainValidatedData.subGalleries.galleries).toHaveLength(2);
|
|
128
|
-
const subGalleryTitles = mainValidatedData.subGalleries.galleries.map((sg) => sg.title);
|
|
129
|
-
expect(subGalleryTitles).toContain('First');
|
|
130
|
-
expect(subGalleryTitles).toContain('Second');
|
|
131
|
-
// Validate first subgallery (2 images)
|
|
132
|
-
const firstGalleryPath = path.resolve(multiTestPath, 'first', 'gallery');
|
|
133
|
-
validateGalleryStructure(firstGalleryPath, 2, 0);
|
|
134
|
-
// Validate second subgallery (2 images)
|
|
135
|
-
const secondGalleryPath = path.resolve(multiTestPath, 'second', 'gallery');
|
|
136
|
-
validateGalleryStructure(secondGalleryPath, 2, 0);
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
describe('thumbnails command with recursive option', () => {
|
|
140
|
-
test('should create thumbnails for all galleries recursively', () => {
|
|
141
|
-
// Run thumbnails command with recursive option
|
|
142
|
-
execSync(`${tsxPath} ${cliPath} thumbnails --path ${multiTestPath} -r`);
|
|
143
|
-
// Validate thumbnails for main gallery
|
|
144
|
-
const mainGalleryPath = path.resolve(multiTestPath, 'gallery');
|
|
145
|
-
validateThumbnails(mainGalleryPath, 3);
|
|
146
|
-
// Validate thumbnails for first subgallery
|
|
147
|
-
const firstGalleryPath = path.resolve(multiTestPath, 'first', 'gallery');
|
|
148
|
-
validateThumbnails(firstGalleryPath, 2);
|
|
149
|
-
// Validate thumbnails for second subgallery
|
|
150
|
-
const secondGalleryPath = path.resolve(multiTestPath, 'second', 'gallery');
|
|
151
|
-
validateThumbnails(secondGalleryPath, 2);
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
describe('build command with recursive option', () => {
|
|
155
|
-
test('should build static files for all galleries recursively', () => {
|
|
156
|
-
// Run build command with recursive option
|
|
157
|
-
execSync(`${tsxPath} ${cliPath} build --path ${multiTestPath} -r`);
|
|
158
|
-
// Validate build output for main gallery
|
|
159
|
-
const mainGalleryPath = path.resolve(multiTestPath, 'gallery');
|
|
160
|
-
validateBuildOutput(multiTestPath, mainGalleryPath);
|
|
161
|
-
// Validate build output for subgalleries
|
|
162
|
-
const firstGalleryPath = path.resolve(multiTestPath, 'first', 'gallery');
|
|
163
|
-
const firstTestPath = path.resolve(multiTestPath, 'first');
|
|
164
|
-
validateBuildOutput(firstTestPath, firstGalleryPath);
|
|
165
|
-
const secondGalleryPath = path.resolve(multiTestPath, 'second', 'gallery');
|
|
166
|
-
const secondTestPath = path.resolve(multiTestPath, 'second');
|
|
167
|
-
validateBuildOutput(secondTestPath, secondGalleryPath);
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
});
|
package/src/index.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import process from 'node:process';
|
|
4
|
-
|
|
5
|
-
import { Command } from 'commander';
|
|
6
|
-
|
|
7
|
-
import { build } from './modules/build';
|
|
8
|
-
import { init } from './modules/init';
|
|
9
|
-
import { thumbnails } from './modules/thumbnails';
|
|
10
|
-
|
|
11
|
-
const program = new Command();
|
|
12
|
-
|
|
13
|
-
program.name('gallery').description('Simple Photo Gallery CLI').version('0.0.1');
|
|
14
|
-
|
|
15
|
-
program
|
|
16
|
-
.command('init')
|
|
17
|
-
.description('Initialize a gallery by scaning a folder for images and videos')
|
|
18
|
-
.option(
|
|
19
|
-
'-p, --path <path>',
|
|
20
|
-
'Path where the gallery should be initialized. Default: current working directory',
|
|
21
|
-
process.cwd(),
|
|
22
|
-
)
|
|
23
|
-
.option('-o, --output <path>', 'Output directory for the gallery.json file', '')
|
|
24
|
-
.option('-r, --recursive', 'Scan subdirectories recursively', false)
|
|
25
|
-
.action(init);
|
|
26
|
-
|
|
27
|
-
program
|
|
28
|
-
.command('thumbnails')
|
|
29
|
-
.description('Create thumbnails for all media files in the gallery')
|
|
30
|
-
.option(
|
|
31
|
-
'-p, --path <path>',
|
|
32
|
-
'Path to the folder containing the gallery.json file. Default: current working directory',
|
|
33
|
-
process.cwd(),
|
|
34
|
-
)
|
|
35
|
-
.option('-s, --size <size>', 'Thumbnail height in pixels', '200')
|
|
36
|
-
.option('-r, --recursive', 'Scan subdirectories recursively', false)
|
|
37
|
-
.action(thumbnails);
|
|
38
|
-
|
|
39
|
-
program
|
|
40
|
-
.command('build')
|
|
41
|
-
.description('Build the HTML gallery in the specified directory')
|
|
42
|
-
.option(
|
|
43
|
-
'-p, --path <path>',
|
|
44
|
-
'Path to the folder containing the gallery.json file. Default: current working directory',
|
|
45
|
-
process.cwd(),
|
|
46
|
-
)
|
|
47
|
-
.option('-r, --recursive', 'Scan subdirectories recursively', false)
|
|
48
|
-
.action(build);
|
|
49
|
-
|
|
50
|
-
program.parse();
|
|
@@ -1,68 +0,0 @@
|
|
|
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
|
-
|
|
6
|
-
import { findGalleries } from '../../utils';
|
|
7
|
-
|
|
8
|
-
import type { BuildOptions } from './types';
|
|
9
|
-
|
|
10
|
-
function buildGallery(galleryDir: string, templateDir: string) {
|
|
11
|
-
// Make sure the gallery.json file exists
|
|
12
|
-
const galleryJsonPath = path.join(galleryDir, 'gallery', 'gallery.json');
|
|
13
|
-
if (!fs.existsSync(galleryJsonPath)) {
|
|
14
|
-
console.log(`No gallery/gallery.json found in ${galleryDir}`);
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Build the template
|
|
19
|
-
const originalEnv = { ...process.env };
|
|
20
|
-
try {
|
|
21
|
-
// Set the environment variable for the gallery.json path that will be used by the template
|
|
22
|
-
process.env.GALLERY_JSON_PATH = galleryJsonPath;
|
|
23
|
-
process.env.GALLERY_OUTPUT_DIR = path.join(galleryDir, 'gallery');
|
|
24
|
-
|
|
25
|
-
execSync('yarn build', { cwd: templateDir, stdio: 'inherit' });
|
|
26
|
-
} catch (error) {
|
|
27
|
-
console.error(error);
|
|
28
|
-
console.error(`Build failed for ${galleryDir}`);
|
|
29
|
-
return;
|
|
30
|
-
} finally {
|
|
31
|
-
// Restore original environment and gallery.json
|
|
32
|
-
process.env = originalEnv;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Copy the build output to the output directory
|
|
36
|
-
const outputDir = path.join(galleryDir, 'gallery');
|
|
37
|
-
const buildDir = path.join(outputDir, '_build');
|
|
38
|
-
fs.cpSync(buildDir, outputDir, { recursive: true });
|
|
39
|
-
|
|
40
|
-
// Move the index.html to the gallery directory
|
|
41
|
-
fs.copyFileSync(path.join(outputDir, 'index.html'), path.join(galleryDir, 'index.html'));
|
|
42
|
-
fs.rmSync(path.join(outputDir, 'index.html'));
|
|
43
|
-
|
|
44
|
-
// Clean up the _build directory
|
|
45
|
-
console.log('Cleaning up build directory...');
|
|
46
|
-
fs.rmSync(buildDir, { recursive: true, force: true });
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export async function build(options: BuildOptions): Promise<void> {
|
|
50
|
-
// Get the template directory
|
|
51
|
-
// Resolve the theme-modern package directory
|
|
52
|
-
const themePath = await import.meta.resolve('@simple-photo-gallery/theme-modern/package.json');
|
|
53
|
-
const themeDir = path.dirname(new URL(themePath).pathname);
|
|
54
|
-
|
|
55
|
-
// Find all gallery directories
|
|
56
|
-
const galleryDirs = findGalleries(options.path, options.recursive);
|
|
57
|
-
|
|
58
|
-
// If no galleries are found, exit
|
|
59
|
-
if (galleryDirs.length === 0) {
|
|
60
|
-
console.log('No gallery/gallery.json files found.');
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Process each gallery
|
|
65
|
-
for (const dir of galleryDirs) {
|
|
66
|
-
buildGallery(path.resolve(dir), themeDir);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
|
|
3
|
-
// __dirname workaround for ESM modules
|
|
4
|
-
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
|
5
|
-
|
|
6
|
-
// Helper function to resolve paths relative to current file
|
|
7
|
-
export const resolveFromCurrentDir = (...segments: string[]): string => {
|
|
8
|
-
return path.resolve(__dirname, ...segments);
|
|
9
|
-
};
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
// Image extensions
|
|
2
|
-
export const IMAGE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.tiff', '.tif', '.svg']);
|
|
3
|
-
|
|
4
|
-
// Video extensions
|
|
5
|
-
export const VIDEO_EXTENSIONS = new Set(['.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm', '.mkv', '.m4v', '.3gp']);
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
import { capitalizeTitle, getImageMetadata, getVideoDimensions, isMediaFile } from './utils';
|
|
5
|
-
|
|
6
|
-
import type { ProcessDirectoryResult, ScanOptions, SubGallery } from './types';
|
|
7
|
-
import type { MediaFile } from '../../types';
|
|
8
|
-
|
|
9
|
-
async function scanDirectory(dirPath: string): Promise<MediaFile[]> {
|
|
10
|
-
const mediaFiles: MediaFile[] = [];
|
|
11
|
-
|
|
12
|
-
try {
|
|
13
|
-
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
14
|
-
|
|
15
|
-
for (const entry of entries) {
|
|
16
|
-
if (entry.isFile()) {
|
|
17
|
-
const fullPath = path.join(dirPath, entry.name);
|
|
18
|
-
const mediaType = isMediaFile(entry.name);
|
|
19
|
-
if (mediaType) {
|
|
20
|
-
console.log(`Processing ${mediaType}: ${entry.name}`);
|
|
21
|
-
|
|
22
|
-
let metadata: { width: number; height: number; description?: string } = { width: 0, height: 0 };
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
if (mediaType === 'image') {
|
|
26
|
-
metadata = await getImageMetadata(fullPath);
|
|
27
|
-
} else if (mediaType === 'video') {
|
|
28
|
-
try {
|
|
29
|
-
const videoDimensions = await getVideoDimensions(fullPath);
|
|
30
|
-
metadata = { ...videoDimensions };
|
|
31
|
-
} catch (videoError: unknown) {
|
|
32
|
-
if (
|
|
33
|
-
typeof videoError === 'object' &&
|
|
34
|
-
videoError !== null &&
|
|
35
|
-
'message' in videoError &&
|
|
36
|
-
typeof (videoError as { message: string }).message === 'string' &&
|
|
37
|
-
((videoError as { message: string }).message.includes('ffprobe') ||
|
|
38
|
-
(videoError as { message: string }).message.includes('ffmpeg'))
|
|
39
|
-
) {
|
|
40
|
-
console.error(
|
|
41
|
-
`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}`,
|
|
42
|
-
);
|
|
43
|
-
} else {
|
|
44
|
-
console.error(`Error processing video ${entry.name}:`, videoError);
|
|
45
|
-
}
|
|
46
|
-
continue; // Skip this file
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
} catch (mediaError) {
|
|
50
|
-
console.error(`Error processing file ${entry.name}:`, mediaError);
|
|
51
|
-
continue; // Skip this file
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const mediaFile: MediaFile = {
|
|
55
|
-
type: mediaType,
|
|
56
|
-
path: fullPath,
|
|
57
|
-
width: metadata.width,
|
|
58
|
-
height: metadata.height,
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
if (metadata.description) {
|
|
62
|
-
mediaFile.alt = metadata.description;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
mediaFiles.push(mediaFile);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
} catch (error) {
|
|
70
|
-
console.error(`Error scanning directory ${dirPath}:`, error);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return mediaFiles;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async function createGalleryJson(
|
|
77
|
-
mediaFiles: MediaFile[],
|
|
78
|
-
galleryJsonPath: string,
|
|
79
|
-
subGalleries: SubGallery[] = [],
|
|
80
|
-
): Promise<void> {
|
|
81
|
-
const galleryDir = path.dirname(galleryJsonPath);
|
|
82
|
-
|
|
83
|
-
// Convert media file paths to be relative to gallery.json
|
|
84
|
-
const relativeMediaFiles = mediaFiles.map((file) => ({
|
|
85
|
-
...file,
|
|
86
|
-
path: path.relative(galleryDir, file.path),
|
|
87
|
-
}));
|
|
88
|
-
|
|
89
|
-
// Convert subGallery header image paths to be relative to gallery.json
|
|
90
|
-
const relativeSubGalleries = subGalleries.map((subGallery) => ({
|
|
91
|
-
...subGallery,
|
|
92
|
-
headerImage: subGallery.headerImage ? path.relative(galleryDir, subGallery.headerImage) : '',
|
|
93
|
-
}));
|
|
94
|
-
|
|
95
|
-
const galleryData = {
|
|
96
|
-
title: 'My Gallery',
|
|
97
|
-
description: 'My gallery with fantastic photos.',
|
|
98
|
-
headerImage: relativeMediaFiles[0]?.path || '',
|
|
99
|
-
metadata: { ogUrl: '' },
|
|
100
|
-
sections: [
|
|
101
|
-
{
|
|
102
|
-
images: relativeMediaFiles,
|
|
103
|
-
},
|
|
104
|
-
],
|
|
105
|
-
subGalleries: {
|
|
106
|
-
title: 'Sub Galleries',
|
|
107
|
-
galleries: relativeSubGalleries,
|
|
108
|
-
},
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
await fs.writeFile(galleryJsonPath, JSON.stringify(galleryData, null, 2));
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async function processDirectory(dirPath: string, options: ScanOptions): Promise<ProcessDirectoryResult> {
|
|
115
|
-
let totalFiles = 0;
|
|
116
|
-
const subGalleries: SubGallery[] = [];
|
|
117
|
-
|
|
118
|
-
// Scan current directory for media files
|
|
119
|
-
const mediaFiles = await scanDirectory(dirPath);
|
|
120
|
-
totalFiles += mediaFiles.length;
|
|
121
|
-
|
|
122
|
-
// Process subdirectories only if recursive mode is enabled
|
|
123
|
-
if (options.recursive) {
|
|
124
|
-
try {
|
|
125
|
-
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
126
|
-
|
|
127
|
-
for (const entry of entries) {
|
|
128
|
-
if (entry.isDirectory() && entry.name !== 'gallery') {
|
|
129
|
-
const subDirPath = path.join(dirPath, entry.name);
|
|
130
|
-
const result = await processDirectory(subDirPath, options);
|
|
131
|
-
totalFiles += result.totalFiles;
|
|
132
|
-
|
|
133
|
-
// If the subdirectory had media files, add it as a subGallery
|
|
134
|
-
if (result.subGallery) {
|
|
135
|
-
subGalleries.push(result.subGallery);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
} catch (error) {
|
|
140
|
-
console.error(`Error reading directory ${dirPath}:`, error);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Create gallery.json if there are media files or subGalleries
|
|
145
|
-
if (mediaFiles.length > 0 || subGalleries.length > 0) {
|
|
146
|
-
const outputPath = options.output
|
|
147
|
-
? path.resolve(options.output, path.relative(path.resolve(options.path), dirPath), 'gallery')
|
|
148
|
-
: path.join(dirPath, 'gallery');
|
|
149
|
-
const galleryJsonPath = path.join(outputPath, 'gallery.json');
|
|
150
|
-
|
|
151
|
-
// Create output directory
|
|
152
|
-
await fs.mkdir(outputPath, { recursive: true });
|
|
153
|
-
|
|
154
|
-
// Create gallery.json for this directory
|
|
155
|
-
await createGalleryJson(mediaFiles, galleryJsonPath, subGalleries);
|
|
156
|
-
|
|
157
|
-
console.log(
|
|
158
|
-
`Gallery JSON created at: ${galleryJsonPath} (${mediaFiles.length} files, ${subGalleries.length} subgalleries)`,
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Return result with suGgallery info if this directory has media files
|
|
163
|
-
const result: ProcessDirectoryResult = { totalFiles };
|
|
164
|
-
|
|
165
|
-
if (mediaFiles.length > 0 || subGalleries.length > 0) {
|
|
166
|
-
const dirName = path.basename(dirPath);
|
|
167
|
-
result.subGallery = {
|
|
168
|
-
title: capitalizeTitle(dirName),
|
|
169
|
-
headerImage: mediaFiles[0]?.path || '',
|
|
170
|
-
path: path.join('..', dirName),
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return result;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export async function init(options: ScanOptions): Promise<void> {
|
|
178
|
-
const scanPath = path.resolve(options.path);
|
|
179
|
-
|
|
180
|
-
console.log(`Scanning directory: ${scanPath}`);
|
|
181
|
-
console.log(`Recursive: ${options.recursive}`);
|
|
182
|
-
|
|
183
|
-
try {
|
|
184
|
-
// Ensure scan path exists
|
|
185
|
-
await fs.access(scanPath);
|
|
186
|
-
|
|
187
|
-
// Process the directory tree with the specified recursion setting
|
|
188
|
-
const result = await processDirectory(scanPath, options);
|
|
189
|
-
console.log(`Total files processed: ${result.totalFiles}`);
|
|
190
|
-
} catch (error) {
|
|
191
|
-
throw new Error(`Error during scan: ${error}`);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export interface ScanOptions {
|
|
2
|
-
path: string;
|
|
3
|
-
output: string;
|
|
4
|
-
recursive: boolean;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export interface SubGallery {
|
|
8
|
-
title: string;
|
|
9
|
-
headerImage: string;
|
|
10
|
-
path: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface ProcessDirectoryResult {
|
|
14
|
-
totalFiles: number;
|
|
15
|
-
subGallery?: SubGallery;
|
|
16
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
|
|
3
|
-
declare module 'node-ffprobe' {
|
|
4
|
-
export interface Stream {
|
|
5
|
-
codec_type: string;
|
|
6
|
-
width?: number;
|
|
7
|
-
height?: number;
|
|
8
|
-
[key: string]: any;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface ProbeResult {
|
|
12
|
-
streams: Stream[];
|
|
13
|
-
format: any;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export default function ffprobe(filePath: string): Promise<ProbeResult>;
|
|
17
|
-
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
import exifReader from 'exif-reader';
|
|
5
|
-
import ffprobe from 'node-ffprobe';
|
|
6
|
-
import sharp from 'sharp';
|
|
7
|
-
|
|
8
|
-
import { IMAGE_EXTENSIONS, VIDEO_EXTENSIONS } from '../const';
|
|
9
|
-
|
|
10
|
-
export async function getImageMetadata(filePath: string): Promise<{ width: number; height: number; description?: string }> {
|
|
11
|
-
try {
|
|
12
|
-
const metadata = await sharp(filePath).metadata();
|
|
13
|
-
let description: string | undefined;
|
|
14
|
-
|
|
15
|
-
// Extract description from EXIF data
|
|
16
|
-
if (metadata.exif) {
|
|
17
|
-
try {
|
|
18
|
-
const exifData = exifReader(metadata.exif);
|
|
19
|
-
if (exifData.Image?.ImageDescription) {
|
|
20
|
-
description = exifData.Image.ImageDescription;
|
|
21
|
-
} else if (exifData.Image?.Description) {
|
|
22
|
-
description = exifData.Image.Description.toString();
|
|
23
|
-
}
|
|
24
|
-
} catch {
|
|
25
|
-
// EXIF parsing failed, but that's OK
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return {
|
|
30
|
-
width: metadata.width || 0,
|
|
31
|
-
height: metadata.height || 0,
|
|
32
|
-
description,
|
|
33
|
-
};
|
|
34
|
-
} catch (error) {
|
|
35
|
-
console.warn(`Warning: Could not get metadata for image ${filePath}:`, error);
|
|
36
|
-
return { width: 0, height: 0 };
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export async function getVideoDimensions(filePath: string): Promise<{ width: number; height: number }> {
|
|
41
|
-
try {
|
|
42
|
-
const data = await ffprobe(filePath);
|
|
43
|
-
const videoStream = data.streams.find((stream) => stream.codec_type === 'video');
|
|
44
|
-
if (videoStream) {
|
|
45
|
-
return {
|
|
46
|
-
width: videoStream.width || 0,
|
|
47
|
-
height: videoStream.height || 0,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
return { width: 0, height: 0 };
|
|
51
|
-
} catch (error) {
|
|
52
|
-
console.warn(`Warning: Could not get dimensions for video ${filePath}:`, error);
|
|
53
|
-
return { width: 0, height: 0 };
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function isMediaFile(fileName: string): 'image' | 'video' | null {
|
|
58
|
-
const ext = path.extname(fileName).toLowerCase();
|
|
59
|
-
if (IMAGE_EXTENSIONS.has(ext)) return 'image';
|
|
60
|
-
if (VIDEO_EXTENSIONS.has(ext)) return 'video';
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function capitalizeTitle(folderName: string): string {
|
|
65
|
-
return folderName
|
|
66
|
-
.replace('-', ' ')
|
|
67
|
-
.replace('_', ' ')
|
|
68
|
-
.split(' ')
|
|
69
|
-
.map((word: string) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
70
|
-
.join(' ');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export async function hasMediaFiles(dirPath: string): Promise<boolean> {
|
|
74
|
-
try {
|
|
75
|
-
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
76
|
-
|
|
77
|
-
// Check for media files in current directory
|
|
78
|
-
for (const entry of entries) {
|
|
79
|
-
if (entry.isFile() && isMediaFile(entry.name)) {
|
|
80
|
-
return true;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Check subdirectories recursively
|
|
85
|
-
for (const entry of entries) {
|
|
86
|
-
if (entry.isDirectory() && entry.name !== 'gallery') {
|
|
87
|
-
const subDirPath = path.join(dirPath, entry.name);
|
|
88
|
-
if (await hasMediaFiles(subDirPath)) {
|
|
89
|
-
return true;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
} catch (error) {
|
|
94
|
-
console.error(`Error checking for media files in ${dirPath}:`, error);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return false;
|
|
98
|
-
}
|