UNPKG

@vimeo/vimeo

Version:

A Node.js library for the new Vimeo API.

734 lines (652 loc) 22.4 kB
'use strict' /** * Copyright 2013 Vimeo * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const qsModule = require('querystring') const urlModule = require('url') const httpModule = require('http') const httpsModule = require('https') const fs = require('fs') const path = require('path') const tus = require('tus-js-client') module.exports.request_defaults = { protocol: 'https:', hostname: 'api.vimeo.com', port: 443, method: 'GET', query: {}, headers: { Accept: 'application/vnd.vimeo.*+json;version=3.4', 'User-Agent': 'Vimeo.js/2.1.1' } } const authEndpoints = module.exports.authEndpoints = { authorization: '/oauth/authorize', accessToken: '/oauth/access_token', clientCredentials: '/oauth/authorize/client' } /** * This object is used to interface with the Vimeo API. * * @param {string} clientId OAuth 2 Client Identifier * @param {string} clientSecret OAuth 2 Client Secret * @param {string} [accessToken] OAuth 2 Optional pre-authorized access token */ const Vimeo = module.exports.Vimeo = function Vimeo (clientId, clientSecret, accessToken) { this._clientId = clientId this._clientSecret = clientSecret if (accessToken) { this._accessToken = accessToken } } Vimeo.prototype._clientId = null Vimeo.prototype._clientSecret = null Vimeo.prototype._accessToken = null /** * Performs an API call. * * Can be implemented using a Callback or a Promise: * * .request( Url | Options [, Callback]) * * - Url <string> * If a url is provided, we fill in the rest of the request options with defaults * (GET http://api.vimeo.com/{url}). * * - Options <Object> * If an object is provided, it should match the response of urlModule.parse. Path is the only * required parameter. * * - hostname * - port * - query (will be applied to the url if GET, request body if POST with content type application/x-www-form-urlencoded or application/json) * - headers * - path (can include a querystring) * - method * - body (will be applied to request body if POST with content type that is not application/x-www-form-urlencoded or application/json) * * - Callback (optional) * The callback takes two parameters, `err` and `json`. * If an error has occured, your callback will be called as `callback(err)`; * If an error has not occured, your callback will be called as `callback(null, json)`; * If not passed in, a Promise will be returned. * * @param {string|Object} options String path (default GET), or object with `method`, path`, * `host`, `port`, `query`, `body`, or `headers`. * @param {Function} [callback] (optional) Called when complete, `function (err, json)`. If not passed in, a Promise will be returned. */ Vimeo.prototype.request = function (options, callback) { let client = null // If a URL was provided, build an options object. if (typeof options === 'string') { options = urlModule.parse(options, true) // eslint-disable-line n/no-deprecated-api options.method = 'GET' } // If we don't have a path at this point, error. a path is the only required field. We have // defaults for everything else important. if (typeof options.path !== 'string') { if (callback === undefined) { return new Promise((resolve, reject) => { reject(new Error('You must provide an API path.')) }) } else { return callback(new Error('You must provide an API path.')) } } // Add leading slash to path if missing if (options.path.charAt(0) !== '/') { options.path = '/' + options.path } // Turn the provided options into options that are valid for `client.request`. const requestOptions = this._buildRequestOptions(options) client = requestOptions.protocol === 'https:' ? httpsModule : httpModule if (['POST', 'PATCH', 'PUT', 'DELETE'].indexOf(requestOptions.method) !== -1) { if (requestOptions.headers['Content-Type'] === 'application/json') { requestOptions.body = JSON.stringify(options.query) } else if (requestOptions.headers['Content-Type'] === 'application/x-www-form-urlencoded') { requestOptions.body = qsModule.stringify(options.query) } else { requestOptions.body = options.body } if (requestOptions.body) { requestOptions.headers['Content-Length'] = Buffer.byteLength(requestOptions.body, 'utf8') } else { requestOptions.headers['Content-Length'] = 0 } } if (callback === undefined) { return new Promise((resolve, reject) => { const req = client.request(requestOptions, this._handleRequest(resolve, reject)) if (requestOptions.body) { req.write(requestOptions.body) } req.on('error', function (e) { reject(e) }) req.end() }) } else { // Perform the Vimeo API request const req = client.request(requestOptions, this._handleRequest(callback)) if (requestOptions.body) { req.write(requestOptions.body) } req.on('error', function (e) { callback(e) }) req.end() } } /** * Creates the standard request handler for http requests * * @param {Function} callback * @param {Function} [reject] (optional) used when called inside a Promise * @return {Function} */ Vimeo.prototype._handleRequest = function (callback, reject) { const isPromise = reject !== undefined reject = reject || callback return function (res) { res.setEncoding('utf8') let buffer = '' res.on('readable', function () { buffer += res.read() || '' }) if (res.statusCode >= 400) { // Failed api calls should wait for the response to end and then call the callback or the reject fn if passed in with an // error. res.on('end', function () { const err = new Error(buffer) reject(err, buffer, res.statusCode, res.headers) }) } else { // Successful api calls should wait for the response to end and then call the callback with // the response body. let body = null res.on('end', function () { try { body = buffer.length ? JSON.parse(buffer) : {} if (isPromise) { const callbackData = { statusCode: res.statusCode, body, headers: res.headers } callback(callbackData) } else { callback(null, body, res.statusCode, res.headers) } } catch (err) { return reject(err, buffer, res.statusCode, res.headers) } }) } } } /** * Merge the request options defaults into the request options * * @param {Object} options * @return {Object} */ Vimeo.prototype._buildRequestOptions = function (options) { // Set up the request object. we always use the options paramter first, and if no value is // provided we fall back to request defaults. const requestOptions = this._applyDefaultRequestOptions(options) if (this._accessToken) { requestOptions.headers.Authorization = 'Bearer ' + this._accessToken } else if (this._clientId && this._clientSecret) { const basicToken = Buffer.from(this._clientId + ':' + this._clientSecret) requestOptions.headers.Authorization = 'Basic ' + basicToken.toString('base64') } if (['POST', 'PATCH', 'PUT', 'DELETE'].indexOf(requestOptions.method) !== -1 && !requestOptions.headers['Content-Type'] ) { // Set proper headers for POST, PATCH and PUT bodies. requestOptions.headers['Content-Type'] = 'application/json' } else if (requestOptions.method === 'GET') { // Apply parameters to the URL for GET requests. requestOptions.path = this._applyQuerystringParams(requestOptions, options) } return requestOptions } /** * Create an object of request options based on the provided list of options, and the request * defaults. * * @param {Object} options * @return {Object} */ Vimeo.prototype._applyDefaultRequestOptions = function (options) { const requestOptions = { protocol: options.protocol || module.exports.request_defaults.protocol, host: options.hostname || module.exports.request_defaults.hostname, port: options.port || module.exports.request_defaults.port, method: options.method || module.exports.request_defaults.method, headers: options.headers || {}, body: '', path: options.path } let key = null // Apply the default headers if (module.exports.request_defaults.headers) { for (key in module.exports.request_defaults.headers) { if (!requestOptions.headers[key]) { requestOptions.headers[key] = module.exports.request_defaults.headers[key] } } } return requestOptions } /** * Apply the query parameter onto the final request URL. * * @param {Object} requestOptions * @param {string} requestOptions.path * @param {Object} options * @param {string} options.query * @return {string} */ Vimeo.prototype._applyQuerystringParams = function (requestOptions, options) { let querystring = '' if (!options.query) { return requestOptions.path } // If we have parameters, apply them to the URL. if (Object.keys(options.query).length) { if (requestOptions.path.indexOf('?') < 0) { // If the existing path does not contain any parameters, apply them as the only options. querystring = '?' + qsModule.stringify(options.query) } else { // If the user already added parameters to the URL, we want to add them as additional // parameters. querystring = '&' + qsModule.stringify(options.query) } } return requestOptions.path + querystring } /** * Set a user access token to be used with library requests. * * @param {string} accessToken */ Vimeo.prototype.setAccessToken = function (accessToken) { this._accessToken = accessToken } /** * Exchange a code for an access token. This code should exist on your `redirectUri`. * * @param {string} code The code provided on your `redirectUri`. * @param {string} redirectUri The exact `redirectUri` provided to `buildAuthorizationEndpoint` * and configured in your API app settings. * @param {Function} [fn] (optional) Callback to execute on completion. If not passed in, a Promise will be returned. */ Vimeo.prototype.accessToken = function (code, redirectUri, fn) { const options = { method: 'POST', hostname: module.exports.request_defaults.hostname, path: authEndpoints.accessToken, query: { grant_type: 'authorization_code', code, redirect_uri: redirectUri }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } if (fn === undefined) { return this.request(options) } this.request(options, function (err, body, status, headers) { if (err) { return fn(err, null, status, headers) } else { fn(null, body, status, headers) } }) } /** * The first step of the authorization process. * * This function returns a URL, which the user should be sent to (via redirect or link). * * The destination allows the user to accept or deny connecting with vimeo, and accept or deny each * of the scopes you requested. Scopes are passed through the second parameter as an array of * strings, or a space delimited list. * * Once accepted or denied, the user is redirected back to the `redirectUri`. * * @param {string} redirectUri The URI that will exchange a code for an access token. Must match * the URI in your API app settings. * @param {string|string[]} scope An array of scopes. See https://developer.vimeo.com/api/authentication#scopes * for more. * @param {string} state A unique state that will be returned to you on your redirect URI. */ Vimeo.prototype.buildAuthorizationEndpoint = function (redirectUri, scope, state) { const query = { response_type: 'code', client_id: this._clientId, redirect_uri: redirectUri } if (scope) { if (Array.isArray(scope)) { query.scope = scope.join(' ') } else { query.scope = scope } } else { query.scope = 'public' } if (state) { query.state = state } return module.exports.request_defaults.protocol + '//' + module.exports.request_defaults.hostname + authEndpoints.authorization + '?' + qsModule.stringify(query) } /** * Generates an unauthenticated access token. This is necessary to make unauthenticated requests * * @param {string|string[]} scope An array of scopes. See https://developer.vimeo.com/api/authentication#scopes * for more. * @param {Function} [fn] (optional) A function that is called when the request is complete. If an error * occured the first parameter will be that error, otherwise the first * parameter will be null. If not passed in, a Promise will be returned. */ Vimeo.prototype.generateClientCredentials = function (scope, fn) { const query = { grant_type: 'client_credentials' } if (scope) { if (Array.isArray(scope)) { query.scope = scope.join(' ') } else { query.scope = scope } } else { query.scope = 'public' } const options = { method: 'POST', hostname: module.exports.request_defaults.hostname, path: authEndpoints.clientCredentials, query, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } if (fn === undefined) { return this.request(options) } this.request(options, function (err, body, status, headers) { if (err) { return fn(err, null, status, headers) } else { fn(null, body, status, headers) } }) } /** * Upload a file. * * This should be used to upload a local file. If you want a form for your site to upload direct to * Vimeo, you should look at the `POST /me/videos` endpoint. * * https://developer.vimeo.com/api/reference/videos#upload_video * * .upload( file [, params] [, completeCallback], progressCallback [, errorCallback]) * * - params (optional) * If an object is not provided, default upload params are used. * * - completeCallback and errorCallback (optional) * If neither passed in, a Promise will be returned. * Ex. vimeo.upload(file, progressCallback) or vimeo.upload(file, params, progressCallback) * * @param {string} file Path to the file you wish to upload. * @param {Object=} [params] (optional) Parameters to send when creating a new video (name, * privacy restrictions, etc.). See the API documentation for * supported parameters. * @param {Function} [completeCallback] (optional) Callback to be executed when the upload completes. * @param {Function} progressCallback Callback to be executed when upload progress is updated. * @param {Function} [errorCallback] (optional) Callback to be executed when the upload returns an error. */ Vimeo.prototype.upload = function ( file, params, completeCallback, progressCallback, errorCallback ) { const _self = this let fileSize if (typeof params === 'function') { errorCallback = progressCallback progressCallback = completeCallback completeCallback = params params = {} } const isPromise = progressCallback === undefined && errorCallback === undefined if (isPromise) { progressCallback = completeCallback } if (typeof file === 'string') { try { fileSize = fs.statSync(file).size } catch (e) { if (isPromise) { return new Promise((resolve, reject) => reject(e)) } return errorCallback('Unable to locate file to upload.') } } else { const error = new Error('Please pass in a valid file path.') if (isPromise) { return new Promise((resolve, reject) => reject(error)) } return errorCallback(error) } // Ignore any specified upload approach and size. if (typeof params.upload === 'undefined') { params.upload = { approach: 'tus', size: fileSize } } else { params.upload.approach = 'tus' params.upload.size = fileSize } const options = { path: '/me/videos?fields=uri,name,upload', method: 'POST', query: params } if (isPromise) { return new Promise((resolve, reject) => { this.request(options).then(attempt => { _self._performTusUpload( file, fileSize, attempt.body, resolve, progressCallback, reject ) }).catch(err => { reject(new Error('Unable to initiate an upload. [' + err.message + ']')) }) }) } // Use JSON filtering so we only receive the data that we need to make an upload happen. this.request(options, function (err, attempt) { if (err) { return errorCallback('Unable to initiate an upload. [' + err + ']') } _self._performTusUpload( file, fileSize, attempt, completeCallback, progressCallback, errorCallback ) }) } /** * Replace the source of a single Vimeo video. * * https://developer.vimeo.com/api/reference/videos#create_video_version * * .replace( file, videoUri [, params] [, completeCallback], progressCallback [, errorCallback]) * * - params (optional) * If an object is not provided, default upload params are used. * * - completeCallback and errorCallback (optional) * If neither passed in, a Promise will be returned. * Ex. vimeo.replace(file, videoUri, progressCallback) or vimeo.replace(file, videoUri, params, progressCallback) * * @param {string} file Path to the file you wish to upload. * @param {string} videoUri Video URI of the video file to replace. * @param {Object=} [params] (optional) Parameters to send when creating a new video (name, * privacy restrictions, etc.). See the API documentation for * supported parameters. * @param {Function} [completeCallback] (optional) Callback to be executed when the upload completes. * @param {Function} progressCallback Callback to be executed when upload progress is updated. * @param {Function} [errorCallback] (optional) Callback to be executed when the upload returns an error. */ Vimeo.prototype.replace = function ( file, videoUri, params, completeCallback, progressCallback, errorCallback ) { const _self = this let fileSize if (typeof params === 'function') { errorCallback = progressCallback progressCallback = completeCallback completeCallback = params params = {} } const isPromise = progressCallback === undefined && errorCallback === undefined if (isPromise) { progressCallback = completeCallback } if (typeof file === 'string') { try { fileSize = fs.statSync(file).size } catch (e) { if (isPromise) { return new Promise((resolve, reject) => reject(e)) } return errorCallback('Unable to locate file to upload.') } params.file_name = path.basename(file) } else { const error = new Error('Please pass in a valid file path.') if (isPromise) { return new Promise((resolve, reject) => reject(error)) } return errorCallback(error) } // Ignore any specified upload approach and size. if (typeof params.upload === 'undefined') { params.upload = { approach: 'tus', size: fileSize } } else { params.upload.approach = 'tus' params.upload.size = fileSize } const options = { path: videoUri + '/versions?fields=upload', method: 'POST', query: params } if (isPromise) { return new Promise((resolve, reject) => { this.request(options).then(attempt => { attempt.body.uri = videoUri _self._performTusUpload( file, fileSize, attempt.body, resolve, progressCallback, reject ) }) .catch(err => { reject(new Error('Unable to initiate an upload. [' + err.message + ']')) }) }) } // Use JSON filtering so we only receive the data that we need to make an upload happen. _self.request(options, function (err, attempt) { if (err) { return errorCallback('Unable to initiate an upload. [' + err + ']') } attempt.uri = videoUri _self._performTusUpload( file, fileSize, attempt, completeCallback, progressCallback, errorCallback ) }) } /** * Take an upload attempt and perform the actual upload via tus. * * https://tus.io/ * * @param {string} file Path to the file you wish to upload. * @param {integer} fileSize Size of the file that will be uploaded. * @param {Object} attempt Upload attempt data. * @param {Function} completeCallback Callback to be executed when the upload completes. * @param {Function} progressCallback Callback to be executed when the upload progress is updated. * @param {Function} errorCallback Callback to be executed when the upload returns an error. */ Vimeo.prototype._performTusUpload = function ( file, fileSize, attempt, completeCallback, progressCallback, errorCallback ) { let fileUpload = file if (typeof file === 'string') { fileUpload = fs.createReadStream(file) } const upload = new tus.Upload(fileUpload, { uploadUrl: attempt.upload.upload_link, uploadSize: fileSize, retryDelays: [0, 1000, 3000, 5000], onError: errorCallback, onProgress: progressCallback, onSuccess: function () { return completeCallback(attempt.uri) } }) upload.start() }