atris 3.48.1 → 3.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/youtube.js +397 -1
- package/package.json +1 -1
package/commands/youtube.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { apiRequestJson } = require('../utils/api');
|
|
2
2
|
const { ensureValidCredentials } = require('../utils/auth');
|
|
3
3
|
const { spawnSync } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
4
5
|
const path = require('path');
|
|
5
6
|
const https = require('https');
|
|
6
7
|
|
|
@@ -25,9 +26,14 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
|
|
|
25
26
|
output('');
|
|
26
27
|
output(`Usage: ${commandName} notes <youtube-url> [engine]`);
|
|
27
28
|
output(` ${commandName} process <youtube-url> [options]`);
|
|
29
|
+
output(` ${commandName} watch add <channel-url-or-@handle>`);
|
|
30
|
+
output(` ${commandName} watch list`);
|
|
31
|
+
output(` ${commandName} watch remove <number>`);
|
|
32
|
+
output(` ${commandName} watch tick`);
|
|
28
33
|
output(` ${commandName} <youtube-url> [options]`);
|
|
29
34
|
output('');
|
|
30
35
|
output('notes = free local notes, process = 5 credits cloud knowledge');
|
|
36
|
+
output('watch = subscribed channels turn into briefs without a human');
|
|
31
37
|
output('Process a YouTube video through Atris using timestamped transcript-first analysis.');
|
|
32
38
|
output('Falls back to cloud video processing when local captions are unavailable.');
|
|
33
39
|
output('');
|
|
@@ -46,6 +52,8 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
|
|
|
46
52
|
output(` ${commandName} notes https://www.youtube.com/watch?v=VIDEO_ID`);
|
|
47
53
|
output(` ${commandName} https://www.youtube.com/watch?v=VIDEO_ID`);
|
|
48
54
|
output(` ${commandName} process https://youtu.be/VIDEO_ID --query "Key takeaways"`);
|
|
55
|
+
output(` ${commandName} watch add @veritasium`);
|
|
56
|
+
output(` ${commandName} watch tick`);
|
|
49
57
|
output('');
|
|
50
58
|
}
|
|
51
59
|
|
|
@@ -81,7 +89,7 @@ function parseYoutubeArgs(argv = []) {
|
|
|
81
89
|
return options;
|
|
82
90
|
}
|
|
83
91
|
|
|
84
|
-
if (['process', 'analyze'
|
|
92
|
+
if (['process', 'analyze'].includes(args[0])) {
|
|
85
93
|
args.shift();
|
|
86
94
|
}
|
|
87
95
|
|
|
@@ -444,6 +452,370 @@ function formatYoutubeResult(data) {
|
|
|
444
452
|
return lines.join('\n');
|
|
445
453
|
}
|
|
446
454
|
|
|
455
|
+
function videoIdFromUrl(url) {
|
|
456
|
+
const text = String(url || '');
|
|
457
|
+
const watch = text.match(/[?&]v=([^&]+)/);
|
|
458
|
+
if (watch) return watch[1];
|
|
459
|
+
const short = text.match(/youtu\.be\/([^?&/]+)/);
|
|
460
|
+
return short ? short[1] : null;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function dateStamp(now) {
|
|
464
|
+
if (typeof now === 'string' && /^\d{4}-\d{2}-\d{2}/.test(now)) {
|
|
465
|
+
return now.slice(0, 10);
|
|
466
|
+
}
|
|
467
|
+
const value = now instanceof Date ? now : new Date(now || Date.now());
|
|
468
|
+
if (Number.isNaN(value.getTime())) return new Date().toISOString().slice(0, 10);
|
|
469
|
+
return value.toISOString().slice(0, 10);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function firstHeading(notes) {
|
|
473
|
+
const match = String(notes || '').replace(/\r\n/g, '\n').match(/^#{1,6}\s+(.+)$/m);
|
|
474
|
+
return match ? match[1].trim() : '';
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function fileBriefFromNotes({ cwd, url, workDir, now } = {}) {
|
|
478
|
+
try {
|
|
479
|
+
const id = videoIdFromUrl(url);
|
|
480
|
+
if (!id) return;
|
|
481
|
+
const notesPath = path.join(workDir, `yt_${id}.md`);
|
|
482
|
+
if (!fs.existsSync(notesPath)) return;
|
|
483
|
+
const notes = fs.readFileSync(notesPath, 'utf8');
|
|
484
|
+
const wikiDir = path.join(cwd, 'atris', 'wiki');
|
|
485
|
+
if (!fs.existsSync(wikiDir)) return;
|
|
486
|
+
|
|
487
|
+
const heading = firstHeading(notes);
|
|
488
|
+
const date = dateStamp(now);
|
|
489
|
+
const header = [
|
|
490
|
+
heading.toLowerCase(),
|
|
491
|
+
'',
|
|
492
|
+
`date: ${date}`,
|
|
493
|
+
`source: ${url}`,
|
|
494
|
+
'rail: atris youtube notes, quotes repaired against the transcript',
|
|
495
|
+
].join('\n');
|
|
496
|
+
const briefsDir = path.join(wikiDir, 'briefs');
|
|
497
|
+
fs.mkdirSync(briefsDir, { recursive: true });
|
|
498
|
+
const relBrief = `atris/wiki/briefs/youtube-${id}.md`;
|
|
499
|
+
fs.writeFileSync(path.join(cwd, relBrief), `${header}\n${notes}`);
|
|
500
|
+
|
|
501
|
+
const year = date.slice(0, 4);
|
|
502
|
+
const journalPath = path.join(cwd, 'atris', 'logs', year, `${date}.md`);
|
|
503
|
+
fs.mkdirSync(path.dirname(journalPath), { recursive: true });
|
|
504
|
+
let existing = '';
|
|
505
|
+
if (fs.existsSync(journalPath)) existing = fs.readFileSync(journalPath, 'utf8');
|
|
506
|
+
const prefix = existing && !existing.endsWith('\n') ? '\n' : '';
|
|
507
|
+
const line = `- [claimable] watched: ${heading} -> ${relBrief}`;
|
|
508
|
+
fs.writeFileSync(journalPath, `${existing}${prefix}${line}\n`);
|
|
509
|
+
|
|
510
|
+
console.log(`brief filed: ${relBrief}`);
|
|
511
|
+
} catch {
|
|
512
|
+
// notes filing must never break the youtube command
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function watchStatePath(cwd = process.cwd()) {
|
|
517
|
+
return path.join(cwd, '.atris', 'state', 'youtube_watch.json');
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function emptyWatchState() {
|
|
521
|
+
return { channels: [], seen: {}, seeded: {}, seenByChannel: {} };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function asObject(value) {
|
|
525
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function loadWatchState(statePath) {
|
|
529
|
+
try {
|
|
530
|
+
const parsed = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
531
|
+
const channels = Array.isArray(parsed?.channels) ? parsed.channels : [];
|
|
532
|
+
return {
|
|
533
|
+
channels,
|
|
534
|
+
seen: asObject(parsed?.seen),
|
|
535
|
+
seeded: asObject(parsed?.seeded),
|
|
536
|
+
seenByChannel: asObject(parsed?.seenByChannel),
|
|
537
|
+
};
|
|
538
|
+
} catch {
|
|
539
|
+
return emptyWatchState();
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function saveWatchState(statePath, state) {
|
|
544
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
545
|
+
const payload = {
|
|
546
|
+
channels: (state.channels || []).map((row) => ({
|
|
547
|
+
channel: row.channel,
|
|
548
|
+
added: row.added,
|
|
549
|
+
})),
|
|
550
|
+
seen: asObject(state.seen),
|
|
551
|
+
seeded: asObject(state.seeded),
|
|
552
|
+
seenByChannel: asObject(state.seenByChannel),
|
|
553
|
+
};
|
|
554
|
+
fs.writeFileSync(statePath, `${JSON.stringify(payload, null, 2)}\n`);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function stampNow(now) {
|
|
558
|
+
if (typeof now === 'function') now = now();
|
|
559
|
+
if (typeof now === 'string' && now) return now;
|
|
560
|
+
const value = now instanceof Date ? now : new Date(now || Date.now());
|
|
561
|
+
if (Number.isNaN(value.getTime())) return new Date().toISOString();
|
|
562
|
+
return value.toISOString();
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function normalizeWatchChannel(input) {
|
|
566
|
+
const raw = String(input || '').trim();
|
|
567
|
+
if (!raw) throw new Error('Missing channel url or @handle. Run "atris youtube watch --help".');
|
|
568
|
+
|
|
569
|
+
let text = raw;
|
|
570
|
+
if (text.startsWith('@')) {
|
|
571
|
+
text = `https://www.youtube.com/${text}`;
|
|
572
|
+
} else if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(text)) {
|
|
573
|
+
if (/^(www\.)?youtube\.com\//i.test(text) || /^youtu\.be\//i.test(text)) {
|
|
574
|
+
text = `https://${text}`;
|
|
575
|
+
} else if (/^@?[\w.-]+$/.test(text)) {
|
|
576
|
+
text = `https://www.youtube.com/@${text.replace(/^@/, '')}`;
|
|
577
|
+
} else {
|
|
578
|
+
throw new Error(`Invalid channel: ${raw}`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
let parsed;
|
|
583
|
+
try {
|
|
584
|
+
parsed = new URL(text);
|
|
585
|
+
} catch {
|
|
586
|
+
throw new Error(`Invalid channel: ${raw}`);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
parsed.hash = '';
|
|
590
|
+
parsed.search = '';
|
|
591
|
+
let href = parsed.toString().replace(/\/+$/, '');
|
|
592
|
+
href = href.replace(/\/(videos|featured|streams|shorts)$/i, '');
|
|
593
|
+
return href;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function channelVideosUrl(channel) {
|
|
597
|
+
const base = String(channel || '').replace(/\/+$/, '');
|
|
598
|
+
if (/\/videos$/i.test(base)) return base;
|
|
599
|
+
return `${base}/videos`;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function parseFlatPlaylist(stdout) {
|
|
603
|
+
const videos = [];
|
|
604
|
+
for (const line of String(stdout || '').split(/\r?\n/)) {
|
|
605
|
+
const trimmed = line.trim();
|
|
606
|
+
if (!trimmed || !trimmed.includes('|')) continue;
|
|
607
|
+
const idx = trimmed.indexOf('|');
|
|
608
|
+
const id = trimmed.slice(0, idx).trim();
|
|
609
|
+
const title = trimmed.slice(idx + 1).trim();
|
|
610
|
+
if (id && id !== 'NA') videos.push({ id, title });
|
|
611
|
+
}
|
|
612
|
+
return videos;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function defaultChannelFetcher(videosUrl, deps = {}) {
|
|
616
|
+
const spawn = deps.spawnSync || spawnSync;
|
|
617
|
+
const result = spawn('yt-dlp', [
|
|
618
|
+
'--no-update',
|
|
619
|
+
'--flat-playlist',
|
|
620
|
+
'--playlist-end',
|
|
621
|
+
'3',
|
|
622
|
+
'--print',
|
|
623
|
+
'%(id)s|%(title)s',
|
|
624
|
+
videosUrl,
|
|
625
|
+
], {
|
|
626
|
+
encoding: 'utf8',
|
|
627
|
+
timeout: 60000,
|
|
628
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
629
|
+
});
|
|
630
|
+
if (result.error || result.status !== 0) {
|
|
631
|
+
const detail = String(result.stderr || result.error?.message || 'fetch failed').trim();
|
|
632
|
+
throw new Error(detail || 'fetch failed');
|
|
633
|
+
}
|
|
634
|
+
return parseFlatPlaylist(result.stdout);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function defaultNotesRunner(url, deps = {}) {
|
|
638
|
+
const script = path.join(__dirname, '..', 'scripts', 'det', 'ytnotes');
|
|
639
|
+
const spawn = deps.spawnSync || spawnSync;
|
|
640
|
+
return spawn(script, [url], { stdio: 'inherit' });
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function resolveWatchStatePath(deps = {}) {
|
|
644
|
+
if (deps.statePath) return deps.statePath;
|
|
645
|
+
return watchStatePath(deps.cwd || process.cwd());
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function channelSeenMap(state, channel) {
|
|
649
|
+
return asObject(asObject(state.seenByChannel)[channel]);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function markSeen(state, channel, id, timestamp) {
|
|
653
|
+
if (!state.seen || typeof state.seen !== 'object') state.seen = {};
|
|
654
|
+
if (!state.seenByChannel || typeof state.seenByChannel !== 'object') state.seenByChannel = {};
|
|
655
|
+
if (!state.seenByChannel[channel] || typeof state.seenByChannel[channel] !== 'object') {
|
|
656
|
+
state.seenByChannel[channel] = {};
|
|
657
|
+
}
|
|
658
|
+
state.seen[id] = timestamp;
|
|
659
|
+
state.seenByChannel[channel][id] = timestamp;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function addWatchChannel(channelInput, deps = {}) {
|
|
663
|
+
const output = deps.output || ((line = '') => console.log(line));
|
|
664
|
+
if (!channelInput) {
|
|
665
|
+
output('usage: atris youtube watch add <channel-url-or-@handle>');
|
|
666
|
+
return 2;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
let channel;
|
|
670
|
+
try {
|
|
671
|
+
channel = normalizeWatchChannel(channelInput);
|
|
672
|
+
} catch (err) {
|
|
673
|
+
output(err.message);
|
|
674
|
+
return 2;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const statePath = resolveWatchStatePath(deps);
|
|
678
|
+
const state = loadWatchState(statePath);
|
|
679
|
+
if (state.channels.some((row) => row.channel === channel)) {
|
|
680
|
+
output(`already watching ${channel}`);
|
|
681
|
+
return 0;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
state.channels.push({
|
|
685
|
+
channel,
|
|
686
|
+
added: stampNow(deps.now),
|
|
687
|
+
});
|
|
688
|
+
saveWatchState(statePath, state);
|
|
689
|
+
output(`watching ${channel}`);
|
|
690
|
+
return 0;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function listWatchChannels(deps = {}) {
|
|
694
|
+
const output = deps.output || ((line = '') => console.log(line));
|
|
695
|
+
const state = loadWatchState(resolveWatchStatePath(deps));
|
|
696
|
+
if (!state.channels.length) {
|
|
697
|
+
output('no channels watched');
|
|
698
|
+
return 0;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
state.channels.forEach((row, index) => {
|
|
702
|
+
const count = Object.keys(channelSeenMap(state, row.channel)).length;
|
|
703
|
+
output(`${index + 1}. ${row.channel} (${count} seen)`);
|
|
704
|
+
});
|
|
705
|
+
return 0;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function removeWatchChannel(rawNumber, deps = {}) {
|
|
709
|
+
const output = deps.output || ((line = '') => console.log(line));
|
|
710
|
+
const index = Number(rawNumber);
|
|
711
|
+
const statePath = resolveWatchStatePath(deps);
|
|
712
|
+
const state = loadWatchState(statePath);
|
|
713
|
+
if (!Number.isInteger(index) || index < 1 || index > state.channels.length) {
|
|
714
|
+
output('usage: atris youtube watch remove <number>');
|
|
715
|
+
return 2;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const [removed] = state.channels.splice(index - 1, 1);
|
|
719
|
+
saveWatchState(statePath, state);
|
|
720
|
+
output(`removed ${removed.channel}`);
|
|
721
|
+
return 0;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
async function tickWatch(deps = {}) {
|
|
725
|
+
const output = deps.output || ((line = '') => console.log(line));
|
|
726
|
+
const statePath = resolveWatchStatePath(deps);
|
|
727
|
+
const fetcher = deps.fetcher || ((videosUrl) => defaultChannelFetcher(videosUrl, deps));
|
|
728
|
+
const runner = deps.runner || ((url) => defaultNotesRunner(url, deps));
|
|
729
|
+
const briefFiler = deps.briefFiler || fileBriefFromNotes;
|
|
730
|
+
const cwd = deps.cwd || process.cwd();
|
|
731
|
+
const workDir = deps.workDir || path.join(process.env.TMPDIR || '/tmp', 'ytnotes');
|
|
732
|
+
const timestamp = stampNow(deps.now);
|
|
733
|
+
const now = deps.now || timestamp;
|
|
734
|
+
|
|
735
|
+
const state = loadWatchState(statePath);
|
|
736
|
+
let totalNew = 0;
|
|
737
|
+
let totalBriefed = 0;
|
|
738
|
+
|
|
739
|
+
for (const row of state.channels) {
|
|
740
|
+
const videosUrl = channelVideosUrl(row.channel);
|
|
741
|
+
let videos;
|
|
742
|
+
try {
|
|
743
|
+
videos = await Promise.resolve(fetcher(videosUrl, row));
|
|
744
|
+
} catch {
|
|
745
|
+
output(`warning: channel ${row.channel} fetch failed`);
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
if (!Array.isArray(videos)) {
|
|
749
|
+
output(`warning: channel ${row.channel} fetch failed`);
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const localSeen = channelSeenMap(state, row.channel);
|
|
754
|
+
const isFresh = !state.seeded[row.channel];
|
|
755
|
+
const unseen = videos.filter((video) => video?.id && !localSeen[video.id] && !state.seen[video.id]);
|
|
756
|
+
const newest = videos[0];
|
|
757
|
+
const toBrief = isFresh
|
|
758
|
+
? (newest?.id ? [newest] : [])
|
|
759
|
+
: unseen;
|
|
760
|
+
|
|
761
|
+
if (isFresh) {
|
|
762
|
+
state.seeded[row.channel] = true;
|
|
763
|
+
for (const video of videos) {
|
|
764
|
+
if (video?.id) markSeen(state, row.channel, video.id, timestamp);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
let briefed = 0;
|
|
769
|
+
for (const video of toBrief) {
|
|
770
|
+
if (!video?.id) continue;
|
|
771
|
+
const url = `https://www.youtube.com/watch?v=${video.id}`;
|
|
772
|
+
try {
|
|
773
|
+
runner(url, deps);
|
|
774
|
+
} catch {
|
|
775
|
+
// notes failure must not stop the rest of the tick
|
|
776
|
+
}
|
|
777
|
+
try {
|
|
778
|
+
briefFiler({ cwd, url, workDir, now });
|
|
779
|
+
} catch {
|
|
780
|
+
// brief filing must never break the watch tick
|
|
781
|
+
}
|
|
782
|
+
markSeen(state, row.channel, video.id, timestamp);
|
|
783
|
+
briefed += 1;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if (!isFresh) {
|
|
787
|
+
for (const video of unseen) {
|
|
788
|
+
if (video?.id) markSeen(state, row.channel, video.id, timestamp);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
const newCount = isFresh ? (newest?.id ? 1 : 0) : unseen.length;
|
|
793
|
+
output(`channel ${row.channel}: ${newCount} new, ${briefed} briefed`);
|
|
794
|
+
totalNew += newCount;
|
|
795
|
+
totalBriefed += briefed;
|
|
796
|
+
saveWatchState(statePath, state);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
output(`total: ${totalNew} new, ${totalBriefed} briefed`);
|
|
800
|
+
saveWatchState(statePath, state);
|
|
801
|
+
return 0;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
async function watchCommand(args = [], deps = {}) {
|
|
805
|
+
const output = deps.output || ((line = '') => console.log(line));
|
|
806
|
+
const sub = args[0];
|
|
807
|
+
if (!sub || ['help', '--help', '-h'].includes(sub)) {
|
|
808
|
+
showYoutubeHelp(output, deps.commandName || 'atris youtube');
|
|
809
|
+
return sub ? 0 : 2;
|
|
810
|
+
}
|
|
811
|
+
if (sub === 'add') return addWatchChannel(args[1], deps);
|
|
812
|
+
if (sub === 'list') return listWatchChannels(deps);
|
|
813
|
+
if (sub === 'remove') return removeWatchChannel(args[1], deps);
|
|
814
|
+
if (sub === 'tick') return tickWatch(deps);
|
|
815
|
+
output(`unknown watch command: ${sub}`);
|
|
816
|
+
return 2;
|
|
817
|
+
}
|
|
818
|
+
|
|
447
819
|
function runYoutubeNotes(args = [], deps = {}) {
|
|
448
820
|
const output = deps.output || ((line = '') => console.error(line));
|
|
449
821
|
const url = args[0];
|
|
@@ -459,6 +831,14 @@ function runYoutubeNotes(args = [], deps = {}) {
|
|
|
459
831
|
const childArgs = engine ? [url, engine] : [url];
|
|
460
832
|
const result = spawn(script, childArgs, { stdio: 'inherit' });
|
|
461
833
|
if (result.status == null) return 1;
|
|
834
|
+
if (result.status === 0) {
|
|
835
|
+
fileBriefFromNotes({
|
|
836
|
+
cwd: deps.cwd || process.cwd(),
|
|
837
|
+
url,
|
|
838
|
+
workDir: deps.workDir || path.join(process.env.TMPDIR || '/tmp', 'ytnotes'),
|
|
839
|
+
now: deps.now || new Date(),
|
|
840
|
+
});
|
|
841
|
+
}
|
|
462
842
|
return result.status;
|
|
463
843
|
}
|
|
464
844
|
|
|
@@ -469,6 +849,11 @@ async function youtubeCommand(argv = process.argv.slice(3), deps = {}) {
|
|
|
469
849
|
if (!deps.output && !deps.spawnSync) process.exit(code);
|
|
470
850
|
return code;
|
|
471
851
|
}
|
|
852
|
+
if (argv[0] === 'watch') {
|
|
853
|
+
const code = await watchCommand(argv.slice(1), { ...deps, output });
|
|
854
|
+
if (!deps.output && !deps.fetcher && !deps.runner && !deps.briefFiler) process.exit(code);
|
|
855
|
+
return code;
|
|
856
|
+
}
|
|
472
857
|
const options = parseYoutubeArgs(argv);
|
|
473
858
|
if (options.help) {
|
|
474
859
|
showYoutubeHelp(output, deps.commandName || 'atris youtube');
|
|
@@ -488,5 +873,16 @@ module.exports = {
|
|
|
488
873
|
processYoutube,
|
|
489
874
|
shouldRetryWithLocalTranscript,
|
|
490
875
|
formatYoutubeResult,
|
|
876
|
+
fileBriefFromNotes,
|
|
877
|
+
normalizeWatchChannel,
|
|
878
|
+
channelVideosUrl,
|
|
879
|
+
parseFlatPlaylist,
|
|
880
|
+
loadWatchState,
|
|
881
|
+
saveWatchState,
|
|
882
|
+
addWatchChannel,
|
|
883
|
+
listWatchChannels,
|
|
884
|
+
removeWatchChannel,
|
|
885
|
+
tickWatch,
|
|
886
|
+
watchCommand,
|
|
491
887
|
youtubeCommand,
|
|
492
888
|
};
|