feat(groups): sujets émergents + analyse approfondie des chaînes
This commit is contained in:
@@ -2,11 +2,15 @@ import { ChangeDetectionStrategy, Component, computed, HostListener, inject, sig
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { DatePipe, DecimalPipe } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import {
|
||||
GROUP_COLORS,
|
||||
GROUP_ICONS,
|
||||
GROUP_ICON_KEYWORDS,
|
||||
detectEmergentTopics,
|
||||
normalizeChannelText,
|
||||
suggestGroupsForChannels,
|
||||
SubscriptionsService,
|
||||
type GroupSuggestion,
|
||||
@@ -93,17 +97,91 @@ export class SubscriptionsComponent {
|
||||
showSuggestions = signal<boolean>(false);
|
||||
suggestBusy = signal<boolean>(false);
|
||||
|
||||
readonly suggestions = computed<GroupSuggestion[]>(() =>
|
||||
suggestGroupsForChannels(
|
||||
this.items().map((item) => ({
|
||||
subscriptionId: item.subscriptionId,
|
||||
title: item.channel?.title || '',
|
||||
handle: item.channel?.handle || null,
|
||||
groupIds: this.itemGroupIds(item),
|
||||
})),
|
||||
this.groups().map((g) => ({ id: g.id, name: g.name })),
|
||||
)
|
||||
);
|
||||
// --- Analyse approfondie : titres des dernières vidéos par chaîne ---
|
||||
private http = inject(HttpClient);
|
||||
private deepCancel = false;
|
||||
deepAnalyzing = signal<boolean>(false);
|
||||
deepProgress = signal<{ done: number; total: number } | null>(null);
|
||||
private deepTexts = signal<Record<number, string>>({});
|
||||
|
||||
private suggestionBaseItems() {
|
||||
const extra = this.deepTexts();
|
||||
return this.items().map((item) => ({
|
||||
subscriptionId: item.subscriptionId,
|
||||
title: item.channel?.title || '',
|
||||
handle: item.channel?.handle || null,
|
||||
groupIds: this.itemGroupIds(item),
|
||||
extra: extra[item.subscriptionId] || '',
|
||||
}));
|
||||
}
|
||||
|
||||
readonly suggestions = computed<GroupSuggestion[]>(() => {
|
||||
const items = this.suggestionBaseItems();
|
||||
const groups = this.groups().map((g) => ({ id: g.id, name: g.name }));
|
||||
const rules = suggestGroupsForChannels(items, groups);
|
||||
const taken = new Set(rules.map((r) => normalizeChannelText(r.name).trim()));
|
||||
const topics = detectEmergentTopics(items, {
|
||||
minSize: 2,
|
||||
maxGroups: 10,
|
||||
excludeNames: [...taken, ...groups.map((g) => g.name)],
|
||||
});
|
||||
return [...rules, ...topics].sort((a, b) => b.channelIds.length - a.channelIds.length);
|
||||
});
|
||||
|
||||
/** Analyse approfondie : récupère les titres récents des chaînes non groupées. */
|
||||
async runDeepAnalysis(): Promise<void> {
|
||||
if (this.deepAnalyzing()) return;
|
||||
const targets = this.items().filter((item) => {
|
||||
if (this.deepTexts()[item.subscriptionId] !== undefined) return false;
|
||||
if (!item.channel?.provider || !item.channel?.externalId) return false;
|
||||
return (this.itemGroupIds(item).length === 0);
|
||||
});
|
||||
if (!targets.length) return;
|
||||
this.deepCancel = false;
|
||||
this.deepAnalyzing.set(true);
|
||||
this.deepProgress.set({ done: 0, total: targets.length });
|
||||
const texts: Record<number, string> = { ...this.deepTexts() };
|
||||
const counter = { done: 0 };
|
||||
const queue = [...targets];
|
||||
const workers = Array.from({ length: 4 }, () => this.deepWorker(queue, texts, counter, targets.length));
|
||||
await Promise.all(workers);
|
||||
this.deepTexts.set(texts);
|
||||
this.deepProgress.set(null);
|
||||
this.deepAnalyzing.set(false);
|
||||
}
|
||||
|
||||
stopDeepAnalysis(): void {
|
||||
this.deepCancel = true;
|
||||
}
|
||||
|
||||
private async deepWorker(
|
||||
queue: SubscriptionItem[],
|
||||
texts: Record<number, string>,
|
||||
counter: { done: number },
|
||||
total: number,
|
||||
): Promise<void> {
|
||||
while (queue.length && !this.deepCancel) {
|
||||
const item = queue.shift();
|
||||
if (!item?.channel) continue;
|
||||
try {
|
||||
const res: any = await firstValueFrom(
|
||||
this.http.get(
|
||||
`/api/channels/${encodeURIComponent(item.channel.provider)}/${encodeURIComponent(item.channel.externalId)}/content`,
|
||||
{ params: { type: 'videos', limit: '5' } },
|
||||
),
|
||||
);
|
||||
const titles = ((res?.items || []) as any[])
|
||||
.map((v: any) => String(v?.title || ''))
|
||||
.filter(Boolean)
|
||||
.slice(0, 5);
|
||||
texts[item.subscriptionId] = titles.join(' ');
|
||||
} catch {
|
||||
texts[item.subscriptionId] = '';
|
||||
}
|
||||
counter.done++;
|
||||
this.deepProgress.set({ done: Math.min(counter.done, total), total });
|
||||
}
|
||||
}
|
||||
|
||||
/** Crée le groupe suggéré (ou complète l'existant) et y assigne les chaînes. */
|
||||
applySuggestion(s: GroupSuggestion): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user