mirror of https://github.com/thumbsup/thumbsup
Use [exiftool-json-db] to maintain the JSON database of media files
parent
dcb06c5242
commit
8dccb88f25
@ -0,0 +1,39 @@
|
||||
const debug = require('debug')('thumbsup')
|
||||
const exifdb = require('exiftool-json-db')
|
||||
const pad = require('pad')
|
||||
const progress = require('../utils/progress')
|
||||
|
||||
exports.update = function(media, databasePath, callback) {
|
||||
|
||||
var updateBar = null
|
||||
var emitter = null
|
||||
|
||||
try {
|
||||
emitter = exifdb.create({media: media, database: databasePath})
|
||||
} catch (ex) {
|
||||
const message = 'Loading database\n' + ex.toString() + '\n' +
|
||||
'If migrating from thumbsup v1, delete <metadata.json> to rebuild the database from scratch'
|
||||
callback(new Error(message))
|
||||
}
|
||||
|
||||
emitter.on('stats', (stats) => {
|
||||
debug(`Database stats: total=${stats.total}`)
|
||||
const totalBar = progress.create('List media files', stats.total)
|
||||
totalBar.tick(stats.total)
|
||||
updateBar = progress.create('Updating database', stats.added + stats.modified)
|
||||
if (stats.added + stats.modified === 0) {
|
||||
updateBar.tick(1)
|
||||
}
|
||||
})
|
||||
|
||||
emitter.on('file', (file) => {
|
||||
updateBar.tick()
|
||||
})
|
||||
|
||||
emitter.on('done', (files) => {
|
||||
callback(null, files)
|
||||
})
|
||||
|
||||
emitter.on('error', callback)
|
||||
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
var fs = require('fs');
|
||||
var async = require('async');
|
||||
var exif = require('exif-parser');
|
||||
var exec = require('child_process').exec;
|
||||
|
||||
// convert video rotation in degrees
|
||||
// to the standard EXIF rotation number
|
||||
var ROTATION_TABLE = {
|
||||
'0': 1,
|
||||
'90': 6,
|
||||
'180': 3,
|
||||
'270': 8
|
||||
};
|
||||
|
||||
var FFPROBE_DATE = /creation_time\s*:\s*(.*)\n/;
|
||||
var FFPROBE_ROTATE = /rotate\s*:\s*(.*)\n/;
|
||||
|
||||
exports.read = function(filePath, callback) {
|
||||
if (filePath.match(/\.(jpg|jpeg)$/i)) {
|
||||
photo(filePath, callback);
|
||||
} else if (filePath.match(/\.(mp4|mov|mts)$/i)) {
|
||||
video(filePath, callback);
|
||||
} else {
|
||||
callback(new Error('Unknown format'));
|
||||
}
|
||||
};
|
||||
|
||||
function photo(filePath, callback) {
|
||||
fs.readFile(filePath, function(err, contents) {
|
||||
if (err) return callback(new Error('Failed to read file ' + filePath));
|
||||
try {
|
||||
var result = exif.create(contents).parse();
|
||||
} catch (ex) {
|
||||
return callback(new Error('Failed to read EXIF from ' + filePath));
|
||||
}
|
||||
callback(null, {
|
||||
date: result.tags.DateTimeOriginal ? (result.tags.DateTimeOriginal * 1000) : null,
|
||||
orientation: result.tags.Orientation || null,
|
||||
caption: result.tags.ImageDescription
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function video(filePath, callback) {
|
||||
var ffprobe = 'ffprobe "' + filePath + '"';
|
||||
exec(ffprobe, function(err, stdout, stderr) {
|
||||
var dateMatch = FFPROBE_DATE.exec(stderr);
|
||||
var rotateMatch = FFPROBE_ROTATE.exec(stderr);
|
||||
callback(null, {
|
||||
date: dateMatch ? Date.parse(dateMatch[1]) : null,
|
||||
orientation: rotateMatch ? ROTATION_TABLE[rotateMatch[1]] : null
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
const moment = require('moment')
|
||||
|
||||
const MIME_REGEX = /([^/])\/(.*)/
|
||||
const EXIF_DATE_FORMAT = 'YYYY:MM:DD HH:mm:ssZ'
|
||||
|
||||
function File (dbFile) {
|
||||
this.path = dbFile.SourceFile
|
||||
this.fileDate = fileDate(dbFile)
|
||||
this.mediaType = mediaType(dbFile)
|
||||
this.exif = {
|
||||
date: exifDate(dbFile),
|
||||
caption: caption(dbFile)
|
||||
}
|
||||
}
|
||||
|
||||
function mediaType (dbFile) {
|
||||
const match = MIME_REGEX.exec(dbFile.File.MIMEType)
|
||||
if (match) return match[1]
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
function fileDate (dbFile) {
|
||||
return moment(dbFile.File.FileModifyDate, EXIF_DATE_FORMAT).valueOf()
|
||||
}
|
||||
|
||||
function exifDate (dbFile) {
|
||||
if (!dbFile.EXIF) return null
|
||||
return moment(dbFile.EXIF.DateTimeOriginal, EXIF_DATE_FORMAT).valueOf()
|
||||
}
|
||||
|
||||
function caption (dbFile) {
|
||||
return dbFile.EXIF ? dbFile.EXIF.ImageDescription : null
|
||||
}
|
||||
|
||||
module.exports = File
|
@ -1,111 +0,0 @@
|
||||
var _ = require('lodash');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var glob = require('glob');
|
||||
var async = require('async');
|
||||
var pad = require('pad');
|
||||
var progress = require('../utils/progress');
|
||||
var exif = require('./exif');
|
||||
|
||||
exports.update = function(opts, callback) {
|
||||
|
||||
var metadataPath = path.join(opts.output, 'metadata.json');
|
||||
var existing = null;
|
||||
var existingDate = null;
|
||||
|
||||
try {
|
||||
existing = require(metadataPath);
|
||||
existingDate = fs.statSync(metadataPath).mtime;
|
||||
} catch (ex) {
|
||||
existing = {};
|
||||
existingDate = 0;
|
||||
}
|
||||
|
||||
function findFiles(callback) {
|
||||
var globOptions = {
|
||||
cwd: opts.input,
|
||||
nonull: false,
|
||||
nocase: true
|
||||
};
|
||||
glob('**/*.{jpg,jpeg,png,gif,mp4,mov,mts,m2ts}', globOptions, callback);
|
||||
}
|
||||
|
||||
function pathAndDate(filePath, next) {
|
||||
var absolute = path.join(opts.input, filePath);
|
||||
fs.stat(absolute, function(err, stats) {
|
||||
next(null, {
|
||||
absolute: absolute,
|
||||
relative: filePath,
|
||||
fileDate: stats.mtime.getTime()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function newer(fileInfo) {
|
||||
var found = existing[fileInfo.relative];
|
||||
if (!found) return true;
|
||||
return fileInfo.fileDate > existingDate;
|
||||
}
|
||||
|
||||
function removeDeletedFiles(allFiles) {
|
||||
var existingPaths = _.keys(existing);
|
||||
var actualPaths = _.map(allFiles, 'relative');
|
||||
var deleted = _.difference(existingPaths, actualPaths);
|
||||
deleted.forEach(function(key) {
|
||||
delete existing[key];
|
||||
});
|
||||
return deleted.length > 0;
|
||||
}
|
||||
|
||||
function metadata(fileInfo, callback) {
|
||||
exif.read(fileInfo.absolute, function(err, exifData) {
|
||||
callback(null, {
|
||||
path: fileInfo.relative,
|
||||
fileDate: fileInfo.fileDate,
|
||||
mediaType: mediaType(fileInfo),
|
||||
exif: {
|
||||
date: exifData ? exifData.date : null,
|
||||
orientation: exifData ? exifData.orientation : null,
|
||||
caption: exifData ? exifData.caption: null
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function mediaType(fileInfo) {
|
||||
return fileInfo.relative.match(/\.(mp4|mov|mts|m2ts)$/i) ? 'video' : 'photo';
|
||||
}
|
||||
|
||||
function writeToDisk() {
|
||||
fs.writeFileSync(metadataPath, JSON.stringify(existing, null, ' '));
|
||||
}
|
||||
|
||||
findFiles(function(err, files) {
|
||||
var bar = progress.create('List all files', files.length);
|
||||
bar.tick(files.length);
|
||||
async.map(files, pathAndDate, function (err, allFiles) {
|
||||
var deleted = removeDeletedFiles(allFiles);
|
||||
var toProcess = allFiles.filter(newer);
|
||||
var count = toProcess.length;
|
||||
var bar = progress.create('Update metadata', count);
|
||||
if (count > 0) {
|
||||
bar.tick(0);
|
||||
async.mapLimit(toProcess, 100, function(fileInfo, next) {
|
||||
bar.tick();
|
||||
metadata(fileInfo, next);
|
||||
}, function(err, update) {
|
||||
update.forEach(function(fileInfo) {
|
||||
existing[fileInfo.path] = _.omit(fileInfo, 'path');
|
||||
});
|
||||
writeToDisk();
|
||||
callback(null, existing);
|
||||
});
|
||||
} else {
|
||||
bar.tick(1);
|
||||
if (deleted) writeToDisk();
|
||||
callback(null, existing);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
};
|
@ -1,9 +0,0 @@
|
||||
var File = require('./file');
|
||||
|
||||
exports.fromMetadata = function(metadata) {
|
||||
return {
|
||||
files: Object.keys(metadata).map(function(filepath) {
|
||||
return new File(filepath, metadata[filepath]);
|
||||
})
|
||||
}
|
||||
};
|
@ -1,12 +1,12 @@
|
||||
var pad = require('pad');
|
||||
var ProgressBar = require('progress');
|
||||
var pad = require('pad')
|
||||
var ProgressBar = require('progress')
|
||||
|
||||
exports.create = function(message, count) {
|
||||
if (count > 0) {
|
||||
var format = pad(message, 20) + '[:bar] :current/:total files';
|
||||
return new ProgressBar(format, { total: count, width: 20 });
|
||||
var format = pad(message, 20) + '[:bar] :current/:total files (:etas)'
|
||||
return new ProgressBar(format, { total: count, width: 20 })
|
||||
} else {
|
||||
var format = pad(message, 20) + '[:bar] up to date';
|
||||
return new ProgressBar(format, { total: 1, width: 20 });
|
||||
var format = pad(message, 20) + '[:bar] up to date'
|
||||
return new ProgressBar(format, { total: 1, width: 20, incomplete: '=' })
|
||||
}
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue