1100 lines
34 KiB
JavaScript
Executable File
1100 lines
34 KiB
JavaScript
Executable File
"use strict";
|
|
|
|
const express = require("express"),
|
|
fs = require("fs"),
|
|
config = require("config"),
|
|
moment = require("moment-holiday"),
|
|
crypto = require("crypto"),
|
|
util = require("util"),
|
|
Promise = require("bluebird");
|
|
|
|
require("../lib/pascha.js")(moment);
|
|
|
|
const execFile = util.promisify(require("child_process").execFile);
|
|
|
|
let photoDB;
|
|
|
|
require("../db/photos").then(function(db) {
|
|
photoDB = db;
|
|
});
|
|
|
|
const router = express.Router();
|
|
const picturesPath = config.get("picturesPath").replace(/\/$/, "") + "/";
|
|
|
|
const unlink = function (_path) {
|
|
if (_path.indexOf(picturesPath.replace(/\/$/, "")) == 0) {
|
|
_path = _path.substring(picturesPath.length);
|
|
}
|
|
|
|
let path = picturesPath + _path;
|
|
|
|
return new Promise(function (resolve, reject) {
|
|
fs.unlink(path, function (error, stats) {
|
|
if (error) {
|
|
return reject(error);
|
|
}
|
|
return resolve(stats);
|
|
});
|
|
});
|
|
}
|
|
|
|
const rename = function (_src, _dst) {
|
|
if (_src.indexOf(picturesPath.replace(/\/$/, "")) == 0) {
|
|
_src = _src.substring(picturesPath.length);
|
|
}
|
|
if (_dst.indexOf(picturesPath.replace(/\/$/, "")) == 0) {
|
|
_dst = _dst.substring(picturesPath.length);
|
|
}
|
|
|
|
let src = picturesPath + _src,
|
|
dst = picturesPath + _dst;
|
|
|
|
return new Promise(function (resolve, reject) {
|
|
fs.rename(src, dst, function (error, stats) {
|
|
if (error) {
|
|
return reject(error);
|
|
}
|
|
return resolve(stats);
|
|
});
|
|
});
|
|
}
|
|
|
|
|
|
const computeHash = function(_filepath) {
|
|
if (_filepath.indexOf(picturesPath.replace(/\/$/, "")) == 0) {
|
|
_filepath = _filepath.substring(picturesPath.length);
|
|
}
|
|
|
|
let filepath = picturesPath + _filepath;
|
|
return new Promise(function(resolve, reject) {
|
|
let input = fs.createReadStream(filepath),
|
|
hash = crypto.createHash("sha256");
|
|
if (!input) {
|
|
return reject();
|
|
}
|
|
|
|
input.on("readable", function() {
|
|
const data = input.read();
|
|
if (data) {
|
|
hash.update(data);
|
|
} else {
|
|
input.close();
|
|
resolve(hash.digest("hex"));
|
|
hash = null;
|
|
input = null;
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
const stat = function (_path) {
|
|
if (_path.indexOf(picturesPath.replace(/\/$/, "")) == 0) {
|
|
_path = _path.substring(picturesPath.length);
|
|
}
|
|
|
|
let path = picturesPath + _path;
|
|
|
|
return new Promise(function (resolve, reject) {
|
|
fs.stat(path, function (error, stats) {
|
|
if (error) {
|
|
return reject(error);
|
|
}
|
|
return resolve(stats);
|
|
});
|
|
});
|
|
}
|
|
|
|
const sharp = require("sharp");
|
|
|
|
const inProcess = [];
|
|
|
|
|
|
router.put("/:id", function(req, res/*, next*/) {
|
|
if (!req.user.maintainer) {
|
|
return res.status(401).send("Unauthorized to modify photos.");
|
|
}
|
|
|
|
const replacements = {
|
|
id: req.params.id
|
|
};
|
|
|
|
if (inProcess.indexOf(req.params.id) != -1) {
|
|
console.log("Request to modify asset currently in modification: " + req.params.id);
|
|
return res.status(409).send("Asset " + req.params.id + " is already being processed. Please try again.");
|
|
}
|
|
|
|
inProcess.push(req.params.id);
|
|
|
|
console.log("PUT /" + replacements.id, req.query);
|
|
switch (req.query.a) {
|
|
case "undelete":
|
|
console.log("Undeleting " + req.params.id);
|
|
return photoDB.sequelize.query("UPDATE photos SET deleted=0,updated=CURRENT_TIMESTAMP WHERE id=:id", {
|
|
replacements: replacements
|
|
}).then(function() {
|
|
return res.status(200).send(req.params.id + " updated.");
|
|
}).catch(function(error) {
|
|
console.log(error);
|
|
return res.status(500).send(error);
|
|
}).then(function() {
|
|
inProcess.splice(inProcess.indexOf(req.params.id), 1);
|
|
});
|
|
|
|
case "rename":
|
|
return getPhoto(req.params.id).then(function(asset) {
|
|
if (!asset) {
|
|
return res.status(404).send(req.params.id + " not found.");
|
|
}
|
|
|
|
let src = asset.filename;
|
|
|
|
asset.filename = asset.filename.replace(/(\.[^.]*)$/, "-" + asset.hash.substring(0, 8) + "$1");
|
|
return rename(picturesPath + asset.path + src, picturesPath + asset.path + asset.filename).then(function() {
|
|
return rename(picturesPath + asset.path + "thumbs/" + src, picturesPath + asset.path + "thumbs/" + asset.filename);
|
|
}).then(function() {
|
|
return rename(picturesPath + asset.path + "thumbs/scaled/" + src, picturesPath + asset.path + "thumbs/scaled/" + asset.filename);
|
|
}).then(function() {
|
|
return photoDB.sequelize.query("UPDATE photos SET filename=:filename WHERE id=:id", {
|
|
replacements: asset
|
|
}).then(function() {
|
|
return res.status(200).send(asset);
|
|
});
|
|
});
|
|
}).catch(function(error) {
|
|
console.log(error);
|
|
return res.status(500).send(error);
|
|
}).then(function() {
|
|
inProcess.splice(inProcess.indexOf(req.params.id), 1);
|
|
});
|
|
|
|
case "rotate":
|
|
let direction = req.query.direction || "right";
|
|
if (direction == "right") {
|
|
direction = 90;
|
|
} else {
|
|
direction = -90;
|
|
}
|
|
return getPhoto(req.params.id).then(function(asset) {
|
|
if (!asset) {
|
|
return res.status(404).send(req.params.id + " not found.");
|
|
}
|
|
|
|
let original = picturesPath + asset.path + asset.filename,
|
|
target = picturesPath + asset.path + ".tmp." + asset.filename,
|
|
thumb = picturesPath + asset.path + "thumbs/" + asset.filename,
|
|
scaled = picturesPath + asset.path + "thumbs/scaled/" + asset.filename;
|
|
|
|
let tmp = asset.width;
|
|
asset.width = asset.height;
|
|
asset.height = tmp;
|
|
|
|
asset.image = sharp(original);
|
|
return asset.image.rotate(direction).withMetadata().toFile(target).then(function() {
|
|
let stamp = moment(new Date(asset.modified)).format("YYYYMMDDhhmm.ss");
|
|
console.log("Restamping " + target + " to " + stamp);
|
|
/* Re-stamp the file's ctime with the original ctime */
|
|
return execFile("touch", [
|
|
"-t",
|
|
stamp,
|
|
target
|
|
]);
|
|
}).then(function() {
|
|
return asset.image.rotate(direction).resize(256, 256).withMetadata().toFile(thumb);
|
|
}).then(function() {
|
|
return asset.image.resize(Math.min(1024, asset.width)).withMetadata().toFile(scaled);
|
|
}).then(function() {
|
|
return stat(target).then(function(stats) {
|
|
if (!stats) {
|
|
throw "Unable to find original file after attempting to rotate!";
|
|
}
|
|
asset.size = stats.size;
|
|
asset.modified = stats.mtime;
|
|
return unlink(original).then(function() {
|
|
return rename(target, original);
|
|
}).then(function() {
|
|
return photoDB.sequelize.query("UPDATE photos SET " +
|
|
"modified=:modified,width=:width,height=:height,size=:size,updated=CURRENT_TIMESTAMP,scanned=CURRENT_TIMESTAMP " +
|
|
"WHERE id=:id", {
|
|
replacements: asset
|
|
});
|
|
});
|
|
});
|
|
}).then(function() {
|
|
sharp.cache(false);
|
|
sharp.cache(true);
|
|
res.status(200).send(asset);
|
|
|
|
return computeHash(asset.filepath).then(function(hash) {
|
|
asset.hash = hash;
|
|
return asset;
|
|
}).then(function(asset) {
|
|
return photoDB.sequelize.query("SELECT photos.id,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)";
|
|
console.warn("HASH being updated and photoId " + asset.id + " *should* already exist, but it doesn't.");
|
|
} else if (results.length > 1) {
|
|
/* This image is now a duplicate! */
|
|
for (var i = 0; i < results.length; i++) {
|
|
if (results[i].id != asset.id) {
|
|
console.log("Duplicate asset: " +
|
|
"'" + asset.filepath + "' is a copy of " +
|
|
"'" + results[i].path + results[i].filename + "'");
|
|
asset.duplicate = results[i].id;
|
|
break;
|
|
}
|
|
}
|
|
|
|
query = "UPDATE photos SET duplicate=:duplicate WHERE id=:id; " +
|
|
"DELETE FROM photohashes WHERE photoId=:id";
|
|
|
|
console.log("Updating photo " + asset.id + " as duplicate of " + asset.duplicate);
|
|
} else if (results[0].hash != asset.hash) {
|
|
query = "UPDATE photohashes SET hash=:hash WHERE photoId=:id; UPDATE photos SET duplicate=0 WHERE id=:id";
|
|
|
|
console.log("Updating photohash for " + asset.id + " to " + asset.hash + ", and clearing duplicate field.");
|
|
} else {
|
|
console.log("Unexpected!", asset, results[0]);
|
|
return;
|
|
}
|
|
|
|
return photoDB.sequelize.query(query, {
|
|
replacements: asset,
|
|
}).then(function() {
|
|
return asset;
|
|
});
|
|
});
|
|
});
|
|
});
|
|
}).catch(function(error) {
|
|
console.log(error);
|
|
return res.status(500).send(error);
|
|
}).then(function() {
|
|
inProcess.splice(inProcess.indexOf(req.params.id), 1);
|
|
});
|
|
}
|
|
|
|
return res.status(400).send("Invalid request");
|
|
});
|
|
|
|
const getPhoto = function(id) {
|
|
return photoDB.sequelize.query("SELECT " +
|
|
"photos.*,albums.path AS path,photohashes.hash,modified,(albums.path || photos.filename) AS filepath FROM photos " +
|
|
"LEFT JOIN albums ON albums.id=photos.albumId " +
|
|
"LEFT JOIN photohashes ON photohashes.photoId=photos.id " +
|
|
"WHERE photos.id=:id", {
|
|
replacements: {
|
|
id: id
|
|
},
|
|
type: photoDB.Sequelize.QueryTypes.SELECT,
|
|
raw: true
|
|
}).then(function(photos) {
|
|
if (photos.length == 0) {
|
|
return null;
|
|
}
|
|
|
|
if (!photos[0].duplicate) {
|
|
return photos[0];
|
|
}
|
|
|
|
return photoDB.sequelize.query("SELECT hash FROM photohashes WHERE photoId=:duplicate", {
|
|
replacements: photos[0],
|
|
type: photoDB.Sequelize.QueryTypes.SELECT,
|
|
raw: true
|
|
}).then(function(results) {
|
|
if (results.length != 0) {
|
|
photos[0].hash = results[0].hash;
|
|
}
|
|
return photos[0];
|
|
});
|
|
});
|
|
}
|
|
|
|
const tasks = [];
|
|
const Task = function() {
|
|
return new Promise(function(resolve, reject) {
|
|
crypto.randomBytes(16, function(error, buffer) {
|
|
if (error) {
|
|
return reject(error);
|
|
}
|
|
|
|
let now = Date.now(), task;
|
|
|
|
task = {
|
|
started: now,
|
|
lastUpdate: now,
|
|
eta: now,
|
|
processed: 0,
|
|
remaining: 0,
|
|
token: buffer.toString('hex'),
|
|
};
|
|
|
|
tasks.push(task);
|
|
return resolve(task);
|
|
});
|
|
});
|
|
};
|
|
|
|
const removeTask = function(task) {
|
|
let i = tasks.indexOf(task);
|
|
if (i != -1) {
|
|
tasks.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
router.get("/status/:token", function(req, res) {
|
|
if (!req.params.token) {
|
|
return res.status(400).send("Usage /status/:token");
|
|
}
|
|
|
|
for (var i = 0; i < tasks.length; i++) {
|
|
if (tasks[i].token == req.params.token) {
|
|
return res.status(200).send(tasks[i]);
|
|
}
|
|
}
|
|
|
|
return res.status(404).send("Task " + req.params.token + " not found.");
|
|
});
|
|
|
|
/**
|
|
* 1. Look if there are duplicates.
|
|
* 2. Update all other duplicates to be duplicates of the first image that remains
|
|
* 3. If no duplicates, DELETE the entry from photohashes
|
|
* 4. If there are duplicates, update the HASH entry to point to the first image that remains
|
|
* 5. Delete the entry from photos
|
|
* 6. Delete the scaled, thumb, and original from disk
|
|
*/
|
|
const deletePhoto = function(photo) {
|
|
return photoDB.sequelize.transaction(function(transaction) {
|
|
return photoDB.sequelize.query("SELECT id FROM photos WHERE duplicate=:id", {
|
|
replacements: photo,
|
|
type: photoDB.Sequelize.QueryTypes.SELECT,
|
|
raw: true
|
|
}).then(function(duplicates) {
|
|
if (!duplicates.length) {
|
|
return null;
|
|
}
|
|
|
|
let first = duplicates.shift();
|
|
let needsUpdate = [];
|
|
duplicates.forEach(function(duplicate) {
|
|
needsUpdate.push(duplicate.id);
|
|
});
|
|
|
|
if (!needsUpdate.length) {
|
|
return first;
|
|
}
|
|
|
|
// 2. Update all other duplicates to be duplicates of the first image that remains
|
|
console.log("Updating " + needsUpdate + " to point to " + first.id);
|
|
return photoDB.sequelize.query(
|
|
"UPDATE photos SET duplicate=:first WHERE id IN (:needsUpdate)", {
|
|
replacements: {
|
|
first: first.id,
|
|
needsUpdate: needsUpdate
|
|
},
|
|
transaction: transaction
|
|
}).then(function() {
|
|
return first;
|
|
});
|
|
}).then(function(first) {
|
|
if (!first) {
|
|
console.log("Deleting "+ photo.id + " from photohash.");
|
|
// 3. If no duplicates, DELETE the entry from photohashes
|
|
return photoDB.sequelize.query(
|
|
"DELETE FROM photohashes WHERE photoId=:id", {
|
|
replacements: photo,
|
|
transaction: transaction
|
|
});
|
|
}
|
|
console.log("Updating photohash for " + photo.id + " to point to " + first.id);
|
|
// 4. If there are duplicates, update the HASH entry to point to the first image that remains
|
|
return photoDB.sequelize.query(
|
|
"UPDATE photohashes SET photoId=:first WHERE photoId=:photo", {
|
|
replacements: {
|
|
first: first.id,
|
|
photo: photo.id
|
|
},
|
|
transaction: transaction
|
|
});
|
|
}).then(function() {
|
|
console.log("Deleting " + photo.path + photo.filename + " from DB.");
|
|
// 5. Delete the entry from photos
|
|
return photoDB.sequelize.query("DELETE FROM photos WHERE id=:id", {
|
|
replacements: photo,
|
|
transaction: transaction
|
|
});
|
|
}).then(function() {
|
|
// 6. Delete the scaled, thumb, and original from disk
|
|
console.log("Deleting " + photo.path + photo.filename + " from disk.");
|
|
return unlink(photo.path + "thumbs/scaled/" + photo.filename).catch(function() {}).then(function() {
|
|
return unlink(photo.path + "thumbs/" + photo.filename).catch(function() {}).then(function() {
|
|
return unlink(photo.path + photo.filename).catch(function(error) {
|
|
console.log("Error removing file: " + error);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
router.delete("/:id?", function(req, res/*, next*/) {
|
|
if (!req.user.maintainer) {
|
|
return res.status(401).send("Unauthorized to delete photos.");
|
|
}
|
|
|
|
const replacements = {
|
|
id: req.params.id || "*"
|
|
};
|
|
|
|
if (!req.params.id && !req.query.permanent) {
|
|
return res.status(400).send("Trash can only be emptied if permanent.");
|
|
}
|
|
|
|
let where = "";
|
|
if (req.params.id) {
|
|
where = "photos.id=:id";
|
|
if (req.query.permanent) {
|
|
where += " AND photos.deleted=1";
|
|
}
|
|
} else {
|
|
where = "photos.deleted=1";
|
|
}
|
|
|
|
console.log("DELETE /" + replacements.id, req.query);
|
|
|
|
let sent = false;
|
|
|
|
return photoDB.sequelize.query("SELECT " +
|
|
"photos.*,albums.path AS path,photohashes.hash,(albums.path || photos.filename) AS filepath FROM photos " +
|
|
"LEFT JOIN albums ON albums.id=photos.albumId " +
|
|
"LEFT JOIN photohashes ON photohashes.photoId=photos.id " +
|
|
"WHERE " + where, {
|
|
replacements: replacements,
|
|
type: photoDB.Sequelize.QueryTypes.SELECT,
|
|
raw: true
|
|
}).then(function(photos) {
|
|
if (photos.length == 0) {
|
|
sent = true;
|
|
return res.status(404).send("Unable to find photo " + replacements.id);
|
|
}
|
|
|
|
return photoDB.sequelize.query(
|
|
"SELECT id FROM faces WHERE photoId IN (:photos)", {
|
|
replacements: photos.map(photo => photo.id),
|
|
type: photoDB.Sequelize.QueryTypes.SELECT,
|
|
raw: true
|
|
}).then((faces) => {
|
|
const faceIds = faces.map(face => face.id);
|
|
return photoDB.sequelize.query(
|
|
"DELETE FROM facedistances WHERE face1Id IN (:ids) OR face2Id IN (:ids)", {
|
|
replacements: {
|
|
ids: faceIds
|
|
}
|
|
}).then(() => {
|
|
return photoDB.sequelize.query(
|
|
"DELETE FROM faces WHERE id IN (:ids)", {
|
|
replacements: {
|
|
ids: faceIds
|
|
}
|
|
});
|
|
});
|
|
}).then(() => {
|
|
if (!req.query.permanent) {
|
|
return photoDB.sequelize.query("UPDATE photos SET deleted=1,updated=CURRENT_TIMESTAMP WHERE id=:id", {
|
|
replacements: replacements
|
|
}).then(function() {
|
|
sent = true;
|
|
return res.status(200).send({
|
|
id: req.params.id,
|
|
deleted: true,
|
|
permanent: (req.query && req.query.permanent) || false
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Delete the asset from disk and the DB
|
|
*/
|
|
return Task().then(function(task) {
|
|
task.remaining = photos.length;
|
|
task.eta = -1;
|
|
|
|
sent = true;
|
|
res.status(200).send({
|
|
id: req.params.id,
|
|
task: task,
|
|
permanent: (req.query && req.query.permanent) || false
|
|
});
|
|
|
|
return Promise.mapSeries(photos, function(photo) {
|
|
let lastStamp = Date.now();
|
|
return deletePhoto(photo).then(function() {
|
|
let now = Date.now(), elapsed = now - lastStamp;
|
|
lastStamp= now;
|
|
task.processed++;
|
|
task.remaining--;
|
|
task.lastUpdate = Date.now();
|
|
task.eta = elapsed * task.remaining;
|
|
})
|
|
}).then(function() {
|
|
console.log("Processing task " + task.token + " finished: " + (task.lastUpdate - task.started));
|
|
removeTask(task);
|
|
}).catch(function(error) {
|
|
console.log("Processing task " + task.token + " failed: " + error);
|
|
removeTask(task);
|
|
});
|
|
});
|
|
});
|
|
}).catch(function(error) {
|
|
console.log(error);
|
|
if (!sent) {
|
|
sent = true;
|
|
res.status(500).send("Unable to delete photo " + req.params.id + ": " + error);
|
|
}
|
|
});
|
|
});
|
|
|
|
router.get("/holiday/:holiday", function(req, res/*, next*/) {
|
|
let startYear = 1990,
|
|
endYear = moment().year(),
|
|
dayIsHoliday = "",
|
|
holidayName,
|
|
date = undefined;
|
|
|
|
/* Find the holiday in the list of holidays */
|
|
let lookup = moment().holidays([req.params.holiday]);
|
|
if (!lookup) {
|
|
date = req.params.holiday.match(/^((\d{4})-)?(\d{2})-(\d{2})$/);
|
|
if (!date) {
|
|
return res.status(404).send(req.params.holiday + " holiday not found.");
|
|
}
|
|
date = {
|
|
year: date[1],
|
|
month: date[3],
|
|
day: date[4]
|
|
};
|
|
if (date.year) {
|
|
startYear = date.year;
|
|
endYear = date.year;
|
|
}
|
|
}
|
|
|
|
holidayName = date ? req.params.holiday : Object.getOwnPropertyNames(lookup)[0];
|
|
console.log("Searching for holiday: " + holidayName);
|
|
/* Lookup the date for the holiday on every year from 'startYear' (1990) to today */
|
|
for (let year = startYear; year <= endYear; year++) {
|
|
console.log("Getting year: " + year);
|
|
let holiday;
|
|
|
|
if (!date) {
|
|
holiday = moment(year + "-01-01", "YYYY-MM-DD").holiday(req.params.holiday);
|
|
if (!holiday) {
|
|
/* 'Leap Year' doesn't exist every year... */
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
* NOTE: Memorial Day and Labor Day are two special cases -- the holiday is a Monday,
|
|
* however the entire weekend typically is holidy-esque. Account for that below.
|
|
*
|
|
* For those that have 'eve' celebrations, include those too.
|
|
*
|
|
* We (should) could expand this to account for Fri or Mon on the 4th of July or the
|
|
* entire weekend if it occurs on a Thu or Tues */
|
|
|
|
let extraDays = 0;
|
|
switch (req.params.holiday.toLowerCase()) {
|
|
case 'labor day':
|
|
case 'memorial day':
|
|
extraDays = -2; /* Include two days prior */
|
|
break;
|
|
case 'christmas day':
|
|
case 'new year\'s day':
|
|
extraDays = -1; /* Include 'Eve' */
|
|
break;
|
|
}
|
|
let direction = extraDays < 0 ? -1 : 1;
|
|
|
|
for (let i = 0; i <= Math.abs(extraDays); i++) {
|
|
let comparison = "strftime('%Y-%m-%d',taken)='" + holiday.format("YYYY-MM-DD") + "'";
|
|
/* If no holiday has been set yet, start the comparison function passed to WHERE
|
|
* otherwise append it with OR. */
|
|
if (!dayIsHoliday) {
|
|
dayIsHoliday = comparison;
|
|
} else {
|
|
dayIsHoliday += " OR " + comparison;
|
|
}
|
|
|
|
holiday.date(holiday.date() + direction);
|
|
}
|
|
} else {
|
|
let comparison = "strftime('%Y-%m-%d',taken)='"
|
|
+ moment(year + "-" + date.month + "-" + date.day, "YYYY-MM-DD").format("YYYY-MM-DD")
|
|
+ "'";
|
|
if (!dayIsHoliday) {
|
|
dayIsHoliday = comparison;
|
|
} else {
|
|
dayIsHoliday += " OR " + comparison;
|
|
}
|
|
}
|
|
}
|
|
|
|
let query = "SELECT photos.*,albums.path AS path FROM photos " +
|
|
"INNER JOIN albums ON (albums.id=photos.albumId) " +
|
|
"WHERE (photos.duplicate=0 AND photos.deleted=0 AND photos.scanned NOT NULL AND (" + dayIsHoliday + ")) " +
|
|
"ORDER BY strftime('%Y-%m-%d', taken) DESC,id DESC";
|
|
|
|
return photoDB.sequelize.query(query, {
|
|
type: photoDB.Sequelize.QueryTypes.SELECT
|
|
}).then(function(photos) {
|
|
photos.forEach(function(photo) {
|
|
for (var key in photo) {
|
|
if (photo[key] instanceof Date) {
|
|
photo[key] = moment(photo[key]);
|
|
}
|
|
}
|
|
});
|
|
|
|
return res.status(200).json({
|
|
holiday: holidayName,
|
|
items: photos
|
|
});
|
|
|
|
}).catch(function(error) {
|
|
console.error("Query failed: " + query);
|
|
return Promise.reject(error);
|
|
});
|
|
});
|
|
|
|
|
|
router.get("/folder/:folder", function(req, res/*, next*/) {
|
|
|
|
const folder = req.params.folder;
|
|
|
|
console.log("Searching for photos under folder: " + folder);
|
|
|
|
let query = "SELECT photos.*,albums.path AS path FROM photos " +
|
|
"INNER JOIN albums ON (photos.albumId=albums.id AND albums.path LIKE :folder) " +
|
|
"WHERE (photos.duplicate=0 AND photos.deleted=0 AND photos.scanned NOT NULL) " +
|
|
"ORDER BY strftime('%Y-%m-%d', taken) DESC,id DESC";
|
|
|
|
return photoDB.sequelize.query(query, {
|
|
replacements: {
|
|
folder: folder + '%'
|
|
},
|
|
type: photoDB.Sequelize.QueryTypes.SELECT
|
|
}).then(function(photos) {
|
|
console.log("Found " + photos.length + " photos.");
|
|
photos.forEach(function(photo) {
|
|
for (var key in photo) {
|
|
if (photo[key] instanceof Date) {
|
|
photo[key] = moment(photo[key]);
|
|
}
|
|
}
|
|
});
|
|
|
|
return res.status(200).json({
|
|
folder: folder,
|
|
items: photos
|
|
});
|
|
|
|
}).catch(function(error) {
|
|
console.error("Query failed: " + query);
|
|
return Promise.reject(error);
|
|
});
|
|
});
|
|
|
|
/* Each photos has:
|
|
|
|
* locations
|
|
* people
|
|
* date
|
|
* tags
|
|
* photo info
|
|
|
|
*/
|
|
router.get("/memories/:date?", function(req, res/*, next*/) {
|
|
let limit = parseInt(req.query.limit) || 50,
|
|
id, cursor, index;
|
|
|
|
if (req.query.next) {
|
|
let parts = req.query.next.split("_");
|
|
cursor = new Date(parts[0]);
|
|
id = parseInt(parts[1]);
|
|
} else {
|
|
cursor = "";
|
|
id = -1;
|
|
}
|
|
|
|
if (id == -1) {
|
|
index = "strftime('%m-%d',taken)=strftime('%m-%d',:date)";
|
|
} else {
|
|
index = "((strftime('%Y-%m-%d',taken)=strftime('%Y-%m-%d',:cursor) AND photos.id<"+id+ ") OR " +
|
|
"(strftime('%m-%d',taken)=strftime('%m-%d',:cursor) AND strftime('%Y',taken)<strftime('%Y',:cursor)))";
|
|
}
|
|
|
|
let date = new Date("2016-" + req.params.date);
|
|
let query = "SELECT photos.*,albums.path AS path FROM photos " +
|
|
"INNER JOIN albums ON (albums.id=photos.albumId) " +
|
|
"WHERE (photos.duplicate=0 AND photos.deleted=0 AND photos.scanned NOT NULL AND " + index + ") " +
|
|
"ORDER BY strftime('%Y-%m-%d',taken) DESC,id DESC LIMIT " + (limit + 1);
|
|
|
|
// console.log("Memories for " + date.toISOString().replace(/^2016-(.*)T.*$/, "$1"));
|
|
// if (cursor) {
|
|
// console.log("Cursor" + cursor.toISOString().replace(/T.*/, ""));
|
|
// }
|
|
|
|
return photoDB.sequelize.query(query, {
|
|
replacements: {
|
|
cursor: cursor,
|
|
date: date
|
|
},
|
|
type: photoDB.Sequelize.QueryTypes.SELECT
|
|
}).then(function(photos) {
|
|
photos.forEach(function(photo) {
|
|
for (var key in photo) {
|
|
if (photo[key] instanceof Date) {
|
|
photo[key] = moment(photo[key]);
|
|
}
|
|
}
|
|
});
|
|
|
|
let more = photos.length > limit; /* We queried one extra item to see if there are more than LIMIT available */
|
|
|
|
let last;
|
|
if (more) {
|
|
photos.splice(limit);
|
|
last = photos[photos.length - 1];
|
|
}
|
|
|
|
let results = {
|
|
items: photos.sort(function(a, b) {
|
|
return new Date(b.taken) - new Date(a.taken);
|
|
})
|
|
};
|
|
|
|
if (more) {
|
|
results.cursor = new Date(last.taken).toISOString().replace(/T.*/, "") + "_" + last.id;
|
|
results.more = true;
|
|
}
|
|
return res.status(200).json(results);
|
|
}).catch(function(error) {
|
|
console.error("Query failed: " + query);
|
|
return Promise.reject(error);
|
|
});
|
|
});
|
|
|
|
router.get("/duplicates", function(req, res/*, next*/) {
|
|
let replacements = {};
|
|
|
|
return photoDB.sequelize.query(
|
|
"SELECT filename,COUNT(*) AS count FROM photos WHERE photos.duplicate=0 AND photos.deleted!=1 " +
|
|
"GROUP BY filename HAVING count > 1", {
|
|
replacements: replacements,
|
|
type: photoDB.Sequelize.QueryTypes.SELECT,
|
|
raw: true
|
|
}).then(function(duplicates) {
|
|
let filenames = [];
|
|
duplicates.forEach(function(duplicate) {
|
|
filenames.push(duplicate.filename);
|
|
});
|
|
replacements.filenames = filenames;
|
|
|
|
return photoDB.sequelize.query(
|
|
"SELECT photos.*,albums.path AS path,photohashes.hash,(albums.path || photos.filename) AS filepath FROM photos " +
|
|
"LEFT JOIN albums ON albums.id=photos.albumId " +
|
|
"LEFT JOIN photohashes ON photohashes.photoId=photos.id " +
|
|
"WHERE filename IN (:filenames) ORDER BY photos.filename,photos.width,photos.height,photos.size DESC", {
|
|
replacements: replacements,
|
|
type: photoDB.Sequelize.QueryTypes.SELECT,
|
|
raw: true
|
|
}).then(function(photos) {
|
|
return res.status(200).json({
|
|
items: photos
|
|
});
|
|
});
|
|
}).catch(function(error) {
|
|
return Promise.reject(error);
|
|
});
|
|
});
|
|
|
|
router.get("/trash", function(req, res/*, next*/) {
|
|
return photoDB.sequelize.query(
|
|
"SELECT photos.*,albums.path AS path,(albums.path || photos.filename) AS filepath FROM photos " +
|
|
"LEFT JOIN albums ON albums.id=photos.albumId " +
|
|
"WHERE deleted=1 ORDER BY photos.updated DESC", {
|
|
type: photoDB.Sequelize.QueryTypes.SELECT,
|
|
raw: true
|
|
}).then(function(photos) {
|
|
return res.status(200).json({
|
|
items: photos
|
|
});
|
|
});
|
|
});
|
|
|
|
function getFacesForPhoto(id) {
|
|
/* Get the set of faces in this photo */
|
|
return photoDB.sequelize.query(
|
|
"SELECT * FROM faces WHERE photoId=:id AND faceConfidence>0.9", {
|
|
replacements: {
|
|
id: id,
|
|
},
|
|
type: photoDB.Sequelize.QueryTypes.SELECT,
|
|
raw: true
|
|
}).then((faces) => {
|
|
/* For each face in the photo, get the related faces */
|
|
return photoDB.sequelize.query(
|
|
"SELECT relatedFaces.photoId AS photoId,fd.face1Id,fd.face2Id,fd.distance,relatedFaces.faceConfidence " +
|
|
"FROM (SELECT id,photoId,faceConfidence FROM faces WHERE faces.faceConfidence>=0.9 AND faces.id IN (:ids)) AS faces " +
|
|
"INNER JOIN faces AS relatedFaces ON relatedFaces.faceConfidence>=0.9 AND relatedFaces.id IN (fd.face1Id,fd.face2Id) " +
|
|
"INNER JOIN facedistances AS fd ON fd.distance<=0.5 " +
|
|
" AND (fd.face1Id=faces.id OR fd.face2Id=faces.id) " +
|
|
"WHERE (faces.id=fd.face1Id OR faces.id=fd.face2Id) " +
|
|
"ORDER BY fd.distance ASC", {
|
|
replacements: {
|
|
ids: faces.map(face => face.id),
|
|
},
|
|
type: photoDB.Sequelize.QueryTypes.SELECT,
|
|
raw: true
|
|
}).then((relatedFaces) => {
|
|
faces.forEach((face) => {
|
|
face.relatedFaces = relatedFaces.filter((related) => {
|
|
return (related.photoId != id && (related.face1Id == face.id || related.face2Id == face.id));
|
|
}).map((related) => {
|
|
return {
|
|
distance: related.distance,
|
|
faceConfidence: related.faceConfidence,
|
|
photoId: related.photoId,
|
|
faceId: related.face1Id != face.id ? related.face1Id : related.face2Id
|
|
}
|
|
});
|
|
});
|
|
return faces;
|
|
});
|
|
});
|
|
}
|
|
|
|
router.get("/faces/:id", (req, res) => {
|
|
const id = parseInt(req.params.id);
|
|
if (id != req.params.id) {
|
|
return res.status(400).send({ message: "Usage faces/:id"});
|
|
}
|
|
return getFacesForPhoto(id).then((faces) => {
|
|
return res.status(200).json(faces);
|
|
});
|
|
});
|
|
|
|
router.get("/random/:id?", (req, res) => {
|
|
let id = parseInt(req.params.id),
|
|
filter = "";
|
|
|
|
if (id == req.params.id) {
|
|
console.log("GET /random/" + id);
|
|
filter = "AND id=:id";
|
|
} else {
|
|
filter = "AND faces>0";
|
|
id = undefined;
|
|
}
|
|
|
|
return photoDB.sequelize.query("SELECT id,duplicate FROM photos WHERE deleted=0 " + filter, {
|
|
replacements: {
|
|
id: id
|
|
},
|
|
type: photoDB.Sequelize.QueryTypes.SELECT,
|
|
raw: true
|
|
}).then((results) => {
|
|
if (!results.length) {
|
|
return [];
|
|
}
|
|
if (id) {
|
|
if (results[0].duplicate) {
|
|
id = results[0].duplicate;
|
|
}
|
|
} else {
|
|
id = results[Math.floor(Math.random() * results.length)].id;
|
|
}
|
|
|
|
return photoDB.sequelize.query(
|
|
"SELECT photos.*,albums.path AS path FROM photos " +
|
|
"INNER JOIN albums ON albums.id=photos.albumId " +
|
|
"WHERE photos.duplicate=0 AND photos.deleted=0 AND photos.scanned NOT NULL AND photos.id=:id", {
|
|
replacements: {
|
|
id: id,
|
|
},
|
|
type: photoDB.Sequelize.QueryTypes.SELECT,
|
|
raw: true
|
|
});
|
|
}).then(function(photos) {
|
|
if (!photos.length) {
|
|
return res.status(404).send({ message: id + " not found." });
|
|
}
|
|
const photo = photos[0];
|
|
for (var key in photo) {
|
|
if (photo[key] instanceof Date) {
|
|
photo[key] = moment(photo[key]);
|
|
}
|
|
}
|
|
return getFacesForPhoto(photo.id).then((faces) => {
|
|
photo.faces = faces;
|
|
return res.status(200).json(photo);
|
|
})
|
|
});
|
|
})
|
|
|
|
router.get("/mvimg/*", function(req, res/*, next*/) {
|
|
let limit = parseInt(req.query.limit) || 50,
|
|
id, cursor, index;
|
|
|
|
if (req.query.next) {
|
|
let parts = req.query.next.split("_");
|
|
cursor = parts[0];
|
|
id = parseInt(parts[1]);
|
|
} else {
|
|
cursor = "";
|
|
id = -1;
|
|
}
|
|
|
|
if (id == -1) {
|
|
index = "";
|
|
} else {
|
|
index = "AND ((strftime('%Y-%m-%d',taken)=strftime('%Y-%m-%d',:cursor) AND photos.id<:id) OR " +
|
|
"strftime('%Y-%m-%d',taken)<strftime('%Y-%m-%d',:cursor))";
|
|
}
|
|
|
|
console.log(req.url);
|
|
let path = decodeURI(req.url).replace(/\?.*$/, "").replace(/^\//, "").replace(/^mvimg\//, ""),
|
|
query = "SELECT photos.*,albums.path AS path FROM photos " +
|
|
"INNER JOIN albums ON (albums.id=photos.albumId AND albums.path LIKE :path) " +
|
|
"WHERE (photos.name LIKE 'MVIMG%' AND photos.duplicate=0 AND photos.deleted=0 AND photos.scanned NOT NULL) " + index + " " +
|
|
"ORDER BY strftime('%Y-%m-%d',taken) DESC,id DESC LIMIT " + (limit + 1),
|
|
replacements = {
|
|
id: id,
|
|
cursor: cursor,
|
|
path: path + "%"
|
|
};
|
|
console.log("Trying path as: " + path);
|
|
// console.log("Fetching from: " + JSON.stringify(replacements, null, 2) + "\nwith:\n" + query);
|
|
|
|
return photoDB.sequelize.query(query, {
|
|
replacements: replacements,
|
|
type: photoDB.Sequelize.QueryTypes.SELECT
|
|
}).then(function(photos) {
|
|
photos.forEach(function(photo) {
|
|
for (var key in photo) {
|
|
if (photo[key] instanceof Date) {
|
|
photo[key] = moment(photo[key]);
|
|
}
|
|
}
|
|
});
|
|
|
|
let more = photos.length > limit; /* We queried one extra item to see if there are more than LIMIT available */
|
|
|
|
// console.log("Requested " + limit + " and matched " + photos.length);
|
|
|
|
let last;
|
|
if (more) {
|
|
photos.splice(limit);
|
|
last = photos[photos.length - 1];
|
|
}
|
|
|
|
let results = {
|
|
items: photos.sort(function(a, b) {
|
|
return new Date(b.taken) - new Date(a.taken);
|
|
})
|
|
};
|
|
|
|
if (more) {
|
|
results.cursor = new Date(last.taken).toISOString().replace(/T.*/, "") + "_" + last.id;
|
|
results.more = true;
|
|
}
|
|
return res.status(200).json(results);
|
|
}).catch(function(error) {
|
|
console.error("Query failed: " + query);
|
|
return Promise.reject(error);
|
|
});
|
|
});
|
|
|
|
router.get("/*", function(req, res/*, next*/) {
|
|
console.log("Generic loader");
|
|
|
|
let limit = parseInt(req.query.limit) || 50,
|
|
id, cursor, index;
|
|
|
|
if (req.query.next) {
|
|
let parts = req.query.next.split("_");
|
|
cursor = parts[0];
|
|
id = parseInt(parts[1]);
|
|
} else {
|
|
cursor = "";
|
|
id = -1;
|
|
}
|
|
|
|
if (id == -1) {
|
|
index = "";
|
|
} else {
|
|
index = "AND ((strftime('%Y-%m-%d',taken)=strftime('%Y-%m-%d',:cursor) AND photos.id<:id) OR " +
|
|
"strftime('%Y-%m-%d',taken)<strftime('%Y-%m-%d',:cursor))";
|
|
}
|
|
|
|
let path = decodeURI(req.url).replace(/\?.*$/, "").replace(/^\//, ""),
|
|
query = "SELECT photos.*,albums.path AS path FROM photos " +
|
|
"INNER JOIN albums ON (albums.id=photos.albumId AND albums.path LIKE :path) " +
|
|
"WHERE (photos.duplicate=0 AND photos.deleted=0 AND photos.scanned NOT NULL) " + index + " " +
|
|
"ORDER BY strftime('%Y-%m-%d',taken) DESC,id DESC LIMIT " + (limit + 1),
|
|
replacements = {
|
|
id: id,
|
|
cursor: cursor,
|
|
path: path + "%"
|
|
};
|
|
|
|
// console.log("Fetching from: " + JSON.stringify(replacements, null, 2) + "\nwith:\n" + query);
|
|
|
|
return photoDB.sequelize.query(query, {
|
|
replacements: replacements,
|
|
type: photoDB.Sequelize.QueryTypes.SELECT
|
|
}).then(function(photos) {
|
|
photos.forEach(function(photo) {
|
|
for (var key in photo) {
|
|
if (photo[key] instanceof Date) {
|
|
photo[key] = moment(photo[key]);
|
|
}
|
|
}
|
|
});
|
|
|
|
let more = photos.length > limit; /* We queried one extra item to see if there are more than LIMIT available */
|
|
|
|
// console.log("Requested " + limit + " and matched " + photos.length);
|
|
|
|
let last;
|
|
if (more) {
|
|
photos.splice(limit);
|
|
last = photos[photos.length - 1];
|
|
}
|
|
|
|
let results = {
|
|
items: photos.sort(function(a, b) {
|
|
return new Date(b.taken) - new Date(a.taken);
|
|
})
|
|
};
|
|
|
|
if (more) {
|
|
results.cursor = new Date(last.taken).toISOString().replace(/T.*/, "") + "_" + last.id;
|
|
results.more = true;
|
|
}
|
|
return res.status(200).json(results);
|
|
}).catch(function(error) {
|
|
console.error("Query failed: " + query);
|
|
return Promise.reject(error);
|
|
});
|
|
});
|
|
|
|
module.exports = router;
|