atris 3.51.0 → 3.52.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 +208 -17
- package/package.json +1 -1
package/commands/youtube.js
CHANGED
|
@@ -5,8 +5,9 @@ const fs = require('fs');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const https = require('https');
|
|
7
7
|
|
|
8
|
-
const YTNOTES_USAGE = 'usage: ytnotes <youtube-url> [haiku|atris-fast|gemini|grok|codex|cursor]';
|
|
8
|
+
const YTNOTES_USAGE = 'usage: ytnotes <youtube-url> [youtube-url-or-playlist...] [haiku|atris-fast|gemini|grok|codex|cursor]';
|
|
9
9
|
const YTNOTES_HINT = 'zero credits, local captions + a fast engine';
|
|
10
|
+
const NOTES_PLAYLIST_CAP = 10;
|
|
10
11
|
|
|
11
12
|
const DEFAULT_QUERY = [
|
|
12
13
|
'Create a timestamped YouTube brief for Atris.',
|
|
@@ -24,7 +25,7 @@ const ALLOWED_CAPTION_HOST_SUFFIXES = [
|
|
|
24
25
|
|
|
25
26
|
function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
|
|
26
27
|
output('');
|
|
27
|
-
output(`Usage: ${commandName} notes <youtube-url> [engine]`);
|
|
28
|
+
output(`Usage: ${commandName} notes <youtube-url> [youtube-url-or-playlist...] [engine]`);
|
|
28
29
|
output(` ${commandName} process <youtube-url> [options]`);
|
|
29
30
|
output(` ${commandName} digest [--days N]`);
|
|
30
31
|
output(` ${commandName} watch add <channel-url-or-@handle>`);
|
|
@@ -33,7 +34,8 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
|
|
|
33
34
|
output(` ${commandName} watch tick`);
|
|
34
35
|
output(` ${commandName} <youtube-url> [options]`);
|
|
35
36
|
output('');
|
|
36
|
-
output('notes = free local notes,
|
|
37
|
+
output('notes = free local notes for one url, several urls, or a playlist');
|
|
38
|
+
output('process = 5 credits cloud knowledge');
|
|
37
39
|
output('digest = one decision page from this week\'s video briefs');
|
|
38
40
|
output('watch = subscribed channels turn into briefs without a human');
|
|
39
41
|
output('Process a YouTube video through Atris using timestamped transcript-first analysis.');
|
|
@@ -52,6 +54,8 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
|
|
|
52
54
|
output('');
|
|
53
55
|
output('Examples:');
|
|
54
56
|
output(` ${commandName} notes https://www.youtube.com/watch?v=VIDEO_ID`);
|
|
57
|
+
output(` ${commandName} notes https://www.youtube.com/watch?v=VIDEO_ID https://youtu.be/OTHER_ID`);
|
|
58
|
+
output(` ${commandName} notes https://www.youtube.com/playlist?list=PLAYLIST_ID`);
|
|
55
59
|
output(` ${commandName} https://www.youtube.com/watch?v=VIDEO_ID`);
|
|
56
60
|
output(` ${commandName} process https://youtu.be/VIDEO_ID --query "Key takeaways"`);
|
|
57
61
|
output(` ${commandName} digest`);
|
|
@@ -464,6 +468,34 @@ function videoIdFromUrl(url) {
|
|
|
464
468
|
return short ? short[1] : null;
|
|
465
469
|
}
|
|
466
470
|
|
|
471
|
+
function looksLikeYoutubeUrl(arg) {
|
|
472
|
+
const text = String(arg || '').trim();
|
|
473
|
+
if (!text || text.startsWith('-')) return false;
|
|
474
|
+
return /youtube\.com|youtu\.be/i.test(text);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function isPlaylistUrl(url) {
|
|
478
|
+
const text = String(url || '');
|
|
479
|
+
return /[?&]list=/.test(text) || /\/playlist(?:\?|$|\/)/i.test(text);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function parseNotesArgs(argv = []) {
|
|
483
|
+
const urls = [];
|
|
484
|
+
let engine = null;
|
|
485
|
+
let help = false;
|
|
486
|
+
for (const raw of argv) {
|
|
487
|
+
const arg = String(raw);
|
|
488
|
+
if (arg === '--help' || arg === '-h' || arg === 'help') {
|
|
489
|
+
help = true;
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (arg.startsWith('-')) continue;
|
|
493
|
+
if (looksLikeYoutubeUrl(arg)) urls.push(arg);
|
|
494
|
+
else engine = arg;
|
|
495
|
+
}
|
|
496
|
+
return { urls, engine, help };
|
|
497
|
+
}
|
|
498
|
+
|
|
467
499
|
function dateStamp(now) {
|
|
468
500
|
if (typeof now === 'string' && /^\d{4}-\d{2}-\d{2}/.test(now)) {
|
|
469
501
|
return now.slice(0, 10);
|
|
@@ -512,6 +544,7 @@ function fileBriefFromNotes({ cwd, url, workDir, now } = {}) {
|
|
|
512
544
|
fs.writeFileSync(journalPath, `${existing}${prefix}${line}\n`);
|
|
513
545
|
|
|
514
546
|
console.log(`brief filed: ${relBrief}`);
|
|
547
|
+
return relBrief;
|
|
515
548
|
} catch {
|
|
516
549
|
// notes filing must never break the youtube command
|
|
517
550
|
}
|
|
@@ -1015,37 +1048,189 @@ async function watchCommand(args = [], deps = {}) {
|
|
|
1015
1048
|
return 2;
|
|
1016
1049
|
}
|
|
1017
1050
|
|
|
1018
|
-
function
|
|
1019
|
-
const
|
|
1020
|
-
const
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1051
|
+
function defaultPlaylistExpander(playlistUrl, deps = {}) {
|
|
1052
|
+
const spawn = deps.spawnSync || spawnSync;
|
|
1053
|
+
const result = spawn('yt-dlp', [
|
|
1054
|
+
'--no-update',
|
|
1055
|
+
'--flat-playlist',
|
|
1056
|
+
'--print',
|
|
1057
|
+
'%(id)s|%(title)s',
|
|
1058
|
+
playlistUrl,
|
|
1059
|
+
], {
|
|
1060
|
+
encoding: 'utf8',
|
|
1061
|
+
timeout: 60000,
|
|
1062
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
1063
|
+
});
|
|
1064
|
+
if (result.error || (result.status != null && result.status !== 0)) {
|
|
1065
|
+
const detail = String(result.stderr || result.error?.message || 'playlist expand failed').trim();
|
|
1066
|
+
throw new Error(detail || 'playlist expand failed');
|
|
1026
1067
|
}
|
|
1068
|
+
return parseFlatPlaylist(result.stdout);
|
|
1069
|
+
}
|
|
1027
1070
|
|
|
1071
|
+
function defaultNotesItemRunner(url, engine, deps = {}) {
|
|
1028
1072
|
const script = path.join(__dirname, '..', 'scripts', 'det', 'ytnotes');
|
|
1029
1073
|
const spawn = deps.spawnSync || spawnSync;
|
|
1030
1074
|
const childArgs = engine ? [url, engine] : [url];
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1075
|
+
return spawn(script, childArgs, { stdio: 'inherit' });
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function readNowMs(deps = {}) {
|
|
1079
|
+
if (typeof deps.nowMs === 'function') return Number(deps.nowMs()) || 0;
|
|
1080
|
+
if (Number.isFinite(deps.nowMs)) return Number(deps.nowMs);
|
|
1081
|
+
return Date.now();
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function notesItemLabel(item = {}) {
|
|
1085
|
+
return item.id || item.url || '';
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function expandNotesTargets(urls = [], deps = {}) {
|
|
1089
|
+
const output = deps.output || ((line = '') => console.error(line));
|
|
1090
|
+
const expander = deps.expander || ((playlistUrl) => defaultPlaylistExpander(playlistUrl, deps));
|
|
1091
|
+
const items = [];
|
|
1092
|
+
for (const url of urls) {
|
|
1093
|
+
if (!isPlaylistUrl(url)) {
|
|
1094
|
+
items.push({ url, id: videoIdFromUrl(url) });
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
1097
|
+
let videos = [];
|
|
1098
|
+
try {
|
|
1099
|
+
videos = expander(url, deps);
|
|
1100
|
+
} catch {
|
|
1101
|
+
items.push({ url, id: videoIdFromUrl(url), failed: true });
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
if (!Array.isArray(videos) || videos.length === 0) {
|
|
1105
|
+
items.push({ url, id: videoIdFromUrl(url), failed: true });
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
if (videos.length > NOTES_PLAYLIST_CAP) {
|
|
1109
|
+
output(`playlist capped at ${NOTES_PLAYLIST_CAP} videos (${videos.length} found)`);
|
|
1110
|
+
videos = videos.slice(0, NOTES_PLAYLIST_CAP);
|
|
1111
|
+
}
|
|
1112
|
+
for (const video of videos) {
|
|
1113
|
+
if (!video?.id) continue;
|
|
1114
|
+
items.push({
|
|
1115
|
+
url: `https://www.youtube.com/watch?v=${video.id}`,
|
|
1116
|
+
id: video.id,
|
|
1117
|
+
title: video.title,
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
return items;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function invokeNotesRunner(url, engine, deps = {}) {
|
|
1125
|
+
const runner = deps.runner;
|
|
1126
|
+
if (runner) return runner(url, engine, deps);
|
|
1127
|
+
return defaultNotesItemRunner(url, engine, deps);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function readRunnerStatus(result) {
|
|
1131
|
+
if (typeof result === 'number') return result;
|
|
1132
|
+
if (result && typeof result === 'object') {
|
|
1133
|
+
return result.status == null ? 1 : result.status;
|
|
1134
|
+
}
|
|
1135
|
+
return 1;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function fileNotesBrief(url, deps = {}) {
|
|
1139
|
+
const briefFiler = deps.briefFiler || fileBriefFromNotes;
|
|
1140
|
+
try {
|
|
1141
|
+
const filed = briefFiler({
|
|
1035
1142
|
cwd: deps.cwd || process.cwd(),
|
|
1036
1143
|
url,
|
|
1037
1144
|
workDir: deps.workDir || path.join(process.env.TMPDIR || '/tmp', 'ytnotes'),
|
|
1038
1145
|
now: deps.now || new Date(),
|
|
1039
1146
|
});
|
|
1147
|
+
return typeof filed === 'string' && filed ? filed : null;
|
|
1148
|
+
} catch {
|
|
1149
|
+
return null;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function runOneNotesItem(item, engine, deps = {}) {
|
|
1154
|
+
const output = deps.output || ((line = '') => console.error(line));
|
|
1155
|
+
const label = notesItemLabel(item);
|
|
1156
|
+
if (item.failed) {
|
|
1157
|
+
output(`${label} 0s FAILED`);
|
|
1158
|
+
return { url: item.url, id: item.id, seconds: 0, ok: false, brief: null };
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
const started = readNowMs(deps);
|
|
1162
|
+
let status = 1;
|
|
1163
|
+
try {
|
|
1164
|
+
status = readRunnerStatus(invokeNotesRunner(item.url, engine, deps));
|
|
1165
|
+
} catch {
|
|
1166
|
+
status = 1;
|
|
1167
|
+
}
|
|
1168
|
+
const brief = status === 0 ? fileNotesBrief(item.url, deps) : null;
|
|
1169
|
+
const seconds = Math.max(0, Math.round((readNowMs(deps) - started) / 1000));
|
|
1170
|
+
const ok = status === 0;
|
|
1171
|
+
output(`${label} ${seconds}s ${ok ? (brief || 'ok') : 'FAILED'}`);
|
|
1172
|
+
return { url: item.url, id: item.id, seconds, ok, brief };
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function formatNotesSummary(rows = []) {
|
|
1176
|
+
const lines = ['url or id seconds result'];
|
|
1177
|
+
for (const row of rows) {
|
|
1178
|
+
const result = row.ok ? (row.brief || 'ok') : 'FAILED';
|
|
1179
|
+
lines.push(`${notesItemLabel(row)} ${row.seconds}s ${result}`);
|
|
1180
|
+
}
|
|
1181
|
+
return lines.join('\n');
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function runYoutubeNotesBatch({ urls, engine } = {}, deps = {}) {
|
|
1185
|
+
const output = deps.output || ((line = '') => console.error(line));
|
|
1186
|
+
const items = expandNotesTargets(urls || [], deps);
|
|
1187
|
+
const rows = [];
|
|
1188
|
+
for (const item of items) {
|
|
1189
|
+
rows.push(runOneNotesItem(item, engine, deps));
|
|
1190
|
+
}
|
|
1191
|
+
if (rows.length) {
|
|
1192
|
+
output('');
|
|
1193
|
+
output(formatNotesSummary(rows));
|
|
1194
|
+
}
|
|
1195
|
+
if (!rows.length) return 2;
|
|
1196
|
+
return rows.some((row) => row.ok) ? 0 : 2;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function runSingleYoutubeNotes(url, engine, deps = {}) {
|
|
1200
|
+
let result;
|
|
1201
|
+
try {
|
|
1202
|
+
result = invokeNotesRunner(url, engine, deps);
|
|
1203
|
+
} catch {
|
|
1204
|
+
return 1;
|
|
1205
|
+
}
|
|
1206
|
+
const status = readRunnerStatus(result);
|
|
1207
|
+
if (status === 0) fileNotesBrief(url, deps);
|
|
1208
|
+
return status;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function runYoutubeNotes(args = [], deps = {}) {
|
|
1212
|
+
const output = deps.output || ((line = '') => console.error(line));
|
|
1213
|
+
const parsed = parseNotesArgs(args);
|
|
1214
|
+
if (parsed.help) {
|
|
1215
|
+
showYoutubeHelp(output, deps.commandName || 'atris youtube');
|
|
1216
|
+
return 0;
|
|
1217
|
+
}
|
|
1218
|
+
if (!parsed.urls.length) {
|
|
1219
|
+
output(YTNOTES_USAGE);
|
|
1220
|
+
output(YTNOTES_HINT);
|
|
1221
|
+
return 2;
|
|
1222
|
+
}
|
|
1223
|
+
if (parsed.urls.length === 1 && !isPlaylistUrl(parsed.urls[0])) {
|
|
1224
|
+
return runSingleYoutubeNotes(parsed.urls[0], parsed.engine, deps);
|
|
1040
1225
|
}
|
|
1041
|
-
return
|
|
1226
|
+
return runYoutubeNotesBatch(parsed, deps);
|
|
1042
1227
|
}
|
|
1043
1228
|
|
|
1044
1229
|
async function youtubeCommand(argv = process.argv.slice(3), deps = {}) {
|
|
1045
1230
|
const output = deps.output || ((line = '') => console.log(line));
|
|
1046
1231
|
if (argv[0] === 'notes') {
|
|
1047
1232
|
const code = runYoutubeNotes(argv.slice(1), deps);
|
|
1048
|
-
if (!deps.output && !deps.spawnSync) process.exit(code);
|
|
1233
|
+
if (!deps.output && !deps.spawnSync && !deps.runner && !deps.expander) process.exit(code);
|
|
1049
1234
|
return code;
|
|
1050
1235
|
}
|
|
1051
1236
|
if (argv[0] === 'digest') {
|
|
@@ -1078,6 +1263,12 @@ module.exports = {
|
|
|
1078
1263
|
shouldRetryWithLocalTranscript,
|
|
1079
1264
|
formatYoutubeResult,
|
|
1080
1265
|
fileBriefFromNotes,
|
|
1266
|
+
looksLikeYoutubeUrl,
|
|
1267
|
+
isPlaylistUrl,
|
|
1268
|
+
parseNotesArgs,
|
|
1269
|
+
expandNotesTargets,
|
|
1270
|
+
runYoutubeNotesBatch,
|
|
1271
|
+
runYoutubeNotes,
|
|
1081
1272
|
parseDigestArgs,
|
|
1082
1273
|
collectVideoBriefs,
|
|
1083
1274
|
buildDigestPrompt,
|