/*
* Copyright (c) 2015-2017, Michael A. Updike All rights reserved.
* Licensed under the BSD-3-Clause
* https://opensource.org/licenses/BSD-3-Clause
* https://github.com/opus1269/photo-screen-saver/blob/master/LICENSE.md
*/
(function() {
'use strict';
window.app = window.app || {};
new ExceptionHandler();
/**
* A Google Photo Album
* @typedef {Object} app.GoogleSource.Album
* @property {int} index - Array index
* @property {string} uid - unique identifier
* @property {string} name - album name
* @property {string} id - Google album Id
* @property {string} thumb - thumbnail url
* @property {boolean} checked - is album selected
* @property {int} ct - number of photos
* @property {app.PhotoSource.Photo[]} photos - Array of photos
* @memberOf app.GoogleSource
*/
/**
* A Selected Google Photo Album
* @typedef {Object} app.GoogleSource.SelectedAlbum
* @property {string} id - Google album Id
* @property {app.PhotoSource.Photo[]} photos - Array of photos
* @memberOf app.GoogleSource
*/
/**
* Path to Picasa API
* @type {string}
* @const
* @default
* @private
* @memberOf app.GoogleSource
*/
const _URL_BASE = 'https://picasaweb.google.com/data/feed/api/user/';
/**
* Query for list of albums
* @type {string}
* @const
* @default
* @private
* @memberOf app.GoogleSource
*/
const _ALBUMS_QUERY = '?max-results=2000&access=all&kind=album' +
'&fields=entry(gphoto:albumType,gphoto:id)&v=2&alt=json';
/**
* Query an album for its photos
* @type {string}
* @const
* @default
* @private
* @memberOf app.GoogleSource
*/
const _ALBUM_QUERY = '&thumbsize=72' +
'&fields=title,gphoto:id,entry(media:group/media:content,' +
'media:group/media:credit,media:group/media:thumbnail,georss:where)' +
'&v=2&alt=json';
/**
* A potential source of photos from Google
* @alias app.GoogleSource
*/
app.GoogleSource = class extends app.PhotoSource {
/**
* Create a new photo source
* @param {string} useKey - The key for if the source is selected
* @param {string} photosKey - The key for the collection of photos
* @param {string} type - A descriptor of the photo source
* @param {string} desc - A human readable description of the source
* @param {boolean} isDaily - Should the source be updated daily
* @param {boolean} isArray - Is the source an Array of photo Arrays
* @param {?Object} [loadArg=null] - optional arg for load function
* @constructor
*/
constructor(useKey, photosKey, type, desc, isDaily, isArray,
loadArg = null) {
super(useKey, photosKey, type, desc, isDaily, isArray, loadArg);
}
/** Determine if a Picasa entry is an image
* @param {Object} entry - Picasa media object
* @returns {boolean} true if entry is a photo
* @private
*/
static _isImage(entry) {
const content = entry.media$group.media$content;
for (let i = 0; i < content.length; i++) {
if (content[i].medium !== 'image') {
return false;
}
}
return true;
}
/** Get max image size to retrieve
* @returns {string} image size description
* @private
*/
static _getMaxImageSize() {
let ret = '1600';
if (Chrome.Storage.getBool('fullResGoogle')) {
ret = 'd';
}
return ret;
}
/** Determine if a Picasa entry has Geo position
* @param {Object} entry - Picasa media object
* @returns {boolean} true if entry has Geo position
* @private
*/
static _hasGeo(entry) {
return !!(entry.georss$where &&
entry.georss$where.gml$Point &&
entry.georss$where.gml$Point.gml$pos &&
entry.georss$where.gml$Point.gml$pos.$t);
}
/** Get the thumbnail url if it exists
* @param {Array} entry - Picasa media object
* @returns {?string} url or null
* @private
*/
static _getThumbnail(entry) {
let ret = null;
if (entry.length &&
entry[0].media$group &&
entry[0].media$group.media$thumbnail[0]) {
ret = entry[0].media$group.media$thumbnail[0].url;
}
return ret;
}
/**
* Extract the Picasa photos into an Array
* @param {Object} root - root object from Picasa API call
* @returns {app.PhotoSource.Photo[]} Array of photos
* @private
*/
static _processPhotos(root) {
const photos = [];
if (root) {
const feed = root.feed;
const entries = feed.entry || [];
for (const entry of entries) {
if (app.GoogleSource._isImage(entry)) {
const url = entry.media$group.media$content[0].url;
const width = entry.media$group.media$content[0].width;
const height = entry.media$group.media$content[0].height;
const asp = width / height;
const author = entry.media$group.media$credit[0].$t;
let point;
if (app.GoogleSource._hasGeo(entry)) {
point = entry.georss$where.gml$Point.gml$pos.$t;
}
app.PhotoSource.addPhoto(photos, url, author, asp, {},
point);
}
}
}
return photos;
}
/**
* Retrieve a Google Photos album
* @param {string} albumId - Picasa album ID
* @param {string} [userId='default'] - userId for non-authenticated request
* @returns {Promise<Object>} Root object from Picasa call null if not found
* @private
*/
static _loadAlbum(albumId, userId = 'default') {
const imageMax = app.GoogleSource._getMaxImageSize();
const queryParams = `?imgmax=${imageMax}${_ALBUM_QUERY}`;
const url = `${_URL_BASE}${userId}/albumid/${albumId}/${queryParams}`;
if (userId === 'default') {
const conf = Chrome.JSONUtils.shallowCopy(Chrome.Http.conf);
conf.isAuth = true;
return Chrome.Http.doGet(url, conf).catch((err) => {
const statusMsg = `${Chrome.Locale.localize('err_status')}: 404`;
if (err.message.includes(statusMsg)) {
// album was probably deleted
return Promise.resolve(null);
} else {
return Promise.reject(err);
}
});
} else {
return Chrome.Http.doGet(url);
}
}
/**
* Retrieve the users list of albums, including the photos in each
* @returns {Promise<app.GoogleSource.Album[]>} Array of albums
*/
static loadAlbumList() {
const url = `${_URL_BASE}default/${_ALBUMS_QUERY}`;
// get list of albums
const conf = Chrome.JSONUtils.shallowCopy(Chrome.Http.conf);
conf.isAuth = true;
conf.retryToken = true;
conf.interactive = true;
return Chrome.Http.doGet(url, conf).then((root) => {
if (!root || !root.feed || !root.feed.entry) {
const err = new Error(Chrome.Locale.localize('err_no_albums'));
return Promise.reject(err);
}
const feed = root.feed;
const entries = feed.entry || [];
const promises = [];
for (const entry of entries) {
// series of API calls to get each album
if (!entry.gphoto$albumType) {
// skip special albums (e.g. Google+ posts, backups)
const albumId = entry.gphoto$id.$t;
promises.push(app.GoogleSource._loadAlbum(albumId));
}
}
// Collate the albums
return Promise.all(promises);
}).then((vals) => {
/** @type {app.GoogleSource.Album[]} */
let albums = [];
let ct = 0;
const values = vals || [];
for (const value of values) {
if (value !== null) {
const feed = value.feed;
if (feed && feed.entry) {
const thumb = app.GoogleSource._getThumbnail(feed.entry);
const photos = app.GoogleSource._processPhotos(value);
if (photos && photos.length) {
/** @type {app.GoogleSource.Album} */
const album = {};
album.index = ct;
album.uid = 'album' + ct;
album.name = feed.title.$t;
album.id = feed.gphoto$id.$t;
album.ct = photos.length;
album.thumb = thumb;
album.checked = false;
album.photos = photos;
albums.push(album);
ct++;
}
}
}
}
return Promise.resolve(albums);
});
}
/**
* Fetch the photos for the selected albums
* @returns {Promise<app.PhotoSource.Photo[]>} Array of photos
*/
static _fetchAlbumPhotos() {
let vals = Chrome.Storage.get('albumSelections');
// series of API calls to get each album
const promises = [];
const albums = vals || [];
for (const album of albums) {
promises.push(app.GoogleSource._loadAlbum(album.id));
}
// Collate the albums
return Promise.all(promises).then((vals) => {
/** @type {app.GoogleSource.SelectedAlbum[]} */
const albums = [];
const values = vals || [];
for (const value of values) {
if (value) {
const feed = value.feed;
const photos = app.GoogleSource._processPhotos(value);
if (photos && photos.length) {
albums.push({
id: feed.gphoto$id.$t,
photos: photos,
});
}
}
}
return Promise.resolve(albums);
});
}
/**
* Fetch the photos for this source
* @returns {Promise<app.PhotoSource.Photo[]>} Array of photos
*/
fetchPhotos() {
return app.GoogleSource._fetchAlbumPhotos();
}
};
})();