OpenVeo Publish server

API Docs for: 8.0.0
Show:

File: app/server/providers/mediaPlatforms/youtube/YoutubeProvider.js

'use strict';

/**
 * @module providers
 */

var fs = require('fs');
var util = require('util');
var mime = require('mime');
var async = require('async');
var google = require('googleapis');
var youtube = google.youtube('v3');
var YoutubeResumableUpload = process.requirePublish(
  'app/server/providers/mediaPlatforms/youtube/YoutubeResumableUpload.js'
);
var MediaPlatformProvider = process.requirePublish('app/server/providers/mediaPlatforms/MediaPlatformProvider.js');

/**
 * Available upload methods.
 *
 * @property UPLOAD_METHODS
 * @type Array
 * @private
 * @final
 */
var UPLOAD_METHODS = ['uploadClassic', 'uploadResumable'];
Object.freeze(UPLOAD_METHODS);

/**
 * Available privacy statuses.
 *
 * @property PRIVACY_STATUSES
 * @type Array
 * @private
 * @final
 */
var PRIVACY_STATUSES = ['public', 'private', 'unlisted'];
Object.freeze(PRIVACY_STATUSES);

/**
 * Defines a YoutubeProvider class to interact with [youtube platform](https://youtube.com/).
 *
 * @class YoutubeProvider
 * @extends MediaPlatformProvider
 * @constructor
 * @param {Object} providerConf A youtube configuration object
 * @param {String} providerConf.uploadMethod The upload method to use (see UPLOAD_METHODS)
 * @param {String} providerConf.privacy The media privacy on Youtube (see PRIVACY_STATUSES)
 * @param {GoogleOAuthHelper} googleOAuthHelper The Google OAuth helper
 */
function YoutubeProvider(providerConf, googleOAuthHelper) {
  YoutubeProvider.super_.call(this, providerConf);

  Object.defineProperties(this, {

    /**
     * Youtube upload method, uploadClassic or uploadResumable.
     *
     * @property uploadMethod
     * @type String
     */
    uploadMethod: {
      value: UPLOAD_METHODS.indexOf(this.conf.uploadMethod) > -1 ? this.conf.uploadMethod : 'uploadClassic'
    },

    /**
     * Privacy to apply to uploaded medias either public, private or unlisted.
     *
     * @property privacy
     * @type String
     */
    privacy: {
      value: PRIVACY_STATUSES.indexOf(this.conf['privacy']) > -1 ? this.conf['privacy'] : 'public'
    },

    /**
     * The Google OAuth Helper to use to connect to Google APIs.
     *
     * @property googleOAuthHelper
     * @type GoogleOAuthHelper
     */
    googleOAuthHelper: {value: googleOAuthHelper}

  });
}

module.exports = YoutubeProvider;
util.inherits(YoutubeProvider, MediaPlatformProvider);

/**
 * Youtube category ids.
 *
 * @property CATEGORIES
 * @type Object
 * @static
 * @final
 */
YoutubeProvider.CATEGORIES = {
  EDUCATION: 27
};
Object.freeze(YoutubeProvider.CATEGORIES);

/**
 * Uploads a media to the Youtube platform.
 *
 * @method upload
 * @async
 * @param {String} mediaFilePath The absolute path of the media to upload
 * @param {Function} callback The function to call when it's done
 *   - **Error** The error if an error occurred, null otherwise
 *   - **String** The media id on the Youtube platform
 */
YoutubeProvider.prototype.upload = function(mediaFilePath, callback) {
  var uploadParams = {
    resource: {
      snippet: {
        title: 'Media name',
        description: 'Media description',
        categoryId: YoutubeProvider.CATEGORIES.EDUCATION
      },
      status: {
        privacyStatus: this.privacy
      }
    }
  };
  uploadParams.part = (['id'].concat(Object.keys(uploadParams.resource))).join(',');
  this[this.uploadMethod](mediaFilePath, uploadParams, callback);
};

/**
 * Uploads to Youtube in the classic way, using Youtube API.
 *
 * @method uploadClassic
 * @async
 * @param {String} mediaFilePath The absolute path to the media to upload
 * @param {Object} uploadParams Parameters to send to Youtube when calling the API
 * @param {Function} callback callback function with:
 *  - **Error** The error if an error occurred, null otherwise
 *  - **String** The media id on the Youtube platform
 */
YoutubeProvider.prototype.uploadClassic = function(mediaFilePath, uploadParams, callback) {
  var self = this;
  var mediaId;
  var media = fs.createReadStream(mediaFilePath);

  uploadParams.media = {
    mediaType: mime.lookup(mediaFilePath),
    body: media
  };

  async.series([

    // Check auth
    function(callback) {
      self.googleOAuthHelper.getFreshToken(function(error, token) {
        if (error) return callback(error);

        self.googleOAuthHelper.oauth2Client.setCredentials(token);
        callback();
      });
    },

    // Upload media
    function(callback) {
      uploadParams.auth = self.googleOAuthHelper.oauth2Client;
      youtube.videos.insert(uploadParams, function(error, response) {
        if (error) {
          callback(error);
          return;
        }
        mediaId = response.id;
        callback();
      });

    }
  ], function(error) {
    callback(error, mediaId);
  });

};


/**
 * Uploads to Youtube in a fail safe way, using resumable uploads.
 *
 * The upload can fail 3 times before failing globally, each times it fails it perform an upload again starting where
 * it previously failed (ie: not re-uploading all the media)
 *
 * @method uploadResumable
 * @async
 * @param {String} mediaFilePath The absolute path to the media to upload
 * @param {Object} uploadParams Parameters to send to Youtube when calling the API
 * @param {Function} callback callback function with:
 *  - **Error** The error if an error occurred, null otherwise
 *  - **String** The uploaded media id
 */
YoutubeProvider.prototype.uploadResumable = function(mediaFilePath, uploadParams, callback) {
  var self = this;
  var mediaId;
  var stats;

  async.series([

    // Check auth
    function(callback) {
      self.googleOAuthHelper.getFreshToken(function(error, token) {
        if (error) return callback(error);

        uploadParams.auth = token;
        callback();
      });
    },

    // Get file size
    function(callback) {
      fs.stat(mediaFilePath, function(error, fileStats) {
        if (error) {
          callback(error);
          return;
        }
        stats = fileStats;
        callback();
      });
    },

    // Upload media
    function(callback) {
      if (!uploadParams.hasOwnProperty('auth') || !uploadParams.auth) {
        callback(new Error('Auth has not been set correctly'));
        return;
      }
      var resumableUpload = new YoutubeResumableUpload();

      resumableUpload.tokens = uploadParams.auth;
      resumableUpload.filepath = mediaFilePath;
      resumableUpload.stats = stats;
      resumableUpload.metadata = uploadParams.resource;
      resumableUpload.retry = 3;

      resumableUpload.on('progress', function(progress) {
        process.logger.debug('Upload progress', progress);
      });
      resumableUpload.on('error', function(error) {
        process.logger.debug('Upload error', error);
        if (resumableUpload.retry === 0) {
          callback(error);
        }
      });
      resumableUpload.on('success', function(media) {
        media = JSON.parse(media);
        mediaId = media.id;
        callback();
      });
      resumableUpload.upload();
    }
  ], function(error) {
    callback(error, mediaId);
  });

};

/**
 * Gets information about a media hosted by Youtube.
 *
 * @method getMediaInfo
 * @async
 * @param {String} mediaId The Youtube id of the media
 * @param {Function} callback The function to call when it's done
 *   - **Error** The error if an error occurred, null otherwise
 *   - **Object** Information about the media
 */
YoutubeProvider.prototype.getMediaInfo = function(mediaId, definition, callback) {
  if (!mediaId) {
    callback(new Error('media id should be defined'), null);
    return;
  }

  // sources and pictures are not necessary: youtube player manage its own data
  callback(null, {available: true, sources: [], pictures: [], mediaId: mediaId});
};

/**
 * Removes a media from the Youtube platform.
 *
 * @method remove
 * @async
 * @param {Array} mediaIds Youtube media ids to remove
 * @param {Function} callback The function to call when it's done
 *   - **Error** The error if an error occurred, null otherwise
 */
YoutubeProvider.prototype.remove = function(mediaIds, callback) {
  var self = this;

  if (!mediaIds) {
    callback(new Error('media id should be defined'), null);
    return;
  }
  var series = [];

  series.push(function(callback) {
    self.googleOAuthHelper.getFreshToken(function(error, token) {
      if (error) return callback(error);

      self.googleOAuthHelper.oauth2Client.setCredentials(token);
      callback();
    });
  });

  mediaIds.forEach(function(mediaId) {
    series.push(function(callback) {
      var deleteParam = {
        id: mediaId,
        part: 'id'
      };
      deleteParam.auth = self.googleOAuthHelper.oauth2Client;
      youtube.videos.delete(deleteParam, function(error) {
        if (error) {
          callback(error);
          return;
        }
        callback();
      });
    });
  });

  async.series(series, function(error) {
    callback(error);
  });
};

/**
 * Updates a media resources on the platform.
 *
 * If media has several resources on the platform, the same update will be performed for all resources.
 * Actually only the media title is synchronized with Youtube.
 *
 * @method update
 * @async
 * @param {Object} media The media
 * @param {Array} media.mediaId The list of media resource ids
 * @param {Object} data The datas to update
 * @param {String} [data.title] The media title. Be careful only the first 100 characters will be used, also
 * "less than" and "greater than" characters will be removed
 * @param {Boolean} force true to force the update even if title hasn't changed, false otherwise
 * @param {Function} callback The function to call when it's done
 *   - **Error** The error if an error occurred, null otherwise
 */
YoutubeProvider.prototype.update = function(media, data, force, callback) {
  if (!data.title || (data.title === media.title && !force)) return callback();

  var self = this;
  var series = [];

  series.push(function(callback) {
    self.googleOAuthHelper.getFreshToken(function(error, token) {
      if (error) return callback(error);

      self.googleOAuthHelper.oauth2Client.setCredentials(token);
      callback();
    });
  });

  media.mediaId.forEach(function(mediaId) {
    series.push(function(callback) {
      youtube.videos.update({
        part: 'snippet',
        auth: self.googleOAuthHelper.oauth2Client,
        resource: {
          id: mediaId,
          snippet: {
            title: data.title.substring(0, 100).replace(/<|>/g, ''),
            categoryId: YoutubeProvider.CATEGORIES.EDUCATION
          }
        }
      }, callback);
    });
  });

  async.series(series, function(error) {
    callback(error);
  });
};