feat(providers): phases 7.3/7.4/7.6/8.1 — provenance, health, NDJSON, contrat unique
CI / build-and-test (push) Successful in 14m43s
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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user