fix(shorts): pré-validation (clips morts, Rumble 404), autoplay Rumble, onError auto-skip, zone sous-vidéo retirée
CI / build-and-test (push) Successful in 14m16s

This commit is contained in:
2026-09-29 11:23:33 -04:00
parent f76a3795c2
commit 3f4cd56f14
3 changed files with 255 additions and 125 deletions
+65 -12
View File
@@ -1715,14 +1715,31 @@ export class YoutubeApiService {
const fallback = `/proxy/twitch-api/helix/clips?game_id=${encodeURIComponent(gameId)}&first=20&started_at=${encodeURIComponent(since)}${after}`;
return this.http.get<any>(primary, { headers }).pipe(
catchError(() => this.http.get<any>(fallback, { headers })),
map(res => {
const clips = Array.isArray(res?.data) ? res.data : [];
const page: string | null = res?.pagination?.cursor || null;
return {
items: clips.map((i: any) => this.mapTwitchClipToVideo(i)),
// Jeu suivant quand la page est épuisée (rotation infinie).
nextCursor: page ? `${gameId}::${page}` : this.nextTwitchGameCursor(gameId),
} as PagedResult<Video>;
map(res => ({
clips: Array.isArray(res?.data) ? res.data : [],
page: (res?.pagination?.cursor || null) as string | null,
})),
// Ne garde que les clips existants (les morts affichent
// "time machine" dans le player).
switchMap(({ clips, page }) => {
const ids = clips.map((c: any) => String(c?.id || '')).filter(Boolean);
if (!ids.length) {
return of({
items: [],
nextCursor: page ? `${gameId}::${page}` : this.nextTwitchGameCursor(gameId),
} as PagedResult<Video>);
}
return this.validateTwitchClipIds(ids).pipe(
map(valid => ({
items: clips.filter((c: any) => valid.has(String(c?.id || ''))).map((i: any) => this.mapTwitchClipToVideo(i)),
// Jeu suivant quand la page est épuisée (rotation infinie).
nextCursor: page ? `${gameId}::${page}` : this.nextTwitchGameCursor(gameId),
} as PagedResult<Video>)),
catchError(() => of({
items: clips.map((i: any) => this.mapTwitchClipToVideo(i)),
nextCursor: page ? `${gameId}::${page}` : this.nextTwitchGameCursor(gameId),
} as PagedResult<Video>))
);
}),
catchError(() => of({
items: [],
@@ -1733,6 +1750,33 @@ export class YoutubeApiService {
);
}
/**
* Valide l'existence des clips par lot (`GET /helix/clips?id=…`, 100 max).
* Les clips supprimés/expirés affichent "Sorry. Unless you've got a time
* machine, that content is unavailable." dans le player : on les exclut
* AVANT affichage. En cas d'échec de validation, le doute profite aux
* clips (on garde tout) pour ne jamais vider le flux.
*/
public validateTwitchClipIds(ids: string[]): Observable<Set<string>> {
const clean = Array.from(new Set((ids || []).map(s => String(s || '').trim()).filter(Boolean))).slice(0, 100);
if (!clean.length) return of(new Set<string>());
return this.getTwitchAuthToken().pipe(timeout(15000),
switchMap(token => {
if (!token) return of(new Set<string>(clean));
const headers = new HttpHeaders({ 'Client-ID': this.resolveTwitchClientId()!, 'Authorization': `Bearer ${token}` });
const params = clean.map(s => `id=${encodeURIComponent(s)}`).join('&');
const primary = `/api/twitch-api/helix/clips?${params}`;
const fallback = `/proxy/twitch-api/helix/clips?${params}`;
return this.http.get<any>(primary, { headers }).pipe(
catchError(() => this.http.get<any>(fallback, { headers })),
map(res => new Set<string>((Array.isArray(res?.data) ? res.data : []).map((c: any) => String(c?.id || '')).filter(Boolean))),
catchError(() => of(new Set<string>(clean)))
);
}),
catchError(() => of(new Set<string>(clean)))
);
}
/** Curseur vers le jeu suivant (boucle infinie sur les top games). */
private nextTwitchGameCursor(currentGameId: string): string | null {
const games = this.twitchTopGameIds || [];
@@ -1795,10 +1839,19 @@ export class YoutubeApiService {
});
return forkJoin(perUser).pipe(
map((lists: any[][]) => lists.flat().slice(0, 24)),
map((clips: any[]) => ({
items: clips.map((i: any) => this.mapTwitchClipToVideo(i)),
nextCursor: sRes?.pagination?.cursor || null,
} as PagedResult<Video>)),
// Exclut les clips morts ("time machine") avant affichage.
switchMap((clips: any[]) => {
const ids = clips.map((c: any) => String(c?.id || '')).filter(Boolean);
if (!ids.length) {
return of({ items: [], nextCursor: sRes?.pagination?.cursor || null } as PagedResult<Video>);
}
return this.validateTwitchClipIds(ids).pipe(
map(valid => ({
items: clips.filter((c: any) => valid.has(String(c?.id || ''))).map((i: any) => this.mapTwitchClipToVideo(i)),
nextCursor: sRes?.pagination?.cursor || null,
} as PagedResult<Video>))
);
}),
catchError(() => of({ items: [], nextCursor: null } as PagedResult<Video>))
);
}),