├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── index.js ├── package.json └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.mp4 2 | *.log 3 | .DS_Store 4 | node_modules/ 5 | test/*.json 6 | secrets.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/secrets.json 2 | *.mp4 3 | node_modules/ 4 | *.log 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Luther Blissett 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-youtube-resumable-upload 2 | ============================= 3 | 4 | Upload large videos to youtube (and others) via Google's 'resumable upload' API, following https://developers.google.com/youtube/v3/guides/using_resumable_upload_protocol 5 | 6 | Benchmarked with an 800mb video - this module bypasses the filesize restrictions on node's `fs.readFileSync()` (used by the official googleapis node client for uploading) by using `fs.createReadStream()` and then piping the stream to Youtube's servers. 7 | 8 | 9 | How to Use 10 | ========== 11 | 12 | Requires OAuth2 tokens from google - packages such as `googleapis` (the official nodejs client) and `Passport` will do the trick. 13 | 14 | If you need a more robust CLI OAuth2 solution, I suggest trying [martinheidegger/google-cli-auth](https://github.com/martinheidegger/google-cli-auth) instead of the `google-auth-cli` that is used in this library's test file. 15 | 16 | Install with `npm install node-youtube-resumable-upload` 17 | 18 | The module emits the video metadata that Google responds with on a successful upload, or emits an error if something goes wrong. If monitoring is enabled (it is by default), the number of bytes uploaded is emitted every 5 seconds. 19 | 20 | Look at test/test.js for a use-case example, but this is the gist of it: 21 | 22 | ```javascript 23 | var ResumableUpload = require('node-youtube-resumable-upload'); 24 | var resumableUpload = new ResumableUpload(); //create new ResumableUpload 25 | resumableUpload.tokens = tokens; //Google OAuth2 tokens 26 | resumableUpload.filepath = './video.mp4'; 27 | resumableUpload.metadata = metadata; //include the snippet and status for the video 28 | resumableUpload.retry = 3; // Maximum retries when upload failed. 29 | resumableUpload.upload(); 30 | resumeableUpload.on('progress', function(progress) { 31 | console.log(progress); 32 | }); 33 | resumableUpload.on('success', function(success) { 34 | console.log(success); 35 | }); 36 | resumableUpload.on('error', function(error) { 37 | console.log(error); 38 | }); 39 | ``` 40 | 41 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var request = require('request'); 3 | var EventEmitter = require('events').EventEmitter; 4 | var mime = require('mime'); 5 | var util = require('util'); 6 | 7 | function resumableUpload() { 8 | this.byteCount = 0; //init variables 9 | this.tokens = {}; 10 | this.filepath = ''; 11 | this.metadata = {}; 12 | this.retry = -1; 13 | this.host = 'www.googleapis.com'; 14 | this.api = '/upload/youtube/v3/videos'; 15 | }; 16 | 17 | util.inherits(resumableUpload, EventEmitter); 18 | 19 | //Init the upload by POSTing google for an upload URL (saved to self.location) 20 | resumableUpload.prototype.upload = function() { 21 | var self = this; 22 | var options = { 23 | url: 'https://' + self.host + self.api + '?uploadType=resumable&part=snippet,status,contentDetails', 24 | headers: { 25 | 'Host': self.host, 26 | 'Authorization': 'Bearer ' + self.tokens.access_token, 27 | 'Content-Length': new Buffer(JSON.stringify(self.metadata)).length, 28 | 'Content-Type': 'application/json', 29 | 'X-Upload-Content-Length': fs.statSync(self.filepath).size, 30 | 'X-Upload-Content-Type': mime.lookup(self.filepath) 31 | }, 32 | body: JSON.stringify(self.metadata) 33 | }; 34 | //Send request and start upload if success 35 | request.post(options, function(err, res, body) { 36 | if (err || !res.headers.location) { 37 | self.emit('error', new Error(err)); 38 | self.emit('progress', 'Retrying ...'); 39 | if ((self.retry > 0) || (self.retry <= -1)) { 40 | self.retry--; 41 | self.upload(); // retry 42 | } else { 43 | return; 44 | } 45 | } 46 | self.location = res.headers.location; 47 | self.send(); 48 | }); 49 | } 50 | 51 | //Pipes uploadPipe to self.location (Google's Location header) 52 | resumableUpload.prototype.send = function() { 53 | var self = this; 54 | var options = { 55 | url: self.location, //self.location becomes the Google-provided URL to PUT to 56 | headers: { 57 | 'Authorization': 'Bearer ' + self.tokens.access_token, 58 | 'Content-Length': fs.statSync(self.filepath).size - self.byteCount, 59 | 'Content-Type': mime.lookup(self.filepath) 60 | } 61 | }; 62 | try { 63 | //creates file stream, pipes it to self.location 64 | var uploadPipe = fs.createReadStream(self.filepath, { 65 | start: self.byteCount, 66 | end: fs.statSync(self.filepath).size 67 | }); 68 | } catch (e) { 69 | self.emit('error', new Error(e)); 70 | return; 71 | } 72 | var health = setInterval(function(){ 73 | self.getProgress(function(err, res, body) { 74 | if (!err && typeof res.headers.range !== 'undefined') { 75 | self.emit('progress', res.headers.range.substring(8)); 76 | } 77 | }); 78 | }, 5000); 79 | uploadPipe.pipe(request.put(options, function(error, response, body) { 80 | clearInterval(health); 81 | if (!error) { 82 | self.emit('success', body); 83 | return; 84 | } 85 | self.emit('error', new Error(error)); 86 | if ((self.retry > 0) || (self.retry <= -1)) { 87 | self.retry--; 88 | self.getProgress(function(err, res, b) { 89 | if (typeof res.headers.range !== 'undefined') { 90 | self.byteCount = res.headers.range.substring(8); //parse response 91 | } else { 92 | self.byteCount = 0; 93 | } 94 | self.send(); 95 | }); 96 | } 97 | })); 98 | } 99 | 100 | resumableUpload.prototype.getProgress = function(handler) { 101 | var self = this; 102 | var options = { 103 | url: self.location, 104 | headers: { 105 | 'Authorization': 'Bearer ' + self.tokens.access_token, 106 | 'Content-Length': 0, 107 | 'Content-Range': 'bytes */' + fs.statSync(self.filepath).size 108 | } 109 | }; 110 | request.put(options, handler); 111 | } 112 | 113 | module.exports = resumableUpload; 114 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-youtube-resumable-upload", 3 | "version": "0.2.1", 4 | "description": "Create resumable uploads via googleapis", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Gray Leonard", 10 | "license": "ISC", 11 | "dependencies": { 12 | "google-auth-cli": "0.0.1", 13 | "mime": "^1.2.11", 14 | "request": "2.44.0" 15 | }, 16 | "directories": { 17 | "test": "test" 18 | }, 19 | "devDependencies": {}, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/grayleonard/node-youtube-resumable-upload.git" 23 | }, 24 | "keywords": [ 25 | "youtube", 26 | "google", 27 | "resumable", 28 | "upload", 29 | "googleapis" 30 | ], 31 | "bugs": { 32 | "url": "https://github.com/grayleonard/node-youtube-resumable-upload/issues" 33 | }, 34 | "homepage": "https://github.com/grayleonard/node-youtube-resumable-upload" 35 | } 36 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var googleauth = require('google-auth-cli'); 2 | var ResumableUpload = require('../index.js'); 3 | var google_secrets = require('./secrets.json'); 4 | 5 | var tokens; 6 | 7 | var upload = function() { 8 | var metadata = {snippet: { title: 'New Upload', description: 'Uploaded with ResumableUpload' }, 9 | status: { privacyStatus: 'private' }}; 10 | var resumableUpload = new ResumableUpload(); //create new ResumableUpload 11 | resumableUpload.tokens = tokens; 12 | resumableUpload.filepath = 'thescore.mp4'; 13 | resumableUpload.metadata = metadata; 14 | resumableUpload.monitor = true; 15 | resumableUpload.retry = -1; //infinite retries, change to desired amount 16 | resumableUpload.upload(); 17 | resumableUpload.on('progress', function(progress) { 18 | console.log(progress); 19 | }); 20 | resumableUpload.on('error', function(error) { 21 | console.log(error); 22 | }); 23 | resumableUpload.on('success', function(success) { 24 | console.log(success); 25 | }); 26 | } 27 | 28 | var getTokens = function(callback) { 29 | googleauth({ 30 | access_type: 'offline', 31 | scope: 'https://www.googleapis.com/auth/youtube.upload' //can do just 'youtube', but 'youtube.upload' is more restrictive 32 | }, 33 | { client_id: google_secrets.client_id, //replace with your client_id and _secret 34 | client_secret: google_secrets.client_secret, 35 | timeout: 60 * 60 * 1000, // This allows uploads to take up to an hour 36 | port: 3000 37 | }, 38 | function(err, authClient, tokens) { 39 | console.log(tokens); 40 | callback(tokens); 41 | return; 42 | }); 43 | }; 44 | 45 | getTokens(function(result) { 46 | console.log('tokens:' + result); 47 | tokens = result; 48 | upload(); 49 | return; 50 | }); 51 | --------------------------------------------------------------------------------