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
CI / build-and-test (push) Successful in 14m16s
This commit is contained in:
@@ -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>))
|
||||
);
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user