tuna-agent 0.1.155 → 0.1.156

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.
@@ -116,6 +116,65 @@ function run(cmd, args, opts = {}) {
116
116
  p.on('close', (code) => code === 0 ? resolve({ out, err }) : reject(new Error(`${cmd} exit ${code}: ${err.slice(0, 500)}`)));
117
117
  });
118
118
  }
119
+ // Modern Chrome UA — TikTok/Douyin reject unknown user-agents.
120
+ const SRC_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
121
+ // Optional cookies for auth-gated sources (Facebook private/page videos,
122
+ // some Douyin). YT_DLP_COOKIES = path to a Netscape cookies.txt; or
123
+ // YT_DLP_COOKIES_FROM_BROWSER = a browser name yt-dlp can read cookies from.
124
+ function cookieArgs() {
125
+ if (process.env.YT_DLP_COOKIES)
126
+ return ['--cookies', process.env.YT_DLP_COOKIES];
127
+ if (process.env.YT_DLP_COOKIES_FROM_BROWSER)
128
+ return ['--cookies-from-browser', process.env.YT_DLP_COOKIES_FROM_BROWSER];
129
+ return [];
130
+ }
131
+ function detectPlatform(url) {
132
+ let h = '';
133
+ try {
134
+ h = new URL(url).hostname.toLowerCase();
135
+ }
136
+ catch { /* malformed → 'other' */ }
137
+ if (/(^|\.)youtube\.com$|(^|\.)youtu\.be$/.test(h))
138
+ return 'youtube';
139
+ if (/(^|\.)tiktok\.com$/.test(h))
140
+ return 'tiktok';
141
+ if (/douyin\.com$|iesdouyin\.com$/.test(h))
142
+ return 'douyin';
143
+ if (/facebook\.com$|fb\.watch$|(^|\.)fb\.com$/.test(h))
144
+ return 'facebook';
145
+ return 'other';
146
+ }
147
+ // Download a source video across YouTube / TikTok / Douyin / Facebook.
148
+ // yt-dlp supports all of them, but a single rigid `-f` that works for
149
+ // YouTube fails on the others, so try a tolerant 720p-capped format then
150
+ // fall back to letting yt-dlp pick. UA + optional cookies harden the
151
+ // non-YouTube extractors (FB private/page + Douyin need cookies).
152
+ async function downloadSourceVideo(url, dest) {
153
+ const platform = detectPlatform(url);
154
+ const common = [
155
+ '--no-playlist', '--no-warnings', '--retries', '3', '--fragment-retries', '3',
156
+ '--user-agent', SRC_UA, ...cookieArgs(), '-o', dest, url,
157
+ ];
158
+ const attempts = [
159
+ ['-f', 'bv*[height<=720]+ba/b[height<=720]/best', ...common],
160
+ ['-f', 'best/mp4', ...common], // let yt-dlp choose (TikTok/Douyin/FB quirks)
161
+ ];
162
+ let lastErr;
163
+ for (let i = 0; i < attempts.length; i++) {
164
+ try {
165
+ await run(YT_DLP, attempts[i]);
166
+ return;
167
+ }
168
+ catch (e) {
169
+ lastErr = e;
170
+ console.warn(`[analyze_video] yt-dlp attempt ${i + 1}/${attempts.length} failed (${platform}): ${String(e?.message || e).slice(0, 220)}`);
171
+ }
172
+ }
173
+ const hint = (platform === 'facebook' || platform === 'douyin')
174
+ ? ' — FB private/page & some Douyin need cookies (set YT_DLP_COOKIES)'
175
+ : '';
176
+ throw new Error(`yt-dlp failed for ${platform} after ${attempts.length} attempts${hint}: ${String(lastErr?.message || lastErr).slice(0, 300)}`);
177
+ }
119
178
  async function whisperTranscribe(audioPath) {
120
179
  if (!OPENAI_KEY)
121
180
  throw new Error('OPENAI_API_KEY not set');
@@ -450,7 +509,7 @@ export async function analyzeVideo(url, onProgress) {
450
509
  // analyze of the same URL never reads a half-written file.
451
510
  const dlTmp = path.join(CACHE_DIR, `${urlHash}.dl-${crypto.randomBytes(4).toString('hex')}.mp4`);
452
511
  try {
453
- await run(YT_DLP, ['-f', 'best[height<=720]/best', '-o', dlTmp, '--no-playlist', '--quiet', url]);
512
+ await downloadSourceVideo(url, dlTmp);
454
513
  await fs.rename(dlTmp, videoPath);
455
514
  }
456
515
  catch (e) {
@@ -462,7 +521,7 @@ export async function analyzeVideo(url, onProgress) {
462
521
  // clone idea gets a real name instead of "Clone: www.youtube.com".
463
522
  let source_title = '';
464
523
  try {
465
- const t = await run(YT_DLP, ['--skip-download', '--no-warnings', '--no-playlist', '--print', '%(title)s', url]);
524
+ const t = await run(YT_DLP, ['--skip-download', '--no-warnings', '--no-playlist', '--user-agent', SRC_UA, ...cookieArgs(), '--print', '%(title)s', url]);
466
525
  source_title = (t.out || '').trim().split('\n')[0].slice(0, 200);
467
526
  }
468
527
  catch { /* title is best-effort — analysis still proceeds without it */ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tuna-agent",
3
- "version": "0.1.155",
3
+ "version": "0.1.156",
4
4
  "description": "Tuna Agent - Run AI coding tasks on your machine",
5
5
  "bin": {
6
6
  "tuna-agent": "dist/cli/index.js"