feat(providers): phases 7.3/7.4/7.6/8.1 — provenance, health, NDJSON, contrat unique
CI / build-and-test (push) Successful in 14m43s

7.3: capturedAt/source au registre + 6 adaptateurs + module provenance.ts + ?debug=1 (search-transport.mjs). 7.4: ProviderHealthService + badge source degradee. 7.6: squelettes par provider + snapshots progressifs + transport NDJSON /api/search. 8.1: ProviderAdapter unifie (search enveloppe + channelContent/channelMeta/capabilities) via getProviderAdapter + test de contrat offline.
This commit is contained in:
2026-09-30 07:57:11 -04:00
parent 37681c4f53
commit 665a0f0ebd
87 changed files with 8850 additions and 550 deletions
+78
View File
@@ -47,6 +47,84 @@ export function getSearchMode() {
return 'innertube-first';
}
// --- Phase 4.4 : bascule automatique InnerTube -> scrape --------------------
/**
* InnerTube casse plus souvent que la Data API (changement de format, bot-check).
* On mesure le taux d'echec InnerTube sur une fenetre glissante et on bascule
* temporairement sur `scrape-first`, avec retour automatique et cooldown.
*
* Pourquoi cet etat est dans le process et pas en base : une bascule doit
* survivre a un redemarrage (les bases survivent au redemarrage, l'etat non),
* et `provider_metrics` est de toute facon purge au-dela de 24 h.
*/
const failover = {
/** null = bascule inactive, sinon { until, previousMode, reason }. */
active: null,
lastToggleAt: 0,
};
/** Seuil de declenchement, 0.2 = 20 % d'echecs InnerTube sur 1 h. */
function failoverThreshold() {
const v = Number(process.env.YT_INNERTUBE_AUTO_FAILOVER);
return Number.isFinite(v) && v > 0 && v < 1 ? v : 0.2;
}
/** Cooldown apres un retour au mode normal, pour eviter l'oscillation. */
function failoverCooldownMs() {
const v = Number(process.env.YT_INNERTUBE_FAILOVER_COOLDOWN_MS);
return Number.isFinite(v) && v > 0 ? v : 15 * 60 * 1000;
}
/**
* A appeler apres chaque tentative InnerTube.
* @param {boolean} ok
* @param {object} snapshot `providerMetricsSnapshot({ hours: 1 })`
*/
export function recordInnerTubeOutcome(ok, snapshot) {
try {
if (!ok) ytMetrics.innertubeErrors++;
const now = Date.now();
// Retour au mode normal des que le taux repasse sous le seuil, en respectant
// le cooldown anti-oscillation.
if (failover.active && now < failover.active.until) return failover.active;
if (failover.active && now - failover.lastToggleAt < failoverCooldownMs()) return failover.active;
if (!Array.isArray(snapshot) || snapshot.length === 0) return failover.active;
const row = snapshot.find((r) => r.provider === 'yt');
if (!row || !row.calls || row.calls < 3) return failover.active; // pas assez d'echantillons
if (row.errorRate >= failoverThreshold()) {
const previousMode = getSearchMode();
failover.active = { until: now + failoverCooldownMs(), previousMode, reason: `errorRate=${row.errorRate.toFixed(2)} calls=${row.calls}` };
failover.lastToggleAt = now;
console.warn(`[YT failover] InnerTube dégradé (${failover.active.reason}) -> scrape-first pendant ${Math.round(failoverCooldownMs() / 60000)} min`);
return failover.active;
}
if (failover.active) {
console.log(`[YT failover] InnerTube rétabli (errorRate=${(row.errorRate || 0).toFixed(2)}) -> retour ${failover.active.previousMode}`);
failover.active = null;
failover.lastToggleAt = now;
}
return failover.active;
} catch { return failover.active; }
}
export function getFailoverState() {
return failover.active ? { ...failover.active, mode: 'scrape-first' } : null;
}
/**
* Mode **effectif** : le mode configure, force sur `scrape-first` pendant un
* failover actif. `getSearchMode()` reste la lecture de l'intention (env) ;
* les appelants doivent utiliser cette fonction.
*/
export function getEffectiveSearchMode() {
if (failover.active && Date.now() < failover.active.until) {
const base = getSearchMode();
// On n'ecrase que les modes qui dependent d'InnerTube.
return base.startsWith('innertube') ? 'scrape-first' : base;
}
return getSearchMode();
}
export function getScrapeTtlMs() {
const v = Number(process.env.YT_SCRAPE_TTL_MS || 30 * 60 * 1000);
return Number.isFinite(v) && v > 0 ? v : 30 * 60 * 1000;