Snippets

VirtualWolf getPhotoMetadata() before and after

Created by VirtualWolf last modified
// 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],
        },
    };
}

Comments (0)