├── .gitignore ├── README.md ├── index.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A simple command line program to subscribe to youtube channels and 2 | view thumbnail links of videos in a locally generated html page. No 3 | youtube/google account needed. 4 | 5 |

6 | 7 | 8 | 9 |

10 | 11 |

12 | 13 |

14 | 15 | # Install 16 | 17 | 1. Have [Node.js](https://nodejs.org/en/) installed (any version 18 | higher than 6 should be fine) 19 | 2. Run command **`npm install -g vidlist@latest`** and you are all set 20 | + To update run `npm update -g vidlist` 21 | + To uninstall, run `npm uninstall -g vidlist` 22 | + _Unix may need `sudo` before commands_ 23 | + _Windows may need running as admistrator_ 24 | 25 | # Use 26 | 27 | + Run and see `vl --help` 28 | + **Note**: The program will run the same with the names: `vidlist`, `vl` or `sub` 29 | 30 | # Common Usages Example 31 | 32 | **Subscribe to a youtube channel:** 33 | 34 | `vl https://www.youtube.com/watch?v=EeNiqKNtpAA` 35 | 36 | or 37 | 38 | `vl -s https://www.youtube.com/watch?v=EeNiqKNtpAA` 39 | 40 | **Pull update from channel feed, show update progress, generate HTML and then open the HTML with your default browser:** 41 | 42 | `vl -upgo` 43 | 44 | **Generated HTML use** 45 | 46 | + Clicking on thumbnail takes to full width video embed link 47 | + Clicking on video title takes to normal youtube video view link 48 | + Hovering over channel name shows time since video published 49 | + Hovering over video title shows video description text 50 | + Channel list and links will be on the bottom of the page 51 | 52 | **Want to start with my recommended list of channels ?** 53 | 54 | 1. Save [this file](https://raw.githubusercontent.com/dxwc/subscribe/files/subscriptions.json) somewhere on your computer. 55 | 2. Run: `vl --import ` 56 | 57 | # Features 58 | 59 | + Doesn't need any google account/API key 60 | + Latest videos are always on top 61 | + Everything runs and kept locally, no background processing 62 | + Does not require frontend javascript 63 | + Thumbnail image lazy-loading after first 10 64 | + Basic validation and XSS protection are in place 65 | + It is easy to see where in the `index.js` to edit to change CSS to one's liking 66 | + Can easily import or export subscription list as text (JSON) file 67 | 68 | # Changelog 69 | + 2.0.0 70 | + Fixed broken subscription option after youtube update 71 | + Updated packages 72 | + 1.1.0 73 | + Added thumbnail lazy-loading after first 10 74 | + 1.0.2 75 | + YouTube watch/user/channel/embed/shortcut URL are all correctly supported for 76 | subscribing 77 | + Better channel removal confirmation 78 | + 1.0.1 79 | + Dependency package versions updated 80 | + Progress text change bug fixed 81 | + 1.0.0 82 | + Identical to previous version 0.1.2, now following npm semantic 83 | versioning 84 | + 0.1.2 85 | + Fix bug that was causing channel name mismatch since 0.1.0 86 | + 0.1.1 87 | + --one multiple update status counter fix with -p 88 | + 0.1.0 89 | + Print status text on individual subscription 90 | + Open option will not wait for browser to close after opening the html 91 | on non-windows platforms if the open option was invoked when the 92 | default browser was not open already 93 | + Fixed channel name printing on console containing html entities 94 | + Added option to fetch updates from specific channel/s at a time 95 | + Fixed description text not being displayed properly on special characters 96 | + Fixed a minor wrongly ordered set of operations during subscribing 97 | + 0.0.9 98 | + Fixed html not opening in windows issue 99 | + 0.0.8 100 | + CSS changed 101 | + Channel name is now part of video tile instead of hover 102 | + Channel name hover displays relative time since video published 103 | + Fixed channel name escaping issue 104 | + Thumbnail embed link autoplays on click 105 | + Uses youtube.com instead of youtube-nocookie.com for embed link 106 | + 0.0.7 107 | + Prevent potential running of 'open' command until browser closes 108 | + 0.0.6 109 | + Minor progress/help texts fixes 110 | 111 | ---- 112 | 113 | This software was not produced by or directly for YouTube, LLC and has no 114 | affiliation with the LLC. Use this software only at your own volition. 115 | 116 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 117 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 118 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 119 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 120 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 121 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 122 | SOFTWARE. 123 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const https = require('https'); 6 | 7 | const opn = require('open'); 8 | const moment = require('moment'); 9 | const sqlite3 = require('sqlite3'); 10 | const entities = require('entities'); 11 | const validator = require('validator'); 12 | const Getopt = require('node-getopt'); 13 | const xss = require('xss-filters'); 14 | const vd = require('vid_data'); 15 | 16 | global.old_video_limit_sec = 15*24*60*60; // 15 days 17 | 18 | global.dot = path.join(require('os').homedir(), '.vidlist'); 19 | global.html = path.join(require('os').tmpdir() , 'view_subscriptions.html'); 20 | 21 | try 22 | { 23 | if(!fs.statSync(global.dot).isDirectory()) 24 | { 25 | console.error 26 | ( 27 | `=> Error:\nCan not create a directory as there is an ` + 28 | `existing file with the same name ( ${global.dot} ). ` + 29 | `Remove/rename the file and then re-run to continue` 30 | ); 31 | process.exit(1); 32 | } 33 | } 34 | catch(err) 35 | { 36 | if(err.code === 'ENOENT') 37 | { 38 | try 39 | { 40 | fs.mkdirSync(global.dot); 41 | } 42 | catch(err) 43 | { 44 | console.error(`=> Error creating directory ${global.dot}`); 45 | console.error(err); 46 | throw err; 47 | } 48 | } 49 | else 50 | { 51 | console.error('=> Unhandled Error\n', err); 52 | throw err; 53 | } 54 | } 55 | 56 | global.previous_length = 0; 57 | 58 | function stdout_write(text) 59 | { 60 | process.stdout.write 61 | ( 62 | ' '.repeat(global.previous_length ? previous_length : text.length) + '\r' 63 | ); 64 | 65 | global.previous_length = text.length; 66 | process.stdout.write(text + '\r'); 67 | } 68 | 69 | function clean_say_dont_replace(text) 70 | { 71 | process.stdout.write 72 | ( 73 | ' '.repeat(global.previous_length ? previous_length : text.length) + '\r' 74 | ); 75 | 76 | console.info(text); 77 | } 78 | 79 | /** 80 | * Asynchronously downloads content given https link 81 | * @param {String} link Full https youtube url 82 | * @returns {Promise} Resolved parameter is the downloaded content 83 | */ 84 | function download_page(link) 85 | { 86 | return new Promise((resolve, reject) => 87 | { 88 | let data = ''; 89 | https.get(link, (res) => 90 | { 91 | res.on('data', (chunk) => data += chunk); 92 | res.on('end', () => resolve(data)); 93 | res.on('error', (err) => reject(err)); 94 | }) 95 | .on('error', (err) => reject(err)); 96 | }); 97 | } 98 | 99 | /** 100 | * Run a sql command to sqlite3 101 | * @param {String} command SQL command 102 | * @returns {Promise} 103 | */ 104 | function sql_promise(command) 105 | { 106 | return new Promise((resolve, reject) => 107 | { 108 | db.run 109 | ( 110 | command, 111 | (err) => 112 | { 113 | if(err) return reject(err); 114 | else return resolve(); 115 | } 116 | ); 117 | }); 118 | } 119 | 120 | /** 121 | * Open database for use, create tables and indexes if opening for the first time 122 | * @returns {Promise} 123 | */ 124 | function open_db_global() 125 | { 126 | return new Promise((resolve, reject) => 127 | { 128 | global.db = new sqlite3.Database 129 | ( 130 | path.join(global.dot, 'subscription_data.db'), 131 | (err) => 132 | { 133 | if(err) return reject(err); 134 | sql_promise 135 | ( 136 | ` 137 | CREATE TABLE IF NOT EXISTS subscriptions 138 | ( 139 | channel_id_id INTEGER PRIMARY KEY AUTOINCREMENT, 140 | channel_id TEXT UNIQUE NOT NULL, 141 | channel_name TEXT 142 | ); 143 | ` 144 | ) 145 | .then(() => 146 | { 147 | return sql_promise 148 | ( 149 | ` 150 | CREATE TABLE IF NOT EXISTS videos 151 | ( 152 | channel_id_id INTEGER REFERENCES 153 | subscriptions(channel_id_id), 154 | video_id TEXT PRIMARY KEY, 155 | video_title TEXT, 156 | video_published INTEGER, 157 | video_description TEXT 158 | ); 159 | ` 160 | ); 161 | }) 162 | .then(() => 163 | { 164 | return sql_promise 165 | ( 166 | ` 167 | CREATE INDEX IF NOT EXISTS video_published_i 168 | ON videos(video_published DESC); 169 | ` 170 | ); 171 | }) 172 | .then(() => 173 | { 174 | return sql_promise 175 | ( 176 | ` 177 | CREATE UNIQUE INDEX IF NOT EXISTS channel_id_i 178 | ON subscriptions(channel_id); 179 | ` 180 | ); 181 | }) 182 | .then(() => 183 | { 184 | return resolve(); 185 | }) 186 | .catch((err) => 187 | { 188 | return reject(err); 189 | }) 190 | } 191 | ); 192 | }); 193 | } 194 | 195 | /** 196 | * Used by subscribe() to insert an subscription 197 | * @param {String} ch_id Channel ID 198 | * @param {String} ch_name Channel name 199 | * @returns {Promise} 200 | */ 201 | function insert_for_subscribe(ch_id, ch_name) 202 | { 203 | return new Promise((resolve, reject) => 204 | { 205 | db.run 206 | ( 207 | ` 208 | INSERT INTO subscriptions 209 | (channel_id, channel_name) 210 | VALUES 211 | ('${ch_id}', '${ch_name}'); 212 | `, 213 | (result) => 214 | { 215 | if(result && result.errno) 216 | { 217 | if(result.errno == 19) 218 | console.info( 219 | `You were already subscribed to '${entities.decodeHTML 220 | (validator.unescape 221 | (ch_name))}' (${ch_id})`); 222 | else 223 | { 224 | return reject(result); 225 | } 226 | } 227 | else if(result === null) 228 | { 229 | console.info(`Subscribed to '${entities.decodeHTML 230 | (validator.unescape 231 | (ch_name))}' (${ch_id})`); 232 | return resolve(); 233 | } 234 | else 235 | { 236 | return reject('Undefined error'); 237 | } 238 | } 239 | ); 240 | }); 241 | } 242 | 243 | /** 244 | * Verify url, download content, parse channel ID and name and then insert an entry 245 | * to subscriptions table of sqlite3 db 246 | * @param {String} url A acceptable youtube url 247 | */ 248 | function subscribe(url) 249 | { 250 | stdout_write(': Attempting to get channel data...'); 251 | 252 | return vd.get_channel_id_and_name(url) 253 | .then((res) => 254 | { 255 | // console.log(res, res=== null); 256 | if(res === null || !res.channel_id || !res.channel_name) 257 | return clean_say_dont_replace 258 | ('Failed. Ensure URL is valid, else file a bug report'); 259 | else 260 | stdout_write(': Data extraction successful. Saving locally...'); 261 | res.channel_name = validator.escape(res.channel_name); 262 | return insert_a_subscription(res.channel_id, res.channel_name); 263 | }) 264 | .catch((err) => 265 | { 266 | clean_say_dont_replace('Error:\n' + String(err)); 267 | }); 268 | } 269 | 270 | /** 271 | * Deletes video entries from videos db whose publish date are older than the limit 272 | * set by global.old_video_limit_sec variable 273 | * @returns {Promise} 274 | */ 275 | function keep_db_shorter() 276 | { 277 | return new Promise((resolve, reject) => 278 | { 279 | db.run 280 | ( 281 | ` 282 | DELETE 283 | FROM videos 284 | WHERE 285 | video_published < ${ 286 | (new Date().getTime()/1000) - global.old_video_limit_sec}`, 287 | (err) => 288 | { 289 | if(err) return reject(err); 290 | else return resolve(); 291 | } 292 | ); 293 | }); 294 | } 295 | 296 | /** 297 | * Prints subscriptions list to stdout 298 | * @param {Boolean} names_only To display only channel names 299 | */ 300 | function list_subscriptions(names_only) 301 | { 302 | return new Promise((resolve, reject) => 303 | { 304 | db.all 305 | ( 306 | ` 307 | SELECT * FROM subscriptions 308 | `, 309 | (err, rows) => 310 | { 311 | if(err) return reject(err); 312 | if(names_only) 313 | { 314 | 315 | for(let i = 0; i < rows.length; ++i) 316 | { 317 | console.info 318 | ( 319 | String(rows[i].channel_id_id) + '.', 320 | entities.decodeHTML 321 | ( 322 | validator.unescape(rows[i].channel_name) 323 | ) 324 | ); 325 | } 326 | } 327 | else 328 | { 329 | for(let i = 0; i < rows.length; ++i) 330 | { 331 | console.info 332 | ( 333 | rows[i].channel_id, 334 | entities.decodeHTML 335 | ( 336 | validator.unescape(rows[i].channel_name) 337 | ) 338 | ); 339 | } 340 | } 341 | return resolve(); 342 | } 343 | ); 344 | }); 345 | } 346 | 347 | /** 348 | * Insert entries into videos table in sqlite3 db 349 | * @param {String} values String must be formatted as sql values data 350 | */ 351 | function insert_entries(values) 352 | { 353 | return new Promise((resolve, reject) => 354 | { 355 | if(values.length === 0) return resolve(); 356 | 357 | db.run 358 | ( 359 | ` 360 | INSERT OR REPLACE INTO videos 361 | ( 362 | channel_id_id, 363 | video_id, 364 | video_title, 365 | video_published, 366 | video_description 367 | ) 368 | VALUES 369 | ${values} 370 | `, 371 | (err) => 372 | { 373 | if(err && err.errno !== 19) return reject(err); 374 | else return resolve(); 375 | } 376 | ); 377 | }); 378 | } 379 | 380 | /** 381 | * Extract and gather video id, title, published and description information 382 | * @param {String} page Downloaded rss content of a youtube channel 383 | * @param {String} ch_id_id Associated db PK id 384 | * @returns {Promise} Resolve indicates both parsing and saving data has been 385 | * completed 386 | */ 387 | function parse_and_save_data(page, ch_id_id) 388 | { 389 | let v_id_pre = -1; 390 | let v_id_post = -1; 391 | let v_title_pre = -1; 392 | let v_title_post = -1; 393 | let v_published_pre = -1; 394 | let v_published_post = -1; 395 | let v_description_pre = -1; 396 | let v_description_post = -1; 397 | 398 | let a_id; 399 | let a_title; 400 | let a_pubDate; 401 | let a_description; 402 | 403 | let values = ''; 404 | 405 | return new Promise((resolve, reject) => 406 | { 407 | while(page.indexOf('') !== -1) 408 | { 409 | page = page.substring(page.indexOf('')-1); 410 | 411 | v_id_pre = page.indexOf(''); 412 | v_id_post = page.indexOf(''); 413 | v_title_pre = page.indexOf(''); 414 | v_title_post = page.indexOf(''); 415 | v_published_pre = page.indexOf(''); 416 | v_published_post = page.indexOf(''); 417 | v_description_pre = page.indexOf(''); 418 | v_description_post = page.indexOf(''); 419 | 420 | if 421 | ( 422 | v_id_pre === -1 || 423 | v_id_post === -1 || 424 | v_title_pre === -1 || 425 | v_title_post === -1 || 426 | v_published_pre === -1 || 427 | v_published_post === -1 || 428 | v_description_pre === -1 || 429 | v_description_post === -1 430 | ) 431 | { 432 | return reject('tagname/s under entry not found'); 433 | break; 434 | } 435 | 436 | a_title = page.substring(v_title_pre+7, v_title_post); 437 | a_id = page.substring(v_id_pre+12, v_id_post); 438 | a_pubDate = new Date 439 | ( 440 | page.substring(v_published_pre+11, v_published_post) 441 | ).getTime()/1000; 442 | a_description = page.substring(v_description_pre+19, v_description_post); 443 | 444 | a_title = validator.escape(a_title); 445 | 446 | if(!validator.whitelist( 447 | a_id.toLowerCase(), 'abcdefghijklmnopqrstuvwxyz1234567890_-')) 448 | { 449 | return reject('Extracted id is not of the expected form'); 450 | break; 451 | } 452 | 453 | a_description = validator.escape(a_description); 454 | 455 | if(page.indexOf('') == -1) 456 | { 457 | return reject(' not found'); 458 | break; 459 | } 460 | 461 | page = page.substring(page.indexOf('')); 462 | 463 | if(a_pubDate >= (new Date().getTime()/1000) - global.old_video_limit_sec) 464 | { 465 | values += `${values.length ? ',' : ''} 466 | (${ch_id_id}, '${a_id}', '${a_title}', ${a_pubDate}, '${a_description}')`; 467 | } 468 | } 469 | 470 | return insert_entries(values) 471 | .then(() => 472 | { 473 | return resolve(); 474 | }); 475 | }); 476 | } 477 | 478 | /** Used by process_one(...) to display prcessing progress */ 479 | global.remaining = 0; 480 | 481 | /** 482 | * Downloads content of a channel's rss feed and pass data to parse_and_save_data() 483 | * @param {Number} channel_id_id The database PK from db of the channel to process 484 | * @param {String} channel_id The channel ID 485 | */ 486 | function process_one(channel_id_id, channel_id) 487 | { 488 | return Promise.resolve() 489 | .then(() => 490 | { 491 | if(global.prog) 492 | stdout_write 493 | ( 494 | `: ${global.remaining} channel's download and processing remaining` 495 | ); 496 | 497 | return true; 498 | }) 499 | .then(() => 500 | { 501 | return download_page 502 | ( 503 | `https://www.youtube.com/feeds/videos.xml?channel_id=${channel_id}` 504 | ); 505 | }) 506 | .then((page) => 507 | { 508 | return parse_and_save_data(page, channel_id_id); 509 | }) 510 | .then(() => 511 | { 512 | global.remaining -= 1; 513 | }); 514 | } 515 | 516 | /** 517 | * Upadate videos data by parsing and processing all channel's rss feed 518 | * @returns {Promise} A promise chain representing all processing for update 519 | */ 520 | function download_and_save_feed() 521 | { 522 | return new Promise((resolve, reject) => 523 | { 524 | db.all 525 | ( 526 | `SELECT channel_id_id, channel_id FROM subscriptions`, 527 | (err, rows) => 528 | { 529 | if(err) return reject(err); 530 | else 531 | { 532 | if(global.prog) 533 | stdout_write(`Initiating downloader and processor`); 534 | 535 | let all_downloads = Promise.resolve(); 536 | global.remaining = rows.length; 537 | 538 | for(let i = 0; i < rows.length; ++i) 539 | { 540 | if(i + 5 < rows.length) 541 | { 542 | let k = i; 543 | all_downloads = all_downloads 544 | .then(() => 545 | { 546 | return Promise.all 547 | ( 548 | [ 549 | process_one(rows[k+5].channel_id_id, rows[k+5].channel_id), 550 | process_one(rows[k+4].channel_id_id, rows[k+4].channel_id), 551 | process_one(rows[k+3].channel_id_id, rows[k+3].channel_id), 552 | process_one(rows[k+2].channel_id_id, rows[k+2].channel_id), 553 | process_one(rows[k+1].channel_id_id, rows[k+1].channel_id), 554 | process_one(rows[k].channel_id_id, rows[k].channel_id) 555 | ] 556 | ); 557 | }); 558 | 559 | i += 5; 560 | } 561 | else if(i + 4 < rows.length) 562 | { 563 | let k = i; 564 | all_downloads = all_downloads 565 | .then(() => 566 | { 567 | return Promise.all 568 | ( 569 | [ 570 | process_one(rows[k+4].channel_id_id, rows[k+4].channel_id), 571 | process_one(rows[k+3].channel_id_id, rows[k+3].channel_id), 572 | process_one(rows[k+2].channel_id_id, rows[k+2].channel_id), 573 | process_one(rows[k+1].channel_id_id, rows[k+1].channel_id), 574 | process_one(rows[k].channel_id_id, rows[k].channel_id) 575 | ] 576 | ); 577 | }); 578 | 579 | i += 4; 580 | } 581 | else if(i + 3 < rows.length) 582 | { 583 | let k = i; 584 | all_downloads = all_downloads 585 | .then(() => 586 | { 587 | return Promise.all 588 | ( 589 | [ 590 | process_one(rows[k+3].channel_id_id, rows[k+3].channel_id), 591 | process_one(rows[k+2].channel_id_id, rows[k+2].channel_id), 592 | process_one(rows[k+1].channel_id_id, rows[k+1].channel_id), 593 | process_one(rows[k].channel_id_id, rows[k].channel_id) 594 | ] 595 | ); 596 | }); 597 | 598 | i += 3; 599 | } 600 | else if(i + 2 < rows.length) 601 | { 602 | let k = i; 603 | all_downloads = all_downloads 604 | .then(() => 605 | { 606 | return Promise.all 607 | ( 608 | [ 609 | process_one(rows[k+2].channel_id_id, rows[k+2].channel_id), 610 | process_one(rows[k+1].channel_id_id, rows[k+1].channel_id), 611 | process_one(rows[k].channel_id_id, rows[k].channel_id) 612 | ] 613 | ); 614 | }); 615 | 616 | i += 2; 617 | } 618 | else if(i + 1 < rows.length) 619 | { 620 | let k = i; 621 | all_downloads = all_downloads 622 | .then(() => 623 | { 624 | return Promise.all 625 | ( 626 | [ 627 | process_one 628 | ( 629 | rows[k].channel_id_id, 630 | rows[k].channel_id 631 | ), 632 | process_one 633 | ( 634 | rows[k+1].channel_id_id, 635 | rows[k+1].channel_id 636 | ) 637 | ] 638 | ); 639 | }); 640 | 641 | i += 1; 642 | } 643 | else 644 | { 645 | all_downloads = all_downloads 646 | .then(() => 647 | { 648 | return process_one 649 | ( 650 | rows[i].channel_id_id, 651 | rows[i].channel_id 652 | ); 653 | }); 654 | } 655 | } 656 | return resolve(all_downloads); 657 | } 658 | } 659 | ); 660 | }); 661 | } 662 | 663 | /** 664 | * Upadate videos data by parsing and processing chosen channel's rss feed 665 | * @returns {Promise} Resolves when update completes 666 | */ 667 | function download_and_save_one_feed(num) 668 | { 669 | return Promise.resolve() 670 | .then(() => 671 | { 672 | if(typeof num === 'string' && validator.isInt(num)) return Number(num); 673 | return list_subscriptions(true) 674 | .then(() => 675 | { 676 | return new Promise((resolve, reject) => 677 | { 678 | require('readline').createInterface 679 | ({ 680 | input: process.stdin, 681 | output: process.stdout 682 | }) 683 | .question 684 | ( 685 | 'Enter the channel number you wish to update: ', 686 | (answer) => 687 | { 688 | if(validator.isInt(answer)) return resolve(Number(answer)); 689 | else return reject 690 | ('Invalid answer, must be one of the number shown'); 691 | } 692 | ); 693 | }) 694 | }); 695 | }) 696 | .then((channel_number) => 697 | { 698 | return new Promise((resolve, reject) => 699 | { 700 | db.all 701 | ( 702 | ` 703 | SELECT 704 | channel_id_id, 705 | channel_id 706 | FROM 707 | subscriptions 708 | WHERE 709 | channel_id_id=${channel_number}`, 710 | (err, rows) => 711 | { 712 | if(err) return reject(err); 713 | else 714 | { 715 | if(global.prog) 716 | stdout_write(`Initiating downloader and processor`); 717 | 718 | if(rows.length) 719 | process_one(rows[0].channel_id_id, rows[0].channel_id) 720 | .then(() => 721 | { 722 | return resolve(); 723 | }); 724 | else 725 | { 726 | console.error 727 | ( 728 | '=> No channel found associated with given number,' + 729 | ' SKIPPING', 730 | channel_number 731 | ); 732 | return resolve(); 733 | } 734 | } 735 | } 736 | ); 737 | }); 738 | }); 739 | } 740 | 741 | /** 742 | * @returns {Promise} All video data as well as channel id and title 743 | */ 744 | function get_video_data() 745 | { 746 | return new Promise((resolve, reject) => 747 | { 748 | db.all 749 | ( 750 | ` 751 | SELECT 752 | channel_id, 753 | channel_name, 754 | video_id, 755 | video_title, 756 | video_published, 757 | video_description 758 | FROM 759 | subscriptions 760 | INNER JOIN 761 | (SELECT * FROM videos ORDER BY video_published DESC) vi 762 | ON subscriptions.channel_id_id = vi.channel_id_id 763 | `, 764 | (err, rows) => 765 | { 766 | if(err) return reject(err); 767 | return resolve(rows); 768 | } 769 | ); 770 | }); 771 | } 772 | 773 | /** 774 | * @returns {Promise} Returns all channel_id and channel_name 775 | */ 776 | function get_channel_data() 777 | { 778 | return new Promise((resolve, reject) => 779 | { 780 | db.all 781 | ( 782 | ` 783 | SELECT 784 | channel_id, 785 | channel_name 786 | FROM 787 | subscriptions 788 | `, 789 | (err, rows) => 790 | { 791 | if(err) return reject(err); 792 | return resolve(rows); 793 | } 794 | ); 795 | }); 796 | } 797 | 798 | /** 799 | * Generates html 800 | * @returns {Promise} 801 | */ 802 | function generate_html() 803 | { 804 | return Promise.all([get_video_data(), get_channel_data()]) 805 | .then((result) => 806 | { 807 | 808 | let full = 809 | ` 810 | 811 | 812 | 813 | Subscriptions 814 | 931 | 932 | 933 | 968 | `; 969 | 970 | full += `
` 971 | for (let i = 0; i < result[0].length; ++i) { 972 | full += ` 973 |
974 | 976 | 978 | 979 |

\ 986 | ${xss.inHTMLData(validator.unescape(result[0][i].channel_name))}

987 | 989 |

\ 993 | ${xss.inHTMLData(validator.unescape(result[0][i].video_title))}

994 |
995 |
`; 996 | } 997 | 998 | full += `
` 999 | 1000 | full += 1001 | ` 1002 |
1003 |

Channels

1004 |
1005 | 1018 |
1019 |
1020 | 1022 | 1023 | 1024 | `; 1025 | fs.writeFileSync(global.html, full); 1026 | return true; 1027 | }); 1028 | } 1029 | 1030 | /** 1031 | * Promt to enter integer, if there is a matching ch_ch_id, then delete 1032 | * @returns {Promise} 1033 | */ 1034 | function remove_subscription() 1035 | { 1036 | return list_subscriptions(true) 1037 | .then(() => 1038 | { 1039 | return new Promise((resolve, reject) => 1040 | { 1041 | let rl = require('readline').createInterface 1042 | ({ 1043 | input: process.stdin, 1044 | output: process.stdout 1045 | }); 1046 | 1047 | rl.question('Enter the channel number you wish to remove: ', (answer) => 1048 | { 1049 | let channel_number; 1050 | if(validator.isInt(answer)) 1051 | { 1052 | channel_number = Number(answer); 1053 | let channel_name; 1054 | let channel_id; 1055 | return resolve 1056 | ( 1057 | new Promise((reso, reje) => 1058 | { 1059 | db.get 1060 | ( 1061 | `SELECT channel_name, channel_id 1062 | FROM subscriptions 1063 | WHERE channel_id_id=${channel_number}`, 1064 | (err, row) => 1065 | { 1066 | if(row && row.channel_name && row.channel_id) 1067 | { 1068 | channel_name = row.channel_name; 1069 | channel_id = row.channel_id; 1070 | 1071 | } 1072 | return reso(); 1073 | } 1074 | ); 1075 | }) 1076 | .then(() => 1077 | { 1078 | return sql_promise 1079 | ( 1080 | ` 1081 | DELETE FROM videos 1082 | WHERE 1083 | channel_id_id=${channel_number} 1084 | ` 1085 | ); 1086 | }) 1087 | .then(() => 1088 | { 1089 | return sql_promise 1090 | ( 1091 | ` 1092 | DELETE FROM subscriptions 1093 | WHERE 1094 | channel_id_id=${channel_number} 1095 | ` 1096 | ); 1097 | }) 1098 | .then(() => 1099 | { 1100 | if(!channel_id) 1101 | clean_say_dont_replace(`--Invalid selection`); 1102 | else clean_say_dont_replace 1103 | ( 1104 | `--Channel '${channel_name}'(${channel_id}) ` + 1105 | `has been removed` 1106 | ); 1107 | }) 1108 | ); 1109 | } 1110 | else return reject('--Invalid input, not an integer'); 1111 | }); 1112 | }); 1113 | }); 1114 | } 1115 | 1116 | /** 1117 | * Close db and exit 1118 | * @param {Number} code Exit code 1119 | */ 1120 | function close_everything(code) 1121 | { 1122 | return new Promise((resolve, reject) => 1123 | { 1124 | // TODO: need global ? 1125 | db.close((err) => 1126 | { 1127 | if(err) { console.error('=> Error:\n', err); process.exit(1) } 1128 | else return resolve(); 1129 | }); 1130 | }) 1131 | .then(() => 1132 | { 1133 | process.exit(code); 1134 | }); 1135 | } 1136 | 1137 | /** 1138 | * Convert subscription list (name and id) into JSON and save at export_file location 1139 | * @returns {Promise} 1140 | */ 1141 | function export_subscription_list() 1142 | { 1143 | return new Promise((resolve, reject) => 1144 | { 1145 | db.all 1146 | ( 1147 | `SELECT * FROM subscriptions`, 1148 | (err, rows) => 1149 | { 1150 | if(err) return reject(err); 1151 | 1152 | 1153 | let subs = []; 1154 | for(let i = 0; i < rows.length; ++i) 1155 | { 1156 | subs.push 1157 | ( 1158 | [ 1159 | rows[i].channel_id, 1160 | validator.unescape(rows[i].channel_name) 1161 | ] 1162 | ); 1163 | } 1164 | 1165 | let export_file = path.join(global.dot, 'subscriptions.json'); 1166 | fs.writeFileSync 1167 | ( 1168 | export_file, 1169 | JSON.stringify(subs) 1170 | ); 1171 | 1172 | console.info(`--Exported ${export_file}`); 1173 | 1174 | return resolve(); 1175 | } 1176 | ); 1177 | }); 1178 | } 1179 | 1180 | /** 1181 | * Add a subscription given channel ID and channel name 1182 | * @param {String} ch_id 1183 | * @param {String} ch_name 1184 | */ 1185 | function insert_a_subscription(ch_id, ch_name) 1186 | { 1187 | return new Promise((resolve, reject) => 1188 | { 1189 | db.run 1190 | ( 1191 | ` 1192 | INSERT INTO subscriptions 1193 | (channel_id, channel_name) 1194 | VALUES 1195 | ('${ch_id}', '${ch_name}'); 1196 | `, 1197 | (result) => 1198 | { 1199 | if(result && result.errno) 1200 | { 1201 | if(result.errno == 19) 1202 | console.info(`Already subscribed. Skipping: '${entities.decodeHTML 1203 | (validator.unescape 1204 | (ch_name))}' (${ch_id})`); 1205 | else 1206 | console.info(result); 1207 | } 1208 | else if(result === null) 1209 | console.info(`You are now subscribed to '${entities.decodeHTML 1210 | (validator.unescape 1211 | (ch_name))}' (${ch_id})`); 1212 | else 1213 | console.error('=> Error', result); 1214 | return resolve(); 1215 | } 1216 | ); 1217 | }); 1218 | } 1219 | 1220 | /** 1221 | * Given valid exported subscription JSON, imports subscription into db 1222 | * @param {String} json_file 1223 | * @returns {Promise} 1224 | */ 1225 | function import_subscription_list(json_file) 1226 | { 1227 | try 1228 | { 1229 | let imported = fs.readFileSync(json_file); 1230 | let arr; 1231 | try 1232 | { 1233 | arr = JSON.parse(imported); 1234 | } 1235 | catch(err) 1236 | { 1237 | console.error(`=> Error: File doesn't contain valid JSON`); 1238 | throw err; 1239 | } 1240 | 1241 | let promises = []; 1242 | for(let i = 0; i < arr.length; ++i) 1243 | { 1244 | if( 1245 | !validator.isWhitelisted 1246 | ( 1247 | arr[i][0].toLowerCase(), 1248 | 'abcdefghijklmnopqrstuvwxyz0123456789-_' 1249 | ) 1250 | ) 1251 | { 1252 | console.error('=> SKIPPING CORRUPTED DATA:', arr[i]); 1253 | continue; 1254 | } 1255 | promises.push 1256 | ( 1257 | insert_a_subscription 1258 | ( 1259 | arr[i][0], 1260 | validator.escape(arr[i][1]) 1261 | ) 1262 | ); 1263 | } 1264 | 1265 | return Promise.all(promises); 1266 | } 1267 | catch(err) 1268 | { 1269 | if(err.code === 'ENOENT') 1270 | { 1271 | console.error(`=> Error: File not found`); 1272 | process.exit(0); 1273 | } 1274 | else 1275 | { 1276 | throw err; 1277 | } 1278 | } 1279 | } 1280 | 1281 | /// ---------------------------------- 1282 | 1283 | 1284 | 1285 | let getopt = new Getopt 1286 | ([ 1287 | ['s', 'subscribe=ARG', 'Subscribe with a video/channel url'], 1288 | ['u', 'update', 'Fetch new updates from channels'], 1289 | ['n', 'onei', 'Interactively select and update a channel'], 1290 | ['', 'one=ARG', 'Update a specific known channel'], 1291 | ['g', 'generate', 'Generate yt_view_subscriptions.html'], 1292 | ['o', 'open', 'Open generated html file in default browser'], 1293 | ['l', 'list', 'Print a list of your subscrbed channels'], 1294 | ['p', 'progress', 'Prints progress information for update'], 1295 | ['r', 'remove', 'Prompts to remove a subscription'], 1296 | ['e', 'export', 'Exports subscription list in a JSON file'], 1297 | ['i', 'import=ARG', 'Imports subscriptions given JSON file'], 1298 | ['v', 'version', 'Prints running version'], 1299 | ['h', 'help', 'Display this help'] 1300 | ]) 1301 | .setHelp 1302 | ( 1303 | ` 1304 | Usages: 1305 | 1306 | vidlist [options] [arguments] 1307 | sub [options] [arguments] 1308 | vl [options] [arguments] 1309 | 1310 | [[OPTIONS]] 1311 | 1312 | NOTE: 1313 | 1314 | 1. Progress option works with update options only 1315 | 2. Options to update, generate and open can be combined. For 1316 | all other options, combining will produce unexpeted results 1317 | 3. Program file is in directory: 1318 | ${__dirname} 1319 | 4. Database and exported JSON files will be kept in directory: 1320 | ${global.dot} 1321 | 5. Generated HTML file location will be: 1322 | ${global.html} 1323 | 6. Variable 'global.old_video_limit_sec' near the top of 1324 | 'index.js' file determines the maximum age of a video 1325 | (since published) to keep in database for use, any older 1326 | videos are removed on update. Default limit is set to 15 1327 | days 1328 | 7. Bug report goes here: 1329 | https://github.com/dxwc/vidlist/issues 1330 | 8. This software and latest update information are stored here: 1331 | https://www.npmjs.com/package/vidlist 1332 | 1333 | This software was not produced by or directly for YouTube, LLC and has no 1334 | affiliation with the LLC. Use this software only at your own volition. 1335 | 1336 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 1337 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 1338 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 1339 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 1340 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 1341 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 1342 | SOFTWARE. 1343 | 1344 | EXAMPLE Usages: 1345 | 1346 | > Subscribe to a youtube channel: 1347 | 1348 | vl "https://www.youtube.com/watch?v=EeNiqKNtpAA" 1349 | 1350 | or 1351 | 1352 | vl -s "https://www.youtube.com/watch?v=EeNiqKNtpAA" 1353 | 1354 | > Remove a subscription: 1355 | 1356 | vl --remove 1357 | 1358 | > List your subscriptions: 1359 | 1360 | vl --list 1361 | 1362 | > Pull update from channel feed, show update progress, generate HTML 1363 | and open the HTML with your default browser: 1364 | 1365 | vl -upgo 1366 | 1367 | > update ONLY a specific channel interactively, show progress, generate 1368 | html and then open the HTML with your default browser: 1369 | 1370 | vl -npgo 1371 | 1372 | > update particular known selection or selections of channel 1373 | (eg: channel 3,4,5) show progress, generate html and then 1374 | open the HTML with your default browser 1375 | 1376 | vl --one 3,4,5 -pgo 1377 | ` 1378 | ) 1379 | .error(() => 1380 | { 1381 | console.info('Invalid option\n', getopt.getHelp()); 1382 | process.exit(1); 1383 | }); 1384 | 1385 | let opt = getopt.parse(process.argv.slice(2)); 1386 | 1387 | if(process.argv.length <= 2 || opt.options.help) 1388 | { 1389 | console.info(getopt.getHelp()); 1390 | process.exit(0); 1391 | } 1392 | 1393 | if(opt.options.version) 1394 | { 1395 | console.info('vidlist 2.0.0'); 1396 | process.exit(0); 1397 | } 1398 | 1399 | open_db_global() 1400 | .then(() => 1401 | { 1402 | if(opt.options.progress) global.prog = true; 1403 | 1404 | if(opt.options.list) 1405 | return list_subscriptions(); 1406 | else if(opt.options.export) 1407 | return export_subscription_list(); 1408 | else if(opt.options.import) 1409 | { 1410 | global.is_import = true; 1411 | return import_subscription_list(opt.options.import); 1412 | } 1413 | else if(opt.options.remove) 1414 | return remove_subscription(); 1415 | else if(opt.options.subscribe) 1416 | return subscribe(opt.options.subscribe); 1417 | else if(opt.options.update) 1418 | return download_and_save_feed() 1419 | .then(() => 1420 | { 1421 | if(global.prog) 1422 | stdout_write(`: Removing any older [see -h] data from db`); 1423 | }) 1424 | .then(() => keep_db_shorter()); 1425 | else if(opt.options.onei) 1426 | { 1427 | global.remaining = 1; 1428 | return download_and_save_one_feed().then(() => keep_db_shorter()); 1429 | } 1430 | else if(opt.options.one) 1431 | { 1432 | if(typeof opt.options.one === 'string') 1433 | { 1434 | 1435 | if(validator.isInt(opt.options.one)) 1436 | return download_and_save_one_feed(opt.options.one) 1437 | .then(() => keep_db_shorter()); 1438 | else 1439 | { 1440 | let arr = opt.options.one.split(','); 1441 | let promise_chain = Promise.resolve(); 1442 | global.remaining = 0; 1443 | arr.forEach((val) => 1444 | { 1445 | if(typeof val === 'string' && validator.isInt(val)) 1446 | { 1447 | ++global.remaining; 1448 | promise_chain = promise_chain.then(() => 1449 | { 1450 | stdout_write(`: Fetching update channel #${val}`); 1451 | return download_and_save_one_feed(val); 1452 | }); 1453 | } 1454 | else 1455 | { 1456 | console.error('=> Skipping update for invalid input:', val); 1457 | } 1458 | }); 1459 | 1460 | if(global.remaining === 0) return Promise 1461 | .reject('No updates were performed due to invalid input/s'); 1462 | 1463 | return promise_chain.then(() => keep_db_shorter()); 1464 | } 1465 | } 1466 | else throw new Error('Argument to --one is not an integer'); 1467 | } 1468 | else if(opt.options.generate || opt.options.open) 1469 | { 1470 | return true; 1471 | } 1472 | else if(process.argv[2]) 1473 | { 1474 | opt.options.subscribe = true; 1475 | return subscribe(process.argv[2]); 1476 | } 1477 | else 1478 | { 1479 | console.info(getopt.getHelp()); 1480 | return close_everything(1); 1481 | } 1482 | }) 1483 | .then(() => 1484 | { 1485 | if(opt.options.update || opt.options.onei || opt.options.one) 1486 | { 1487 | clean_say_dont_replace(`--Fetched updates`); 1488 | } 1489 | else if 1490 | ( 1491 | opt.options.list || 1492 | opt.options.subscribe || 1493 | opt.options.remove || 1494 | opt.options.export || 1495 | opt.options.import 1496 | ) 1497 | { 1498 | return close_everything(0); 1499 | } 1500 | 1501 | if(opt.options.generate) return generate_html(); 1502 | else return true; 1503 | }) 1504 | .then(() => 1505 | { 1506 | if(opt.options.generate) 1507 | { 1508 | clean_say_dont_replace(`--Generated HTML`); 1509 | } 1510 | if(opt.options.open) 1511 | { 1512 | if(require('os').platform() === 'win32') return opn(global.html); 1513 | else return opn(global.html, { wait : false }); 1514 | } 1515 | else 1516 | { 1517 | return true; 1518 | } 1519 | }) 1520 | .then(() => 1521 | { 1522 | if(opt.options.open) 1523 | { 1524 | clean_say_dont_replace 1525 | ( 1526 | `--Asked OS to open HTML with your default web browser` 1527 | ); 1528 | } 1529 | close_everything(0); 1530 | }) 1531 | .catch((err) => 1532 | { 1533 | console.error('=> There was an error in operation:\n', err); 1534 | close_everything(1); 1535 | }); 1536 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vidlist", 3 | "version": "2.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "abbrev": { 8 | "version": "1.1.1", 9 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", 10 | "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" 11 | }, 12 | "ajv": { 13 | "version": "6.10.0", 14 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", 15 | "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", 16 | "requires": { 17 | "fast-deep-equal": "^2.0.1", 18 | "fast-json-stable-stringify": "^2.0.0", 19 | "json-schema-traverse": "^0.4.1", 20 | "uri-js": "^4.2.2" 21 | } 22 | }, 23 | "ansi-regex": { 24 | "version": "2.1.1", 25 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 26 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" 27 | }, 28 | "aproba": { 29 | "version": "1.2.0", 30 | "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", 31 | "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" 32 | }, 33 | "are-we-there-yet": { 34 | "version": "1.1.5", 35 | "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", 36 | "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", 37 | "requires": { 38 | "delegates": "^1.0.0", 39 | "readable-stream": "^2.0.6" 40 | } 41 | }, 42 | "asn1": { 43 | "version": "0.2.4", 44 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 45 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 46 | "requires": { 47 | "safer-buffer": "~2.1.0" 48 | } 49 | }, 50 | "assert-plus": { 51 | "version": "1.0.0", 52 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 53 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 54 | }, 55 | "asynckit": { 56 | "version": "0.4.0", 57 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 58 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 59 | }, 60 | "aws-sign2": { 61 | "version": "0.7.0", 62 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 63 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" 64 | }, 65 | "aws4": { 66 | "version": "1.8.0", 67 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", 68 | "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" 69 | }, 70 | "balanced-match": { 71 | "version": "1.0.0", 72 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 73 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 74 | }, 75 | "bcrypt-pbkdf": { 76 | "version": "1.0.2", 77 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 78 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 79 | "requires": { 80 | "tweetnacl": "^0.14.3" 81 | } 82 | }, 83 | "brace-expansion": { 84 | "version": "1.1.11", 85 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 86 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 87 | "requires": { 88 | "balanced-match": "^1.0.0", 89 | "concat-map": "0.0.1" 90 | } 91 | }, 92 | "caseless": { 93 | "version": "0.12.0", 94 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 95 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 96 | }, 97 | "chownr": { 98 | "version": "1.1.2", 99 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz", 100 | "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==" 101 | }, 102 | "code-point-at": { 103 | "version": "1.1.0", 104 | "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", 105 | "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" 106 | }, 107 | "combined-stream": { 108 | "version": "1.0.8", 109 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 110 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 111 | "requires": { 112 | "delayed-stream": "~1.0.0" 113 | } 114 | }, 115 | "concat-map": { 116 | "version": "0.0.1", 117 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 118 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 119 | }, 120 | "console-control-strings": { 121 | "version": "1.1.0", 122 | "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", 123 | "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" 124 | }, 125 | "core-util-is": { 126 | "version": "1.0.2", 127 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 128 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 129 | }, 130 | "dashdash": { 131 | "version": "1.14.1", 132 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 133 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 134 | "requires": { 135 | "assert-plus": "^1.0.0" 136 | } 137 | }, 138 | "debug": { 139 | "version": "3.2.6", 140 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", 141 | "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", 142 | "requires": { 143 | "ms": "^2.1.1" 144 | } 145 | }, 146 | "deep-extend": { 147 | "version": "0.6.0", 148 | "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", 149 | "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" 150 | }, 151 | "delayed-stream": { 152 | "version": "1.0.0", 153 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 154 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 155 | }, 156 | "delegates": { 157 | "version": "1.0.0", 158 | "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", 159 | "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" 160 | }, 161 | "detect-libc": { 162 | "version": "1.0.3", 163 | "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", 164 | "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" 165 | }, 166 | "ecc-jsbn": { 167 | "version": "0.1.2", 168 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 169 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 170 | "requires": { 171 | "jsbn": "~0.1.0", 172 | "safer-buffer": "^2.1.0" 173 | } 174 | }, 175 | "entities": { 176 | "version": "2.0.0", 177 | "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz", 178 | "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==" 179 | }, 180 | "extend": { 181 | "version": "3.0.2", 182 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 183 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 184 | }, 185 | "extsprintf": { 186 | "version": "1.3.0", 187 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 188 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" 189 | }, 190 | "fast-deep-equal": { 191 | "version": "2.0.1", 192 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", 193 | "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" 194 | }, 195 | "fast-json-stable-stringify": { 196 | "version": "2.0.0", 197 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", 198 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" 199 | }, 200 | "forever-agent": { 201 | "version": "0.6.1", 202 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 203 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" 204 | }, 205 | "form-data": { 206 | "version": "2.3.3", 207 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 208 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 209 | "requires": { 210 | "asynckit": "^0.4.0", 211 | "combined-stream": "^1.0.6", 212 | "mime-types": "^2.1.12" 213 | } 214 | }, 215 | "fs-minipass": { 216 | "version": "1.2.6", 217 | "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.6.tgz", 218 | "integrity": "sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ==", 219 | "requires": { 220 | "minipass": "^2.2.1" 221 | } 222 | }, 223 | "fs.realpath": { 224 | "version": "1.0.0", 225 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 226 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" 227 | }, 228 | "gauge": { 229 | "version": "2.7.4", 230 | "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", 231 | "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", 232 | "requires": { 233 | "aproba": "^1.0.3", 234 | "console-control-strings": "^1.0.0", 235 | "has-unicode": "^2.0.0", 236 | "object-assign": "^4.1.0", 237 | "signal-exit": "^3.0.0", 238 | "string-width": "^1.0.1", 239 | "strip-ansi": "^3.0.1", 240 | "wide-align": "^1.1.0" 241 | } 242 | }, 243 | "getpass": { 244 | "version": "0.1.7", 245 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 246 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 247 | "requires": { 248 | "assert-plus": "^1.0.0" 249 | } 250 | }, 251 | "glob": { 252 | "version": "7.1.4", 253 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", 254 | "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", 255 | "requires": { 256 | "fs.realpath": "^1.0.0", 257 | "inflight": "^1.0.4", 258 | "inherits": "2", 259 | "minimatch": "^3.0.4", 260 | "once": "^1.3.0", 261 | "path-is-absolute": "^1.0.0" 262 | } 263 | }, 264 | "har-schema": { 265 | "version": "2.0.0", 266 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 267 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" 268 | }, 269 | "har-validator": { 270 | "version": "5.1.3", 271 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", 272 | "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", 273 | "requires": { 274 | "ajv": "^6.5.5", 275 | "har-schema": "^2.0.0" 276 | } 277 | }, 278 | "has-unicode": { 279 | "version": "2.0.1", 280 | "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", 281 | "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" 282 | }, 283 | "http-signature": { 284 | "version": "1.2.0", 285 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 286 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 287 | "requires": { 288 | "assert-plus": "^1.0.0", 289 | "jsprim": "^1.2.2", 290 | "sshpk": "^1.7.0" 291 | } 292 | }, 293 | "iconv-lite": { 294 | "version": "0.4.24", 295 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 296 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 297 | "requires": { 298 | "safer-buffer": ">= 2.1.2 < 3" 299 | } 300 | }, 301 | "ignore-walk": { 302 | "version": "3.0.1", 303 | "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", 304 | "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", 305 | "requires": { 306 | "minimatch": "^3.0.4" 307 | } 308 | }, 309 | "inflight": { 310 | "version": "1.0.6", 311 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 312 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 313 | "requires": { 314 | "once": "^1.3.0", 315 | "wrappy": "1" 316 | } 317 | }, 318 | "inherits": { 319 | "version": "2.0.4", 320 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 321 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 322 | }, 323 | "ini": { 324 | "version": "1.3.5", 325 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", 326 | "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" 327 | }, 328 | "is-fullwidth-code-point": { 329 | "version": "1.0.0", 330 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", 331 | "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", 332 | "requires": { 333 | "number-is-nan": "^1.0.0" 334 | } 335 | }, 336 | "is-typedarray": { 337 | "version": "1.0.0", 338 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 339 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 340 | }, 341 | "is-wsl": { 342 | "version": "1.1.0", 343 | "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", 344 | "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=" 345 | }, 346 | "isarray": { 347 | "version": "1.0.0", 348 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 349 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 350 | }, 351 | "isstream": { 352 | "version": "0.1.2", 353 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 354 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 355 | }, 356 | "jsbn": { 357 | "version": "0.1.1", 358 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 359 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" 360 | }, 361 | "json-schema": { 362 | "version": "0.2.3", 363 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 364 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" 365 | }, 366 | "json-schema-traverse": { 367 | "version": "0.4.1", 368 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 369 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 370 | }, 371 | "json-stringify-safe": { 372 | "version": "5.0.1", 373 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 374 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 375 | }, 376 | "jsprim": { 377 | "version": "1.4.1", 378 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", 379 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 380 | "requires": { 381 | "assert-plus": "1.0.0", 382 | "extsprintf": "1.3.0", 383 | "json-schema": "0.2.3", 384 | "verror": "1.10.0" 385 | } 386 | }, 387 | "mime-db": { 388 | "version": "1.40.0", 389 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", 390 | "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" 391 | }, 392 | "mime-types": { 393 | "version": "2.1.24", 394 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", 395 | "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", 396 | "requires": { 397 | "mime-db": "1.40.0" 398 | } 399 | }, 400 | "minimatch": { 401 | "version": "3.0.4", 402 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 403 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 404 | "requires": { 405 | "brace-expansion": "^1.1.7" 406 | } 407 | }, 408 | "minimist": { 409 | "version": "0.0.8", 410 | "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 411 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" 412 | }, 413 | "minipass": { 414 | "version": "2.3.5", 415 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", 416 | "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", 417 | "requires": { 418 | "safe-buffer": "^5.1.2", 419 | "yallist": "^3.0.0" 420 | } 421 | }, 422 | "minizlib": { 423 | "version": "1.2.1", 424 | "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", 425 | "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", 426 | "requires": { 427 | "minipass": "^2.2.1" 428 | } 429 | }, 430 | "mkdirp": { 431 | "version": "0.5.1", 432 | "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 433 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 434 | "requires": { 435 | "minimist": "0.0.8" 436 | } 437 | }, 438 | "moment": { 439 | "version": "2.24.0", 440 | "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", 441 | "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" 442 | }, 443 | "ms": { 444 | "version": "2.1.2", 445 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 446 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 447 | }, 448 | "nan": { 449 | "version": "2.14.0", 450 | "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", 451 | "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" 452 | }, 453 | "needle": { 454 | "version": "2.4.0", 455 | "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", 456 | "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==", 457 | "requires": { 458 | "debug": "^3.2.6", 459 | "iconv-lite": "^0.4.4", 460 | "sax": "^1.2.4" 461 | } 462 | }, 463 | "node-getopt": { 464 | "version": "0.3.2", 465 | "resolved": "https://registry.npmjs.org/node-getopt/-/node-getopt-0.3.2.tgz", 466 | "integrity": "sha512-yqkmYrMbK1wPrfz7mgeYvA4tBperLg9FQ4S3Sau3nSAkpOA0x0zC8nQ1siBwozy1f4SE8vq2n1WKv99r+PCa1Q==" 467 | }, 468 | "node-pre-gyp": { 469 | "version": "0.11.0", 470 | "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", 471 | "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", 472 | "requires": { 473 | "detect-libc": "^1.0.2", 474 | "mkdirp": "^0.5.1", 475 | "needle": "^2.2.1", 476 | "nopt": "^4.0.1", 477 | "npm-packlist": "^1.1.6", 478 | "npmlog": "^4.0.2", 479 | "rc": "^1.2.7", 480 | "rimraf": "^2.6.1", 481 | "semver": "^5.3.0", 482 | "tar": "^4" 483 | } 484 | }, 485 | "nopt": { 486 | "version": "4.0.1", 487 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", 488 | "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", 489 | "requires": { 490 | "abbrev": "1", 491 | "osenv": "^0.1.4" 492 | } 493 | }, 494 | "npm-bundled": { 495 | "version": "1.0.6", 496 | "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz", 497 | "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==" 498 | }, 499 | "npm-packlist": { 500 | "version": "1.4.4", 501 | "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.4.tgz", 502 | "integrity": "sha512-zTLo8UcVYtDU3gdeaFu2Xu0n0EvelfHDGuqtNIn5RO7yQj4H1TqNdBc/yZjxnWA0PVB8D3Woyp0i5B43JwQ6Vw==", 503 | "requires": { 504 | "ignore-walk": "^3.0.1", 505 | "npm-bundled": "^1.0.1" 506 | } 507 | }, 508 | "npmlog": { 509 | "version": "4.1.2", 510 | "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", 511 | "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", 512 | "requires": { 513 | "are-we-there-yet": "~1.1.2", 514 | "console-control-strings": "~1.1.0", 515 | "gauge": "~2.7.3", 516 | "set-blocking": "~2.0.0" 517 | } 518 | }, 519 | "number-is-nan": { 520 | "version": "1.0.1", 521 | "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", 522 | "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" 523 | }, 524 | "oauth-sign": { 525 | "version": "0.9.0", 526 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 527 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" 528 | }, 529 | "object-assign": { 530 | "version": "4.1.1", 531 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 532 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 533 | }, 534 | "once": { 535 | "version": "1.4.0", 536 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 537 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 538 | "requires": { 539 | "wrappy": "1" 540 | } 541 | }, 542 | "open": { 543 | "version": "6.4.0", 544 | "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz", 545 | "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", 546 | "requires": { 547 | "is-wsl": "^1.1.0" 548 | } 549 | }, 550 | "os-homedir": { 551 | "version": "1.0.2", 552 | "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", 553 | "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" 554 | }, 555 | "os-tmpdir": { 556 | "version": "1.0.2", 557 | "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", 558 | "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" 559 | }, 560 | "osenv": { 561 | "version": "0.1.5", 562 | "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", 563 | "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", 564 | "requires": { 565 | "os-homedir": "^1.0.0", 566 | "os-tmpdir": "^1.0.0" 567 | } 568 | }, 569 | "path-is-absolute": { 570 | "version": "1.0.1", 571 | "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 572 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 573 | }, 574 | "performance-now": { 575 | "version": "2.1.0", 576 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 577 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" 578 | }, 579 | "process-nextick-args": { 580 | "version": "2.0.1", 581 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 582 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" 583 | }, 584 | "psl": { 585 | "version": "1.2.0", 586 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.2.0.tgz", 587 | "integrity": "sha512-GEn74ZffufCmkDDLNcl3uuyF/aSD6exEyh1v/ZSdAomB82t6G9hzJVRx0jBmLDW+VfZqks3aScmMw9DszwUalA==" 588 | }, 589 | "punycode": { 590 | "version": "2.1.1", 591 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 592 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" 593 | }, 594 | "qs": { 595 | "version": "6.5.2", 596 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 597 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" 598 | }, 599 | "rc": { 600 | "version": "1.2.8", 601 | "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", 602 | "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", 603 | "requires": { 604 | "deep-extend": "^0.6.0", 605 | "ini": "~1.3.0", 606 | "minimist": "^1.2.0", 607 | "strip-json-comments": "~2.0.1" 608 | }, 609 | "dependencies": { 610 | "minimist": { 611 | "version": "1.2.0", 612 | "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", 613 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" 614 | } 615 | } 616 | }, 617 | "readable-stream": { 618 | "version": "2.3.6", 619 | "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", 620 | "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", 621 | "requires": { 622 | "core-util-is": "~1.0.0", 623 | "inherits": "~2.0.3", 624 | "isarray": "~1.0.0", 625 | "process-nextick-args": "~2.0.0", 626 | "safe-buffer": "~5.1.1", 627 | "string_decoder": "~1.1.1", 628 | "util-deprecate": "~1.0.1" 629 | } 630 | }, 631 | "request": { 632 | "version": "2.88.0", 633 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", 634 | "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", 635 | "requires": { 636 | "aws-sign2": "~0.7.0", 637 | "aws4": "^1.8.0", 638 | "caseless": "~0.12.0", 639 | "combined-stream": "~1.0.6", 640 | "extend": "~3.0.2", 641 | "forever-agent": "~0.6.1", 642 | "form-data": "~2.3.2", 643 | "har-validator": "~5.1.0", 644 | "http-signature": "~1.2.0", 645 | "is-typedarray": "~1.0.0", 646 | "isstream": "~0.1.2", 647 | "json-stringify-safe": "~5.0.1", 648 | "mime-types": "~2.1.19", 649 | "oauth-sign": "~0.9.0", 650 | "performance-now": "^2.1.0", 651 | "qs": "~6.5.2", 652 | "safe-buffer": "^5.1.2", 653 | "tough-cookie": "~2.4.3", 654 | "tunnel-agent": "^0.6.0", 655 | "uuid": "^3.3.2" 656 | } 657 | }, 658 | "rimraf": { 659 | "version": "2.6.3", 660 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", 661 | "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", 662 | "requires": { 663 | "glob": "^7.1.3" 664 | } 665 | }, 666 | "safe-buffer": { 667 | "version": "5.1.2", 668 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 669 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 670 | }, 671 | "safer-buffer": { 672 | "version": "2.1.2", 673 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 674 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 675 | }, 676 | "sax": { 677 | "version": "1.2.4", 678 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", 679 | "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" 680 | }, 681 | "semver": { 682 | "version": "5.7.0", 683 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", 684 | "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" 685 | }, 686 | "set-blocking": { 687 | "version": "2.0.0", 688 | "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", 689 | "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" 690 | }, 691 | "signal-exit": { 692 | "version": "3.0.2", 693 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", 694 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" 695 | }, 696 | "sqlite3": { 697 | "version": "4.0.9", 698 | "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.9.tgz", 699 | "integrity": "sha512-IkvzjmsWQl9BuBiM4xKpl5X8WCR4w0AeJHRdobCdXZ8dT/lNc1XS6WqvY35N6+YzIIgzSBeY5prdFObID9F9tA==", 700 | "requires": { 701 | "nan": "^2.12.1", 702 | "node-pre-gyp": "^0.11.0", 703 | "request": "^2.87.0" 704 | } 705 | }, 706 | "sshpk": { 707 | "version": "1.16.1", 708 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", 709 | "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", 710 | "requires": { 711 | "asn1": "~0.2.3", 712 | "assert-plus": "^1.0.0", 713 | "bcrypt-pbkdf": "^1.0.0", 714 | "dashdash": "^1.12.0", 715 | "ecc-jsbn": "~0.1.1", 716 | "getpass": "^0.1.1", 717 | "jsbn": "~0.1.0", 718 | "safer-buffer": "^2.0.2", 719 | "tweetnacl": "~0.14.0" 720 | } 721 | }, 722 | "string-width": { 723 | "version": "1.0.2", 724 | "resolved": "http://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", 725 | "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", 726 | "requires": { 727 | "code-point-at": "^1.0.0", 728 | "is-fullwidth-code-point": "^1.0.0", 729 | "strip-ansi": "^3.0.0" 730 | } 731 | }, 732 | "string_decoder": { 733 | "version": "1.1.1", 734 | "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 735 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 736 | "requires": { 737 | "safe-buffer": "~5.1.0" 738 | } 739 | }, 740 | "strip-ansi": { 741 | "version": "3.0.1", 742 | "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 743 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 744 | "requires": { 745 | "ansi-regex": "^2.0.0" 746 | } 747 | }, 748 | "strip-json-comments": { 749 | "version": "2.0.1", 750 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 751 | "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" 752 | }, 753 | "tar": { 754 | "version": "4.4.10", 755 | "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.10.tgz", 756 | "integrity": "sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA==", 757 | "requires": { 758 | "chownr": "^1.1.1", 759 | "fs-minipass": "^1.2.5", 760 | "minipass": "^2.3.5", 761 | "minizlib": "^1.2.1", 762 | "mkdirp": "^0.5.0", 763 | "safe-buffer": "^5.1.2", 764 | "yallist": "^3.0.3" 765 | } 766 | }, 767 | "tough-cookie": { 768 | "version": "2.4.3", 769 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", 770 | "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", 771 | "requires": { 772 | "psl": "^1.1.24", 773 | "punycode": "^1.4.1" 774 | }, 775 | "dependencies": { 776 | "punycode": { 777 | "version": "1.4.1", 778 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", 779 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" 780 | } 781 | } 782 | }, 783 | "tunnel-agent": { 784 | "version": "0.6.0", 785 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 786 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 787 | "requires": { 788 | "safe-buffer": "^5.0.1" 789 | } 790 | }, 791 | "tweetnacl": { 792 | "version": "0.14.5", 793 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 794 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" 795 | }, 796 | "uri-js": { 797 | "version": "4.2.2", 798 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 799 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 800 | "requires": { 801 | "punycode": "^2.1.0" 802 | } 803 | }, 804 | "util-deprecate": { 805 | "version": "1.0.2", 806 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 807 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 808 | }, 809 | "uuid": { 810 | "version": "3.3.2", 811 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 812 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" 813 | }, 814 | "validator": { 815 | "version": "11.1.0", 816 | "resolved": "https://registry.npmjs.org/validator/-/validator-11.1.0.tgz", 817 | "integrity": "sha512-qiQ5ktdO7CD6C/5/mYV4jku/7qnqzjrxb3C/Q5wR3vGGinHTgJZN/TdFT3ZX4vXhX2R1PXx42fB1cn5W+uJ4lg==" 818 | }, 819 | "verror": { 820 | "version": "1.10.0", 821 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 822 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 823 | "requires": { 824 | "assert-plus": "^1.0.0", 825 | "core-util-is": "1.0.2", 826 | "extsprintf": "^1.2.0" 827 | } 828 | }, 829 | "vid_data": { 830 | "version": "1.0.0", 831 | "resolved": "https://registry.npmjs.org/vid_data/-/vid_data-1.0.0.tgz", 832 | "integrity": "sha512-1dJGgAp951picNaFthX3jmnsm7QT80jRsr8zK/lH34IQGsd6rwnVNMG8j/NoAirTFAsnDCEcNyfAW6qdZ2/diw==" 833 | }, 834 | "wide-align": { 835 | "version": "1.1.3", 836 | "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", 837 | "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", 838 | "requires": { 839 | "string-width": "^1.0.2 || 2" 840 | } 841 | }, 842 | "wrappy": { 843 | "version": "1.0.2", 844 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 845 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 846 | }, 847 | "xss-filters": { 848 | "version": "1.2.7", 849 | "resolved": "https://registry.npmjs.org/xss-filters/-/xss-filters-1.2.7.tgz", 850 | "integrity": "sha1-Wfod4gHzby80cNysX1jMwoMLCpo=" 851 | }, 852 | "yallist": { 853 | "version": "3.0.3", 854 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", 855 | "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" 856 | } 857 | } 858 | } 859 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vidlist", 3 | "version": "2.0.0", 4 | "description": "A command line tool to subscribe to youtube channels and view updates in a locally generated html file", 5 | "keywords": [ 6 | "youtube", 7 | "subscribe", 8 | "subscriber", 9 | "subscription" 10 | ], 11 | "main": "index.js", 12 | "bin": { 13 | "vidlist": "index.js", 14 | "vl": "index.js", 15 | "sub": "index.js" 16 | }, 17 | "scripts": { 18 | "start": "node index.js" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/dxwc/vidlist.git" 23 | }, 24 | "author": "", 25 | "license": "UNLICENSED", 26 | "bugs": { 27 | "url": "https://github.com/dxwc/vidlist/issues" 28 | }, 29 | "homepage": "https://github.com/dxwc/vidlist#readme", 30 | "dependencies": { 31 | "entities": "^2.0.0", 32 | "moment": "^2.24.0", 33 | "node-getopt": "^0.3.2", 34 | "open": "^6.4.0", 35 | "sqlite3": "^4.0.9", 36 | "validator": "^11.1.0", 37 | "vid_data": "^1.0.0", 38 | "xss-filters": "^1.2.7" 39 | } 40 | } 41 | --------------------------------------------------------------------------------