fix(themes): pages themes Twitch vides -> requetes Helix tokenisees + fallbacks game_id/top-streams
This commit is contained in:
@@ -193,12 +193,15 @@ export class ProviderThemePageComponent implements OnDestroy {
|
||||
const baseTokens = this.themes.tokensFor(slug);
|
||||
|
||||
if (provider === 'twitch') {
|
||||
// Helix /search/channels et /search/categories ne matchent que des noms
|
||||
// proches : une longue phrase multi-mots ("cooking food Food Cooking
|
||||
// Recipe") ne retourne jamais rien. On envoie UN mot-clé court ; le
|
||||
// service tente ensuite les variantes + game_id + top streams.
|
||||
const twitchTokens = TWITCH_THEME_KEYWORDS[slug] || [];
|
||||
const merged = [...new Set([...twitchTokens, ...baseTokens])];
|
||||
const value = merged.join(' ').trim();
|
||||
if (value) return value;
|
||||
if (twitchTokens.length) return twitchTokens.join(' ');
|
||||
return slug.replace(/-/g, ' ');
|
||||
const first = (twitchTokens[0] || baseTokens[0] || slug.replace(/-/g, ' ') || '').trim();
|
||||
// Garde le premier mot pour maximiser le taux de match Helix.
|
||||
const short = String(first).split(/[^A-Za-z0-9à-ÿ]+/).filter(Boolean)[0] || first;
|
||||
return (short || first || slug.replace(/-/g, ' ')).trim();
|
||||
}
|
||||
|
||||
const value = baseTokens.join(' ').trim();
|
||||
|
||||
@@ -215,8 +215,10 @@ export class ThemePageComponent {
|
||||
return;
|
||||
}
|
||||
// Twitch hors live/trending : VODs + clips + fallback top streams.
|
||||
// Helix ne matche pas les longues phrases : un seul mot-clé court.
|
||||
if (block.provider === 'twitch') {
|
||||
const q = tokens.join(' ');
|
||||
const raw = tokens.join(' ').trim();
|
||||
const q = (raw.split(/[^A-Za-z0-9à-ÿ]+/).filter(w => w.length >= 3)[0] || raw).trim();
|
||||
const call = q ? this.api.searchTwitchThemePage(q, null) : this.api.getTwitchTopStreamsPage(null);
|
||||
call.subscribe({
|
||||
next: res => {
|
||||
|
||||
@@ -121,6 +121,50 @@ export class YoutubeApiService {
|
||||
// ... (rest of the code remains the same)
|
||||
|
||||
// ===================== Twitch Search Helpers =====================
|
||||
/**
|
||||
* Découpe une requête thème (ex. "cooking food Food Cooking Recipe") en
|
||||
* mots-clés unitaires exploitables par Helix : /search/channels et
|
||||
* /search/categories ne matchent que des noms proches, jamais de longues
|
||||
* phrases multi-mots. On retourne jusqu'à 3 mots distincts >= 3 lettres.
|
||||
*/
|
||||
private twitchQueryKeywords(query: string, max = 3): string[] {
|
||||
const words = String(query || '')
|
||||
.toLowerCase()
|
||||
.split(/[^a-z0-9à-ÿ]+/i)
|
||||
.map(w => w.trim())
|
||||
.filter(w => w.length >= 3);
|
||||
const seen = new Set<string>();
|
||||
const out: string[] = [];
|
||||
for (const w of words) {
|
||||
if (seen.has(w)) continue;
|
||||
seen.add(w);
|
||||
out.push(w);
|
||||
if (out.length >= max) break;
|
||||
}
|
||||
// Repli : requête brute courte si aucun mot exploitable.
|
||||
if (!out.length) {
|
||||
const raw = String(query || '').trim();
|
||||
if (raw) out.push(raw.slice(0, 40));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private fetchTwitchChannelsForKeyword(token: string, headers: HttpHeaders, keyword: string, first: number, after = ''): Observable<any> {
|
||||
const q = `/api/twitch-api/helix/search/channels?query=${encodeURIComponent(keyword)}&first=${first}${after}`;
|
||||
const f = `/proxy/twitch-api/helix/search/channels?query=${encodeURIComponent(keyword)}&first=${first}${after}`;
|
||||
return this.http.get<any>(q, { headers }).pipe(
|
||||
catchError(() => this.http.get<any>(f, { headers }))
|
||||
);
|
||||
}
|
||||
|
||||
private fetchTwitchCategoryForKeyword(token: string, headers: HttpHeaders, keyword: string): Observable<any> {
|
||||
const q = `/api/twitch-api/helix/search/categories?query=${encodeURIComponent(keyword)}&first=5`;
|
||||
const f = `/proxy/twitch-api/helix/search/categories?query=${encodeURIComponent(keyword)}&first=5`;
|
||||
return this.http.get<any>(q, { headers }).pipe(
|
||||
catchError(() => this.http.get<any>(f, { headers }))
|
||||
);
|
||||
}
|
||||
|
||||
/** Chaînes Twitch (lives + hors-ligne) pour une requête, façon directory. */
|
||||
public searchTwitchAllChannelsPage(query: string, cursor?: string | null, limit = 20): Observable<PagedResult<Video>> {
|
||||
return this.getTwitchAuthToken().pipe(
|
||||
@@ -128,14 +172,48 @@ export class YoutubeApiService {
|
||||
if (!token) return of({ items: [], nextCursor: null });
|
||||
const headers = new HttpHeaders({ 'Client-ID': this.resolveTwitchClientId()!, 'Authorization': `Bearer ${token}` });
|
||||
const after = cursor ? `&after=${encodeURIComponent(cursor)}` : '';
|
||||
const primary = `/api/twitch-api/helix/search/channels?query=${encodeURIComponent(query)}&first=${limit}${after}`;
|
||||
const fallback = `/proxy/twitch-api/helix/search/channels?query=${encodeURIComponent(query)}&first=${limit}${after}`;
|
||||
return this.http.get<any>(primary, { headers }).pipe(
|
||||
catchError(() => this.http.get<any>(fallback, { headers })),
|
||||
map(res => ({
|
||||
items: (Array.isArray(res?.data) ? res.data : []).map((i: any) => this.mapTwitchChannelToVideo(i)),
|
||||
nextCursor: res?.pagination?.cursor || null,
|
||||
} as PagedResult<Video>)),
|
||||
const q = String(query || '').trim();
|
||||
if (!q) return of({ items: [], nextCursor: null });
|
||||
// 1) Essai requête complète d'abord (pertinent pour "just chatting", noms de jeux...).
|
||||
return this.fetchTwitchChannelsForKeyword(token, headers, q, limit, after).pipe(
|
||||
switchMap(res => {
|
||||
const list = Array.isArray(res?.data) ? res.data : [];
|
||||
if (list.length > 0 || cursor) {
|
||||
return of({
|
||||
items: list.map((i: any) => this.mapTwitchChannelToVideo(i)),
|
||||
nextCursor: res?.pagination?.cursor || null,
|
||||
} as PagedResult<Video>);
|
||||
}
|
||||
// 2) Repli mot-à-mot : chaque mot-clé testé séparément, résultats fusionnés.
|
||||
const keywords = this.twitchQueryKeywords(q, 3);
|
||||
if (!keywords.length) return of({ items: [], nextCursor: null } as PagedResult<Video>);
|
||||
const calls = keywords.map(k =>
|
||||
this.fetchTwitchChannelsForKeyword(token, headers, k, Math.min(10, limit)).pipe(
|
||||
catchError(() => of({ data: [], pagination: {} }))
|
||||
)
|
||||
);
|
||||
return forkJoin(calls).pipe(
|
||||
map((responses: any[]) => {
|
||||
const seen = new Set<string>();
|
||||
const merged: any[] = [];
|
||||
let next: string | null = null;
|
||||
for (const r of responses) {
|
||||
const arr = Array.isArray(r?.data) ? r.data : [];
|
||||
if (!next && r?.pagination?.cursor) next = r.pagination.cursor;
|
||||
for (const c of arr) {
|
||||
const key = String(c?.id || c?.broadcaster_login || c?.display_name || '');
|
||||
if (!key || seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
merged.push(c);
|
||||
}
|
||||
}
|
||||
return {
|
||||
items: merged.slice(0, limit).map((i: any) => this.mapTwitchChannelToVideo(i)),
|
||||
nextCursor: next,
|
||||
} as PagedResult<Video>;
|
||||
})
|
||||
);
|
||||
}),
|
||||
catchError((e) => { console.error('Twitch search channels error', e); return of({ items: [], nextCursor: null }); })
|
||||
);
|
||||
})
|
||||
@@ -150,20 +228,30 @@ export class YoutubeApiService {
|
||||
const q = String(query || '').trim();
|
||||
if (!q) return of(null);
|
||||
const headers = new HttpHeaders({ 'Client-ID': this.resolveTwitchClientId()!, 'Authorization': `Bearer ${token}` });
|
||||
const primary = `/api/twitch-api/helix/search/categories?query=${encodeURIComponent(q)}&first=5`;
|
||||
const fallback = `/proxy/twitch-api/helix/search/categories?query=${encodeURIComponent(q)}&first=5`;
|
||||
return this.http.get<any>(primary, { headers }).pipe(
|
||||
catchError(() => this.http.get<any>(fallback, { headers })),
|
||||
map(res => {
|
||||
const list: any[] = Array.isArray(res?.data) ? res.data : [];
|
||||
if (!list.length) return null;
|
||||
const needle = q.toLowerCase();
|
||||
const exact = list.find(c => String(c?.name || '').toLowerCase() === needle);
|
||||
const best = exact || list[0];
|
||||
return best?.id ? { id: String(best.id), name: String(best.name || ''), boxArtUrl: best.box_art_url || undefined } : null;
|
||||
}),
|
||||
catchError(() => of(null))
|
||||
// Essaie la requête complète puis chaque mot-clé (une longue phrase ne matche jamais une catégorie).
|
||||
const attempts = [q, ...this.twitchQueryKeywords(q, 3).filter(k => k !== q.toLowerCase())];
|
||||
const calls = attempts.slice(0, 4).map(a =>
|
||||
this.fetchTwitchCategoryForKeyword(token, headers, a).pipe(
|
||||
map(res => ({ attempt: a, list: Array.isArray(res?.data) ? res.data : [] })),
|
||||
catchError(() => of({ attempt: a, list: [] }))
|
||||
)
|
||||
);
|
||||
// Séquentiel : premier match exact, sinon premier non-vide.
|
||||
const tryNext = (idx: number): Observable<{ id: string; name: string; boxArtUrl?: string } | null> => {
|
||||
if (idx >= calls.length) return of(null);
|
||||
return calls[idx].pipe(
|
||||
switchMap(({ attempt, list }: any) => {
|
||||
if (list.length) {
|
||||
const needle = String(attempt || '').toLowerCase();
|
||||
const exact = list.find((c: any) => String(c?.name || '').toLowerCase() === needle);
|
||||
const best = exact || list[0];
|
||||
if (best?.id) return of({ id: String(best.id), name: String(best.name || ''), boxArtUrl: best.box_art_url || undefined });
|
||||
}
|
||||
return tryNext(idx + 1);
|
||||
})
|
||||
);
|
||||
};
|
||||
return tryNext(0).pipe(catchError(() => of(null)));
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -243,32 +331,89 @@ export class YoutubeApiService {
|
||||
switchMap(token => {
|
||||
if (!token) return of({ items: [], nextCursor: null });
|
||||
const headers = new HttpHeaders({ 'Client-ID': this.resolveTwitchClientId()!, 'Authorization': `Bearer ${token}` });
|
||||
const q = String(query || '').trim();
|
||||
if (!q) return of({ items: [], nextCursor: null });
|
||||
const after = cursor ? `&after=${encodeURIComponent(cursor)}` : '';
|
||||
const chPrimary = `/api/twitch-api/helix/search/channels?query=${encodeURIComponent(query)}&first=10${after}`;
|
||||
const chFallback = `/proxy/twitch-api/helix/search/channels?query=${encodeURIComponent(query)}&first=10${after}`;
|
||||
const fetchChannels$ = this.http.get<any>(chPrimary, { headers }).pipe(
|
||||
catchError(() => this.http.get<any>(chFallback, { headers }))
|
||||
// Chaînes candidates : requête complète + mots-clés unitaires (Helix ne matche pas les phrases).
|
||||
const keywords = [q, ...this.twitchQueryKeywords(q, 3).filter(k => k !== q.toLowerCase())].slice(0, 4);
|
||||
const channelCalls = keywords.map((k, idx) =>
|
||||
this.fetchTwitchChannelsForKeyword(token, headers, k, 10, idx === 0 ? after : '').pipe(
|
||||
catchError(() => of({ data: [], pagination: {} }))
|
||||
)
|
||||
);
|
||||
return fetchChannels$.pipe(
|
||||
switchMap((chRes: any) => {
|
||||
const channels = Array.isArray(chRes?.data) ? chRes.data : [];
|
||||
return forkJoin(channelCalls).pipe(
|
||||
switchMap((responses: any[]) => {
|
||||
const seenCh = new Set<string>();
|
||||
const channels: any[] = [];
|
||||
let next: string | null = null;
|
||||
for (const r of responses) {
|
||||
const arr = Array.isArray(r?.data) ? r.data : [];
|
||||
if (!next && r?.pagination?.cursor) next = r.pagination.cursor;
|
||||
for (const c of arr) {
|
||||
const key = String(c?.id || '');
|
||||
if (!key || seenCh.has(key)) continue;
|
||||
seenCh.add(key);
|
||||
channels.push(c);
|
||||
}
|
||||
}
|
||||
const userIds: string[] = channels.map((c: any) => c?.id).filter(Boolean).slice(0, 10);
|
||||
if (userIds.length === 0) return of({ items: [], nextCursor: null } as PagedResult<Video>);
|
||||
const perUserRequests = userIds.map(uid => {
|
||||
// type=all to include archive, upload, highlight; fetch a few more to increase hit rate
|
||||
const vPrimary = `/api/twitch-api/helix/videos?user_id=${encodeURIComponent(uid)}&first=10&type=all`;
|
||||
const vFallback = `/proxy/twitch-api/helix/videos?user_id=${encodeURIComponent(uid)}&first=10&type=all`;
|
||||
return this.http.get<any>(vPrimary, { headers }).pipe(
|
||||
catchError(() => this.http.get<any>(vFallback, { headers })),
|
||||
map(vres => Array.isArray(vres?.data) ? vres.data : [])
|
||||
const fetchVodsForUsers = (ids: string[]) => {
|
||||
if (!ids.length) return of([] as any[]);
|
||||
const perUserRequests = ids.map(uid => {
|
||||
// type=all to include archive, upload, highlight; fetch a few more to increase hit rate
|
||||
const vPrimary = `/api/twitch-api/helix/videos?user_id=${encodeURIComponent(uid)}&first=10&type=all`;
|
||||
const vFallback = `/proxy/twitch-api/helix/videos?user_id=${encodeURIComponent(uid)}&first=10&type=all`;
|
||||
return this.http.get<any>(vPrimary, { headers }).pipe(
|
||||
catchError(() => this.http.get<any>(vFallback, { headers })),
|
||||
map(vres => Array.isArray(vres?.data) ? vres.data : []),
|
||||
catchError(() => of([] as any[]))
|
||||
);
|
||||
});
|
||||
return forkJoin(perUserRequests).pipe(map((lists: any[][]) => lists.flat()));
|
||||
};
|
||||
// Complément par catégorie (game_id) quand les chaînes ne suffisent pas (thèmes génériques).
|
||||
const fetchVodsByCategory = (): Observable<any[]> => {
|
||||
const cats = this.twitchQueryKeywords(q, 2);
|
||||
if (!cats.length) return of([]);
|
||||
const catCalls = cats.slice(0, 2).map(k =>
|
||||
this.fetchTwitchCategoryForKeyword(token, headers, k).pipe(catchError(() => of({ data: [] })))
|
||||
);
|
||||
});
|
||||
return forkJoin(perUserRequests).pipe(
|
||||
map((lists: any[][]) => lists.flat().slice(0, 20)),
|
||||
map((videos: any[]) => ({
|
||||
items: videos.map((i: any) => this.mapTwitchItemToVideo(i)),
|
||||
nextCursor: chRes?.pagination?.cursor || null,
|
||||
} as PagedResult<Video>))
|
||||
return forkJoin(catCalls).pipe(
|
||||
switchMap((catRes: any[]) => {
|
||||
const gameIds: string[] = [];
|
||||
for (const cr of catRes) {
|
||||
const list = Array.isArray(cr?.data) ? cr.data : [];
|
||||
if (list[0]?.id) gameIds.push(String(list[0].id));
|
||||
}
|
||||
if (!gameIds.length) return of([]);
|
||||
const gameCalls = gameIds.slice(0, 2).map(gid => {
|
||||
const gPrimary = `/api/twitch-api/helix/videos?game_id=${encodeURIComponent(gid)}&first=10&sort=trending&type=all`;
|
||||
const gFallback = `/proxy/twitch-api/helix/videos?game_id=${encodeURIComponent(gid)}&first=10&sort=trending&type=all`;
|
||||
return this.http.get<any>(gPrimary, { headers }).pipe(
|
||||
catchError(() => this.http.get<any>(gFallback, { headers })),
|
||||
map(vres => Array.isArray(vres?.data) ? vres.data : []),
|
||||
catchError(() => of([] as any[]))
|
||||
);
|
||||
});
|
||||
return forkJoin(gameCalls).pipe(map((lists: any[][]) => lists.flat()));
|
||||
})
|
||||
);
|
||||
};
|
||||
return fetchVodsForUsers(userIds).pipe(
|
||||
switchMap(vodsByUser => {
|
||||
if (vodsByUser.length >= 4) {
|
||||
return of({
|
||||
items: vodsByUser.slice(0, 20).map((i: any) => this.mapTwitchItemToVideo(i)),
|
||||
nextCursor: next,
|
||||
} as PagedResult<Video>);
|
||||
}
|
||||
return fetchVodsByCategory().pipe(
|
||||
map(vodsByGame => ({
|
||||
items: [...vodsByUser, ...vodsByGame].slice(0, 20).map((i: any) => this.mapTwitchItemToVideo(i)),
|
||||
nextCursor: next,
|
||||
} as PagedResult<Video>))
|
||||
);
|
||||
})
|
||||
);
|
||||
}),
|
||||
catchError((e) => { console.error('Twitch search VODs error', e); return of({ items: [], nextCursor: null }); })
|
||||
@@ -1090,32 +1235,88 @@ export class YoutubeApiService {
|
||||
switchMap(token => {
|
||||
if (!token) return of({ items: [], nextCursor: null });
|
||||
const headers = new HttpHeaders({ 'Client-ID': this.resolveTwitchClientId()!, 'Authorization': `Bearer ${token}` });
|
||||
const q = String(query || '').trim();
|
||||
if (!q) return of({ items: [], nextCursor: null });
|
||||
const after = cursor ? `&after=${encodeURIComponent(cursor)}` : '';
|
||||
const q = String(query || '');
|
||||
const chPrimary = `/api/twitch-api/helix/search/channels?query=${encodeURIComponent(q)}&first=10${after}`;
|
||||
const chFallback = `/proxy/twitch-api/helix/search/channels?query=${encodeURIComponent(q)}&first=10${after}`;
|
||||
return this.http.get<any>(chPrimary, { headers }).pipe(
|
||||
catchError(() => this.http.get<any>(chFallback, { headers })),
|
||||
switchMap(chRes => {
|
||||
const channels = Array.isArray(chRes?.data) ? chRes.data : [];
|
||||
const keywords = [q, ...this.twitchQueryKeywords(q, 3).filter(k => k !== q.toLowerCase())].slice(0, 4);
|
||||
const channelCalls = keywords.map((k, idx) =>
|
||||
this.fetchTwitchChannelsForKeyword(token, headers, k, 10, idx === 0 ? after : '').pipe(
|
||||
catchError(() => of({ data: [], pagination: {} }))
|
||||
)
|
||||
);
|
||||
// Fenêtre large (30j -> 90j) : beaucoup de petites chaînes n'ont pas de clips sur 7j.
|
||||
const since = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString();
|
||||
const fetchClipsForUsers = (ids: string[]) => {
|
||||
if (!ids.length) return of([] as any[]);
|
||||
const perUser = ids.slice(0, 8).map(uid => {
|
||||
const cPrimary = `/api/twitch-api/helix/clips?broadcaster_id=${encodeURIComponent(uid)}&first=5&started_at=${encodeURIComponent(since)}`;
|
||||
const cFallback = `/proxy/twitch-api/helix/clips?broadcaster_id=${encodeURIComponent(uid)}&first=5&started_at=${encodeURIComponent(since)}`;
|
||||
return this.http.get<any>(cPrimary, { headers }).pipe(
|
||||
catchError(() => this.http.get<any>(cFallback, { headers })),
|
||||
map(res => Array.isArray(res?.data) ? res.data : []),
|
||||
catchError(() => of([] as any[]))
|
||||
);
|
||||
});
|
||||
return forkJoin(perUser).pipe(map((lists: any[][]) => lists.flat()));
|
||||
};
|
||||
return forkJoin(channelCalls).pipe(
|
||||
switchMap((responses: any[]) => {
|
||||
const seen = new Set<string>();
|
||||
const channels: any[] = [];
|
||||
let next: string | null = null;
|
||||
for (const r of responses) {
|
||||
const arr = Array.isArray(r?.data) ? r.data : [];
|
||||
if (!next && r?.pagination?.cursor) next = r.pagination.cursor;
|
||||
for (const c of arr) {
|
||||
const key = String(c?.id || '');
|
||||
if (!key || seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
channels.push(c);
|
||||
}
|
||||
}
|
||||
const userIds: string[] = channels.map((c: any) => c?.id).filter(Boolean).slice(0, 10);
|
||||
if (userIds.length === 0) return of({ items: [], nextCursor: null } as PagedResult<Video>);
|
||||
const since = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
||||
const perUserRequests = userIds.map(uid => {
|
||||
const cPrimary = `/api/twitch-api/helix/clips?broadcaster_id=${encodeURIComponent(uid)}&first=5&started_at=${encodeURIComponent(since)}`;
|
||||
const cFallback = `/proxy/twitch-api/helix/clips?broadcaster_id=${encodeURIComponent(uid)}&first=5&started_at=${encodeURIComponent(since)}`;
|
||||
return this.http.get<any>(cPrimary, { headers }).pipe(
|
||||
catchError(() => this.http.get<any>(cFallback, { headers })),
|
||||
map(res => Array.isArray(res?.data) ? res.data : [])
|
||||
);
|
||||
});
|
||||
return forkJoin(perUserRequests).pipe(
|
||||
map((lists: any[][]) => lists.flat().slice(0, 24)),
|
||||
map((clips: any[]) => ({
|
||||
items: clips.map((i: any) => this.mapTwitchClipToVideo(i)),
|
||||
nextCursor: chRes?.pagination?.cursor || null,
|
||||
} as PagedResult<Video>)),
|
||||
catchError(() => of({ items: [], nextCursor: null } as PagedResult<Video>))
|
||||
return fetchClipsForUsers(userIds).pipe(
|
||||
switchMap(clipsByUser => {
|
||||
if (clipsByUser.length >= 3) {
|
||||
return of({
|
||||
items: clipsByUser.slice(0, 24).map((i: any) => this.mapTwitchClipToVideo(i)),
|
||||
nextCursor: next,
|
||||
} as PagedResult<Video>);
|
||||
}
|
||||
// Repli catégorie (game_id) : clips thématiques même sans chaîne matchée.
|
||||
const cats = this.twitchQueryKeywords(q, 2);
|
||||
if (!cats.length) return of({ items: clipsByUser.slice(0, 24).map((i: any) => this.mapTwitchClipToVideo(i)), nextCursor: next } as PagedResult<Video>);
|
||||
const catCalls = cats.slice(0, 2).map(k =>
|
||||
this.fetchTwitchCategoryForKeyword(token, headers, k).pipe(catchError(() => of({ data: [] })))
|
||||
);
|
||||
return forkJoin(catCalls).pipe(
|
||||
switchMap((catRes: any[]) => {
|
||||
const gameIds: string[] = [];
|
||||
for (const cr of catRes) {
|
||||
const list = Array.isArray(cr?.data) ? cr.data : [];
|
||||
if (list[0]?.id) gameIds.push(String(list[0].id));
|
||||
}
|
||||
if (!gameIds.length) {
|
||||
return of({ items: clipsByUser.slice(0, 24).map((i: any) => this.mapTwitchClipToVideo(i)), nextCursor: next } as PagedResult<Video>);
|
||||
}
|
||||
const gameCalls = gameIds.slice(0, 2).map(gid => {
|
||||
const gPrimary = `/api/twitch-api/helix/clips?game_id=${encodeURIComponent(gid)}&first=10&started_at=${encodeURIComponent(since)}`;
|
||||
const gFallback = `/proxy/twitch-api/helix/clips?game_id=${encodeURIComponent(gid)}&first=10&started_at=${encodeURIComponent(since)}`;
|
||||
return this.http.get<any>(gPrimary, { headers }).pipe(
|
||||
catchError(() => this.http.get<any>(gFallback, { headers })),
|
||||
map(res => Array.isArray(res?.data) ? res.data : []),
|
||||
catchError(() => of([] as any[]))
|
||||
);
|
||||
});
|
||||
return forkJoin(gameCalls).pipe(
|
||||
map((lists: any[][]) => ({
|
||||
items: [...clipsByUser, ...lists.flat()].slice(0, 24).map((i: any) => this.mapTwitchClipToVideo(i)),
|
||||
nextCursor: next,
|
||||
} as PagedResult<Video>))
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}),
|
||||
catchError((e) => { console.error('Twitch clips search error', e); return of({ items: [], nextCursor: null }); })
|
||||
|
||||
Reference in New Issue
Block a user