Generated website contains original media too

- now generate thumbs + large (original is too big for web download)
- simpler build system
pull/9/head
rprieto 10 years ago
parent 3a388181ee
commit 1608b3cfac

@ -3,14 +3,12 @@
Static HTML galleries from a list of photos & videos.
- creates thumbnails for fast previews
- only rebuilds changed files: it's fast!
- uses relative paths so you can deploy the pages anywhere
- supports custom CSS for styling
- works great with Amazon S3 for static hosting
![screenshot](https://raw.github.com/rprieto/thumbsup/master/screenshot.jpg)
*Note: `thumbsup` keeps generated content separate from the original media. This means you're free to upload the media anywhere, and never have to worry about deleting the output folder*
## Requirements
- [Node.js](http://nodejs.org/): `brew install Node`
@ -43,25 +41,39 @@ The following args are required:
- `--input <path>` path to the folder with photos / videos
- `--output <path>` target output folder
- `--media-prefix <url>` prefix for the photos / videos URLS (can be relative or absolute)
And you can optionally specify:
- `--size <pixels>` size of the thumbnails
- `--css <path>` use the given CSS file instead of the default style
- `--thumb-size <pixels>` thumbnail image size (default 120)
- `--large-size <pixels>` fullscreen image size (default 1000)
For example:
```bash
thumbsup --input "/media/photos" --output "./website" --media-prefix "http://my.photo.bucket.s3.amazon.com" --css "custom.css" --size 200
thumbsup --input "/media/photos" --output "./website" --thumb-size 200 --large-size 1500
```
## Website structure
The generated static website has the following structure:
```
website
|__ index.html
|__ sydney.html
|__ paris.html
|__ public
|__ media
| |__ original
| |__ large
| |__ thumbs
```
## Deployment
The simplest is to deploy the media and generated pages to S3 buckets on AWS using the [AWS CLI tools](http://aws.amazon.com/cli/).
- `aws s3 sync /media/photos s3://my.photo.bucket --delete`
- `aws s3 sync /generated/website s3://my.website.bucket --delete`
- `aws s3 sync ./generated/website s3://my.website.bucket --delete`
## Password protection

@ -11,9 +11,12 @@ thumbsup.build({
// for the thumbnails and static pages
output: 'example/website',
// relative path to the media
// a local path for testing
// but this could be the URL to an S3 bucket
mediaPrefix: '../media'
// size of the square thumbnails
// in pixels
thumbSize: 120,
// size of the "fullscreen" view
// in pixels
largeSize: 400
});

@ -1,14 +1,4 @@
var fs = require('fs-extra');
var glob = require('glob');
exports.find = function(folder, ext, callback) {
var opts = {
cwd: folder,
nonull: false,
nocase: true
};
glob('**/*.{' + ext + '}', opts, callback);
};
exports.newer = function(src, dest) {
var srcTime = 0;

@ -1,40 +1,37 @@
var _ = require('lodash');
var fs = require('fs');
var path = require('path');
var files = require('./files');
var _ = require('lodash');
var fs = require('fs');
var path = require('path');
var glob = require('glob');
exports.fromDisk = function(mediaPath, mediaPrefix, size, callback) {
exports.fromDisk = function(mediaPath, callback) {
function fileInfo(file) {
return {
date: date(mediaPath, file),
name: path.basename(file),
path: file,
url: mediaPrefix + '/' + file,
thumbnail: thumbsPath(file),
video: isVideo(file),
poster: videoPoster(file),
size: size
video: video(file),
urls: {
original: mediaUrl(file, 'original'),
large: mediaUrl(file, 'large'),
thumb: mediaUrl(file, 'thumbs')
}
}
}
function date(mediaPath, file) {
return fs.statSync(path.join(mediaPath + '/' + file)).ctime.getTime();
function date(file) {
return fs.statSync(file).ctime.getTime();
}
function thumbsPath(file) {
return path.join('thumbs', file.replace(/\.[a-z0-9]+$/, '.jpg'));
}
function videoPoster(file) {
if (isVideo(file)) {
return path.join('thumbs', file.replace(/\.[a-z0-9]+$/, '_poster.jpg'));
} else {
return null;
function mediaUrl(file, type) {
if (type != 'original') {
file = file.replace(/\.(mp4|mov)$/, '.jpg');
}
return path.join('media', type, file);
}
function isVideo(file) {
return file.match(/\.(mp4|mov)$/) != null;
function video(file) {
return (file.match(/\.(mp4|mov)$/) != null);
}
function byFolder(file) {
@ -49,7 +46,14 @@ exports.fromDisk = function(mediaPath, mediaPrefix, size, callback) {
};
}
files.find(mediaPath, 'jpg,jpeg,png,mp4,mov', function (err, files) {
var globOptions = {
cwd: mediaPath,
nonull: false,
nocase: true
};
glob('**/*.{jpg,jpeg,png,mp4,mov}', globOptions, function (err, files) {
if (err) return callback(err);
var galleries = _(files)
.map(fileInfo)
.sortBy('date')

@ -7,79 +7,115 @@ var galleries = require('./galleries');
var render = require('./render');
var thumbs = require('./thumbs');
var files = require('./files');
var make = require('./make');
exports.build = function(opts) {
console.log('Building galleries...\n')
if (opts.thumbSize) thumbs.sizes.thumb = opts.thumbSize;
if (opts.largeSize) thumbs.sizes.large = opts.largeSize;
fs.mkdirp(opts.output);
thumbs.size = opts.size || 100;
var media = path.join(opts.output, 'media');
photos(opts);
videos(opts);
website(opts);
support(opts);
function website(callback) {
galleries.fromDisk(opts.input, function(err, list) {
if (err) return callback(err);
};
function photos(opts) {
var thumbsFolder = path.join(path.resolve(opts.output), 'thumbs');
files.find(opts.input, 'jpg,png', function (err, files) {
var fns = files.map(function(file) {
return thumbs.photo.bind(this, {
input: path.join(opts.input, file),
thumbnail: path.join(thumbsFolder, file)
var rendered = render.gallery(list, list[0]);
var outputPath = path.join(opts.output, 'index.html');
fs.writeFileSync(outputPath, rendered);
list.forEach(function(folder) {
var rendered = render.gallery(list, folder, opts.size);
var outputPath = path.join(opts.output, folder.url);
fs.writeFileSync(outputPath, rendered);
});
callback();
});
async.parallel(fns, log('Photos'));
});
}
}
function videos(opts) {
var thumbsFolder = path.join(path.resolve(opts.output), 'thumbs');
files.find(opts.input, 'mp4,mov', function (err, files) {
var fns = files.map(function(file) {
return thumbs.video.bind(this, {
input: path.join(opts.input, file),
thumbnail: path.join(thumbsFolder, ext(file, '.jpg')),
poster: path.join(thumbsFolder, ext(file, '_poster.jpg'))
});
});
async.parallel(fns, log('Videos'));
});
}
function support(callback) {
var src = path.join(__dirname, '..', 'public');
var dest = path.join(opts.output, 'public');
copyFolder(src, dest, callback);
}
function website(opts) {
galleries.fromDisk(opts.input, opts.mediaPrefix, opts.size, function(err, list) {
var rendered = render.gallery(list, list[0]);
var outputPath = path.join(opts.output, 'index.html');
fs.writeFileSync(outputPath, rendered);
list.forEach(function(folder) {
var rendered = render.gallery(list, folder);
var outputPath = path.join(opts.output, folder.url);
fs.writeFileSync(outputPath, rendered);
});
log('Website')();
});
}
function copyMedia(callback) {
var dest = path.join(opts.output, 'media', 'original');
copyFolder(opts.input, dest, callback);
}
function support(opts) {
var pub = path.join(__dirname, '..', 'public');
var out = path.join(path.resolve(opts.output), 'public');
if (files.newer(pub, out)) {
fs.copy(pub, out, log('Supporting files'));
function photoLarge(callback) {
make({
source: opts.input,
filter: '**/*.{jpg,jpeg,png}',
dest: media + '/large/$path/$name.$ext',
process: thumbs.photoLarge
}, callback);
}
function photoThumbs(callback) {
make({
source: opts.input,
filter: '**/*.{jpg,jpeg,png}',
dest: media + '/thumbs/$path/$name.$ext',
process: thumbs.photoSquare
}, callback);
}
function videoLarge(callback) {
make({
source: opts.input,
filter: '**/*.{mp4,mov}',
dest: media + '/large/$path/$name.jpg',
process: thumbs.videoLarge
}, callback);
}
function videoThumbs(callback) {
make({
source: opts.input,
filter: '**/*.{mp4,mov}',
dest: media + '/thumbs/$path/$name.jpg',
process: thumbs.videoSquare
}, callback);
}
async.series([
step('Website', website),
step('Support', support),
step('Original media', copyMedia),
step('Photos (large)', photoLarge),
step('Photos (thumbs)', photoThumbs),
step('Videos (large)', videoLarge),
step('Videos (thumbs)', videoThumbs)
], finish);
};
function copyFolder(src, dest, callback) {
var src = path.resolve(src);
var dest = path.resolve(dest);
if (files.newer(src, dest)) {
fs.copy(src, dest, callback);
} else {
callback();
}
}
function ext(file, newExtension) {
return file.replace(/\.[a-z0-9]+$/, newExtension);
function step(msg, fn) {
return function(callback) {
console.log(pad(msg, 20) + '[STARTED]')
fn(function(err) {
console.log(pad(msg, 20) + (err ? '[FAILED]\n' : '[OK]'));
callback(err);
});
};
}
function log(message) {
return function(err) {
var tag = pad(message + ': ', 25);
console.log(tag + (err || '[OK]'));
}
function finish(err) {
console.log(err || 'Done');
console.log();
process.exit(err ? 1 : 0)
}

@ -0,0 +1,57 @@
var _ = require('lodash');
var fs = require('fs-extra');
var os = require('os');
var path = require('path');
var glob = require('glob');
var async = require('async');
var fileUtils = require('./files');
module.exports = function(opts, callback) {
if (typeof opts.dest === 'string') {
opts.dest = rename(opts.dest);
}
var globOptions = {
cwd: opts.source,
nonull: false,
nocase: true
};
glob(opts.filter, globOptions, function (err, files) {
if (err) return callback(err);
// create list of src/dest pairs
var tasks = files.map(function(file) {
return {
src: path.resolve(path.join(opts.source, file)),
dest: path.resolve(opts.dest(file))
};
});
// create all required folders
var folders = _(tasks).pluck('dest').map(path.dirname).uniq().value();
folders.forEach(function(f) { fs.mkdirsSync(f, 0777); });
// run them in parallel
var fns = tasks.filter(function(task) {
return fileUtils.newer(task.src, task.dest);
}).map(function(task) {
return opts.process.bind(this, task.src, task.dest);
});
async.parallelLimit(fns, os.cpus().length, callback);
});
};
function rename(pattern) {
var parts = pattern.split('/');
var full = path.join.apply(this, parts);
return function(file) {
return full.replace('$path', path.dirname(file))
.replace('$name', path.basename(file, path.extname(file)))
.replace('$ext', path.extname(file).substr(1));
}
}

@ -9,7 +9,7 @@ function compileTemplate(hbsFile) {
var galleryTemplate = compileTemplate('gallery.hbs');
exports.gallery = function(list, active) {
exports.gallery = function(list, active, size) {
var links = list.map(function(item) {
return {
@ -21,7 +21,8 @@ exports.gallery = function(list, active) {
return galleryTemplate({
links: links,
gallery: active
gallery: active,
size: size
});
};

@ -1,46 +1,36 @@
var exec = require('child_process').exec;
var fs = require('fs-extra');
var path = require('path');
var async = require('async');
var gm = require('gm');
var files = require('./files');
// default thumbnail size (square)
exports.size = 100;
// opts = input, thumbnail
exports.photo = function(opts, callback) {
var async = require('async');
if (files.newer(opts.thumbnail, opts.input)) return callback();
fs.mkdirpSync(path.dirname(opts.thumbnail));
exports.sizes = {
thumb: 120,
large: 1000,
};
gm(path.resolve(opts.input))
.resize(exports.size, exports.size, "^")
exports.photoSquare = function(src, dest, callback) {
gm(src)
.resize(exports.sizes.thumb, exports.sizes.thumb, '^')
.gravity('Center')
.crop(exports.size, exports.size)
.crop(exports.sizes.thumb, exports.sizes.thumb)
.quality(90)
.write(path.resolve(opts.thumbnail), callback);
.write(dest, callback);
};
// opts = input, thumbnail, poster
exports.video = function(opts, callback) {
var fnVideo = function(next) {
var ffmpeg = 'ffmpeg -itsoffset -1 -i "' + opts.input + '" -ss 0.1 -vframes 1 -y "' + opts.poster + '"';
exec(ffmpeg, next);
};
var fnPhoto = function(next) {
exports.photo({
input: opts.poster,
thumbnail: opts.thumbnail
}, next);
};
exports.photoLarge = function(src, dest, callback) {
gm(src)
.resize(null, exports.sizes.large, '>')
.quality(95)
.write(dest, callback);
};
if (files.newer(opts.thumbnail, opts.input)) return callback();
fs.mkdirpSync(path.dirname(opts.thumbnail));
async.series([fnVideo, fnPhoto], callback);
exports.videoLarge = function(src, dest, callback) {
var ffmpeg = 'ffmpeg -itsoffset -1 -i "' + src + '" -ss 0.1 -vframes 1 -y "' + dest + '"';
exec(ffmpeg, callback);
};
exports.videoSquare = function(src, dest, callback) {
async.series([
exports.videoLarge.bind(this, src, dest),
exports.photoSquare.bind(this, dest, dest)
], callback);
};

@ -32,17 +32,17 @@
<ul id="gallery">
{{#each gallery.media}}<li>
{{#if video}}
<a href="{{url}}"
<a href="{{urls.original}}"
type="video/mp4"
data-poster="{{poster}}">
<img src="{{thumbnail}}"
data-poster="{{urls.large}}">
<img src="{{urls.thumb}}"
width="{{size}}"
height="{{size}}"
alt="{{name}}" />
</a>
{{else}}
<a href="{{url}}">
<img src="{{thumbnail}}"
<a href="{{urls.large}}">
<img src="{{urls.thumb}}"
width="{{size}}"
height="{{size}}"
alt="{{name}}" />

Loading…
Cancel
Save