Files
NewTube/src/app/search/adapters/pt.ts
T
bruno 474de0570c
CI / build-and-test (push) Canceled after 0s
fix(shorts): embed PeerTube sur l'instance d'origine + FAB glissable + layout bureau
- 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).
2026-10-01 23:09:28 -04:00

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 };
}
}