Règle partagée front/back : flag natif, verticale stricte (ratio ≤ 0.8, ≤ 90 s), verticale sans durée jamais short, repli durée (70 YT / 75 autres). Dimensions alimentées par Dailymotion/Odysee/PeerTube, flag maison Rumble supprimé, condition innertube morte simplifiée. Shorts : langues utilisateur (UI + transcripts) via relevanceLanguage/languageOneOf/Twitch language, son activé par défaut.
This commit is contained in:
@@ -84,8 +84,13 @@ export class WatchShortComponent implements OnDestroy {
|
||||
// ---- Expérience façon YouTube Shorts ----
|
||||
/** Cadre 9:16 (plein écran + focus). */
|
||||
frameRef = viewChild<ElementRef<HTMLElement>>('shortsFrame');
|
||||
/** Son coupé (autoplay) : rebascule l'URL d'embed (reprend au début). */
|
||||
muted = signal(true);
|
||||
/**
|
||||
* Son coupé ou non. Par défaut le son est ACTIVÉ sur les Shorts : les
|
||||
* navigateurs le permettent dès la première interaction (molette/clic),
|
||||
* sinon le player démarre en pause et l'utilisateur appuie sur lecture.
|
||||
* Rebasculer rebascule l'URL d'embed (reprend au début).
|
||||
*/
|
||||
muted = signal(false);
|
||||
/**
|
||||
* Embed Rumble autoritaire : id demandé -> vrai id scrapé via
|
||||
* /api/rumble/video/<id> (comme /watch). Les IDs de liste approximatifs
|
||||
@@ -809,7 +814,10 @@ export class WatchShortComponent implements OnDestroy {
|
||||
// Regroupe par provider puis entrelace (tourniquet) pour mixer les sources.
|
||||
const byProvider: Video[][] = defs.map((d, i) => {
|
||||
const raw = ((pages[i]?.items || []) as Video[]).filter(v => !!v?.videoId);
|
||||
const list = raw.filter(v => this.isShort({ ...v, provider: d.provider } as Video));
|
||||
const list = raw.filter(v =>
|
||||
this.isShort({ ...v, provider: d.provider } as Video) &&
|
||||
(d.provider !== 'twitch' || this.shortsLangOk({ ...v, provider: d.provider } as Video))
|
||||
);
|
||||
// Mélange léger par sujet pour varier l'ordre à chaque visite.
|
||||
for (let k = list.length - 1; k > 0; k--) {
|
||||
const j = Math.floor(Math.random() * (k + 1));
|
||||
@@ -918,8 +926,8 @@ export class WatchShortComponent implements OnDestroy {
|
||||
this.api.getTwitchClipsFeed()?.subscribe({
|
||||
next: (res: { items: Video[]; nextCursor?: string | null }) => {
|
||||
const raw: Video[] = (res.items || []).filter((v: Video) => !!v.videoId);
|
||||
// Double sécurité anti "time machine" sur les clips non validés.
|
||||
this.withoutDeadClips(raw.filter((v: Video) => this.isShort(v))).subscribe({
|
||||
// Langue utilisateur + double sécurité anti "time machine".
|
||||
this.twitchShortsReady(raw.filter((v: Video) => this.isShort(v))).subscribe({
|
||||
next: (filtered) => {
|
||||
const list: Video[] = this.dedup(filtered);
|
||||
this.items.set(list);
|
||||
@@ -959,8 +967,8 @@ export class WatchShortComponent implements OnDestroy {
|
||||
next: (res) => {
|
||||
const raw = (res?.items || []).filter(v => !!v.videoId);
|
||||
const shorts = raw.filter(v => this.isShort(v));
|
||||
// Les clips de recherche ne sont pas validés côté service.
|
||||
this.withoutDeadClips(shorts).subscribe({
|
||||
// Langue utilisateur + validation (recherche non validée côté service).
|
||||
this.twitchShortsReady(shorts).subscribe({
|
||||
next: (filtered) => {
|
||||
const list = this.dedup(filtered);
|
||||
this.showTwitchFallbackList(list, res?.nextCursor || null);
|
||||
@@ -1105,6 +1113,29 @@ export class WatchShortComponent implements OnDestroy {
|
||||
return isShortVideo({ ...(v as object), provider: p } as Parameters<typeof isShortVideo>[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* La vidéo respecte-t-elle les langues de Shorts de l'utilisateur
|
||||
* (langue UI + préférences + transcripts) ? Seuls les items Twitch
|
||||
* portent la langue (champ Helix) ; sans métadonnée, le doute profite.
|
||||
* YouTube/PeerTube sont filtrés côté serveur (relevanceLanguage,
|
||||
* languageOneOf) ; Dailymotion/Odysee/Rumble n'exposent pas la langue.
|
||||
*/
|
||||
private shortsLangOk(v: Video): boolean {
|
||||
const p = (v as any).provider || this.provider();
|
||||
if (String(p) !== 'twitch') return true;
|
||||
const lang = String((v as any)?.raw?.language || '').toLowerCase().split('-')[0];
|
||||
if (!lang || lang === 'other') return true;
|
||||
let accepted: string[] = ['en'];
|
||||
try { accepted = this.api.getShortsLanguages(); } catch {}
|
||||
return accepted.includes(lang);
|
||||
}
|
||||
|
||||
/** Shorts Twitch : langue utilisateur + clips morts exclus avant affichage. */
|
||||
private twitchShortsReady(list: Video[]): import('rxjs').Observable<Video[]> {
|
||||
const inLang = list.filter(v => this.shortsLangOk(v));
|
||||
return this.withoutDeadClips(inLang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exclut les clips Twitch morts ("time machine") avant affichage.
|
||||
* Les flux game/broadcaster sont déjà validés côté service ; ce garde-fou
|
||||
@@ -1238,10 +1269,11 @@ export class WatchShortComponent implements OnDestroy {
|
||||
if (p === 'youtube' && raw.length) {
|
||||
this.pickYouTubeShorts(raw).then((list: Video[]) => apply(list.filter((v: Video) => this.isShort(v)))).catch(() => { apply([]); });
|
||||
} else if (p === 'twitch' && raw.length) {
|
||||
// Les clips paginés ne sont pas tous validés côté service.
|
||||
this.withoutDeadClips(raw.filter((v: Video) => this.isShort(v))).subscribe({
|
||||
// Langue utilisateur + clips paginés non tous validés côté service.
|
||||
const shorts = raw.filter((v: Video) => this.isShort(v));
|
||||
this.twitchShortsReady(shorts).subscribe({
|
||||
next: (clean) => apply(clean),
|
||||
error: () => apply(raw.filter((v: Video) => this.isShort(v))),
|
||||
error: () => apply(shorts),
|
||||
});
|
||||
} else {
|
||||
apply(raw.filter((v: Video) => this.isShort(v)));
|
||||
|
||||
Reference in New Issue
Block a user