From 6b20b1ce79ff1b2e4ccd15fff0a16ea112769caf Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Wed, 30 Sep 2026 19:55:23 -0400 Subject: [PATCH] fix(shorts): anti-Forbidden Dailymotion + selecteur icon-only - DM: embed mute par defaut (comme /watch) + son sur action explicite - helmet: Referrer-Policy strict-origin-when-cross-origin (Dailymotion exige un Referer) - searchVideosPage DM: champs allow_embed/width/height/language/owner.id - iframes shorts/watch: allow fullscreen (conforme embed officiel DM) - selecteur shorts icon-only (pastilles + globe cyclique) --- docs/plan-phases-catalogue-classification.md | 2 +- server/index.mjs | 6 +++ .../shorts/watch-short.component.html | 53 ++++++++++--------- .../shorts/watch-short.component.ts | 25 +++++++-- src/components/watch/watch.component.html | 2 +- src/services/youtube-api.service.ts | 5 +- 6 files changed, 61 insertions(+), 32 deletions(-) diff --git a/docs/plan-phases-catalogue-classification.md b/docs/plan-phases-catalogue-classification.md index 9effca7..c2ac282 100644 --- a/docs/plan-phases-catalogue-classification.md +++ b/docs/plan-phases-catalogue-classification.md @@ -488,7 +488,7 @@ La table est **additive**. Le Phase 5.3 (LEFT JOIN) est le seul changement de le - [x] Anti-Forbidden Dailymotion : `allow_embed` demandé côté serveur → `embeddable` → `raw.allowEmbed`, le garde `isPlayable` existant exclut ces vidéos avant affichage (même garde que le chemin historique). - [x] Chrome auto-hide au hover (desktop, 2,5 s d'immobilité) ; capteur tactile mobile (l'iframe mange les touchers) : swipe = navigation garantie, tap = play/pause YouTube (API iframe) ou 6 s d'accès direct au lecteur sinon (les boutons du fournisseur restent atteignables) ; flèche retour accueil en haut à gauche sur mobile ; cadre quasi plein écran (`--shorts-chrome: 5,5rem`). - [x] Avance auto fiabilisée : handshake `listening` YouTube SANS LEQUEL aucun événement player n'arrive (progression + fin mortes), `onStateChange` canonique, réessais bornés quand `next()` est absorbé par un pré-chargement en cours. -- [x] Pastilles à logos (disque aux couleurs de la marque + glyphe : play YouTube, bulle Twitch, initiale sinon ; icône étincelles pour « Pour toi ») et filtre de langues préférées visible (`Profil / Français / Anglais / FR+EN / Toutes`, persisté `newtube.shorts.langFilter`, invité compris) : chaque bouton — chaque provider + « Pour toi », catalogue comme historique — passe par `effectiveShortsLangs()` (`resolveShortsLangs` pur, spec). +- [x] Pastilles icon-only (aucun texte : disque aux couleurs de la marque + glyphe — play YouTube, bulle Twitch, initiale sinon ; étincelles pour « Pour toi » ; globe cyclique Profil → FR → EN → FR+EN → Toutes pour les langues, anneau ambré quand un filtre actif, état dans le tooltip/`aria-label`) et filtre de langues préférées : chaque bouton — chaque provider + « Pour toi », catalogue comme historique — passe par `effectiveShortsLangs()` (`resolveShortsLangs` pur, spec). --- diff --git a/server/index.mjs b/server/index.mjs index e293964..7578849 100644 --- a/server/index.mjs +++ b/server/index.mjs @@ -957,6 +957,12 @@ app.use(helmet({ crossOriginEmbedderPolicy: false, // Allow loading cross‑origin images crossOriginResourcePolicy: { policy: 'cross-origin' }, + // Dailymotion exige un Referer sur la page d'embed (sans lui, le player + // bloque la lecture). Le défaut helmet (`no-referrer`) coupait le Referer + // de toute la page : on envoie l'origine (recommandation Dailymotion + // "Referrer policy and Player embedding", valeur par défaut des navigateurs + // depuis 2021). Les iframes précisent déjà leur propre referrerpolicy. + referrerPolicy: { policy: 'strict-origin-when-cross-origin' }, })); app.use(express.json()); app.use(express.urlencoded({ extended: true })); diff --git a/src/components/shorts/watch-short.component.html b/src/components/shorts/watch-short.component.html index 29f8bc9..4222fc5 100644 --- a/src/components/shorts/watch-short.component.html +++ b/src/components/shorts/watch-short.component.html @@ -15,50 +15,51 @@ - +
- - - - - + +
@@ -77,7 +78,7 @@
@if (embedUrl(); as url) { } @else if (loading()) {
diff --git a/src/components/shorts/watch-short.component.ts b/src/components/shorts/watch-short.component.ts index c884b33..5f0edfe 100644 --- a/src/components/shorts/watch-short.component.ts +++ b/src/components/shorts/watch-short.component.ts @@ -1,6 +1,5 @@ import { ChangeDetectionStrategy, Component, computed, effect, ElementRef, inject, signal, viewChild, HostListener, OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; import { RouterLink } from '@angular/router'; import { HttpClient } from '@angular/common/http'; import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; @@ -24,6 +23,7 @@ import { formatCapturedAgo, normalizeShortsLangMode, resolveShortsLangs, + SHORTS_LANG_MODES, type ShortsLangMode, SHORTS_CATALOG_PAGE_SIZE, SHORTS_CATALOG_MIN_ITEMS, @@ -86,7 +86,7 @@ const SHORTS_QUERIES: Record = { selector: 'app-watch-short', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [CommonModule, FormsModule, RouterLink], + imports: [CommonModule, RouterLink], templateUrl: './watch-short.component.html' }) export class WatchShortComponent implements OnDestroy { @@ -244,6 +244,17 @@ export class WatchShortComponent implements OnDestroy { try { this.loadFeed(); } catch {} } + /** + * Globe cyclique (bouton icon-only) : Profil → Français → Anglais → + * FR+EN → Toutes → Profil… Le filtre actif se lit dans le tooltip et + * l'anneau ambré ; aucun texte affiché. + */ + cycleLangFilter(): void { + const order = [...SHORTS_LANG_MODES]; + const at = order.indexOf(this.langFilter()); + this.setLangFilter(order[(at + 1) % order.length]); + } + /** Langues effectives : `[]` = aucune restriction (tout passe). */ private effectiveShortsLangs(): string[] { try { @@ -367,7 +378,15 @@ export class WatchShortComponent implements OnDestroy { // Nouveau player obligatoire depuis fév. 2026 (l'ancien /embed est // coupé et répond "Forbidden"). Seuls mute/startTime/loop passent // en query sans Player ID ; autoplay est tenté au mieux. - const u = `https://geo.dailymotion.com/player.html?video=${encodeURIComponent(v.videoId)}&mute=${this.muted() ? 1 : 0}&autoplay=1`; + // Correctif bug "Forbidden" systématique en Shorts alors que /watch + // (toujours muet) joue la même vidéo : on démarre MUET comme /watch + // (même URL, mute=1) et on ne passe au son QUE sur action explicite + // du bouton Son — le clic EST le geste qui autorise le son (même + // motif que le player Odysee, cf. soundTouched). L'autoplay sonore + // sans geste préalable est bloqué et le player par défaut l'affiche + // comme "Forbidden". + const withSound = !this.muted() && this.soundTouched(); + const u = `https://geo.dailymotion.com/player.html?video=${encodeURIComponent(v.videoId)}&mute=${withSound ? 0 : 1}&autoplay=1`; return this.sanitizer.bypassSecurityTrustResourceUrl(u); } if (p === 'twitch' && v.videoId) { diff --git a/src/components/watch/watch.component.html b/src/components/watch/watch.component.html index b51ec42..1a9ab40 100644 --- a/src/components/watch/watch.component.html +++ b/src/components/watch/watch.component.html @@ -26,7 +26,7 @@ @if (embedUrl(); as src) { } @else if (videoSource()) {