430 lines
14 KiB
JavaScript
430 lines
14 KiB
JavaScript
"use strict";
|
|
|
|
const Promise = require("bluebird"),
|
|
fs = require("fs"),
|
|
config = require("config"),
|
|
moment = require("moment");
|
|
|
|
let scanning = 0;
|
|
|
|
let photoDB = null;
|
|
|
|
const picturesPath = config.get("picturesPath");
|
|
|
|
const processQueue = [], triedClean = [];
|
|
|
|
function removeNewerFile(path, fileA, fileB) {
|
|
fs.stat(path + "/" + fileA, function(err, statsA) {
|
|
if (err) {
|
|
return;
|
|
}
|
|
fs.stat(path + "/" + fileB, function(err, statsB) {
|
|
if (err) {
|
|
return;
|
|
}
|
|
if (statsA.mtime > statsB.mtime) {
|
|
console.log("Removing file by moving to 'corrupt':" + fileA);
|
|
moveCorrupt(path, fileA);
|
|
} else {
|
|
console.log("Removing file by moving to 'corrupt':" + fileB);
|
|
moveCorrupt(path, fileB);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function scanDir(parent, path) {
|
|
let extensions = [ "jpg", "jpeg", "png", "gif", "nef" ],
|
|
re = new RegExp("\.((" + extensions.join(")|(") + "))$", "i"),
|
|
replacements = {
|
|
path: path.slice(picturesPath.length),
|
|
name: path.replace(/.*\//, "").replace(/_/g, " "),
|
|
parent: parent || null
|
|
};
|
|
|
|
let query = "SELECT id FROM albums WHERE path=:path AND ";
|
|
if (!parent) {
|
|
query += "parentId IS NULL";
|
|
} else {
|
|
query += "parentId=:parent";
|
|
}
|
|
|
|
return photoDB.sequelize.query(query, {
|
|
replacements: replacements,
|
|
type: photoDB.sequelize.QueryTypes.SELECT
|
|
}).then(function(results) {
|
|
if (results.length == 0) {
|
|
// console.log("Adding " + path + " under " + parent, replacements);
|
|
return photoDB.sequelize.query("INSERT INTO albums (path,parentId,name) VALUES(:path,:parent,:name)", {
|
|
replacements: replacements
|
|
}).then(function(results) {
|
|
return results[1].lastID;
|
|
});
|
|
} else {
|
|
return results[0].id;
|
|
}
|
|
}).then(function(parent) {
|
|
return new Promise(function(resolve, reject) {
|
|
console.log("Scanning " + replacements.path);
|
|
|
|
fs.readdir(path, function(err, files) {
|
|
if (err) {
|
|
console.warn(" Could not readdir " + path);
|
|
return resolve(null);
|
|
}
|
|
|
|
scanning++;
|
|
|
|
let hasThumbs = false;
|
|
for (let i = 0; i < files.length; i++) {
|
|
if (files[i] == "thumbs") {
|
|
hasThumbs = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
let tmp;
|
|
if (!hasThumbs) {
|
|
tmp = mkdirPromise(path + "/thumbs");
|
|
} else {
|
|
tmp = Promise.resolve();
|
|
}
|
|
|
|
/* Remove 'thumbs' and 'raw' directories from being processed */
|
|
files = files.filter(function(file) {
|
|
for (var i = 0; i < files.length; i++) {
|
|
/* If this file has an original NEF on the system, don't add the JPG to the DB */
|
|
if (/\.nef$/i.exec(files[i]) && file == files[i].replace(/\.nef$/i, ".jpg")) {
|
|
return false;
|
|
}
|
|
|
|
/* If there is a different CASE (eg. JPG vs jpg) don't add it, and remove the 'lower case'
|
|
* version from disk. */
|
|
if (file != files[i] && file.toUpperCase() == files[i]) {
|
|
removeNewerFile(path, file, files[i]);
|
|
console.log("Duplicate file in " + path + ": ", file, files[i]);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return file != "raw" && file != "thumbs" && file != ".git" && file != "corrupt";
|
|
});
|
|
|
|
return tmp.then(function() {
|
|
return Promise.map(files, function(file) {
|
|
let filepath = path + "/" + file;
|
|
|
|
return new Promise(function(resolve, reject) {
|
|
fs.stat(filepath, function(err, stats) {
|
|
if (err) {
|
|
console.warn("Could not stat " + filepath);
|
|
return resolve(false);
|
|
}
|
|
|
|
if (stats.isDirectory()) {
|
|
return scanDir(parent, filepath, stats).then(function(entry) {
|
|
return resolve(true);
|
|
});
|
|
}
|
|
|
|
/* Check file extensions */
|
|
if (!re.exec(file)) {
|
|
return resolve(true);
|
|
}
|
|
|
|
|
|
const replacements = {
|
|
path: path.slice(picturesPath.length),
|
|
filename: file.replace(/\.nef$/i, ".jpg") /* We will be converting from NEF => JPG */
|
|
};
|
|
|
|
return photoDB.sequelize.query("SELECT id FROM photos WHERE path=:path AND filename=:filename", {
|
|
replacements: replacements,
|
|
type: photoDB.sequelize.QueryTypes.SELECT
|
|
}).then(function(photo) {
|
|
if (photo.length == 0) {
|
|
processQueue.push([ replacements.path, file, stats.mtime, parent ]);
|
|
}
|
|
return resolve(true);
|
|
});
|
|
});
|
|
});
|
|
}, {
|
|
concurrency: 1
|
|
}).then(function() {
|
|
scanning--;
|
|
if (scanning == 0) {
|
|
const endStamp = Date.now();
|
|
console.log("Scanning completed in " + Math.round(((endStamp - startStamp))) + "ms. " + processQueue.length + " items to process.");
|
|
}
|
|
}).then(function() {
|
|
return resolve();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
const startStamp = Date.now();
|
|
|
|
let processRunning = false;
|
|
|
|
const { spawn } = require('child_process');
|
|
|
|
const sharp = require("sharp"), exif = require("exif-reader");
|
|
|
|
function mkdirPromise(path) {
|
|
return new Promise(function(resolve, reject) {
|
|
fs.stat(path, function(err, stats) {
|
|
if (err && err.code != 'ENOENT') {
|
|
console.warn("Could not stat " + path + "/raw");
|
|
return reject();
|
|
}
|
|
|
|
if (!err) {
|
|
return resolve();
|
|
}
|
|
|
|
fs.mkdir(path, function(err) {
|
|
if (err) {
|
|
return reject("Unable to create " + path, err);
|
|
}
|
|
return resolve();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function existsPromise(path) {
|
|
return new Promise(function(resolve, reject) {
|
|
fs.stat(path, function(err, stats) {
|
|
if (!err) {
|
|
return resolve(true);
|
|
}
|
|
if (err.code == 'ENOENT') {
|
|
return resolve(false);
|
|
}
|
|
return reject(err);
|
|
});
|
|
})
|
|
}
|
|
|
|
function convertNefToJpg(path, file) {
|
|
console.log("Converting " + path + "/" + file);
|
|
|
|
path = picturesPath + path;
|
|
|
|
return new Promise(function(resolve, reject) {
|
|
fs.stat(path + "/" + file.replace(/\.nef$/i, ".jpg"), function(err, stats) {
|
|
if (!err) {
|
|
console.log("Skipping already converted file: " + file);
|
|
return resolve();
|
|
}
|
|
|
|
const ufraw = spawn("ufraw-batch", [
|
|
"--silent",
|
|
"--wb=camera",
|
|
"--rotate=camera",
|
|
"--out-type=jpg",
|
|
"--compression=90",
|
|
"--exif",
|
|
"--overwrite",
|
|
"--output", path + "/" + file.replace(/\.nef$/i, ".jpg"),
|
|
path + "/" + file
|
|
]);
|
|
|
|
let error = "";
|
|
ufraw.stderr.on('data', function(data) {
|
|
error += data;
|
|
});
|
|
|
|
ufraw.on('close', function(code) {
|
|
if (code != 0) {
|
|
return reject("UFRAW for " + path + "/" + file + " returned an error: ", error);
|
|
}
|
|
return mkdirPromise(path + "/raw").then(function() {
|
|
fs.rename(path + "/" + file, path + "/raw/" + file, function(err) {
|
|
if (err) {
|
|
console.error("Unable to move RAW file: " + path + "/" + file);
|
|
return reject(err);
|
|
}
|
|
return resolve();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function moveCorrupt(path, file) {
|
|
if (path.indexOf(picturesPath) != 0) {
|
|
path = picturesPath + path;
|
|
}
|
|
|
|
console.warn("Moving corrupt file '" + file + "' to " + path + "/corrupt");
|
|
|
|
return mkdirPromise(path + "/corrupt").then(function() {
|
|
return new Promise(function(resolve, reject) {
|
|
fs.rename(path + "/" + file, path + "/corrupt/" + file, function(err) {
|
|
if (err) {
|
|
console.error("Unable to move corrupt file: " + path + "/" + file);
|
|
return reject(err);
|
|
} else {
|
|
return resolve();
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function triggerWatcher() {
|
|
setTimeout(triggerWatcher, 1000);
|
|
|
|
if (!processRunning && processQueue.length) {
|
|
let lastMessage = moment(), toProcess = processQueue.length, processing = processQueue.splice(0);
|
|
processRunning = true;
|
|
|
|
/* Sort to newest files to be processed first */
|
|
processing.sort(function(a, b) {
|
|
return a[2] - b[2];
|
|
});
|
|
|
|
return Promise.map(processing, function(entry) {
|
|
var path = entry[0], file = entry[1], created = entry[2], albumId = entry[3];
|
|
|
|
// console.log("Processing " + src);
|
|
|
|
let tmp = Promise.resolve(file);
|
|
/* If this is a Nikon RAW file, convert it to JPG and move to /raw dir */
|
|
if (/\.nef$/i.exec(file)) {
|
|
tmp = existsPromise(picturesPath + path + "/" + file.replace(/\.nef$/i, ".jpg")).then(function(exists) {
|
|
if (exists) {
|
|
return file.replace(/\.nef$/i, ".jpg"); /* We converted from NEF => JPG */
|
|
}
|
|
|
|
return mkdirPromise(picturesPath + path + "/raw").then(function() {
|
|
return convertNefToJpg(path, file);
|
|
}).then(function() {
|
|
return file.replace(/\.nef$/i, ".jpg"); /* We converted from NEF => JPG */
|
|
});
|
|
});
|
|
}
|
|
|
|
return tmp.then(function(file) {
|
|
var src = picturesPath + path + "/" + file,
|
|
dst = picturesPath + path + "/thumbs/" + file,
|
|
image = sharp(src);
|
|
|
|
return image.limitInputPixels(1073741824).metadata().then(function(metadata) {
|
|
if (metadata.exif) {
|
|
metadata.exif = exif(metadata.exif);
|
|
delete metadata.exif.thumbnail;
|
|
delete metadata.exif.image;
|
|
for (var key in metadata.exif.exif) {
|
|
if (Buffer.isBuffer(metadata.exif.exif[key])) {
|
|
metadata.exif.exif[key] = "Buffer[" + metadata.exif.exif[key].length + "]";
|
|
}
|
|
}
|
|
}
|
|
|
|
let replacements = {
|
|
albumId: albumId,
|
|
name: file.replace(/.[^.]*$/, ""),
|
|
path: path,
|
|
filename: file,
|
|
width: metadata.width,
|
|
height: metadata.height,
|
|
added: moment().format()
|
|
};
|
|
|
|
if (metadata.exif && metadata.exif.exif && metadata.exif.exif.DateTimeOriginal && !isNaN(metadata.exif.exif.DateTimeOriginal.valueOf())) {
|
|
replacements.taken = moment(metadata.exif.exif.DateTimeOriginal).format();
|
|
replacements.modified = moment(metadata.exif.exif.DateTimeOriginal).format();
|
|
|
|
if (replacements.taken == "Invalid date" || replacements.taken.replace(/T.*/, "") == "1899-11-30") {
|
|
console.log("Invalid EXIF date information: ", JSON.stringify(metadata.exif.exif));
|
|
replacements.taken = replacements.modified = moment(created).format();
|
|
}
|
|
} else {
|
|
let patterns = /(20[0-9][0-9]-?[0-9][0-9]-?[0-9][0-9])[_\-]?([0-9]*)/, date = moment(created).format();
|
|
let match = file.match(patterns);
|
|
if (match) {
|
|
date = moment(match[1].replace(/-/g, ""), "YYYYMMDD").format();
|
|
if (date == "Invalid date") {
|
|
date = moment(created).format();
|
|
}
|
|
} else {
|
|
date = moment(created).format();
|
|
}
|
|
replacements.taken = replacements.modified = date;
|
|
}
|
|
|
|
return existsPromise(dst).then(function(exists) {
|
|
let resize;
|
|
if (!exists) {
|
|
resize = image.resize(256, 256).toFile(dst);
|
|
} else {
|
|
resize = Promise.resolve();
|
|
}
|
|
|
|
return resize.then(function() {
|
|
return photoDB.sequelize.query("INSERT INTO photos " +
|
|
"(albumId,path,filename,added,modified,taken,width,height,name)" +
|
|
"VALUES(:albumId,:path,:filename,DATE(:added),DATE(:modified),DATE(:taken),:width,:height,:name)", {
|
|
replacements: replacements
|
|
});
|
|
}).catch(function(error) {
|
|
console.error("Error resizing image, writing to disc, or updating DB: " + src, error);
|
|
throw error;
|
|
});
|
|
}).then(function() {
|
|
toProcess--;
|
|
if (moment().add(-5, 'seconds') > lastMessage) {
|
|
console.log("Items to be processed: " + toProcess);
|
|
lastMessage = moment();
|
|
}
|
|
});
|
|
}).catch(function(error) {
|
|
console.error("Error reading image " + src + ": ", error);
|
|
return moveCorrupt(path, file).then(function() {
|
|
|
|
/* If the original file was not a NEF, we are done... */
|
|
if (!/\.nef$/i.exec(entry[1])) {
|
|
return;
|
|
}
|
|
|
|
/* ... otherwise, attempt to re-convert the NEF->JPG and then resize again */
|
|
for (var i = 0; i < triedClean.length; i++) {
|
|
if (triedClean[i] == path + "/" + file) {
|
|
/* Move the NEF to /corrupt as well so it isn't re-checked again and again... */
|
|
// return moveCorrupt(path, entry[1]);
|
|
|
|
console.error("Already attempted to convert NEF to JPG: " + path + "/" + file);
|
|
return;
|
|
}
|
|
}
|
|
|
|
console.warn("Adding " + path + "/" + file + " back onto processing queue.");
|
|
triedClean.push(path + "/" + file);
|
|
processQueue.push([ path, file, created, albumId ]);
|
|
});
|
|
});
|
|
});
|
|
}, {
|
|
concurrency: 1
|
|
}).then(function() {
|
|
console.log("Completed processing queue.");
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
scan: function (db) {
|
|
photoDB = db;
|
|
return scanDir(null, picturesPath).then(function() {
|
|
triggerWatcher();
|
|
});
|
|
}
|
|
};
|