feat(mcp): serveur MCP stdio 7 outils + guide API/MCP + couverture tests
CI / build-and-test (push) Successful in 14m3s

- mcp/server.mjs : search, suggest, details, trending, transcript,
  transcript_text, health_check (client fin vers API REST)
- docs/API_MCP_GUIDE.md : reference complete REST + MCP + transcript
- tests : server/tests/api_coverage.test.mjs (17 cas HTTP),
  mcp/tools.test.mjs (7 outils sur stub), scripts test:mcp/test:api
- fix(db): DELETE playlist 500 FK -> metrique avant delete + cleanup
  enfants + recordPlaylistMetric tolerant (observabilite)
This commit is contained in:
2026-09-27 18:39:26 -04:00
parent d0d1c07535
commit 1812b86dd4
8 changed files with 1025 additions and 13 deletions
+12 -5
View File
@@ -3387,6 +3387,10 @@ async function transcriptViaYtDlp(url, langs, netOpts = {}) {
noWarnings: true,
noCheckCertificates: true,
noPlaylist: true,
// Espace les requêtes sous-titres : les rafales déclenchent des 429
// YouTube que les retries immédiats ne font qu'aggraver.
sleepRequests: 2,
sleepSubtitles: 5,
...netOpts,
output: path.join(dir, '%(id)s'),
});
@@ -3515,11 +3519,11 @@ r.get('/transcript/:provider/:videoId', transcriptLimiter, async (req, res) => {
const allowedSet = new Set(allowed.map(transcriptPrimary));
const keepAllowed = (cands) => (cands || []).filter(c => allowedSet.has(transcriptPrimary(c.lang)));
const baseCands = keepAllowed(normalized === 'youtube'
? firstPerLanguage(orderedTracks(meta, lang), 10)
? firstPerLanguage(orderedTracks(meta, lang), 6)
: orderedTracks(meta, lang).slice(0, 5));
// Server-side auto-translation only towards an allowed target language.
const candidates = normalized === 'youtube'
? baseCands.concat(keepAllowed(translatedFallbacks(baseCands, lang))).slice(0, 12)
? baseCands.concat(keepAllowed(translatedFallbacks(baseCands, lang))).slice(0, 8)
: baseCands;
for (const cand of candidates) {
let attempt = 0;
@@ -3534,11 +3538,12 @@ r.get('/transcript/:provider/:videoId', transcriptLimiter, async (req, res) => {
} catch (e) {
const msg = String(e?.message || e);
console.warn('[transcript] track fetch failed:', cand.lang, msg);
// One retry after a short pause on transient 429s.
if (msg.includes(':429') && attempt === 0) {
// Backoff exponentiel + jitter sur 429 (buckets YouTube à la minute) :
// 1 retry @1.5s ne suffit pas quand l'IP est limitée.
if (msg.includes(':429') && attempt < 2) {
sawRateLimit = true;
attempt += 1;
await sleep(1500);
await sleep([2000, 8000][attempt - 1] + Math.floor(Math.random() * 1000));
continue;
}
if (msg.includes(':429')) sawRateLimit = true;
@@ -3546,6 +3551,8 @@ r.get('/transcript/:provider/:videoId', transcriptLimiter, async (req, res) => {
}
}
if (lines.length) break;
// Refroidissement entre candidats quand YouTube limite (évite d'aggraver le 429).
if (sawRateLimit) await sleep(700);
}
if (!lines || lines.length === 0) {
// Direct timedtext fetches failed (429 / Sorry pages / impersonation):