mirror of https://github.com/thumbsup/thumbsup
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
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)
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue