fix(sync): WL diagnostic, import-all paginé + totaux, sync auto historique InnerTube
- Watch Later: youtube_no_channel / watchlater_not_found avec hint actionnable + log cles relatedPlaylists (sans PII) - Preview Google: totaux compte (subscriptionTotal/likeTotal), UI 'X / N, l'import prend tout'; import-all paginé (subs<=1000, likes<=500) au lieu de 50/25 - Sync auto historique: GET /oauth/google/yt-history (browse FEhistory authentifie, continuations, <=500), bouton Sync auto dans carte Takeout, import via lots takeout existants
This commit is contained in:
@@ -69,7 +69,8 @@
|
||||
<div *ngIf="errors()[p.id]" class="mb-2 rounded-lg border border-red-700 bg-red-900/30 p-2 text-xs text-red-200">{{ errors()[p.id] }}</div>
|
||||
|
||||
<div *ngIf="previews()[p.id] as pv" class="mb-2 text-sm">
|
||||
<p class="text-slate-300">Abonnements : <strong>{{ pv.subscriptionCount }}</strong><span *ngIf="p.id === 'google'"> • Favoris : <strong>{{ pv.likeCount }}</strong></span></p>
|
||||
<p class="text-slate-300">Abonnements : <strong>{{ pv.subscriptionCount }}</strong><span *ngIf="pv.subscriptionTotal && pv.subscriptionTotal > pv.subscriptionCount"> / {{ pv.subscriptionTotal }}</span><span *ngIf="p.id === 'google'"> • Favoris : <strong>{{ pv.likeCount }}</strong><span *ngIf="pv.likeTotal && pv.likeTotal > pv.likeCount"> / {{ pv.likeTotal }}</span></span></p>
|
||||
<p *ngIf="p.id === 'google' && ((pv.subscriptionTotal && pv.subscriptionTotal > pv.subscriptionCount) || (pv.likeTotal && pv.likeTotal > pv.likeCount))" class="text-xs text-slate-500">Aperçu limité — le bouton Importer prend tout le compte.</p>
|
||||
<ul class="mt-2 max-h-48 space-y-1 overflow-auto text-xs text-slate-400">
|
||||
<li *ngFor="let s of pv.subscriptions.slice(0, 20)" class="flex items-center gap-2">
|
||||
<img *ngIf="s.avatarUrl" [src]="s.avatarUrl" alt="" class="h-5 w-5 rounded-full object-cover" />
|
||||
@@ -92,11 +93,15 @@
|
||||
<div *ngIf="!loading()" class="mt-4 rounded-xl border border-slate-800 bg-slate-900/60 p-4">
|
||||
<h3 class="text-lg font-semibold text-slate-100"><span class="mr-2">🕘</span>Historique YouTube (Takeout)</h3>
|
||||
<p class="mt-1 text-sm text-slate-400">
|
||||
Google n’expose pas l’historique par API : exportez-le via
|
||||
<code>takeout.google.com → YouTube → watch-history.json</code>, puis importez le fichier ici.
|
||||
Parsé <strong class="text-slate-200">localement dans votre navigateur</strong>, sans doublons.
|
||||
Google n’expose pas l’historique par API Data : <strong class="text-slate-200">sync auto</strong> via votre compte
|
||||
connecté, ou export manuel via <code>takeout.google.com → YouTube → watch-history.json</code>.
|
||||
Fichier parsé <strong class="text-slate-200">localement dans votre navigateur</strong>, sans doublons.
|
||||
</p>
|
||||
<div class="mt-3 flex flex-wrap items-center gap-2">
|
||||
<button *ngIf="isConnected('google')" type="button" (click)="syncAutoHistory()" [disabled]="ytAutoLoading() || takeoutImporting()"
|
||||
class="rounded-md bg-red-600 px-3 py-1.5 text-sm font-semibold text-white transition hover:bg-red-500 disabled:opacity-40">
|
||||
{{ ytAutoLoading() ? 'Sync…' : '⚡ Sync auto (compte Google)' }}
|
||||
</button>
|
||||
<label class="cursor-pointer rounded-md bg-slate-700 px-3 py-1.5 text-sm text-white transition hover:bg-slate-600">
|
||||
Choisir watch-history.json
|
||||
<input type="file" accept=".json,application/json" class="hidden" (change)="onTakeoutFile($event)" />
|
||||
@@ -106,6 +111,7 @@
|
||||
{{ takeoutImporting() ? 'Import…' : 'Importer dans l’historique' }}
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="ytAutoError()" class="mt-2 rounded-lg border border-red-700 bg-red-900/30 p-2 text-xs text-red-200">{{ ytAutoError() }}</div>
|
||||
<div *ngIf="takeoutParsing()" class="mt-2 text-sm text-slate-400">Analyse du fichier…</div>
|
||||
<div *ngIf="takeoutItems().length && takeoutRange() as rg" class="mt-2 text-sm text-slate-300">
|
||||
{{ takeoutItems().length }} vidéo(s) — du {{ rg.from }} au {{ rg.to }}<span *ngIf="takeoutFileName()"> ({{ takeoutFileName() }})</span>
|
||||
|
||||
@@ -129,6 +129,39 @@ export class ImportComponent implements OnInit {
|
||||
takeoutProgress = signal<{ done: number; total: number } | null>(null);
|
||||
takeoutResult = signal<{ imported: number; skipped: number } | null>(null);
|
||||
takeoutError = signal<string | null>(null);
|
||||
ytAutoLoading = signal(false);
|
||||
ytAutoError = signal<string | null>(null);
|
||||
|
||||
/** Sync auto : historique via InnerTube authentifié, puis import par lots. */
|
||||
async syncAutoHistory(): Promise<void> {
|
||||
if (!this.isConnected('google') || this.ytAutoLoading() || this.takeoutImporting()) return;
|
||||
this.ytAutoLoading.set(true);
|
||||
this.ytAutoError.set(null);
|
||||
this.takeoutError.set(null);
|
||||
this.takeoutResult.set(null);
|
||||
this.takeoutProgress.set(null);
|
||||
try {
|
||||
const res = await firstValueFrom(this.api.ytHistory(200));
|
||||
const items = (res?.items || []).map((v: any) => ({
|
||||
videoId: String(v.videoId),
|
||||
title: String(v.title || v.videoId).slice(0, 300),
|
||||
watchedAt: String(v.watchedAt || new Date().toISOString()),
|
||||
}));
|
||||
if (!items.length) {
|
||||
this.ytAutoError.set('Aucune entrée renvoyée par YouTube (historique vide ou suspendu côté Google ?).');
|
||||
return;
|
||||
}
|
||||
this.takeoutItems.set(items);
|
||||
this.takeoutFileName.set('sync auto YouTube');
|
||||
await this.importTakeout();
|
||||
} catch (e: any) {
|
||||
const code = e?.error?.error || e?.message || 'ythistory_fetch_failed';
|
||||
const hint = e?.error?.hint;
|
||||
this.ytAutoError.set(hint ? `${code} — ${hint}` : code);
|
||||
} finally {
|
||||
this.ytAutoLoading.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
takeoutRange(): { from: string; to: string } | null {
|
||||
const items = this.takeoutItems();
|
||||
@@ -213,7 +246,9 @@ export class ImportComponent implements OnInit {
|
||||
this.wlLoading.set(false);
|
||||
},
|
||||
error: (e) => {
|
||||
this.wlError.set(e?.error?.error || e?.message || 'watchlater_fetch_failed');
|
||||
const code = e?.error?.error || e?.message || 'watchlater_fetch_failed';
|
||||
const hint = e?.error?.hint;
|
||||
this.wlError.set(hint ? `${code} — ${hint}` : code);
|
||||
this.wlLoading.set(false);
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user