import { Injectable, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; export interface OAuthConnection { provider: string; externalUserId: string | null; displayName: string | null; avatarUrl: string | null; scopes: string | null; expiresAt: number | null; connectedAt: number | null; updatedAt: number | null; ytChannelId?: string | null; ytPageId?: string | null; } export interface YtChannel { channelId: string | null; title: string; handle: string | null; selected: boolean; } export interface OAuthPreview { provider: string; subscriptions: Array<{ provider: string; externalId: string; title: string; handle?: string | null; avatarUrl?: string | null; url?: string | null }>; likes: Array<{ provider: string; videoId: string; title: string; thumbnail?: string }>; subscriptionCount: number; likeCount: number; /** Totaux du compte (l'aperçu n'affiche que les premiers, l'import prend tout). */ subscriptionTotal?: number; likeTotal?: number; } export interface OAuthImportResult { provider: string; importedSubscriptions: number; importedLikes: number; skippedSubscriptions: number; skippedLikes: number; } @Injectable({ providedIn: 'root' }) export class OAuthImportService { private http = inject(HttpClient); status(): Observable<{ google: { configured: boolean; missing: string[] }; twitch: { configured: boolean; missing: string[] } }> { return this.http.get('/api/oauth/status'); } connections(): Observable<{ connections: OAuthConnection[]; status: any }> { return this.http.get('/api/oauth/connections'); } async connect(provider: 'google' | 'twitch'): Promise { const res: any = await this.http.get(`/api/oauth/${provider}/url`).toPromise().catch((e) => { throw e; }); const url = res?.url; if (!url) throw new Error('oauth_url_failed'); window.location.href = url; } disconnect(provider: string): Observable { return this.http.delete(`/api/oauth/${provider}`); } preview(provider: string): Observable { return this.http.get(`/api/oauth/${provider}/preview`); } importAll(provider: string, types: string[]): Observable { return this.http.post(`/api/oauth/${provider}/import`, { types }); } /** Contenu "Regarder plus tard" YouTube (lecture seule OK). */ watchLater(): Observable<{ items: Array<{ videoId: string; title: string; thumbnail?: string }>; count: number; writeGranted: boolean; via?: string; note?: string }> { return this.http.get(`/api/oauth/google/watchlater`); } /** Pousse des vidéos vers "Regarder plus tard" YouTube (scope écriture requis). */ pushWatchLater(videoIds: string[]): Observable<{ added: number; skipped: number; total: number }> { return this.http.post(`/api/oauth/google/watchlater`, { videoIds }); } /** Lot d'entrées d'historique Takeout (max 1000/lot côté serveur). */ importTakeoutBatch(items: Array<{ videoId: string; title?: string; watchedAt?: string }>): Observable<{ imported: number; skipped: number; total: number }> { return this.http.post(`/api/user/history/takeout`, { items }); } /** Historique YouTube via InnerTube authentifié (sync auto, sans fichier). */ ytHistory(max = 200): Observable<{ items: Array<{ videoId: string; title: string; thumbnail?: string; watchedAt?: string }>; count: number; hasMore: boolean }> { return this.http.get(`/api/oauth/google/yt-history?max=${max}`); } /** Chaînes YouTube du compte (sélecteur multi-chaînes / marque). */ ytChannels(): Observable<{ channels: YtChannel[]; selected: { channelId: string | null; pageId: string | null } }> { return this.http.get(`/api/oauth/google/yt-channels`); } /** Mémorise la chaîne à cibler pour l'historique et Regarder plus tard. */ saveYtChannel(channelId: string | null, pageId: string | null): Observable<{ selected: { channelId: string | null; pageId: string | null } }> { return this.http.put(`/api/oauth/google/yt-channel`, { channelId, pageId }); } } /** Parse local du `watch-history.json` de Google Takeout (confidentiel : rien n'est uploadé tel quel). */ export function parseTakeoutHistory(json: unknown): Array<{ videoId: string; title: string; watchedAt: string }> { const arr = Array.isArray(json) ? json : []; const byId = new Map(); for (const e of arr) { const url = String((e as any)?.titleUrl || ''); const m = /(?:[?&]v=|youtu\.be\/|\/(?:shorts|live|embed)\/)([A-Za-z0-9_-]{11})/.exec(url); if (!m) continue; const videoId = m[1]; const rawTitle = String((e as any)?.title || '').replace(/^Watched\s+/i, '').trim(); let watchedAt = new Date().toISOString(); try { const d = new Date(String((e as any)?.time || '')); if (!Number.isNaN(d.getTime())) watchedAt = d.toISOString(); } catch {} const prev = byId.get(videoId); if (!prev || watchedAt > prev.watchedAt) { byId.set(videoId, { videoId, title: (rawTitle || videoId).slice(0, 300), watchedAt }); } } return [...byId.values()].sort((a, b) => (a.watchedAt < b.watchedAt ? 1 : -1)); }