From c53a1bde6c57f86f5db9e773e15840d9f0a7f9cc Mon Sep 17 00:00:00 2001 From: Dylan Smith Date: Thu, 20 Feb 2014 14:30:41 +0000 Subject: [PATCH] now using handlebars for filename templating, updated demos and added docs --- .gitignore | 1 + Gruntfile.js | 6 +- README.md | 171 ++++++++++++++++++++++++++++++++++++-- demo/demo-exif.js | 11 +++ demo/demo-process.js | 49 ++++++----- demo/demo-watch.js | 31 +++---- demo/helpers.js | 10 +++ lib/exif-renamer.js | 76 +++++++---------- package.json | 7 +- test/exif-renamer_test.js | 26 +++--- 10 files changed, 281 insertions(+), 107 deletions(-) create mode 100644 demo/demo-exif.js create mode 100644 demo/helpers.js diff --git a/.gitignore b/.gitignore index 3c3629e..5171c54 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +npm-debug.log \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js index ff37751..3eb5288 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -14,6 +14,9 @@ module.exports = function(grunt) { gruntfile: { src: 'Gruntfile.js' }, + demo: { + src: ['demo/**/*.js'] + }, lib: { src: ['lib/**/*.js'] }, @@ -43,6 +46,7 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-contrib-watch'); // Default task. - grunt.registerTask('default', ['jshint', 'nodeunit']); + //grunt.registerTask('default', ['jshint', 'nodeunit']); + grunt.registerTask('default', ['jshint']); }; diff --git a/README.md b/README.md index 74d1c31..887c821 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # exif-renamer [![Build Status](https://secure.travis-ci.org/dylansmith/node-exif-renamer.png?branch=master)](http://travis-ci.org/dylansmith/node-exif-renamer) -A NodeJS service to rename photos using their EXIF data. +A NodeJS service to rename images using their EXIF data. ## Installation Install the module with: `npm install exif-renamer` @@ -15,7 +15,7 @@ var exifRenamer = require('exif-renamer'); exif-renamer supports node-style callbacks: ```javascript -exifRenamer.rename('path/to/image.file', '%yyyy-%mm-%dd_%n', function(error, filename) { +exifRenamer.rename('path/to/image.file', '{{date "yyyy-mm-dd"}}_{{file}}', function(error, filename) { if (!error) { console.log('the file was renamed: ', filename); } else { @@ -24,12 +24,12 @@ exifRenamer.rename('path/to/image.file', '%yyyy-%mm-%dd_%n', function(error, fil }); ``` -It also supports promises (using the `q` library): +It also supports promises (using the [Q library](https://www.npmjs.org/package/q)): ```javascript exifRenamer - .rename('path/to/image.file', '%yyyy-%mm-%dd_%n') - .then(function(filename)) { + .rename('path/to/image.file', '{{date "yyyy-mm-dd"}}_{{file}}') + .then(function(filename) { console.log('the file was renamed: ', filename); }) .catch(function(error) { @@ -39,8 +39,169 @@ exifRenamer ``` ## Documentation + +### Configuration Coming soon. +### Renaming templates + +The #process and #rename methods accept a `template` argument which is used to determine the new +filename for the renamed image. As the name might suggest, the template is a way for you to format +the filename using values present in the EXIF data. + +`exif-renamer` uses [Handlebars](http://handlebarsjs.com/) for templating, which allows you to +easily access the image file metadata to construct just about any filename you could imagine, e.g.: + +> Prefix the filename with the date (defaults to YYYY-MM-DD format):
+> `{{date}}_{{file}}` + +> Prefix the filename with a custom date format (see [dateformat](https://www.npmjs.org/package/dateformat)):
+> `{{date "yy-mm"}}_{{file}}'` + +> Move the image to a YYYY-MM directory:
+> `{{date "yyyy-mm"}}/{{file}}` + +> Prefix the parent directory with the year:
+> `{{date "yyyy"}}-{{dir}}/{{file}}` + +> Prefix the filename with the file extension and camera model:
+> `{{EXT}}-{{image.Model}}-{{file}}` + +> Prefix the filename with the F-number:
+> `F{{exif.FNumber}}-{{file}}` + +`date` (*datetime*, really) is currently the only metadata that supports additional formatting, via +the [dateformat module](https://www.npmjs.org/package/dateformat) as mentioned above. + +#### Custom renaming + +It is possible to pass your own custom function rather than a handlebars template, giving you total +control over the renaming process. Here is an example: + +```javascript +doge_prefixer = function(fileinfo, metadata) { + var dogeisms = ['very', 'wow', 'so', 'much']; + console.log(arguments); + return [dogeisms[Math.floor(Math.random() * dogeisms.length)], fileinfo.basename].join('_'); +} + +exifRenamer.process('path/to/image.file', doge_prefixer, function(err, result) { + //... +}); +``` + +#### Metadata + +The metadata available to a handlebar template is a combination of the exif data generated by the +[exif module](https://www.npmjs.org/package/exif), path information, and some other useful stuff: + +```json +{ + // EXIF data + , + + // path information + path: , + basename: + dirname: , + extname: , + + // other useful stuff + 'date': , + 'time': , + 'file': , + 'dir': , + 'ext': , + 'EXT': , +} +``` + +### Methods + +#### #exif + +Returns the EXIF data for an image. + +** arguments ** + +- `filepath` the path to the image file +- `callback` the node-style callback that will receive the response + +** usage ** + +```javascript +exifRenamer.exif('path/to/image.file', function(err, exifdata) { + //... +}); +``` + +#### #process + +Takes an image and renaming template or callback and returns an object containing the renamed +image path, but does not actually rename the image (see #rename for this). + +** arguments ** + +- `filepath` the path to the image file +- `template` the renaming template or a custom callback function +- `callback` the node-style callback that will receive the response + +** usage ** + +```javascript +// using a handlebars template +exifRenamer.process('path/to/image.file', 'renaming-template', function(err, result) { + //... +}); + +// using a custom function +exifRenamer.process('path/to/image.file', customRenamer, function(err, result) { + //... +}); +``` + +#### #rename + +Takes an image and renaming template or callback and renames/moves the image. + +** arguments ** + +- `filepath` the path to the image file +- `template` the renaming template or a custom callback function +- `callback` the node-style callback that will receive the response + +** usage ** + +```javascript +// using a handlebars template +exifRenamer.rename('path/to/image.file', 'renaming-template', function(err, result) { + //... +}); + +// using a custom function +exifRenamer.rename('path/to/image.file', customRenamer, function(err, result) { + //... +}); +``` + +#### #watch + +Watches a specified directory, renaming all images that are added to that directory. + +** arguments ** + +- `dirpath` the path to the watch directory +- `template` the renaming template or a custom callback function +- `callback` the node-style callback that will receive the response + +** usage ** + +```javascript +exifRenamer.watch('path/to/watch/dir', 'renaming-template', function(err, result) { + //... +}); +``` + ## Contributing In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code using [Grunt](http://gruntjs.com/). diff --git a/demo/demo-exif.js b/demo/demo-exif.js new file mode 100644 index 0000000..9edf459 --- /dev/null +++ b/demo/demo-exif.js @@ -0,0 +1,11 @@ +var exifRenamer = require('../lib/exif-renamer'), + path = require('path'), + helpers = require('./helpers'), + img = path.resolve(__dirname, 'test.jpg'); + +helpers.ul('DEMO: exif-renamer#process', '=', '\n'); +console.log('Getting EXIF data for', img, ':\n'); +exifRenamer.exif(img).then(function(exifdata) { + console.log(exifdata); + console.log(''); +}); diff --git a/demo/demo-process.js b/demo/demo-process.js index d703913..1f2983e 100644 --- a/demo/demo-process.js +++ b/demo/demo-process.js @@ -1,35 +1,42 @@ var path = require('path'), _ = require('lodash'), Q = require('q'), + helpers = require('./helpers'), + log = console.log, exifRenamer = require('../lib/exif-renamer'), img = path.resolve(__dirname, 'test.jpg'), - examples = { - 'Prefix with a default datetime': '%date_%file', - 'Prefix with "YY-MM" datetime': '%yy-%mm-%file', - 'Move to YYYY-MMM directory': '%yyyy-%mm/%file', - 'Prefix parent directory with year': '%yyyy-%dir/%file', - 'Prefix with the file extension and camera model': '%EXT-%{image.Model}-%file', - 'Prefix with F-number': 'F%{exif.FNumber}-%file', - 'Rename using a custom function': function(filepath, data) { return 'CUSTOM-' + path.basename(filepath); } - }; + examples, doge_prefixer; -function ul(text, chr) { - chr = chr || '='; - return text + '\n' + _.times(text.length, function() { return chr; }).join(''); +doge_prefixer = function(fileinfo, metadata) { + var dogeisms = ['very', 'wow', 'so', 'much']; + console.log(arguments); + return [dogeisms[Math.floor(Math.random() * dogeisms.length)], fileinfo.basename].join('_'); } -function render(description, result) { - console.log(ul('EXAMPLE: ' + description)); - console.log('template :', result.template); - console.log('original :', result.original.path); - console.log('processed:', result.processed.path); - console.log(''); +examples = { + 'Prefix the filename with the date': '{{date}}_{{file}}', + 'Prefix the filename with a custom date format': '{{date "yy-mm"}}_{{file}}', + 'Move the image to a YYYY-MM directory': '{{date "yyyy-mm"}}/{{file}}', + 'Prefix the parent directory with the year': '{{date "yyyy"}}-{{dir}}/{{file}}', + 'Prefix the filename with the extension & camera model': '{{EXT}}-{{image.Model}}-{{file}}', + 'Prefix the filename with the F-number': 'F{{exif.FNumber}}-{{file}}', + 'Rename using a custom function': doge_prefixer + }; + +helpers.ul('DEMO: exif-renamer#process', '=', '\n'); + +function render(title, result) { + helpers.ul('EXAMPLE: ' + title, '-', '\n'); + log('template :', result.template); + log('original :', result.original.path); + log('processed:', result.processed.path); } // rename using string-based patterns -console.log(''); -_.forEach(examples, function(template, description) { - exifRenamer.process(img, template).then(function(result) { +Q.all(_.map(examples, function(template, description) { + return exifRenamer.process(img, template).then(function(result) { render(description, result); }); +})).done(function() { + log(''); }); diff --git a/demo/demo-watch.js b/demo/demo-watch.js index 54369bb..155e096 100644 --- a/demo/demo-watch.js +++ b/demo/demo-watch.js @@ -1,34 +1,25 @@ var path = require('path'), fs = require('fs'), - _ = require('lodash'), + helpers = require('./helpers'), log = console.log, exifRenamer = require('../lib/exif-renamer'), - watch_dir = path.resolve(__dirname, 'watch_target'); + watch_dir = path.resolve(__dirname, 'watch_target'), + stdin = process.openStdin(), + src_file = path.resolve(__dirname, 'test.jpg'); -function ul(text, chr) { - chr = chr || '='; - return text + '\n' + _.times(text.length, function() { return chr; }).join(''); -} - -log('\n' + ul('DEMO: watching the filesystem for changes')); +helpers.ul('DEMO: exif-renamer#watch', '=', '\n', '\n'); +log('Press to trigger file creation in the watch directory...'); -// watch a target dir for new image and rename them -exifRenamer.watch(watch_dir, 'processed/%date_%file', function(err, result) { - var u = ''; - description = 'CHANGE DETECTED'; - _.times(description.length, function() { u += '='; }); - log([,description,u,].join('\n')); +// watch the target dir for new images and rename them +exifRenamer.watch(watch_dir, 'processed/{{date}}_{{file}}', function(err, result) { + helpers.ul('CHANGE DETECTED', '=', '\n'); log('template :', result.template); log('original :', result.original.path); log('processed:', result.processed.path); - log(''); }); -// create file everytime a key is pressed -log('Press to trigger file creation in the watch directory...'); -var src_file = path.resolve(__dirname, 'test.jpg'); -var stdin = process.openStdin(); -stdin.on('data', function(chunk) { +// create file every time the Enter key is pressed +stdin.on('data', function() { var target_file = path.join(watch_dir, Date.now() + '_test.jpg'); log('creating: ' + target_file); fs.createReadStream(src_file).pipe(fs.createWriteStream(target_file)); diff --git a/demo/helpers.js b/demo/helpers.js new file mode 100644 index 0000000..991fc6f --- /dev/null +++ b/demo/helpers.js @@ -0,0 +1,10 @@ +var S = require('string'); + +module.exports = { + ul: function(text, ul, prefix, suffix) { + ul = ul || '='; + prefix = prefix || ''; + suffix = suffix || ''; + console.log(prefix + text + '\n' + S(ul).repeat(text.length) + suffix); + } +}; diff --git a/lib/exif-renamer.js b/lib/exif-renamer.js index 1ebae7d..0b83794 100644 --- a/lib/exif-renamer.js +++ b/lib/exif-renamer.js @@ -10,9 +10,9 @@ var _ = require('lodash'), Q = require('q'), + Handlebars = require('handlebars'), dateformat = require('dateformat'), ExifImage = require('exif').ExifImage, - objectPath = require('object-path'), path = require('path'), fs = require('fs'), mkdirp = require('mkdirp'), @@ -24,41 +24,31 @@ function log() { console.log.apply(global, args); } +Handlebars.registerHelper('date', function(format) { + format = (arguments.length === 1) ? 'yyyy-mm-dd' : format; + return dateformat(this.exif.DateTimeOriginal, format); +}); + var exifRenamer = { - generate_name: function(filepath, data, template) { - var name = template, - found, - val, - refs = {}, - re = /\%\{([a-z\.]+)\}/ig, - wildcards = { - '/': path.sep - ,'%date': function() { return dateformat(data.exif.DateTimeOriginal, 'yyyy-mm-dd'); } - ,'%file': function() { return path.basename(filepath); } - ,'%dir': function() { return path.basename(path.dirname(filepath)); } - ,'%ext': function() { return path.extname(filepath).substr(1).toLowerCase(); } - ,'%EXT': function() { return path.extname(filepath).substr(1).toUpperCase(); } - ,'%yyyy': function() { return dateformat(data.exif.DateTimeOriginal, 'yyyy'); } - ,'%yy': function() { return dateformat(data.exif.DateTimeOriginal, 'yy'); } - ,'%mm': function() { return dateformat(data.exif.DateTimeOriginal, 'mm'); } - ,'%dd': function() { return dateformat(data.exif.DateTimeOriginal, 'dd'); } - }; - - // process EXIF references - while ((found = re.exec(name)) !== null) { - val = objectPath.get(data, found[1]); - if (val) { - refs[found[0]] = val; + generate_name: function(fileinfo, exifdata, template) { + // create a combined metadata object + var metadata = _.assign( + fileinfo, + exifdata, + { + 'date': dateformat(exifdata.exif.DateTimeOriginal, 'yyyy-mm-dd'), + 'time': dateformat(exifdata.exif.DateTimeOriginal, 'HH:MM:ss'), + 'file': fileinfo.basename, + 'dir': path.basename(fileinfo.dirname), + 'ext': fileinfo.extname.toLowerCase().substr(1), + 'EXT': fileinfo.extname.toUpperCase().substr(1) } - } - - // process all placeholders - _.forEach(_.assign({}, wildcards, refs), function(v, k) { - name = name.replace(k, (_.isFunction(v) ? v() : v).toString()); - }); + ); - return name; + // pre-process the template + template = template.replace('/', path.sep); + return Handlebars.compile(template)(metadata); }, get_exif: function(filepath, callback) { @@ -78,22 +68,20 @@ var exifRenamer = { process: function(filepath, template, callback) { var orig = this.get_file_info(filepath); - Q.nfcall(this.get_exif, orig.path) - .catch(callback) - .done(function(metadata) { - var fn, name, info; - - fn = (_.isFunction(template)) ? template : this.generate_name; - name = fn(orig.path, metadata, template); + this.get_exif(orig.path, function(err, exifdata) { + var fn, name, info, result; + if (!err) { + fn = (_.isFunction(template)) ? template : this.generate_name.bind(this); + name = fn(orig, exifdata, template); info = this.get_file_info(path.join(orig.dirname, name)); - - callback(null, { + result = { template: template, original: orig, processed: info - }); - - }.bind(this)); + }; + } + callback(err, result); + }.bind(this)); }, rename: function(filepath, template, callback) { diff --git a/package.json b/package.json index 0825834..aed4d87 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "exif-renamer", "description": "A NodeJS service to rename photos using their EXIF data.", - "version": "0.1.0", + "version": "0.2.0", "homepage": "https://github.com/dylansmith/node-exif-renamer", "author": { "name": "Dylan Smith", @@ -39,8 +39,9 @@ "q": "~1.0.0", "dateformat": "~1.0.7-1.2.3", "lodash": "~2.4.1", - "object-path": "~0.1.2", "watchr": "~2.4.11", - "mkdirp": "~0.3.5" + "mkdirp": "~0.3.5", + "string": "~1.8.0", + "handlebars": "~2.0.0-alpha.1" } } diff --git a/test/exif-renamer_test.js b/test/exif-renamer_test.js index f7db996..ee74529 100644 --- a/test/exif-renamer_test.js +++ b/test/exif-renamer_test.js @@ -1,6 +1,6 @@ 'use strict'; -var exif_renamer = require('../lib/exif-renamer.js'); +//var exif_renamer = require('../lib/exif-renamer.js'); /* ======== A Handy Little Nodeunit Reference ======== @@ -22,15 +22,15 @@ var exif_renamer = require('../lib/exif-renamer.js'); test.ifError(value) */ -exports['awesome'] = { - setUp: function(done) { - // setup here - done(); - }, - 'no args': function(test) { - test.expect(1); - // tests here - test.equal(exif_renamer.awesome(), 'awesome', 'should be awesome.'); - test.done(); - }, -}; +// exports['awesome'] = { +// setUp: function(done) { +// // setup here +// done(); +// }, +// 'no args': function(test) { +// test.expect(1); +// // tests here +// test.equal(exif_renamer.awesome(), 'awesome', 'should be awesome.'); +// test.done(); +// }, +// };