fix(twitch,rumble,shorts): replis serveur, embed Rumble autoritaire, refonte UX Shorts
CI / build-and-test (push) Successful in 14m15s

This commit is contained in:
2026-09-29 10:39:03 -04:00
parent 3fe05b3c30
commit f76a3795c2
6 changed files with 530 additions and 90 deletions
+75 -3
View File
@@ -1544,7 +1544,7 @@ export class YoutubeApiService {
public getTwitchTopStreamsPage(cursor?: string | null, limit = 24): Observable<PagedResult<Video>> {
return this.getTwitchAuthToken().pipe(timeout(30000),
switchMap(token => {
if (!token) return of({ items: [], nextCursor: null });
if (!token) return this.getTwitchServerTopStreams(limit);
const headers = new HttpHeaders({ 'Client-ID': this.resolveTwitchClientId()!, 'Authorization': `Bearer ${token}` });
const after = cursor ? `&after=${encodeURIComponent(cursor)}` : '';
const primary = `/api/twitch-api/helix/streams?first=${limit}${after}`;
@@ -1555,12 +1555,84 @@ export class YoutubeApiService {
items: (res.data || []).map((i: any) => this.mapTwitchStreamToVideo(i)),
nextCursor: res.pagination?.cursor || null,
} as PagedResult<Video>)),
catchError((e) => { console.error('Twitch top streams error', e); return of({ items: [], nextCursor: null }); })
// Helix direct vide/en erreur (proxy, 429, CORS…) : repli serveur.
switchMap(res => (res?.items?.length ? of(res) : this.getTwitchServerTopStreams(limit))),
catchError(() => this.getTwitchServerTopStreams(limit))
);
})
}),
catchError(() => this.getTwitchServerTopStreams(limit))
);
}
/**
* Top streams via le serveur (`GET /api/twitch/top-streams`, token +
* retries côté back). Repli fiable pour Home/Explore quand les appels
* Helix directs du navigateur échouent.
*/
public getTwitchServerTopStreams(limit = 24): Observable<PagedResult<Video>> {
const n = Math.min(50, Math.max(1, Number(limit) || 24));
return this.http.get<any>(`/api/twitch/top-streams?first=${n}`).pipe(
timeout(20000),
map(res => ({
items: ((res?.items || []) as any[]).map(i => this.mapTwitchServerItemToVideo(i)),
nextCursor: null,
} as PagedResult<Video>)),
catchError((e) => { console.error('Twitch server top streams error', e); return of({ items: [], nextCursor: null }); })
);
}
/**
* Recherche Twitch via le serveur unifié (`/api/search?providers=tw`,
* token + retries + filets côté back). Dernier repli des pages Explore.
*/
public searchTwitchViaServer(query: string, limit = 24): Observable<PagedResult<Video>> {
const q = String(query || '').trim();
if (!q) return this.getTwitchServerTopStreams(limit);
const n = Math.min(50, Math.max(1, Number(limit) || 24));
const url = `/api/search?q=${encodeURIComponent(q)}&providers=tw&pageSize=${n}`;
return this.http.get<any>(url).pipe(
timeout(25000),
map(res => {
const list = (res?.groups?.tw || res?.items || []) as any[];
return {
items: list.map(i => this.mapTwitchServerItemToVideo(i)).filter(v => !!v.videoId),
nextCursor: null,
} as PagedResult<Video>;
}),
catchError((e) => { console.error('Twitch server search error', e); return of({ items: [], nextCursor: null }); })
);
}
/** Mappe un item Twitch "forme serveur" (twitch.mjs) vers Video. */
private mapTwitchServerItemToVideo = (i: any): Video => {
const kind = String(i?.kind || '').toLowerCase();
const type = String(i?.type || 'video');
const login: string | undefined = i?.channelHandle || i?.channelExternalId || undefined;
const thumb: string = String(i?.thumbnail || i?.uploaderAvatar || '') ||
'https://static-cdn.jtvnw.net/ttv-static/404_preview-640x360.jpg';
return {
url: i?.url || (login ? `https://www.twitch.tv/${login}` : `/watch/${i?.id || ''}`),
videoId: String(i?.id || ''),
type,
provider: 'twitch',
kind: kind === 'clip' ? 'clip' : kind === 'vod' ? 'vod' : undefined,
title: i?.title || '',
thumbnail: thumb,
uploaderName: i?.uploaderName || '',
uploaderUrl: login ? `https://www.twitch.tv/${login}` : '',
uploaderAvatar: i?.uploaderAvatar || '',
channelId: i?.channelId || undefined,
channelHandle: login,
channelExternalId: login || undefined,
channelAvatarUrl: i?.uploaderAvatar || undefined,
uploadedDate: i?.publishedAt || '',
duration: Number(i?.duration || 0),
views: Number(i?.views || 0),
uploaded: i?.publishedAt ? Date.parse(i.publishedAt) : 0,
isLive: i?.isLive === true,
} as Video;
};
/**
* Recherche thématique Twitch robuste : VODs puis clips, avec fallback vers
* les top streams live si les deux sont quasi vides (évite les blocs vides