import { ChangeDetectionStrategy, Component, computed, effect, inject, signal, HostListener } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterLink } from '@angular/router'; import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; import { firstValueFrom, forkJoin, of } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { YoutubeApiService } from '../../services/youtube-api.service'; import { InstanceService, Provider } from '../../services/instance.service'; import { HistoryService } from '../../services/history.service'; import { LikesService } from '../../services/likes.service'; import { DownloadService } from '../../services/download.service'; import { SubscriptionsService, GROUP_ICON_KEYWORDS } from '../../services/subscriptions.service'; import { AuthService } from '../../services/auth.service'; import { Video } from '../../models/video.model'; import { isShortVideo, YOUTUBE_SHORT_MAX_SECONDS } from '../../app/shared/utils/video-kind'; /** Durée max d'un Short Rumble natif (rumble.com/shorts : vertical ≤ 90 s). */ const RUMBLE_SHORT_MAX_SECONDS = 90; /** Mots vides FR/EN + termes vidéo génériques (extraction de sujets "Pour toi"). */ const TOPIC_STOPWORDS = new Set([ 'les', 'des', 'une', 'dans', 'avec', 'pour', 'sur', 'par', 'pas', 'plus', 'tout', 'tous', 'toute', 'toutes', 'cette', 'ces', 'son', 'sa', 'ses', 'mon', 'ma', 'mes', 'ton', 'ta', 'tes', 'notre', 'votre', 'leurs', 'qui', 'que', 'quoi', 'dont', 'est', 'sont', 'été', 'être', 'avoir', 'faire', 'fait', 'faites', 'vous', 'nous', 'ils', 'elles', 'mais', 'comme', 'aussi', 'entre', 'sans', 'sous', 'chez', 'vers', 'très', 'the', 'and', 'for', 'with', 'from', 'that', 'this', 'these', 'those', 'have', 'has', 'had', 'will', 'would', 'can', 'all', 'your', 'you', 'our', 'their', 'they', 'them', 'she', 'her', 'his', 'him', 'are', 'was', 'were', 'been', 'being', 'what', 'when', 'where', 'which', 'how', 'why', 'not', 'but', 'video', 'vidéo', 'videos', 'vidéos', 'official', 'officiel', 'officielle', 'clip', 'clips', 'live', 'short', 'shorts', 'bande', 'annonce', 'trailer', 'episode', 'épisode', 'part', 'partie', 'full', 'entier', 'entière', 'complete', 'complet', 'complète', 'best', 'top', 'nouveau', 'nouvelle', 'bestof', ]); /** * Rotation de requêtes par provider pour un défilement infini façon YouTube * Shorts : quand une page/un curseur est épuisé, on passe à la requête * suivante au lieu d'afficher une impasse. Les requêtes restent génériques * pour maximiser le nombre de résultats courts. */ const SHORTS_QUERIES: Record = { youtube: ['shorts', '#shorts', 'vertical video', 'short film', 'funny shorts'], dailymotion: ['shorts', '#shorts', 'vertical', 'funny', 'music'], peertube: ['shorts', 'vertical', 'funny', 'music', 'clip'], rumble: ['shorts', 'funny', 'viral', 'music', 'sports'], odysee: ['shorts', 'funny', 'crypto', 'music', 'gaming'], twitch: ['Just Chatting', 'gaming', 'music', 'sports', 'food'], }; @Component({ selector: 'app-watch-short', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, RouterLink], templateUrl: './watch-short.component.html' }) export class WatchShortComponent { private api = inject(YoutubeApiService); private instances = inject(InstanceService); private sanitizer = inject(DomSanitizer); private history = inject(HistoryService); private likes = inject(LikesService); private downloads = inject(DownloadService); private subs = inject(SubscriptionsService); private auth = inject(AuthService); loading = signal(true); error = signal(null); items = signal([]); index = signal(0); nextCursor = signal(null); busyMore = signal(false); /** Mode "Pour toi" : flux perso multi-fournisseurs (voir loadForYou). */ forYou = signal(false); topics = signal([]); topicsLoading = signal(false); /** IDs déjà vus (dedup inter-pages pour l'infini). */ private seenIds = new Set(); /** Position dans la rotation SHORTS_QUERIES pour le provider courant. */ private queryIndex = 0; /** Requêtes déjà épuisées (curseur null) pour ce chargement. */ private exhaustedQueries = new Set(); // scroll/swipe helpers private wheelAccum = 0; private lastSwipeY: number | null = null; private lastScrollTs = 0; private lastProvider: string | null = null; readonly provider = computed(() => this.instances.selectedProvider()); readonly providersList = computed(() => this.instances.providers()); readonly current = computed