Source: generator/index.js

const path = require('path');
const fs = require('fs-extra');
const { promisify } = require('util');
const { progress } = require('../helpers');
const { generateThumbnails } = require('./thumbnails');

const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
const copy = promisify(fs.copy);
const mkdir = promisify(fs.mkdir);
const ensureSymlink = promisify(fs.ensureSymlink);

/**
 * Create a directory if it doesn't exist yet.
 *
 * @param {string} path The path to the folder to create.
 */
async function mkdirIfNotExists(path) {
	if (!(await fs.exists(path))) await mkdir(path);
}

/**
 * Load the images and videos for a given album and return the total count of media in that folder.
 *
 * Once the media is loaded, it is cached within the Album class so it doesn't need to be
 * fetched from the database again.
 * The total count of media also includes subfolder.
 * Gets called recursively.
 *
 * @param {Folder} folder The folder to load the media for.
 * @return {int} The total count of media in that folder (including subfolders).
 */
async function loadMedia(folder) {
	let mediaCount = 0;

	for (const album of folder.albums) {
		progress().increment(0, { album: album.name, action: 'loadMedia' });

		const media = await album.media();
		mediaCount += media.length;

		progress().increment(1);
	}

	for (const subfolder of folder.folders) {
		mediaCount += await loadMedia(subfolder);
	}

	return mediaCount;
}

/**
 * Process the images and videos for a given album and return the total count of media in that folder.
 *
 * If specified at launch, this will generate the thumbnails for the images.
 *
 * @param {Folder} folder The folder to process the media from.
 * @param {Object} options Options for processing.
 * @param {Bool} options.thumbnails Wether to generate thumbnails or not.
 * @param {string} options.albumsDir The directory to put the album JSON files into.
 * @param {string} options.thumbnailsDir The directory to put the thumbnails into.
 */
async function processMedia(
	folder,
	options = { albumsDir: '', thumbnailsDir: '', thumbnails: false }
) {
	for (const album of folder.albums) {
		const media = await album.media();

		progress().increment(0, {
			album: `${album.name} (${media.length})`,
			action: 'writeAlbumJSON'
		});

		await writeFile(
			`${options.albumsDir}/${album.uuid}.json`,
			JSON.stringify(media)
		);

		if (options.thumbnails) {
			progress().increment(0, {
				album: `${album.name} (${media.length})`,
				action: 'generateThumbnail'
			});

			await generateThumbnails(
				options.thumbnailsDir,
				await album.media()
			);
		}
	}

	for (const subfolder of folder.folders) {
		await processMedia(subfolder, options);
	}
}

/**
 * Generate the static web application for a given folder structure.
 * @param {string} outputFolder The output folder to put the web app in.
 * @param {string} libraryPath The path to the .photosLibrary folder.
 * @param {Array.<Folder>} folders The folder structure to process.
 * @param {Object} options The options for generation.
 * @param {Object} options.shouldGenerateThumbnails - Wether thumbnails should be generated or not.
 */
module.exports = async function generateWebApp(
	outputFolder,
	libraryPath,
	folders,
	options = { shouldGenerateThumbnails: false }
) {
	progress().update(0, { action: 'generateSkeleton' });

	// Resolve the output folder using the current working directory.
	// Useful so the user can specify the output folder easier.
	const outputPath = path.resolve(process.cwd(), outputFolder);

	// Copy the HTML template folder to the output folder
	await copy(path.resolve(__dirname, '../../template/dist/index.html'), outputPath + '/index.html');
	await copy(path.resolve(__dirname, '../../template/dist/index.css'), outputPath + '/index.css');
	await copy(path.resolve(__dirname, '../../template/dist/index.js'), outputPath + '/index.js');
	await copy(path.resolve(__dirname, '../../template/dist/assets'), outputPath + '/assets');

	// If the /data/ folder doesn't exist yet, create it
	const dataDir = path.resolve(outputPath, 'data');
	await mkdirIfNotExists(dataDir);

	// Write the folder structure json file.
	await writeFile(dataDir + '/folders.json', JSON.stringify(folders));

	// If the /albums/ folder doesn't exist yet, create it
	const albumsDir = path.resolve(dataDir, 'albums');
	await mkdirIfNotExists(albumsDir);

	// If the /thumbnails/ folder doesn't exist yet, create it
	const thumbnailsDir = path.resolve(dataDir, 'thumbnails');
	await mkdirIfNotExists(thumbnailsDir);

	// Ensure a symlink from the library to the output direcroty exists
	await ensureSymlink(path.resolve(libraryPath, 'Masters'), path.resolve(dataDir, 'media'));

	// Total Count of albums to be processed.
	const albumCount = folders.reduce((total, folder) => {
		return total + folder.countAlbums();
	}, 0);

	// Set the total progress to the album count so we can see them loading
	progress().update(0, { action: 'loadMedia', album: '' });
	progress().setTotal(albumCount);

	// Load & count Media
	let totalMediaCount = 0;
	for (const folder of folders) {
		totalMediaCount += await loadMedia(folder);
	}

	// Update progress bar to process media
	progress().update(0, { action: 'processMedia', album: '' });
	progress().setTotal(totalMediaCount);

	// Process media & generate thumbnails (if enabled)
	for (const folder of folders) {
		await processMedia(folder, {
			thumbnails: options.shouldGenerateThumbnails,
			thumbnailsDir,
			albumsDir
		});
	}
};