Single source of truth for input/outputs + view model for display (#58)

pull/60/head
Romain 7 years ago committed by GitHub
parent 631aee6a4d
commit e719bf15b1

@ -5,12 +5,12 @@ const pad = require('pad')
const path = require('path')
const database = require('./input/database')
const File = require('./input/file')
const MediaFile = require('./model/file')
const Media = require('./model/media')
const hierarchy = require('./model/hierarchy.js')
const thumbs = require('./output-media/thumbs')
const website = require('./output-website/website')
exports.build = function(opts) {
exports.build = function (opts) {
thumbs.sizes.thumb = opts.thumbSize;
thumbs.sizes.large = opts.largeSize;
@ -25,7 +25,7 @@ exports.build = function(opts) {
var album = null // root album with nested albums
var collection = null // all files in the database
function buildStep(options) {
function buildStep (options) {
return function(callback) {
if (options.condition !== false) {
make.exec(opts.input, media, collection, options, callback);
@ -35,7 +35,7 @@ exports.build = function(opts) {
}
}
function callbackStep(name, fn) {
function callbackStep (name, fn) {
return function(next) {
process.stdout.write(pad(name, 20));
fn(function(err) {
@ -58,7 +58,7 @@ exports.build = function(opts) {
function updateDatabase(callback) {
database.update(opts.input, databaseFile, (err, dbFiles) => {
collection = dbFiles.map(f => new File(f))
collection = dbFiles.map(f => new File(f, opts))
callback(err)
})
},
@ -115,20 +115,20 @@ exports.build = function(opts) {
}),
callbackStep('Album hierarchy', function(next) {
const mediaCollection = collection.map(f => new MediaFile(f.path, f))
albums = hierarchy.createAlbums(mediaCollection, opts);
next();
const mediaCollection = collection.map(f => new Media(f))
albums = hierarchy.createAlbums(mediaCollection, opts)
next()
}),
callbackStep('Static website', function(next) {
website.build(albums, opts, next);
website.build(albums, opts, next)
})
], finish);
};
}
function finish(err) {
function finish (err) {
console.log();
console.log(err || 'Gallery generated successfully');
console.log();

@ -18,7 +18,7 @@ exports.update = function(media, databasePath, callback) {
emitter.on('stats', (stats) => {
debug(`Database stats: total=${stats.total}`)
const totalBar = progress.create('List media files', stats.total)
const totalBar = progress.create('Finding media files', stats.total)
totalBar.tick(stats.total)
updateBar = progress.create('Updating database', stats.added + stats.modified)
})

@ -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)
}

@ -19,7 +19,7 @@ var SORT_MEDIA_BY = {
var PREVIEW_MISSING = {
urls: {
thumb: 'public/missing.png'
thumbnail: 'public/missing.png'
}
};

@ -12,10 +12,10 @@ exports.albums = function(collection, opts) {
});
var groups = {};
// put all files in the right albums
collection.forEach(function(file) {
var groupName = moment(file.date).format(opts.grouping);
collection.forEach(function(media) {
var groupName = moment(media.date).format(opts.grouping);
createAlbumHierarchy(groups, groupName);
groups[groupName].files.push(file);
groups[groupName].files.push(media);
});
// only return top-level albums
var topLevel = _.keys(groups).filter(function(dir) {

@ -8,10 +8,10 @@ var Album = require('./album');
exports.albums = function(collection, opts) {
var albumsByFullPath = {};
// put all files in the right album
collection.forEach(function(file) {
var fullDir = path.dirname(file.filepath);
collection.forEach(function(media) {
var fullDir = path.dirname(media.file.path);
createAlbumHierarchy(albumsByFullPath, fullDir);
albumsByFullPath[fullDir].files.push(file);
albumsByFullPath[fullDir].files.push(media);
});
// only return top-level albums
var topLevel = _.keys(albumsByFullPath).filter(function(dir) {

@ -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

@ -96,16 +96,6 @@ exports.create = function(options) {
}
});
// utility helper
// render the correct download path based on user options
handlebars.registerHelper('download', function(file) {
if (file.isVideo) {
return options.originalVideos ? file.urls.original : file.urls.video;
} else {
return options.originalPhotos ? file.urls.original : file.urls.large;
}
});
// utility helper
// return the relative path from the current folder to the argument
var currentFolder = '.';

@ -40,31 +40,25 @@
{{#each album.files~}}
{{#if isVideo~}}
<li data-html="#media{{id}}"
data-poster="{{relative urls.poster}}"
data-download-url="{{{relative (download this)}}}">
<a href="{{{relative (download this)}}}">
<img src="{{relative urls.thumb}}"
data-poster="{{relative urls.large}}"
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}}"
data-html="#media{{id}}"
data-poster="{{relative urls.poster}}"
data-download-url="{{{relative (download this)}}}"
/>
alt="{{file.name}}" />
</a>
<img class="video-overlay" src="{{relative 'public/play.png'}}" />
</li>
{{~else~}}
<li data-src="{{relative urls.large}}"
data-sub-html="{{caption}}"
data-download-url="{{{relative (download this)}}}">
<a href="{{relative urls.large}}"
data-sub-html="{{caption}}"
data-download-url="{{{relative (download this)}}}">
<img src="{{relative urls.thumb}}"
data-download-url="{{relative urls.download}}">
<a href="{{relative urls.large}}">
<img src="{{relative urls.thumbnail}}"
width="{{@root.gallery.thumbSize}}"
height="{{@root.gallery.thumbSize}}"
alt="{{relative filename}}" />
alt="{{relative file.name}}" />
</a>
{{#if isAnimated}}
<img class="video-overlay" src="{{relative 'public/play.png'}}" />

@ -24,7 +24,7 @@
</div>
<ul class="grid">
{{~#slice previews count=4~}}
<li><img src="{{relative this.urls.thumb}}" /></li>
<li><img src="{{relative urls.thumbnail}}" /></li>
{{~/slice}}
</ul>
</a>
@ -36,25 +36,25 @@
{{#each album.files}}
{{#if isVideo~}}
<li data-html="#media{{id}}"
data-poster="{{relative urls.poster}}"
data-download-url="{{relative (download this)}}">
<a href="{{relative (download this)}}">
<img src="{{relative urls.thumb}}"
data-poster="{{relative urls.large}}"
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="{{name}}" />
alt="{{file.name}}" />
</a>
<img class="video-overlay" src="{{relative 'public/play.png'}}" />
</li>
{{else}}
<li data-src="{{relative urls.large}}"
data-sub-html="{{caption}}"
data-download-url="{{relative (download this)}}">
<a href="{{relative (download this)}}">
<img src="{{relative urls.thumb}}"
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="{{name}}" />
alt="{{file.name}}" />
</a>
{{#if isAnimated}}
<img class="video-overlay" src="{{relative 'public/play.png'}}" />

@ -39,7 +39,7 @@
</div>
<ul class="grid clearfix">
{{~#slice previews count=8~}}
<li><img src="{{relative this.urls.thumb}}" /></li>
<li><img src="{{relative this.urls.thumbnail}}" /></li>
{{~/slice}}
</ul>
</a>
@ -55,24 +55,24 @@
{{#if isVideo~}}
<li data-html="#media{{id}}"
data-poster="{{relative urls.poster}}"
data-download-url="{{{relative (download this)}}}">
<a href="{{{relative (download this)}}}">
<img src="{{relative urls.thumb}}"
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="{{relative filename}}" />
alt="{{relative file.name}}" />
</a>
<img class="video-overlay" src="{{relative 'public/play.png'}}" />
</li>
{{else}}
<li data-src="{{relative urls.large}}"
data-sub-html="{{caption}}"
data-download-url="{{{relative (download this)}}}">
<a href="{{{relative (download this)}}}">
<img src="{{relative urls.thumb}}"
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="{{relative filename}}" />
alt="{{relative file.name}}" />
</a>
{{#if isAnimated}}
<img class="video-overlay" src="{{relative 'public/play.png'}}" />

@ -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))
}

@ -4,7 +4,9 @@ const File = require('../../src/input/file')
describe('Input file', function () {
it('reads the relative file path', function () {
var file = new File(dbFile({SourceFile: 'holidays/beach.jpg'}))
var file = new File(dbFile({
SourceFile: 'holidays/beach.jpg'
}))
should(file.path).eql('holidays/beach.jpg')
})
@ -14,43 +16,25 @@ describe('Input file', function () {
FileModifyDate: '2017:01:27 14:38:29+05:00'
}
}))
should(file.fileDate).eql(1485509909000)
should(file.date).eql(1485509909000)
})
it('can guess the media type for photos', function () {
var file = new File(dbFile({
File: {MIMEType: 'image/jpeg'}
File: {
MIMEType: 'image/jpeg'
}
}))
should(file.mediaType).eql('image')
should(file.type).eql('image')
})
it('can guess the media type for videos', function () {
var file = new File(dbFile({
File: {MIMEType: 'video/quicktime'}
}))
should(file.mediaType).eql('video')
})
it('uses the EXIF caption if present', function () {
var file = new File(dbFile({
EXIF: {ImageDescription: 'some caption'}
}))
should(file.exif.caption).eql('some caption')
})
it('uses the IPTC caption if present', function () {
var file = new File(dbFile({
IPTC: {'Caption-Abstract': 'some caption'}
}))
should(file.exif.caption).eql('some caption')
})
it('uses the EXIF caption if both EXIF and IPTC exist', function () {
var file = new File(dbFile({
EXIF: {ImageDescription: 'exif caption'},
IPTC: {'Caption-Abstract': 'iptc caption'}
File: {
MIMEType: 'video/quicktime'
}
}))
should(file.exif.caption).eql('exif caption')
should(file.type).eql('video')
})
})

@ -36,8 +36,8 @@ describe('Album', function() {
]
});
a.finalize();
should(a.stats.fromDate).eql(fixtures.date('2016-09-02'));
should(a.stats.toDate).eql(fixtures.date('2016-10-21'));
should(a.stats.fromDate).eql(fixtures.date('2016-09-02').getTime());
should(a.stats.toDate).eql(fixtures.date('2016-10-21').getTime());
});
});
@ -91,8 +91,8 @@ describe('Album', function() {
]
});
a.finalize();
should(a.stats.fromDate).eql(fixtures.date('2016-09-02'));
should(a.stats.toDate).eql(fixtures.date('2016-10-21'));
should(a.stats.fromDate).eql(fixtures.date('2016-09-02').getTime());
should(a.stats.toDate).eql(fixtures.date('2016-10-21').getTime());
});
});

@ -114,8 +114,8 @@ describe('Album', function() {
fixtures.photo(), fixtures.photo(),
]});
a.finalize();
should(a.previews[2].urls.thumb).eql('public/missing.png');
should(a.previews[9].urls.thumb).eql('public/missing.png');
should(a.previews[2].urls.thumbnail).eql('public/missing.png');
should(a.previews[9].urls.thumbnail).eql('public/missing.png');
});
it('uses files from nested albums too', function() {
@ -135,7 +135,7 @@ describe('Album', function() {
a.finalize();
should(a.previews).have.length(10);
for (var i = 0; i < 4; ++i) {
should(a.previews[i].urls.thumb).not.eql('public/missing.png');
should(a.previews[i].urls.thumbnail).not.eql('public/missing.png');
}
});
@ -195,8 +195,8 @@ describe('Album', function() {
]});
var root = new Album({title: 'home', albums: [nested]});
root.finalize({sortMediaBy: 'filename'});
should(nested.files[0].filepath).eql('a');
should(nested.files[1].filepath).eql('b');
should(nested.files[0].file.path).eql('a');
should(nested.files[1].file.path).eql('b');
});
});
@ -238,8 +238,8 @@ describe('Album', function() {
]});
var root = new Album({title: 'home', albums: [nested]});
root.finalize({sortMediaBy: 'filename'});
should(nested.files[0].filepath).eql('a');
should(nested.files[1].filepath).eql('b');
should(nested.files[0].file.path).eql('a');
should(nested.files[1].file.path).eql('b');
});
});

@ -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…
Cancel
Save