CI / build-and-test (push) Canceled after 0s
- PeerTube : l'adaptateur pt.ts reconstruisait l'item SANS `url` et le pont catalogue ne conservait l'URL que pour Odysee → /shorts reconstruisait l'embed sur l'instance ACTIVE → « Sorry this video does not exist » sur toute vidéo fédérée. `url` est désormais miroitée dans VideoItem (contrat v2), propagée par PtAdapter et gardée par catalogShortToVideo : peerTubeHostOf() dérive enfin le host d'origine. Vérifié de bout en bout : embed tube.blueben.net → 200 + vrai titre, cas de test dédié dans shorts-catalog.spec. - FAB du menu : déplaçable au pointeur (seuil 6 px, position en % du cadre, persistée en localStorage). Le clic qui suit un glissement n'ouvre pas le menu, et le panneau s'ouvre TOUJOURS vers l'intérieur du cadre (classes pm-opens-left / pm-opens-right / pm-opens-up posées par applyAnchor) : sinon overflow:hidden le rognait dès qu'on déplaçait le bouton. - Bureau : --shorts-chrome 9rem → 4rem (la vidéo passe de 691 à 771 px de haut en 1280x900, 389 → 434 de large) et menu compact au-delà de 768 px (FAB 36 → 30 px, panneau 233 → 202 px). Mobile inchangé (36 / 636 px). Tests : test:shorts-catalog (nouveau cas PeerTube), test:kind, test:section, test:search verts ; build ng OK ; sondage Playwright 19/19 sur l'instance locale (layout, glissement + restauration, embed PeerTube 200 sans erreur).
63 lines
3.5 KiB
TypeScript
63 lines
3.5 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';
|
|
import { readSearchGroup } from '../search-contract';
|
|
import { mapProvenance } from './provenance';
|
|
import { buildPeerTubeComposite, serverRefValue } from '../../shared/providers/channel-ref';
|
|
|
|
export class PtAdapter implements ProviderAdapter<VideoItem> {
|
|
key = 'pt' as const;
|
|
label = 'PeerTube';
|
|
constructor(private http: HttpClient) {}
|
|
async search(params: ProviderSearchParams, _signal: AbortSignal): Promise<SearchResult<VideoItem>> {
|
|
let httpParams = new HttpParams().set('q', params.q).set('providers', 'pt');
|
|
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 { items: rawItems, providerError } = readSearchGroup(res, 'pt');
|
|
const items: VideoItem[] = rawItems.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`.
|
|
// Phase 6 : si le serveur a envoyé `channelRef`, sa valeur canonique
|
|
// prime — sinon on reconstruit avec la règle historique.
|
|
let channelExternalId: string | undefined =
|
|
serverRefValue(it?.channelRef, 'pt') ?? buildPeerTubeComposite(it?.url, it?.channelId);
|
|
return {
|
|
// Phase 7.3 — provenance (instant de collecte + voie), commune aux 6.
|
|
...mapProvenance(it),
|
|
id: it.id,
|
|
provider: 'peertube',
|
|
title: it.title,
|
|
channelName: it.uploaderName,
|
|
durationSec: it.duration,
|
|
thumbnailUrl: it.thumbnail,
|
|
// `viewCount: undefined` en dur alors que le serveur envoie déjà le
|
|
// nombre de vues : les compteurs PeerTube n'arrivaient jamais dans l'UI
|
|
// (trouvé en gelant la sortie réelle, phase 8.4). On lit le champ
|
|
// canonique `views` et on garde `viewCount` en repli.
|
|
viewCount: typeof it.views === 'number' ? it.views
|
|
: (typeof it.viewCount === 'number' ? it.viewCount : undefined),
|
|
// Le serveur envoie `publishedAt` (ISO) quand l'instance l'expose.
|
|
publishedAt: typeof it.publishedAt === 'string' ? it.publishedAt : undefined,
|
|
channelExternalId,
|
|
// URL canonnique : instance d'origine (vidéos fédérées). Sans elle,
|
|
// /shorts reconstruit l'embed sur l'instance ACTIVE → « Sorry this
|
|
// video does not exist ».
|
|
url: typeof it.url === 'string' ? it.url : undefined,
|
|
// Signaux de type pour le raffinement client des filtres.
|
|
type: typeof it.type === 'string' ? it.type : undefined,
|
|
isShort: it.isShort === true ? true : undefined,
|
|
// Dimensions (signal d'orientation pour la détection Short).
|
|
width: typeof it.width === 'number' ? it.width : undefined,
|
|
height: typeof it.height === 'number' ? it.height : undefined,
|
|
} as VideoItem;
|
|
});
|
|
return { items, total: rawItems.length, providerError };
|
|
}
|
|
}
|