chore: update Angular cache and TypeScript build info files
This commit is contained in:
parent
2420140cad
commit
80935ebd74
2
.angular/cache/20.2.2/app/.tsbuildinfo
vendored
2
.angular/cache/20.2.2/app/.tsbuildinfo
vendored
File diff suppressed because one or more lines are too long
BIN
db/newtube.db
BIN
db/newtube.db
Binary file not shown.
@ -58,6 +58,12 @@ export class WatchShortComponent {
|
|||||||
if (p === 'twitch' && v.videoId) {
|
if (p === 'twitch' && v.videoId) {
|
||||||
const parentsSet = new Set<string>([host, 'localhost', '127.0.0.1']);
|
const parentsSet = new Set<string>([host, 'localhost', '127.0.0.1']);
|
||||||
const parentParams = Array.from(parentsSet).map(h => `parent=${encodeURIComponent(h)}`).join('&');
|
const parentParams = Array.from(parentsSet).map(h => `parent=${encodeURIComponent(h)}`).join('&');
|
||||||
|
// If it's a clip, use the Clips embed endpoint
|
||||||
|
if ((v as any).kind === 'clip') {
|
||||||
|
const u = `https://clips.twitch.tv/embed?clip=${encodeURIComponent(v.videoId)}&${parentParams}&autoplay=true`;
|
||||||
|
return this.sanitizer.bypassSecurityTrustResourceUrl(u);
|
||||||
|
}
|
||||||
|
// Otherwise treat it as a channel embed (live stream)
|
||||||
const base = 'https://player.twitch.tv/';
|
const base = 'https://player.twitch.tv/';
|
||||||
const qs = `?channel=${encodeURIComponent(v.videoId)}&${parentParams}&autoplay=false`;
|
const qs = `?channel=${encodeURIComponent(v.videoId)}&${parentParams}&autoplay=false`;
|
||||||
const u = base + qs;
|
const u = base + qs;
|
||||||
@ -91,11 +97,14 @@ export class WatchShortComponent {
|
|||||||
|
|
||||||
// Called when a query returns no usable items; falls back to Trending
|
// Called when a query returns no usable items; falls back to Trending
|
||||||
private afterNoResults(): void {
|
private afterNoResults(): void {
|
||||||
|
// For Shorts page, do NOT fallback to trending; display an appropriate message instead.
|
||||||
const p = this.provider();
|
const p = this.provider();
|
||||||
const hint = p === 'youtube'
|
const unsupported = this.isProviderUnsupportedForShorts(p);
|
||||||
? 'Aucun Short trouvé. Assurez-vous de définir YOUTUBE_API_KEY dans assets/config.local.js (window.YOUTUBE_API_KEY = "...") ou changez de fournisseur.'
|
if (unsupported) {
|
||||||
: 'Aucun Short trouvé pour ce fournisseur.';
|
this.error.set('Ce fournisseur ne supporte pas les shorts');
|
||||||
this.loadFallbackTrending(hint);
|
} else {
|
||||||
|
this.error.set('Aucun Short trouvé pour ce fournisseur.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter YouTube results to real Shorts using the Videos API to get durations.
|
// Filter YouTube results to real Shorts using the Videos API to get durations.
|
||||||
@ -114,7 +123,7 @@ export class WatchShortComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Basic feed: search for "shorts"
|
// Initialize feed for Shorts according to current provider
|
||||||
this.loadFeed();
|
this.loadFeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,6 +136,28 @@ export class WatchShortComponent {
|
|||||||
this.error.set(ready.reason || 'Provider not ready.');
|
this.error.set(ready.reason || 'Provider not ready.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const provider = this.provider();
|
||||||
|
// Use provider-specific source for Shorts
|
||||||
|
if (provider === 'twitch') {
|
||||||
|
// Use Twitch Clips as Shorts
|
||||||
|
this.api.searchTwitchClipsPage('')?.subscribe({
|
||||||
|
next: (res: { items: Video[]; nextCursor?: string | null }) => {
|
||||||
|
const raw: Video[] = (res.items || []).filter((v: Video) => !!v.videoId);
|
||||||
|
const list: Video[] = raw.filter((v: Video) => this.isShort(v));
|
||||||
|
this.items.set(list);
|
||||||
|
this.index.set(0);
|
||||||
|
this.nextCursor.set(res.nextCursor || null);
|
||||||
|
if (list.length === 0) this.afterNoResults(); else this.error.set(null);
|
||||||
|
this.loading.set(false);
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.error.set('Failed to load Shorts.');
|
||||||
|
this.loading.set(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Default: search for shorts keywords via generic search
|
||||||
this.api.searchVideosPage('shorts OR #shorts').subscribe({
|
this.api.searchVideosPage('shorts OR #shorts').subscribe({
|
||||||
next: (res) => {
|
next: (res) => {
|
||||||
const raw = (res.items || []).filter(v => !!v.videoId);
|
const raw = (res.items || []).filter(v => !!v.videoId);
|
||||||
@ -134,7 +165,7 @@ export class WatchShortComponent {
|
|||||||
const p = this.provider();
|
const p = this.provider();
|
||||||
if (p === 'youtube' && raw.length) {
|
if (p === 'youtube' && raw.length) {
|
||||||
this.filterYouTubeShorts(raw).then(list => {
|
this.filterYouTubeShorts(raw).then(list => {
|
||||||
this.items.set(list);
|
this.items.set(list.filter(v => this.isShort(v)));
|
||||||
this.index.set(0);
|
this.index.set(0);
|
||||||
if (list.length === 0) this.afterNoResults();
|
if (list.length === 0) this.afterNoResults();
|
||||||
else this.error.set(null);
|
else this.error.set(null);
|
||||||
@ -142,21 +173,11 @@ export class WatchShortComponent {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Non-YouTube: take raw as-is
|
// Non-YouTube: apply provider-specific minimal filtering
|
||||||
const list = raw;
|
const list = raw.filter(v => this.isShort(v));
|
||||||
this.items.set(list);
|
this.items.set(list);
|
||||||
this.index.set(0);
|
this.index.set(0);
|
||||||
if (list.length === 0) {
|
if (list.length === 0) this.afterNoResults(); else this.error.set(null);
|
||||||
const p = this.provider();
|
|
||||||
const hint = p === 'youtube'
|
|
||||||
? 'Aucun Short trouvé. Assurez-vous de définir YOUTUBE_API_KEY dans assets/config.local.js (window.YOUTUBE_API_KEY = "...") ou changez de fournisseur.'
|
|
||||||
: 'Aucun Short trouvé pour ce fournisseur.';
|
|
||||||
// Essayez une retombée vers le fil des tendances
|
|
||||||
this.loadFallbackTrending(hint);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
this.error.set(null);
|
|
||||||
}
|
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
@ -166,31 +187,51 @@ export class WatchShortComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadFallbackTrending(hint?: string): void {
|
// Trending fallback removed for Shorts page to avoid showing non-short content
|
||||||
this.api.getTrendingPage().subscribe({
|
private loadFallbackTrending(_hint?: string): void { this.error.set('Ce fournisseur ne supporte pas les shorts'); this.loading.set(false); }
|
||||||
next: (res) => {
|
|
||||||
const list = (res.items || []).filter(v => !!v.videoId);
|
// Generic Shorts check based on provider
|
||||||
this.items.set(list);
|
private isShort(v: Video): boolean {
|
||||||
this.index.set(0);
|
const p = (v as any).provider || this.provider();
|
||||||
this.nextCursor.set(res.nextCursor || null);
|
const url = (v as any).url || '';
|
||||||
if (list.length === 0) this.error.set(hint || 'Aucun contenu disponible.');
|
switch (p) {
|
||||||
else this.error.set(null);
|
case 'youtube':
|
||||||
this.loading.set(false);
|
return (v as any).isShort === true || (typeof url === 'string' && url.includes('/shorts/')) || ((v.duration ?? 0) > 0 && (v.duration ?? 0) <= 70);
|
||||||
},
|
case 'dailymotion':
|
||||||
error: () => {
|
return (v as any).isShort === true || (((v.duration ?? 0) <= 60) && ((v as any).isVertical === true));
|
||||||
this.error.set(hint || 'Impossible de charger du contenu.');
|
case 'twitch':
|
||||||
this.loading.set(false);
|
return (v as any).kind === 'clip';
|
||||||
|
case 'rumble':
|
||||||
|
return (v as any).isShort === true;
|
||||||
|
case 'odysee':
|
||||||
|
return (v as any).isShort === true;
|
||||||
|
case 'peertube':
|
||||||
|
return (v as any).isShort === true || (((v.duration ?? 0) <= 60) && ((v as any).isVertical === true));
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private isProviderUnsupportedForShorts(p: string | undefined | null): boolean {
|
||||||
|
switch (p) {
|
||||||
|
case 'rumble':
|
||||||
|
case 'odysee':
|
||||||
|
return true; // Only if provider exposes isShort; otherwise unsupported
|
||||||
|
case 'twitch':
|
||||||
|
case 'youtube':
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetchNextPage(autoAdvance = false) {
|
private fetchNextPage(autoAdvance = false) {
|
||||||
if (this.busyMore() || !this.nextCursor()) return;
|
if (this.busyMore() || !this.nextCursor()) return;
|
||||||
this.busyMore.set(true);
|
this.busyMore.set(true);
|
||||||
const cursor = this.nextCursor();
|
const cursor = this.nextCursor();
|
||||||
this.api.searchVideosPage('shorts OR #shorts', cursor || undefined).subscribe({
|
const provider = this.provider();
|
||||||
next: (res) => {
|
const handle = (res: any) => {
|
||||||
const raw = (res.items || []).filter(v => !!v.videoId);
|
const raw: Video[] = (res.items || []).filter((v: Video) => !!v.videoId);
|
||||||
const p = this.provider();
|
const p = this.provider();
|
||||||
const apply = (more: Video[]) => {
|
const apply = (more: Video[]) => {
|
||||||
const merged = this.items().concat(more);
|
const merged = this.items().concat(more);
|
||||||
@ -202,10 +243,23 @@ export class WatchShortComponent {
|
|||||||
this.busyMore.set(false);
|
this.busyMore.set(false);
|
||||||
};
|
};
|
||||||
if (p === 'youtube' && raw.length) {
|
if (p === 'youtube' && raw.length) {
|
||||||
this.filterYouTubeShorts(raw).then(apply).catch(() => { apply([]); });
|
this.filterYouTubeShorts(raw).then((list: Video[]) => apply(list.filter((v: Video) => this.isShort(v)))).catch(() => { apply([]); });
|
||||||
} else {
|
} else {
|
||||||
apply(raw);
|
apply(raw.filter((v: Video) => this.isShort(v)));
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (provider === 'twitch') {
|
||||||
|
this.api.searchTwitchClipsPage('', cursor || undefined)?.subscribe({
|
||||||
|
next: handle,
|
||||||
|
error: () => { this.busyMore.set(false); }
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.api.searchVideosPage('shorts OR #shorts', cursor || undefined).subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
handle(res);
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.busyMore.set(false);
|
this.busyMore.set(false);
|
||||||
|
@ -14,6 +14,12 @@ export interface Video {
|
|||||||
uploaded: number;
|
uploaded: number;
|
||||||
videoId: string;
|
videoId: string;
|
||||||
providerUrl?: string; // URL complète de la vidéo sur le site du fournisseur
|
providerUrl?: string; // URL complète de la vidéo sur le site du fournisseur
|
||||||
|
// Champs optionnels pour le filtrage des Shorts (non obligatoires selon les providers)
|
||||||
|
provider?: 'youtube'|'dailymotion'|'twitch'|'rumble'|'odysee'|'peertube';
|
||||||
|
isVertical?: boolean; // true si 9:16
|
||||||
|
isShort?: boolean; // indicateur natif si disponible
|
||||||
|
kind?: 'clip'|'vod'|'short'|'channel'; // ex. Twitch=clip
|
||||||
|
raw?: any; // métadonnées brutes si besoin
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VideoDetail extends Video {
|
export interface VideoDetail extends Video {
|
||||||
|
@ -376,15 +376,14 @@ export class YoutubeApiService {
|
|||||||
if (!key) return of({ items: [], nextCursor: null });
|
if (!key) return of({ items: [], nextCursor: null });
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
type: 'video',
|
type: 'video',
|
||||||
part: 'snippet,contentDetails,statistics',
|
// search.list only supports 'snippet'
|
||||||
|
part: 'snippet',
|
||||||
// Optimize for quota usage: reduce initial results
|
// Optimize for quota usage: reduce initial results
|
||||||
maxResults: '12',
|
maxResults: '12',
|
||||||
q: String(query),
|
q: String(query),
|
||||||
regionCode: String(region),
|
regionCode: String(region),
|
||||||
// Add safeSearch for better content filtering
|
// Add safeSearch for better content filtering
|
||||||
safeSearch: 'moderate',
|
safeSearch: 'moderate',
|
||||||
// Video category filter for better relevance
|
|
||||||
videoCategoryId: '0', // All categories
|
|
||||||
key: String(key),
|
key: String(key),
|
||||||
});
|
});
|
||||||
// Prefer results matching user's language when available
|
// Prefer results matching user's language when available
|
||||||
@ -569,6 +568,7 @@ export class YoutubeApiService {
|
|||||||
url: `/watch/${id}`,
|
url: `/watch/${id}`,
|
||||||
videoId: id,
|
videoId: id,
|
||||||
type: 'video',
|
type: 'video',
|
||||||
|
provider: 'youtube',
|
||||||
title: sn.title || '',
|
title: sn.title || '',
|
||||||
thumbnail: thumb,
|
thumbnail: thumb,
|
||||||
uploaderName: sn.channelTitle || '',
|
uploaderName: sn.channelTitle || '',
|
||||||
@ -590,6 +590,7 @@ export class YoutubeApiService {
|
|||||||
url: `/watch/${id}`,
|
url: `/watch/${id}`,
|
||||||
videoId: id,
|
videoId: id,
|
||||||
type: 'video',
|
type: 'video',
|
||||||
|
provider: 'youtube',
|
||||||
title: sn.title || '',
|
title: sn.title || '',
|
||||||
thumbnail: thumb,
|
thumbnail: thumb,
|
||||||
uploaderName: sn.channelTitle || '',
|
uploaderName: sn.channelTitle || '',
|
||||||
@ -607,6 +608,7 @@ export class YoutubeApiService {
|
|||||||
url: `/watch/${i.id}`,
|
url: `/watch/${i.id}`,
|
||||||
videoId: i.id || '',
|
videoId: i.id || '',
|
||||||
type: 'video',
|
type: 'video',
|
||||||
|
provider: 'dailymotion',
|
||||||
title: i.title || '',
|
title: i.title || '',
|
||||||
thumbnail: i.thumbnail_720_url || i.thumbnail_480_url || i.thumbnail_url || '',
|
thumbnail: i.thumbnail_720_url || i.thumbnail_480_url || i.thumbnail_url || '',
|
||||||
uploaderName: i['owner.screenname'] || '',
|
uploaderName: i['owner.screenname'] || '',
|
||||||
@ -634,6 +636,7 @@ export class YoutubeApiService {
|
|||||||
url: `https://${instance}/w/${i.uuid}`,
|
url: `https://${instance}/w/${i.uuid}`,
|
||||||
videoId: i.uuid || '',
|
videoId: i.uuid || '',
|
||||||
type: 'video',
|
type: 'video',
|
||||||
|
provider: 'peertube',
|
||||||
title: i.name || '',
|
title: i.name || '',
|
||||||
thumbnail,
|
thumbnail,
|
||||||
uploaderName,
|
uploaderName,
|
||||||
@ -664,6 +667,7 @@ export class YoutubeApiService {
|
|||||||
url: webUrl,
|
url: webUrl,
|
||||||
videoId: i?.claim_id || i?.txid || i?.claimId || i?.id || '',
|
videoId: i?.claim_id || i?.txid || i?.claimId || i?.id || '',
|
||||||
type: 'video',
|
type: 'video',
|
||||||
|
provider: 'odysee',
|
||||||
title: val?.title || i?.name || '',
|
title: val?.title || i?.name || '',
|
||||||
thumbnail: thumb,
|
thumbnail: thumb,
|
||||||
uploaderName,
|
uploaderName,
|
||||||
@ -702,6 +706,7 @@ export class YoutubeApiService {
|
|||||||
url,
|
url,
|
||||||
videoId: id,
|
videoId: id,
|
||||||
type: 'video',
|
type: 'video',
|
||||||
|
provider: 'rumble',
|
||||||
title,
|
title,
|
||||||
thumbnail: thumb,
|
thumbnail: thumb,
|
||||||
uploaderName,
|
uploaderName,
|
||||||
@ -719,6 +724,7 @@ export class YoutubeApiService {
|
|||||||
url: `/watch/${i.id}`,
|
url: `/watch/${i.id}`,
|
||||||
videoId: i.id || '',
|
videoId: i.id || '',
|
||||||
type: 'video',
|
type: 'video',
|
||||||
|
provider: 'twitch',
|
||||||
title: i.title || '',
|
title: i.title || '',
|
||||||
thumbnail: i.thumbnail_url?.replace('%{width}', '640').replace('%{height}', '360') || '',
|
thumbnail: i.thumbnail_url?.replace('%{width}', '640').replace('%{height}', '360') || '',
|
||||||
uploaderName: i.user_name || '',
|
uploaderName: i.user_name || '',
|
||||||
@ -735,6 +741,7 @@ export class YoutubeApiService {
|
|||||||
url: `https://www.twitch.tv/${i.user_login}`,
|
url: `https://www.twitch.tv/${i.user_login}`,
|
||||||
videoId: i.user_login || i.id || '',
|
videoId: i.user_login || i.id || '',
|
||||||
type: 'channel',
|
type: 'channel',
|
||||||
|
provider: 'twitch',
|
||||||
title: i.title || '',
|
title: i.title || '',
|
||||||
thumbnail: (i.thumbnail_url || '').replace('{width}', '640').replace('{height}', '360'),
|
thumbnail: (i.thumbnail_url || '').replace('{width}', '640').replace('{height}', '360'),
|
||||||
uploaderName: i.user_name || '',
|
uploaderName: i.user_name || '',
|
||||||
@ -750,17 +757,80 @@ export class YoutubeApiService {
|
|||||||
url: `https://www.twitch.tv/${i.broadcaster_login || i.user_login || ''}`,
|
url: `https://www.twitch.tv/${i.broadcaster_login || i.user_login || ''}`,
|
||||||
videoId: i.broadcaster_login || i.user_login || '',
|
videoId: i.broadcaster_login || i.user_login || '',
|
||||||
type: 'channel',
|
type: 'channel',
|
||||||
title: i.display_name || i.title || '',
|
provider: 'twitch',
|
||||||
|
title: i.title || i.display_name || i.broadcaster_login || i.user_name || '',
|
||||||
thumbnail: (i.thumbnail_url || '').replace('{width}', '640').replace('{height}', '360'),
|
thumbnail: (i.thumbnail_url || '').replace('{width}', '640').replace('{height}', '360'),
|
||||||
uploaderName: i.display_name || i.broadcaster_login || i.user_login || '',
|
uploaderName: i.display_name || i.broadcaster_name || i.user_name || '',
|
||||||
uploaderUrl: `https://www.twitch.tv/${i.broadcaster_login || i.user_login || ''}`,
|
uploaderUrl: `https://www.twitch.tv/${i.broadcaster_login || i.user_login || ''}`,
|
||||||
uploaderAvatar: (i.thumbnail_url || '').replace('{width}', '70').replace('{height}', '70'),
|
uploaderAvatar: (i.thumbnail_url || '').replace('{width}', '70').replace('{height}', '70'),
|
||||||
uploadedDate: i.started_at || '',
|
uploadedDate: '',
|
||||||
duration: 0, // Not a VOD
|
duration: 0,
|
||||||
views: 0, // Not applicable
|
views: 0,
|
||||||
uploaded: i.started_at ? Date.parse(i.started_at) : 0,
|
uploaded: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Twitch Clips mapper (Shorts-equivalent)
|
||||||
|
private mapTwitchClipToVideo = (i: any): Video => ({
|
||||||
|
url: i.url || `https://clips.twitch.tv/${i.id}`,
|
||||||
|
videoId: i.id || '',
|
||||||
|
type: 'video',
|
||||||
|
provider: 'twitch',
|
||||||
|
kind: 'clip',
|
||||||
|
title: i.title || '',
|
||||||
|
// Clip thumbnails often have width/height placeholders or fixed preview sizes
|
||||||
|
thumbnail: (i.thumbnail_url || '').replace('%{width}', '640').replace('%{height}', '360'),
|
||||||
|
uploaderName: i.broadcaster_name || i.creator_name || '',
|
||||||
|
uploaderUrl: i.broadcaster_name ? `https://www.twitch.tv/${i.broadcaster_name}` : '',
|
||||||
|
uploaderAvatar: '',
|
||||||
|
uploadedDate: i.created_at || '',
|
||||||
|
duration: Number(i.duration || 0),
|
||||||
|
views: Number(i.view_count || 0),
|
||||||
|
uploaded: i.created_at ? Date.parse(i.created_at) : 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search Twitch Clips by first finding channels for a query (or recent popular channels when empty),
|
||||||
|
* then fetching recent clips per broadcaster. Returns up to ~24 clips with pagination via channel search cursor.
|
||||||
|
*/
|
||||||
|
public searchTwitchClipsPage(query: string, cursor?: string | null): Observable<PagedResult<Video>> {
|
||||||
|
return this.getTwitchAuthToken().pipe(
|
||||||
|
switchMap(token => {
|
||||||
|
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 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 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>))
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
catchError((e) => { console.error('Twitch clips search error', e); return of({ items: [], nextCursor: null }); })
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private parseISODurationToSeconds(iso: string): number {
|
private parseISODurationToSeconds(iso: string): number {
|
||||||
// ... (rest of the code remains the same)
|
// ... (rest of the code remains the same)
|
||||||
const m = /^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$/i.exec(iso || '');
|
const m = /^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$/i.exec(iso || '');
|
||||||
|
Loading…
x
Reference in New Issue
Block a user