Updated to working
Signed-off-by: James Ketrenos <james_git@ketrenos.com>
This commit is contained in:
parent
2c427707fa
commit
6d234bdbc4
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,4 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
elements
|
./elements
|
||||||
frontend/bower_components
|
frontend/bower_components
|
||||||
pictures
|
pictures
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
"db": {
|
"db": {
|
||||||
"host": "mysql://photos:p4$$w0rd@localhost:3306/photos",
|
"host": "mysql://photos:p4$$w0rd@localhost:3306/photos",
|
||||||
"options": {
|
"options": {
|
||||||
"logging" : false
|
"logging" : false,
|
||||||
|
"timezone": "+00:00"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
"app-layout": "PolymerElements/app-layout#^0.9.1",
|
"app-layout": "PolymerElements/app-layout#^0.9.1",
|
||||||
"paper-checkbox": "PolymerElements/paper-checkbox#^1.2.0",
|
"paper-checkbox": "PolymerElements/paper-checkbox#^1.2.0",
|
||||||
"iron-form": "PolymerElements/iron-form#^1.0.16",
|
"iron-form": "PolymerElements/iron-form#^1.0.16",
|
||||||
"iron-resizable-behavior": "PolymerElements/iron-resizable-behavior#^1.0.4",
|
"iron-resizable-behavior": "PolymerElements/iron-resizable-behavior#^2.1.1",
|
||||||
"paper-dialog": "PolymerElements/paper-dialog#^1.1.0",
|
"paper-dialog": "PolymerElements/paper-dialog#^1.1.0",
|
||||||
"paper-dialog-scrollable": "PolymerElements/paper-dialog-scrollable#^1.1.5",
|
"paper-dialog-scrollable": "PolymerElements/paper-dialog-scrollable#^1.1.5",
|
||||||
"iron-collapse": "PolymerElements/iron-collapse#^1.2.1",
|
"iron-collapse": "PolymerElements/iron-collapse#^1.2.1",
|
||||||
@ -53,6 +53,7 @@
|
|||||||
"polymer": "^1.4.0",
|
"polymer": "^1.4.0",
|
||||||
"iron-location": "^1.0.0",
|
"iron-location": "^1.0.0",
|
||||||
"iron-collapse": "^1.2.1",
|
"iron-collapse": "^1.2.1",
|
||||||
"paper-spinner": "^1.0.0"
|
"paper-spinner": "^1.0.0",
|
||||||
|
"iron-resizable-behavior": "^1.0.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
101
frontend/elements/photo-thumbnail.html
Normal file
101
frontend/elements/photo-thumbnail.html
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||||
|
<link rel="import" href="../bower_components/iron-icon/iron-icon.html">
|
||||||
|
<link rel="import" href="../bower_components/iron-icons/iron-icons.html">
|
||||||
|
<link rel="import" href="../bower_components/iron-pages/iron-pages.html">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<dom-module id="photo-thumbnail">
|
||||||
|
<template>
|
||||||
|
<style is="custom-style" include="iron-flex iron-flex-alignment iron-positioning">
|
||||||
|
:host {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: 50% 50%;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: white;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host > div {
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="layout vertical">
|
||||||
|
<div>[[item.id]]</div>
|
||||||
|
<div>[[date(item)]]</div>
|
||||||
|
<div>[[item.path]]</div>
|
||||||
|
<div>[[item.filename]]</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
"use strict";
|
||||||
|
Polymer({
|
||||||
|
is: "photo-thumbnail",
|
||||||
|
properties: {
|
||||||
|
"item": {
|
||||||
|
type: Object
|
||||||
|
},
|
||||||
|
"thumbpath": {
|
||||||
|
type: String,
|
||||||
|
computed: "safeItemThumbFilepath(item, base)"
|
||||||
|
},
|
||||||
|
"width": {
|
||||||
|
type: Number
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
observers: [
|
||||||
|
"widthChanged(width)",
|
||||||
|
"thumbChanged(thumbpath)"
|
||||||
|
],
|
||||||
|
|
||||||
|
thumbChanged: function(thumbpath) {
|
||||||
|
this.style.backgroundImage = "url(" + thumbpath + ")";
|
||||||
|
},
|
||||||
|
|
||||||
|
widthChanged: function(width) {
|
||||||
|
this.style.width = width + "px";
|
||||||
|
this.style.height = width + "px";
|
||||||
|
},
|
||||||
|
|
||||||
|
safeItemThumbFilepath: function(item, base) {
|
||||||
|
return "'" + (base + item.path + "/thumbs/" + item.filename).replace(/'/, "\\'") + "'";
|
||||||
|
},
|
||||||
|
|
||||||
|
date: function(item) {
|
||||||
|
var datetime = item.taken || item.modified || item.added;
|
||||||
|
return datetime.replace(/T.*$/, "");
|
||||||
|
},
|
||||||
|
|
||||||
|
_imageTap: function(event) {
|
||||||
|
window.open(this.base + event.model.item.path + "/" + event.model.item.filename, "image");
|
||||||
|
},
|
||||||
|
|
||||||
|
_pathTap: function(event) {
|
||||||
|
window.location.href = event.model.item.filepath;
|
||||||
|
},
|
||||||
|
|
||||||
|
attached: function() {
|
||||||
|
var base = document.querySelector("base");
|
||||||
|
if (base) {
|
||||||
|
this.base = new URL(base.href).pathname.replace(/\/$/, ""); /* Remove trailing slash if there */
|
||||||
|
} else {
|
||||||
|
this.base = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</dom-module>
|
||||||
|
</html>
|
@ -18,9 +18,12 @@
|
|||||||
<link rel="import" href="../../bower_components/paper-input/paper-input.html">
|
<link rel="import" href="../../bower_components/paper-input/paper-input.html">
|
||||||
<link rel="import" href="../../bower_components/paper-spinner/paper-spinner.html">
|
<link rel="import" href="../../bower_components/paper-spinner/paper-spinner.html">
|
||||||
<link rel="import" href="../../bower_components/paper-toast/paper-toast.html">
|
<link rel="import" href="../../bower_components/paper-toast/paper-toast.html">
|
||||||
|
<link rel="import" href="../../bower_components/iron-resizable-behavior/iron-resizable-behavior.html">
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Droid+Sans+Mono" />
|
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Droid+Sans+Mono" />
|
||||||
|
|
||||||
|
<link rel="import" href="../../elements/photo-thumbnail.html">
|
||||||
|
|
||||||
<script src="fetch.js"></script>
|
<script src="fetch.js"></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@ -51,18 +54,6 @@
|
|||||||
--paper-toast-color: white;
|
--paper-toast-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumbnails > div {
|
|
||||||
margin: 0.5em;
|
|
||||||
width: 200px;
|
|
||||||
height: 200px;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: cover;
|
|
||||||
background-position: 50% 50%;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 3px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.folders > div {
|
.folders > div {
|
||||||
margin: 0.5em;
|
margin: 0.5em;
|
||||||
width: 200px;
|
width: 200px;
|
||||||
@ -72,32 +63,45 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumbnails > div:hover,
|
#placeholder {
|
||||||
.folders > div:hover {
|
position: absolute;
|
||||||
box-shadow: 0px 4px 2px -2px #ddd;
|
left: -1000px;
|
||||||
text-decoration: underline;
|
}
|
||||||
|
|
||||||
|
app-header-layout {
|
||||||
|
--layout-fit: {
|
||||||
|
overflow-y: hidden !important;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<app-location route="{{route}}"></app-location>
|
<app-location route="{{route}}"></app-location>
|
||||||
|
|
||||||
|
<photo-thumbnail id="placeholder"></photo-thumbnail>
|
||||||
|
|
||||||
<app-header-layout fullbleed has-scrolling-region>
|
<app-header-layout fullbleed has-scrolling-region>
|
||||||
<app-header slot="header" fixed>
|
<app-header slot="header" fixed>
|
||||||
<paper-spinner active$="[[loading]]" class="thin"></paper-spinner>
|
<paper-spinner active$="[[loading]]" class="thin"></paper-spinner>
|
||||||
<div>[[path]]</div>
|
<div>[[path]]</div>
|
||||||
</app-header>
|
</app-header>
|
||||||
<div class="thumbnails layout horizontal wrap">
|
<div id="content">
|
||||||
<template is="dom-repeat" items="[[photos]]">
|
<div class="thumbnails layout horizontal wrap">
|
||||||
<div style$="background-image:url([[item.path]]/[[item.filename]])" on-tap="_imageTap" info="[[item]]"></div>
|
<template is="dom-repeat" items="[[photos]]">
|
||||||
</template>
|
<photo-thumbnail width="[[calcWidth]]" on-tap="_imageTap" item="[[item]]"></photo-thumbnail>
|
||||||
</div>
|
</template>
|
||||||
<div class="folders layout horizontal wrap">
|
</div>
|
||||||
<template is="dom-repeat" items="[[photos.paths]]">
|
<div id="magic"></div>
|
||||||
<div info="[[item]]" on-tap="_pathTap" >[[item.filepath]]</div>
|
<div class="layout horizontal">
|
||||||
</template>
|
<paper-button disabled$="[[!prev]]" on-tap="loadPrevPhotos">prev</paper-button>
|
||||||
|
<paper-button disabled$="[[!next]]" on-tap="loadNextPhotos">next</paper-button>
|
||||||
|
</div>
|
||||||
|
<div class="folders layout horizontal wrap">
|
||||||
|
<template is="dom-repeat" items="[[photos.paths]]">
|
||||||
|
<div info="[[item]]" on-tap="_pathTap" >[[item.filepath]]</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</app-header-layout>
|
</app-header-layout>
|
||||||
|
|
||||||
<paper-toast id="toast"></paper-toast>
|
<paper-toast id="toast"></paper-toast>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -108,35 +112,88 @@
|
|||||||
is: "ketr-photos",
|
is: "ketr-photos",
|
||||||
properties: {
|
properties: {
|
||||||
"loading": Boolean,
|
"loading": Boolean,
|
||||||
"photos": Array
|
"photos": Array,
|
||||||
|
prev: {
|
||||||
|
type: Boolean,
|
||||||
|
value: false
|
||||||
|
},
|
||||||
|
next: {
|
||||||
|
type: Boolean,
|
||||||
|
value: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
observers: [
|
observers: [
|
||||||
],
|
],
|
||||||
|
|
||||||
|
behaviors: [
|
||||||
|
/* @polymerBehavior Polymer.IronResizableBehavior */
|
||||||
|
Polymer.IronResizableBehavior
|
||||||
|
],
|
||||||
|
|
||||||
|
listeners: {
|
||||||
|
"iron-resize" : "onResize"
|
||||||
|
},
|
||||||
|
|
||||||
|
date: function(item) {
|
||||||
|
var datetime = item.taken || item.modified || item.added;
|
||||||
|
return datetime.replace(/T.*$/, "");
|
||||||
|
},
|
||||||
|
|
||||||
_imageTap: function(event) {
|
_imageTap: function(event) {
|
||||||
window.open(event.model.item.filepath, "image");
|
window.open(this.base + event.model.item.path + "/" + event.model.item.filename, "image");
|
||||||
},
|
},
|
||||||
|
|
||||||
_pathTap: function(event) {
|
_pathTap: function(event) {
|
||||||
window.location.href = event.model.item.filepath;
|
window.location.href = event.model.item.filepath;
|
||||||
},
|
},
|
||||||
|
|
||||||
_loadPhotos: function() {
|
loadNextPhotos: function() {
|
||||||
|
var cursor = this.photos[this.photos.length - 1];
|
||||||
|
this._loadPhotos(cursor.id + "_" + cursor.taken.toString().replace(/T.*/, ""), +1, true);
|
||||||
|
},
|
||||||
|
|
||||||
|
loadPrevPhotos: function() {
|
||||||
|
var cursor = this.photos[0];
|
||||||
|
this._loadPhotos(cursor.id + "_" + cursor.taken.toString().replace(/T.*/, ""), -1);
|
||||||
|
},
|
||||||
|
|
||||||
|
_loadPhotos: function(start, dir, append) {
|
||||||
if (this.loading == true) {
|
if (this.loading == true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
window.fetch("api/v1/photos", function(error, xhr) {
|
|
||||||
|
dir = dir || +1;
|
||||||
|
var params = {
|
||||||
|
limit: Math.ceil(this.clientWidth / 200) * Math.ceil(this.clientHeight / 200),
|
||||||
|
dir: dir
|
||||||
|
}, url = "";
|
||||||
|
if (start) {
|
||||||
|
params.next = start;
|
||||||
|
}
|
||||||
|
if (this.sortOrder) {
|
||||||
|
params.sort = this.sortOrder;
|
||||||
|
}
|
||||||
|
for (var key in params) {
|
||||||
|
if (url == "") {
|
||||||
|
url = "?";
|
||||||
|
} else {
|
||||||
|
url += "&";
|
||||||
|
}
|
||||||
|
url += key + "=" + encodeURIComponent(params[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.fetch("api/v1/photos" + url, function(error, xhr) {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(JSON.stringify(error, null, 2));
|
console.error(JSON.stringify(error, null, 2));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let photos;
|
var results;
|
||||||
try {
|
try {
|
||||||
photos = JSON.parse(xhr.responseText);
|
results = JSON.parse(xhr.responseText);
|
||||||
} catch (___) {
|
} catch (___) {
|
||||||
this.$.toast.text = "Unable to load/parse photo list.";
|
this.$.toast.text = "Unable to load/parse photo list.";
|
||||||
this.$.toast.setAttribute("error", true);
|
this.$.toast.setAttribute("error", true);
|
||||||
@ -148,42 +205,41 @@
|
|||||||
|
|
||||||
var base = document.querySelector("base");
|
var base = document.querySelector("base");
|
||||||
if (base) {
|
if (base) {
|
||||||
this.base = new URL(base.href).pathname;
|
this.base = new URL(base.href).pathname.replace(/\/$/, ""); /* Remove trailing slash if there */
|
||||||
} else {
|
} else {
|
||||||
this.base = "";
|
this.base = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
photos.forEach(function(photo) {
|
if (append) {
|
||||||
photo.path = this.base + photo.path;
|
results.items.forEach(function(photo) {
|
||||||
}.bind(this));
|
this.push("photos", photo);
|
||||||
|
}.bind(this));
|
||||||
function findPath(path, item) {
|
} else {
|
||||||
if (path.indexOf(item.path) != 0) {
|
this.photos = results.items;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (path == item.path || path == item.path + "/") {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < item.paths.length; i++) {
|
|
||||||
var tmp = findPath(path, item.paths[i]);
|
|
||||||
if (tmp) {
|
|
||||||
return tmp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.base != "") {
|
if (dir == +1) {
|
||||||
this.photos = findPath(this.base, photos) || photos;
|
this.prev = start ? true : false;
|
||||||
|
this.next = results.more ? true : false;
|
||||||
} else {
|
} else {
|
||||||
this.photos = photos;
|
this.prev = results.more ? true : false;
|
||||||
|
this.next = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onResize: function(event) {
|
||||||
|
this.debounce("resize", function() {
|
||||||
|
var width = Math.max(this.$.placeholder.offsetWidth || 0, 200),
|
||||||
|
cols = Math.floor(this.clientWidth / width),
|
||||||
|
calc = width + Math.floor((this.clientWidth % width) / cols);
|
||||||
|
if (calc != this.calcWidth) {
|
||||||
|
this.calcWidth = calc;
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
},
|
||||||
|
|
||||||
ready: function() {
|
ready: function() {
|
||||||
window.addEventListener("hashchange", function(event) {
|
window.addEventListener("hashchange", function(event) {
|
||||||
this.hash = event.newURL.replace(/^[^#]*/, "");
|
this.hash = event.newURL.replace(/^[^#]*/, "");
|
||||||
@ -197,7 +253,30 @@
|
|||||||
}
|
}
|
||||||
}.bind(this), 100);
|
}.bind(this), 100);
|
||||||
|
|
||||||
|
window.setInterval(function() {
|
||||||
|
function isElementInViewport(el) {
|
||||||
|
var rect = el.getBoundingClientRect(),
|
||||||
|
vWidth = window.innerWidth || doc.documentElement.clientWidth,
|
||||||
|
vHeight = window.innerHeight || doc.documentElement.clientHeight;
|
||||||
|
|
||||||
|
// Return false if it's not in the viewport
|
||||||
|
if (rect.right < 0 || rect.bottom < 0
|
||||||
|
|| rect.left > vWidth || rect.top > vHeight) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.next && isElementInViewport(this.$.magic)) {
|
||||||
|
this.loadNextPhotos();
|
||||||
|
}
|
||||||
|
|
||||||
|
}.bind(this), 500);
|
||||||
|
|
||||||
this._loadPhotos();
|
this._loadPhotos();
|
||||||
|
|
||||||
|
this.onResize();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -19,13 +19,17 @@
|
|||||||
"body-parser": "^1.18.2",
|
"body-parser": "^1.18.2",
|
||||||
"config": "^1.28.1",
|
"config": "^1.28.1",
|
||||||
"cookie-parser": "^1.4.3",
|
"cookie-parser": "^1.4.3",
|
||||||
|
"exif-reader": "github:paras20xx/exif-reader",
|
||||||
"express": "^4.16.2",
|
"express": "^4.16.2",
|
||||||
"mariasql": "^0.2.6",
|
"mariasql": "^0.2.6",
|
||||||
|
"moment": "^2.22.2",
|
||||||
"morgan": "^1.9.0",
|
"morgan": "^1.9.0",
|
||||||
"mysql2": "^1.5.1",
|
"mysql2": "^1.5.1",
|
||||||
"node-inspector": "^1.1.1",
|
"node-inspector": "^1.1.1",
|
||||||
|
"qs": "^6.5.2",
|
||||||
"sequelize": "^4.28.6",
|
"sequelize": "^4.28.6",
|
||||||
"sequelize-mysql": "^1.7.0"
|
"sequelize-mysql": "^1.7.0",
|
||||||
|
"sharp": "^0.20.5"
|
||||||
},
|
},
|
||||||
"jshintConfig": {
|
"jshintConfig": {
|
||||||
"undef": true,
|
"undef": true,
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
process.env.TZ = "Etc/GMT";
|
||||||
|
|
||||||
|
if (process.env.LOG_LINE) {
|
||||||
|
require("./monkey.js"); /* monkey patch console.log */
|
||||||
|
}
|
||||||
|
|
||||||
console.log("Loading photos.ketr");
|
console.log("Loading photos.ketr");
|
||||||
|
|
||||||
const express = require("express"),
|
const express = require("express"),
|
||||||
|
@ -27,12 +27,49 @@ function init() {
|
|||||||
console.log("DB initialization beginning. DB access will block.");
|
console.log("DB initialization beginning. DB access will block.");
|
||||||
|
|
||||||
return db.sequelize.authenticate().then(function () {
|
return db.sequelize.authenticate().then(function () {
|
||||||
|
const Album = db.sequelize.define('album', {
|
||||||
|
id: {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true
|
||||||
|
},
|
||||||
|
path: Sequelize.STRING,
|
||||||
|
parentId: {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
allowNull: true
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
classMethods: {
|
||||||
|
associate: function() {
|
||||||
|
Album.hasOne(Album, {as:'Album', foreignKey: 'parentId'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const Photo = db.sequelize.define('photo', {
|
const Photo = db.sequelize.define('photo', {
|
||||||
|
id: {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true
|
||||||
|
},
|
||||||
path: Sequelize.STRING,
|
path: Sequelize.STRING,
|
||||||
filename: Sequelize.STRING,
|
filename: Sequelize.STRING,
|
||||||
added: Sequelize.DATE
|
added: Sequelize.DATE,
|
||||||
|
modified: Sequelize.DATE,
|
||||||
|
taken: Sequelize.DATE,
|
||||||
|
width: Sequelize.INTEGER,
|
||||||
|
height: Sequelize.INTEGER,
|
||||||
|
albumId: {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
references: {
|
||||||
|
model: Album,
|
||||||
|
key: 'id',
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
console.log("Connection established successfully with DB.");
|
console.log("Connection established successfully with DB.");
|
||||||
return db.sequelize.sync({
|
return db.sequelize.sync({
|
||||||
force: false
|
force: false
|
||||||
|
29
server/monkey.js
Normal file
29
server/monkey.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
/* monkey-patch console.log to prefix with file/line-number */
|
||||||
|
function lineLogger(logFn) {
|
||||||
|
let cwd = process.cwd(),
|
||||||
|
cwdRe = new RegExp("^[^/]*" + cwd.replace("/", "\\/") + "\/([^:]*:[0-9]*).*$");
|
||||||
|
|
||||||
|
function getErrorObject() {
|
||||||
|
try {
|
||||||
|
throw Error();
|
||||||
|
} catch (err) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let err = getErrorObject(),
|
||||||
|
caller_line = err.stack.split("\n")[4],
|
||||||
|
args = [caller_line.replace(cwdRe, "$1 -")];
|
||||||
|
|
||||||
|
/* arguments.unshift() doesn't exist... */
|
||||||
|
for (var i = 1; i < arguments.length; i++) {
|
||||||
|
args.push(arguments[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
logFn.apply(this, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log = lineLogger.bind(console, console.log);
|
||||||
|
console.warn = lineLogger.bind(console, console.warn);
|
||||||
|
console.error = lineLogger.bind(console, console.error);
|
||||||
|
|
@ -3,7 +3,8 @@
|
|||||||
const express = require("express"),
|
const express = require("express"),
|
||||||
fs = require("fs"),
|
fs = require("fs"),
|
||||||
url = require("url"),
|
url = require("url"),
|
||||||
config = require("config");
|
config = require("config"),
|
||||||
|
moment = require("moment");
|
||||||
|
|
||||||
let photoDB;
|
let photoDB;
|
||||||
|
|
||||||
@ -24,13 +25,95 @@ const router = express.Router();
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
router.get("/", function(req, res/*, next*/) {
|
router.get("/", function(req, res/*, next*/) {
|
||||||
return photoDB.sequelize.query("SELECT path,filename,added FROM photos WHERE path LIKE :path", {
|
let limit = parseInt(req.query.limit) || 50,
|
||||||
|
order = (parseInt(req.query.dir) == -1) ? "DESC" : "", id, cursor, index;
|
||||||
|
|
||||||
|
if (req.query.next) {
|
||||||
|
let parts = req.query.next.split("_");
|
||||||
|
cursor = parts[1];
|
||||||
|
id = parseInt(parts[0]);
|
||||||
|
} else {
|
||||||
|
cursor = "";
|
||||||
|
id = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id == -1) {
|
||||||
|
index = "";
|
||||||
|
} else {
|
||||||
|
if (order == "DESC") {
|
||||||
|
if (id != -1) {
|
||||||
|
index = " AND ((taken=DATE(:cursor) AND id<"+id+ ") OR taken<DATE(:cursor))";
|
||||||
|
} else {
|
||||||
|
index = " AND (taken<=DATE(:cursor))";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (id != -1) {
|
||||||
|
index = " AND ((taken=DATE(:cursor) AND id>"+id+ ") OR taken>DATE(:cursor))";
|
||||||
|
} else {
|
||||||
|
index = " AND (taken>=DATE(:cursor))";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let query = "SELECT * FROM photos WHERE path LIKE :path " + index + " ORDER BY taken " + order + ",id " + order + " LIMIT " + (limit * 2 + 1);
|
||||||
|
return photoDB.sequelize.query(query, {
|
||||||
replacements: {
|
replacements: {
|
||||||
path: req.url + "%"
|
cursor: cursor,
|
||||||
|
path: req.url.replace(/\?.*$/, "") + "%"
|
||||||
},
|
},
|
||||||
type: photoDB.Sequelize.QueryTypes.SELECT
|
type: photoDB.Sequelize.QueryTypes.SELECT
|
||||||
}).then(function(photos) {
|
}).then(function(photos) {
|
||||||
return res.status(200).json(photos);
|
photos.forEach(function(photo) {
|
||||||
|
for (var key in photo) {
|
||||||
|
if (photo[key] instanceof Date) {
|
||||||
|
photo[key].setHours(0, 0, 0, 0);
|
||||||
|
photo[key] = moment(photo[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (order == "DESC") {
|
||||||
|
if (cursor) {
|
||||||
|
photos = photos.filter(function(photo) {
|
||||||
|
if (!cursor.isSame(photo.taken, "day")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return photo.id < id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
photos.reverse();
|
||||||
|
} else {
|
||||||
|
if (cursor) {
|
||||||
|
cursor = moment(cursor);
|
||||||
|
photos = photos.filter(function(photo) {
|
||||||
|
if (!cursor.isSame(photo.taken, "day")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return photo.id > id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let more = photos.length > limit; /* We queried one extra item to see if there are more than LIMIT available */
|
||||||
|
|
||||||
|
if (more) {
|
||||||
|
photos.slice(limit, photos.length);
|
||||||
|
}
|
||||||
|
photos.forEach(function(photo) {
|
||||||
|
photo.path = encodeURI(photo.path);
|
||||||
|
photo.filename = encodeURI(photo.filename);
|
||||||
|
});
|
||||||
|
|
||||||
|
let results = {
|
||||||
|
items: photos
|
||||||
|
};
|
||||||
|
if (more) {
|
||||||
|
results.more = true;
|
||||||
|
}
|
||||||
|
return res.status(200).json(results);
|
||||||
|
}).catch(function(error) {
|
||||||
|
console.error("Query failed: " + query);
|
||||||
|
return Promise.reject(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
const Promise = require("bluebird"),
|
const Promise = require("bluebird"),
|
||||||
fs = require("fs"),
|
fs = require("fs"),
|
||||||
config = require("config");
|
config = require("config"),
|
||||||
|
moment = require("moment");
|
||||||
|
|
||||||
let scanning = 0;
|
let scanning = 0;
|
||||||
|
|
||||||
@ -10,79 +11,119 @@ let photoDB = null;
|
|||||||
|
|
||||||
const picturesPath = config.get("picturesPath");
|
const picturesPath = config.get("picturesPath");
|
||||||
|
|
||||||
function scanFile(path, file, stats) {
|
const processQueue = [];
|
||||||
return new Promise(function(resolve, reject) {
|
|
||||||
console.log("Scanning file: " + path + "/" + file);
|
|
||||||
return resolve(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function scanDir(path) {
|
function scanDir(parent, path) {
|
||||||
return new Promise(function(resolve, reject) {
|
let extensions = [ "jpg", "jpeg", "png", "gif" ],
|
||||||
console.log("Scanning path " + path);
|
re = "\.((" + extensions.join(")|(") + "))$";
|
||||||
|
re = new RegExp(re, "i");
|
||||||
|
|
||||||
fs.readdir(path, function(err, files) {
|
return photoDB.sequelize.query("SELECT id FROM albums WHERE path=:path AND parentId=:parent", {
|
||||||
if (err) {
|
replacements: {
|
||||||
console.warn(" Could not readdir " + path);
|
path: path,
|
||||||
return resolve(null);
|
parent: parent || null
|
||||||
}
|
},
|
||||||
|
type: photoDB.sequelize.QueryTypes.SELECT
|
||||||
|
}).then(function(results) {
|
||||||
|
if (results.length == 0) {
|
||||||
|
console.log("Adding " + path + " under " + parent);
|
||||||
|
return photoDB.sequelize.query("INSERT INTO albums SET path=:path,parentId=:parent", {
|
||||||
|
replacements: {
|
||||||
|
path: path,
|
||||||
|
parent: parent || null
|
||||||
|
},
|
||||||
|
}).then(function(results) {
|
||||||
|
return results[0];
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return results[0].id;
|
||||||
|
}
|
||||||
|
}).then(function(parent) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
// console.log("Scanning path " + path);
|
||||||
|
|
||||||
scanning++;
|
fs.readdir(path, function(err, files) {
|
||||||
|
if (err) {
|
||||||
|
console.warn(" Could not readdir " + path);
|
||||||
|
return resolve(null);
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.map(files, function(file) {
|
scanning++;
|
||||||
let filepath = path + "/" + file;
|
|
||||||
|
|
||||||
return new Promise(function(resolve, reject) {
|
let hasThumbs = false;
|
||||||
fs.stat(filepath, function(err, stats) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
if (err) {
|
if (files[i] == "thumbs") {
|
||||||
console.warn("Could not stat " + filepath);
|
hasThumbs = true;
|
||||||
return resolve(false);
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (stats.isDirectory()) {
|
let tmp = Promise.resolve();
|
||||||
return scanDir(filepath, stats).then(function(entry) {
|
|
||||||
return resolve(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* stats.isFile() */
|
if (!hasThumbs) {
|
||||||
return scanFile(path, file, stats).then(function(entry) {
|
tmp = new Promise(function(resolve, reject) {
|
||||||
if (!entry) {
|
fs.mkdir(path + "/thumbs", function(err) {
|
||||||
return resolve(false);
|
if (err) {
|
||||||
|
return reject("Unable to create " + paths + "/thumbs");
|
||||||
}
|
}
|
||||||
|
return resolve();
|
||||||
const replacements = {
|
|
||||||
path: path.slice(picturesPath.length),
|
|
||||||
filename: file,
|
|
||||||
added: new Date()
|
|
||||||
};
|
|
||||||
|
|
||||||
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) {
|
|
||||||
return photoDB.sequelize.query("INSERT INTO photos " +
|
|
||||||
"SET path=:path,filename=:filename,added=DATE(:added)", {
|
|
||||||
replacements: replacements
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).then(function() {
|
|
||||||
return resolve(true);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}, {
|
|
||||||
concurrency: 10
|
|
||||||
}).then(function() {
|
|
||||||
scanning--;
|
|
||||||
if (scanning == 0) {
|
|
||||||
const endStamp = Date.now();
|
|
||||||
console.log("Scanning completed in " + Math.round(((endStamp - startStamp))) + "ms.");
|
|
||||||
}
|
}
|
||||||
}).then(function() {
|
|
||||||
return resolve();
|
return tmp.then(function() {
|
||||||
|
return Promise.map(files, function(file) {
|
||||||
|
let filepath = path + "/" + file;
|
||||||
|
if (file == "thumbs") {
|
||||||
|
return resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* stats.isFile() */
|
||||||
|
if (!re.exec(file)) {
|
||||||
|
return resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const replacements = {
|
||||||
|
path: path.slice(picturesPath.length),
|
||||||
|
filename: file
|
||||||
|
};
|
||||||
|
|
||||||
|
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: 10
|
||||||
|
}).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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -90,9 +131,86 @@ function scanDir(path) {
|
|||||||
|
|
||||||
const startStamp = Date.now();
|
const startStamp = Date.now();
|
||||||
|
|
||||||
|
let processRunning = false;
|
||||||
|
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
|
||||||
|
const sharp = require("sharp"), exif = require("exif-reader");
|
||||||
|
|
||||||
|
function triggerWatcher() {
|
||||||
|
setTimeout(triggerWatcher, 1000);
|
||||||
|
|
||||||
|
if (!processRunning && processQueue.length) {
|
||||||
|
processRunning = true;
|
||||||
|
return Promise.map(processQueue, function(entry) {
|
||||||
|
var path = entry[0], file = entry[1], created = entry[2], albumId = entry[3],
|
||||||
|
src = picturesPath + path + "/" + file,
|
||||||
|
dst = picturesPath + path + "/thumbs/" + file,
|
||||||
|
image = sharp(src);
|
||||||
|
// console.log("Processing " + src);
|
||||||
|
return image.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,
|
||||||
|
path: path,
|
||||||
|
filename: file,
|
||||||
|
width: metadata.width,
|
||||||
|
height: metadata.height,
|
||||||
|
added: moment().format().replace(/T.*/, "")
|
||||||
|
};
|
||||||
|
|
||||||
|
if (metadata.exif && metadata.exif.exif && metadata.exif.exif.DateTimeOriginal && !isNaN(metadata.exif.exif.DateTimeOriginal.valueOf())) {
|
||||||
|
metadata.exif.exif.DateTimeOriginal.setHours(0, 0, 0, 0);
|
||||||
|
metadata.exif.exif.DateTimeOriginal = metadata.exif.exif.DateTimeOriginal.toISOString().replace(/T.*/, "");
|
||||||
|
// console.log(metadata.exif.exif.DateTimeOriginal);
|
||||||
|
replacements.taken = moment(metadata.exif.exif.DateTimeOriginal, "YYYY-MM-DD").format().replace(/T.*/, "");
|
||||||
|
replacements.modified = moment(metadata.exif.exif.DateTimeOriginal).format().replace(/T.*/, "");
|
||||||
|
} else {
|
||||||
|
// console.log("Missing EXIF info for: " + file);
|
||||||
|
//console.log(JSON.stringify(metadata.exif, null, 2));
|
||||||
|
let patterns = /(20[0-9][0-9]-?[0-9][0-9]-?[0-9][0-9])[_\-]?([0-9]*)/, date = replacements.added;
|
||||||
|
let match = file.match(patterns);
|
||||||
|
if (match) {
|
||||||
|
date = moment(match[1].replace(/-/g, ""), "YYYYMMDD").format();
|
||||||
|
// console.log("Constructed date: " + date);
|
||||||
|
} else {
|
||||||
|
date = moment(created).format();
|
||||||
|
// console.log("Date from file: ", src, date);
|
||||||
|
}
|
||||||
|
replacements.taken = replacements.modified = date;
|
||||||
|
}
|
||||||
|
|
||||||
|
return image.resize(256, 256).toFile(dst).then(function() {
|
||||||
|
return photoDB.sequelize.query("INSERT INTO photos " +
|
||||||
|
"SET albumId=:albumId,path=:path,filename=:filename,added=DATE(:added),modified=DATE(:modified),taken=DATE(:taken),width=:width,height=:height", {
|
||||||
|
replacements: replacements
|
||||||
|
});
|
||||||
|
}).catch(function(error) {
|
||||||
|
console.log("Error resizing or writing " + src, error);
|
||||||
|
return Promise.Reject();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
concurrency: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
scan: function (db) {
|
scan: function (db) {
|
||||||
photoDB = db;
|
photoDB = db;
|
||||||
return scanDir(picturesPath);
|
return scanDir(0, picturesPath).then(function() {
|
||||||
|
triggerWatcher();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user