mirror of https://github.com/thumbsup/thumbsup
Create a Metadata model attached to input files
- for easier unit testing - to enable input filtering (e.g. only include photos with this keyword)pull/77/head
parent
5074fb267f
commit
3e64d2ab38
@ -1,30 +0,0 @@
|
||||
const moment = require('moment')
|
||||
const output = require('./output')
|
||||
|
||||
const MIME_REGEX = /([^/]+)\/(.*)/
|
||||
const EXIF_DATE_FORMAT = 'YYYY:MM:DD HH:mm:ssZ'
|
||||
|
||||
/*
|
||||
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.type, opts || {})
|
||||
}
|
||||
|
||||
function fileDate (dbEntry) {
|
||||
return moment(dbEntry.File.FileModifyDate, EXIF_DATE_FORMAT).valueOf()
|
||||
}
|
||||
|
||||
function mediaType (dbEntry) {
|
||||
const match = MIME_REGEX.exec(dbEntry.File.MIMEType)
|
||||
if (match && match[1] === 'image') return 'image'
|
||||
if (match && match[1] === 'video') return 'video'
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
module.exports = File
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
--------------------------------------------------------------------------------
|
||||
Returns the target album path for a single file.
|
||||
Can be based on anything, e.g. directory name, date, metadata keywords...
|
||||
e.g. `Holidays/London/IMG_00001.jpg` -> `Holidays/London`
|
||||
--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
const moment = require('moment')
|
||||
const path = require('path')
|
||||
|
||||
exports.create = function (opts) {
|
||||
var mapper = null
|
||||
if (opts.albumsFrom === 'folders') {
|
||||
mapper = (file) => path.dirname(file.path)
|
||||
} else if (opts.albumsFrom === 'date') {
|
||||
var dateFormat = opts.albumsDateFormat || 'YYYY MMMM'
|
||||
mapper = (file) => moment(file.meta.date).format(dateFormat)
|
||||
} else {
|
||||
throw new Error('Invalid <albumsFrom> option')
|
||||
}
|
||||
return mapper
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
--------------------------------------------------------------------------------
|
||||
Represents a file on disk, inside the input folder
|
||||
Also includes how it maps to the different output files
|
||||
--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
const _ = require('lodash')
|
||||
const path = require('path')
|
||||
const moment = require('moment')
|
||||
const output = require('./output')
|
||||
|
||||
const MIME_REGEX = /([^/]+)\/(.*)/
|
||||
const EXIF_DATE_FORMAT = 'YYYY:MM:DD HH:mm:ssZ'
|
||||
|
||||
var index = 0
|
||||
|
||||
class File {
|
||||
constructor (dbEntry, meta, opts) {
|
||||
this.id = ++index
|
||||
this.path = dbEntry.SourceFile
|
||||
this.filename = path.basename(dbEntry.SourceFile)
|
||||
this.date = fileDate(dbEntry)
|
||||
this.type = mediaType(dbEntry)
|
||||
this.isVideo = (this.type === 'video')
|
||||
this.output = output.paths(this.path, this.type, opts || {})
|
||||
this.urls = _.mapValues(this.output, o => o.path.replace('\\', '/'))
|
||||
this.meta = meta
|
||||
}
|
||||
}
|
||||
|
||||
function fileDate (dbEntry) {
|
||||
return moment(dbEntry.File.FileModifyDate, EXIF_DATE_FORMAT).valueOf()
|
||||
}
|
||||
|
||||
function mediaType (dbEntry) {
|
||||
const match = MIME_REGEX.exec(dbEntry.File.MIMEType)
|
||||
if (match && match[1] === 'image') return 'image'
|
||||
if (match && match[1] === 'video') return 'video'
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
module.exports = File
|
@ -1,15 +0,0 @@
|
||||
const moment = require('moment')
|
||||
const path = require('path')
|
||||
|
||||
exports.create = function (opts) {
|
||||
var mapper = null
|
||||
if (opts.albumsFrom === 'folders') {
|
||||
mapper = (media) => path.dirname(media.file.path)
|
||||
} else if (opts.albumsFrom === 'date') {
|
||||
var dateFormat = opts.albumsDateFormat || 'YYYY MMMM'
|
||||
mapper = (media) => moment(media.date).format(dateFormat)
|
||||
} else {
|
||||
throw new Error('Invalid <albumsFrom> option')
|
||||
}
|
||||
return mapper
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
const _ = require('lodash')
|
||||
const moment = require('moment')
|
||||
const path = require('path')
|
||||
|
||||
const EXIF_DATE_FORMAT = 'YYYY:MM:DD HH:mm:ssZ'
|
||||
|
||||
// infer dates from files with a date-looking filename
|
||||
const FILENAME_DATE_REGEX = /\d{4}[_\-.\s]?(\d{2}[_\-.\s]?){5}\..{3,4}/
|
||||
|
||||
// moment ignores non-numeric characters when parsing
|
||||
const FILENAME_DATE_FORMAT = 'YYYYMMDD HHmmss'
|
||||
|
||||
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 = getDate(file)
|
||||
this.caption = caption(file)
|
||||
this.isVideo = (file.type === 'video')
|
||||
this.isAnimated = animated(file)
|
||||
this.rating = rating(file)
|
||||
// view model could also include fields like
|
||||
// - country = "England"
|
||||
// - city = "London"
|
||||
// - exif summary = [
|
||||
// { field: "Aperture", icon: "fa-camera", value: "1.8" }
|
||||
// ]
|
||||
}
|
||||
|
||||
function getDate (file) {
|
||||
const date = tagValue(file, 'EXIF', 'DateTimeOriginal') ||
|
||||
tagValue(file, 'H264', 'DateTimeOriginal') ||
|
||||
tagValue(file, 'QuickTime', 'CreationDate')
|
||||
if (date) {
|
||||
return moment(date, EXIF_DATE_FORMAT).valueOf()
|
||||
} else {
|
||||
const filename = path.basename(file.path)
|
||||
if (FILENAME_DATE_REGEX.test(filename)) {
|
||||
const namedate = moment(filename, FILENAME_DATE_FORMAT)
|
||||
if (namedate.isValid()) return namedate.valueOf()
|
||||
}
|
||||
return file.date
|
||||
}
|
||||
}
|
||||
|
||||
function caption (file) {
|
||||
return tagValue(file, 'EXIF', 'ImageDescription') ||
|
||||
tagValue(file, 'IPTC', 'Caption-Abstract') ||
|
||||
tagValue(file, 'IPTC', 'Headline') ||
|
||||
tagValue(file, 'XMP', 'Description') ||
|
||||
tagValue(file, 'XMP', 'Title') ||
|
||||
tagValue(file, 'XMP', 'Label')
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function rating (file) {
|
||||
if (!file.meta.XMP) return 0
|
||||
return file.meta.XMP['Rating'] || 0
|
||||
}
|
||||
|
||||
function tagValue (file, type, name) {
|
||||
if (!file.meta[type]) return null
|
||||
return file.meta[type][name]
|
||||
}
|
||||
|
||||
module.exports = Media
|
@ -0,0 +1,91 @@
|
||||
/*
|
||||
--------------------------------------------------------------------------------
|
||||
Standardised metadata for a given image or video
|
||||
This is based on parsing "provider data" such as Exiftool or Picasa
|
||||
--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
const moment = require('moment')
|
||||
const path = require('path')
|
||||
|
||||
// mime type for videos
|
||||
const MIME_VIDEO_REGEX = /^video\/.*$/
|
||||
|
||||
// standard EXIF date format, which is different from ISO8601
|
||||
const EXIF_DATE_FORMAT = 'YYYY:MM:DD HH:mm:ssZ'
|
||||
|
||||
// infer dates from files with a date-looking filename
|
||||
const FILENAME_DATE_REGEX = /\d{4}[_\-.\s]?(\d{2}[_\-.\s]?){5}\..{3,4}/
|
||||
|
||||
// moment ignores non-numeric characters when parsing
|
||||
const FILENAME_DATE_FORMAT = 'YYYYMMDD HHmmss'
|
||||
|
||||
class Metadata {
|
||||
constructor (exiftool) {
|
||||
// standardise metadata
|
||||
this.date = getDate(exiftool)
|
||||
this.caption = caption(exiftool)
|
||||
this.keywords = keywords(exiftool)
|
||||
this.video = video(exiftool)
|
||||
this.animated = animated(exiftool)
|
||||
this.rating = rating(exiftool)
|
||||
// metadata could also include fields like
|
||||
// - lat = 51.5
|
||||
// - long = 0.12
|
||||
// - country = "England"
|
||||
// - city = "London"
|
||||
// - aperture = 1.8
|
||||
}
|
||||
}
|
||||
|
||||
function getDate (exif) {
|
||||
const date = tagValue(exif, 'EXIF', 'DateTimeOriginal') ||
|
||||
tagValue(exif, 'H264', 'DateTimeOriginal') ||
|
||||
tagValue(exif, 'QuickTime', 'CreationDate')
|
||||
if (date) {
|
||||
return moment(date, EXIF_DATE_FORMAT).valueOf()
|
||||
} else {
|
||||
const filename = path.basename(exif.SourceFile)
|
||||
if (FILENAME_DATE_REGEX.test(filename)) {
|
||||
const namedate = moment(filename, FILENAME_DATE_FORMAT)
|
||||
if (namedate.isValid()) return namedate.valueOf()
|
||||
}
|
||||
return moment(exif.File.FileModifyDate, EXIF_DATE_FORMAT).valueOf()
|
||||
}
|
||||
}
|
||||
|
||||
function caption (exif, picasa) {
|
||||
return tagValue(exif, 'EXIF', 'ImageDescription') ||
|
||||
tagValue(exif, 'IPTC', 'Caption-Abstract') ||
|
||||
tagValue(exif, 'IPTC', 'Headline') ||
|
||||
tagValue(exif, 'XMP', 'Description') ||
|
||||
tagValue(exif, 'XMP', 'Title') ||
|
||||
tagValue(exif, 'XMP', 'Label')
|
||||
}
|
||||
|
||||
function keywords (exif, picasa) {
|
||||
const values = tagValue(exif, 'IPTC', 'Keywords')
|
||||
return values ? values.split(',') : []
|
||||
}
|
||||
|
||||
function video (exif) {
|
||||
return MIME_VIDEO_REGEX.test(exif.File['MIMEType'])
|
||||
}
|
||||
|
||||
function animated (exif) {
|
||||
if (exif.File['MIMEType'] !== 'image/gif') return false
|
||||
if (exif.GIF && exif.GIF.FrameCount > 0) return true
|
||||
return false
|
||||
}
|
||||
|
||||
function rating (exif) {
|
||||
if (!exif.XMP) return 0
|
||||
return exif.XMP['Rating'] || 0
|
||||
}
|
||||
|
||||
function tagValue (exif, type, name) {
|
||||
if (!exif[type]) return null
|
||||
return exif[type][name]
|
||||
}
|
||||
|
||||
module.exports = Metadata
|
@ -0,0 +1,38 @@
|
||||
{{~#if isVideo~}}
|
||||
|
||||
{{!--
|
||||
Video thumbnails
|
||||
--}}
|
||||
|
||||
<li data-html="#media{{id}}"
|
||||
data-download-url="{{relative urls.download}}">
|
||||
<a href="{{relative urls.download}}">
|
||||
<img src="{{relative urls.thumbnail}}"
|
||||
width="{{@root.gallery.thumbSize}}"
|
||||
height="{{@root.gallery.thumbSize}}"
|
||||
alt="{{filename}}" />
|
||||
</a>
|
||||
<img class="video-overlay" src="{{relative 'public/play.png'}}" />
|
||||
</li>
|
||||
|
||||
{{~else~}}
|
||||
|
||||
{{!--
|
||||
Image thumbnails
|
||||
--}}
|
||||
|
||||
<li data-src="{{relative urls.large}}"
|
||||
data-sub-html="{{caption}}"
|
||||
data-download-url="{{relative urls.download}}">
|
||||
<a href="{{relative urls.download}}">
|
||||
<img src="{{relative urls.thumbnail}}"
|
||||
width="{{@root.gallery.thumbSize}}"
|
||||
height="{{@root.gallery.thumbSize}}"
|
||||
alt="{{filename}}" />
|
||||
</a>
|
||||
{{#if isAnimated}}
|
||||
<img class="video-overlay" src="{{relative 'public/play.png'}}" />
|
||||
{{/if}}
|
||||
</li>
|
||||
|
||||
{{~/if~}}
|
@ -1,33 +1,46 @@
|
||||
const Media = require('../src/model/media')
|
||||
const moment = require('moment')
|
||||
const File = require('../src/model/file')
|
||||
const Metadata = require('../src/model/metadata')
|
||||
|
||||
exports.file = function (opts) {
|
||||
exports.exiftool = function (opts) {
|
||||
opts = opts || {}
|
||||
return {
|
||||
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: {}
|
||||
}
|
||||
SourceFile: opts.path || 'path/image.jpg',
|
||||
File: {
|
||||
FileModifyDate: opts.date || '2016:08:24 14:51:36',
|
||||
MIMEType: opts.mimeType || 'image/jpg'
|
||||
},
|
||||
EXIF: {},
|
||||
IPTC: {},
|
||||
XMP: {},
|
||||
H264: {},
|
||||
QuickTime: {}
|
||||
}
|
||||
}
|
||||
|
||||
exports.metadata = function (opts) {
|
||||
return new Metadata(exports.exiftool(opts))
|
||||
}
|
||||
|
||||
exports.file = function (opts) {
|
||||
const exiftool = exports.exiftool(opts)
|
||||
const meta = new Metadata(exiftool)
|
||||
return new File(exiftool, meta)
|
||||
}
|
||||
|
||||
exports.date = function (str) {
|
||||
return new Date(Date.parse(str))
|
||||
return new Date(moment(str, 'YYYYMMDD HHmmss').valueOf())
|
||||
// return new Date(Date.parse(str))
|
||||
}
|
||||
|
||||
exports.photo = function (opts) {
|
||||
opts = opts || {}
|
||||
opts.type = 'image'
|
||||
return new Media(exports.file(opts))
|
||||
opts.mimeType = 'image/jpg'
|
||||
return exports.file(opts)
|
||||
}
|
||||
|
||||
exports.video = function (opts) {
|
||||
opts = opts || {}
|
||||
opts.type = 'video'
|
||||
return new Media(exports.file(opts))
|
||||
opts.mimeType = 'video/mp4'
|
||||
return exports.file(opts)
|
||||
}
|
||||
|
@ -1,32 +1,32 @@
|
||||
var should = require('should/as-function')
|
||||
var mapper = require('../../src/model/mapper.js')
|
||||
var mapper = require('../../src/input/mapper.js')
|
||||
var fixtures = require('../fixtures.js')
|
||||
|
||||
describe('mapper', function () {
|
||||
it('can create a path mapper', function () {
|
||||
const map = mapper.create({albumsFrom: 'folders'})
|
||||
const media = fixtures.photo({
|
||||
const entry = fixtures.photo({
|
||||
path: 'holidays/canada/IMG_0001.jpg'
|
||||
})
|
||||
should(map(media)).eql('holidays/canada')
|
||||
should(map(entry)).eql('holidays/canada')
|
||||
})
|
||||
it('can create a default date mapper', function () {
|
||||
const map = mapper.create({albumsFrom: 'date'})
|
||||
const media = fixtures.photo({
|
||||
const entry = fixtures.photo({
|
||||
path: 'holidays/canada/IMG_0001.jpg',
|
||||
date: '2016-07-14 12:07:41'
|
||||
date: '2016:07:14 12:07:41'
|
||||
})
|
||||
should(map(media)).eql('2016 July')
|
||||
should(map(entry)).eql('2016 July')
|
||||
})
|
||||
it('can create a custom date mapper', function () {
|
||||
const map = mapper.create({
|
||||
albumsFrom: 'date',
|
||||
albumsDateFormat: 'YYYY/MM'
|
||||
})
|
||||
const media = fixtures.photo({
|
||||
const entry = fixtures.photo({
|
||||
path: 'holidays/canada/IMG_0001.jpg',
|
||||
date: '2016-07-14 12:07:41'
|
||||
date: '2016:07:14 12:07:41'
|
||||
})
|
||||
should(map(media)).eql('2016/07')
|
||||
should(map(entry)).eql('2016/07')
|
||||
})
|
||||
})
|
@ -1,137 +0,0 @@
|
||||
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('reads the H264 date if present', function () {
|
||||
const file = fixtures.file()
|
||||
file.meta.H264 = {}
|
||||
file.meta.H264.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('reads the QuickTime date if present', function () {
|
||||
const file = fixtures.file()
|
||||
file.meta.QuickTime = {}
|
||||
file.meta.QuickTime.CreationDate = '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('infers the date from the filename (Android format)', function () {
|
||||
const file = fixtures.file({path: 'folder/VID_20170220_114006.mp4'})
|
||||
const media = new Media(file)
|
||||
should(media.date).eql(fixtures.date('2017-02-20 11:40:06').getTime())
|
||||
})
|
||||
|
||||
it('infers the date from the filename (Dropbox format)', function () {
|
||||
const file = fixtures.file({path: 'folder/2017-03-24 19.42.30.jpg'})
|
||||
const media = new Media(file)
|
||||
should(media.date).eql(fixtures.date('2017-03-24 19:42:30').getTime())
|
||||
})
|
||||
|
||||
it('only uses the file name, not digits from the folder name', function () {
|
||||
const file = fixtures.file({path: '1000 photos/2017-03-24 19.42.30.jpg'})
|
||||
const media = new Media(file)
|
||||
should(media.date).eql(fixtures.date('2017-03-24 19:42:30').getTime())
|
||||
})
|
||||
|
||||
it('only infers dates from valid formats', function () {
|
||||
const file = fixtures.file({
|
||||
path: 'folder/IMG_1234.jpg',
|
||||
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())
|
||||
})
|
||||
|
||||
it('does not look at the file name if it already has EXIF data', function () {
|
||||
const file = fixtures.file({path: '2017-03-24 19.42.30.jpg'})
|
||||
file.meta.EXIF.DateTimeOriginal = '2016:10:28 17:34:58'
|
||||
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 other 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('is read from all standard EXIF/IPTC/XMP tags', function () {
|
||||
const tags = [
|
||||
{ type: 'EXIF', tag: 'ImageDescription' },
|
||||
{ type: 'IPTC', tag: 'Caption-Abstract' },
|
||||
{ type: 'IPTC', tag: 'Headline' },
|
||||
{ type: 'XMP', tag: 'Description' },
|
||||
{ type: 'XMP', tag: 'Title' },
|
||||
{ type: 'XMP', tag: 'Label' }
|
||||
]
|
||||
tags.forEach(t => {
|
||||
const file = fixtures.file()
|
||||
file.meta[t.type][t.tag] = 'some caption'
|
||||
const media = new Media(file)
|
||||
should(media.caption).eql('some caption')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('rating', function () {
|
||||
it('defaults to a rating of 0', function () {
|
||||
const file = fixtures.file()
|
||||
const media = new Media(file)
|
||||
should(media.rating).eql(0)
|
||||
})
|
||||
it('reads the rating from the XMP tags', function () {
|
||||
const file = fixtures.file()
|
||||
file.meta.XMP['Rating'] = 3
|
||||
const media = new Media(file)
|
||||
should(media.rating).eql(3)
|
||||
})
|
||||
})
|
||||
})
|
@ -0,0 +1,171 @@
|
||||
const should = require('should/as-function')
|
||||
const Metadata = require('../../src/model/metadata')
|
||||
var fixtures = require('../fixtures')
|
||||
|
||||
describe('Metadata', function () {
|
||||
describe('date taken', function () {
|
||||
it('reads the EXIF date if present', function () {
|
||||
const exiftool = fixtures.exiftool()
|
||||
exiftool.EXIF.DateTimeOriginal = '2016:10:28 17:34:58' // EXIF date format
|
||||
const meta = new Metadata(exiftool)
|
||||
should(meta.date).eql(fixtures.date('2016-10-28 17:34:58').getTime())
|
||||
})
|
||||
|
||||
it('reads the H264 date if present', function () {
|
||||
const exiftool = fixtures.exiftool()
|
||||
exiftool.H264.DateTimeOriginal = '2016:10:28 17:34:58' // EXIF date format
|
||||
const meta = new Metadata(exiftool)
|
||||
should(meta.date).eql(fixtures.date('2016-10-28 17:34:58').getTime())
|
||||
})
|
||||
|
||||
it('reads the QuickTime date if present', function () {
|
||||
const exiftool = fixtures.exiftool()
|
||||
exiftool.QuickTime.CreationDate = '2016:10:28 17:34:58' // EXIF date format
|
||||
const meta = new Metadata(exiftool)
|
||||
should(meta.date).eql(fixtures.date('2016-10-28 17:34:58').getTime())
|
||||
})
|
||||
|
||||
it('infers the date from the filename (Android format)', function () {
|
||||
const exiftool = fixtures.exiftool()
|
||||
exiftool.SourceFile = 'folder/VID_20170220_114006.mp4'
|
||||
const meta = new Metadata(exiftool)
|
||||
should(meta.date).eql(fixtures.date('2017-02-20 11:40:06').getTime())
|
||||
})
|
||||
|
||||
it('infers the date from the filename (Dropbox format)', function () {
|
||||
const exiftool = fixtures.exiftool()
|
||||
exiftool.SourceFile = 'folder/2017-03-24 19.42.30.jpg'
|
||||
const meta = new Metadata(exiftool)
|
||||
should(meta.date).eql(fixtures.date('2017-03-24 19:42:30').getTime())
|
||||
})
|
||||
|
||||
it('only infers dates from valid formats', function () {
|
||||
const exiftool = fixtures.exiftool()
|
||||
exiftool.SourceFile = 'folder/IMG_1234.jpg'
|
||||
exiftool.File.FileModifyDate = '2016:10:28 17:34:58'
|
||||
const meta = new Metadata(exiftool)
|
||||
should(meta.date).eql(fixtures.date('2016-10-28 17:34:58').getTime())
|
||||
})
|
||||
|
||||
it('does not look at the file name if it already has EXIF data', function () {
|
||||
const exiftool = fixtures.exiftool()
|
||||
exiftool.SourceFile = '2017-03-24 19.42.30.jpg'
|
||||
exiftool.EXIF.DateTimeOriginal = '2016:10:28 17:34:58'
|
||||
const meta = new Metadata(exiftool)
|
||||
should(meta.date).eql(fixtures.date('2016-10-28 17:34:58').getTime())
|
||||
})
|
||||
|
||||
it('defaults to the file date if there is no other date', function () {
|
||||
const exiftool = fixtures.exiftool()
|
||||
exiftool.File.FileModifyDate = '2016:10:28 17:34:58'
|
||||
const meta = new Metadata(exiftool)
|
||||
should(meta.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 exiftool = fixtures.exiftool()
|
||||
exiftool.File.MIMEType = 'image/jpeg'
|
||||
const meta = new Metadata(exiftool)
|
||||
should(meta.video).eql(false)
|
||||
should(meta.animated).eql(false)
|
||||
})
|
||||
|
||||
it('can tell if a file is a non-animated gif', function () {
|
||||
const exiftool = fixtures.exiftool()
|
||||
exiftool.File.MIMEType = 'image/gif'
|
||||
const meta = new Metadata(exiftool)
|
||||
should(meta.video).eql(false)
|
||||
should(meta.animated).eql(false)
|
||||
})
|
||||
|
||||
it('can tell if a file is an animated gif', function () {
|
||||
const exiftool = fixtures.exiftool()
|
||||
exiftool.File.MIMEType = 'image/gif'
|
||||
exiftool.GIF = {FrameCount: 10}
|
||||
const meta = new Metadata(exiftool)
|
||||
should(meta.video).eql(false)
|
||||
should(meta.animated).eql(true)
|
||||
})
|
||||
|
||||
it('can tell if a file is a video', function () {
|
||||
const exiftool = fixtures.exiftool()
|
||||
exiftool.File.MIMEType = 'video/mp4'
|
||||
const meta = new Metadata(exiftool)
|
||||
should(meta.video).eql(true)
|
||||
should(meta.animated).eql(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('caption', function () {
|
||||
it('is read from all standard EXIF/IPTC/XMP tags', function () {
|
||||
const tags = [
|
||||
{ type: 'EXIF', tag: 'ImageDescription' },
|
||||
{ type: 'IPTC', tag: 'Caption-Abstract' },
|
||||
{ type: 'IPTC', tag: 'Headline' },
|
||||
{ type: 'XMP', tag: 'Description' },
|
||||
{ type: 'XMP', tag: 'Title' },
|
||||
{ type: 'XMP', tag: 'Label' }
|
||||
]
|
||||
tags.forEach(t => {
|
||||
const exiftool = fixtures.exiftool()
|
||||
exiftool[t.type][t.tag] = 'some caption'
|
||||
const meta = new Metadata(exiftool)
|
||||
should(meta.caption).eql('some caption')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('keywords', function () {
|
||||
it('defaults to an empty array', function () {
|
||||
const exiftool = fixtures.exiftool()
|
||||
const meta = new Metadata(exiftool)
|
||||
should(meta.keywords).eql([])
|
||||
})
|
||||
|
||||
it('can read IPTC keywords', function () {
|
||||
const exiftool = fixtures.exiftool()
|
||||
exiftool.IPTC['Keywords'] = 'beach,sunset'
|
||||
const meta = new Metadata(exiftool)
|
||||
should(meta.keywords).eql(['beach', 'sunset'])
|
||||
})
|
||||
|
||||
xit('can read Picasa keywords', function () {
|
||||
const exiftool = fixtures.exiftool()
|
||||
const picasa = {keywords: 'beach,sunset'}
|
||||
const meta = new Metadata(exiftool, picasa)
|
||||
should(meta.keywords).eql(['beach', 'sunset'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('rating', function () {
|
||||
it('defaults to a rating of 0', function () {
|
||||
const exiftool = fixtures.exiftool()
|
||||
const meta = new Metadata(exiftool)
|
||||
should(meta.rating).eql(0)
|
||||
})
|
||||
|
||||
it('reads the rating from the XMP tags', function () {
|
||||
const exiftool = fixtures.exiftool()
|
||||
exiftool.XMP['Rating'] = 3
|
||||
const meta = new Metadata(exiftool)
|
||||
should(meta.rating).eql(3)
|
||||
})
|
||||
})
|
||||
|
||||
xdescribe('favourite', function () {
|
||||
it('defaults to false', function () {
|
||||
const exiftool = fixtures.exiftool()
|
||||
const meta = new Metadata(exiftool)
|
||||
should(meta.favourite).eql(false)
|
||||
})
|
||||
|
||||
it('understands the Picasa <star> feature', function () {
|
||||
const exiftool = fixtures.exiftool()
|
||||
const picasa = {star: 'yes'}
|
||||
const meta = new Metadata(exiftool, picasa)
|
||||
should(meta.favourite).eql(true)
|
||||
})
|
||||
})
|
||||
})
|
@ -1,5 +1,5 @@
|
||||
const should = require('should/as-function')
|
||||
const output = require('../../src/input/output')
|
||||
const output = require('../../src/model/output')
|
||||
|
||||
describe('Output paths', function () {
|
||||
describe('Images', function () {
|
Loading…
Reference in New Issue