fix(shorts): anti-Forbidden Dailymotion + selecteur icon-only
CI / build-and-test (push) Successful in 14m55s
CI / build-and-test (push) Successful in 14m55s
- 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)
This commit is contained in:
@@ -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).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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 }));
|
||||
|
||||
@@ -15,50 +15,51 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M19 12H5m7-7l-7 7 7 7"/></svg>
|
||||
</a>
|
||||
|
||||
<!-- Sélecteur : Pour toi + fournisseurs (logos) + filtre de langues préférées -->
|
||||
<!-- Sélecteur icon-only : Pour toi + fournisseurs (logos) + filtre de langues.
|
||||
Aucun texte visible (logos + title/aria-label) ; le filtre actif se lit
|
||||
dans le tooltip et l'anneau ambré du globe. -->
|
||||
<div class="scroll-rail no-scrollbar shorts-chrome relative z-10 flex w-full max-w-full items-center justify-start gap-2 overflow-x-auto px-3 pt-3 sm:justify-center" role="tablist" aria-label="Fournisseur des Shorts" (click)="$event.stopPropagation()">
|
||||
<button (click)="toggleForYou()"
|
||||
[attr.aria-pressed]="forYou()"
|
||||
class="chip flex shrink-0 items-center gap-1.5 px-3 py-2 rounded-full text-[13px] border transition-colors min-h-[44px]"
|
||||
[class]="forYou() ? 'bg-violet-600 border-violet-500 text-white' : 'bg-black/60 border-white/15 text-slate-200 hover:bg-white/10'"
|
||||
title="Shorts personnalisés multi-fournisseurs à partir de vos abonnements, groupes, recherches, likes et téléchargements">
|
||||
<span class="flex h-6 w-6 items-center justify-center rounded-full bg-gradient-to-br from-violet-500 to-fuchsia-500 text-white shadow" aria-hidden="true">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2c.7 4.8 3.2 7.3 8 8-4.8.7-7.3 3.2-8 8-.7-4.8-3.2-7.3-8-8 4.8-.7 7.3-3.2 8-8z"/><path d="M19 3c.3 2 1.3 3 3.3 3.3-2 .3-3 1.3-3.3 3.2-.3-1.9-1.3-2.9-3.2-3.2 1.9-.3 2.9-1.3 3.2-3.3z"/></svg>
|
||||
aria-label="Pour toi"
|
||||
title="Pour toi — Shorts personnalisés multi-fournisseurs à partir de vos abonnements, groupes, recherches, likes et téléchargements"
|
||||
class="chip flex h-11 w-11 shrink-0 items-center justify-center rounded-full border transition-colors"
|
||||
[class]="forYou() ? 'bg-violet-600 border-violet-500 text-white' : 'bg-black/60 border-white/15 text-slate-200 hover:bg-white/10'">
|
||||
<span class="flex h-7 w-7 items-center justify-center rounded-full bg-gradient-to-br from-violet-500 to-fuchsia-500 text-white shadow" aria-hidden="true">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2c.7 4.8 3.2 7.3 8 8-4.8.7-7.3 3.2-8 8-.7-4.8-3.2-7.3-8-8 4.8-.7 7.3-3.2 8-8z"/><path d="M19 3c.3 2 1.3 3 3.3 3.3-2 .3-3 1.3-3.3 3.2-.3-1.9-1.3-2.9-3.2-3.2 1.9-.3 2.9-1.3 3.2-3.3z"/></svg>
|
||||
</span>
|
||||
Pour toi
|
||||
</button>
|
||||
<button *ngFor="let p of providerButtons()"
|
||||
(click)="selectProvider(p.id)"
|
||||
[attr.aria-pressed]="!forYou() && provider() === p.id"
|
||||
class="chip flex shrink-0 items-center gap-1.5 px-3 py-2 rounded-full text-[13px] border transition-colors min-h-[44px]"
|
||||
[attr.aria-label]="p.label"
|
||||
[title]="p.label"
|
||||
class="chip flex h-11 w-11 shrink-0 items-center justify-center rounded-full border transition-colors"
|
||||
[class]="!forYou() && provider() === p.id ? 'bg-red-600 border-red-500 text-white' : 'bg-black/60 border-white/15 text-slate-200 hover:bg-white/10'">
|
||||
<span class="flex h-6 w-6 items-center justify-center rounded-full text-white shadow" [class]="p.disc" aria-hidden="true">
|
||||
<span class="flex h-7 w-7 items-center justify-center rounded-full text-white shadow" [class]="p.disc" aria-hidden="true">
|
||||
@switch (p.glyph.kind) {
|
||||
@case ('play') {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>
|
||||
}
|
||||
@case ('chat') {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="4" y="3" width="16" height="13" rx="2"/><path d="M9 16v4l4-4"/><line x1="10" y1="7" x2="10" y2="11"/><line x1="14" y1="7" x2="14" y2="11"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="4" y="3" width="16" height="13" rx="2"/><path d="M9 16v4l4-4"/><line x1="10" y1="7" x2="10" y2="11"/><line x1="14" y1="7" x2="14" y2="11"/></svg>
|
||||
}
|
||||
@default {
|
||||
<span class="text-[13px] font-extrabold leading-none">{{ glyphLetter(p) }}</span>
|
||||
<span class="text-[15px] font-extrabold leading-none">{{ glyphLetter(p) }}</span>
|
||||
}
|
||||
}
|
||||
</span>
|
||||
{{ p.label }}
|
||||
</button>
|
||||
<!-- Filtre de langues préférées : traverse TOUS les boutons (chaque flux
|
||||
passe par `effectiveShortsLangs`). Persisté, invité compris. -->
|
||||
<span class="flex shrink-0 items-center gap-1 rounded-full border border-white/15 bg-black/60 px-2.5 text-[13px] text-slate-200 min-h-[44px]" [title]="langFilterHint()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 shrink-0 opacity-80" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
<select [ngModel]="langFilter()" (ngModelChange)="setLangFilter($event)" class="bg-transparent py-2 pr-0.5 outline-none cursor-pointer" aria-label="Langues préférées des Shorts">
|
||||
<option value="profile">Profil</option>
|
||||
<option value="fr">Français</option>
|
||||
<option value="en">Anglais</option>
|
||||
<option value="fren">FR + EN</option>
|
||||
<option value="all">Toutes</option>
|
||||
</select>
|
||||
</span>
|
||||
<!-- Filtre de langues préférées (globe cyclique, sans texte) : chaque cran
|
||||
change le filtre de TOUS les flux ; l'anneau ambré signale un filtre
|
||||
actif (non-profil), le détail est dans le tooltip. -->
|
||||
<button (click)="cycleLangFilter()"
|
||||
[title]="langFilterHint()"
|
||||
[attr.aria-label]="langFilterHint()"
|
||||
class="flex h-11 w-11 shrink-0 items-center justify-center rounded-full border transition-colors"
|
||||
[class]="langFilter() !== 'profile' ? 'bg-black/60 border-amber-400/70 text-amber-200 hover:bg-white/10 ring-2 ring-amber-400/40' : 'bg-black/60 border-white/15 text-slate-200 hover:bg-white/10'">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Scène immersive : flèches (bureau) + cadre 9:16 -->
|
||||
@@ -77,7 +78,7 @@
|
||||
<div #shortsFrame class="shorts-frame relative shrink-0 overflow-hidden bg-black rounded-none ring-0 shadow-none sm:rounded-2xl sm:ring-1 sm:ring-white/10 sm:shadow-2xl">
|
||||
@if (embedUrl(); as url) {
|
||||
<iframe [src]="url" [title]="current()?.title || 'Short video'" class="absolute inset-0 w-full h-full" frameborder="0" playsinline
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; gyroscope; picture-in-picture; web-share"
|
||||
allowfullscreen referrerpolicy="strict-origin-when-cross-origin"></iframe>
|
||||
} @else if (loading()) {
|
||||
<div class="absolute inset-0 flex flex-col gap-3 items-center justify-center bg-black text-slate-400 text-sm">
|
||||
|
||||
@@ -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<string, string[]> = {
|
||||
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) {
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
@if (embedUrl(); as src) {
|
||||
<iframe #embedFrame class="h-full w-full" [src]="src" frameborder="0" playsinline (load)="onEmbedFrameLoad()"
|
||||
referrerpolicy="origin"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; gyroscope; picture-in-picture; web-share"
|
||||
allowfullscreen title="Lecteur vidéo"></iframe>
|
||||
} @else if (videoSource()) {
|
||||
<app-video-player
|
||||
|
||||
@@ -764,7 +764,10 @@ export class YoutubeApiService {
|
||||
limit: String(limit),
|
||||
page: String(page),
|
||||
country: String(region).toLowerCase(),
|
||||
fields: 'id,title,thumbnail_url,thumbnail_480_url,thumbnail_720_url,duration,views_total,owner.screenname,owner.avatar_80_url,created_time',
|
||||
// `allow_embed` indispensable : sans lui, les vidéos non-embeddables
|
||||
// passent le filtre et le player répond "Forbidden" (même garde que
|
||||
// searchShortsPage ; `width,height` = validation verticale Shorts).
|
||||
fields: 'id,title,thumbnail_url,thumbnail_480_url,thumbnail_720_url,duration,width,height,views_total,language,allow_embed,owner.id,owner.screenname,owner.avatar_80_url,created_time',
|
||||
search: String(query),
|
||||
});
|
||||
return this.fetchDailymotion('/videos', params).pipe(
|
||||
|
||||
Reference in New Issue
Block a user