atris 3.58.5 → 3.58.6
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/atris/skills/x-search/SKILL.md +2 -2
- package/atris/skills/youtube/SKILL.md +44 -28
- package/commands/auth.js +52 -24
- package/commands/learn.js +205 -39
- package/commands/workflow.js +4 -8
- package/commands/x-search.js +9 -10
- package/commands/youtube.js +444 -101
- package/package.json +1 -1
- package/scripts/det/ytnotes +82 -5
- package/utils/auth.js +47 -6
package/commands/youtube.js
CHANGED
|
@@ -42,12 +42,12 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
|
|
|
42
42
|
output(` ${commandName} watch tick`);
|
|
43
43
|
output(` ${commandName} <youtube-url> [options]`);
|
|
44
44
|
output('');
|
|
45
|
-
output('search = free local discovery (ytsearch / yt-dlp), returns youtu.be links; rich free search
|
|
46
|
-
output('search --paid = 5 credits, watch permalinks + titles from Atris; rich paid search
|
|
45
|
+
output('search = free local discovery (ytsearch / yt-dlp), returns youtu.be links; rich free search writes one apply and a failing keep/revert pack; thin hands off to teach');
|
|
46
|
+
output('search --paid = 5 credits, watch permalinks + titles from Atris; rich paid search writes one apply and a failing keep/revert pack; thin hands off to teach');
|
|
47
47
|
output('notes = free local notes to stdout; ephemeral unless --save; hands off to teach');
|
|
48
48
|
output('teach = one chapter from local captions; bare teach resumes unpaid checks, then the next chapter after recap or skip');
|
|
49
49
|
output('rich ephemeral notes/teach print one apply next-step and one failing check (no files)');
|
|
50
|
-
output('process = 5 credits cloud knowledge (needs a filled Apply); rich process
|
|
50
|
+
output('process = 5 credits cloud knowledge (needs a filled Apply); rich process writes one apply and a failing keep/revert pack');
|
|
51
51
|
output('digest = one decision page from this week\'s video briefs; rich digest writes one apply and a failing keep/revert pack');
|
|
52
52
|
output('watch = subscribed channels turn into briefs without a human; add hands off to tick; tick hands off to teach when it briefed');
|
|
53
53
|
output('Process a YouTube video through Atris using timestamped transcript-first analysis.');
|
|
@@ -211,8 +211,16 @@ function youtubeFailureError(result) {
|
|
|
211
211
|
? ' Run "atris login --force".'
|
|
212
212
|
: result.status === 402
|
|
213
213
|
? ' Check Atris credits.'
|
|
214
|
-
:
|
|
215
|
-
|
|
214
|
+
: result.status === 502
|
|
215
|
+
? ' YouTube processing is unavailable; retry in a few seconds.'
|
|
216
|
+
: '';
|
|
217
|
+
const credits = paidSearchCredits(result.data);
|
|
218
|
+
const refundHint = result.status === 502 && creditsRefundedExplicitly(credits)
|
|
219
|
+
? ' credits refunded.'
|
|
220
|
+
: '';
|
|
221
|
+
const lines = [`YouTube processing failed (${result.status}): ${resultErrorText(result)}.${hint}${refundHint}`];
|
|
222
|
+
lines.push(...formatCreditsLines(credits));
|
|
223
|
+
return new Error(lines.join('\n'));
|
|
216
224
|
}
|
|
217
225
|
|
|
218
226
|
function captionHostAllowed(urlString) {
|
|
@@ -406,33 +414,110 @@ function parseCaptionText(raw) {
|
|
|
406
414
|
return segments.join(' ');
|
|
407
415
|
}
|
|
408
416
|
|
|
417
|
+
function parseYtDlpInfoJson(result) {
|
|
418
|
+
const raw = String((result && result.stdout) || '').trim();
|
|
419
|
+
if (!raw) return null;
|
|
420
|
+
try {
|
|
421
|
+
const info = JSON.parse(raw);
|
|
422
|
+
return info && typeof info === 'object' && !Array.isArray(info) ? info : null;
|
|
423
|
+
} catch {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function localCaptionNames(id) {
|
|
429
|
+
// scripts/det/ytnotes keeps the same VTT names (not clean.txt).
|
|
430
|
+
return [
|
|
431
|
+
`yt_${id}.en.vtt`,
|
|
432
|
+
`yt_${id}.en-orig.vtt`,
|
|
433
|
+
`yt_${id}.en-US.vtt`,
|
|
434
|
+
`yt_${id}.en-GB.vtt`,
|
|
435
|
+
`yt_${id}.clean.txt`,
|
|
436
|
+
];
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function captionLookupIds({ url, id } = {}) {
|
|
440
|
+
const ids = [];
|
|
441
|
+
const seen = new Set();
|
|
442
|
+
const add = (value) => {
|
|
443
|
+
const text = String(value || '').trim();
|
|
444
|
+
if (!text || seen.has(text)) return;
|
|
445
|
+
seen.add(text);
|
|
446
|
+
ids.push(text);
|
|
447
|
+
};
|
|
448
|
+
add(id);
|
|
449
|
+
add(videoIdFromUrl(url));
|
|
450
|
+
return ids;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function readLocalCaptionText({ url, id, workDir } = {}) {
|
|
454
|
+
if (!workDir) return '';
|
|
455
|
+
for (const videoId of captionLookupIds({ url, id })) {
|
|
456
|
+
for (const name of localCaptionNames(videoId)) {
|
|
457
|
+
const abs = path.join(workDir, name);
|
|
458
|
+
try {
|
|
459
|
+
if (!fs.existsSync(abs)) continue;
|
|
460
|
+
const text = fs.readFileSync(abs, 'utf8');
|
|
461
|
+
if (String(text).trim()) return text;
|
|
462
|
+
} catch {
|
|
463
|
+
// try the next written caption file
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return '';
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function loadCaptionRaw(info, youtubeUrl, deps = {}) {
|
|
471
|
+
const payload = info && typeof info === 'object' ? info : {};
|
|
472
|
+
const selected = chooseCaptionTrack(payload);
|
|
473
|
+
let raw = '';
|
|
474
|
+
if (selected?.track?.url) {
|
|
475
|
+
raw = await (deps.fetchCaptionText || fetchCaptionText)(selected.track.url);
|
|
476
|
+
}
|
|
477
|
+
if (String(raw || '').trim()) {
|
|
478
|
+
return { raw, language: selected?.language || 'unknown' };
|
|
479
|
+
}
|
|
480
|
+
const local = readLocalCaptionText({
|
|
481
|
+
url: youtubeUrl,
|
|
482
|
+
id: payload.id,
|
|
483
|
+
workDir: deps.workDir,
|
|
484
|
+
});
|
|
485
|
+
if (!String(local || '').trim()) return null;
|
|
486
|
+
return { raw: local, language: selected?.language || 'en' };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function ytDlpInfoArgs(youtubeUrl) {
|
|
490
|
+
return ['-J', '--skip-download', '--no-warnings', '--no-playlist', youtubeUrl];
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function teachSourceVideoId(info, youtubeUrl) {
|
|
494
|
+
const fromUrl = videoIdFromUrl(youtubeUrl);
|
|
495
|
+
const fromInfo = info && info.id;
|
|
496
|
+
const entries = info && Array.isArray(info.entries) ? info.entries : [];
|
|
497
|
+
const fromEntry = entries[0] && entries[0].id;
|
|
498
|
+
if (info && (info._type === 'playlist' || entries.length)) {
|
|
499
|
+
return fromUrl || fromEntry || fromInfo;
|
|
500
|
+
}
|
|
501
|
+
return fromInfo || fromUrl;
|
|
502
|
+
}
|
|
503
|
+
|
|
409
504
|
async function extractLocalTranscript(youtubeUrl, deps = {}) {
|
|
410
505
|
if (process.env.ATRIS_YOUTUBE_LOCAL_TRANSCRIPT === '0') return null;
|
|
411
506
|
const runner = deps.spawnSync || spawnSync;
|
|
412
|
-
const result = runner('yt-dlp',
|
|
507
|
+
const result = runner('yt-dlp', ytDlpInfoArgs(youtubeUrl), {
|
|
413
508
|
encoding: 'utf8',
|
|
414
509
|
timeout: 20000,
|
|
415
510
|
maxBuffer: 10 * 1024 * 1024,
|
|
416
511
|
});
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
try {
|
|
421
|
-
info = JSON.parse(result.stdout);
|
|
422
|
-
} catch {
|
|
423
|
-
return null;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
const selected = chooseCaptionTrack(info);
|
|
427
|
-
if (!selected?.track?.url) return null;
|
|
428
|
-
const rawCaption = await (deps.fetchCaptionText || fetchCaptionText)(selected.track.url);
|
|
429
|
-
const transcript = parseCaptionText(rawCaption);
|
|
512
|
+
const info = parseYtDlpInfoJson(result);
|
|
513
|
+
const loaded = await loadCaptionRaw(info, youtubeUrl, deps);
|
|
514
|
+
const transcript = parseCaptionText(loaded && loaded.raw);
|
|
430
515
|
if (!transcript) return null;
|
|
431
516
|
|
|
432
517
|
return {
|
|
433
518
|
transcriptText: transcript.slice(0, LOCAL_TRANSCRIPT_MAX_CHARS),
|
|
434
|
-
language:
|
|
435
|
-
durationSeconds: Number(info.duration || 0) || undefined,
|
|
519
|
+
language: loaded.language || 'unknown',
|
|
520
|
+
durationSeconds: Number((info && info.duration) || 0) || undefined,
|
|
436
521
|
};
|
|
437
522
|
}
|
|
438
523
|
|
|
@@ -468,6 +553,12 @@ async function processYoutube(options, deps = {}) {
|
|
|
468
553
|
if (!result.ok && result.status === 401 && !auth.minted) {
|
|
469
554
|
const remint = await ensureBilled('youtube', { ...deps, forceMint: true });
|
|
470
555
|
if (remint?.ok && remint.token) {
|
|
556
|
+
if (!options.json) {
|
|
557
|
+
const print = typeof deps.output === 'function' ? deps.output : () => {};
|
|
558
|
+
for (const line of formatCreditsLines(paidSearchCredits(result.data))) {
|
|
559
|
+
print(line);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
471
562
|
auth = remint;
|
|
472
563
|
result = await apiFn('/agent/process_youtube', {
|
|
473
564
|
method: 'POST',
|
|
@@ -484,7 +575,10 @@ async function processYoutube(options, deps = {}) {
|
|
|
484
575
|
const localExtractor = deps.extractLocalTranscript || extractLocalTranscript;
|
|
485
576
|
let localTranscript = null;
|
|
486
577
|
try {
|
|
487
|
-
localTranscript = await localExtractor(options.youtubeUrl,
|
|
578
|
+
localTranscript = await localExtractor(options.youtubeUrl, {
|
|
579
|
+
...deps,
|
|
580
|
+
workDir: deps.workDir || notesWorkDir(deps),
|
|
581
|
+
});
|
|
488
582
|
} catch {
|
|
489
583
|
localTranscript = null;
|
|
490
584
|
}
|
|
@@ -501,6 +595,12 @@ async function processYoutube(options, deps = {}) {
|
|
|
501
595
|
if (transcriptResult.status === 401 || transcriptResult.status === 402 || transcriptResult.status === 400) {
|
|
502
596
|
throw youtubeFailureError(transcriptResult);
|
|
503
597
|
}
|
|
598
|
+
if (!options.json) {
|
|
599
|
+
const print = typeof deps.output === 'function' ? deps.output : () => {};
|
|
600
|
+
for (const line of formatCreditsLines(paidSearchCredits(transcriptResult.data))) {
|
|
601
|
+
print(line);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
504
604
|
}
|
|
505
605
|
|
|
506
606
|
const result = await requestYoutube(buildYoutubePayload(options));
|
|
@@ -537,11 +637,8 @@ function formatYoutubeResult(data) {
|
|
|
537
637
|
: '';
|
|
538
638
|
lines.push(`Processing: ${method}${source}`);
|
|
539
639
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
const remaining = data.credits_remaining !== undefined ? data.credits_remaining : '?';
|
|
543
|
-
lines.push(`Credits: ${used} used, ${remaining} remaining`);
|
|
544
|
-
}
|
|
640
|
+
const creditLines = formatCreditsLines(paidSearchCredits(data));
|
|
641
|
+
if (creditLines.length) lines.push(...creditLines);
|
|
545
642
|
const analysis = processAnalysisText(data);
|
|
546
643
|
if (analysis) {
|
|
547
644
|
lines.push('');
|
|
@@ -551,11 +648,13 @@ function formatYoutubeResult(data) {
|
|
|
551
648
|
}
|
|
552
649
|
|
|
553
650
|
function videoIdFromUrl(url) {
|
|
554
|
-
const text = String(url || '');
|
|
651
|
+
const text = String(url || '').split('#')[0];
|
|
555
652
|
const watch = text.match(/[?&]v=([^&]+)/);
|
|
556
653
|
if (watch) return watch[1];
|
|
557
654
|
const short = text.match(/youtu\.be\/([^?&/]+)/);
|
|
558
|
-
|
|
655
|
+
if (short) return short[1];
|
|
656
|
+
const pathId = text.match(/\/(?:shorts|embed|live|v|e)\/([^?&/]+)/i);
|
|
657
|
+
return pathId ? pathId[1] : null;
|
|
559
658
|
}
|
|
560
659
|
|
|
561
660
|
function videoIdFromArg(arg) {
|
|
@@ -569,7 +668,7 @@ function videoIdFromArg(arg) {
|
|
|
569
668
|
function looksLikeYoutubeUrl(arg) {
|
|
570
669
|
const text = String(arg || '').trim();
|
|
571
670
|
if (!text || text.startsWith('-')) return false;
|
|
572
|
-
return /youtube\.com|youtu\.be/i.test(text);
|
|
671
|
+
return /youtube\.com|youtu\.be|youtube-nocookie\.com/i.test(text);
|
|
573
672
|
}
|
|
574
673
|
|
|
575
674
|
function isPlaylistUrl(url) {
|
|
@@ -694,6 +793,14 @@ function readNotesText({ url, workDir } = {}) {
|
|
|
694
793
|
}
|
|
695
794
|
}
|
|
696
795
|
|
|
796
|
+
function notesWorkDir(deps = {}) {
|
|
797
|
+
return deps.workDir || path.join(process.env.TMPDIR || '/tmp', 'ytnotes');
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function keptPrintedNotes({ url, workDir } = {}) {
|
|
801
|
+
return Boolean(String(readNotesText({ url, workDir }) || '').trim());
|
|
802
|
+
}
|
|
803
|
+
|
|
697
804
|
function saveRichNotes(url, deps = {}) {
|
|
698
805
|
const workDir = deps.workDir || path.join(process.env.TMPDIR || '/tmp', 'ytnotes');
|
|
699
806
|
const lesson = notesLessonFromText(readNotesText({ url, workDir }));
|
|
@@ -806,6 +913,8 @@ function unsaveYoutubeNotes(target, deps = {}) {
|
|
|
806
913
|
}
|
|
807
914
|
for (const rel of listTeachSidecarRels(cwd, id)) add(rel);
|
|
808
915
|
add(notesExperimentRel(id));
|
|
916
|
+
add(processApplyRel(id));
|
|
917
|
+
add(processExperimentRel(id));
|
|
809
918
|
for (const section of sections) add(teachExperimentRel(id, section));
|
|
810
919
|
|
|
811
920
|
const removed = [];
|
|
@@ -853,6 +962,85 @@ function ensureProcessApply({ cwd, url, now, output } = {}) {
|
|
|
853
962
|
});
|
|
854
963
|
}
|
|
855
964
|
|
|
965
|
+
function processExperimentSlug(id) {
|
|
966
|
+
return `process-${experimentIdToken(id)}`;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function processExperimentRel(id) {
|
|
970
|
+
return `atris/experiments/${processExperimentSlug(id)}`;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function processApplyRel(id) {
|
|
974
|
+
return applyGate.applySidecarRel('process', experimentIdToken(id));
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function saveRichProcess({ cwd, url, lesson } = {}) {
|
|
978
|
+
if (isThinTeachLesson(lesson)) {
|
|
979
|
+
return { thin: true, packRel: null, lesson };
|
|
980
|
+
}
|
|
981
|
+
if (cwd) fs.mkdirSync(path.join(cwd, 'atris', 'wiki'), { recursive: true });
|
|
982
|
+
const id = videoIdFromUrl(url);
|
|
983
|
+
const packRel = fileTeachExperiment({
|
|
984
|
+
cwd,
|
|
985
|
+
url,
|
|
986
|
+
lesson,
|
|
987
|
+
slug: id ? processExperimentSlug(id) : null,
|
|
988
|
+
applyRel: id ? processApplyRel(id) : null,
|
|
989
|
+
});
|
|
990
|
+
return { thin: false, packRel, lesson, source: url };
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function ensureProcessLearnerApply({ cwd, url, packRel, now, output, source } = {}) {
|
|
994
|
+
const id = videoIdFromUrl(url);
|
|
995
|
+
const pack = packRel || (id ? processExperimentRel(id) : null);
|
|
996
|
+
const slug = pack ? path.basename(pack) : null;
|
|
997
|
+
if (cwd) fs.mkdirSync(path.join(cwd, 'atris', 'wiki'), { recursive: true });
|
|
998
|
+
return applyGate.ensureApply({
|
|
999
|
+
cwd,
|
|
1000
|
+
source: source || url || (id ? `process:${id}` : 'process'),
|
|
1001
|
+
rel: id ? processApplyRel(id) : null,
|
|
1002
|
+
now,
|
|
1003
|
+
output,
|
|
1004
|
+
incompleteMessage: slug
|
|
1005
|
+
? `next: atris experiments keep ${slug}`
|
|
1006
|
+
: applyGate.ephemeralApplyMessage('process'),
|
|
1007
|
+
required: false,
|
|
1008
|
+
change: pack ? `apply ${pack}` : undefined,
|
|
1009
|
+
receipt: pack ? TEACH_KEEP_RULE : undefined,
|
|
1010
|
+
journalLine: pack ? `- [claimable] apply: ${pack}. ${TEACH_KEEP_RULE}` : undefined,
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function mintRichProcess({ cwd, url, data, now, output, ensureApply, json } = {}) {
|
|
1015
|
+
const print = typeof output === 'function' ? output : (line = '') => console.log(line);
|
|
1016
|
+
const lesson = notesLessonFromText(processAnalysisText(data));
|
|
1017
|
+
if (isThinTeachLesson(lesson)) {
|
|
1018
|
+
printProcessLearnerGate(data, { json }, print);
|
|
1019
|
+
return 0;
|
|
1020
|
+
}
|
|
1021
|
+
const saved = saveRichProcess({ cwd, url, lesson });
|
|
1022
|
+
const applyFn = ensureApply || ensureProcessLearnerApply;
|
|
1023
|
+
const applyCode = applyFn({
|
|
1024
|
+
cwd,
|
|
1025
|
+
url,
|
|
1026
|
+
packRel: saved.packRel,
|
|
1027
|
+
now,
|
|
1028
|
+
output: print,
|
|
1029
|
+
source: url,
|
|
1030
|
+
});
|
|
1031
|
+
if (ensureApply) return applyCode;
|
|
1032
|
+
const id = videoIdFromUrl(url);
|
|
1033
|
+
const baseline = proveSavedLearnerBaseline({
|
|
1034
|
+
cwd,
|
|
1035
|
+
applyRel: id ? processApplyRel(id) : null,
|
|
1036
|
+
lesson: saved.lesson,
|
|
1037
|
+
output: print,
|
|
1038
|
+
json,
|
|
1039
|
+
});
|
|
1040
|
+
if (baseline !== 0) return baseline;
|
|
1041
|
+
return applyCode;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
856
1044
|
const DIGEST_ENGINE_TIMEOUT_MS = 240000;
|
|
857
1045
|
const DEFAULT_DIGEST_DAYS = 7;
|
|
858
1046
|
|
|
@@ -1082,6 +1270,15 @@ function ensureWatchApply({ cwd, url, packRel, now, output, source } = {}) {
|
|
|
1082
1270
|
});
|
|
1083
1271
|
}
|
|
1084
1272
|
|
|
1273
|
+
function firstRichWatchLesson(urls, workDir) {
|
|
1274
|
+
for (const url of Array.isArray(urls) ? urls : []) {
|
|
1275
|
+
if (!url) continue;
|
|
1276
|
+
const lesson = notesLessonFromText(readNotesText({ url, workDir }));
|
|
1277
|
+
if (!isThinTeachLesson(lesson)) return { url, lesson };
|
|
1278
|
+
}
|
|
1279
|
+
return null;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1085
1282
|
function saveRichWatch({ cwd, url, lesson } = {}) {
|
|
1086
1283
|
if (isThinTeachLesson(lesson)) {
|
|
1087
1284
|
return { thin: true, packRel: null, lesson };
|
|
@@ -1276,6 +1473,7 @@ function defaultChannelFetcher(videosUrl, deps = {}) {
|
|
|
1276
1473
|
const result = spawn('yt-dlp', [
|
|
1277
1474
|
'--no-update',
|
|
1278
1475
|
'--flat-playlist',
|
|
1476
|
+
'--no-warnings',
|
|
1279
1477
|
'--playlist-end',
|
|
1280
1478
|
'3',
|
|
1281
1479
|
'--print',
|
|
@@ -1286,11 +1484,13 @@ function defaultChannelFetcher(videosUrl, deps = {}) {
|
|
|
1286
1484
|
timeout: 60000,
|
|
1287
1485
|
maxBuffer: 2 * 1024 * 1024,
|
|
1288
1486
|
});
|
|
1487
|
+
const videos = parseFlatPlaylist(result && result.stdout);
|
|
1488
|
+
if (videos.length) return videos;
|
|
1289
1489
|
if (result.error || result.status !== 0) {
|
|
1290
1490
|
const detail = String(result.stderr || result.error?.message || 'fetch failed').trim();
|
|
1291
1491
|
throw new Error(detail || 'fetch failed');
|
|
1292
1492
|
}
|
|
1293
|
-
return
|
|
1493
|
+
return videos;
|
|
1294
1494
|
}
|
|
1295
1495
|
|
|
1296
1496
|
function defaultNotesRunner(url, deps = {}) {
|
|
@@ -1420,6 +1620,7 @@ async function tickWatch(deps = {}) {
|
|
|
1420
1620
|
let totalNew = 0;
|
|
1421
1621
|
let totalBriefed = 0;
|
|
1422
1622
|
let firstBriefedUrl = null;
|
|
1623
|
+
const briefedUrls = [];
|
|
1423
1624
|
|
|
1424
1625
|
for (const row of state.channels) {
|
|
1425
1626
|
const videosUrl = channelVideosUrl(row.channel);
|
|
@@ -1466,6 +1667,7 @@ async function tickWatch(deps = {}) {
|
|
|
1466
1667
|
}
|
|
1467
1668
|
markSeen(state, row.channel, video.id, timestamp);
|
|
1468
1669
|
briefed += 1;
|
|
1670
|
+
briefedUrls.push(url);
|
|
1469
1671
|
if (!firstBriefedUrl) firstBriefedUrl = url;
|
|
1470
1672
|
}
|
|
1471
1673
|
|
|
@@ -1485,34 +1687,35 @@ async function tickWatch(deps = {}) {
|
|
|
1485
1687
|
output(`total: ${totalNew} new, ${totalBriefed} briefed`);
|
|
1486
1688
|
saveWatchState(statePath, state);
|
|
1487
1689
|
if (totalBriefed > 0) {
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
printLearnerCheckGate(output, lesson, { includeCheck: true });
|
|
1492
|
-
printYoutubeTeachNext(firstBriefedUrl, {}, output);
|
|
1493
|
-
return 0;
|
|
1494
|
-
}
|
|
1495
|
-
const saved = saveRichWatch({ cwd, url: firstBriefedUrl, lesson });
|
|
1690
|
+
const rich = firstRichWatchLesson(briefedUrls, workDir);
|
|
1691
|
+
if (rich) {
|
|
1692
|
+
const saved = saveRichWatch({ cwd, url: rich.url, lesson: rich.lesson });
|
|
1496
1693
|
const ensureApply = deps.ensureApply || ensureWatchApply;
|
|
1497
1694
|
const applyCode = ensureApply({
|
|
1498
1695
|
cwd,
|
|
1499
|
-
url:
|
|
1696
|
+
url: rich.url,
|
|
1500
1697
|
packRel: saved.packRel,
|
|
1501
1698
|
now,
|
|
1502
1699
|
output,
|
|
1503
|
-
source:
|
|
1700
|
+
source: rich.url,
|
|
1504
1701
|
});
|
|
1505
1702
|
if (deps.ensureApply) return applyCode;
|
|
1506
|
-
const id = videoIdFromUrl(
|
|
1703
|
+
const id = videoIdFromUrl(rich.url);
|
|
1507
1704
|
const baseline = proveSavedLearnerBaseline({
|
|
1508
1705
|
cwd,
|
|
1509
1706
|
applyRel: id ? watchApplyRel(id) : null,
|
|
1510
|
-
lesson,
|
|
1707
|
+
lesson: rich.lesson,
|
|
1511
1708
|
output,
|
|
1512
1709
|
});
|
|
1513
1710
|
if (baseline !== 0) return baseline;
|
|
1514
1711
|
return applyCode;
|
|
1515
1712
|
}
|
|
1713
|
+
if (firstBriefedUrl) {
|
|
1714
|
+
const lesson = notesLessonFromText(readNotesText({ url: firstBriefedUrl, workDir }));
|
|
1715
|
+
printLearnerCheckGate(output, lesson, { includeCheck: true });
|
|
1716
|
+
printYoutubeTeachNext(firstBriefedUrl, {}, output);
|
|
1717
|
+
return 0;
|
|
1718
|
+
}
|
|
1516
1719
|
printYoutubeTeachNext(firstBriefedUrl, {}, output);
|
|
1517
1720
|
return 0;
|
|
1518
1721
|
}
|
|
@@ -1549,11 +1752,13 @@ function defaultPlaylistExpander(playlistUrl, deps = {}) {
|
|
|
1549
1752
|
timeout: 60000,
|
|
1550
1753
|
maxBuffer: 2 * 1024 * 1024,
|
|
1551
1754
|
});
|
|
1755
|
+
const videos = parseFlatPlaylist(result && result.stdout);
|
|
1756
|
+
if (videos.length) return videos;
|
|
1552
1757
|
if (result.error || (result.status != null && result.status !== 0)) {
|
|
1553
1758
|
const detail = String(result.stderr || result.error?.message || 'playlist expand failed').trim();
|
|
1554
1759
|
throw new Error(detail || 'playlist expand failed');
|
|
1555
1760
|
}
|
|
1556
|
-
return
|
|
1761
|
+
return videos;
|
|
1557
1762
|
}
|
|
1558
1763
|
|
|
1559
1764
|
function defaultNotesItemRunner(url, engine, deps = {}) {
|
|
@@ -1647,15 +1852,20 @@ function runOneNotesItem(item, engine, deps = {}) {
|
|
|
1647
1852
|
}
|
|
1648
1853
|
|
|
1649
1854
|
const started = readNowMs(deps);
|
|
1650
|
-
let
|
|
1855
|
+
let result = { status: 1 };
|
|
1651
1856
|
try {
|
|
1652
|
-
|
|
1653
|
-
} catch {
|
|
1654
|
-
|
|
1857
|
+
result = invokeNotesRunner(item.url, engine, deps);
|
|
1858
|
+
} catch (err) {
|
|
1859
|
+
result = { status: 1, stderr: String((err && err.message) || err || '') };
|
|
1655
1860
|
}
|
|
1861
|
+
const status = readRunnerStatus(result);
|
|
1656
1862
|
let brief = null;
|
|
1657
1863
|
let lesson = null;
|
|
1658
|
-
let ok = status === 0
|
|
1864
|
+
let ok = status === 0 || keptPrintedNotes({
|
|
1865
|
+
url: item.url,
|
|
1866
|
+
workDir: notesWorkDir(deps),
|
|
1867
|
+
result,
|
|
1868
|
+
});
|
|
1659
1869
|
if (ok && deps.save) {
|
|
1660
1870
|
const saved = saveRichNotes(item.url, deps);
|
|
1661
1871
|
if (saved.thin) {
|
|
@@ -1743,10 +1953,16 @@ function runSingleYoutubeNotes(url, engine, deps = {}) {
|
|
|
1743
1953
|
try {
|
|
1744
1954
|
result = invokeNotesRunner(url, engine, deps);
|
|
1745
1955
|
} catch {
|
|
1746
|
-
|
|
1956
|
+
result = { status: 1 };
|
|
1747
1957
|
}
|
|
1748
1958
|
const status = readRunnerStatus(result);
|
|
1749
|
-
if (status !== 0
|
|
1959
|
+
if (status !== 0 && !keptPrintedNotes({
|
|
1960
|
+
url,
|
|
1961
|
+
workDir: notesWorkDir(deps),
|
|
1962
|
+
result,
|
|
1963
|
+
})) {
|
|
1964
|
+
return status == null ? 1 : status;
|
|
1965
|
+
}
|
|
1750
1966
|
const output = deps.output || ((line = '') => console.error(line));
|
|
1751
1967
|
if (!deps.save) {
|
|
1752
1968
|
const json = deps.json === true;
|
|
@@ -1827,14 +2043,14 @@ function showYoutubeSearchHelp(output = console.log, commandName = 'atris youtub
|
|
|
1827
2043
|
output('');
|
|
1828
2044
|
output('Free local discovery. Uses ytsearch on PATH when present, else the');
|
|
1829
2045
|
output('bundled scripts/det/ytsearch, else yt-dlp ytsearchN with the same print contract.');
|
|
1830
|
-
output('Does not bill credits. A hit prints one next: atris youtube teach <first-url>.');
|
|
1831
|
-
output('A rich hit
|
|
2046
|
+
output('Does not bill credits. A thin hit prints check: fill this and one next: atris youtube teach <first-url>.');
|
|
2047
|
+
output('A rich hit writes one apply and a failing keep/revert pack (score 0).');
|
|
1832
2048
|
output('');
|
|
1833
2049
|
output(`--paid buys watch permalinks from Atris (${PAID_SEARCH_COST_HINT}).`);
|
|
1834
2050
|
output('Requires login. Same auth path as atris youtube process.');
|
|
1835
|
-
output('A hit
|
|
1836
|
-
output('A
|
|
1837
|
-
output('Empty or failed paid search
|
|
2051
|
+
output('A rich hit writes one apply and a failing keep/revert pack (score 0).');
|
|
2052
|
+
output('A thin hit prints check: fill this and one next: atris youtube teach <first-url>.');
|
|
2053
|
+
output('Empty or failed paid search prints credits refunded only when the server marks a refund.');
|
|
1838
2054
|
output('');
|
|
1839
2055
|
output('Options:');
|
|
1840
2056
|
output(` --limit <n> Max results (default: ${DEFAULT_SEARCH_LIMIT})`);
|
|
@@ -2079,9 +2295,8 @@ function paidSearchCredits(data) {
|
|
|
2079
2295
|
return { used, remaining, refunded };
|
|
2080
2296
|
}
|
|
2081
2297
|
|
|
2082
|
-
function
|
|
2298
|
+
function creditsRefundedExplicitly(credits) {
|
|
2083
2299
|
if (!credits) return false;
|
|
2084
|
-
if (credits.used === 0) return true;
|
|
2085
2300
|
if (credits.refunded === true) return true;
|
|
2086
2301
|
return typeof credits.refunded === 'number' && credits.refunded > 0;
|
|
2087
2302
|
}
|
|
@@ -2091,7 +2306,7 @@ function formatCreditsLines(credits) {
|
|
|
2091
2306
|
if (credits.used !== undefined || credits.remaining !== undefined) {
|
|
2092
2307
|
lines.push(`Credits: ${credits.used !== undefined ? credits.used : '?'} used, ${credits.remaining !== undefined ? credits.remaining : '?'} remaining`);
|
|
2093
2308
|
}
|
|
2094
|
-
if (
|
|
2309
|
+
if (creditsRefundedExplicitly(credits)) {
|
|
2095
2310
|
lines.push('credits refunded');
|
|
2096
2311
|
}
|
|
2097
2312
|
return lines;
|
|
@@ -2118,7 +2333,7 @@ function youtubeSearchFailureError(result) {
|
|
|
2118
2333
|
? ' YouTube search is unavailable; retry in a few seconds.'
|
|
2119
2334
|
: '';
|
|
2120
2335
|
const credits = paidSearchCredits(result.data);
|
|
2121
|
-
const refundHint = result.status === 502 &&
|
|
2336
|
+
const refundHint = result.status === 502 && creditsRefundedExplicitly(credits)
|
|
2122
2337
|
? ' credits refunded.'
|
|
2123
2338
|
: '';
|
|
2124
2339
|
const lines = [`YouTube search failed (${result.status}): ${resultErrorText(result)}.${hint}${refundHint}`];
|
|
@@ -2147,6 +2362,12 @@ async function requestPaidYoutubeSearch(options, deps = {}) {
|
|
|
2147
2362
|
if (!result.ok && result.status === 401 && !auth.minted) {
|
|
2148
2363
|
const remint = await ensureBilled('youtube', { ...deps, forceMint: true });
|
|
2149
2364
|
if (remint?.ok && remint.token) {
|
|
2365
|
+
if (!options.json) {
|
|
2366
|
+
const print = typeof deps.output === 'function' ? deps.output : () => {};
|
|
2367
|
+
for (const line of formatCreditsLines(paidSearchCredits(result.data))) {
|
|
2368
|
+
print(line);
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2150
2371
|
auth = remint;
|
|
2151
2372
|
result = await call(auth.token);
|
|
2152
2373
|
}
|
|
@@ -2178,9 +2399,7 @@ async function runPaidYoutubeSearch(options, deps = {}) {
|
|
|
2178
2399
|
return 2;
|
|
2179
2400
|
}
|
|
2180
2401
|
output(rendered);
|
|
2181
|
-
|
|
2182
|
-
printSearchTeachNext(videos, options, output);
|
|
2183
|
-
return 0;
|
|
2402
|
+
return gateSearchLearner(videos, options, output, deps);
|
|
2184
2403
|
}
|
|
2185
2404
|
|
|
2186
2405
|
function searchLessonText(rows) {
|
|
@@ -2190,6 +2409,93 @@ function searchLessonText(rows) {
|
|
|
2190
2409
|
.join('\n');
|
|
2191
2410
|
}
|
|
2192
2411
|
|
|
2412
|
+
function searchExperimentSlug(query) {
|
|
2413
|
+
return `search-${applyGate.applySlug(query)}`;
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
function searchExperimentRel(query) {
|
|
2417
|
+
return `atris/experiments/${searchExperimentSlug(query)}`;
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
function searchApplyRel(query) {
|
|
2421
|
+
return applyGate.applySidecarRel('search', applyGate.applySlug(query));
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
function searchApplyNow(now) {
|
|
2425
|
+
if (typeof now === 'function') {
|
|
2426
|
+
const value = now();
|
|
2427
|
+
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}/.test(value)) return value;
|
|
2428
|
+
const ms = Number(value);
|
|
2429
|
+
if (Number.isFinite(ms) && ms > 0) return new Date(ms).toISOString().slice(0, 10);
|
|
2430
|
+
return undefined;
|
|
2431
|
+
}
|
|
2432
|
+
return now;
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
function saveRichSearch({ cwd, query, lesson } = {}) {
|
|
2436
|
+
if (isThinTeachLesson(lesson)) {
|
|
2437
|
+
return { thin: true, packRel: null, lesson };
|
|
2438
|
+
}
|
|
2439
|
+
if (cwd) fs.mkdirSync(path.join(cwd, 'atris', 'wiki'), { recursive: true });
|
|
2440
|
+
const packRel = fileTeachExperiment({
|
|
2441
|
+
cwd,
|
|
2442
|
+
lesson,
|
|
2443
|
+
slug: query ? searchExperimentSlug(query) : null,
|
|
2444
|
+
applyRel: query ? searchApplyRel(query) : null,
|
|
2445
|
+
});
|
|
2446
|
+
return { thin: false, packRel, lesson, source: query };
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
function ensureSearchApply({ cwd, query, packRel, now, output, source } = {}) {
|
|
2450
|
+
const pack = packRel || (query ? searchExperimentRel(query) : null);
|
|
2451
|
+
const slug = pack ? path.basename(pack) : null;
|
|
2452
|
+
if (cwd) fs.mkdirSync(path.join(cwd, 'atris', 'wiki'), { recursive: true });
|
|
2453
|
+
return applyGate.ensureApply({
|
|
2454
|
+
cwd,
|
|
2455
|
+
source: source || query || 'search',
|
|
2456
|
+
rel: query ? searchApplyRel(query) : null,
|
|
2457
|
+
now,
|
|
2458
|
+
output,
|
|
2459
|
+
incompleteMessage: slug
|
|
2460
|
+
? `next: atris experiments keep ${slug}`
|
|
2461
|
+
: applyGate.ephemeralApplyMessage('search'),
|
|
2462
|
+
required: false,
|
|
2463
|
+
change: pack ? `apply ${pack}` : undefined,
|
|
2464
|
+
receipt: pack ? TEACH_KEEP_RULE : undefined,
|
|
2465
|
+
journalLine: pack ? `- [claimable] apply: ${pack}. ${TEACH_KEEP_RULE}` : undefined,
|
|
2466
|
+
});
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
function mintRichSearch({ cwd, query, rows, now, output, ensureApply, json } = {}) {
|
|
2470
|
+
const print = typeof output === 'function' ? output : (line = '') => console.log(line);
|
|
2471
|
+
const lesson = notesLessonFromText(searchLessonText(rows));
|
|
2472
|
+
if (json) return { thin: false, code: 0, lesson };
|
|
2473
|
+
if (isThinTeachLesson(lesson)) {
|
|
2474
|
+
printSearchLearnerGate(rows, { json: false }, print);
|
|
2475
|
+
return { thin: true, code: 0, lesson };
|
|
2476
|
+
}
|
|
2477
|
+
const saved = saveRichSearch({ cwd, query, lesson });
|
|
2478
|
+
const applyFn = ensureApply || ensureSearchApply;
|
|
2479
|
+
const applyCode = applyFn({
|
|
2480
|
+
cwd,
|
|
2481
|
+
query,
|
|
2482
|
+
packRel: saved.packRel,
|
|
2483
|
+
now: searchApplyNow(now),
|
|
2484
|
+
output: print,
|
|
2485
|
+
source: query,
|
|
2486
|
+
});
|
|
2487
|
+
if (ensureApply) return { thin: false, code: applyCode, lesson: saved.lesson };
|
|
2488
|
+
const baseline = proveSavedLearnerBaseline({
|
|
2489
|
+
cwd,
|
|
2490
|
+
applyRel: query ? searchApplyRel(query) : null,
|
|
2491
|
+
lesson: saved.lesson,
|
|
2492
|
+
output: print,
|
|
2493
|
+
json,
|
|
2494
|
+
});
|
|
2495
|
+
if (baseline !== 0) return { thin: false, code: baseline, lesson: saved.lesson };
|
|
2496
|
+
return { thin: false, code: applyCode, lesson: saved.lesson };
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2193
2499
|
function printSearchLearnerGate(rows, options, output) {
|
|
2194
2500
|
printLearnerCheckGate(output, notesLessonFromText(searchLessonText(rows)), {
|
|
2195
2501
|
includeCheck: true,
|
|
@@ -2197,6 +2503,40 @@ function printSearchLearnerGate(rows, options, output) {
|
|
|
2197
2503
|
});
|
|
2198
2504
|
}
|
|
2199
2505
|
|
|
2506
|
+
function gateSearchLearner(rows, options, output, deps = {}) {
|
|
2507
|
+
if (options && options.json) return 0;
|
|
2508
|
+
const minted = mintRichSearch({
|
|
2509
|
+
cwd: deps.cwd || process.cwd(),
|
|
2510
|
+
query: options && options.query,
|
|
2511
|
+
rows,
|
|
2512
|
+
now: deps.now,
|
|
2513
|
+
output,
|
|
2514
|
+
ensureApply: deps.ensureApply,
|
|
2515
|
+
json: false,
|
|
2516
|
+
});
|
|
2517
|
+
if (minted.thin) printSearchTeachNext(rows, options, output);
|
|
2518
|
+
return minted.code;
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
function printSearchOutcome(rows, options, output, deps = {}) {
|
|
2522
|
+
printSearchRows(rows, options, output);
|
|
2523
|
+
return gateSearchLearner(rows, options, output, deps);
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
function resultStdout(result) {
|
|
2527
|
+
if (typeof result === 'string') return result;
|
|
2528
|
+
return String((result && result.stdout) || '');
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
function searchRowsFromResult(result) {
|
|
2532
|
+
return parseSearchStdout(resultStdout(result));
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
function finishSuccessfulSearch(rows, options, output, deps) {
|
|
2536
|
+
writeLocalSearchCache(options.query, rows, deps);
|
|
2537
|
+
return printSearchOutcome(rows, options, output, deps);
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2200
2540
|
function commandOnPath(name, deps = {}) {
|
|
2201
2541
|
const spawn = deps.spawnSync || spawnSync;
|
|
2202
2542
|
const result = spawn('sh', ['-c', `command -v ${shellSingleQuote(name)}`], {
|
|
@@ -2313,6 +2653,9 @@ async function runYoutubeSearch(args = [], deps = {}) {
|
|
|
2313
2653
|
return 2;
|
|
2314
2654
|
}
|
|
2315
2655
|
|
|
2656
|
+
let rows = searchRowsFromResult(result);
|
|
2657
|
+
if (rows.length) return finishSuccessfulSearch(rows, options, output, deps);
|
|
2658
|
+
|
|
2316
2659
|
let status = searchRunnerStatus(result);
|
|
2317
2660
|
if (status != null && status !== 0 && isLocalSearchRateLimited(result)) {
|
|
2318
2661
|
await waitLocalSearchBackoff(deps);
|
|
@@ -2326,15 +2669,16 @@ async function runYoutubeSearch(args = [], deps = {}) {
|
|
|
2326
2669
|
output('ytsearch and yt-dlp not found. Install yt-dlp or put ytsearch on PATH.');
|
|
2327
2670
|
return 2;
|
|
2328
2671
|
}
|
|
2672
|
+
rows = searchRowsFromResult(result);
|
|
2673
|
+
if (rows.length) return finishSuccessfulSearch(rows, options, output, deps);
|
|
2329
2674
|
status = searchRunnerStatus(result);
|
|
2330
2675
|
if (status != null && status !== 0 && isLocalSearchRateLimited(result)) {
|
|
2331
2676
|
const cached = readFreshLocalSearchCache(options.query, deps);
|
|
2332
2677
|
if (cached) {
|
|
2333
|
-
const
|
|
2334
|
-
|
|
2335
|
-
printSearchTeachNext(rows, options, output);
|
|
2678
|
+
const cachedRows = cached.rows.slice(0, options.limit);
|
|
2679
|
+
const code = printSearchOutcome(cachedRows, options, output, deps);
|
|
2336
2680
|
output(LOCAL_SEARCH_CACHE_NOTE);
|
|
2337
|
-
return
|
|
2681
|
+
return code;
|
|
2338
2682
|
}
|
|
2339
2683
|
output(LOCAL_SEARCH_RATE_LIMIT_MESSAGE);
|
|
2340
2684
|
return status == null ? 1 : status;
|
|
@@ -2347,19 +2691,9 @@ async function runYoutubeSearch(args = [], deps = {}) {
|
|
|
2347
2691
|
return status == null ? 1 : status;
|
|
2348
2692
|
}
|
|
2349
2693
|
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
output('no videos found');
|
|
2354
|
-
if (!options.json) printWatchTickNext(output);
|
|
2355
|
-
return 2;
|
|
2356
|
-
}
|
|
2357
|
-
|
|
2358
|
-
writeLocalSearchCache(options.query, rows, deps);
|
|
2359
|
-
printSearchRows(rows, options, output);
|
|
2360
|
-
printSearchLearnerGate(rows, options, output);
|
|
2361
|
-
printSearchTeachNext(rows, options, output);
|
|
2362
|
-
return 0;
|
|
2694
|
+
output('no videos found');
|
|
2695
|
+
if (!options.json) printWatchTickNext(output);
|
|
2696
|
+
return 2;
|
|
2363
2697
|
}
|
|
2364
2698
|
|
|
2365
2699
|
const YTTEACH_USAGE = 'usage: atris youtube teach <youtube-url> [--section N] [--save] [--recap TEXT] [--skip] | owed | next';
|
|
@@ -3292,33 +3626,23 @@ async function extractTeachSource(youtubeUrl, deps = {}) {
|
|
|
3292
3626
|
return deps.extractTeachSource(youtubeUrl, deps);
|
|
3293
3627
|
}
|
|
3294
3628
|
const runner = deps.spawnSync || spawnSync;
|
|
3295
|
-
const result = runner('yt-dlp',
|
|
3629
|
+
const result = runner('yt-dlp', ytDlpInfoArgs(youtubeUrl), {
|
|
3296
3630
|
encoding: 'utf8',
|
|
3297
3631
|
timeout: 20000,
|
|
3298
3632
|
maxBuffer: 10 * 1024 * 1024,
|
|
3299
3633
|
});
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
try {
|
|
3304
|
-
info = JSON.parse(result.stdout);
|
|
3305
|
-
} catch {
|
|
3306
|
-
return null;
|
|
3307
|
-
}
|
|
3308
|
-
|
|
3309
|
-
const selected = chooseCaptionTrack(info);
|
|
3310
|
-
if (!selected?.track?.url) return null;
|
|
3311
|
-
const rawCaption = await (deps.fetchCaptionText || fetchCaptionText)(selected.track.url);
|
|
3312
|
-
const cues = parseCaptionCues(rawCaption);
|
|
3634
|
+
const info = parseYtDlpInfoJson(result);
|
|
3635
|
+
const loaded = await loadCaptionRaw(info, youtubeUrl, deps);
|
|
3636
|
+
const cues = parseCaptionCues(loaded && loaded.raw);
|
|
3313
3637
|
if (!cues.length) return null;
|
|
3314
3638
|
|
|
3315
3639
|
return {
|
|
3316
|
-
id: info
|
|
3317
|
-
title: info.title || '',
|
|
3640
|
+
id: teachSourceVideoId(info, youtubeUrl),
|
|
3641
|
+
title: (info && info.title) || '',
|
|
3318
3642
|
url: youtubeUrl,
|
|
3319
|
-
durationSeconds: Number(info.duration || 0) || undefined,
|
|
3320
|
-
language:
|
|
3321
|
-
chapters: normalizeChapters(info.chapters, info.duration),
|
|
3643
|
+
durationSeconds: Number((info && info.duration) || 0) || undefined,
|
|
3644
|
+
language: loaded.language || 'unknown',
|
|
3645
|
+
chapters: normalizeChapters(info && info.chapters, info && info.duration),
|
|
3322
3646
|
cues,
|
|
3323
3647
|
};
|
|
3324
3648
|
}
|
|
@@ -3368,7 +3692,10 @@ async function runYoutubeTeach(args = [], deps = {}) {
|
|
|
3368
3692
|
}
|
|
3369
3693
|
}
|
|
3370
3694
|
|
|
3371
|
-
const source = await (deps.extractTeachSource || extractTeachSource)(parsed.url,
|
|
3695
|
+
const source = await (deps.extractTeachSource || extractTeachSource)(parsed.url, {
|
|
3696
|
+
...deps,
|
|
3697
|
+
workDir: deps.workDir || notesWorkDir(deps),
|
|
3698
|
+
});
|
|
3372
3699
|
if (!source || !Array.isArray(source.cues) || !source.cues.length) {
|
|
3373
3700
|
output('no english captions for this url. teach stays local and will not call process.');
|
|
3374
3701
|
return 2;
|
|
@@ -3492,7 +3819,15 @@ async function youtubeCommand(argv = process.argv.slice(3), deps = {}) {
|
|
|
3492
3819
|
output(JSON.stringify(data, null, 2));
|
|
3493
3820
|
} else {
|
|
3494
3821
|
output(formatYoutubeResult(data));
|
|
3495
|
-
|
|
3822
|
+
const mintCode = mintRichProcess({
|
|
3823
|
+
cwd: deps.cwd || process.cwd(),
|
|
3824
|
+
url: options.youtubeUrl,
|
|
3825
|
+
data,
|
|
3826
|
+
now: deps.now,
|
|
3827
|
+
output,
|
|
3828
|
+
ensureApply: deps.ensureApply,
|
|
3829
|
+
});
|
|
3830
|
+
if (mintCode !== 0) status = mintCode;
|
|
3496
3831
|
}
|
|
3497
3832
|
} catch (err) {
|
|
3498
3833
|
if (!err.applyRequired) output(err.message);
|
|
@@ -3510,10 +3845,12 @@ module.exports = {
|
|
|
3510
3845
|
parseYoutubeArgs,
|
|
3511
3846
|
buildYoutubePayload,
|
|
3512
3847
|
extractLocalTranscript,
|
|
3848
|
+
parseYtDlpInfoJson,
|
|
3513
3849
|
processYoutube,
|
|
3514
3850
|
shouldRetryWithLocalTranscript,
|
|
3515
3851
|
formatYoutubeResult,
|
|
3516
3852
|
fileBriefFromNotes,
|
|
3853
|
+
keptPrintedNotes,
|
|
3517
3854
|
ensureNotesApply,
|
|
3518
3855
|
unsaveYoutubeNotes,
|
|
3519
3856
|
APPLY_NEXT_MESSAGE,
|
|
@@ -3541,6 +3878,7 @@ module.exports = {
|
|
|
3541
3878
|
extractTeachNumbers,
|
|
3542
3879
|
extractTeachMechanisms,
|
|
3543
3880
|
extractTeachSource,
|
|
3881
|
+
readLocalCaptionText,
|
|
3544
3882
|
oneTeachCheck,
|
|
3545
3883
|
learnerCheckFromLesson,
|
|
3546
3884
|
scoreLearnerNeedles,
|
|
@@ -3556,6 +3894,11 @@ module.exports = {
|
|
|
3556
3894
|
notesExperimentSlug,
|
|
3557
3895
|
digestExperimentSlug,
|
|
3558
3896
|
watchExperimentSlug,
|
|
3897
|
+
processExperimentSlug,
|
|
3898
|
+
processApplyRel,
|
|
3899
|
+
searchExperimentSlug,
|
|
3900
|
+
searchApplyRel,
|
|
3901
|
+
firstRichWatchLesson,
|
|
3559
3902
|
fileTeachExperiment,
|
|
3560
3903
|
youtubeCommand,
|
|
3561
3904
|
};
|