Source: sources/photo_source_google.js

/*
 *  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();
    }
  };
})();