/** * Classification partagée Vidéo / Short / Live. * * Miroir exact de `isShortItem()` (`server/search-filters.mjs`). Ordre : * 1. `kind === 'clip'` (Twitch) ⇒ Short (les clips font ≤ 60 s par * construction ; une durée aberrante est du bruit, pas une preuve). * 2. Flag natif (`isShort`, `type === 'short'`, URL `/shorts/`) ⇒ Short, * sauf durée connue > SHORT_MAX_SECONDS. * 3. Orientation verticale stricte (`height > width`, ratio ≤ 0.8) ET durée * connue ≤ VERTICAL_SHORT_MAX_SECONDS (90 s) ⇒ Short. * 4. Verticale SANS durée connue ⇒ PAS un Short (même doctrine que * « durée 0 ne qualifie jamais »). * 5. Repli durée seule : 1..70 s (YouTube), 1..75 s (autres). */ export type VideoKind = 'short' | 'live' | 'video'; export interface ClassifiableVideo { duration?: number | null; type?: string | null; kind?: string | null; url?: string | null; provider?: string | null; isShort?: boolean | null; isLive?: boolean | null; width?: number | null; height?: number | null; videoId?: string | null; id?: string | null; /** Métadonnées brutes provider (ex. Dailymotion `allowEmbed`). */ raw?: { allowEmbed?: boolean } | null; } export const SHORT_MAX_SECONDS = 75; export const YOUTUBE_SHORT_MAX_SECONDS = 70; export const VERTICAL_SHORT_MAX_SECONDS = 90; // ponytail: ratio 0.8 arbitré, 0.75 si de faux carrés Instagram apparaissent. export const VERTICAL_MAX_RATIO = 0.8; /** * Fournisseurs à production nativement verticale dont le scraper renvoie * souvent une durée inconnue (0). La tolérance « durée inconnue » du flux * Shorts s'appuie sur cette liste, PAS sur une règle de type (cf. * `shortsFeedAccepts` dans section-policy.ts). */ export const NATIVE_VERTICAL_PROVIDERS = new Set(['rumble', 'odysee']); /** Le provider est-il nativement vertical (flux Shorts dédié) ? */ export function isNativeVerticalProvider(provider?: string | null): boolean { return NATIVE_VERTICAL_PROVIDERS.has(String(provider || '').toLowerCase()); } /** Durée connue = nombre strictement positif (0 / null / undefined = inconnu). */ export function hasKnownDuration(v: ClassifiableVideo | null | undefined): boolean { const d = Number((v as { duration?: unknown })?.duration ?? 0); return Number.isFinite(d) && d > 0; } function numOrZero(v: unknown): number { const n = Number(v); return Number.isFinite(n) && n > 0 ? n : 0; } export function isShortVideo(v: ClassifiableVideo | null | undefined): boolean { if (!v) return false; const duration = Number((v as { duration?: unknown }).duration ?? 0); const hasKnownDuration = Number.isFinite(duration) && duration > 0; const type = String((v as { type?: unknown }).type || '').toLowerCase(); const kind = String((v as { kind?: unknown }).kind || '').toLowerCase(); const url = String((v as { url?: unknown }).url || ''); const provider = String((v as { provider?: unknown }).provider || '').toLowerCase(); // 1) Clips Twitch = équivalent Shorts. if (kind === 'clip') return true; // 2) Flag natif — ignoré si la durée connue le contredit. if ((v as { isShort?: unknown }).isShort === true || type === 'short' || url.includes('/shorts/')) { if (hasKnownDuration && duration > SHORT_MAX_SECONDS) return false; return true; } // 3) + 4) Orientation (width/height quand le provider les expose). const w = numOrZero((v as { width?: unknown }).width); const h = numOrZero((v as { height?: unknown }).height); if (w > 0 && h > 0) { const vertical = h > w && w / h <= VERTICAL_MAX_RATIO; if (vertical) { if (!hasKnownDuration) return false; return duration <= VERTICAL_SHORT_MAX_SECONDS; } } // 5) Repli durée seule (comportement historique inchangé). if (!hasKnownDuration) return false; if (provider === 'youtube') return duration <= YOUTUBE_SHORT_MAX_SECONDS; return duration <= SHORT_MAX_SECONDS; } export function isLiveVideo(v: ClassifiableVideo | null | undefined): boolean { if (!v) return false; if ((v as { isLive?: unknown }).isLive === true) return true; const type = String((v as { type?: unknown }).type || '').toLowerCase(); return type === 'live' || type === 'stream' || type === 'channel'; } export function classifyVideo(v: ClassifiableVideo | null | undefined): VideoKind { if (isShortVideo(v)) return 'short'; if (isLiveVideo(v)) return 'live'; return 'video'; }