You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
thumbsup/src/cli/options.js

550 lines
15 KiB
JavaScript

/* eslint-disable quote-props */
const path = require('node:path')
const os = require('node:os')
const _ = require('lodash')
const yargs = require('yargs')
const messages = require('./messages')
const OPTIONS = {
// ------------------------------------
// Required arguments
// ------------------------------------
'input': {
group: 'Required:',
description: 'Path to the folder with all photos/videos',
type: 'string',
normalize: true,
demand: true
},
'output': {
group: 'Required:',
description: 'Output path for the static website',
type: 'string',
normalize: true,
demand: true
},
// ------------------------------------
// Input options
// ------------------------------------
'scan-mode': {
group: 'Input options:',
description: 'How files are indexed',
choices: ['full', 'partial', 'incremental'],
'default': 'full'
},
'include-photos': {
group: 'Input options:',
description: 'Include photos in the gallery',
type: 'boolean',
'default': true
},
'include-videos': {
group: 'Input options:',
description: 'Include videos in the gallery',
type: 'boolean',
'default': true
},
'include-raw-photos': {
group: 'Input options:',
description: 'Include raw photos in the gallery',
type: 'boolean',
'default': false
},
'include': {
group: 'Input options:',
description: 'Glob pattern of files to include',
type: 'array'
},
'exclude': {
group: 'Input options:',
description: 'Glob pattern of files to exclude',
type: 'array'
},
// ------------------------------------
// Output options
// ------------------------------------
'thumb-size': {
group: 'Output options:',
description: 'Pixel size of the square thumbnails',
type: 'number',
'default': 120
},
'small-size': {
group: 'Output options:',
description: 'Pixel height of the small photos',
type: 'number',
'default': 300
},
'large-size': {
group: 'Output options:',
description: 'Pixel height of the fullscreen photos',
type: 'number',
'default': 1000
},
'photo-quality': {
group: 'Output options:',
description: 'Quality of the resized/converted photos',
type: 'number',
'default': 90
},
'video-quality': {
group: 'Output options:',
description: 'Quality of the converted video (percent)',
type: 'number',
'default': 75
},
'video-bitrate': {
group: 'Output options:',
description: 'Bitrate of the converted videos (e.g. 120k)',
type: 'string',
'default': null
},
'video-format': {
group: 'Output options:',
description: 'Video output format',
choices: ['mp4', 'webm'],
'default': 'mp4'
},
'video-hwaccel': {
group: 'Output options:',
description: 'Use hardware acceleration (requires bitrate)',
choices: ['none', 'vaapi'],
'default': 'none'
},
'video-stills': {
group: 'Output options:',
description: 'Where the video still frame is taken',
choices: ['seek', 'middle'],
'default': 'seek'
},
'video-stills-seek': {
group: 'Output options:',
description: 'Number of seconds where the still frame is taken',
type: 'number',
'default': 1
},
'photo-preview': {
group: 'Output options:',
description: 'How lightbox photos are generated',
choices: ['resize', 'copy', 'symlink', 'link'],
'default': 'resize'
},
'video-preview': {
group: 'Output options:',
description: 'How lightbox videos are generated',
choices: ['resize', 'copy', 'symlink', 'link'],
'default': 'resize'
},
'photo-download': {
group: 'Output options:',
description: 'How downloadable photos are generated',
choices: ['resize', 'copy', 'symlink', 'link'],
'default': 'resize'
},
'video-download': {
group: 'Output options:',
description: 'How downloadable videos are generated',
choices: ['resize', 'copy', 'symlink', 'link'],
'default': 'resize'
},
'link-prefix': {
group: 'Output options:',
description: 'Path or URL prefix for "linked" photos and videos',
type: 'string'
},
'cleanup': {
group: 'Output options:',
description: 'Remove any output file that\'s no longer needed',
type: 'boolean',
'default': false
},
'concurrency': {
group: 'Output options:',
description: 'Number of parallel parsing/processing operations',
type: 'number',
'default': os.cpus().length
},
'output-structure': {
group: 'Output options:',
description: 'File and folder structure for output media',
choices: ['folders', 'suffix'],
'default': 'folders'
},
'gm-args': {
group: 'Output options:',
description: 'Custom image processing arguments for GraphicsMagick',
type: 'array'
},
'watermark': {
group: 'Output options:',
description: 'Path to a transparent PNG to be overlaid on all images',
type: 'string',
normalize: true
},
'watermark-position': {
group: 'Output options:',
description: 'Position of the watermark',
choices: [
'Repeat', 'Center', 'NorthWest', 'North', 'NorthEast',
'West', 'East', 'SouthWest', 'South', 'SouthEast'
]
},
// ------------------------------------
// Album options
// ------------------------------------
'albums-from': {
group: 'Album options:',
description: 'How files are grouped into albums',
type: 'array',
'default': ['%path']
},
'sort-albums-by': {
group: 'Album options:',
description: 'How to sort albums',
choices: ['title', 'start-date', 'end-date'],
coerce: commaSeparated,
'default': 'start-date'
},
'sort-albums-direction': {
group: 'Album options:',
description: 'Album sorting direction',
choices: ['asc', 'desc'],
coerce: commaSeparated,
'default': 'asc'
},
'sort-media-by': {
group: 'Album options:',
description: 'How to sort photos and videos',
choices: ['filename', 'date'],
coerce: commaSeparated,
'default': 'date'
},
'sort-media-direction': {
group: 'Album options:',
description: 'Media sorting direction',
choices: ['asc', 'desc'],
coerce: commaSeparated,
'default': 'asc'
},
'home-album-name': {
group: 'Album options:',
description: 'Name of the top-level album',
type: 'string',
'default': 'Home'
},
'album-page-size': {
group: 'Album options:',
description: 'Max number of files displayed on a page',
type: 'number',
'default': null
},
'album-zip-files': {
group: 'Album options:',
description: 'Create a ZIP file per album',
type: 'boolean',
'default': false
},
// 'keyword-fields': {
// group: 'Album options:',
// description: 'Where to look in the metadata data for keywords (for %keywords)',
// type: 'array'
// },
'include-keywords': {
group: 'Album options:',
description: 'Keywords to include in %keywords',
type: 'array'
},
'exclude-keywords': {
group: 'Album options:',
description: 'Keywords to exclude from %keywords',
type: 'array'
},
// 'people-fields': {
// group: 'Album options:',
// description: 'Where to look in the metadata data for people names (for %people)',
// type: 'array'
// },
'include-people': {
group: 'Album options:',
description: 'Names to include in %people',
type: 'array'
},
'exclude-people': {
group: 'Album options:',
description: 'Names to exclude from %people',
type: 'array'
},
'album-previews': {
group: 'Album options:',
description: 'How previews are selected',
choices: ['first', 'spread', 'random'],
'default': 'first'
},
// ------------------------------------
// Website options
// ------------------------------------
'index': {
group: 'Website options:',
description: 'Filename of the home page',
type: 'string',
'default': 'index.html'
},
'albums-output-folder': {
group: 'Website options:',
description: 'Output subfolder for HTML albums (default: website root)',
type: 'string',
'default': '.'
},
'theme': {
group: 'Website options:',
description: 'Name of a built-in gallery theme',
choices: ['classic', 'cards', 'mosaic', 'flow'],
'default': 'classic'
},
'theme-path': {
group: 'Website options:',
description: 'Path to a custom theme',
type: 'string',
normalize: true
},
'theme-style': {
group: 'Website options:',
description: 'Path to a custom LESS/CSS file for additional styles',
type: 'string',
normalize: true
},
'theme-settings': {
group: 'Website options:',
description: 'Path to a JSON file with theme settings',
type: 'string',
normalize: true
},
'title': {
group: 'Website options:',
description: 'Website title',
type: 'string',
'default': 'Photo album'
},
'footer': {
group: 'Website options:',
description: 'Text or HTML footer',
type: 'string',
'default': null
},
'google-analytics': {
group: 'Website options:',
description: 'Code for Google Analytics tracking',
type: 'string'
},
'embed-exif': {
group: 'Website options:',
description: 'Embed the exif metadata for each image into the gallery page',
type: 'boolean',
'default': false
},
'locale': {
group: 'Website options:',
description: 'Locale for regional settings like dates',
type: 'string',
'default': 'en'
},
'seo-location': {
group: 'Website options:',
description: 'Location where the site will be hosted. If provided, sitemap.xml and robots.txt will be created.',
type: 'string',
'default': null
},
// ------------------------------------
// Misc options
// ------------------------------------
'config': {
group: 'Misc options:',
description: 'JSON config file (one key per argument)',
normalize: true
},
'database-file': {
group: 'Misc options:',
description: 'Path to the database file',
normalize: true
},
'log-file': {
group: 'Misc options:',
description: 'Path to the log file',
normalize: true
},
'log': {
group: 'Misc options:',
description: 'Print a detailed text log',
choices: ['default', 'info', 'debug', 'trace'],
'default': 'default'
},
'dry-run': {
group: 'Misc options:',
description: "Update the index, but don't create the media files / website",
type: 'boolean',
'default': false
},
// ------------------------------------
// Deprecated options
// ------------------------------------
'original-photos': {
group: 'Deprecated:',
description: 'Copy and allow download of full-size photos',
type: 'boolean'
},
'original-videos': {
group: 'Deprecated:',
description: 'Copy and allow download of full-size videos',
type: 'boolean'
},
'albums-date-format': {
group: 'Deprecated:',
description: 'How albums are named in <date> mode [moment.js pattern]'
},
'css': {
group: 'Deprecated:',
description: 'Path to a custom provided CSS/LESS file for styling',
normalize: true
},
'download-photos': {
group: 'Deprecated:',
description: 'Target of the photo download links',
choices: ['large', 'copy', 'symlink', 'link']
},
'download-videos': {
group: 'Deprecated:',
description: 'Target of the video download links',
choices: ['large', 'copy', 'symlink', 'link']
},
'download-link-prefix': {
group: 'Deprecated:',
description: 'Path or URL prefix for linked downloads',
type: 'string'
},
'usage-stats': {
group: 'Deprecated:',
description: 'Enable anonymous usage statistics',
type: 'boolean'
}
}
// explicitly pass <process.argv> so we can unit test this logic
// otherwise it pre-loads all process arguments on require()
exports.get = (args, exitOnFailure = true) => {
const parsedOptions = yargs(args)
.usage(messages.USAGE())
.wrap(null)
.help('help')
.config('config')
.options(OPTIONS)
.epilogue(messages.CONFIG_USAGE())
.exitProcess(exitOnFailure)
.check(validation)
.argv
// Warn users when they use deprecated options
const deprecated = Object.keys(OPTIONS).filter(name => OPTIONS[name].group === 'Deprecated:')
const specified = deprecated.filter(name => typeof parsedOptions[name] !== 'undefined')
if (specified.length > 0) {
const warnings = specified.map(name => `Warning: --${name} is deprecated`)
console.error(warnings.join('\n') + '\n')
}
// Delete all options containing dashes, because yargs already aliases them as camelCase
// This means we can process the camelCase version only after that
const opts = _.omitBy(parsedOptions, (value, key) => key.indexOf('-') >= 0)
// Default database file
if (!opts.databaseFile) {
opts.databaseFile = path.join(opts.output, 'thumbsup.db')
}
// Default log file
if (!opts.logFile) {
opts.logFile = changeExtension(opts.databaseFile, '.log')
}
// Better to work with absolute paths
opts.input = path.resolve(opts.input)
opts.output = path.resolve(opts.output)
opts.databaseFile = path.resolve(opts.databaseFile)
opts.logFile = path.resolve(opts.logFile)
// By default, use relative links to the input folder
if (opts.downloadLinkPrefix) opts.linkPrefix = opts.downloadLinkPrefix
if (!opts.linkPrefix) {
opts.linkPrefix = path.relative(opts.output, opts.input)
}
// Convert deprecated --download
if (opts.originalPhotos) opts.downloadPhotos = 'copy'
if (opts.originalVideos) opts.downloadVideos = 'copy'
if (opts.downloadPhotos) opts.photoDownload = opts.downloadPhotos
if (opts.downloadVideos) opts.videoDownload = opts.downloadVideos
if (opts.photoDownload === 'large') opts.photoDownload = 'resize'
if (opts.videoDownload === 'large') opts.videoDownload = 'resize'
// Convert deprecated --albums-from
replaceInArray(opts.albumsFrom, 'folders', '%path')
replaceInArray(opts.albumsFrom, 'date', `{${opts.albumsDateFormat}}`)
// Convert deprecated --css
if (opts.css) opts.themeStyle = opts.css
// Add a dash prefix to any --gm-args value
// We can't specify the prefix on the CLI otherwise the parser thinks it's a thumbsup arg
if (opts.gmArgs) {
opts.gmArgs = opts.gmArgs.map(val => val.startsWith('+') ? val : `-${val}`)
}
return opts
}
function validation (opts) {
// all custom validation rules go here
// this way, they are reported the same way as invalid arguments
if (opts.videoHwaccel !== 'none' && !opts.videoBitrate) {
throw new Error('--video-hwaccel requires a value for --bitrate')
}
// everything is OK
return true
}
function replaceInArray (list, match, replacement) {
for (let i = 0; i < list.length; ++i) {
if (list[i] === match) {
list[i] = replacement
}
}
}
function commaSeparated (value) {
if (value.indexOf(',') === -1) return value
return value.split(',')
}
function changeExtension (file, ext) {
const originalExt = path.extname(file)
const filename = path.basename(file, originalExt)
return path.join(path.dirname(file), filename + ext)
}
/* eslint-enable quote-props */