feat(preferences): section langues a telecharger (defaut fr+en), transcripts filtres cote serveur et watch
CI / build-and-test (push) Successful in 13m50s
CI / build-and-test (push) Successful in 13m50s
This commit is contained in:
@@ -13,6 +13,7 @@ export class DmAdapter implements ProviderAdapter<VideoItem> {
|
||||
if (params.pageToken) httpParams = httpParams.set('page', params.pageToken);
|
||||
if (params.sort) httpParams = httpParams.set('sort', params.sort);
|
||||
const res = await firstValueFrom(this.http.get<any>(`/api/search`, { params: httpParams }));
|
||||
const providerError = typeof res?.errors?.dm?.message === 'string' ? res.errors.dm.message : undefined;
|
||||
const items: VideoItem[] = (res?.groups?.dm || []).map((it: any) => ({
|
||||
id: it.id,
|
||||
provider: 'dailymotion',
|
||||
@@ -25,6 +26,6 @@ export class DmAdapter implements ProviderAdapter<VideoItem> {
|
||||
channelExternalId: it.channelId || undefined,
|
||||
channelAvatarUrl: it.uploaderAvatar || undefined,
|
||||
}));
|
||||
return { items, total: Array.isArray(res?.groups?.dm) ? res.groups.dm.length : 0 };
|
||||
return { items, total: Array.isArray(res?.groups?.dm) ? res.groups.dm.length : 0, providerError };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export class OdAdapter implements ProviderAdapter<VideoItem> {
|
||||
if (params.pageToken) httpParams = httpParams.set('page', params.pageToken);
|
||||
if (params.sort) httpParams = httpParams.set('sort', params.sort);
|
||||
const res = await firstValueFrom(this.http.get<any>(`/api/search`, { params: httpParams }));
|
||||
const providerError = typeof res?.errors?.od?.message === 'string' ? res.errors.od.message : undefined;
|
||||
const items: VideoItem[] = (res?.groups?.od || []).map((it: any) => {
|
||||
let slug: string | undefined;
|
||||
try {
|
||||
@@ -35,6 +36,6 @@ export class OdAdapter implements ProviderAdapter<VideoItem> {
|
||||
channelExternalId: it.uploaderName || undefined,
|
||||
} as VideoItem;
|
||||
});
|
||||
return { items, total: Array.isArray(res?.groups?.od) ? res.groups.od.length : 0 };
|
||||
return { items, total: Array.isArray(res?.groups?.od) ? res.groups.od.length : 0, providerError };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export class PtAdapter implements ProviderAdapter<VideoItem> {
|
||||
if (params.pageToken) httpParams = httpParams.set('page', params.pageToken);
|
||||
if (params.sort) httpParams = httpParams.set('sort', params.sort);
|
||||
const res = await firstValueFrom(this.http.get<any>(`/api/search`, { params: httpParams }));
|
||||
const providerError = typeof res?.errors?.pt?.message === 'string' ? res.errors.pt.message : undefined;
|
||||
const items: VideoItem[] = (res?.groups?.pt || []).map((it: any) => {
|
||||
// PeerTube identifie une chaîne par `instance|channel` : l'instance est
|
||||
// dérivée de l'URL vidéo, le nom de chaîne de `channelId`.
|
||||
@@ -32,6 +33,6 @@ export class PtAdapter implements ProviderAdapter<VideoItem> {
|
||||
channelExternalId,
|
||||
};
|
||||
});
|
||||
return { items, total: Array.isArray(res?.groups?.pt) ? res.groups.pt.length : 0 };
|
||||
return { items, total: Array.isArray(res?.groups?.pt) ? res.groups.pt.length : 0, providerError };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export class RuAdapter implements ProviderAdapter<VideoItem> {
|
||||
if (params.pageToken) httpParams = httpParams.set('page', params.pageToken);
|
||||
if (params.sort) httpParams = httpParams.set('sort', params.sort);
|
||||
const res = await firstValueFrom(this.http.get<any>(`/api/search`, { params: httpParams }));
|
||||
const providerError = typeof res?.errors?.ru?.message === 'string' ? res.errors.ru.message : undefined;
|
||||
const items: VideoItem[] = (res?.groups?.ru || []).map((it: any) => ({
|
||||
id: it.id,
|
||||
provider: 'rumble',
|
||||
@@ -22,6 +23,6 @@ export class RuAdapter implements ProviderAdapter<VideoItem> {
|
||||
viewCount: typeof it.views === 'number' ? it.views : undefined,
|
||||
publishedAt: it.publishedAt,
|
||||
}));
|
||||
return { items, total: Array.isArray(res?.groups?.ru) ? res.groups.ru.length : 0 };
|
||||
return { items, total: Array.isArray(res?.groups?.ru) ? res.groups.ru.length : 0, providerError };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,29 +12,38 @@ export class TwAdapter implements ProviderAdapter<VideoItem> {
|
||||
if (params.pageToken) httpParams = httpParams.set('page', params.pageToken);
|
||||
if (params.sort) httpParams = httpParams.set('sort', params.sort);
|
||||
const res = await firstValueFrom(this.http.get<any>(`/api/search`, { params: httpParams }));
|
||||
const providerError = typeof res?.errors?.tw?.message === 'string' ? res.errors.tw.message : undefined;
|
||||
const items: VideoItem[] = (res?.groups?.tw || []).map((it: any) => {
|
||||
// /api/search (tw) ne retourne que des chaînes : le player exige le login
|
||||
// (?channel=<login>), jamais l'id numérique. Sans `channel`, /watch tente
|
||||
// ?video=<id> et Twitch répond par une erreur.
|
||||
// /api/search (tw) renvoie un mixte façon directory : lives, VODs, clips, chaînes.
|
||||
// Seuls les items 'live' / 'channel' utilisent l'embed ?channel=<login> ;
|
||||
// VODs (id numérique) et clips (slug) ont leur propre lecture.
|
||||
const login: string | undefined =
|
||||
it.channelExternalId || it.channelHandle || undefined;
|
||||
const kind = String(it?.type || '').toLowerCase();
|
||||
const isChannel = kind === 'channel' || kind === 'live' || Boolean(login);
|
||||
const clipKind = String(it?.kind || '').toLowerCase();
|
||||
const isClip = clipKind === 'clip';
|
||||
const isChannelLike = kind === 'channel' || kind === 'live';
|
||||
return {
|
||||
id: it.id,
|
||||
provider: 'twitch',
|
||||
title: it.title,
|
||||
channelName: it.uploaderName,
|
||||
durationSec: it.duration,
|
||||
durationSec: typeof it.duration === 'number' ? it.duration : undefined,
|
||||
thumbnailUrl: it.thumbnail,
|
||||
viewCount: typeof it.views === 'number' ? it.views : undefined,
|
||||
publishedAt: it.publishedAt,
|
||||
channelExternalId: login,
|
||||
channelAvatarUrl: it.uploaderAvatar || undefined,
|
||||
// Login pour l'embed ?channel=... (video-card -> watchQueryParams).
|
||||
channel: isChannel ? login : undefined,
|
||||
// Login pour l'embed ?channel=... (video-card -> watchQueryParams),
|
||||
// uniquement pour les chaînes / lives — jamais pour VODs ni clips.
|
||||
channel: isChannelLike && !isClip ? login : undefined,
|
||||
// Statut conservé pour le filtre "Type" (live / vidéo / clip / chaîne).
|
||||
type: typeof it.type === 'string' ? it.type : undefined,
|
||||
isLive: typeof it.isLive === 'boolean' ? it.isLive : undefined,
|
||||
kind: typeof it.kind === 'string' ? it.kind : undefined,
|
||||
game: typeof it.game === 'string' ? it.game : undefined,
|
||||
} as VideoItem;
|
||||
});
|
||||
return { items, total: Array.isArray(res?.groups?.tw) ? res.groups.tw.length : 0 };
|
||||
}).filter((v: VideoItem) => v.id != null && String(v.id).length > 0 && (v.thumbnailUrl || v.channelAvatarUrl));
|
||||
return { items, total: items.length, providerError };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export class YtAdapter implements ProviderAdapter<VideoItem> {
|
||||
if (params.pageToken) httpParams = httpParams.set('page', params.pageToken);
|
||||
if (params.sort) httpParams = httpParams.set('sort', params.sort);
|
||||
const res = await firstValueFrom(this.http.get<any>(`/api/search`, { params: httpParams }));
|
||||
const providerError = typeof res?.errors?.yt?.message === 'string' ? res.errors.yt.message : undefined;
|
||||
const items: VideoItem[] = (res?.groups?.yt || []).map((it: any) => ({
|
||||
id: it.id,
|
||||
provider: 'youtube',
|
||||
@@ -25,6 +26,6 @@ export class YtAdapter implements ProviderAdapter<VideoItem> {
|
||||
// (l'avatar est résolu via /api/channels, l'API search n'en fournit pas).
|
||||
channelExternalId: it.channelExternalId || it.channelId || undefined,
|
||||
}));
|
||||
return { items, total: Array.isArray(res?.groups?.yt) ? res.groups.yt.length : 0 };
|
||||
return { items, total: Array.isArray(res?.groups?.yt) ? res.groups.yt.length : 0, providerError };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,4 +18,6 @@ export interface SearchResponseV1 {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
sort?: 'relevance' | 'date' | 'views';
|
||||
/** Per-provider backend failures, keyed by provider id. */
|
||||
errors?: Partial<Record<ProviderId, string>>;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ export interface SearchResult<T = SearchItem> {
|
||||
items: T[];
|
||||
total?: number;
|
||||
nextPageToken?: string; // or page: string|number
|
||||
/** Backend per-provider failure (e.g. quota), surfaced for UI notice. */
|
||||
providerError?: string;
|
||||
}
|
||||
|
||||
export interface ProviderSearchParams {
|
||||
@@ -26,7 +28,7 @@ export interface ProviderSearchParams {
|
||||
sort?: 'relevance'|'date'|'views'|'duration';
|
||||
time?: 'today'|'7d'|'30d'|'all';
|
||||
length?: 'short'|'medium'|'long'|'all';
|
||||
type?: 'video'|'live'|'shorts'|'all';
|
||||
type?: 'video'|'live'|'shorts'|'short'|'recorded'|'channel'|'all';
|
||||
}
|
||||
|
||||
export interface ProviderAdapter<T = SearchItem> {
|
||||
|
||||
@@ -64,9 +64,17 @@ export class SearchService {
|
||||
acc[pid as ProviderId] = result.items;
|
||||
return acc;
|
||||
}, {} as Record<ProviderId, VideoItem[]>);
|
||||
// Surface per-provider backend failures (e.g. YouTube quota) for UI notice.
|
||||
const errors: Partial<Record<ProviderId, string>> = {};
|
||||
for (const [pid, result] of Object.entries(byProvider)) {
|
||||
if (result?.providerError) errors[pid as ProviderId] = result.providerError;
|
||||
}
|
||||
|
||||
const providers = Object.keys(byProvider) as ProviderId[];
|
||||
const resp: SearchResponse = { q: this.q$.value, providers, groups };
|
||||
const resp: SearchResponse = {
|
||||
q: this.q$.value, providers, groups,
|
||||
...(Object.keys(errors).length > 0 ? { errors } : {}),
|
||||
};
|
||||
return resp;
|
||||
}),
|
||||
shareReplay(1)
|
||||
|
||||
Reference in New Issue
Block a user