Skip to content

Instantly share code, notes, and snippets.

@VirtualWolf
Last active Apr 11, 2020
Embed
What would you like to do?
getPhotoMetadata() before and after
// Original version using callbacks and the entire setup in a single giant function
getPhotoMetadata: (options, callback) => {
async.parallel({
exif: cb => {
jsonService.getJsonFromUrl(baseRestUrlService.getUrl('flickr')
.addQueryParam('method', 'flickr.photos.getExif')
.addQueryParam('photo_id', options.photoId),
(err, data) => {
if (err) { return cb(err, null); }
if (data.stat === 'fail') { return cb(data.message, null); }
return cb(null, data);
});
},
details: cb => {
jsonService.getJsonFromUrl(baseRestUrlService.getUrl('flickr')
.addQueryParam('method', 'flickr.photos.getInfo')
.addQueryParam('photo_id', options.photoId),
(err, data) => {
if (err) { return cb(err, null); }
if (data.stat === 'fail') { return cb(data.message, null); }
return cb(null, data);
});
},
favourites: cb => {
jsonService.getJsonFromUrl(baseRestUrlService.getUrl('flickr')
.addQueryParam('method', 'flickr.photos.getFavorites')
.addQueryParam('photo_id', options.photoId),
(err, data) => {
if (err) { return cb(err); }
if (data.stat === 'fail') { return cb(data.message); }
return cb(null, data);
});
},
photoset: cb => {
if (options.type === 'photosets') {
jsonService.getJsonFromUrl(baseRestUrlService.getUrl('flickr')
.addQueryParam('method', 'flickr.photosets.getPhotos')
.addQueryParam('photoset_id', options.photosetId)
.addQueryParam('per_page', '500'),
(err, data) => {
if (err) { return cb(err, null); }
if (data.stat === 'fail') { return cb(data.message, null); }
return cb(null, data);
});
} else {
return cb();
}
},
},
(err, results) => {
if (err) { return callback(err, null); }
return callback(null, {
exif: {
camera: results.exif.photo.exif.filter(
item => {
return item.label.match(/^Model$/);
})
.reduce((previous, current) => { return current.raw._content; }, null),
focalLength: results.exif.photo.exif.filter(
item => {
return item.label.match(/^Focal Length$/);
})
.reduce((previous, current) => { return current.clean ? current.clean._content : null; }, null),
aperture: results.exif.photo.exif.filter(item => {
return item.label.match(/^Aperture$/);
})
.reduce((previous, current) => { return current.clean ? current.clean._content : null; }, null),
shutterSpeed: results.exif.photo.exif.filter(
item => {
return item.label.match(/^Exposure$/);
})
.reduce((previous, current) => {
// Exposures of 1 second or higher have only the raw number of seconds, there's no "clean" version
return current.clean
? current.clean._content
: `${current.raw._content} sec`;
}, null),
iso: results.exif.photo.exif.filter(
item => {
return item.label.match(/^ISO Speed$/);
})
.reduce((previous, current) => { return current.raw._content; }, null),
lens: results.details.photo.tags.tag.filter(
tag => {
return tag.raw.match(/(\d+)mm(.*)/g);
})
.map(tag => {
return tag.raw.replace(/\sUSM/, '');
})
.reduce((previous, current) => { return current; }, null),
dateTaken: results.details.photo.dates.taken
? timestampFormatService.generateTimestamp({value: results.details.photo.dates.taken, type: 'photos'})
: null,
},
title: results.details.photo.title._content
? results.details.photo.title._content
: 'Untitled',
description: results.details.photo.description._content
? results.details.photo.description._content.replace(/\n/, '<br />')
: null,
imageUrl: photosService.generateImageUrl({
farm: results.exif.photo.farm,
server: results.exif.photo.server,
photo_id: results.exif.photo.id,
secret: results.exif.photo.secret,
dateupload: results.details.photo.dates.posted,
}),
lightboxUrl: `https://www.flickr.com/photos/virtualwolf/${results.exif.photo.id}/lightbox`,
numComments: results.details.photo.comments._content,
numViews: results.details.photo.views,
numFavourites: results.favourites.photo.total,
photosetInfo: options.type === 'photosets'
? (function(photosetData, photoId) {
const photoIds = photosetData.photoset.photo.map(photo => { return photo.id; });
const currentImageIndex = photoIds.findIndex(i => i == photoId);
return {
title: photosetData.photoset.title,
photosInSet: {
currentPhoto: photoIds[currentImageIndex],
nextPhoto: photoIds[currentImageIndex + 1],
previousPhoto: photoIds[currentImageIndex - 1],
lastPhoto: photoIds[photoIds.length - 1],
},
};
})(results.photoset, results.exif.photo.id)
: null,
});
});
};
// Updated version using Promises, it's almost identical in terms of number of lines of code to above but
// the readability is significantly better thanks to being split into several smaller functions
getPhotoMetadata: options => {
const exif = jsonService.getJsonFromUrl(
photosService.buildFlickrApiUrl({
type: 'exifData',
photoId: options.photoId,
})
);
const details = jsonService.getJsonFromUrl(
photosService.buildFlickrApiUrl({
type: 'photoInfo',
photoId: options.photoId,
})
);
const favourites = jsonService.getJsonFromUrl(
photosService.buildFlickrApiUrl({
type: 'favouritesInfo',
photoId: options.photoId,
})
);
const photoset = options.type === 'photosets'
? jsonService.getJsonFromUrl(
photosService.buildFlickrApiUrl({
type: 'photosetPhotos',
photosetId: options.photosetId,
})
)
: null;
return Promise.all([
exif, details, favourites, photoset,
]).then(results => {
results.map(result => {
if (result && result.stat === 'fail') { throw new Error(result.message); }
});
const exif = results[0];
const details = results[1];
const favourites = results[2];
const photoset = results[3];
return {
exif: photosService.generateExifData({exif: exif, details: details}),
title: details.photo.title._content
? details.photo.title._content
: 'Untitled',
description: details.photo.description._content
? details.photo.description._content.replace(/\n/, '<br />')
: null,
imageUrl: photosService.generateImageUrl({
farm: details.photo.farm,
server: details.photo.server,
photo_id: details.photo.id,
secret: details.photo.secret,
dateupload: details.photo.dates.posted,
}),
lightboxUrl: `https://www.flickr.com/photos/virtualwolf/${details.photo.id}/lightbox`,
numComments: details.photo.comments._content,
numViews: details.photo.views,
numFavourites: favourites.photo.total,
photosetInfo: options.type === 'photosets'
? photosService.generatePhotosetInfo({photosetDetails: photoset.photoset, photoId: details.photo.id})
: null,
};
}).catch(err => {
throw err;
});
};
generateExifData: data => {
const camera = photosService.getRawExifName({exifData: data.exif.photo, exifType: 'Model'});
const focalLength = photosService.getCleanExifName({exifData: data.exif.photo, exifType: 'Focal Length'});
const aperture = photosService.getCleanExifName({exifData: data.exif.photo, exifType: 'Aperture'});
const iso = photosService.getRawExifName({exifData: data.exif.photo, exifType: 'ISO Speed'});
// Exposures of 1 second or higher have only the raw number of seconds, there's no "clean" version, so
// the shutter speed might be clean _or_ raw.
const shutterSpeed = data.exif.photo.exif.filter(item => {
return item.label.match(/^Exposure$/);
}).reduce((previous, current) => {
return current.clean
? current.clean._content
: `${current.raw._content} sec`;
}, null);
const lens = data.details.photo.tags.tag.filter(tag => {
return tag.raw.match(/(\d+)mm(.*)/g);
}).map(tag => {
return tag.raw.replace(/\sUSM/, '');
}).reduce((previous, current) => { return current; }, null);
const dateTaken = data.details.photo.dates.taken
? timestampFormatService.generateTimestamp({value: data.details.photo.dates.taken, type: 'photos'})
: null;
return {
camera: camera,
focalLength: focalLength,
aperture: aperture,
shutterSpeed: shutterSpeed,
iso: iso,
lens: lens,
dateTaken: dateTaken,
};
};
getCleanExifName: options => {
const regex = new RegExp(`^${options.exifType}$`);
return options.exifData.exif.filter(item => {
return item.label.match(regex);
}).filter(item => {
return item.clean;
}).reduce((previous, current) => {
return current.clean._content;
}, null);
};
getRawExifName: options => {
const regex = new RegExp(`^${options.exifType}$`);
return options.exifData.exif.filter(item => {
return item.label.match(regex);
}).filter(item => {
return item.raw;
}).reduce((previous, current) => {
return current.raw._content;
}, null);
};
// Further simplifying and tidying, with more code being split out into separate functions
getPhotoMetadata: ({type, page, photoId, photosetId}) => {
const exif = jsonService.getJsonFromUrl(apiUrlService.flickrExifDataUrl(photoId));
const details = jsonService.getJsonFromUrl(apiUrlService.flickrPhotoInfoUrl(photoId));
const favourites = jsonService.getJsonFromUrl(apiUrlService.flickrFavouritesUrl(photoId));
const photoset = type === 'photosets'
? jsonService.getJsonFromUrl(apiUrlService.flickrPhotosetPhotosUrl(photosetId))
: null;
return Promise.all([exif, details, favourites, photoset])
.then(results => results.map(result => checkForError(result)))
.then(results => {
const [exif, details, favourites, photoset] = results;
const exifDetails = generateExifData({exif, details});
const title = details.photo.title._content
? details.photo.title._content
: 'Untitled';
const description = details.photo.description._content
? details.photo.description._content.replace(/\n/, '<br />')
: null;
const imageUrl = generateImageUrl({
farm: details.photo.farm,
server: details.photo.server,
photo_id: details.photo.id,
secret: details.photo.secret,
dateupload: details.photo.dates.posted, });
const lightboxUrl = `https://www.flickr.com/photos/virtualwolf/${details.photo.id}/lightbox`;
const numComments = details.photo.comments._content;
const numViews = details.photo.views;
const numFavourites = favourites.photo.total;
const photosetInfo = type === 'photosets'
? generatePhotosetInfo({photosetDetails: photoset.photoset, photoId: details.photo.id})
: null;
return {
exif: exifDetails,
title,
description,
imageUrl,
lightboxUrl,
numComments,
numViews,
numFavourites,
photosetInfo,
};
})
.catch(err => {
throw err;
});
}
function generateExifData({exif, details}) {
const camera = getRawExifName({data: exif.photo.exif, type: 'Model'});
const iso = getRawExifName({data: exif.photo.exif, type: 'ISO Speed'});
const focalLength = getCleanExifName({data: exif.photo.exif, type: 'Focal Length'});
const aperture = getCleanExifName({data: exif.photo.exif, type: 'Aperture'});
const shutterSpeed = getShutterSpeed(exif.photo.exif);
const lens = getLensName(details.photo.tags.tag);
const dateTaken = details.photo.dates.taken
? timestampFormatService.flickrDatetime(details.photo.dates.taken)
: null;
return {
camera,
focalLength,
aperture,
shutterSpeed,
iso,
lens,
dateTaken,
};
}
function getCleanExifName({data, type}) {
return data
.filter(item => item.label.match(`^${type}$`))
.filter(item => item.clean)
.reduce((previous, current) => current.clean._content, null);
}
function getRawExifName({data, type}) {
return data
.filter(item => item.label.match(`^${type}$`))
.filter(item => item.raw)
.reduce((previous, current) => current.raw._content, null);
}
function getShutterSpeed(exif) {
// Exposures of 1 second or higher have only the raw number of seconds, there's no "clean" version, so
// the shutter speed might be clean _or_ raw.
return exif
.filter(item => item.label.match(/^Exposure$/))
.reduce((previous, current) => current.clean
? current.clean._content
: `${current.raw._content} sec`, null);
}
function getLensName(tags) {
return tags
.filter(tag => tag.raw.match(/(\d+)mm(.*)/g))
.map(tag => tag.raw.replace(/\sUSM/, ''))
.reduce((previous, current) => current, null);
}
function generatePhotosetInfo({photosetDetails, photoId}) {
const photoIds = photosetDetails.photo.map(photo => { return photo.id; });
const currentImageIndex = photoIds.findIndex(i => i === photoId);
return {
title: photosetDetails.title,
photosInSet: {
currentPhoto: photoIds[currentImageIndex],
nextPhoto: photoIds[currentImageIndex + 1],
previousPhoto: photoIds[currentImageIndex - 1],
lastPhoto: photoIds[photoIds.length - 1],
},
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment