feat(channel): page details chaine multi-providers avec onglets et infinite scroll
CI / build-and-test (push) Successful in 13m34s
CI / build-and-test (push) Successful in 13m34s
- Strategy Pattern front: ChannelProviderInterface + 6 providers (yt/dm/tw/pt/od/ru) + factory (ids longs/courts) - ChannelContentService: cache par onglet/tri/recherche (TTL 5min), pagination nextPage - ChannelPage: header (banniere/avatar/subs/description ...plus), tabs dynamiques par capabilities, grilles videos/shorts 9:16/playlists/live, recherche debounced, tri recent/populaire, infinite scroll IntersectionObserver, skeletons/empty/error - Backend: GET /api/channels/:provider/:id/content (videos/shorts/playlists/live) natif par provider
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Classification partagée Vidéo / Short / Live.
|
||||
*
|
||||
* Règles (alignées front + back) :
|
||||
* - `isShort === true` (flag natif) ⇒ Short, sauf durée connue > 75 s.
|
||||
* - Twitch `kind === 'clip'` ⇒ Short (les clips font ≤ 60 s par construction).
|
||||
* - YouTube : URL `/shorts/` ⇒ Short ; sinon durée 1..70 s ⇒ Short.
|
||||
* - Autres providers : durée 1..75 s ⇒ Short.
|
||||
* - IMPORTANT : une durée `0` / absente ne qualifie JAMAIS un Short
|
||||
* (les scrapers renvoient 0 quand la durée est inconnue).
|
||||
*/
|
||||
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;
|
||||
isVertical?: boolean | null;
|
||||
}
|
||||
|
||||
export const SHORT_MAX_SECONDS = 75;
|
||||
export const YOUTUBE_SHORT_MAX_SECONDS = 70;
|
||||
|
||||
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();
|
||||
|
||||
// Clips Twitch = équivalent Shorts (durée max 60 s côté Twitch).
|
||||
if (kind === 'clip') return true;
|
||||
if (type === 'short') return true;
|
||||
|
||||
// Flag natif (Rumble router, YouTube, …) — ignoré si la durée connue le contredit.
|
||||
if ((v as { isShort?: unknown }).isShort === true) {
|
||||
if (hasKnownDuration && duration > SHORT_MAX_SECONDS) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!hasKnownDuration) return false;
|
||||
if (url.includes('/shorts/')) return true;
|
||||
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';
|
||||
}
|
||||
Reference in New Issue
Block a user