├── .bithoundrc ├── .gitignore ├── .travis.yml ├── README.md ├── UNLICENSE ├── deprecated.js ├── package.json ├── test.js └── youtube-feeds.js /.bithoundrc: -------------------------------------------------------------------------------- 1 | { 2 | "critics": { 3 | "lint": { 4 | "engine": "eslint" 5 | } 6 | }, 7 | "ignore": [ 8 | "example.js" 9 | ], 10 | "test": [ 11 | "test.js" 12 | ] 13 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | .*_history 5 | npm-debug.log* 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directories 31 | node_modules 32 | jspm_packages 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 'iojs-2.0' 5 | - 'iojs-1.8' 6 | - 'iojs-1.7' 7 | - '0.12' 8 | - '0.11' 9 | - '0.10' 10 | - '0.8' 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | youtube-feeds 2 | ============= 3 | 4 | 5 | Access public YouTube API feeds from your Node.js apps 6 | 7 | [![Build Status](https://travis-ci.org/fvdm/nodejs-youtube.svg?branch=master)](https://travis-ci.org/fvdm/nodejs-youtube) 8 | 9 | 10 | DEPRECATED 11 | ---------- 12 | 13 | This module relies heavily on YouTube Data API v2 which was deprecated on **April 20th 2015** 14 | to be replaced by version 3 of their API. The code is not being maintained anymore. 15 | 16 | It is recommended for all users to switch to a YouTube module with API v3 support. 17 | 18 | 19 | **See the Wiki for a working example:** 20 | 21 | 22 | Installation 23 | ------------ 24 | 25 | Stable: `npm install youtube-feeds` 26 | 27 | Develop: `npm install fvdm/youtube-feeds#develop` 28 | 29 | 30 | Usage 31 | ----- 32 | 33 | ```js 34 | // load the module 35 | var youtube = require('youtube-feeds') 36 | 37 | // search parkour videos 38 | youtube.feeds.videos( {q: 'parkour'}, console.log ) 39 | ``` 40 | 41 | 42 | Configuration 43 | ------------- 44 | 45 | param | type | default | description 46 | -------------|---------|---------|------------ 47 | httpProtocol | string | http | Which HTTP protocol to use 48 | timeout | integer | 30000 | Request wait timeout in ms 49 | developerKey | string | | Your YouTube [developer key](http://code.google.com/apis/youtube/dashboard/) 50 | 51 | 52 | > `developerKey` is required for some methods, ie. [user.activity](#useractivity). 53 | > You can also temporarily override the global setting with the `key` property in a method's vars. 54 | 55 | 56 | ### Example: 57 | 58 | ```js 59 | var youtube = require ('youtube-feeds'); 60 | youtube.httpProtocol = 'https'; 61 | youtube.feeds.videos ({q: 'keywords'}, callback); 62 | ``` 63 | 64 | 65 | Callbacks 66 | --------- 67 | 68 | Each method takes a `callback` function as last parameter. When everything seems alright `err` is null, otherwise `err` will be `instanceof Error` for tracing. 69 | 70 | ```js 71 | function (err, data) { 72 | if (err) { 73 | console.log (err); 74 | } else { 75 | console.log (data); 76 | } 77 | } 78 | ``` 79 | 80 | #### Error Properties 81 | 82 | property | type | description 83 | ------------|--------|---------------------------------- 84 | err.message | string | the error message 85 | err.stack | string | stack trace 86 | err.origin | string | Context; api, method, request 87 | err.details | mixed | API response or other information 88 | 89 | 90 | #### Error Messages 91 | 92 | message | origin | description 93 | ----------------------|---------|------------------------------------------------ 94 | invalid response | api | API response can't be parsed 95 | not json | api | Expected JSON, received something else 96 | not found | method | Requested data was not found 97 | not allowed | method | No permission to requested data 98 | invalid id | method | Requested video ID is invalid 99 | connection closed | api | Connection dropped early 100 | connection error | request | Can't connect to API 101 | request timeout | request | The request took too long to connect or process 102 | error | api | API returned an error, see err.details 103 | developer key missing | api | developerKey is not set, see Configuration. 104 | 105 | 106 | ================================================================================= 107 | 108 | 109 | Feeds 110 | ----- 111 | 112 | Retrieve lists, search videos, related material. 113 | 114 | 115 | feeds.videos 116 | ------------ 117 | ### ( [vars], callback ) 118 | 119 | Get a list of recently published or updated videos, or search them all, filter, sort, etc. 120 | 121 | [API docs: custom query parameters](https://developers.google.com/youtube/2.0/developers_guide_protocol_api_query_parameters#Custom_parameters) 122 | 123 | ```js 124 | youtube.feeds.videos ( 125 | { 126 | q: 'parkour', 127 | 'max-results': 2, 128 | orderby: 'published' 129 | }, 130 | console.log 131 | ); 132 | ``` 133 | 134 | Output: 135 | 136 | ```js 137 | { updated: '2012-06-18T17:55:11.294Z', 138 | totalItems: 6985, 139 | startIndex: 1, 140 | itemsPerPage: 2, 141 | items: 142 | [ { id: 'WEeqHj3Nj2c', 143 | uploaded: '2006-06-08T01:17:06.000Z', 144 | updated: '2012-06-18T15:53:06.000Z', 145 | uploader: 'sauloca', 146 | category: 'Sports', 147 | title: 'Parkour and FreeRunning', 148 | description: 'Edited by: Saulo Sampson Chase [..]', 149 | tags: 150 | [ 'le', 151 | 'parkour', 152 | 'free', 153 | 'running' ], 154 | thumbnail: 155 | { sqDefault: 'http://i.ytimg.com/vi/WEeqHj3Nj2c/default.jpg', 156 | hqDefault: 'http://i.ytimg.com/vi/WEeqHj3Nj2c/hqdefault.jpg' }, 157 | player: 158 | { default: 'https://www.youtube.com/watch?v=WEeqHj3Nj2c&feature=youtube_gdata_player', 159 | mobile: 'https://m.youtube.com/details?v=WEeqHj3Nj2c' }, 160 | content: 161 | { '1': 'rtsp://v8.cache8.c.youtube.com/CiILENy73wIaGQlnj809HqpHWBMYDSANFEgGUgZ2aWRlb3MM/0/0/0/video.3gp', 162 | '5': 'https://www.youtube.com/v/WEeqHj3Nj2c?version=3&f=videos&app=youtube_gdata', 163 | '6': 'rtsp://v8.cache8.c.youtube.com/CiILENy73wIaGQlnj809HqpHWBMYESARFEgGUgZ2aWRlb3MM/0/0/0/video.3gp' }, 164 | duration: 218, 165 | geoCoordinates: { latitude: -7.100892543792725, longitude: -34.91455078125 }, 166 | rating: 4.862864, 167 | likeCount: '85314', 168 | ratingCount: 88343, 169 | viewCount: 32718590, 170 | favoriteCount: 99541, 171 | accessControl: 172 | { comment: 'denied', 173 | commentVote: 'allowed', 174 | videoRespond: 'moderated', 175 | rate: 'allowed', 176 | embed: 'allowed', 177 | list: 'allowed', 178 | autoPlay: 'allowed', 179 | syndicate: 'allowed' } } ] } 180 | ``` 181 | 182 | 183 | feeds.related 184 | ------------- 185 | ### ( videoid, [vars], callback ) 186 | 187 | Get related videos for a video with **videoid**. 188 | 189 | 190 | feeds.responses 191 | --------------- 192 | ### ( videoid, [vars], callback ) 193 | 194 | Get videos in response to **videoid**. 195 | 196 | 197 | feeds.comments 198 | -------------- 199 | ### ( videoid, [vars], callback ) 200 | 201 | Get comments to a video. This is still in the original XML-to-JSON format as YouTube does not have JSON-C available for this feed. This may change in future (major) versions of this module. 202 | 203 | 204 | feeds.standard 205 | -------------- 206 | ### ( feed, [vars], callback ) 207 | 208 | Get a standard feed, such as most viewed or top rated videos. Worldwide, local or by subject (or a combination). 209 | 210 | [API docs: Standard feeds](https://developers.google.com/youtube/2.0/reference#Standard_feeds) 211 | 212 | 213 | **Example:** most recent videos worldwide: 214 | 215 | ```js 216 | youtube.feeds.standard ('most_recent', console.log); 217 | ``` 218 | 219 | 220 | **Example:** today's top-rated News videos in the Netherlands: 221 | 222 | ```js 223 | youtube.feeds.standard ('NL/top_rated_News', {time: 'today'}, console.log); 224 | ``` 225 | 226 | 227 | feeds.playlist 228 | -------------- 229 | ### ( playlistid, [vars], callback ) 230 | 231 | Get videos on a certain playlist. 232 | 233 | 234 | ======================================================================= 235 | 236 | 237 | Video 238 | ----- 239 | 240 | The **video** function provides shorthand methods for one specific video. 241 | 242 | 243 | video 244 | ----- 245 | ### ( videoid, [callback] ) 246 | 247 | Same as [video.details](#videodetails) 248 | 249 | ```js 250 | youtube.video ('ern37eWDnT0', console.log); 251 | ``` 252 | 253 | 254 | video.details 255 | ------------- 256 | ### ( callback ) 257 | 258 | Get details for one video. 259 | 260 | ```js 261 | youtube.video ('ern37eWDnT0').details (console.log); 262 | ``` 263 | 264 | 265 | video.related 266 | ------------- 267 | ### ( [vars], callback ) 268 | 269 | Get related videos, same as [feeds.related](#feedsrelated). 270 | 271 | ```js 272 | youtube.video ('ern37eWDnT0').related ({'max-results': 2}, console.log); 273 | ``` 274 | 275 | 276 | video.responses 277 | --------------- 278 | ### ( [vars], callback ) 279 | 280 | Get videos in response to one video, same as [feeds.responses](#feedsresponses). 281 | 282 | ```js 283 | youtube.video ('ern37eWDnT0').responses ({'max-results': 2}, console.log); 284 | ``` 285 | 286 | 287 | videos.comments 288 | --------------- 289 | ### ( [vars], callback ) 290 | 291 | Get comments to a video, same as [feeds.comments](#feedscomments). 292 | 293 | ```js 294 | youtube.video ('ern37eWDnT0').comments ({'max-results': 2}, console.log); 295 | ``` 296 | 297 | 298 | ======================================================================= 299 | 300 | 301 | User 302 | ---- 303 | 304 | Get (public) feed data for one specific user. 305 | 306 | 307 | user 308 | ---- 309 | ### ( userid, [callback] ) 310 | 311 | Same as [user.profile](#userprofile). 312 | 313 | ```js 314 | youtube.user ('user', console.log); 315 | ``` 316 | 317 | 318 | user.profile 319 | ------------ 320 | ### ( callback ) 321 | 322 | Get user profile, in old XML-to-JSON style. 323 | 324 | ```js 325 | youtube.user ('user').profile (console.log); 326 | ``` 327 | 328 | 329 | user.favorites 330 | -------------- 331 | ### ( [vars], callback ) 332 | 333 | Get the user's favorite videos. You can optionally filter the results like the other feeds. 334 | 335 | ```js 336 | youtube.user ('user').favorites (console.log); 337 | ``` 338 | 339 | 340 | user.playlists 341 | -------------- 342 | ### ( [vars], callback ) 343 | 344 | Get user playlists. Use **[feeds.playlist](#feedsplaylist)** to get the videos. 345 | 346 | 347 | user.uploads 348 | ------------ 349 | ### ( [vars], callback ) 350 | 351 | Get the user's uploaded videos. 352 | 353 | ```js 354 | youtube.user ('user').uploads (console.log); 355 | ``` 356 | 357 | 358 | ======================================================================= 359 | 360 | 361 | talk 362 | ---- 363 | ### ( path, [fields], callback, [oldJsonKey] ) 364 | 365 | Directly talk to the API. This function takes care of connecting and calling the callback only when valid JSON is returned. 366 | 367 | 368 | Param | Type | Description 369 | -----------|----------|------------------------------------------------ 370 | path | string | full method path without leading slash 371 | fields | object | GET parameters 372 | callback | function | callback function to receive results 373 | oldJsonKey | boolean | force old XML-to-JSON format instead of clean JSON-C its value is the key containing the expected results 374 | 375 | 376 | Unlicense / Public Domain 377 | ------------------------- 378 | 379 | This is free and unencumbered software released into the public domain. 380 | 381 | Anyone is free to copy, modify, publish, use, compile, sell, or 382 | distribute this software, either in source code form or as a compiled 383 | binary, for any purpose, commercial or non-commercial, and by any 384 | means. 385 | 386 | In jurisdictions that recognize copyright laws, the author or authors 387 | of this software dedicate any and all copyright interest in the 388 | software to the public domain. We make this dedication for the benefit 389 | of the public at large and to the detriment of our heirs and 390 | successors. We intend this dedication to be an overt act of 391 | relinquishment in perpetuity of all present and future rights to this 392 | software under copyright law. 393 | 394 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 395 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 396 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 397 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 398 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 399 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 400 | OTHER DEALINGS IN THE SOFTWARE. 401 | 402 | For more information, please refer to 403 | 404 | 405 | Author 406 | ------ 407 | 408 | [Franklin van de Meent](https://frankl.in) 409 | 410 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /deprecated.js: -------------------------------------------------------------------------------- 1 | console.log ('\u001b[31mDEPRECATED\u001b[0m - youtube-feeds is deprecated, use googleapis instead'); 2 | console.log ('\u001b[31mDEPRECATED\u001b[0m - Example at https://github.com/fvdm/nodejs-youtube/wiki'); 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "name": "Franklin van de Meent", 4 | "email": "fr@nkl.in", 5 | "url": "http://frankl.in" 6 | }, 7 | "name": "youtube-feeds", 8 | "description": "Deprecated, use googleapis instead. Access public YouTube Data API v2 feeds, mostly with the clean JSON-C results", 9 | "version": "2.4.1", 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/fvdm/nodejs-youtube.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/fvdm/nodejs-youtube/issues" 16 | }, 17 | "main": "youtube-feeds.js", 18 | "dependencies": { 19 | "node-xml2json": "1.x.x" 20 | }, 21 | "devDependencies": {}, 22 | "optionalDependencies": {}, 23 | "engines": { 24 | "node": ">=0.8.28" 25 | }, 26 | "scripts": { 27 | "preinstall": "node deprecated.js", 28 | "test": "node test.js" 29 | }, 30 | "keywords": [ 31 | "youtube", 32 | "youtubev2", 33 | "video", 34 | "feeds", 35 | "api" 36 | ], 37 | "license": "Unlicense" 38 | } 39 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Name: youtube-feeds 3 | Description: Test script for youtube-feeds.js 4 | Source: https://github.com/fvdm/nodejs-youtube 5 | Feedback: https://github.com/fvdm/nodejs-youtube/issues 6 | License: Public Domain / Unlicense (see UNLICENSE file) 7 | */ 8 | 9 | var util = require ('util'); 10 | 11 | // Setup 12 | var app = require ('./'); 13 | app.httpProtocol = process.env.YOUTUBE_PROTOCOL || 'http'; 14 | app.timeout = process.env.YOUTUBE_TIMEOUT || 5000; 15 | app.developerKey = process.env.YOUTUBE_KEY || null; 16 | 17 | 18 | // handle exits 19 | var errors = 0; 20 | process.on ('exit', function () { 21 | if (errors === 0) { 22 | console.log ('\n\033[1mDONE, no errors.\033[0m\n'); 23 | process.exit (0); 24 | } else { 25 | console.log ('\n\033[1mFAIL, '+ errors +' error'+ (errors > 1 ? 's' : '') +' occurred!\033[0m\n'); 26 | process.exit (1); 27 | } 28 | }); 29 | 30 | // prevent errors from killing the process 31 | process.on ('uncaughtException', function (err) { 32 | console.log (); 33 | console.error (err.stack); 34 | console.trace (); 35 | console.log (); 36 | errors++; 37 | }); 38 | 39 | // Queue to prevent flooding 40 | var queue = []; 41 | var next = 0; 42 | 43 | function doNext () { 44 | next++; 45 | if (queue[next]) { 46 | queue[next] (); 47 | } 48 | } 49 | 50 | // doTest( passErr, 'methods', [ 51 | // ['feeds', typeof feeds === 'object'] 52 | // ]) 53 | function doTest (err, label, tests) { 54 | if (err instanceof Error) { 55 | console.error (label +': \033[1m\033[31mERROR\033[0m\n'); 56 | console.error (util.inspect (err, {depth: 10, colors: true})); 57 | console.log (); 58 | console.error (err.stack); 59 | console.log (); 60 | errors++; 61 | } else { 62 | var testErrors = []; 63 | tests.forEach (function (test) { 64 | if (test[1] !== true) { 65 | testErrors.push (test[0]); 66 | errors++; 67 | } 68 | }); 69 | 70 | if (testErrors.length === 0) { 71 | console.log (label +': \033[1m\033[32mok\033[0m'); 72 | } else { 73 | console.error (label +': \033[1m\033[31mfailed\033[0m ('+ testErrors.join (', ') +')'); 74 | } 75 | } 76 | 77 | doNext (); 78 | } 79 | 80 | 81 | queue.push (function () { 82 | app.feeds.videos ({q: 'ShouldNotExists_'+ Date.now()}, function (err, data) { 83 | doTest (null, 'Error: not found', [ 84 | ['type', err && err instanceof Error], 85 | ['message', err && err.message === 'not found'], 86 | ['data', data && data.totalItems === 0] 87 | ]); 88 | }); 89 | }); 90 | 91 | queue.push (function () { 92 | app.video (0, function (err) { 93 | doTest (null, 'Error: invalid id', [ 94 | ['type', err && err instanceof Error], 95 | ['message', err && err.message === 'invalid id'] 96 | ]); 97 | }); 98 | }); 99 | 100 | queue.push (function () { 101 | app.feeds.videos ({q: 'cheetah'}, function (err, data) { 102 | doTest (err, 'feeds.videos', [ 103 | ['type', data instanceof Object], 104 | ['prop', typeof data.itemsPerPage === 'number'] 105 | ]); 106 | }); 107 | }); 108 | 109 | 110 | // Start the tests 111 | console.log ('Running tests...\n'); 112 | queue[0] (); 113 | -------------------------------------------------------------------------------- /youtube-feeds.js: -------------------------------------------------------------------------------- 1 | /* 2 | Name: youtube-feeds 3 | Description: Node.js module to access public YouTube data feeds. 4 | Source: https://github.com/fvdm/nodejs-youtube 5 | Feedback: https://github.com/fvdm/nodejs-youtube/issues 6 | License: Unlicense / Public Domain 7 | (see UNLICENSE file) 8 | */ 9 | 10 | var xml2json = require ('node-xml2json'); 11 | var querystring = require ('querystring'); 12 | 13 | var app = { 14 | httpProtocol: 'http', // http, https 15 | timeout: 5000, // max execution time in milliseconds 16 | developerKey: null // YouTube developer key 17 | }; 18 | 19 | 20 | /////////// 21 | // FEEDS // 22 | /////////// 23 | 24 | app.feeds = { 25 | // Videos 26 | videos: function (vars, cb) { 27 | app.talk ('feeds/api/videos', vars, cb); 28 | }, 29 | 30 | // Related videos 31 | related: function (videoid, vars, cb) { 32 | app.talk ('feeds/api/videos/'+ videoid +'/related', vars, cb); 33 | }, 34 | 35 | // Responses 36 | responses: function (videoid, vars, cb) { 37 | app.talk ('feeds/api/videos/'+ videoid +'/responses', vars, cb); 38 | }, 39 | 40 | // Comments 41 | comments: function (videoid, vars, cb) { 42 | app.talk ('feeds/api/videos/'+ videoid +'/comments', vars, cb, 'feed'); 43 | }, 44 | 45 | // Standard feed 46 | // https://developers.google.com/youtube/2.0/reference#Standard_feeds 47 | // feeds.standard ('most_recent', console.log) 48 | // feeds.standard ('NL/top_rated_News', {time: 'today'}, console.log) 49 | standard: function (feed, vars, cb) { 50 | app.talk ('feeds/api/standardfeeds/'+ feed, vars, cb); 51 | }, 52 | 53 | // Playlist 54 | playlist: function (playlistid, vars, cb) { 55 | app.talk ('feeds/api/playlists/'+ playlistid, vars, cb); 56 | } 57 | }; 58 | 59 | 60 | /////////// 61 | // VIDEO // 62 | /////////// 63 | 64 | app.video = function (videoid, cb) { 65 | if (typeof cb === 'function') { 66 | app.talk ('feeds/api/videos/'+ videoid, cb); 67 | } 68 | 69 | // video shortcuts 70 | return { 71 | details: function (cb) { 72 | app.video (videoid, cb); 73 | }, 74 | 75 | related: function (vars, cb) { 76 | app.feeds.related (videoid, vars, cb); 77 | }, 78 | 79 | responses: function (vars, cb) { 80 | app.feeds.responses (videoid, vars, cb); 81 | }, 82 | 83 | comments: function (vars, cb) { 84 | app.feeds.comments (videoid, vars, cb); 85 | } 86 | }; 87 | }; 88 | 89 | 90 | ////////// 91 | // USER // 92 | ////////// 93 | 94 | // User 95 | app.user = function (userid, cb) { 96 | if (cb && typeof cb === 'function') { 97 | app.user (userid).profile (cb); 98 | } 99 | 100 | return { 101 | // Favorites 102 | favorites: function (vars, cb) { 103 | app.talk ('feeds/api/users/'+ userid +'/favorites', vars, cb); 104 | }, 105 | 106 | // Playlists 107 | playlists: function (vars, cb) { 108 | app.talk ('feeds/api/users/'+ userid +'/playlists', vars, cb); 109 | }, 110 | 111 | // Profile 112 | profile: function (cb) { 113 | app.talk ('feeds/api/users/'+ userid, {}, cb, 'entry'); 114 | }, 115 | 116 | // Uploads 117 | uploads: function (vars, cb) { 118 | app.talk ('feeds/api/users/'+ userid +'/uploads', vars, cb); 119 | }, 120 | 121 | // New subscription videos 122 | newsubscriptionvideos: function (vars, cb) { 123 | app.talk ('feeds/api/users/'+ userid +'/newsubscriptionvideos', vars, cb); 124 | } 125 | }; 126 | }; 127 | 128 | 129 | ///////////////// 130 | // COMMUNICATE // 131 | ///////////////// 132 | 133 | app.talk = function (path, fields, cb, oldJsonKey) { 134 | var complete = false; 135 | 136 | // fix callback 137 | if (!cb && typeof fields === 'function') { 138 | cb = fields; 139 | fields = {}; 140 | } 141 | 142 | // fix fields 143 | if (!fields || typeof fields !== 'object') { 144 | fields = {}; 145 | } 146 | 147 | // force JSON-C and version 148 | fields.alt = oldJsonKey ? 'json' : 'jsonc'; 149 | fields.v = 2; 150 | 151 | // prepare 152 | var options = { 153 | hostname: 'gdata.youtube.com', 154 | path: '/'+ path +'?'+ querystring.stringify (fields), 155 | headers: { 156 | 'User-Agent': 'youtube-feeds.js (https://github.com/fvdm/nodejs-youtube)', 157 | 'Accept': 'application/json', 158 | 'GData-Version': '2' 159 | }, 160 | method: 'GET' 161 | }; 162 | 163 | // use X-GData-Key instead of adding it to the url, as per http://goo.gl/HEiCj 164 | // basically more secure in headers than in query string 165 | if (fields.key || app.developerKey) { 166 | options.headers['X-GData-Key'] = 'key=' + (fields.key || app.developerKey); 167 | delete fields.key; 168 | } 169 | 170 | var http = require ('http'); 171 | if (app.httpProtocol === 'https') { 172 | http = require ('https'); 173 | } 174 | 175 | var request = http.request (options); 176 | 177 | // response 178 | request.on ('response', function (response) { 179 | var data = []; 180 | var size = 0; 181 | 182 | response.on ('data', function (chunk) { 183 | data.push (chunk); 184 | size += chunk.length; 185 | }); 186 | 187 | response.on ('end', function () { 188 | 189 | if (complete) { 190 | return; 191 | } else { 192 | complete = true; 193 | } 194 | 195 | // process buffer and clear mem 196 | data = new Buffer.concat (data, size).toString ('utf8').trim (); 197 | var error = null; 198 | 199 | // validate 200 | if (data.match (/^(\{.*\}|\[.*\])$/)) { 201 | // ok 202 | data = JSON.parse (data); 203 | 204 | if (data.data) { 205 | data = data.data; 206 | } else if (data.error) { 207 | complete = true; 208 | error = new Error ('error'); 209 | error.origin = 'api'; 210 | error.details = data.error; 211 | } else if (oldJsonKey) { 212 | if (!data[oldJsonKey]) { 213 | complete = true; 214 | error = new Error ('invalid response'); 215 | error.origin = 'api'; 216 | } else { 217 | data = data[oldJsonKey]; 218 | } 219 | } 220 | } else if (data.match (/^$/) || data.match (/^<\?xml version.+<\/errors>$/)) { 221 | data = xml2json.parser (data); 222 | 223 | // fix for JSONC compatibility 224 | complete = true; 225 | error = new Error ('error'); 226 | error.origin = 'api'; 227 | error.details = data.errors.error ? [data.errors.error] : data.errors; 228 | 229 | error.details.forEach (function (err, errk) { 230 | if (err.internalreason) { 231 | error.details[errk].internalReason = err.internalreason; 232 | delete error.details[errk].internalreason; 233 | } 234 | }); 235 | } else if (data.match (/

Error /)) { 236 | // html error response 237 | complete = true; 238 | error = new Error ('error'); 239 | data.replace (/

([^<]+)<\/H1>\n

Error (\d+)<\/H2>/, function (s, reason, code) { 240 | error.origin = 'api'; 241 | error.details = { 242 | internalReason: reason, 243 | code: code 244 | }; 245 | }); 246 | } else { 247 | // not json 248 | complete = true; 249 | error = new Error ('not json'); 250 | error.origin = 'api'; 251 | } 252 | 253 | // parse error 254 | if (error && error.origin === 'api' && error.message === 'error') { 255 | var errorDetails = error.details; 256 | if (error.details[0] && error.details[0].code && error.details[0].code === 'ResourceNotFoundException') { 257 | complete = true; 258 | error = new Error ('not found'); 259 | error.origin = 'method'; 260 | error.details = errorDetails; 261 | } else if (error.details.code === 403) { 262 | complete = true; 263 | error = new Error ('not allowed'); 264 | error.origin = 'method'; 265 | error.details = errorDetails; 266 | } else if (error.details.message === 'Invalid id') { 267 | complete = true; 268 | error = new Error ('invalid id'); 269 | error.origin = 'method'; 270 | error.details = errorDetails; 271 | } else if (error.details[0] && error.details[0].internalReason === 'Developer key required for this operation') { 272 | complete = true; 273 | error = new Error ('developer key missing'); 274 | error.origin = 'api'; 275 | error.details = errorDetails; 276 | } 277 | } 278 | 279 | // parse response 280 | if (typeof data.totalItems !== 'undefined' && data.totalItems === 0) { 281 | complete = true; 282 | error = new Error ('not found'); 283 | error.origin = 'method'; 284 | } else if (data.feed && data.feed.openSearch$totalResults && data.feed.openSearch$totalResults.$t && data.feed.openSearch$totalResults.$t === 0) { 285 | complete = true; 286 | error = new Error ('not found'); 287 | error.origin = 'method'; 288 | } 289 | 290 | // do callback 291 | cb (error, data); 292 | }); 293 | 294 | // early disconnect 295 | response.on ('close', function () { 296 | if (!complete) { 297 | complete = true; 298 | var err = new Error ('connection closed'); 299 | err.origin = 'api'; 300 | cb (err); 301 | } 302 | }); 303 | }); 304 | 305 | // no endless waiting 306 | request.setTimeout (parseInt (app.timeout), function () { 307 | if (!complete) { 308 | complete = true; 309 | var err = new Error ('request timeout'); 310 | err.origin = 'request'; 311 | cb (err); 312 | request.destroy (); 313 | } 314 | }); 315 | 316 | // connection error 317 | request.on ('error', function (error) { 318 | if (!complete) { 319 | complete = true; 320 | var err = new Error ('connection error'); 321 | err.origin = 'request'; 322 | err.details = error; 323 | cb (err); 324 | } 325 | }); 326 | 327 | // perform and finish request 328 | request.end(); 329 | }; 330 | 331 | // ready 332 | module.exports = app; 333 | --------------------------------------------------------------------------------