refactor(video): uniformiser classification et routage par section
CI / build-and-test (push) Successful in 14m9s
CI / build-and-test (push) Successful in 14m9s
Source unique de classification (video-kind.ts) + politique de section centralisee (section-policy.ts). Les composants ne portent plus de seuils ni de regles de type maison. - section-policy.ts: sectionAccepts / routeForVideo / shortsFeedAccepts / isPlayable + classifyOnce (enrichissement _kind a l'ingestion) - watch-short: separe classification (shortsFeedAccepts) et jouabilite (isPlayable: allowEmbed DM, live/channel Twitch, Rumble 404); supprime le seuil Rumble 90 s et l'import YOUTUBE_SHORT_MAX_SECONDS du composant - home: filtre sectionAccepts(...,'home') (plus de shorts dans le trending), routeForVideo, pagination bouclee (min 20, max 5 iterations) - themes: _kind pre-calcule une fois, sectionAccepts, capacites live lues depuis le registre providers (fin de LIVE_PROVIDERS codes en dur) - video-kind: helpers isNativeVerticalProvider / hasKnownDuration - routes: alias /shorts/:id et /live/:id pour routeForVideo - tests: section-policy.spec.ts + video-kind.spec.ts etendus; npm run test:section
This commit is contained in:
@@ -172,7 +172,7 @@
|
||||
<div class="absolute top-2 left-2 flex gap-1">
|
||||
<span *ngIf="isLiveCard(v)" class="bg-purple-600 text-white text-[11px] font-bold px-1.5 py-0.5 rounded">LIVE</span>
|
||||
<span *ngIf="!isLiveCard(v) && isClipCard(v)" class="bg-red-600 text-white text-[11px] font-bold px-1.5 py-0.5 rounded">CLIP</span>
|
||||
<span *ngIf="!isLiveCard(v) && kindOf(v) === 'short' && !isClipCard(v)" class="bg-red-600 text-white text-[11px] font-bold px-1.5 py-0.5 rounded">SHORT</span>
|
||||
<span *ngIf="!isLiveCard(v) && v._kind === 'short' && !isClipCard(v)" class="bg-red-600 text-white text-[11px] font-bold px-1.5 py-0.5 rounded">SHORT</span>
|
||||
<span *ngIf="isChannelCard(v)" class="bg-slate-700/90 text-slate-200 text-[11px] font-bold px-1.5 py-0.5 rounded ring-1 ring-slate-500/50">CHAÎNE</span>
|
||||
</div>
|
||||
<div *ngIf="v.duration && v.duration > 0" class="absolute bottom-2 right-2 bg-black/75 text-white text-xs px-2 py-1 rounded-md font-mono">
|
||||
|
||||
@@ -12,7 +12,8 @@ import { DurationPipe } from '../../app/shared/pipes/duration.pipe';
|
||||
import { ProviderBadgeComponent } from '../../app/shared/components/provider-badge/provider-badge.component';
|
||||
import { ChannelIdentityComponent } from '../../app/shared/components/channel-identity/channel-identity.component';
|
||||
import { InfiniteAnchorComponent } from '../shared/infinite-anchor/infinite-anchor.component';
|
||||
import { classifyVideo, isLiveVideo, isShortVideo } from '../../app/shared/utils/video-kind';
|
||||
import { classifyOnce, type WithKind } from '../../app/shared/utils/section-policy';
|
||||
import { providerSupportsLive } from '../../app/core/providers/provider-registry';
|
||||
import { map, of, switchMap, catchError, forkJoin, Observable } from 'rxjs';
|
||||
import { timeout } from 'rxjs/operators';
|
||||
import { PagedResult } from '../../services/youtube-api.service';
|
||||
@@ -73,7 +74,7 @@ export class ProviderThemePageComponent implements OnDestroy {
|
||||
date = signal<'any' | 'day' | 'week' | 'month' | 'year'>('any');
|
||||
|
||||
// Data state
|
||||
allVideos = signal<Video[]>([]);
|
||||
allVideos = signal<WithKind<Video>[]>([]);
|
||||
nextCursor = signal<string | null>(null);
|
||||
// Curseurs par section Twitch (les paginations Helix sont indépendantes).
|
||||
private twitchCursors = { live: null as string | null, vod: null as string | null, clip: null as string | null, channel: null as string | null };
|
||||
@@ -96,39 +97,20 @@ export class ProviderThemePageComponent implements OnDestroy {
|
||||
return this.applyFiltersWithType(this.allVideos(), 'any');
|
||||
}
|
||||
|
||||
twitchLives = computed(() => {
|
||||
const p = this.provider();
|
||||
return this.twitchFilteredNoType().filter(v =>
|
||||
isLiveVideo({ ...v, provider: (v.provider as string) || p }));
|
||||
});
|
||||
twitchVods = computed(() => {
|
||||
const p = this.provider();
|
||||
return this.twitchFilteredNoType().filter(v => {
|
||||
const w = { ...v, provider: (v.provider as string) || p };
|
||||
if (isLiveVideo(w)) return false;
|
||||
if (isShortVideo(w)) return false;
|
||||
if (String(v.type || '').toLowerCase() === 'channel') return false;
|
||||
return true;
|
||||
});
|
||||
});
|
||||
twitchClips = computed(() => {
|
||||
const p = this.provider();
|
||||
return this.twitchFilteredNoType().filter(v => {
|
||||
const w = { ...v, provider: (v.provider as string) || p };
|
||||
if (isLiveVideo(w)) return false;
|
||||
if (String(v.type || '').toLowerCase() === 'channel') return false;
|
||||
if (String((v as any).kind || '').toLowerCase() === 'clip') return true;
|
||||
return isShortVideo(w);
|
||||
});
|
||||
});
|
||||
twitchChannels = computed(() => {
|
||||
const p = this.provider();
|
||||
return this.twitchFilteredNoType().filter(v => {
|
||||
if ((v as any).isLive === true) return false;
|
||||
if (isLiveVideo({ ...v, provider: (v.provider as string) || p })) return false;
|
||||
return String(v.type || '').toLowerCase() === 'channel';
|
||||
});
|
||||
});
|
||||
// Sections Twitch regroupées sur `_kind` pré-calculé (les chaînes hors-ligne
|
||||
// ont leur propre onglet, plus de doublon dans "En direct").
|
||||
twitchLives = computed(() =>
|
||||
this.twitchFilteredNoType().filter(v => v._kind === 'live' && !this.isChannelCard(v))
|
||||
);
|
||||
twitchVods = computed(() =>
|
||||
this.twitchFilteredNoType().filter(v => v._kind === 'video')
|
||||
);
|
||||
twitchClips = computed(() =>
|
||||
this.twitchFilteredNoType().filter(v => v._kind === 'short')
|
||||
);
|
||||
twitchChannels = computed(() =>
|
||||
this.twitchFilteredNoType().filter(v => this.isChannelCard(v))
|
||||
);
|
||||
|
||||
/** Onglet Twitch synchronisé avec le filtre "type" existant. */
|
||||
twitchTab = computed<'all' | 'live' | 'video' | 'clip' | 'channel'>(() => {
|
||||
@@ -231,8 +213,7 @@ export class ProviderThemePageComponent implements OnDestroy {
|
||||
const isLiveTheme = themeSlug === 'live';
|
||||
|
||||
// Fournisseurs sans aucun direct : aucune vidéo, message explicite.
|
||||
const LIVE_PROVIDERS: Provider[] = ['youtube', 'twitch'];
|
||||
if (isLiveTheme && !LIVE_PROVIDERS.includes(provider)) {
|
||||
if (isLiveTheme && !providerSupportsLive(provider)) {
|
||||
this.error.set('Ce fournisseur ne propose pas de directs.');
|
||||
this.loading.set(false);
|
||||
this.loadingMore.set(false);
|
||||
@@ -262,15 +243,13 @@ export class ProviderThemePageComponent implements OnDestroy {
|
||||
next: (res: PagedResult<Video>) => {
|
||||
if (snapshotVersion !== this.version) return;
|
||||
|
||||
// Thème live : garde-fou, ne jamais mélanger des VODs.
|
||||
let items = res.items || [];
|
||||
if (isLiveTheme) {
|
||||
items = items.filter((v: Video) =>
|
||||
isLiveVideo({ ...v, provider: (v.provider as string) || provider })
|
||||
);
|
||||
}
|
||||
// Classification calculée UNE fois par item, puis thème live garde-fou.
|
||||
let items: WithKind<Video>[] = (res.items || []).map((v: Video) =>
|
||||
classifyOnce({ ...(v as object), provider: (v.provider as string) || provider } as Video)
|
||||
);
|
||||
if (isLiveTheme) items = items.filter(v => v._kind === 'live');
|
||||
const existingIds = new Set(this.allVideos().map((v: Video) => v.videoId));
|
||||
const newVideos = items.filter((v: Video) => !existingIds.has(v.videoId));
|
||||
const newVideos = items.filter((v: WithKind<Video>) => !existingIds.has(v.videoId));
|
||||
|
||||
this.allVideos.update(current => [...current, ...newVideos]);
|
||||
this.nextCursor.set(res.nextCursor || null);
|
||||
@@ -309,7 +288,10 @@ export class ProviderThemePageComponent implements OnDestroy {
|
||||
if (snapshotVersion !== this.version) return;
|
||||
if (err) this.error.set(err);
|
||||
const existing = new Set(this.allVideos().map(v => `${v.type}:${v.videoId}`));
|
||||
const fresh = items.filter(v => !existing.has(`${v.type}:${v.videoId}`));
|
||||
// `_kind` posé ici (ingestion), plus aucune reclassification au render.
|
||||
const fresh = items
|
||||
.filter(v => !existing.has(`${v.type}:${v.videoId}`))
|
||||
.map(v => classifyOnce({ ...(v as object), provider: (v.provider as string) || provider } as Video));
|
||||
if (fresh.length) this.allVideos.update(cur => [...cur, ...fresh]);
|
||||
this.nextCursor.set(next);
|
||||
this.loading.set(false);
|
||||
@@ -419,33 +401,22 @@ export class ProviderThemePageComponent implements OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
// Filtres de type basés sur `_kind` (classification pré-calculée, plus de
|
||||
// règle maison ici). Les chaînes hors-ligne gardent leur traitement brut.
|
||||
const typeFilter = typeOverride;
|
||||
if (typeFilter !== 'any') {
|
||||
const p = this.provider();
|
||||
arr = arr.filter(v => {
|
||||
const withProvider = { ...v, provider: (v.provider as string) || p };
|
||||
if (typeFilter === 'video') return !isShortVideo(withProvider) && !isLiveVideo(withProvider) && String(v.type || '').toLowerCase() !== 'channel' && (v.type === 'video' || v.type === 'recorded');
|
||||
if (typeFilter === 'live') return isLiveVideo(withProvider);
|
||||
if (typeFilter === 'recorded') return !isLiveVideo(withProvider) && String(v.type || '').toLowerCase() !== 'channel' && (v.type === 'recorded' || (v.type === 'video' && !isShortVideo(withProvider)));
|
||||
// Shorts / Clips : classification partagée + kind='clip' (durée parfois absente).
|
||||
if (typeFilter === 'short') {
|
||||
if (isLiveVideo(withProvider)) return false;
|
||||
if (String(v.type || '').toLowerCase() === 'channel') return false;
|
||||
if (String((v as any).kind || '').toLowerCase() === 'clip') return true;
|
||||
return isShortVideo(withProvider);
|
||||
}
|
||||
// Chaînes : hors-ligne uniquement (les lives restent dans "Direct").
|
||||
if (typeFilter === 'channel') {
|
||||
if ((v as any).isLive === true) return false;
|
||||
if (isLiveVideo(withProvider) && String(v.type || '').toLowerCase() !== 'channel') return false;
|
||||
return String(v.type || '').toLowerCase() === 'channel';
|
||||
}
|
||||
const type = String(v.type || '').toLowerCase();
|
||||
if (typeFilter === 'video') return v._kind === 'video' && type !== 'channel' && (type === 'video' || type === 'recorded');
|
||||
if (typeFilter === 'live') return v._kind === 'live';
|
||||
if (typeFilter === 'recorded') return v._kind !== 'live' && type !== 'channel' && (type === 'recorded' || (type === 'video' && v._kind !== 'short'));
|
||||
if (typeFilter === 'short') return v._kind === 'short';
|
||||
if (typeFilter === 'channel') return this.isChannelCard(v);
|
||||
return true;
|
||||
});
|
||||
} else if (this.theme() === 'live') {
|
||||
// Thème live même si l'utilisateur remet le filtre sur "Any" : LIVE seulement.
|
||||
const p = this.provider();
|
||||
arr = arr.filter(v => isLiveVideo({ ...v, provider: (v.provider as string) || p }));
|
||||
arr = arr.filter(v => v._kind === 'live');
|
||||
}
|
||||
|
||||
const langFilter = this.language();
|
||||
@@ -504,19 +475,9 @@ export class ProviderThemePageComponent implements OnDestroy {
|
||||
this.type.set(v === 'shorts' ? 'short' : v === 'videos' ? 'video' : 'any');
|
||||
}
|
||||
|
||||
kindOf(v: Video): string {
|
||||
const p = this.provider();
|
||||
return classifyVideo({ ...v, provider: (v.provider as string) || p });
|
||||
}
|
||||
|
||||
/** Badges Twitch : LIVE seulement si réellement en direct (pas les chaînes hors-ligne). */
|
||||
isLiveCard(v: Video): boolean {
|
||||
if ((v as any).isLive === true) return true;
|
||||
if (String(v.type || '').toLowerCase() === 'live') return true;
|
||||
if (String(v.type || '').toLowerCase() === 'channel') return false;
|
||||
const p = this.provider();
|
||||
return isLiveVideo({ ...v, provider: (v.provider as string) || p })
|
||||
&& String(v.type || '').toLowerCase() !== 'channel';
|
||||
return v._kind === 'live' && !this.isChannelCard(v);
|
||||
}
|
||||
|
||||
isChannelCard(v: Video): boolean {
|
||||
@@ -526,17 +487,14 @@ export class ProviderThemePageComponent implements OnDestroy {
|
||||
|
||||
isClipCard(v: Video): boolean {
|
||||
if (this.isLiveCard(v) || this.isChannelCard(v)) return false;
|
||||
if (String((v as any).kind || '').toLowerCase() === 'clip') return true;
|
||||
const p = this.provider();
|
||||
return isShortVideo({ ...v, provider: (v.provider as string) || p });
|
||||
return v._kind === 'short';
|
||||
}
|
||||
|
||||
/** Affichage "Tout" Twitch : sections empilées façon directory. */
|
||||
showTwitchSectionsAll = computed(() => this.isTwitchSections() && this.twitchTab() === 'all');
|
||||
|
||||
shortsCount(): number {
|
||||
const p = this.provider();
|
||||
return this.allVideos().filter(v => isShortVideo({ ...v, provider: (v.provider as string) || p })).length;
|
||||
return this.allVideos().filter(v => v._kind === 'short').length;
|
||||
}
|
||||
onLanguageChange(val: any) { this.language.set(val as any); }
|
||||
onDateChange(val: any) { this.date.set(val as any); }
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
|
||||
<!-- Error / empty -->
|
||||
<div *ngIf="!b.loading && b.error" class="text-amber-300">{{ b.error }}</div>
|
||||
<div *ngIf="!b.loading && !b.error && b.items.length === 0 && themeSlug() === 'live' && !isLiveProvider(b.provider)" class="text-slate-400">Ce fournisseur ne propose pas de directs.</div>
|
||||
<div *ngIf="!b.loading && !b.error && b.items.length === 0 && !(themeSlug() === 'live' && !isLiveProvider(b.provider))" class="text-slate-400">{{ 'empty.noItems' | t }}</div>
|
||||
<div *ngIf="!b.loading && !b.error && b.items.length === 0 && themeSlug() === 'live' && !providerSupportsLive(b.provider)" class="text-slate-400">Ce fournisseur ne propose pas de directs.</div>
|
||||
<div *ngIf="!b.loading && !b.error && b.items.length === 0 && !(themeSlug() === 'live' && !providerSupportsLive(b.provider))" class="text-slate-400">{{ 'empty.noItems' | t }}</div>
|
||||
<div *ngIf="!b.loading && !b.error && b.items.length > 0 && filteredItems(b).length === 0" class="text-slate-400">
|
||||
Aucun {{ contentFilter() === 'shorts' ? 'Short' : 'vidéo' }} pour ce fournisseur — essayez l'autre filtre.
|
||||
</div>
|
||||
@@ -56,18 +56,18 @@
|
||||
<app-provider-badge [provider]="b.provider"></app-provider-badge>
|
||||
</div>
|
||||
<div class="absolute top-2 left-2 flex gap-1">
|
||||
<span *ngIf="isLiveCard(v, b.provider)" class="bg-purple-600 text-white text-[11px] font-bold px-1.5 py-0.5 rounded">LIVE</span>
|
||||
<span *ngIf="!isLiveCard(v, b.provider) && isClipCard(v, b.provider)" class="bg-red-600 text-white text-[11px] font-bold px-1.5 py-0.5 rounded">CLIP</span>
|
||||
<span *ngIf="!isLiveCard(v, b.provider) && !isClipCard(v, b.provider) && kindOf(v, b.provider) === 'short'" class="bg-red-600 text-white text-[11px] font-bold px-1.5 py-0.5 rounded">SHORT</span>
|
||||
<span *ngIf="isLiveCard(v)" class="bg-purple-600 text-white text-[11px] font-bold px-1.5 py-0.5 rounded">LIVE</span>
|
||||
<span *ngIf="!isLiveCard(v) && isClipCard(v)" class="bg-red-600 text-white text-[11px] font-bold px-1.5 py-0.5 rounded">CLIP</span>
|
||||
<span *ngIf="!isLiveCard(v) && !isClipCard(v) && v._kind === 'short'" class="bg-red-600 text-white text-[11px] font-bold px-1.5 py-0.5 rounded">SHORT</span>
|
||||
<span *ngIf="isChannelCard(v)" class="bg-slate-700/90 text-slate-200 text-[11px] font-bold px-1.5 py-0.5 rounded ring-1 ring-slate-500/50">CHAÎNE</span>
|
||||
</div>
|
||||
<div class="absolute bottom-2 right-2 bg-black/70 text-white text-xs px-2 py-0.5 rounded" *ngIf="v.duration && v.duration > 0">
|
||||
{{ v.duration | duration }}
|
||||
</div>
|
||||
<div *ngIf="isLiveCard(v, b.provider) && v.views > 0" class="absolute bottom-2 left-2 bg-black/70 text-white text-xs px-2 py-0.5 rounded">
|
||||
<div *ngIf="isLiveCard(v) && v.views > 0" class="absolute bottom-2 left-2 bg-black/70 text-white text-xs px-2 py-0.5 rounded">
|
||||
{{ v.views | number }} spectateurs
|
||||
</div>
|
||||
<div class="absolute bottom-2 left-2" *ngIf="!isLiveCard(v, b.provider)">
|
||||
<div class="absolute bottom-2 left-2" *ngIf="!isLiveCard(v)">
|
||||
<app-like-button [videoId]="v.videoId" [provider]="b.provider" [title]="v.title" [thumbnail]="v.thumbnail"></app-like-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,14 +10,15 @@ import { LikeButtonComponent } from '../shared/components/like-button/like-butto
|
||||
import { ProviderBadgeComponent } from '../../app/shared/components/provider-badge/provider-badge.component';
|
||||
import { ChannelIdentityComponent } from '../../app/shared/components/channel-identity/channel-identity.component';
|
||||
import { DurationPipe } from '../../app/shared/pipes/duration.pipe';
|
||||
import { classifyVideo, isLiveVideo, isShortVideo } from '../../app/shared/utils/video-kind';
|
||||
import { sectionAccepts, classifyOnce, type WithKind } from '../../app/shared/utils/section-policy';
|
||||
import { providerSupportsLive } from '../../app/core/providers/provider-registry';
|
||||
|
||||
interface ProviderBlock {
|
||||
provider: Provider;
|
||||
label: string;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
items: Video[];
|
||||
items: WithKind<Video>[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -48,42 +49,37 @@ export class ThemePageComponent {
|
||||
this.contentFilter.set(v);
|
||||
}
|
||||
|
||||
/** Fournisseurs capables de fournir de vrais directs. */
|
||||
private static readonly LIVE_PROVIDERS: Provider[] = ['youtube', 'twitch'];
|
||||
/** Fournisseurs capables de fournir de vrais directs (source unique = registre). */
|
||||
readonly providerSupportsLive = providerSupportsLive;
|
||||
|
||||
isLiveTheme(): boolean {
|
||||
return this.themeSlug() === 'live';
|
||||
}
|
||||
|
||||
isLiveProvider(p: Provider): boolean {
|
||||
return (ThemePageComponent.LIVE_PROVIDERS as Provider[]).includes(p);
|
||||
}
|
||||
|
||||
/** Items du bloc après application du filtre Vidéos/Shorts (+ LIVE seulement sur thème live). */
|
||||
filteredItems(b: ProviderBlock): Video[] {
|
||||
let items = b.items;
|
||||
// Thème "En direct" : ne jamais mélanger avec des VODs (garde-fou si une API renvoie du non-live).
|
||||
if (this.themeSlug() === 'live') {
|
||||
items = items.filter(v => isLiveVideo({ ...v, provider: (v.provider as string) || b.provider }));
|
||||
}
|
||||
const f = this.contentFilter();
|
||||
if (f === 'all') return items;
|
||||
return items.filter(v =>
|
||||
f === 'shorts' ? isShortVideo({ ...v, provider: (v.provider as string) || b.provider }) : !isShortVideo({ ...v, provider: (v.provider as string) || b.provider })
|
||||
/** Enrichit une page : `_kind` calculé UNE fois à l'ingestion. */
|
||||
private enrich(items: Video[], provider: Provider): WithKind<Video>[] {
|
||||
return (items || []).map(v =>
|
||||
classifyOnce({ ...(v as object), provider: (v.provider as string) || provider } as Video)
|
||||
);
|
||||
}
|
||||
|
||||
kindOf(v: Video, provider: Provider): string {
|
||||
return classifyVideo({ ...v, provider: (v.provider as string) || provider });
|
||||
/** Items du bloc après filtre Vidéos/Shorts (+ LIVE seulement sur thème live). */
|
||||
filteredItems(b: ProviderBlock): Video[] {
|
||||
let items: Video[] = b.items;
|
||||
// Thème "En direct" : ne jamais mélanger avec des VODs (garde-fou si une API renvoie du non-live).
|
||||
if (this.themeSlug() === 'live') {
|
||||
items = items.filter(v => sectionAccepts(v, 'theme-live'));
|
||||
}
|
||||
const f = this.contentFilter();
|
||||
if (f === 'all') return items;
|
||||
return items.filter(v => f === 'shorts'
|
||||
? sectionAccepts(v, 'theme-shorts')
|
||||
: sectionAccepts(v, 'theme-videos'));
|
||||
}
|
||||
|
||||
/** LIVE seulement si réellement en direct (les chaînes hors-ligne ne le sont pas). */
|
||||
isLiveCard(v: Video, provider: Provider): boolean {
|
||||
if ((v as any).isLive === true) return true;
|
||||
if (String(v.type || '').toLowerCase() === 'live') return true;
|
||||
if (String(v.type || '').toLowerCase() === 'channel') return false;
|
||||
return isLiveVideo({ ...v, provider: (v.provider as string) || provider })
|
||||
&& String(v.type || '').toLowerCase() !== 'channel';
|
||||
isLiveCard(v: Video): boolean {
|
||||
return v._kind === 'live' && !this.isChannelCard(v);
|
||||
}
|
||||
|
||||
isChannelCard(v: Video): boolean {
|
||||
@@ -91,14 +87,13 @@ export class ThemePageComponent {
|
||||
return String(v.type || '').toLowerCase() === 'channel';
|
||||
}
|
||||
|
||||
isClipCard(v: Video, provider: Provider): boolean {
|
||||
if (this.isLiveCard(v, provider) || this.isChannelCard(v)) return false;
|
||||
if (String((v as any).kind || '').toLowerCase() === 'clip') return true;
|
||||
return isShortVideo({ ...v, provider: (v.provider as string) || provider });
|
||||
isClipCard(v: Video): boolean {
|
||||
if (this.isLiveCard(v) || this.isChannelCard(v)) return false;
|
||||
return v._kind === 'short';
|
||||
}
|
||||
|
||||
shortsCount(b: ProviderBlock): number {
|
||||
return b.items.filter(v => isShortVideo({ ...v, provider: (v.provider as string) || b.provider })).length;
|
||||
return b.items.filter(v => v._kind === 'short').length;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
@@ -137,7 +132,7 @@ export class ThemePageComponent {
|
||||
|
||||
private fetchForProvider(block: ProviderBlock, slug: string) {
|
||||
// Thème "En direct" : les fournisseurs sans live n'affichent aucune vidéo.
|
||||
if (slug === 'live' && !this.isLiveProvider(block.provider)) {
|
||||
if (slug === 'live' && !providerSupportsLive(block.provider)) {
|
||||
block.loading = false;
|
||||
block.error = null;
|
||||
block.items = [];
|
||||
@@ -168,7 +163,7 @@ export class ThemePageComponent {
|
||||
if (slug === 'trending') {
|
||||
this.api.getTrendingPage(null, block.provider).subscribe({
|
||||
next: res => {
|
||||
block.items = (res.items || []).slice(0, max);
|
||||
block.items = this.enrich(res.items || [], block.provider).slice(0, max);
|
||||
block.loading = false;
|
||||
if (!block.items.length) console.info('[ThemePage] Empty block page', { provider: block.provider, theme: slug });
|
||||
this.blocks.update(a => [...a]);
|
||||
@@ -195,8 +190,8 @@ export class ThemePageComponent {
|
||||
const mode = block.provider === 'youtube' ? 'youtube_eventType_live' : 'twitch_top_streams';
|
||||
liveCall.subscribe({
|
||||
next: res => {
|
||||
block.items = (res.items || [])
|
||||
.filter(v => isLiveVideo({ ...v, provider: (v.provider as string) || block.provider }))
|
||||
block.items = this.enrich(res.items || [], block.provider)
|
||||
.filter(v => v._kind === 'live')
|
||||
.slice(0, max);
|
||||
block.loading = false;
|
||||
if (!block.items.length) console.info('[ThemePage] Empty block page', { provider: block.provider, theme: slug, mode });
|
||||
@@ -220,7 +215,7 @@ export class ThemePageComponent {
|
||||
const raw = tokens.join(' ').trim();
|
||||
const q = (raw.split(/[^A-Za-z0-9à-ÿ]+/).filter(w => w.length >= 3)[0] || raw).trim();
|
||||
const show = (items: Video[]) => {
|
||||
block.items = (items || []).slice(0, max);
|
||||
block.items = this.enrich(items || [], block.provider).slice(0, max);
|
||||
block.loading = false;
|
||||
if (!block.items.length) console.info('[ThemePage] Empty block page', { provider: block.provider, theme: slug, query: q });
|
||||
this.blocks.update(a => [...a]);
|
||||
@@ -254,7 +249,7 @@ export class ThemePageComponent {
|
||||
// fallback to trending
|
||||
this.api.getTrendingPage(null, block.provider).subscribe({
|
||||
next: res => {
|
||||
block.items = (res.items || []).slice(0, max);
|
||||
block.items = this.enrich(res.items || [], block.provider).slice(0, max);
|
||||
block.loading = false;
|
||||
if (!block.items.length) console.info('[ThemePage] Empty block page', { provider: block.provider, theme: slug, mode: 'fallback_trending' });
|
||||
this.blocks.update(a => [...a]);
|
||||
@@ -272,7 +267,7 @@ export class ThemePageComponent {
|
||||
} else {
|
||||
this.api.searchVideosPage(q, null, block.provider).subscribe({
|
||||
next: res => {
|
||||
block.items = (res.items || []).slice(0, max);
|
||||
block.items = this.enrich(res.items || [], block.provider).slice(0, max);
|
||||
block.loading = false;
|
||||
if (!block.items.length) console.info('[ThemePage] Empty block page', { provider: block.provider, theme: slug, query: q });
|
||||
this.blocks.update(a => [...a]);
|
||||
|
||||
Reference in New Issue
Block a user