mirror of https://github.com/thumbsup/thumbsup
feat(albums): --albums-from can be a list of patterns with special keywords
- %path expands to the path of the photo/video - %keywords expands to the IPTC keywords of the photo - {format} expands to the photo's EXIF date, e.g. {YYYY MM}pull/93/head
parent
23f19566d0
commit
286dc8d15f
@ -0,0 +1,41 @@
|
||||
/*
|
||||
--------------------------------------------------------------------------------
|
||||
Returns all albums that a file should belong to,
|
||||
based on the --albums-from array of patterns provided
|
||||
--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
const _ = require('lodash')
|
||||
const albumPattern = require('./album-pattern')
|
||||
|
||||
class AlbumMapper {
|
||||
constructor (opts) {
|
||||
const patterns = opts.albumsFrom || '%path'
|
||||
this.patterns = patterns.map(pattern => load(pattern, opts.albumsDateFormat))
|
||||
}
|
||||
getAlbums (file) {
|
||||
return _.flatMap(this.patterns, pattern => pattern(file))
|
||||
}
|
||||
}
|
||||
|
||||
function load (pattern, dateFormat) {
|
||||
// legacy short-hand names (deprecated)
|
||||
if (pattern === 'folders') {
|
||||
return albumPattern.create('%path')
|
||||
}
|
||||
if (pattern === 'date') {
|
||||
return albumPattern.create(`{${dateFormat}}`)
|
||||
}
|
||||
// custom mapper file
|
||||
if (typeof pattern === 'string' && pattern.startsWith('file://')) {
|
||||
const filepath = pattern.slice('file://'.length)
|
||||
return require(filepath)
|
||||
}
|
||||
if (typeof pattern === 'string') {
|
||||
return albumPattern.create(pattern)
|
||||
}
|
||||
// already a function
|
||||
return pattern
|
||||
}
|
||||
|
||||
module.exports = AlbumMapper
|
@ -0,0 +1,51 @@
|
||||
/*
|
||||
--------------------------------------------------------------------------------
|
||||
Returns a list of album names for every 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')
|
||||
|
||||
const TOKEN_REGEX = /%[a-z]+/g
|
||||
const DATE_REGEX = /{[^}]+}/g
|
||||
|
||||
const TOKEN_FUNC = {
|
||||
'%path': file => path.dirname(file.path)
|
||||
}
|
||||
|
||||
exports.create = pattern => {
|
||||
const cache = {
|
||||
usesTokens: TOKEN_REGEX.test(pattern),
|
||||
usesDates: DATE_REGEX.test(pattern),
|
||||
usesKeywords: pattern.indexOf('%keywords') > -1
|
||||
}
|
||||
// return a standard mapper function (file => album names)
|
||||
return file => {
|
||||
var album = pattern
|
||||
// replace known tokens
|
||||
if (cache.usesTokens) {
|
||||
album = album.replace(TOKEN_REGEX, token => replaceToken(file, token))
|
||||
}
|
||||
if (cache.usesDates) {
|
||||
album = album.replace(DATE_REGEX, format => replaceDate(file, format))
|
||||
}
|
||||
// create one album per keyword if required
|
||||
if (cache.usesKeywords) {
|
||||
return file.meta.keywords.map(k => album.replace('%keywords', k))
|
||||
} else {
|
||||
return [album]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function replaceToken (file, token) {
|
||||
const fn = TOKEN_FUNC[token]
|
||||
return fn ? fn(file) : token
|
||||
}
|
||||
|
||||
function replaceDate (file, format) {
|
||||
const fmt = format.slice(1, -1)
|
||||
return moment(file.meta.date).format(fmt)
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
/*
|
||||
--------------------------------------------------------------------------------
|
||||
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
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
const Picasa = require('../input/picasa')
|
||||
const mapper = require('../input/mapper')
|
||||
const hierarchy = require('../input/hierarchy.js')
|
||||
const File = require('../model/file')
|
||||
const Metadata = require('../model/metadata')
|
||||
|
||||
exports.run = function (database, opts, callback) {
|
||||
const picasaReader = new Picasa()
|
||||
// create a flat array of files
|
||||
const files = database.map(entry => {
|
||||
// create standarised metadata model
|
||||
const picasa = picasaReader.file(entry.SourceFile)
|
||||
const meta = new Metadata(entry, picasa || {})
|
||||
// create a file entry for the albums
|
||||
return new File(entry, meta, opts)
|
||||
})
|
||||
// create the full album hierarchy
|
||||
const albumMapper = mapper.create(opts)
|
||||
const album = hierarchy.createAlbums(files, albumMapper, opts)
|
||||
// return the results
|
||||
return {files, album}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
const should = require('should/as-function')
|
||||
const AlbumMapper = require('../../src/input/album-mapper.js')
|
||||
const fixtures = require('../fixtures.js')
|
||||
|
||||
const TEST_FILE = fixtures.photo({
|
||||
path: 'Holidays/IMG_0001.jpg',
|
||||
date: '2016:07:14 12:07:41'
|
||||
})
|
||||
|
||||
describe('Album mapper', function () {
|
||||
it('can use a single string pattern', function () {
|
||||
const mapper = new AlbumMapper({
|
||||
albumsFrom: ['%path']
|
||||
})
|
||||
should(mapper.getAlbums(TEST_FILE)).eql(['Holidays'])
|
||||
})
|
||||
it('can use a single function (for testing)', function () {
|
||||
const custom = file => 'hello'
|
||||
const mapper = new AlbumMapper({
|
||||
albumsFrom: [custom]
|
||||
})
|
||||
should(mapper.getAlbums(TEST_FILE)).eql(['hello'])
|
||||
})
|
||||
it('can provide multiple string patterns', function () {
|
||||
const mapper = new AlbumMapper({
|
||||
albumsFrom: ['%path', '{YYYY}']
|
||||
})
|
||||
should(mapper.getAlbums(TEST_FILE)).eql(['Holidays', '2016'])
|
||||
})
|
||||
it('merges all albums into a single array', function () {
|
||||
const custom1 = file => ['one']
|
||||
const custom2 = file => ['two', 'three']
|
||||
const mapper = new AlbumMapper({
|
||||
albumsFrom: [custom1, custom2]
|
||||
})
|
||||
should(mapper.getAlbums(TEST_FILE)).eql(['one', 'two', 'three'])
|
||||
})
|
||||
describe('deprecated options', () => {
|
||||
it('can use <folders> to mean %path', () => {
|
||||
const mapper = new AlbumMapper({
|
||||
albumsFrom: ['folders']
|
||||
})
|
||||
should(mapper.getAlbums(TEST_FILE)).eql(['Holidays'])
|
||||
})
|
||||
it('can use <date> to mean {date}', () => {
|
||||
const mapper = new AlbumMapper({
|
||||
albumsFrom: ['date'],
|
||||
albumsDateFormat: 'YYYY MM'
|
||||
})
|
||||
should(mapper.getAlbums(TEST_FILE)).eql(['2016 07'])
|
||||
})
|
||||
})
|
||||
})
|
@ -0,0 +1,98 @@
|
||||
const should = require('should/as-function')
|
||||
const pattern = require('../../src/input/album-pattern.js')
|
||||
const fixtures = require('../fixtures.js')
|
||||
|
||||
describe('AlbumPattern', function () {
|
||||
describe('text', () => {
|
||||
it('can return a plain text album name', function () {
|
||||
const func = pattern.create('Holidays/Canada')
|
||||
const file = fixtures.photo()
|
||||
should(func(file)).eql(['Holidays/Canada'])
|
||||
})
|
||||
it('can have extra text around keywords', function () {
|
||||
const func = pattern.create('Holidays/%path')
|
||||
const file = fixtures.photo({
|
||||
path: 'Canada/IMG_0001.jpg'
|
||||
})
|
||||
should(func(file)).eql(['Holidays/Canada'])
|
||||
})
|
||||
})
|
||||
describe('path', () => {
|
||||
it('%path returns the relative path of the photo', function () {
|
||||
const func = pattern.create('%path')
|
||||
const file = fixtures.photo({
|
||||
path: 'Holidays/IMG_0001.jpg'
|
||||
})
|
||||
should(func(file)).eql(['Holidays'])
|
||||
})
|
||||
it('%path includes all subfolders', function () {
|
||||
const func = pattern.create('%path')
|
||||
const file = fixtures.photo({
|
||||
path: 'Holidays/Canada/IMG_0001.jpg'
|
||||
})
|
||||
should(func(file)).eql(['Holidays/Canada'])
|
||||
})
|
||||
})
|
||||
describe('creation date', () => {
|
||||
it('can use a moment.js format: {YYYY MM}', function () {
|
||||
const func = pattern.create('{YYYY MM}')
|
||||
const file = fixtures.photo({
|
||||
date: '2016:07:14 12:07:41'
|
||||
})
|
||||
should(func(file)).eql(['2016 07'])
|
||||
})
|
||||
it('can include slashes in the format: {YYYY/MM}', function () {
|
||||
const func = pattern.create('{YYYY/MM}')
|
||||
const file = fixtures.photo({
|
||||
date: '2016:07:14 12:07:41'
|
||||
})
|
||||
should(func(file)).eql(['2016/07'])
|
||||
})
|
||||
it('can have multiple dates in the same pattern: {YYYY}/{MM}', function () {
|
||||
const func = pattern.create('{YYYY}/{MM}')
|
||||
const file = fixtures.photo({
|
||||
date: '2016:07:14 12:07:41'
|
||||
})
|
||||
should(func(file)).eql(['2016/07'])
|
||||
})
|
||||
})
|
||||
describe('keywords', () => {
|
||||
it('can return a single keyword', () => {
|
||||
const func = pattern.create('%keywords')
|
||||
const file = fixtures.photo({
|
||||
keywords: ['beach']
|
||||
})
|
||||
should(func(file)).eql(['beach'])
|
||||
})
|
||||
it('can return multiple keyword', () => {
|
||||
const func = pattern.create('%keywords')
|
||||
const file = fixtures.photo({
|
||||
keywords: ['beach', 'sunset']
|
||||
})
|
||||
should(func(file)).eql(['beach', 'sunset'])
|
||||
})
|
||||
it('can use plain text around the keywords', () => {
|
||||
const func = pattern.create('Tags/%keywords')
|
||||
const file = fixtures.photo({
|
||||
keywords: ['beach', 'sunset']
|
||||
})
|
||||
should(func(file)).eql(['Tags/beach', 'Tags/sunset'])
|
||||
})
|
||||
it('does not return any albums if the photo does not have keywords', () => {
|
||||
const func = pattern.create('{YYYY}/tags/%keywords')
|
||||
const file = fixtures.photo()
|
||||
should(func(file)).eql([])
|
||||
})
|
||||
})
|
||||
describe('Complex patterns', () => {
|
||||
it('can mix several tokens inside a complex pattern', () => {
|
||||
const func = pattern.create('{YYYY}/%path/%keywords')
|
||||
const file = fixtures.photo({
|
||||
path: 'Holidays/IMG_0001.jpg',
|
||||
date: '2016:07:14 12:07:41',
|
||||
keywords: ['beach', 'sunset']
|
||||
})
|
||||
should(func(file)).eql(['2016/Holidays/beach', '2016/Holidays/sunset'])
|
||||
})
|
||||
})
|
||||
})
|
@ -1,32 +0,0 @@
|
||||
var should = require('should/as-function')
|
||||
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 entry = fixtures.photo({
|
||||
path: 'holidays/canada/IMG_0001.jpg'
|
||||
})
|
||||
should(map(entry)).eql('holidays/canada')
|
||||
})
|
||||
it('can create a default date mapper', function () {
|
||||
const map = mapper.create({albumsFrom: 'date'})
|
||||
const entry = fixtures.photo({
|
||||
path: 'holidays/canada/IMG_0001.jpg',
|
||||
date: '2016:07:14 12:07:41'
|
||||
})
|
||||
should(map(entry)).eql('2016 July')
|
||||
})
|
||||
it('can create a custom date mapper', function () {
|
||||
const map = mapper.create({
|
||||
albumsFrom: 'date',
|
||||
albumsDateFormat: 'YYYY/MM'
|
||||
})
|
||||
const entry = fixtures.photo({
|
||||
path: 'holidays/canada/IMG_0001.jpg',
|
||||
date: '2016:07:14 12:07:41'
|
||||
})
|
||||
should(map(entry)).eql('2016/07')
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue