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
+45 -1
View File
@@ -75,6 +75,45 @@ export function parseDurationLabel(label) {
} catch { return undefined; }
}
/** "il y a 2 mois" / "Streamed 3 weeks ago" / "2 days ago" -> ISO, ou undefined. */
export function parseRelativeDate(text, nowMs = Date.now()) {
try {
const s = String(text || '').toLowerCase().trim();
if (!s) return undefined;
// Une date absolue ("17 janv. 2024", "2024-01-17") passe deja par Date.parse.
if (/\d{4}-\d{2}-\d{2}/.test(s)) {
const abs = Date.parse(s);
if (Number.isFinite(abs) && abs > 0 && abs < 8.64e15) return new Date(abs).toISOString();
}
// Fenetre temporelle : YouTube annonce "il y a 2 heures", "3 weeks ago", ...
const re = /(\d+)\s*(second|minute|hour|day|week|month|year|seconde|heure|jour|semaine|mois|an|annee|année)/;
const m = s.match(re);
if (!m) {
// "hier" / "yesterday" / "aujourd'hui" / "today" : cas sans quantite.
if (/\b(yesterday|hier)\b/.test(s)) return new Date(nowMs - 864e5).toISOString();
if (/\b(today|aujourd'hui)\b/.test(s)) return new Date(nowMs).toISOString();
if (/\bjust now|just now|maintenant|venir de\b/.test(s)) return new Date(nowMs).toISOString();
return undefined;
}
const n = Math.max(0, Number(m[1]));
if (!Number.isFinite(n)) return undefined;
const unit = m[2];
const table = {
second: 1e3, seconde: 1e3,
minute: 6e4,
hour: 36e5, heure: 36e5,
day: 864e5, jour: 864e5,
week: 6048e5, semaine: 6048e5,
month: 26298e6, mois: 26298e6,
year: 315576e5, an: 315576e5, annee: 315576e5, année: 315576e5,
};
const mult = table[unit];
if (!mult) return undefined;
const ms = nowMs - (n * mult);
return Number.isFinite(ms) && ms > 0 ? new Date(ms).toISOString() : undefined;
} catch { return undefined; }
}
function bestThumb(thumbs) {
try {
const arr = Array.isArray(thumbs) ? thumbs.filter((t) => t?.url) : [];
@@ -151,7 +190,12 @@ export function mapVideoNode(n) {
type: 'video',
...(Number.isFinite(duration) && duration > 0 ? { duration } : {}),
...(views !== undefined ? { views } : {}),
...(n.published?.text ? { publishedAt: String(n.published.text) } : {}),
// Phase 1.9 - `n.published.text` est un LIBELLE RELATIF ("il y a 2 mois"),
// que Date.parse() renvoie en NaN. On le convertit en ISO : sans cela le
// filtre `period=week` etait inoperant sur YouTube et `sort=date` triait
// sur NaN. Si le libelle est incomprehensible, publishedAt reste absent
// (jamais de date inventee).
...(n.published?.text ? { publishedAt: parseRelativeDate(n.published.text) } : {}),
...(authorId ? { channelId: authorId, channelExternalId: authorId, channelUrl: `https://www.youtube.com/channel/${authorId}` } : {}),
...(authorName ? { channelHandle: String(authorName) } : {}),
...(n.is_live ? { isLive: true } : {}),