mirror of https://github.com/thumbsup/thumbsup
Single source of truth for input/outputs + view model for display (#58)
parent
631aee6a4d
commit
e719bf15b1
@ -1,37 +1,30 @@
|
||||
const moment = require('moment')
|
||||
const output = require('./output')
|
||||
|
||||
const MIME_REGEX = /([^/]+)\/(.*)/
|
||||
const EXIF_DATE_FORMAT = 'YYYY:MM:DD HH:mm:ssZ'
|
||||
|
||||
function File (dbFile) {
|
||||
this.path = dbFile.SourceFile
|
||||
this.fileDate = fileDate(dbFile)
|
||||
this.mediaType = mediaType(dbFile)
|
||||
this.exif = {
|
||||
date: exifDate(dbFile),
|
||||
caption: caption(dbFile)
|
||||
}
|
||||
/*
|
||||
Represents a source file on disk and how it maps to output files
|
||||
+ all known metadata
|
||||
*/
|
||||
function File (dbEntry, opts) {
|
||||
this.meta = dbEntry
|
||||
this.path = dbEntry.SourceFile
|
||||
this.date = fileDate(dbEntry)
|
||||
this.type = mediaType(dbEntry)
|
||||
this.output = output.paths(this.path, this.mediaType, opts)
|
||||
}
|
||||
|
||||
function mediaType (dbFile) {
|
||||
const match = MIME_REGEX.exec(dbFile.File.MIMEType)
|
||||
if (match) return match[1]
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
function fileDate (dbFile) {
|
||||
return moment(dbFile.File.FileModifyDate, EXIF_DATE_FORMAT).valueOf()
|
||||
function fileDate (dbEntry) {
|
||||
return moment(dbEntry.File.FileModifyDate, EXIF_DATE_FORMAT).valueOf()
|
||||
}
|
||||
|
||||
function exifDate (dbFile) {
|
||||
if (!dbFile.EXIF) return null
|
||||
return moment(dbFile.EXIF.DateTimeOriginal, EXIF_DATE_FORMAT).valueOf()
|
||||
}
|
||||
|
||||
function caption (dbFile) {
|
||||
const desc = dbFile.EXIF ? dbFile.EXIF.ImageDescription : null
|
||||
const caption = dbFile.IPTC ? dbFile.IPTC['Caption-Abstract'] : null
|
||||
return desc || caption
|
||||
function mediaType (dbEntry) {
|
||||
const match = MIME_REGEX.exec(dbEntry.File.MIMEType)
|
||||
// "image" or "video"
|
||||
if (match) return match[1]
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
module.exports = File
|
||||
|
@ -0,0 +1,62 @@
|
||||
|
||||
exports.paths = function (filepath, mediaType, config) {
|
||||
if (mediaType === 'video') {
|
||||
var originals = config ? config.originalVideos : false
|
||||
return videoOutput(filepath, original)
|
||||
} else {
|
||||
var originals = config ? config.originalPhotos : false
|
||||
return imageOutput(filepath)
|
||||
}
|
||||
}
|
||||
|
||||
function videoOutput (filepath, originals) {
|
||||
var output = {
|
||||
thumbnail: {
|
||||
path: 'media/thumbs/' + ext(filepath, 'jpg'),
|
||||
rel: 'video:thumbnail'
|
||||
},
|
||||
large: {
|
||||
path: 'media/large/' + ext(filepath, 'jpg'),
|
||||
rel: 'video:poster'
|
||||
},
|
||||
video: {
|
||||
path: 'media/large/' + ext(filepath, 'mp4'),
|
||||
rel: 'video:resized'
|
||||
}
|
||||
}
|
||||
if (originals) {
|
||||
output.download = {
|
||||
path: 'media/original/' + filepath,
|
||||
rel: 'original'
|
||||
}
|
||||
} else {
|
||||
output.download = output.video
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function imageOutput (filepath, originals) {
|
||||
const output = {
|
||||
thumbnail: {
|
||||
path: 'media/thumbs/' + filepath,
|
||||
rel: 'photo:thumbnail'
|
||||
},
|
||||
large: {
|
||||
path: 'media/large/' + filepath,
|
||||
rel: 'photo:large'
|
||||
}
|
||||
}
|
||||
if (originals) {
|
||||
output.download = {
|
||||
path: 'media/original/' + filepath,
|
||||
rel: 'original'
|
||||
}
|
||||
} else {
|
||||
output.download = output.large
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
function ext(file, ext) {
|
||||
return file.replace(/\.[a-z0-9]+$/i, '.' + ext)
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
var path = require('path');
|
||||
var index = 0;
|
||||
|
||||
var GIF_REGEX = /\.gif$/i
|
||||
|
||||
function File(filepath, metadata) {
|
||||
this.id = ++index;
|
||||
this.filepath = filepath;
|
||||
this.filename = path.basename(filepath);
|
||||
this.date = new Date(metadata.exif.date || metadata.fileDate);
|
||||
this.caption = metadata.exif.caption;
|
||||
this.isVideo = (metadata.mediaType === 'video');
|
||||
this.isAnimated = GIF_REGEX.test(filepath);
|
||||
this.urls = urls(filepath, metadata.mediaType);
|
||||
}
|
||||
|
||||
function urls(filepath, mediaType) {
|
||||
return (mediaType === 'video') ? videoUrls(filepath) : photoUrls(filepath);
|
||||
}
|
||||
|
||||
function videoUrls(filepath) {
|
||||
return {
|
||||
thumb: 'media/thumbs/' + ext(filepath, 'jpg'),
|
||||
large: 'media/large/' + ext(filepath, 'jpg'),
|
||||
poster: 'media/large/' + ext(filepath, 'jpg'),
|
||||
video: 'media/large/' + ext(filepath, 'mp4'),
|
||||
original: 'media/original/' + filepath
|
||||
};
|
||||
}
|
||||
|
||||
function photoUrls(filepath) {
|
||||
return {
|
||||
thumb: 'media/thumbs/' + filepath,
|
||||
large: 'media/large/' + filepath,
|
||||
original: 'media/original/' + filepath
|
||||
};
|
||||
}
|
||||
|
||||
function ext(file, ext) {
|
||||
return file.replace(/\.[a-z0-9]+$/i, '.' + ext);
|
||||
}
|
||||
|
||||
module.exports = File;
|
@ -1,27 +1,28 @@
|
||||
var _ = require('lodash');
|
||||
var gm = require('gm');
|
||||
var pad = require('pad');
|
||||
var path = require('path');
|
||||
var Album = require('./album');
|
||||
var byFolder = require('./by-folder');
|
||||
var byDate = require('./by-date');
|
||||
var _ = require('lodash')
|
||||
var gm = require('gm')
|
||||
var pad = require('pad')
|
||||
var path = require('path')
|
||||
var Album = require('./album')
|
||||
var Media = require('./media')
|
||||
var byFolder = require('./by-folder')
|
||||
var byDate = require('./by-date')
|
||||
|
||||
exports.createAlbums = function(collection, opts) {
|
||||
exports.createAlbums = function (collection, opts) {
|
||||
|
||||
// top-level album for the home page
|
||||
var home = new Album('Home');
|
||||
home.filename = opts.index || 'index';
|
||||
var home = new Album('Home')
|
||||
home.filename = opts.index || 'index'
|
||||
|
||||
// create albums
|
||||
if (opts.albumsFrom === 'folders') {
|
||||
home.albums = byFolder.albums(collection, opts);
|
||||
home.albums = byFolder.albums(collection, opts)
|
||||
} else if (opts.albumsFrom === 'date') {
|
||||
home.albums = byDate.albums(collection, opts);
|
||||
home.albums = byDate.albums(collection, opts)
|
||||
} else {
|
||||
throw 'Invalid <albumsFrom> option';
|
||||
throw 'Invalid <albumsFrom> option'
|
||||
}
|
||||
|
||||
// finalize all albums recursively (calculate stats, etc...)
|
||||
home.finalize(opts);
|
||||
return home;
|
||||
};
|
||||
home.finalize(opts)
|
||||
return home
|
||||
}
|
||||
|
@ -0,0 +1,46 @@
|
||||
const _ = require('lodash')
|
||||
const moment = require('moment')
|
||||
const path = require('path')
|
||||
|
||||
const EXIF_DATE_FORMAT = 'YYYY:MM:DD HH:mm:ssZ'
|
||||
var index = 0;
|
||||
|
||||
/*
|
||||
View model for album entries
|
||||
*/
|
||||
function Media (file) {
|
||||
this.id = ++index
|
||||
this.file = file
|
||||
this.filename = path.basename(file.path)
|
||||
this.urls = _.mapValues(file.output, o => o.path)
|
||||
this.date = exifDate(file)
|
||||
this.caption = caption(file)
|
||||
this.isVideo = (file.type === 'video')
|
||||
this.isAnimated = animated(file)
|
||||
// view model could also include fields like
|
||||
// - country = "England"
|
||||
// - city = "London"
|
||||
// - exif summary = [
|
||||
// { field: "Aperture", icon: "fa-camera", value: "1.8" }
|
||||
// ]
|
||||
}
|
||||
|
||||
function exifDate (file) {
|
||||
if (!file.meta.EXIF) return file.date
|
||||
const exifDate = moment(file.meta.EXIF['DateTimeOriginal'], EXIF_DATE_FORMAT).valueOf()
|
||||
return exifDate || file.date
|
||||
}
|
||||
|
||||
function caption (file) {
|
||||
const desc = file.meta.EXIF ? file.meta.EXIF['ImageDescription'] : null
|
||||
const caption = file.meta.IPTC ? file.meta.IPTC['Caption-Abstract'] : null
|
||||
return desc || caption
|
||||
}
|
||||
|
||||
function animated (file) {
|
||||
if (file.meta.File['MIMEType'] !== 'image/gif') return false
|
||||
if (file.meta.GIF && file.meta.GIF.FrameCount > 0) return true
|
||||
return false
|
||||
}
|
||||
|
||||
module.exports = Media
|
@ -1,45 +1,34 @@
|
||||
var File = require('../src/model/file');
|
||||
const File = require('../src/input/file')
|
||||
const Media = require('../src/model/media')
|
||||
|
||||
exports.metadata = function() {
|
||||
exports.file = function (opts) {
|
||||
opts = opts || {}
|
||||
return {
|
||||
fileDate: new Date(),
|
||||
mediaType: 'image',
|
||||
exif: {
|
||||
date: null,
|
||||
orientation: 1,
|
||||
caption: ''
|
||||
path: opts.path || 'path/image.jpg',
|
||||
date: opts.date ? new Date(opts.date).getTime() : new Date().getTime(),
|
||||
type: opts.type || 'image',
|
||||
meta: {
|
||||
SourceFile: 'path/image.jpg',
|
||||
File: {},
|
||||
EXIF: {},
|
||||
IPTC: {},
|
||||
XMP: {}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
exports.date = function(str) {
|
||||
return new Date(Date.parse(str));
|
||||
};
|
||||
exports.date = function (str) {
|
||||
return new Date(Date.parse(str))
|
||||
}
|
||||
|
||||
exports.photo = function(opts) {
|
||||
opts = opts || {};
|
||||
var date = opts.date ? new Date(Date.parse(opts.date)) : new Date();
|
||||
return new File(opts.path || 'tmp', {
|
||||
fileDate: date,
|
||||
mediaType: 'image',
|
||||
exif: {
|
||||
date: null,
|
||||
orientation: 1,
|
||||
caption: ''
|
||||
}
|
||||
});
|
||||
};
|
||||
exports.photo = function (opts) {
|
||||
opts = opts || {}
|
||||
opts.type = 'image'
|
||||
return new Media(exports.file(opts))
|
||||
}
|
||||
|
||||
exports.video = function(opts) {
|
||||
opts = opts || {};
|
||||
var date = opts.date ? new Date(Date.parse(opts.date)) : new Date();
|
||||
return new File(opts.path || 'tmp', {
|
||||
fileDate: date,
|
||||
mediaType: 'video',
|
||||
exif: {
|
||||
date: null,
|
||||
orientation: 1,
|
||||
caption: ''
|
||||
}
|
||||
});
|
||||
};
|
||||
exports.video = function (opts) {
|
||||
opts = opts || {}
|
||||
opts.type = 'video'
|
||||
return new Media(exports.file(opts))
|
||||
}
|
||||
|
@ -1,43 +0,0 @@
|
||||
var should = require('should/as-function');
|
||||
var File = require('../../src/model/file');
|
||||
var fixtures = require('../fixtures');
|
||||
|
||||
describe('File', function() {
|
||||
|
||||
it('stores the file name', function(){
|
||||
var f = new File('holidays/newyork/IMG_000001.jpg', fixtures.metadata());
|
||||
should(f.filename).eql('IMG_000001.jpg');
|
||||
});
|
||||
|
||||
it('reads the date from the file <mdate>', function() {
|
||||
var meta = fixtures.metadata();
|
||||
meta.fileDate = fixtures.date('2016-09-23');
|
||||
meta.exif.date = null;
|
||||
var f = new File('IMG_000001.jpg', meta);
|
||||
should(f.date).eql(fixtures.date('2016-09-23'));
|
||||
});
|
||||
|
||||
it('can tell if a file is a photo', function() {
|
||||
var file = new File('test.jpg', fixtures.metadata());
|
||||
should(file.isVideo).eql(false);
|
||||
should(file.isAnimated).eql(false);
|
||||
});
|
||||
|
||||
it('can tell if a file is a video', function() {
|
||||
var meta = fixtures.metadata();
|
||||
meta.mediaType = 'video';
|
||||
var file = new File('test.mp4', meta);
|
||||
should(file.isVideo).eql(true);
|
||||
should(file.isAnimated).eql(false);
|
||||
});
|
||||
|
||||
it('can tell if a file is an animated GIF', function() {
|
||||
var meta = fixtures.metadata();
|
||||
meta.mediaType = 'image';
|
||||
var file = new File('test.gif', meta);
|
||||
should(file.isVideo).eql(false);
|
||||
should(file.isAnimated).eql(true);
|
||||
});
|
||||
|
||||
|
||||
});
|
@ -0,0 +1,80 @@
|
||||
var should = require('should/as-function');
|
||||
var Media = require('../../src/model/media');
|
||||
var fixtures = require('../fixtures');
|
||||
|
||||
describe('Media', function () {
|
||||
|
||||
describe('date taken', function () {
|
||||
it('reads the EXIF date if present', function () {
|
||||
const file = fixtures.file()
|
||||
file.meta.EXIF.DateTimeOriginal = '2016:10:28 17:34:58' // EXIF date format
|
||||
const media = new Media(file)
|
||||
should(media.date).eql(fixtures.date('2016-10-28 17:34:58').getTime())
|
||||
})
|
||||
|
||||
it('defaults to the file date if there is no EXIF date', function () {
|
||||
const file = fixtures.file({date: '2016-10-28 17:34:58'})
|
||||
const media = new Media(file)
|
||||
should(media.date).eql(fixtures.date('2016-10-28 17:34:58').getTime())
|
||||
})
|
||||
})
|
||||
|
||||
describe('photos and videos', function () {
|
||||
it('can tell if a file is a regular photo', function() {
|
||||
const file = fixtures.file({type: 'image'})
|
||||
file.meta.File.MIMEType = 'image/jpeg'
|
||||
const media = new Media(file)
|
||||
should(media.isVideo).eql(false)
|
||||
should(media.isAnimated).eql(false)
|
||||
})
|
||||
|
||||
it('can tell if a file is a non-animated gif', function() {
|
||||
const file = fixtures.file({type: 'image'})
|
||||
file.meta.File.MIMEType = 'image/gif'
|
||||
const media = new Media(file)
|
||||
should(media.isVideo).eql(false)
|
||||
should(media.isAnimated).eql(false)
|
||||
})
|
||||
|
||||
it('can tell if a file is an animated gif', function() {
|
||||
const file = fixtures.file({type: 'image'})
|
||||
file.meta.File.MIMEType = 'image/gif'
|
||||
file.meta.GIF = {FrameCount: 10}
|
||||
const media = new Media(file)
|
||||
should(media.isVideo).eql(false)
|
||||
should(media.isAnimated).eql(true)
|
||||
})
|
||||
|
||||
it('can tell if a file is a video', function() {
|
||||
const file = fixtures.file({type: 'video'})
|
||||
const media = new Media(file)
|
||||
should(media.isVideo).eql(true)
|
||||
should(media.isAnimated).eql(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('caption', function() {
|
||||
it('uses the EXIF caption if present', function () {
|
||||
const file = fixtures.file()
|
||||
file.meta.EXIF['ImageDescription'] = 'some caption'
|
||||
const media = new Media(file)
|
||||
should(media.caption).eql('some caption')
|
||||
})
|
||||
|
||||
it('uses the IPTC caption if present', function () {
|
||||
const file = fixtures.file()
|
||||
file.meta.IPTC['Caption-Abstract'] = 'some caption'
|
||||
const media = new Media(file)
|
||||
should(media.caption).eql('some caption')
|
||||
})
|
||||
|
||||
it('uses the EXIF caption if both EXIF and IPTC exist', function () {
|
||||
const file = fixtures.file()
|
||||
file.meta.EXIF['ImageDescription'] = 'exif caption'
|
||||
file.meta.IPTC['Caption-Abstract'] = 'iptc caption'
|
||||
const media = new Media(file)
|
||||
should(media.caption).eql('exif caption')
|
||||
})
|
||||
})
|
||||
|
||||
})
|
Loading…
Reference in New Issue