Restructured scanner to be a little more maintainable

Signed-off-by: James Ketrenos <james_git@ketrenos.com>
This commit is contained in:
James Ketr 2023-01-12 12:52:11 -08:00
parent d9f85bed30
commit 7fe6ae43ab
4 changed files with 436 additions and 363 deletions

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
*.pyc
.env .env
node_modules node_modules
bower_components
frontend/auto-clusters.html frontend/auto-clusters.html

View File

@ -17,4 +17,4 @@ services:
- ${PWD}/db:/website/db - ${PWD}/db:/website/db
- ${PWD}/config/local.json:/website/config/local.json - ${PWD}/config/local.json:/website/config/local.json
- /opt/ketrface/models:/root/.deepface - /opt/ketrface/models:/root/.deepface
# - ${PWD}:/website - ${PWD}:/website

View File

@ -5,7 +5,7 @@ const config = require("config"),
Promise = require("bluebird"), Promise = require("bluebird"),
picturesPath = config.get("picturesPath").replace(/\/$/, "") + "/"; picturesPath = config.get("picturesPath").replace(/\/$/, "") + "/";
const stat = function (_path) { const stat = async (_path) => {
if (_path.indexOf(picturesPath.replace(/\/$/, "")) == 0) { if (_path.indexOf(picturesPath.replace(/\/$/, "")) == 0) {
_path = _path.substring(picturesPath.length); _path = _path.substring(picturesPath.length);
} }
@ -22,7 +22,7 @@ const stat = function (_path) {
}); });
} }
const unlink = function (_path) { const unlink = async (_path) => {
if (_path.indexOf(picturesPath.replace(/\/$/, "")) == 0) { if (_path.indexOf(picturesPath.replace(/\/$/, "")) == 0) {
_path = _path.substring(picturesPath.length); _path = _path.substring(picturesPath.length);
} }
@ -39,7 +39,7 @@ const unlink = function (_path) {
}); });
} }
const mkdir = function (_path) { const mkdir = async (_path) => {
if (_path.indexOf(picturesPath) == 0) { if (_path.indexOf(picturesPath) == 0) {
_path = _path.substring(picturesPath.length); _path = _path.substring(picturesPath.length);
} }
@ -72,7 +72,7 @@ const mkdir = function (_path) {
}); });
} }
const exists = function(path) { const exists = async (path) => {
return stat(path).then(function() { return stat(path).then(function() {
return true; return true;
}).catch(function() { }).catch(function() {

View File

@ -134,110 +134,79 @@ function moveCorrupt(path, file) {
}); });
} }
function processBlock(items) { const determineImageDate = (asset, metadata) => {
const created = asset.stats.mtime,
filename = asset.filename;
if (items) { /* Attempt to find CREATED / MODIFIED date based on meta-data or
processQueue = processQueue.concat(items); * FILENAME patterns */
if (metadata.exif
&& metadata.exif.exif
&& metadata.exif.exif.DateTimeOriginal
&& !isNaN(metadata.exif.exif.DateTimeOriginal.valueOf())) {
asset.taken = moment(metadata.exif.exif.DateTimeOriginal).format();
asset.modified = moment(metadata.exif.exif.DateTimeOriginal).format();
if (asset.taken == "Invalid date"
|| asset.taken.replace(/T.*/, "") == "1899-11-30") {
setStatus(
`Invalid EXIF date information for ` +
`${asset.album.path + asset.filename}`);
return moment(created).format();
} }
if (processRunning) { return undefined;
/* Invoke once per second to check if there are items to process */
setTimeout(processBlock, 1000);
return;
} }
let processing = processQueue.splice(0), needsProcessing = [], duplicates = []; /* Attempt to infer the datestamp from the filename */
let date = moment(created).format();
processRunning = true; let match = filename.match(
/WhatsApp Image (20[0-9][0-9]-[0-9][0-9]-[0-9][0-9]) at (.*).(jpeg|jpg)/);
/* Sort to newest files to be processed first */ if (match) {
processing.sort(function(a, b) { date = moment((match[1] + " " + match[2]), "YYYY-MM-DD h.mm.ss a")
return b.stats.mtime - a.stats.mtime; .format();
}); if (date == "Invalid date") {
date = moment(created).format();
let toProcess = processing.length, lastMessage = moment(); }
setStatus("Items to be processed: " + toProcess); return date;
return Promise.mapSeries(processing, (asset) => {
if (!asset.raw) {
return;
} }
const path = asset.album.path; match = filename.match(
/(20[0-9][0-9]-?[0-9][0-9]-?[0-9][0-9])[_\-]?([0-9]{6})?/);
return exists(picturesPath + path + asset.filename).then(function(exist) { if (!match) {
if (exist) { return moment(created).format();
return asset;
} }
return mkdir(picturesPath + path + "raw").then(function() { if (match[2]) { /* Stamp had time in it */
return convertRawToJpg(path, asset.raw, asset.filename); date = moment(`${match[1]}${match[2]}`
}).then(function() { .replace(/-/g, ""), "YYYYMMDDHHmmss")
console.log("Done converting..."); .format();
}); } else {
}); date = moment(`${match[1]}`
}).then(() => { .replace(/-/g, ""), "YYYYMMDD")
return Promise.mapSeries(processing, (asset) => { .format();
return computeHash(picturesPath + asset.album.path + asset.filename).then(function(hash) {
asset.hash = hash;
return asset;
}).then(function(asset) {
return photoDB.sequelize.query("SELECT photohashes.*,photos.filename,albums.path FROM photohashes " +
"LEFT JOIN photos ON (photos.id=photohashes.photoId) " +
"LEFT JOIN albums ON (albums.id=photos.albumId) " +
"WHERE hash=:hash OR photoId=:id", {
replacements: asset,
type: photoDB.sequelize.QueryTypes.SELECT
}).then(function(results) {
let query;
if (results.length == 0) {
query = "INSERT INTO photohashes (hash,photoId) VALUES(:hash,:id)";
} else if (results[0].hash != asset.hash) {
query = "UPDATE photohashes SET hash=:hash WHERE photoId=:id";
} else if (results[0].photoId != asset.id) {
setStatus("Duplicate asset: " +
"'" + asset.album.path + asset.filename + "' is a copy of " +
"'" + results[0].path + results[0].filename + "'");
if (asset.duplicate != results[0].photoId) {
asset.duplicate = results[0].photoId;
duplicates.push(asset);
}
return null;
} }
/* Even if the hash doesn't need to be updated, the entry needs to be scanned */ if (date == "Invalid date") {
// console.log("process needed because of " + query); date = moment(created).format();
needsProcessing.push(asset);
if (!query) {
return asset;
} }
return photoDB.sequelize.query(query, { return date;
replacements: asset,
}).then(function() {
return asset;
});
});
}).then(function(asset) {
if (!asset) { /* The processed entry is a DUPLICATE. Skip it. */
return;
} }
var path = asset.album.path, const processImageAsset = async (asset) => {
file = asset.filename, let path = asset.album.path,
created = asset.stats.mtime, filename = asset.filename;
albumId = asset.album.id;
var src = picturesPath + path + file, let src = picturesPath + path + filename,
image = sharp(src); image = sharp(src);
return image/*.limitInputPixels(1073741824)*/ const metadata = await image/*.limitInputPixels(1073741824)*/
.metadata() .metadata()
.catch(error => { .catch(error => console.error(error) );
console.error(error);
})
.then((metadata) => {
if (metadata.exif) { if (metadata.exif) {
try { try {
metadata.exif = exif(metadata.exif); metadata.exif = exif(metadata.exif);
@ -259,136 +228,200 @@ function processBlock(items) {
asset.height = metadata.height; asset.height = metadata.height;
asset.added = moment().format(); asset.added = moment().format();
if (metadata.exif && metadata.exif.exif && metadata.exif.exif.DateTimeOriginal && !isNaN(metadata.exif.exif.DateTimeOriginal.valueOf())) { const updateDate = determineImageDate(asset, metadata);
asset.taken = moment(metadata.exif.exif.DateTimeOriginal).format(); if (updateDate) {
asset.modified = moment(metadata.exif.exif.DateTimeOriginal).format(); asset.taken = asset.modified = updateDate;
if (asset.taken == "Invalid date" || asset.taken.replace(/T.*/, "") == "1899-11-30") {
setStatus("Invalid EXIF date information for " + asset.album.path + asset.filename);
asset.taken = asset.modified = moment(created).format();
}
} else {
/* Attempt to infer the datestamp from the filename */
let date = moment(created).format();
let match = file.match(/WhatsApp Image (20[0-9][0-9]-[0-9][0-9]-[0-9][0-9]) at (.*).(jpeg|jpg)/);
if (match) {
date = moment((match[1]+" "+match[2]), "YYYY-MM-DD h.mm.ss a").format();
if (date == "Invalid date") {
date = moment(created).format();
}
} else {
match = file.match(/(20[0-9][0-9]-?[0-9][0-9]-?[0-9][0-9])[_\-]?([0-9]{6})?/);
if (match) {
if (match[2]) { /* Stamp had time in it */
date = moment((match[1]+""+match[2]).replace(/-/g, ""), "YYYYMMDDHHmmss").format();
} else {
date = moment(match[1].replace(/-/g, ""), "YYYYMMDD").format();
}
if (date == "Invalid date") {
date = moment(created).format();
}
} else {
date = moment(created).format();
}
}
asset.taken = asset.modified = date;
} }
let dst = picturesPath + path + "thumbs/" + file; let dst = picturesPath + path + "thumbs/" + filename;
return exists(dst).then(function(exist) { let onDisk = await exists(dst);
if (exist) { if (!onDisk) {
await image.resize(256, 256)
.withMetadata()
.toFile(dst)
.catch((error) => {
setStatus(`Error resizing image: ${dst}\n${error}`, "error");
throw error;
});
}
dst = picturesPath + path + "thumbs/scaled/" + filename;
if (!onDisk) {
await image.resize(Math.min(1024, metadata.width))
.withMetadata()
.toFile(dst)
.catch((error) => {
setStatus(`Error resizing image: ${dst}\n${error}`, "error");
throw error;
});
}
};
const processBlock = async (items) => {
if (items) {
processQueue = processQueue.concat(items);
}
if (processRunning) {
/* Invoke once per second to check if there are items to process */
setTimeout(processBlock, 1000);
return; return;
} }
return image.resize(256, 256).withMetadata().toFile(dst).catch(function(error) { let processing = processQueue.splice(0),
setStatus("Error resizing image: " + dst + "\n" + error, "error"); needsProcessing = [],
throw error; duplicates = [];
});
}).then(function() {
let dst = picturesPath + path + "thumbs/scaled/" + file;
return exists(dst).then(function(exist) {
if (exist) {
return;
}
return image.resize(Math.min(1024, metadata.width)).withMetadata().toFile(dst).catch(function(error) { processRunning = true;
setStatus("Error resizing image: " + dst + "\n" + error, "error");
throw error; /* Sort to newest files to be processed first */
processing.sort(function(a, b) {
return b.stats.mtime - a.stats.mtime;
}); });
});
}).then(function() { let toProcess = processing.length, lastMessage = moment();
return photoDB.sequelize.query("UPDATE photos SET " +
"added=:added,modified=:modified,taken=:taken,width=:width,height=:height,size=:size,scanned=CURRENT_TIMESTAMP " + setStatus("Items to be processed: " + toProcess);
"WHERE id=:id", {
replacements: asset, await Promise.mapSeries(processing, async (asset) => {
});
});
}).catch(function(error) {
setStatus("Error reading image " + src + ":\n" + error, "error");
return moveCorrupt(path, file);
});
}).then(function() {
toProcess--; toProcess--;
if (moment().add(-5, 'seconds') > lastMessage) { if (moment().add(-5, 'seconds') > lastMessage) {
setStatus("Items to be processed: " + toProcess); setStatus("Items to be processed: " + toProcess);
lastMessage = moment(); lastMessage = moment();
} }
});
}); /* Create JPG from RAW if there is a RAW file and no JPG */
}).catch(function(error) { if (asset.raw) {
setStatus("Error processing file. Continuing.", "error"); const path = asset.album.path;
throw error; const onDisk = await exists(picturesPath + path + asset.filename)
}).then(function() { if (!onDisk) {
setStatus("Completed processing queue. Marking " + duplicates.length + " duplicates."); await mkdir(picturesPath + path + "raw");
return photoDB.sequelize.transaction(function(transaction) { await convertRawToJpg(path, asset.raw, asset.filename);
return Promise.mapSeries(duplicates, function(asset) { console.log("Done converting...");
return photoDB.sequelize.query("UPDATE photos " + }
"SET duplicate=:duplicate,modified=CURRENT_TIMESTAMP,scanned=CURRENT_TIMESTAMP WHERE id=:id", { }
/* Update PHOTOHASH table */
asset.hash = await computeHash(
picturesPath + asset.album.path + asset.filename)
let results = await photoDB.sequelize.query(
"SELECT photohashes.*,photos.filename,albums.path FROM photohashes " +
"LEFT JOIN photos ON (photos.id=photohashes.photoId) " +
"LEFT JOIN albums ON (albums.id=photos.albumId) " +
"WHERE hash=:hash OR photoId=:id", {
replacements: asset,
type: photoDB.sequelize.QueryTypes.SELECT
});
let query;
if (results.length == 0) {
query = "INSERT INTO photohashes (hash,photoId) VALUES(:hash,:id)";
} else if (results[0].hash != asset.hash) {
query = "UPDATE photohashes SET hash=:hash WHERE photoId=:id";
} else if (results[0].photoId != asset.id) {
setStatus("Duplicate asset: " +
"'" + asset.album.path + asset.filename + "' is a copy of " +
"'" + results[0].path + results[0].filename + "'");
if (asset.duplicate != results[0].photoId) {
asset.duplicate = results[0].photoId;
duplicates.push(asset);
}
return; /* Done processing this asset (DUPLICATE) */
}
/* Update PHOTOHASH table if necessary */
if (query) {
await photoDB.sequelize.query(query, {
replacements: asset,
});
}
/* Additional processing is only skipped if the asset was a
* DUPLICATE above (the empty "return;") */
needsProcessing.push(asset);
try {
await processImageAsset(asset)
} catch (error) {
const path = asset.album.path,
filename = asset.filename;
setStatus(`Error reading image ` +
`${picturesPath}${path}${filename}:\n` +
`${error}`, "error");
await moveCorrupt(path, filename);
return;
}
/* Update the DB with the image information */
await photoDB.sequelize.query("UPDATE photos SET " +
"added=:added,modified=:modified,taken=:taken,width=:width,height=:height,size=:size,scanned=CURRENT_TIMESTAMP " +
"WHERE id=:id", {
replacements: asset, replacements: asset,
transaction: transaction
}); });
}); });
/* Process the DUPLICATES */
setStatus(
`Completed processing queue. Marking ${duplicates.length} duplicates.`);
await photoDB.sequelize.transaction(async (t) => {
await Promise.mapSeries(duplicates, async (item) => {
await photoDB.sequelize.query(
"UPDATE photos " +
"SET duplicate=:duplicate,modified=CURRENT_TIMESTAMP,scanned=CURRENT_TIMESTAMP WHERE id=:id", {
replacements: item,
transaction: t
}); });
}).then(function() { });
})
setStatus("Looking for removed assets"); setStatus("Looking for removed assets");
return photoDB.sequelize.query("SELECT photos.scanned,photos.id,photos.filename,albums.path FROM photos " + let results = await photoDB.sequelize.query(
"SELECT photos.scanned,photos.id,photos.filename,albums.path FROM photos " +
"LEFT JOIN albums ON (albums.id=photos.albumId) " + "LEFT JOIN albums ON (albums.id=photos.albumId) " +
"WHERE photos.deleted=0 AND (DATETIME(photos.scanned)<DATETIME(:lastScan) OR photos.scanned IS NULL)", { "WHERE photos.deleted=0 AND (DATETIME(photos.scanned)<DATETIME(:lastScan) OR photos.scanned IS NULL)", {
replacements: { replacements: {
lastScan: lastScan lastScan: lastScan
}, },
type: photoDB.sequelize.QueryTypes.SELECT type: photoDB.sequelize.QueryTypes.SELECT
}).then(function(results) { });
let deleted = []; let deleted = [];
setStatus("Checking " + results.length + " assets to see if they are on disk."); setStatus(`Checking ${results.length} assets to see if they are on disk.`);
return Promise.map(results, function(asset) { await Promise.map(results, async (item) => {
return exists(asset.path + asset.filename).then(function(exist) { const onDisk = await exists(item.path + item.filename);
if (!exist) { if (!onDisk) {
setStatus(asset.path + asset.filename + " no longer exists on disk. Marking as deleted."); setStatus(
deleted.push(asset.id); `${item.path}${item.filename} no longer exists on disk. ` +
`Marking as deleted.`);
deleted.push(item.id);
} }
}); });
}).then(function() {
return photoDB.sequelize.query("UPDATE photos SET deleted=1,scanned=CURRENT_TIMESTAMP WHERE id IN (:deleted)", { if (deleted.length) {
replacements: { await photoDB.sequelize.transaction(async (t) => {
deleted: deleted await photoDB.sequelize.query(
"UPDATE photos SET deleted=1,scanned=CURRENT_TIMESTAMP " +
"WHERE id IN (:deleted)", {
replacements: { deleted },
transaction: t
} }
}).then(function() { );
return photoDB.sequelize.query("DELETE FROM photohashes WHERE photoId IN (:deleted)", { await photoDB.sequelize.query(
replacements: { "DELETE FROM photohashes WHERE photoId IN (:deleted)", {
deleted: deleted replacements: { deleted },
transaction: t
} }
);
}); });
}).then(function() { }
setStatus(deleted.length + " assets deleted.");
}); setStatus(`${deleted.length} assets deleted.`);
});
});
}).then(function() {
processRunning = false; processRunning = false;
});
} }
function scanDir(parent, path) { function scanDir(parent, path) {
@ -435,7 +468,9 @@ function scanDir(parent, path) {
return stat(filepath).then(function(stats) { return stat(filepath).then(function(stats) {
if (stats.isDirectory()) { if (stats.isDirectory()) {
filepath += "/"; filepath += "/";
return scanDir(album, filepath).spread(function(_albums, _assets) { return scanDir(album, filepath).then((res) => {
const _albums = res.albums,
_assets = res.assets;
album.allAssetCount += _assets.length; album.allAssetCount += _assets.length;
album.allAlbumCount += _albums.length + 1; album.allAlbumCount += _albums.length + 1;
albums = albums.concat(_albums); albums = albums.concat(_albums);
@ -475,11 +510,11 @@ function scanDir(parent, path) {
} }
}); });
}).then(function() { }).then(function() {
return [ albums, assets ]; return { albums, assets };
}); });
} }
function findOrCreateDBAlbum(transaction, album) { const findOrCreateDBAlbum = async (t, album) => {
let query = "SELECT id FROM albums WHERE path=:path AND "; let query = "SELECT id FROM albums WHERE path=:path AND ";
if (!album.parent) { if (!album.parent) {
query += "parentId IS NULL"; query += "parentId IS NULL";
@ -494,30 +529,29 @@ function findOrCreateDBAlbum(transaction, album) {
query += "parentId=:parentId"; query += "parentId=:parentId";
} }
return photoDB.sequelize.query(query, { const results = await photoDB.sequelize.query(query, {
replacements: album, replacements: album,
type: photoDB.sequelize.QueryTypes.SELECT type: photoDB.sequelize.QueryTypes.SELECT
}).then(function(results) { });
if (results.length == 0) { if (results.length == 0) {
if (!album.parent) { if (!album.parent) {
setStatus("Creating top level album: " + picturesPath, "warn" ); setStatus("Creating top level album: " + picturesPath, "warn" );
} }
return photoDB.sequelize.query("INSERT INTO albums (path,parentId,name) VALUES(:path,:parentId,:name)", { return photoDB.sequelize.query("INSERT INTO albums (path,parentId,name) VALUES(:path,:parentId,:name)", {
replacements: album, replacements: album,
transaction: transaction transaction: t
}).then(array => { }).then(array => {
return array[1].lastID; album.id = array[1].lastID;
}); });
} else { } else {
return results[0].id; album.id = results[0].id;
}
}).then(function(id) {
album.id = id;
return id;
});
} }
const findOrUpdateDBAsset = async (transaction, asset) => { return album.id;
}
const findOrUpdateDBAsset = async (t, asset) => {
if (!asset.album || !asset.album.id) { if (!asset.album || !asset.album.id) {
let error = "Asset being processed without an album"; let error = "Asset being processed without an album";
setStatus(error, "warn"); setStatus(error, "warn");
@ -538,7 +572,7 @@ const findOrUpdateDBAsset = async (transaction, asset) => {
return await photoDB.sequelize.query("INSERT INTO photos " + return await photoDB.sequelize.query("INSERT INTO photos " +
"(albumId,filename,name,size) VALUES(:albumId,:filename,:name,:size)", { "(albumId,filename,name,size) VALUES(:albumId,:filename,:name,:size)", {
replacements: asset, replacements: asset,
transaction: transaction transaction: t
}).then(array => { }).then(array => {
asset.id = array[1].lastID; asset.id = array[1].lastID;
return asset; return asset;
@ -615,7 +649,7 @@ function setStatus(status, level) {
} }
} }
function doScan() { const doScan = async () => {
/* 1. Scan for all assets which will be managed by the system. readdir /* 1. Scan for all assets which will be managed by the system. readdir
* 2. Check if entry in DB. Check mod-time in DB vs. stats from #1 * 2. Check if entry in DB. Check mod-time in DB vs. stats from #1
* - For albums * - For albums
@ -636,50 +670,77 @@ function doScan() {
let needsProcessing = []; let needsProcessing = [];
if (scanningStatus.length != 0) { if (scanningStatus.length != 0) {
return Promise.resolve(scanningStatus); return scanningStatus;
} }
return scanDir(null, picturesPath).spread(function(albums, assets) { let { albums, assets } = await scanDir(null, picturesPath);
setStatus("Found " + assets.length + " assets in " + albums.length + " albums after " +
((Date.now() - now) / 1000) + "s"); setStatus(
/* One at a time, in series, as the album[] array has parents first, then descendants. `Found ${assets.length} assets in ${albums.length} albums after ` +
* Operating in parallel could result in a child being searched for prior to the parent */ `${(Date.now() - now) / 1000}s`);
/* One at a time, in series, as the album[] array has parents
* first, then descendants.
* Operating in parallel could result in a child being searched for
* prior to the parent */
now = Date.now(); now = Date.now();
let toProcess = albums.length, lastMessage = moment(); try {
return photoDB.sequelize.transaction(function(transaction) { let toProcess = albums.length,
return Promise.mapSeries(albums, function(album) { lastMessage = moment();
return findOrCreateDBAlbum(transaction, album).then(function() { await photoDB.sequelize.transaction(async (t) => {
await Promise.mapSeries(albums, async (album) => {
await findOrCreateDBAlbum(t, album);
toProcess--; toProcess--;
if (moment().add(-5, 'seconds') > lastMessage) { if (moment().add(-5, 'seconds') > lastMessage) {
setStatus("Albums to be created in DB: " + toProcess); setStatus(`Albums to be created in DB: ${toProcess}`);
lastMessage = moment(); lastMessage = moment();
} }
}); });
}); });
}).then(function() { } catch (error) {
setStatus("Processed " + albums.length + " album DB entries in " + console.error(error);
((Date.now() - now) / 1000) + "s"); process.exit(-1);
now = Date.now();
setStatus(assets.length + " assets remaining to be verified/updated. ETA N/A");
let processed = 0, start = Date.now(), last = 0, updateScanned = [], newEntries = 0;
return photoDB.sequelize.transaction(function(transaction) {
return Promise.map(assets, function(asset) {
return Promise.resolve(asset).then(function(asset) {
/* If both mtime and ctime of the asset are older than the lastScan, skip it
*
* Can only do this after a full scan has occurred */
if (lastScan != null && asset.stats.mtime < lastScan && asset.stats.ctime < lastScan) {
return asset;
} }
return findOrUpdateDBAsset(transaction, asset).then(function(asset) { setStatus(
`Processed ${albums.length} album DB entries in ` +
`${(Date.now() - now) / 1000}s`);
now = Date.now();
setStatus(
`${assets.length} assets remaining to be verified/updated. ETA N/A`);
let updateScanned = [],
newEntries = 0;
try {
let processed = 0,
start = Date.now(),
last = 0;
await photoDB.sequelize.transaction(async (t) => {
await Promise.map(assets, async (asset) => {
/* If both mtime and ctime of the asset are older than the
* lastScan, skip it
* Can only do this after a full scan has occurred */
if (lastScan != null
&& asset.stats.mtime < lastScan
&& asset.stats.ctime < lastScan) {
return;
}
asset = await findOrUpdateDBAsset(t, asset);
if (!asset.scanned) { if (!asset.scanned) {
newEntries++; newEntries++;
} }
if (!asset.scanned || asset.scanned < asset.stats.mtime || !asset.modified) {
if (!asset.scanned
|| asset.scanned < asset.stats.mtime
|| !asset.modified) {
// if (!asset.scanned) { console.log("no scan date on asset"); } // if (!asset.scanned) { console.log("no scan date on asset"); }
// if (asset.scanned < asset.stats.mtime) { console.log("scan date older than mtime"); } // if (asset.scanned < asset.stats.mtime) { console.log("scan date older than mtime"); }
// if (!asset.modified) { console.log("no mtime."); } // if (!asset.modified) { console.log("no mtime."); }
@ -687,68 +748,78 @@ function doScan() {
} else { } else {
updateScanned.push(asset.id); updateScanned.push(asset.id);
} }
return asset;
}).then(function(asset) {
return asset;
});
}).then(function(asset) {
processed++; processed++;
let elapsed = Date.now() - start; let elapsed = Date.now() - start;
if (elapsed < 5000) { if (elapsed < 5000) {
return asset; return;
} }
let remaining = assets.length - processed, eta = Math.ceil((elapsed / 1000) * remaining / (processed - last)); let remaining = assets.length - processed,
setStatus(remaining + " assets remaining be verified/updated " + eta = Math.ceil((elapsed / 1000) * remaining / (processed - last));
"(" + newEntries + " new entries, " + needsProcessing.length + " need processing," + (processed - newEntries) + " up-to-date so far). ETA " + eta + "s"); setStatus(
`${remaining} assets remaining be verified/updated (${newEntries} ` +
`new entries, ${needsProcessing.length} need processing, ` +
`${(processed - newEntries)} up-to-date so far). ETA ${eta}s`
);
last = processed; last = processed;
start = Date.now(); start = Date.now();
});
}, { }, {
concurrency: 10 concurrency: 10
}); });
}).then(function() { });
} catch (error) {
console.error(error);
process.exit(-1);
}
if (updateScanned.length) { if (updateScanned.length) {
return photoDB.sequelize.query("UPDATE photos SET scanned=CURRENT_TIMESTAMP WHERE id IN (:ids)", { await photoDB.sequelize.query(
"UPDATE photos SET scanned=CURRENT_TIMESTAMP WHERE id IN (:ids)", {
replacements: { replacements: {
ids: updateScanned ids: updateScanned
} }
}).then(function() { });
setStatus("Updated scan date of " + updateScanned.length + " assets"); setStatus("Updated scan date of " + updateScanned.length + " assets");
updateScanned = []; updateScanned = [];
});
} }
}).then(function() {
setStatus(newEntries + " assets are new. " + (needsProcessing.length - newEntries) + " assets have been modified.\n" + setStatus(
needsProcessing.length + " assets need HASH computed. " + (assets.length - needsProcessing.length) + " need no update.");; `${newEntries} assets are new. ` +
`${needsProcessing.length - newEntries} assets have been ` +
`modified.\n${needsProcessing.length} assets need HASH computed. ` +
`${assets.length - needsProcessing.length} need no update.`);
processBlock(needsProcessing); processBlock(needsProcessing);
needsProcessing = []; needsProcessing = [];
}).then(function() {
setStatus("Scanned " + assets.length + " asset DB entries in " + setStatus(
((Date.now() - now) / 1000) + "s"); `Scanned ${assets.length} asset DB entries in ` +
`${(Date.now() - now) / 1000}s`);
assets = []; assets = [];
});
}); setStatus(
}).then(function() { `Total time to initialize DB and all scans: ` +
setStatus("Total time to initialize DB and all scans: " + ((Date.now() - initialized) / 1000) + "s"); `${(Date.now() - initialized) / 1000}s`);
return photoDB.sequelize.query("SELECT max(scanned) AS scanned FROM photos", {
let results = await photoDB.sequelize.query(
"SELECT max(scanned) AS scanned FROM photos", {
type: photoDB.sequelize.QueryTypes.SELECT type: photoDB.sequelize.QueryTypes.SELECT
}).then(function(results) { });
if (results[0].scanned == null) { if (results[0].scanned == null) {
lastScan = new Date("1800-01-01"); lastScan = new Date("1800-01-01");
} else { } else {
lastScan = new Date(results[0].scanned); lastScan = new Date(results[0].scanned);
} }
setStatus("Updating any asset newer than " + moment(lastScan).format());
}); setStatus(`Updating any asset newer than ${moment(lastScan).format()}`);
}).then(function() {
setStatus("idle"); setStatus("idle");
return "scan complete"; return "scan complete";
}).catch(function(error) {
setStatus(error);
throw error;
});
} }
module.exports = { module.exports = {