"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 referencing tables: * photohashes, faces * 4. If there are duplicates, update the HASH (and any FACE) 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 and faces return photoDB.sequelize.query( "DELETE FROM photohashes WHERE photoId=:id", { replacements: photo, transaction: transaction }).then(() => { console.log("Deleting "+ photo.id + " from faces."); // 3. If no duplicates, DELETE the entry from photohashes return photoDB.sequelize.query( "DELETE FROM faces 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(() => { console.log("Updating faces for " + photo.id + " to point to " + first.id); return photoDB.sequelize.query( "UPDATE faces 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) 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) 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) 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;