feat(options): new flags to selectively include photos / videos / raw photos

fixes #128
pull/143/head
Romain 5 years ago
parent a1ebe53d70
commit f30eddb6dd

@ -55,6 +55,7 @@ Thumbsup requires the following dependencies:
And optionally:
- [FFmpeg](http://www.ffmpeg.org/) to process videos: `brew install ffmpeg`
- [Gifsicle](http://www.lcdf.org/gifsicle/) to process animated GIFs: `brew install gifsicle`
- [dcraw](https://www.cybercom.net/~dcoffin/dcraw/) to process RAW photos: `brew install dcraw`
You can run thumbsup as a Docker container ([thumbsupgallery/thumbsup](https://hub.docker.com/r/thumbsupgallery/thumbsup/)) which pre-packages all the dependencies above. Read the [thumbsup on Docker](https://thumbsup.github.io/docs/2-installation/docker/) documentation for more detail.
@ -86,6 +87,11 @@ Required:
--input Path to the folder with all photos/videos [string] [required]
--output Output path for the static website [string] [required]
Input options:
--include-photos Include photos in the gallery [boolean] [default: true]
--include-videos Include videos in the gallery [boolean] [default: true]
--include-raw-photos Include raw photos in the gallery [boolean] [default: false]
Output options:
--thumb-size Pixel size of the square thumbnails [number] [default: 120]
--large-size Pixel height of the fullscreen photos [number] [default: 1000]
@ -120,6 +126,12 @@ Website options:
--google-analytics Code for Google Analytics tracking [string]
--embed-exif Embed the exif metadata for each image into the gallery page [boolean] [default: false]
Misc options:
--config JSON config file (one key per argument) [string]
--log Print a detailed text log [choices: null, "info", "debug", "trace"] [default: null]
--usage-stats Enable anonymous usage statistics [boolean] [default: true]
--dry-run Update the index, but don't create the media files / website [boolean] [default: false]
Deprecated:
--original-photos Copy and allow download of full-size photos [boolean] [default: false]
--original-videos Copy and allow download of full-size videos [boolean] [default: false]
@ -127,12 +139,8 @@ Deprecated:
--css Path to a custom provided CSS/LESS file for styling [string]
Options:
--version Show version number [boolean]
--help Show help [boolean]
--config JSON config file (one key per argument) [string]
--log Print a detailed text log [choices: null, "info", "debug", "trace"] [default: null]
--usage-stats Enable anonymous usage statistics [boolean] [default: true]
--dry-run Update the index, but don't create the media files / website [boolean] [default: false]
--version Show version number [boolean]
--help Show help [boolean]
The optional JSON config should contain a single object with one key

@ -31,6 +31,13 @@ const BINARIES = [
cmd: 'gifsicle',
url: 'http://www.lcdf.org/gifsicle',
msg: 'You will not be able to process animated GIFs.'
},
{
// optional to process RAW photos
mandatory: false,
cmd: 'dcraw',
url: 'https://www.cybercom.net/~dcoffin/dcraw/',
msg: 'You will not be able to process RAW photos.'
}
]

@ -22,6 +22,28 @@ const OPTIONS = {
demand: true
},
// ------------------------------------
// Input options
// ------------------------------------
'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
},
// ------------------------------------
// Output options
// ------------------------------------
@ -295,6 +317,9 @@ exports.get = (args) => {
return {
input: opts['input'],
output: opts['output'],
includePhotos: opts['include-photos'],
includeVideos: opts['include-videos'],
includeRawPhotos: opts['include-raw-photos'],
cleanup: opts['cleanup'],
title: opts['title'],
thumbSize: opts['thumb-size'],

@ -9,7 +9,12 @@ const Index = require('./index/index')
// index the contents of a folder
const index = new Index('thumbsup.db')
const emitter = index.update('/Volumes/photos')
const emitter = index.update('/Volumes/photos', {
concurrency: 2,
includePhotos: true,
includeVideos: true,
includeRawPhotos: false,
})
// indexing stats
// this happens before any new files are parsed for metadata

@ -4,16 +4,20 @@ const warn = require('debug')('thumbsup:warn')
const PHOTO_EXT = ['bmp', 'gif', 'jpg', 'jpeg', 'png', 'tif', 'tiff', 'webp']
const VIDEO_EXT = ['3gp', 'flv', 'm2ts', 'm4v', 'mkv', 'mp4', 'mov', 'mts', 'ogg', 'ogv', 'webm']
const MEDIA_GLOB = '**/*.{' + PHOTO_EXT.join(',') + ',' + VIDEO_EXT.join(',') + '}'
const RAW_PHOTO_EXT = [
'3fr', 'arw', 'cr2', 'crw', 'dcr', 'dng', 'erf', 'k25', 'kdc',
'mef', 'mrw', 'nef', 'orf', 'pef', 'raf', 'sr2', 'srf', 'x3f'
]
/*
Return a hashmap of {path, timestamp}
for all the media files in the target folder
*/
exports.find = function (rootFolder, callback) {
exports.find = function (rootFolder, options, callback) {
const entries = {}
const pattern = exports.globPattern(options)
const stream = readdir.readdirStreamStat(rootFolder, {
filter: entry => micromatch.match(entry.path, MEDIA_GLOB, { nocase: true }).length !== 0,
filter: entry => micromatch.match(entry.path, pattern, { nocase: true }).length !== 0,
deep: stats => canTraverse(stats.path),
basePath: '',
sep: '/'
@ -23,6 +27,14 @@ exports.find = function (rootFolder, callback) {
stream.on('end', () => callback(null, entries))
}
exports.globPattern = function (options) {
const extensions = []
if (options.includePhotos !== false) Array.prototype.push.apply(extensions, PHOTO_EXT)
if (options.includeVideos !== false) Array.prototype.push.apply(extensions, VIDEO_EXT)
if (options.includeRawPhotos) Array.prototype.push.apply(extensions, RAW_PHOTO_EXT)
return '**/*.{' + extensions.join(',') + '}'
}
function canTraverse (folder) {
// ignore folders starting with '.'
// and thumbnail folders from Synology NAS

@ -21,7 +21,7 @@ class Index {
/*
Index all the files in <media> and store into <database>
*/
update (mediaFolder, concurrency) {
update (mediaFolder, options = {}) {
// will emit many different events
const emitter = new EventEmitter()
@ -53,7 +53,7 @@ class Index {
}
// find all files on disk
globber.find(mediaFolder, (err, diskMap) => {
globber.find(mediaFolder, options, (err, diskMap) => {
if (err) return console.error('error', err)
// calculate the difference: which files have been added, modified, etc
@ -80,7 +80,7 @@ class Index {
// call <exiftool> on added and modified files
// and write each entry to the database
const stream = exiftool.parse(mediaFolder, toProcess, concurrency)
const stream = exiftool.parse(mediaFolder, toProcess, options.concurrency)
stream.on('data', entry => {
const timestamp = moment(entry.File.FileModifyDate, EXIF_DATE_FORMAT).valueOf()
insertStatement.run(entry.SourceFile, timestamp, JSON.stringify(entry))

@ -19,7 +19,7 @@ exports.run = function (opts, callback) {
return new Observable(observer => {
const picasaReader = new Picasa()
const index = new Index(path.join(opts.output, 'thumbsup.db'))
const emitter = index.update(opts.input, opts.concurrency)
const emitter = index.update(opts.input, opts)
const files = []
emitter.on('stats', stats => {

@ -31,7 +31,7 @@ describe('Index: glob', function () {
'media/IMG_0001.jpg': '...',
'media/IMG_0002.jpg': '...'
})
glob.find('media', (err, map) => {
glob.find('media', {}, (err, map) => {
if (err) return done(err)
const keys = Object.keys(map).sort()
should(keys).eql([
@ -47,7 +47,7 @@ describe('Index: glob', function () {
'media/2016/June/IMG_0001.jpg': '...',
'media/2017/IMG_0002.jpg': '...'
})
glob.find('media', (err, map) => {
glob.find('media', {}, (err, map) => {
if (err) return done(err)
const keys = Object.keys(map).sort()
should(keys).eql([
@ -58,11 +58,66 @@ describe('Index: glob', function () {
})
})
it('includes photos and videos by default', (done) => {
mock({
'media/IMG_0001.jpg': '...',
'media/IMG_0002.mp4': '...'
})
glob.find('media', {}, (err, map) => {
if (err) return done(err)
const keys = Object.keys(map).sort()
should(keys).eql([
'IMG_0001.jpg',
'IMG_0002.mp4'
])
done()
})
})
it('can excludes photos', (done) => {
mock({
'media/IMG_0001.jpg': '...',
'media/IMG_0002.mp4': '...'
})
glob.find('media', { includePhotos: false }, (err, map) => {
if (err) return done(err)
const keys = Object.keys(map).sort()
should(keys).eql(['IMG_0002.mp4'])
done()
})
})
it('can excludes videos', (done) => {
mock({
'media/IMG_0001.jpg': '...',
'media/IMG_0002.mp4': '...'
})
glob.find('media', { includeVideos: false }, (err, map) => {
if (err) return done(err)
const keys = Object.keys(map).sort()
should(keys).eql(['IMG_0001.jpg'])
done()
})
})
it('can include raw photos', (done) => {
mock({
'media/IMG_0001.jpg': '...',
'media/IMG_0002.cr2': '...'
})
glob.find('media', { includeRawPhotos: true }, (err, map) => {
if (err) return done(err)
const keys = Object.keys(map).sort()
should(keys).eql(['IMG_0001.jpg', 'IMG_0002.cr2'])
done()
})
})
it('is case insensitive', (done) => {
mock({
'media/IMG_0001.JPG': '...'
})
glob.find('media', (err, map) => {
glob.find('media', {}, (err, map) => {
if (err) return done(err)
const keys = Object.keys(map).sort()
should(keys).eql([
@ -79,7 +134,7 @@ describe('Index: glob', function () {
'media/nested/.private/IMG_0003.jpg': '...',
'media/just/a.dot/IMG_0004.jpg': '...'
})
glob.find('media', (err, map) => {
glob.find('media', {}, (err, map) => {
if (err) return done(err)
const keys = Object.keys(map).sort()
should(keys).eql([
@ -95,7 +150,7 @@ describe('Index: glob', function () {
'media/holidays/IMG_0001.jpg': '...',
'media/holidays/@eaDir/IMG_0001.jpg': '...'
})
glob.find('media', (err, map) => {
glob.find('media', {}, (err, map) => {
if (err) return done(err)
const keys = Object.keys(map).sort()
should(keys).eql([
@ -110,7 +165,7 @@ describe('Index: glob', function () {
'media/holidays/IMG_0001.jpg': '...',
'media/#recycle/IMG_0002.jpg': '...'
})
glob.find('media', (err, map) => {
glob.find('media', {}, (err, map) => {
if (err) return done(err)
const keys = Object.keys(map).sort()
should(keys).eql([
@ -144,7 +199,7 @@ describe('Index: glob', function () {
filename
]), '...')
}
glob.find(tmpdir.name, (err, map) => {
glob.find(tmpdir.name, {}, (err, map) => {
if (err) return done(err)
const keys = Object.keys(map).sort()
should(keys).eql([
@ -165,7 +220,7 @@ describe('Index: glob', function () {
}),
'media/IMG_0002.jpg': '...'
})
glob.find('media', (err, map) => {
glob.find('media', {}, (err, map) => {
if (err) return done(err)
const keys = Object.keys(map).sort()
should(keys).eql([

Loading…
Cancel
Save