fix(twitch,rumble,shorts): replis serveur, embed Rumble autoritaire, refonte UX Shorts
CI / build-and-test (push) Successful in 14m15s
CI / build-and-test (push) Successful in 14m15s
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user