feat(sync): phase 1 - import Takeout + Watch Later YouTube bidirectionnel
- Takeout: POST /user/history/takeout (lots <=1000, validation videoId, upsert sans doublons), parsing local watch-history.json cote client (vie privee, dedup derniere date), progression par lots de 500 - Watch Later: GET/POST /oauth/google/watchlater (lecture readonly, push force-ssl), import vers playlist NewTube 'Watch Later (YouTube)', export playlist NewTube -> WL YouTube, writeGranted + invite reconnect - Scopes Google elargis (youtube.force-ssl), aide /library/import a jour (limite historique API documentee)
This commit is contained in:
@@ -59,4 +59,42 @@ export class OAuthImportService {
|
||||
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 }> {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
/** 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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user