Files
NewTube/src/services/oauth-import.service.ts
T

128 lines
5.2 KiB
TypeScript

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<any>('/api/oauth/status');
}
connections(): Observable<{ connections: OAuthConnection[]; status: any }> {
return this.http.get<any>('/api/oauth/connections');
}
async connect(provider: 'google' | 'twitch'): Promise<void> {
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<void> {
return this.http.delete<void>(`/api/oauth/${provider}`);
}
preview(provider: string): Observable<OAuthPreview> {
return this.http.get<OAuthPreview>(`/api/oauth/${provider}/preview`);
}
importAll(provider: string, types: string[]): Observable<OAuthImportResult> {
return this.http.post<OAuthImportResult>(`/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<any>(`/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<any>(`/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<any>(`/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<any>(`/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<any>(`/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<any>(`/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<string, { videoId: string; title: string; watchedAt: string }>();
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));
}