Add usage reporting with Google Analytics + greeting/error messages

This will help understand usage patterns to know what to focus on, e.g.
- are many people using thumbsup on Windows?
- are there many galleries with > 10,000 photos?
exif-summary
Romain 7 years ago
parent 3152361e65
commit 06ecd2edad

@ -0,0 +1,35 @@
const Insight = require('insight')
const path = require('path')
const pkg = require(path.join(__dirname, '..', 'package.json'))
// Google Analytics tracking code
const TRACKING_CODE = 'UA-110087713-3'
class Analytics {
constructor ({enabled}) {
this.enabled = enabled
this.insight = new Insight({ trackingCode: TRACKING_CODE, pkg })
this.insight.optOut = !enabled
}
// report that the gallery has started building
start (done) {
this.insight.track('start')
}
// report that the gallery has finished building + some stats
finish (stats, done) {
this.insight.track('finish')
this.insight.trackEvent({ category: 'gallery', action: 'albums', label: 'Album count', value: stats.albums })
this.insight.trackEvent({ category: 'gallery', action: 'photos', label: 'Photo count', value: stats.photos })
this.insight.trackEvent({ category: 'gallery', action: 'videos', label: 'Video count', value: stats.videos })
}
// report that an error happened
// but don't report the contents (might contain file paths etc)
error (done) {
this.insight.track('error')
}
}
module.exports = Analytics

@ -0,0 +1,49 @@
exports.USAGE = () => `
Usages:
thumbsup [required] [options]
thumbsup --config config.json'
`
exports.CONFIG_USAGE = () => `
The optional JSON config should contain a single object with one key
per argument, not including the leading "--". For example:
{ "sort-albums-by": "start-date" }
`
exports.SUCCESS = (stats) => `
Gallery generated successfully
${stats.albums} albums, ${stats.photos} photos, ${stats.videos} videos
`
exports.GREETING = () => `
Thanks for using thumbsup!
We hope it works exactly as you expect. If you have any issues or feature
ideas please raise an issue at https://github.com/thumbsup/thumbsup/issues. │
When building a gallery, thumbsup reports anonymous stats such as the OS and
gallery size. This is used to understand usage patterns & guide development
effort. You can disable usage reporting by specifying --no-usage-report.
This welcome message will not be shown again for this gallery.
Enjoy!
`
exports.SORRY = () => `
Something went wrong!
An unexpected error occurred and the gallery didn't build successfully.
This is most likely an edge-case that hasn't been tested before.
To help improve thumbsup and hopefully resolve your problem,
please raise an issue at https://github.com/thumbsup/thumbsup/issues. │
`

@ -0,0 +1,227 @@
const messages = require('./messages')
const path = require('path')
const yargs = require('yargs')
const OPTIONS = {
// ------------------------------------
// Required arguments
// ------------------------------------
'input': {
group: 'Required:',
description: 'Path to the folder with all photos/videos',
normalize: true,
demand: true
},
'output': {
group: 'Required:',
description: 'Output path for the static website',
normalize: true,
demand: true
},
// ------------------------------------
// Output options
// ------------------------------------
'thumb-size': {
group: 'Output options:',
description: 'Pixel size of the square thumbnails',
type: 'number',
'default': 120
},
'large-size': {
group: 'Output options:',
description: 'Pixel height of the fullscreen photos',
type: 'number',
'default': 1000
},
'download-photos': {
group: 'Output options:',
description: 'Target of the photo download links',
choices: ['large', 'copy', 'symlink', 'link'],
'default': 'large'
},
'download-videos': {
group: 'Output options:',
description: 'Target of the video download links',
choices: ['large', 'copy', 'symlink', 'link'],
'default': 'large'
},
'download-link-prefix': {
group: 'Output options:',
description: 'Path or URL prefix for linked downloads',
type: 'string'
},
'cleanup': {
group: 'Output options:',
description: 'Remove any output file that\'s no longer needed',
type: 'boolean',
'default': false
},
// ------------------------------------
// Album options
// ------------------------------------
'albums-from': {
group: 'Album options:',
description: 'How to group media into albums',
choices: ['folders', 'date'],
'default': 'folders'
},
'albums-date-format': {
group: 'Album options:',
description: 'How albums are named in <date> mode [moment.js pattern]',
'default': 'YYYY-MM'
},
'sort-albums-by': {
group: 'Album options:',
description: 'How to sort albums',
choices: ['title', 'start-date', 'end-date'],
'default': 'start-date'
},
'sort-albums-direction': {
group: 'Album options:',
description: 'Album sorting direction',
choices: ['asc', 'desc'],
'default': 'asc'
},
'sort-media-by': {
group: 'Album options:',
description: 'How to sort photos and videos',
choices: ['filename', 'date'],
'default': 'date'
},
'sort-media-direction': {
group: 'Album options:',
description: 'Media sorting direction',
choices: ['asc', 'desc'],
'default': 'asc'
},
// ------------------------------------
// Website options
// ------------------------------------
'index': {
group: 'Website options:',
description: 'Filename of the home page',
'default': 'index.html'
},
'albums-output-folder': {
group: 'Website options:',
description: 'Output subfolder for HTML albums (default: website root)',
'default': '.'
},
'theme': {
group: 'Website options:',
description: 'Name of the gallery theme to apply',
choices: ['classic', 'cards', 'mosaic'],
'default': 'classic'
},
'title': {
group: 'Website options:',
description: 'Website title',
'default': 'Photo album'
},
'footer': {
group: 'Website options:',
description: 'Text or HTML footer',
'default': null
},
'css': {
group: 'Website options:',
description: 'Path to a custom provided CSS/LESS file for styling',
normalize: true
},
'google-analytics': {
group: 'Website options:',
description: 'Code for Google Analytics tracking',
type: 'string'
},
// ------------------------------------
// Misc options
// ------------------------------------
'config': {
description: 'JSON config file (one key per argument)',
normalize: true
},
'usage-report': {
description: 'Disable anonymous usage statistics',
type: 'boolean',
'default': true
},
// ------------------------------------
// Deprecated options
// ------------------------------------
'original-photos': {
group: 'Deprecated:',
description: 'Copy and allow download of full-size photos',
type: 'boolean',
'default': false
},
'original-videos': {
group: 'Deprecated:',
description: 'Copy and allow download of full-size videos',
type: 'boolean',
'default': false
}
}
exports.get = () => {
var opts = yargs
.usage(messages.USAGE())
.wrap(null)
.help('help')
.config('config')
.options(OPTIONS)
.epilogue(messages.CONFIG_USAGE())
.argv
// Make input/output folder absolute paths
opts['input'] = path.resolve(opts['input'])
opts['output'] = path.resolve(opts['output'])
// By default, use relative links to the input folder
if (!opts['download-link-prefix']) {
opts['download-link-prefix'] = path.relative(opts['output'], opts['input'])
}
// Convert deprecated options to the new replacement
if (opts['original-photos']) opts['download-photos'] = 'copy'
if (opts['original-videos']) opts['download-videos'] = 'copy'
// All options as an object
return {
input: opts['input'],
output: opts['output'],
cleanup: opts['cleanup'],
title: opts['title'],
thumbSize: opts['thumb-size'],
largeSize: opts['large-size'],
downloadPhotos: opts['download-photos'],
downloadVideos: opts['download-videos'],
downloadLinkPrefix: opts['download-link-prefix'],
albumsFrom: opts['albums-from'],
albumsDateFormat: opts['albums-date-format'],
sortAlbumsBy: opts['sort-albums-by'],
sortAlbumsDirection: opts['sort-albums-direction'],
sortMediaBy: opts['sort-media-by'],
sortMediaDirection: opts['sort-media-direction'],
theme: opts['theme'],
css: opts['css'],
googleAnalytics: opts['google-analytics'],
index: opts['index'],
footer: opts['footer'],
albumsOutputFolder: opts['albums-output-folder'],
noUsageReport: opts['no-usage-report']
}
}

@ -1,220 +1,70 @@
#!/usr/bin/env node
var yargs = require('yargs')
var path = require('path')
var index = require('../src/index')
const fs = require('fs')
const index = require('../src/index')
const messages = require('./messages')
const path = require('path')
const Analytics = require('./analytics')
const options = require('./options')
console.log('')
var opts = yargs
.usage('Usages:\n' +
' thumbsup [required] [options]\n' +
' thumbsup --config config.json')
.wrap(null)
.help('help')
.options({
// ------------------------------------
// Required arguments
// ------------------------------------
// Read all options from the command-line / config file
const opts = options.get()
'input': {
group: 'Required:',
description: 'Path to the folder with all photos/videos',
normalize: true,
demand: true
},
'output': {
group: 'Required:',
description: 'Output path for the static website',
normalize: true,
demand: true
},
// ------------------------------------
// Output options
// ------------------------------------
'thumb-size': {
group: 'Output options:',
description: 'Pixel size of the square thumbnails',
type: 'number',
'default': 120
},
'large-size': {
group: 'Output options:',
description: 'Pixel height of the fullscreen photos',
type: 'number',
'default': 1000
},
'download-photos': {
group: 'Output options:',
description: 'Target of the photo download links',
choices: ['large', 'copy', 'symlink', 'link'],
'default': 'large'
},
'download-videos': {
group: 'Output options:',
description: 'Target of the video download links',
choices: ['large', 'copy', 'symlink', 'link'],
'default': 'large'
},
'download-link-prefix': {
group: 'Output options:',
description: 'Path or URL prefix for linked downloads',
type: 'string'
},
'cleanup': {
group: 'Output options:',
description: 'Remove any output file that\'s no longer needed',
type: 'boolean',
'default': false
},
// ------------------------------------
// Album options
// ------------------------------------
'albums-from': {
group: 'Album options:',
description: 'How to group media into albums',
choices: ['folders', 'date'],
'default': 'folders'
},
'albums-date-format': {
group: 'Album options:',
description: 'How albums are named in <date> mode [moment.js pattern]',
'default': 'YYYY-MM'
},
'sort-albums-by': {
group: 'Album options:',
description: 'How to sort albums',
choices: ['title', 'start-date', 'end-date'],
'default': 'start-date'
},
'sort-albums-direction': {
group: 'Album options:',
description: 'Album sorting direction',
choices: ['asc', 'desc'],
'default': 'asc'
},
'sort-media-by': {
group: 'Album options:',
description: 'How to sort photos and videos',
choices: ['filename', 'date'],
'default': 'date'
},
'sort-media-direction': {
group: 'Album options:',
description: 'Media sorting direction',
choices: ['asc', 'desc'],
'default': 'asc'
},
// ------------------------------------
// Website options
// ------------------------------------
'index': {
group: 'Website options:',
description: 'Filename of the home page',
'default': 'index.html'
},
'albums-output-folder': {
group: 'Website options:',
description: 'Output subfolder for HTML albums (default: website root)',
'default': '.'
},
'theme': {
group: 'Website options:',
description: 'Name of the gallery theme to apply',
choices: ['classic', 'cards', 'mosaic'],
'default': 'classic'
},
'title': {
group: 'Website options:',
description: 'Website title',
'default': 'Photo album'
},
'footer': {
group: 'Website options:',
description: 'Text or HTML footer',
'default': null
},
'css': {
group: 'Website options:',
description: 'Path to a custom provided CSS/LESS file for styling',
normalize: true
},
'google-analytics': {
group: 'Website options:',
description: 'Code for Google Analytics tracking',
type: 'string'
},
// ------------------------------------
// Misc options
// ------------------------------------
'config': {
description: 'JSON config file (one key per argument)',
normalize: true
},
// ------------------------------------
// Deprecated options
// ------------------------------------
// If this is the first run, display a welcome message
const indexPath = path.join(opts.output, 'thumbsup.db')
const firstRun = fs.existsSync(indexPath) === false
if (firstRun) {
console.log(`${messages.GREETING()}\n`)
}
'original-photos': {
group: 'Deprecated:',
description: 'Copy and allow download of full-size photos',
type: 'boolean',
'default': false
},
'original-videos': {
group: 'Deprecated:',
description: 'Copy and allow download of full-size videos',
type: 'boolean',
'default': false
// Basic usage report (anonymous statistics)
const analytics = new Analytics({
enabled: opts['usageReport']
})
analytics.start()
// Catch all exceptions and exit gracefully
process.on('uncaughtException', handleError)
// Build the gallery!
index.build(opts, (err, album) => {
if (err) {
handleError(err)
} else {
const stats = {
albums: countAlbums(0, album),
photos: album.stats.photos,
videos: album.stats.videos
}
analytics.finish(stats)
const message = messages.SUCCESS(stats)
console.log(`\n${message}\n`)
exit(0)
}
})
})
.config('config')
.epilogue('The optional JSON config should contain a single object with one key ' +
'per argument, not including the leading "--". For example:\n\n' +
'{ "sort-albums-by": "start-date" }')
.argv
// Post-processing and smart defaults
opts['input'] = path.resolve(opts['input'])
opts['output'] = path.resolve(opts['output'])
if (!opts['download-link-prefix']) {
opts['download-link-prefix'] = path.relative(opts['output'], opts['input'])
// Print an error report and exit
// Note: remove "err.context" (entire data model) which can make the output hard to read
function handleError (err) {
analytics.error()
delete err.context
console.error('\nUnexpected error', err)
console.error(`\n${messages.SORRY()}\n`)
exit(1)
}
// Convert deprecated options to the new replacement
if (opts['original-photos']) opts['download-photos'] = 'copy'
if (opts['original-videos']) opts['download-videos'] = 'copy'
// Force a successful or failed exit
// This is required
// - because capturing unhandled errors will make Listr run forever
// - to ensure pending Analytics HTTP requests don't keep the tool running
function exit (code) {
// just some time to ensure analytics has time to fire
setTimeout(() => process.exit(code), 10)
}
index.build({
input: opts['input'],
output: opts['output'],
cleanup: opts['cleanup'],
title: opts['title'],
thumbSize: opts['thumb-size'],
largeSize: opts['large-size'],
downloadPhotos: opts['download-photos'],
downloadVideos: opts['download-videos'],
downloadLinkPrefix: opts['download-link-prefix'],
albumsFrom: opts['albums-from'],
albumsDateFormat: opts['albums-date-format'],
sortAlbumsBy: opts['sort-albums-by'],
sortAlbumsDirection: opts['sort-albums-direction'],
sortMediaBy: opts['sort-media-by'],
sortMediaDirection: opts['sort-media-direction'],
theme: opts['theme'],
css: opts['css'],
googleAnalytics: opts['google-analytics'],
index: opts['index'],
footer: opts['footer'],
albumsOutputFolder: opts['albums-output-folder']
})
// Cound the total number of nested albums
function countAlbums (total, album) {
return 1 + album.albums.reduce(countAlbums, total)
}

9689
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -37,6 +37,7 @@
"fs-readdir-recursive": "^1.0.0",
"handlebars": "~4.0.5",
"ini": "^1.3.4",
"insight": "^0.8.4",
"less": "^2.7.1",
"lightgallery": "1.2.14",
"listr": "^0.12.0",

@ -1,10 +1,9 @@
const fs = require('fs-extra')
const Listr = require('listr')
const steps = require('./steps/index')
const summary = require('./steps/summary')
const website = require('./website/website')
exports.build = function (opts) {
exports.build = function (opts, done) {
const tasks = new Listr([
{
title: 'Indexing folder',
@ -42,10 +41,8 @@ exports.build = function (opts) {
])
tasks.run().then(ctx => {
console.log('\n' + summary.create(ctx) + '\n')
process.exit(0)
done(null, ctx.album)
}).catch(err => {
console.log('\nUnexpected error', err)
process.exit(1)
done(err)
})
}

@ -7,5 +7,6 @@
"sort-media-by": "date",
"albums-from": "folders",
"theme": "mosaic",
"cleanup": true
"cleanup": true,
"noUsageReport": true
}

Loading…
Cancel
Save