feat(media): support suffix based generated paths instead of folder groups

Fixes #32, Fixes #111
pull/143/head
Romain 5 years ago
parent f9c7bcbc7e
commit 220b2137ec

@ -135,6 +135,12 @@ const OPTIONS = {
type: 'number',
'default': os.cpus().length
},
'output-structure': {
group: 'Output options:',
description: 'File and folder structure for output media',
choices: ['folders', 'suffix'],
'default': 'folders'
},
'gm-args': {
group: 'Output options:',
description: 'Custom image processing arguments for GraphicsMagick',
@ -404,6 +410,7 @@ exports.get = (args) => {
log: opts['log'],
dryRun: opts['dry-run'],
concurrency: opts['concurrency'],
outputStructure: opts['output-structure'],
gmArgs: opts['gm-args'],
watermark: opts['watermark'],
watermarkPosition: opts['watermark-position'],

@ -1,8 +1,5 @@
const warn = require('debug')('thumbsup:warn')
const path = require('path')
const urljoin = require('url-join')
const BROWSER_SUPPORTED_EXT = /\.(jpg|jpeg|png|gif)$/i
const structure = require('./structure')
exports.paths = function (filepath, mediaType, opts) {
if (mediaType === 'image') {
@ -17,7 +14,7 @@ exports.paths = function (filepath, mediaType, opts) {
function image (filepath, opts) {
return {
thumbnail: relationship(filepath, 'photo:thumbnail'),
thumbnail: relationship(filepath, 'photo:thumbnail', opts),
large: relationship(filepath, shortRel('image', opts.photoPreview), opts),
download: relationship(filepath, shortRel('image', opts.photoDownload), opts)
}
@ -25,8 +22,8 @@ function image (filepath, opts) {
function video (filepath, opts) {
return {
thumbnail: relationship(filepath, 'video:thumbnail'),
large: relationship(filepath, 'video:poster'),
thumbnail: relationship(filepath, 'video:thumbnail', opts),
large: relationship(filepath, 'video:poster', opts),
video: relationship(filepath, shortRel('video', opts.videoPreview), opts),
download: relationship(filepath, shortRel('video', opts.videoDownload), opts)
}
@ -39,44 +36,17 @@ function shortRel (mediaType, shorthand) {
case 'copy': return 'fs:copy'
case 'symlink': return 'fs:symlink'
case 'link': return 'fs:link'
default: return null
default: throw new Error(`Invalid relationship: ${shorthand}`)
}
}
function relationship (filepath, rel, options) {
function relationship (filepath, rel, opts) {
const fn = structure[opts.outputStructure || 'folders']
if (!fn) {
throw new Error(`Invalid output structure: ${opts.outputStructure}`)
}
return {
path: pathForRelationship(filepath, rel, options),
path: fn(filepath, rel, opts),
rel: rel
}
}
function pathForRelationship (filepath, rel, options) {
switch (rel) {
case 'photo:thumbnail': return 'media/thumbs/' + supportedPhotoFilename(filepath)
case 'photo:large': return 'media/large/' + supportedPhotoFilename(filepath)
case 'video:thumbnail': return 'media/thumbs/' + ext(filepath, 'jpg')
case 'video:poster': return 'media/large/' + ext(filepath, 'jpg')
case 'video:resized': return 'media/large/' + ext(filepath, options.videoFormat)
case 'fs:copy': return path.join('media', 'original', filepath)
case 'fs:symlink': return path.join('media', 'original', filepath)
case 'fs:link': return join(options.linkPrefix, filepath)
default: return null
}
}
function supportedPhotoFilename (filepath) {
const extension = path.extname(filepath)
return extension.match(BROWSER_SUPPORTED_EXT) ? filepath : ext(filepath, 'jpg')
}
function ext (file, ext) {
return file.replace(/\.[a-z0-9]+$/i, '.' + ext)
}
function join (prefix, filepath) {
if (prefix.match(/^https?:\/\//)) {
return urljoin(prefix, filepath)
} else {
return path.join(prefix, filepath)
}
}

@ -0,0 +1,55 @@
const path = require('path')
const urljoin = require('url-join')
const BROWSER_SUPPORTED_EXT = /(jpg|jpeg|png|gif)$/i
exports.folders = function (filepath, rel, options = {}) {
const dir = path.dirname(filepath)
const name = path.basename(filepath, path.extname(filepath))
const ext = path.extname(filepath).substr(1)
const photoExt = photoExtension(filepath)
const videoExt = options.videoFormat || 'mp4'
switch (rel) {
case 'photo:thumbnail': return `media/thumbs/${dir}/${name}.${photoExt}`
case 'photo:large': return `media/large/${dir}/${name}.${photoExt}`
case 'video:thumbnail': return `media/thumbs/${dir}/${name}.jpg`
case 'video:poster': return `media/large/${dir}/${name}.jpg`
case 'video:resized': return `media/large/${dir}/${name}.${videoExt}`
case 'fs:copy': return `media/original/${dir}/${name}.${ext}`
case 'fs:symlink': return `media/original/${dir}/${name}.${ext}`
case 'fs:link': return join(options.linkPrefix, filepath)
default: throw new Error(`Invalid relationship: ${rel}`)
}
}
exports.suffix = function (filepath, rel, options = {}) {
const dir = path.dirname(filepath)
const name = path.basename(filepath, path.extname(filepath))
const ext = path.extname(filepath).substr(1)
const photoExt = photoExtension(filepath)
const videoExt = options.videoFormat || 'mp4'
switch (rel) {
case 'photo:thumbnail': return `media/${dir}/${name}_${ext}_thumb.${photoExt}`
case 'photo:large': return `media/${dir}/${name}_${ext}_large.${photoExt}`
case 'video:thumbnail': return `media/${dir}/${name}_${ext}_thumb.jpg`
case 'video:poster': return `media/${dir}/${name}_${ext}_poster.jpg`
case 'video:resized': return `media/${dir}/${name}_${ext}_large.${videoExt}`
case 'fs:copy': return `media/${dir}/${name}.${ext}`
case 'fs:symlink': return `media/${dir}/${name}.${ext}`
case 'fs:link': return join(options.linkPrefix, filepath)
default: throw new Error(`Invalid relationship: ${rel}`)
}
}
function photoExtension (filepath) {
const extension = path.extname(filepath).substr(1)
return extension.match(BROWSER_SUPPORTED_EXT) ? extension : 'jpg'
}
function join (prefix, filepath) {
if (prefix.match(/^https?:\/\//)) {
return urljoin(prefix, filepath)
} else {
return path.join(prefix, filepath)
}
}

@ -231,4 +231,42 @@ describe('Output paths', function () {
})
})
})
describe('Output structure', function () {
it('defaults to the <folders> structure', function () {
const o = output.paths('holidays/beach.jpg', 'image', {})
should(o.download).eql({
path: 'media/large/holidays/beach.jpg',
rel: 'photo:large'
})
})
it('can explicitely choose the <folders> structure', function () {
const o = output.paths('holidays/beach.jpg', 'image', {
outputStructure: 'folders'
})
should(o.download).eql({
path: 'media/large/holidays/beach.jpg',
rel: 'photo:large'
})
})
it('can choose the <suffix> structure', function () {
const o = output.paths('holidays/beach.jpg', 'image', {
outputStructure: 'suffix'
})
should(o.download).eql({
path: 'media/holidays/beach_jpg_large.jpg',
rel: 'photo:large'
})
})
it('throws an error for invalid values', function () {
should.throws(function () {
output.paths('holidays/beach.jpg', 'image', {
outputStructure: 'unknown'
})
}, /Invalid output structure: unknown/)
})
})
})

@ -0,0 +1,131 @@
const _ = require('lodash')
const should = require('should/as-function')
const structure = require('../../src/model/structure')
const folders = structure.folders
const suffix = structure.suffix
describe('Structure', () => {
describe('folders', () => {
it('starts with <media>', () => {
should(folders('holidays/IMG_0001.jpg', 'photo:thumbnail')).startWith('media/')
should(folders('holidays/IMG_0001.jpg', 'photo:large')).startWith('media/')
should(folders('holidays/IMG_0001.mp4', 'video:thumbnail')).startWith('media/')
should(folders('holidays/IMG_0001.mp4', 'video:poster')).startWith('media/')
should(folders('holidays/IMG_0001.mp4', 'video:resized')).startWith('media/')
should(folders('holidays/IMG_0001.jpg', 'fs:copy')).startWith('media/')
should(folders('holidays/IMG_0001.jpg', 'fs:symlink')).startWith('media/')
})
it('adds thumbnails to a <thumbs> folder', () => {
should(folders('holidays/IMG_0001.jpg', 'photo:thumbnail')).startWith('media/thumbs/holidays/')
should(folders('holidays/IMG_0001.mp4', 'video:thumbnail')).startWith('media/thumbs/holidays/')
})
it('adds large versions to a <large> folder', () => {
should(folders('holidays/IMG_0001.jpg', 'photo:large')).startWith('media/large/holidays/')
should(folders('holidays/IMG_0001.mp4', 'video:poster')).startWith('media/large/holidays/')
should(folders('holidays/IMG_0001.mp4', 'video:resized')).startWith('media/large/holidays/')
})
it('adds copies and links to an <original> folder', () => {
should(folders('holidays/IMG_0001.jpg', 'fs:copy')).startWith('media/original/holidays/')
should(folders('holidays/IMG_0001.jpg', 'fs:symlink')).startWith('media/original/holidays/')
})
it('keeps the full original name for copies and links', () => {
should(folders('holidays/IMG_0001.jpg', 'fs:copy')).endWith('IMG_0001.jpg')
should(folders('holidays/IMG_0001.jpg', 'fs:symlink')).endWith('IMG_0001.jpg')
})
it('preserves the photo thumbnail extension if supported', () => {
// lower case
should(folders('holidays/IMG_0001.jpg', 'photo:thumbnail')).endWith('IMG_0001.jpg')
should(folders('holidays/IMG_0001.jpeg', 'photo:thumbnail')).endWith('IMG_0001.jpeg')
should(folders('holidays/IMG_0001.png', 'photo:thumbnail')).endWith('IMG_0001.png')
should(folders('holidays/IMG_0001.gif', 'photo:thumbnail')).endWith('IMG_0001.gif')
// upper case
should(folders('holidays/IMG_0001.JPG', 'photo:thumbnail')).endWith('IMG_0001.JPG')
should(folders('holidays/IMG_0001.JPEG', 'photo:thumbnail')).endWith('IMG_0001.JPEG')
should(folders('holidays/IMG_0001.PNG', 'photo:thumbnail')).endWith('IMG_0001.PNG')
should(folders('holidays/IMG_0001.GIF', 'photo:thumbnail')).endWith('IMG_0001.GIF')
})
it('changes the photo thumbnail extension to jpg if not supported', () => {
should(folders('holidays/IMG_0001.tiff', 'photo:thumbnail')).endWith('IMG_0001.jpg')
})
it('supports two different resized video extensions', () => {
should(folders('holidays/IMG_0001.mov', 'video:resized', { videoFormat: 'mp4' })).endWith('IMG_0001.mp4')
should(folders('holidays/IMG_0001.mov', 'video:resized', { videoFormat: 'webm' })).endWith('IMG_0001.webm')
})
it('always uses jpg for video thumbnails and posters', () => {
should(folders('holidays/IMG_0001.mp4', 'video:thumbnail')).endWith('IMG_0001.jpg')
should(folders('holidays/IMG_0001.mp4', 'video:poster')).endWith('IMG_0001.jpg')
})
it('can use a file system link', () => {
const res = folders('holidays/IMG_0001.jpg', 'fs:link', { linkPrefix: '../..' })
should(res).eql('../../holidays/IMG_0001.jpg')
})
it('can use a remote HTTP link', () => {
const res = folders('holidays/IMG_0001.jpg', 'fs:link', { linkPrefix: 'http://test.com' })
should(res).eql('http://test.com/holidays/IMG_0001.jpg')
})
})
describe('suffix', () => {
it('starts with <media>', () => {
should(suffix('holidays/IMG_0001.jpg', 'photo:thumbnail')).startWith('media/')
should(suffix('holidays/IMG_0001.jpg', 'photo:large')).startWith('media/')
should(suffix('holidays/IMG_0001.mp4', 'video:thumbnail')).startWith('media/')
should(suffix('holidays/IMG_0001.mp4', 'video:poster')).startWith('media/')
should(suffix('holidays/IMG_0001.mp4', 'video:resized')).startWith('media/')
should(suffix('holidays/IMG_0001.jpg', 'fs:copy')).startWith('media/')
should(suffix('holidays/IMG_0001.jpg', 'fs:symlink')).startWith('media/')
})
it('uses an _thumb suffix to denote thumbnails', () => {
should(suffix('holidays/IMG_0001.jpg', 'photo:thumbnail')).endWith('holidays/IMG_0001_jpg_thumb.jpg')
should(suffix('holidays/IMG_0001.mp4', 'video:thumbnail')).endWith('holidays/IMG_0001_mp4_thumb.jpg')
})
it('uses a _large suffix to resized versions', () => {
should(suffix('holidays/IMG_0001.jpg', 'photo:large')).endWith('holidays/IMG_0001_jpg_large.jpg')
should(suffix('holidays/IMG_0001.mp4', 'video:resized')).endWith('holidays/IMG_0001_mp4_large.mp4')
})
it('uses a _poster suffix for the video poster', () => {
should(suffix('holidays/IMG_0001.jpg', 'video:poster')).endWith('holidays/IMG_0001_jpg_poster.jpg')
})
it('uses the original filenames for copies and symlinks', () => {
should(suffix('holidays/IMG_0001.jpg', 'fs:copy')).endWith('holidays/IMG_0001.jpg')
should(suffix('holidays/IMG_0001.jpg', 'fs:symlink')).endWith('holidays/IMG_0001.jpg')
})
it('does not have conflicts between generated photo and video files', () => {
// photos
const photoRels = ['photo:thumbnail', 'photo:large', 'fs:copy']
const photos = photoRels.map(rel => suffix('holidays/IMG_0001.jpg', rel))
// videos
const videoRels = ['video:thumbnail', 'video:poster', 'video:resized', 'fs:copy']
const videos = videoRels.map(rel => suffix('holidays/IMG_0001.mp4', rel, { videoFormat: 'mp4' }))
// check
const all = _.union(photos, videos)
should(_.uniq(all).length).eql(all.length)
})
it('can use a file system link', () => {
const res = suffix('holidays/IMG_0001.jpg', 'fs:link', { linkPrefix: '../..' })
should(res).eql('../../holidays/IMG_0001.jpg')
})
it('can use a remote HTTP link', () => {
const res = suffix('holidays/IMG_0001.jpg', 'fs:link', { linkPrefix: 'http://test.com' })
should(res).eql('http://test.com/holidays/IMG_0001.jpg')
})
})
})
Loading…
Cancel
Save