feat(sync): sélecteur de chaîne YouTube (multi-chaînes/marque) via PageId

This commit is contained in:
2026-09-27 08:05:01 -04:00
parent d3836989ea
commit f038f19b71
7 changed files with 216 additions and 4 deletions
@@ -89,6 +89,39 @@
</div>
</div>
<!-- ==================== CHAÎNE YOUTUBE ==================== -->
<div *ngIf="!loading() && isConnected('google')" 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>Chaîne YouTube utilisée</h3>
<p class="mt-1 text-sm text-slate-400">
Comptes multi-chaînes / marque : la chaîne par défaut est parfois vide (pas d’historique ni de Regarder plus tard).
Choisissez la chaîne où se trouve votre activité réelle.
</p>
<div class="mt-3 flex flex-wrap gap-2">
<button type="button" (click)="loadYtChannels()" [disabled]="ytChannelsLoading()"
class="rounded-md border border-slate-700 bg-slate-800 px-3 py-1.5 text-sm text-slate-200 transition hover:border-slate-500">
{{ ytChannelsLoading() ? 'Chargement…' : (ytChannels().length ? 'Recharger' : 'Lister mes chaînes') }}
</button>
</div>
<div *ngIf="ytChannelsError()" class="mt-2 rounded-lg border border-red-700 bg-red-900/30 p-2 text-xs text-red-200">{{ ytChannelsError() }}</div>
<div *ngIf="ytChannels().length" class="mt-3 space-y-2">
<label *ngFor="let c of ytChannels()" class="flex cursor-pointer items-center gap-3 rounded-lg border border-slate-700 bg-slate-800/60 px-3 py-2 text-sm transition hover:border-slate-500">
<input type="radio" name="yt-channel" [value]="c.channelId || ''" [ngModel]="ytSelectedChannelId()" (ngModelChange)="ytSelectedChannelId.set($event)" class="accent-red-600" />
<span class="flex-1">
<span class="block font-medium text-slate-100">{{ c.title }}</span>
<span *ngIf="c.handle" class="block text-xs text-slate-400">{{ c.handle }}</span>
</span>
<span *ngIf="c.selected" class="rounded-full bg-slate-700 px-2 py-0.5 text-xs text-slate-300">actuelle</span>
</label>
<div class="flex flex-wrap items-center gap-2">
<button type="button" (click)="saveYtChannel()" [disabled]="ytSavingChannel()"
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">
{{ ytSavingChannel() ? 'Enregistrement…' : 'Utiliser cette chaîne' }}
</button>
<span *ngIf="ytChannelSaved()" class="text-xs text-green-200">{{ ytChannelSaved() }}</span>
</div>
</div>
</div>
<!-- ==================== HISTORIQUE TAKEOUT ==================== -->
<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>
@@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { firstValueFrom } from 'rxjs';
import { OAuthImportService, parseTakeoutHistory, type OAuthConnection, type OAuthPreview, type OAuthImportResult } from '../../../services/oauth-import.service';
import { OAuthImportService, parseTakeoutHistory, type OAuthConnection, type OAuthPreview, type OAuthImportResult, type YtChannel } from '../../../services/oauth-import.service';
import { PlaylistsService, type Playlist } from '../../../services/playlists.service';
interface ProviderCard {
@@ -241,6 +241,53 @@ export class ImportComponent implements OnInit {
userPlaylists = signal<Playlist[]>([]);
wlExportPlaylistId = signal<string>('');
// -------------------- Chaîne YouTube (multi-chaînes / marque) --------------------
ytChannels = signal<YtChannel[]>([]);
ytChannelsLoading = signal(false);
ytChannelsError = signal<string | null>(null);
ytSelectedChannelId = signal<string>('');
ytSavingChannel = signal(false);
ytChannelSaved = signal<string | null>(null);
loadYtChannels(): void {
if (!this.isConnected('google') || this.ytChannelsLoading()) return;
this.ytChannelsLoading.set(true);
this.ytChannelsError.set(null);
this.api.ytChannels().subscribe({
next: (res) => {
this.ytChannels.set(res?.channels || []);
const sel = res?.selected?.channelId || this.connectionOf('google')?.ytChannelId || '';
this.ytSelectedChannelId.set(sel);
this.ytChannelsLoading.set(false);
},
error: (e) => {
const code = e?.error?.error || e?.message || 'yt_channels_failed';
const detail = e?.error?.detail;
this.ytChannelsError.set(detail ? `${code} — ${detail}` : code);
this.ytChannelsLoading.set(false);
},
});
}
saveYtChannel(): void {
const id = this.ytSelectedChannelId();
if (this.ytSavingChannel()) return;
this.ytSavingChannel.set(true);
this.ytChannelSaved.set(null);
this.api.saveYtChannel(id || null, id || null).subscribe({
next: () => {
this.ytChannelSaved.set('Chaîne mémorisée : relancez la sync auto / Regarder plus tard.');
this.ytSavingChannel.set(false);
this.refresh();
},
error: (e) => {
this.ytChannelsError.set(e?.error?.error || e?.message || 'yt_channel_save_failed');
this.ytSavingChannel.set(false);
},
});
}
loadWatchLater(): void {
this.wlLoading.set(true);
this.wlError.set(null);
+19
View File
@@ -11,6 +11,15 @@ export interface OAuthConnection {
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 {
@@ -82,6 +91,16 @@ export class OAuthImportService {
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). */