feat(mcp): serveur MCP stdio 7 outils + guide API/MCP + couverture tests
CI / build-and-test (push) Successful in 14m3s
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:
+12
-5
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user