CI / build-and-test (push) Successful in 14m10s
Barre de recherche - Panneau de filtres unifié (remplace le ProviderPicker) : sources, type (vidéos/shorts/direct/chaînes), période (heure/jour/semaine/mois/année), durée (<4 / 4-20 / >20 min) et tri. Pastilles + roving tabindex (←/→ dans un groupe, ↑/↓ entre groupes), focus trap, Échap, Ctrl/⌘+Maj+F, recherches récentes, « mémoriser par défaut », « Réinitialiser ». - Pastilles des filtres actifs sous la barre (retrait en un clic) + compteur. - Typeahead : un seul keydown (fin du double Enter), ↑/↓ bouclants, Home/End, PageUp/PageDown, Entrée valide la ligne surlignée (sinon recherche brute), Tab complète sans chercher, Échap ferme puis vide, ligne « Rechercher <q> », défilement de l'option active, aria à jour. - Focus : le panneau se referme dès que le focus quitte la barre (focusout + relatedTarget + activeElement), pointerdown neutralisé sur les lignes pour garder le focus dans l'input (plus de scintillement). - Opérateurs en clair : `linux live:`, `tuto today: long:` deviennent des filtres et sont retirés de la requête (autocomplétion après le `:`). Filtres côté serveur - Modèle partagé : src/app/search/filters.ts + server/search-filters.mjs. - /api/search?type=&duration=&period=&sort= : InnerTube (upload_date, type, duration, features), Data API v3 (type, videoDuration, publishedAfter), yt-dlp --dateafter ; post-filtrage pour les providers sans filtre natif. Un champ manquant ne fait jamais disparaître un résultat. - Filtres dans l'URL (partageables) + adapters + SearchService (clé de cache). - Correction : la recherche Shorts renvoyait 0 (ShortsLockupView/GridShelfView non mappés, id/titre dans l'endpoint) -> mappage + shelf + vignette/vues. Tests / docs - 23 tests unitaires (clavier, focus, opérateurs, panneau de filtres), server/tests/search-filters.test.mjs (npm run test:filters) + CI, scénarios e2e des filtres, README/api/README/todo/MCP à jour.
49 lines
2.3 KiB
TypeScript
49 lines
2.3 KiB
TypeScript
import { HttpClient, HttpParams } from '@angular/common/http';
|
|
import { firstValueFrom } from 'rxjs';
|
|
import type { ProviderAdapter, ProviderSearchParams, SearchResult } from '../models';
|
|
import { VideoItem } from 'src/app/shared/models/video-item.model';
|
|
import { withFilterParams } from './filter-params';
|
|
|
|
export class OdAdapter implements ProviderAdapter<VideoItem> {
|
|
key = 'od' as const;
|
|
label = 'Odysee';
|
|
constructor(private http: HttpClient) {}
|
|
async search(params: ProviderSearchParams, _signal: AbortSignal): Promise<SearchResult<VideoItem>> {
|
|
let httpParams = new HttpParams().set('q', params.q).set('providers', 'od');
|
|
if (params.pageToken) httpParams = httpParams.set('page', params.pageToken);
|
|
if (params.sort) httpParams = httpParams.set('sort', params.sort);
|
|
// Filtres (type / durée / période) : appliqués côté serveur quand le provider
|
|
// le sait, affinés en post-traitement sinon.
|
|
httpParams = withFilterParams(httpParams, params.filters);
|
|
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 {
|
|
const url: string | undefined = it?.url;
|
|
if (url && /^https?:\/\/odysee\.com\//i.test(url)) {
|
|
slug = url.replace(/^https?:\/\/odysee\.com\//i, '').replace(/^\//, '');
|
|
}
|
|
} catch {}
|
|
return {
|
|
id: it.id,
|
|
provider: 'odysee',
|
|
title: it.title,
|
|
channelName: it.uploaderName,
|
|
durationSec: it.duration,
|
|
thumbnailUrl: it.thumbnail,
|
|
viewCount: undefined,
|
|
publishedAt: undefined,
|
|
slug,
|
|
// Nom de chaîne LBRY (ex. @MaChaine) : le backend /api/channels/od
|
|
// sait le résoudre avec ou sans préfixe @.
|
|
channelExternalId: it.uploaderName || undefined,
|
|
// Signaux de type pour le raffinement client des filtres.
|
|
type: typeof it.type === 'string' ? it.type : undefined,
|
|
isShort: it.isShort === true ? true : undefined,
|
|
} as VideoItem;
|
|
});
|
|
return { items, total: Array.isArray(res?.groups?.od) ? res.groups.od.length : 0, providerError };
|
|
}
|
|
}
|