├── .gitignore ├── .travis.yml ├── CHANGELOG ├── LICENSE ├── README.md ├── bower.json ├── composer.json ├── img ├── heart.svg └── icon.svg ├── index.html ├── package.json ├── upload-cordova.js └── vimeo-upload.js /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /bower_components 3 | /node_modules 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | script: npm run deploy 2 | language: node_js 3 | node_js: 4 | - '0.10' 5 | env: 6 | global: 7 | - GH_REF: github.com/websemantics/vimeo-upload.git 8 | - secure: wIZAh8a5s96MT0CsTs0y06ljBH5UpPl3s10NA1QplHafTtSubWpjE51oUQJ+kzrhB/LMVb051C2Eygmhwa1kXgjeH+shQEJmPSnCRo7p5GLnM5EO4U2M6IXu9Yre7BKL07rHXDeMl5ZlHaR52L+uLCCNAlh/XCNgMu0dlKucGGtOG0mqaoZJdcdTpM7tVtZo5jQvN7Ihe4TbscwaYYNUnaqETiQX3OKTWgmGPkrIq26/6o8St1QkQwZd22R2dwhN2hmEUPrvfWjQxk+hUuNQv8K3vhP8mPhFUuQlxo1S5sv4lvT065ekhanDvm8qju5k6yMQivVftoLMVzhzJUqZY/iyb2BHRyFGwqA3Zz5rqE4HmMRhVXITOdAjPtytAMxk9zdYsKX2NSdTacGN65pKvupeIfQV3GC+HlhpXi2ZtyFwpM42NJYUhA9gUJ92RN7hqIpS2jZdTeWTjOuRcjLxC2JNyGSgIY1tJQ9Tie5MFu5UqBoVPXpoL64I8ZKp8yrnLNW0ccqY/bZEmC+kt0Brhs4TwZLA3+Y1IZVBdeKzs2NS4J8uZbgbacDgavr7sFBkGf3XQIbOXC+hcznKY8R2ugTnAQFYi4JDARvYsyK2zh22bIhq/h5nFsmZIAb2Dft+jnZ2bFBxS1UePV9mEc9PhGEtWc/+5NDjRtBNcEA8VFs= 9 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 0.1.6 2 | date: 2016-12-12 3 | changes: 4 | - Set video privacy to `nobody` when private checkbox is ticked. 5 | 6 | 0.1.5 7 | date: 2016-06-22 8 | changes: 9 | - Change license to Apache 2.0 to confirm with Google cors-upload-sample original source code 10 | 11 | 0.1.4 12 | date: 2016-06-22 13 | changes: 14 | - Major code refactoring 15 | - Add Node.js support 16 | - Add Bower support 17 | - Enhanced user experience 18 | 19 | 0.1.3 20 | date: 2016-06-21 21 | changes: 22 | - Make video private 23 | - Add BraGit buttons 24 | 25 | 0.1.2 26 | date: 2016-04-29 27 | changes: 28 | - Update logo 29 | - Integrate with Travis 30 | 31 | 0.1.1 32 | date: 2016-02-25 33 | changes: 34 | - Update video data after upload (name & description) 35 | - Adding data chunking support for Cordova 36 | 37 | 0.1.0 38 | date: 2015-01-14 39 | changes: 40 | - Upload videos 41 | - Support for high-definition videos 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | ╭───╮╭─╮ 3 | │ ││ │╭─╮╭──┬──┬─╮╭───╮╭───╮ 4 | │ ││ │├─┤│ ╭╮ ╭╮ ││ ─ ││╭╮ │ ╭────────┬─────────────────────╮ 5 | ╰╮ ╰╯╭╯│ ││ ││ ││ ││ ─┤│╰╯ │ | UPLOAD │ ▒▒▒▒▒▒▒▒▒▒▒░░░░ %75 | 6 | ╰────╯ ╰─╯╰─╯╰─╯╰─╯╰───╯╰───╯ ╰────────┴─────────────────────╯ 7 | ``` 8 | 9 | [![Build Status](https://travis-ci.org/websemantics/vimeo-upload.svg?branch=master)](https://travis-ci.org/websemantics/vimeo-upload) 10 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) 11 | [![npm version](https://badge.fury.io/js/vimeo-upload.svg)](https://badge.fury.io/js/vimeo-upload) 12 | [![Bower version](https://badge.fury.io/bo/vimeo-upload.svg)](https://badge.fury.io/bo/vimeo-upload) 13 | [![GitHub forks](https://img.shields.io/github/forks/websemantics/vimeo-upload.svg)](https://github.com/websemantics/vimeo-upload/network) [![GitHub stars](https://img.shields.io/github/stars/websemantics/vimeo-upload.svg)](https://github.com/websemantics/vimeo-upload/stargazers) 14 | [![Percentage of issues still open](http://isitmaintained.com/badge/open/websemantics/vimeo-upload.svg)](http://isitmaintained.com/project/websemantics/vimeo-upload "Percentage of issues still open") 15 | > Upload videos to your Vimeo account and update their metadata directly from a browser or a Node.js app. 16 | 17 | Try it [LIVE](http://websemantics.github.io/vimeo-upload/) 18 | 19 | ## Install 20 | 21 | Using Bower 22 | ``` 23 | bower install vimeo-upload 24 | ``` 25 | 26 | Or npm 27 | 28 | ``` 29 | npm install vimeo-upload 30 | ``` 31 | 32 | ## Usage 33 | 34 | Include `vimeo-upload.js` in your index.html. 35 | 36 | ``` 37 | 38 | ``` 39 | 40 | Create a new `VimeoUpload` initialized with a Blob or File and Vimeo Access Token then call `upload()` to start the upload process. 41 | 42 | ```javascript 43 | var uploader = new VimeoUpload({ 44 | file: file, 45 | token: accessToken, 46 | }); 47 | 48 | uploader.upload(); 49 | ``` 50 | 51 | Your access token need to be authorized by Vimeo. Create new Vimeo access token [here](https://developer.vimeo.com/apps). 52 | 53 | Check `index.html` for details and additional parameters you can include when initializing `VimeoUpload`. 54 | 55 | ## Credits 56 | 57 | Sample code for uploading files directly with XHR/CORS: [cors-upload-sample](https://github.com/googledrive/cors-upload-sample) 58 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vimeo-upload", 3 | "main": "vimeo-upload.js", 4 | "version": "0.1.6", 5 | "homepage": "https://github.com/websemantics/vimeo-upload", 6 | "authors": [ 7 | "Adnan Sh. Sagar, PhD. " 8 | ], 9 | "description": "Upload videos to your Vimeo account and update their metadata directly from a browser or a Node.js app.", 10 | "keywords": [ 11 | "vimeo", 12 | "video", 13 | "upload", 14 | "node", 15 | "browser" 16 | ], 17 | "license": "Apache 2.0", 18 | "ignore": [ 19 | "**/.*", 20 | "node_modules", 21 | "test", 22 | "tests" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "websemantics/vimeo-upload", 3 | "description": "Upload videos to your Vimeo account and update their metadata directly from a browser or a Node.js app.", 4 | "type": "library", 5 | "authors": [ 6 | { 7 | "name": "websemantics", 8 | "email": "adnan@websemantics.ca" 9 | }, 10 | { 11 | "name": "sadashiv", 12 | "email": "dalvisadashiv@gmail.com" 13 | } 14 | ], 15 | "require": {} 16 | } 17 | -------------------------------------------------------------------------------- /img/heart.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /img/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vimeo Upload Demo 6 | 7 | 8 | 130 | 131 | 132 |
133 | 153 |

Enter vimeo access token, name and description then drag your video file into the dotted area below to upload to your vimeo account.

154 |
155 |
 0% 156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 | 167 |
Create an access token here: https://developer.vimeo.com/apps
168 |
169 |
170 | 171 |
172 |
173 | 174 |
175 |
176 | 179 |
180 |
181 | 184 |
185 |
186 |
187 |
Drop Files Here
188 |
189 | 192 |
193 |
194 |
195 | 196 | 201 | 202 | 203 | 204 | 205 | 299 | 300 | 315 | 316 | 317 | 318 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vimeo-upload", 3 | "version": "0.1.6", 4 | "description": "Upload videos to your Vimeo account and update their metadata directly from a browser or a Node.js app.", 5 | "main": "vimeo-upload.js", 6 | "homepage": "https://github.com/websemantics/vimeo-upload#readme", 7 | "repository" : { 8 | "type" : "git", 9 | "url" : "websemantics/vimeo-upload.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/websemantics/vimeo-upload/issues" 13 | }, 14 | "keywords": [ 15 | "vimeo", 16 | "video", 17 | "upload", 18 | "node", 19 | "browser" 20 | ], 21 | "deploy": { 22 | "name": "iAyeBot", 23 | "email": "iayebot@websemantics.ca", 24 | "dist": "dist", 25 | "message": "Deploy to gh-pages" 26 | }, 27 | "scripts": { 28 | "deploy": "cd $npm_package_deploy_dist && git push --force --quiet \"https://${GH_TOKEN}@${GH_REF}\" master:gh-pages > /dev/null 2>&1 ", 29 | "predeploy": "mkdir -p ${npm_package_deploy_dist} && npm run copy -s&& cd $npm_package_deploy_dist && git init && git config user.name $npm_package_deploy_name && git config user.email $npm_package_deploy_email && git add -A . && git commit -am \"$npm_package_deploy_message\" ", 30 | "copy": "cp -r ./img $npm_package_deploy_dist && cp ./index.html $npm_package_deploy_dist && cp ./vimeo-upload.js $npm_package_deploy_dist " 31 | }, 32 | "author": "Adnan M.Sagar, PhD.", 33 | "license": "Apache 2.0" 34 | } 35 | -------------------------------------------------------------------------------- /upload-cordova.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | /** 4 | * Helper for implementing retries with backoff. Initial retry 5 | * delay is 1 second, increasing by 2x (+jitter) for subsequent retries 6 | * 7 | * @constructor 8 | */ 9 | var RetryHandler = function() { 10 | this.interval = 1000; // Start at one second 11 | this.maxInterval = 60 * 1000; // Don't wait longer than a minute 12 | }; 13 | 14 | /** 15 | * Invoke the function after waiting 16 | * 17 | * @param {function} fn Function to invoke 18 | */ 19 | RetryHandler.prototype.retry = function(fn) { 20 | setTimeout(fn, this.interval); 21 | this.interval = this.nextInterval_(); 22 | }; 23 | 24 | /** 25 | * Reset the counter (e.g. after successful request.) 26 | */ 27 | RetryHandler.prototype.reset = function() { 28 | this.interval = 1000; 29 | }; 30 | 31 | /** 32 | * Calculate the next wait time. 33 | * @return {number} Next wait interval, in milliseconds 34 | * 35 | * @private 36 | */ 37 | RetryHandler.prototype.nextInterval_ = function() { 38 | var interval = this.interval * 2 + this.getRandomInt_(0, 1000); 39 | return Math.min(interval, this.maxInterval); 40 | }; 41 | 42 | /** 43 | * Get a random int in the range of min to max. Used to add jitter to wait times. 44 | * 45 | * @param {number} min Lower bounds 46 | * @param {number} max Upper bounds 47 | * @private 48 | */ 49 | RetryHandler.prototype.getRandomInt_ = function(min, max) { 50 | return Math.floor(Math.random() * (max - min + 1) + min); 51 | }; 52 | 53 | 54 | /** 55 | * Helper class for resumable uploads using XHR/CORS. Can upload any Blob-like item, whether 56 | * files or in-memory constructs. 57 | * 58 | * @example 59 | * var content = new Blob(["Hello world"], {"type": "text/plain"}); 60 | * var uploader = new MediaUploader({ 61 | * file: content, 62 | * token: accessToken, 63 | * onComplete: function(data) { ... } 64 | * onError: function(data) { ... } 65 | * }); 66 | * uploader.upload(); 67 | * 68 | * @constructor 69 | * @param {object} options Hash of options 70 | * @param {string} options.token Access token 71 | * @param {blob} options.file Blob-like item to upload 72 | * @param {string} [options.fileId] ID of file if replacing 73 | * @param {object} [options.params] Additional query parameters 74 | * @param {string} [options.contentType] Content-type, if overriding the type of the blob. 75 | * @param {object} [options.metadata] File metadata 76 | * @param {function} [options.onComplete] Callback for when upload is complete 77 | * @param {function} [options.onProgress] Callback for status for the in-progress upload 78 | * @param {function} [options.onError] Callback if upload fails 79 | */ 80 | var MediaUploader = function(options) { 81 | var noop = function() {}; 82 | this.isCordovaApp = options.isCordovaApp; 83 | this.realUrl = options.realUrl; 84 | this.file = options.file; 85 | this.contentType = options.contentType || this.file.type || 'application/octet-stream'; 86 | this.metadata = options.metadata || { 87 | 'title': this.file.name, 88 | 'mimeType': this.contentType 89 | }; 90 | this.token = options.token; 91 | this.onComplete = options.onComplete || noop; 92 | this.onProgress = options.onProgress || noop; 93 | this.onError = options.onError || noop; 94 | this.offset = options.offset || 0; 95 | this.chunkSize = options.chunkSize || 0; 96 | this.retryHandler = new RetryHandler(); 97 | 98 | this.url = options.url; 99 | 100 | if (!this.url) { 101 | var params = options.params || {}; 102 | // params.uploadType = 'resumable'; 103 | this.url = this.buildUrl_(options.fileId, params, options.baseUrl); 104 | } 105 | 106 | this.httpMethod = options.fileId ? 'PUT' : 'POST'; 107 | 108 | this.currentXHR = null; 109 | this.chunkProgress = 0; 110 | }; 111 | 112 | MediaUploader.prototype.destroy = function() { 113 | this.isCordovaApp = null; 114 | this.realUrl = null; 115 | this.file = null; 116 | this.contentType = null; 117 | this.metadata = null; 118 | this.token = null; 119 | this.onComplete = null; 120 | this.onProgress = null; 121 | this.onError = null; 122 | this.offset = null; 123 | this.chunkSize = null; 124 | this.retryHandler = null; 125 | this.url = null; 126 | this.httpMethod = null; 127 | 128 | this.currentXHR = null; 129 | this.chunkProgress = null; 130 | 131 | this.user = null; 132 | this.ticket_id = null; 133 | this.complete_url = null; 134 | this.token = null; 135 | 136 | this.offset = null; 137 | } 138 | 139 | /** 140 | * Initiate the upload (Get vimeo ticket number and upload url) 141 | */ 142 | MediaUploader.prototype.upload = function() { 143 | 144 | var xhr = new XMLHttpRequest(); 145 | 146 | xhr.open(this.httpMethod, this.url, true); 147 | xhr.setRequestHeader('Authorization', 'Bearer ' + this.token); 148 | xhr.setRequestHeader('Content-Type', 'application/json'); 149 | 150 | xhr.onload = function(e) { 151 | // get vimeo upload url, user (for available quote), ticket id and complete url 152 | if (e.target.status < 400) { 153 | var response = JSON.parse(e.target.responseText); 154 | this.url = response.upload_link_secure; 155 | this.user = response.user; 156 | this.ticket_id = response.ticket_id; 157 | this.complete_url = "https://api.vimeo.com"+response.complete_uri; 158 | this.sendFile_(); 159 | } else { 160 | this.onUploadError_(e); 161 | } 162 | }.bind(this); 163 | 164 | xhr.onerror = this.onUploadError_.bind(this); 165 | xhr.send(JSON.stringify({ 166 | type:'streaming' 167 | })); 168 | 169 | this.currentXHR = xhr; 170 | }; 171 | 172 | MediaUploader.prototype.abort = function() { 173 | this.currentXHR.abort() 174 | this.onError("Upload aborted"); 175 | }; 176 | 177 | /** 178 | * Send the actual file content. 179 | * New @ 16 June 2015 180 | * This has been modified to support Cordova FileTransfer plugin 181 | * 182 | * @private 183 | */ 184 | MediaUploader.prototype.sendFile_ = function() { 185 | var content = this.file; 186 | var end = this.file.size; 187 | 188 | if (this.offset || this.chunkSize) { 189 | // Only bother to slice the file if we're either resuming or uploading in chunks 190 | if (this.chunkSize) { 191 | end = Math.min(this.offset + this.chunkSize, this.file.size); 192 | } 193 | content = content.slice(this.offset, end); 194 | } 195 | 196 | this.chunkProgress = end 197 | 198 | if(this.isCordovaApp) { 199 | // Read the video file, 200 | var reader = new FileReader(); 201 | 202 | reader.onloadend = function (evt) { 203 | this.send_(evt.target.result, end); 204 | }.bind(this); 205 | 206 | reader.readAsArrayBuffer(content); 207 | 208 | } else{ 209 | this.send_(content, end); 210 | } 211 | 212 | }; 213 | 214 | 215 | /** 216 | * Send the file 217 | * Added @ 16 June 2015, .. 218 | * @private 219 | */ 220 | MediaUploader.prototype.send_ = function(content, end) { 221 | self = this 222 | 223 | var xhr = new XMLHttpRequest(); 224 | xhr.open('PUT', this.url, true); 225 | xhr.setRequestHeader('Content-Type', this.contentType); 226 | // xhr.setRequestHeader('Content-Length', this.file.size); 227 | xhr.setRequestHeader('Content-Range', "bytes " + this.offset + "-" + (end - 1) + "/" + this.file.size); 228 | 229 | if (xhr.upload) { 230 | // xhr.upload.addEventListener('progress', this.onProgress); 231 | xhr.upload.addEventListener('progress', function(xmlHttpRequestProgressEvent) { 232 | loaded = self.offset + xmlHttpRequestProgressEvent.loaded; 233 | total = self.file.size; 234 | self.onProgress(loaded, total); 235 | }); 236 | } 237 | xhr.onload = this.onContentUploadSuccess_.bind(this); 238 | xhr.onerror = this.onContentUploadError_.bind(this); 239 | xhr.send(content); 240 | 241 | this.currentXHR = xhr 242 | } 243 | 244 | /** 245 | * Verify for the state of the file for completion. 246 | * Added @ 16 June 2015, .. 247 | * @private 248 | */ 249 | MediaUploader.prototype.verify_ = function() { 250 | var xhr = new XMLHttpRequest(); 251 | xhr.open('PUT', this.url, true); 252 | xhr.setRequestHeader('Content-Length', "0"); 253 | xhr.setRequestHeader('Content-Range', "bytes */*"); 254 | 255 | xhr.onload = function(e) { 256 | if (e.target.status == 200 || e.target.status == 201) { 257 | // console.log('verify success!!'); 258 | } else if (e.target.status == 308) { 259 | // console.log('status 308'); 260 | } 261 | 262 | }; 263 | 264 | xhr.onerror = function(e) { 265 | if (e.target.status && e.target.status < 500) { 266 | // console.log(e.target.response); 267 | } else { 268 | // Do nothing, 269 | } 270 | }; 271 | xhr.send(); 272 | this.currentXHR = xhr 273 | }; 274 | 275 | /** 276 | * Query for the state of the file for resumption. 277 | * 278 | * @private 279 | */ 280 | MediaUploader.prototype.resume_ = function() { 281 | self = this 282 | var xhr = new XMLHttpRequest(); 283 | xhr.open('PUT', this.url, true); 284 | xhr.setRequestHeader('Content-Range', "bytes */" + this.file.size); 285 | xhr.setRequestHeader('X-Upload-Content-Type', this.file.type); 286 | if (xhr.upload) { 287 | // xhr.upload.addEventListener('progress', this.onProgress); 288 | xhr.upload.addEventListener('progress', function(xmlHttpRequestProgressEvent) { 289 | loaded = self.offset + xmlHttpRequestProgressEvent.loaded; 290 | total = self.file.size; 291 | self.onProgress(loaded, total); 292 | }); 293 | } 294 | xhr.onload = this.onContentUploadSuccess_.bind(this); 295 | xhr.onerror = this.onContentUploadError_.bind(this); 296 | xhr.send(); 297 | this.currentXHR = xhr 298 | }; 299 | 300 | /** 301 | * The final step is to call vimeo.videos.upload.complete to queue up 302 | * the video for transcoding. 303 | * 304 | * If successful call 'onComplete' 305 | * 306 | * @private 307 | */ 308 | MediaUploader.prototype.complete_ = function() { 309 | 310 | var xhr = new XMLHttpRequest(); 311 | xhr.open('DELETE', this.complete_url, true); 312 | xhr.setRequestHeader('Authorization', 'Bearer ' + this.token); 313 | 314 | xhr.onload = function(e) { 315 | 316 | // Get the video location (videoId) 317 | if (e.target.status < 400) { 318 | 319 | var location = e.target.getResponseHeader('Location'); 320 | 321 | // Example of location: ' /videos/115365719', extract the video id only 322 | var video_id = location.split('/').pop(); 323 | 324 | this.onComplete(video_id); 325 | 326 | } else { 327 | this.onCompleteError_(e); 328 | } 329 | }.bind(this); 330 | 331 | xhr.onerror = this.onCompleteError_.bind(this); 332 | xhr.send(); 333 | }; 334 | 335 | /** 336 | * Handle successful responses for uploads. Depending on the context, 337 | * may continue with uploading the next chunk of the file or, if complete, 338 | * invokes vimeo complete service. 339 | * 340 | * @private 341 | * @param {object} e XHR event 342 | */ 343 | MediaUploader.prototype.onContentUploadSuccess_ = function(e) { 344 | 345 | if (e.target.status == 200 || e.target.status == 201 || e.target.status == 308) { 346 | this.offset = this.chunkProgress; 347 | 348 | if (this.offset == this.file.size) { 349 | this.complete_(); 350 | } else { 351 | this.retryHandler.reset(); 352 | this.sendFile_(); 353 | } 354 | } 355 | }; 356 | 357 | /** 358 | * Handles errors for uploads. Either retries or aborts depending 359 | * on the error. 360 | * 361 | * @private 362 | * @param {object} e XHR event 363 | */ 364 | MediaUploader.prototype.onContentUploadError_ = function(e) { 365 | console.error("MediaUploader.prototype.onContentUploadError_ e.target.status: ", e.target.status) 366 | if (e.target.status && e.target.status < 500) { 367 | this.onError(e.target.response); 368 | } else { 369 | this.retryHandler.retry(this.resume_.bind(this)); 370 | } 371 | }; 372 | 373 | /** 374 | * Handles errors for the complete request. 375 | * 376 | * @private 377 | * @param {object} e XHR event 378 | */ 379 | MediaUploader.prototype.onCompleteError_ = function(e) { 380 | console.error("MediaUploader.prototype.onCompleteError_ e.target.response: ", e.target.response) 381 | this.onError(e.target.response); // TODO - Retries for initial upload 382 | }; 383 | 384 | /** 385 | * Handles errors for the initial request. 386 | * 387 | * @private 388 | * @param {object} e XHR event 389 | */ 390 | MediaUploader.prototype.onUploadError_ = function(e) { 391 | console.error("MediaUploader.prototype.onUploadError_ e.target.response: ", e.target.response) 392 | this.onError(e.target.response); // TODO - Retries for initial upload 393 | }; 394 | 395 | /** 396 | * Construct a query string from a hash/object 397 | * 398 | * @private 399 | * @param {object} [params] Key/value pairs for query string 400 | * @return {string} query string 401 | */ 402 | MediaUploader.prototype.buildQuery_ = function(params) { 403 | params = params || {}; 404 | return Object.keys(params).map(function(key) { 405 | return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]); 406 | }).join('&'); 407 | }; 408 | 409 | /** 410 | * Build the drive upload URL 411 | * 412 | * @private 413 | * @param {string} [id] File ID if replacing 414 | * @param {object} [params] Query parameters 415 | * @return {string} URL 416 | */ 417 | MediaUploader.prototype.buildUrl_ = function(id, params, baseUrl) { 418 | var url = baseUrl || 'https://api.vimeo.com/me/videos/'; 419 | if (id) { 420 | url += id; 421 | } 422 | var query = this.buildQuery_(params); 423 | if (query) { 424 | url += '?' + query; 425 | } 426 | return url; 427 | }; 428 | 429 | -------------------------------------------------------------------------------- /vimeo-upload.js: -------------------------------------------------------------------------------- 1 | /* 2 | | Vimeo-Upload: Upload videos to your Vimeo account directly from a 3 | | browser or a Node.js app 4 | | 5 | | ╭───╮╭─╮ 6 | | │ ││ │╭─╮╭──┬──┬─╮╭───╮╭───╮ 7 | | │ ││ │├─┤│ ╭╮ ╭╮ ││ ─ ││╭╮ │ ╭────────┬─────────────────────╮ 8 | | ╰╮ ╰╯╭╯│ ││ ││ ││ ││ ─┤│╰╯ │ | UPLOAD │ ▒▒▒▒▒▒▒▒▒▒▒░░░░ %75 | 9 | | ╰────╯ ╰─╯╰─╯╰─╯╰─╯╰───╯╰───╯ ╰────────┴─────────────────────╯ 10 | | 11 | | 12 | | This project was released under Apache 2.0" license. 13 | | 14 | | @link http://websemantics.ca 15 | | @author Web Semantics, Inc. Dev Team 16 | | @author Adnan M.Sagar, PhD. 17 | | @credits Built on cors-upload-sample, https://github.com/googledrive/cors-upload-sample 18 | */ 19 | 20 | ; 21 | (function(root, factory) { 22 | if (typeof define === 'function' && define.amd) { 23 | define([], function() { 24 | return (root.VimeoUpload = factory()) 25 | }) 26 | } else if (typeof module === 'object' && module.exports) { 27 | module.exports = factory() 28 | } else { 29 | root.VimeoUpload = factory() 30 | } 31 | }(this, function() { 32 | 33 | // ------------------------------------------------------------------------- 34 | // RetryHandler Class 35 | 36 | /** 37 | * Helper for implementing retries with backoff. Initial retry 38 | * delay is 1 second, increasing by 2x (+jitter) for subsequent retries 39 | * 40 | * @constructor 41 | */ 42 | var RetryHandler = function() { 43 | this.interval = 1000 // Start at one second 44 | this.maxInterval = 60 * 1000; // Don't wait longer than a minute 45 | } 46 | 47 | /** 48 | * Invoke the function after waiting 49 | * 50 | * @param {function} fn Function to invoke 51 | */ 52 | RetryHandler.prototype.retry = function(fn) { 53 | setTimeout(fn, this.interval) 54 | this.interval = this.nextInterval_() 55 | } 56 | 57 | /** 58 | * Reset the counter (e.g. after successful request) 59 | */ 60 | RetryHandler.prototype.reset = function() { 61 | this.interval = 1000 62 | } 63 | 64 | /** 65 | * Calculate the next wait time. 66 | * @return {number} Next wait interval, in milliseconds 67 | * 68 | * @private 69 | */ 70 | RetryHandler.prototype.nextInterval_ = function() { 71 | var interval = this.interval * 2 + this.getRandomInt_(0, 1000) 72 | return Math.min(interval, this.maxInterval) 73 | } 74 | 75 | /** 76 | * Get a random int in the range of min to max. Used to add jitter to wait times. 77 | * 78 | * @param {number} min Lower bounds 79 | * @param {number} max Upper bounds 80 | * @private 81 | */ 82 | RetryHandler.prototype.getRandomInt_ = function(min, max) { 83 | return Math.floor(Math.random() * (max - min + 1) + min) 84 | } 85 | 86 | // ------------------------------------------------------------------------- 87 | // Private data 88 | 89 | /* Library defaults, can be changed using the 'defaults' member method, 90 | 91 | - api_url (string), vimeo api url 92 | - name (string), default video name 93 | - description (string), default video description 94 | - contentType (string), video content type 95 | - token (string), vimeo api token 96 | - file (object), video file 97 | - metadata (array), data to associate with the video 98 | - upgrade_to_1080 (boolean), set video resolution to high definition 99 | - offset (int), 100 | - chunkSize (int), 101 | - retryHandler (RetryHandler), hanlder class 102 | - onComplete (function), handler for onComplete event 103 | - onProgress (function), handler for onProgress event 104 | - onError (function), handler for onError event 105 | 106 | */ 107 | 108 | var defaults = { 109 | api_url: 'https://api.vimeo.com', 110 | name: 'Default name', 111 | description: 'Default description', 112 | contentType: 'application/octet-stream', 113 | token: null, 114 | file: {}, 115 | metadata: [], 116 | upgrade_to_1080: false, 117 | offset: 0, 118 | chunkSize: 0, 119 | retryHandler: new RetryHandler(), 120 | onComplete: function() {}, 121 | onProgress: function() {}, 122 | onError: function() {} 123 | } 124 | 125 | /** 126 | * Helper class for resumable uploads using XHR/CORS. Can upload any Blob-like item, whether 127 | * files or in-memory constructs. 128 | * 129 | * @example 130 | * var content = new Blob(["Hello world"], {"type": "text/plain"}) 131 | * var uploader = new VimeoUpload({ 132 | * file: content, 133 | * token: accessToken, 134 | * onComplete: function(data) { ... } 135 | * onError: function(data) { ... } 136 | * }) 137 | * uploader.upload() 138 | * 139 | * @constructor 140 | * @param {object} options Hash of options 141 | * @param {string} options.token Access token 142 | * @param {blob} options.file Blob-like item to upload 143 | * @param {string} [options.fileId] ID of file if replacing 144 | * @param {object} [options.params] Additional query parameters 145 | * @param {string} [options.contentType] Content-type, if overriding the type of the blob. 146 | * @param {object} [options.metadata] File metadata 147 | * @param {function} [options.onComplete] Callback for when upload is complete 148 | * @param {function} [options.onProgress] Callback for status for the in-progress upload 149 | * @param {function} [options.onError] Callback if upload fails 150 | */ 151 | var me = function(opts) { 152 | 153 | /* copy user options or use default values */ 154 | for (var i in defaults) { 155 | this[i] = (opts[i] !== undefined) ? opts[i] : defaults[i] 156 | } 157 | 158 | this.contentType = opts.contentType || this.file.type || defaults.contentType 159 | this.httpMethod = opts.fileId ? 'PUT' : 'POST' 160 | 161 | this.videoData = { 162 | name: (opts.name > '') ? opts.name : defaults.name, 163 | description: (opts.description > '') ? opts.description : defaults.description, 164 | 'privacy.view': opts.private ? 'nobody' : 'anybody' 165 | } 166 | 167 | if (!(this.url = opts.url)) { 168 | var params = opts.params || {} /* TODO params.uploadType = 'resumable' */ 169 | this.url = this.buildUrl_(opts.fileId, params, opts.baseUrl) 170 | } 171 | } 172 | 173 | // ------------------------------------------------------------------------- 174 | // Public methods 175 | 176 | /* 177 | Override class defaults 178 | 179 | Parameters: 180 | - opts (object): name value pairs 181 | 182 | */ 183 | 184 | me.prototype.defaults = function(opts) { 185 | return defaults /* TODO $.extend(true, defaults, opts) */ 186 | } 187 | 188 | /** 189 | * Initiate the upload (Get vimeo ticket number and upload url) 190 | */ 191 | me.prototype.upload = function() { 192 | var xhr = new XMLHttpRequest() 193 | xhr.open(this.httpMethod, this.url, true) 194 | xhr.setRequestHeader('Authorization', 'Bearer ' + this.token) 195 | xhr.setRequestHeader('Content-Type', 'application/json') 196 | 197 | xhr.onload = function(e) { 198 | // get vimeo upload url, user (for available quote), ticket id and complete url 199 | if (e.target.status < 400) { 200 | var response = JSON.parse(e.target.responseText) 201 | this.url = response.upload_link_secure 202 | this.user = response.user 203 | this.ticket_id = response.ticket_id 204 | this.complete_url = defaults.api_url + response.complete_uri 205 | this.sendFile_() 206 | } else { 207 | this.onUploadError_(e) 208 | } 209 | }.bind(this) 210 | 211 | xhr.onerror = this.onUploadError_.bind(this) 212 | xhr.send(JSON.stringify({ 213 | type: 'streaming', 214 | upgrade_to_1080: this.upgrade_to_1080 215 | })) 216 | } 217 | 218 | // ------------------------------------------------------------------------- 219 | // Private methods 220 | 221 | /** 222 | * Send the actual file content. 223 | * 224 | * @private 225 | */ 226 | me.prototype.sendFile_ = function() { 227 | var content = this.file 228 | var end = this.file.size 229 | 230 | if (this.offset || this.chunkSize) { 231 | // Only bother to slice the file if we're either resuming or uploading in chunks 232 | if (this.chunkSize) { 233 | end = Math.min(this.offset + this.chunkSize, this.file.size) 234 | } 235 | content = content.slice(this.offset, end) 236 | } 237 | 238 | var xhr = new XMLHttpRequest() 239 | xhr.open('PUT', this.url, true) 240 | xhr.setRequestHeader('Content-Type', this.contentType) 241 | // xhr.setRequestHeader('Content-Length', this.file.size) 242 | xhr.setRequestHeader('Content-Range', 'bytes ' + this.offset + '-' + (end - 1) + '/' + this.file.size) 243 | 244 | if (xhr.upload) { 245 | xhr.upload.addEventListener('progress', this.onProgress) 246 | } 247 | xhr.onload = this.onContentUploadSuccess_.bind(this) 248 | xhr.onerror = this.onContentUploadError_.bind(this) 249 | xhr.send(content) 250 | } 251 | 252 | /** 253 | * Query for the state of the file for resumption. 254 | * 255 | * @private 256 | */ 257 | me.prototype.resume_ = function() { 258 | var xhr = new XMLHttpRequest() 259 | xhr.open('PUT', this.url, true) 260 | xhr.setRequestHeader('Content-Range', 'bytes */' + this.file.size) 261 | xhr.setRequestHeader('X-Upload-Content-Type', this.file.type) 262 | if (xhr.upload) { 263 | xhr.upload.addEventListener('progress', this.onProgress) 264 | } 265 | xhr.onload = this.onContentUploadSuccess_.bind(this) 266 | xhr.onerror = this.onContentUploadError_.bind(this) 267 | xhr.send() 268 | } 269 | 270 | /** 271 | * Extract the last saved range if available in the request. 272 | * 273 | * @param {XMLHttpRequest} xhr Request object 274 | */ 275 | me.prototype.extractRange_ = function(xhr) { 276 | var range = xhr.getResponseHeader('Range') 277 | if (range) { 278 | this.offset = parseInt(range.match(/\d+/g).pop(), 10) + 1 279 | } 280 | } 281 | 282 | /** 283 | * The final step is to call vimeo.videos.upload.complete to queue up 284 | * the video for transcoding. 285 | * 286 | * If successful call 'onUpdateVideoData_' 287 | * 288 | * @private 289 | */ 290 | me.prototype.complete_ = function(xhr) { 291 | var xhr = new XMLHttpRequest() 292 | xhr.open('DELETE', this.complete_url, true) 293 | xhr.setRequestHeader('Authorization', 'Bearer ' + this.token) 294 | 295 | xhr.onload = function(e) { 296 | 297 | // Get the video location (videoId) 298 | if (e.target.status < 400) { 299 | var location = e.target.getResponseHeader('Location') 300 | 301 | // Example of location: ' /videos/115365719', extract the video id only 302 | var video_id = location.split('/').pop() 303 | // Update the video metadata 304 | this.onUpdateVideoData_(video_id) 305 | } else { 306 | this.onCompleteError_(e) 307 | } 308 | }.bind(this) 309 | 310 | xhr.onerror = this.onCompleteError_.bind(this) 311 | xhr.send() 312 | } 313 | 314 | /** 315 | * Update the Video Data and add the metadata to the upload object 316 | * 317 | * @private 318 | * @param {string} [id] Video Id 319 | */ 320 | me.prototype.onUpdateVideoData_ = function(video_id) { 321 | var url = this.buildUrl_(video_id, [], defaults.api_url + '/videos/') 322 | var httpMethod = 'PATCH' 323 | var xhr = new XMLHttpRequest() 324 | 325 | xhr.open(httpMethod, url, true) 326 | xhr.setRequestHeader('Authorization', 'Bearer ' + this.token) 327 | xhr.onload = function(e) { 328 | // add the metadata 329 | this.onGetMetadata_(e, video_id) 330 | }.bind(this) 331 | xhr.send(this.buildQuery_(this.videoData)) 332 | } 333 | 334 | /** 335 | * Retrieve the metadata from a successful onUpdateVideoData_ response 336 | * This is is useful when uploading unlisted videos as the URI has changed. 337 | * 338 | * If successful call 'onUpdateVideoData_' 339 | * 340 | * @private 341 | * @param {object} e XHR event 342 | * @param {string} [id] Video Id 343 | */ 344 | me.prototype.onGetMetadata_ = function(e, video_id) { 345 | // Get the video location (videoId) 346 | if (e.target.status < 400) { 347 | if (e.target.response) { 348 | // add the returned metadata to the metadata array 349 | var meta = JSON.parse(e.target.response) 350 | // get the new index of the item 351 | var index = this.metadata.push(meta) - 1 352 | // call the complete method 353 | this.onComplete(video_id, index) 354 | } else { 355 | this.onCompleteError_(e) 356 | } 357 | } 358 | } 359 | 360 | /** 361 | * Handle successful responses for uploads. Depending on the context, 362 | * may continue with uploading the next chunk of the file or, if complete, 363 | * invokes vimeo complete service. 364 | * 365 | * @private 366 | * @param {object} e XHR event 367 | */ 368 | me.prototype.onContentUploadSuccess_ = function(e) { 369 | if (e.target.status == 200 || e.target.status == 201) { 370 | this.complete_() 371 | } else if (e.target.status == 308) { 372 | this.extractRange_(e.target) 373 | this.retryHandler.reset() 374 | this.sendFile_() 375 | } 376 | } 377 | 378 | /** 379 | * Handles errors for uploads. Either retries or aborts depending 380 | * on the error. 381 | * 382 | * @private 383 | * @param {object} e XHR event 384 | */ 385 | me.prototype.onContentUploadError_ = function(e) { 386 | if (e.target.status && e.target.status < 500) { 387 | this.onError(e.target.response) 388 | } else { 389 | this.retryHandler.retry(this.resume_()) 390 | } 391 | } 392 | 393 | /** 394 | * Handles errors for the complete request. 395 | * 396 | * @private 397 | * @param {object} e XHR event 398 | */ 399 | me.prototype.onCompleteError_ = function(e) { 400 | this.onError(e.target.response); // TODO - Retries for initial upload 401 | } 402 | 403 | /** 404 | * Handles errors for the initial request. 405 | * 406 | * @private 407 | * @param {object} e XHR event 408 | */ 409 | me.prototype.onUploadError_ = function(e) { 410 | this.onError(e.target.response); // TODO - Retries for initial upload 411 | } 412 | 413 | /** 414 | * Construct a query string from a hash/object 415 | * 416 | * @private 417 | * @param {object} [params] Key/value pairs for query string 418 | * @return {string} query string 419 | */ 420 | me.prototype.buildQuery_ = function(params) { 421 | params = params || {} 422 | return Object.keys(params).map(function(key) { 423 | return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]) 424 | }).join('&') 425 | } 426 | 427 | /** 428 | * Build the drive upload URL 429 | * 430 | * @private 431 | * @param {string} [id] File ID if replacing 432 | * @param {object} [params] Query parameters 433 | * @return {string} URL 434 | */ 435 | me.prototype.buildUrl_ = function(id, params, baseUrl) { 436 | var url = baseUrl || defaults.api_url + '/me/videos' 437 | if (id) { 438 | url += id 439 | } 440 | var query = this.buildQuery_(params) 441 | if (query) { 442 | url += '?' + query 443 | } 444 | return url 445 | } 446 | 447 | return me 448 | })) 449 | --------------------------------------------------------------------------------