feat(sync): sélecteur de chaîne YouTube (multi-chaînes/marque) via PageId
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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). */
|
||||
|
||||
Reference in New Issue
Block a user