├── .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 |
`;
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 |
--------------------------------------------------------------------------------