feat(groups): anti-bruit prénoms, 4 thèmes, persistance analyse, fix filtre provider

This commit is contained in:
2026-09-27 14:34:47 -04:00
parent a2b4139887
commit ebfb39fec2
3 changed files with 64 additions and 19 deletions
@@ -52,7 +52,7 @@
type="button" type="button"
(click)="runDeepAnalysis()" (click)="runDeepAnalysis()"
class="rounded-md border border-violet-500/50 bg-transparent px-3 py-1.5 text-sm text-violet-200 transition hover:bg-violet-600/20" class="rounded-md border border-violet-500/50 bg-transparent px-3 py-1.5 text-sm text-violet-200 transition hover:bg-violet-600/20"
title="Lit les titres récents des chaînes sans groupe pour affiner les suggestions" title="Lit les titres récents des chaînes sans groupe pour affiner les suggestions (résultat conservé entre les sessions)"
> >
🔍 Analyse approfondie 🔍 Analyse approfondie
</button> </button>
@@ -102,7 +102,22 @@ export class SubscriptionsComponent {
private deepCancel = false; private deepCancel = false;
deepAnalyzing = signal<boolean>(false); deepAnalyzing = signal<boolean>(false);
deepProgress = signal<{ done: number; total: number } | null>(null); deepProgress = signal<{ done: number; total: number } | null>(null);
private deepTexts = signal<Record<number, string>>({}); private static readonly DEEP_TEXTS_KEY = 'newtube.deepTexts.v1';
private deepTexts = signal<Record<number, string>>((() => {
try {
const raw = localStorage.getItem(SubscriptionsComponent.DEEP_TEXTS_KEY);
const parsed = raw ? JSON.parse(raw) : {};
return parsed && typeof parsed === 'object' ? parsed : {};
} catch {
return {};
}
})());
private persistDeepTexts(): void {
try {
localStorage.setItem(SubscriptionsComponent.DEEP_TEXTS_KEY, JSON.stringify(this.deepTexts()));
} catch {}
}
private suggestionBaseItems() { private suggestionBaseItems() {
const extra = this.deepTexts(); const extra = this.deepTexts();
@@ -146,6 +161,7 @@ export class SubscriptionsComponent {
const workers = Array.from({ length: 4 }, () => this.deepWorker(queue, texts, counter, targets.length)); const workers = Array.from({ length: 4 }, () => this.deepWorker(queue, texts, counter, targets.length));
await Promise.all(workers); await Promise.all(workers);
this.deepTexts.set(texts); this.deepTexts.set(texts);
this.persistDeepTexts();
this.deepProgress.set(null); this.deepProgress.set(null);
this.deepAnalyzing.set(false); this.deepAnalyzing.set(false);
} }
@@ -470,7 +486,22 @@ export class SubscriptionsComponent {
const q = this.searchQuery().trim().toLowerCase(); const q = this.searchQuery().trim().toLowerCase();
let filtered = list; let filtered = list;
if (filter !== 'all') { if (filter !== 'all') {
filtered = filtered.filter((item) => item.channel?.provider === filter); // Normalise les codes courts ('yt') et longs ('youtube') : les imports
// mixtes stockent les deux formes.
const alias = (p: string | null | undefined): string => {
const v = String(p || '').toLowerCase();
return (
{
youtube: 'yt', yt: 'yt',
dailymotion: 'dm', dm: 'dm',
twitch: 'tw', tw: 'tw',
peertube: 'pt', pt: 'pt',
odysee: 'od', od: 'od',
rumble: 'ru', ru: 'ru',
} as Record<string, string>
)[v] || v;
};
filtered = filtered.filter((item) => alias(item.channel?.provider) === filter);
} }
if (groupId !== 'all') { if (groupId !== 'all') {
const members = this.members(); const members = this.members();
+30 -16
View File
@@ -41,7 +41,7 @@ export const GROUP_ICONS = [
// Général // Général
'📁', '⭐', '❤️', '🔥', '💡', '📌', '🎯', '🏆', '💎', '🎁', '⚡', '🌟', '💯', '✅', '🆕', '📁', '⭐', '❤️', '🔥', '💡', '📌', '🎯', '🏆', '💎', '🎁', '⚡', '🌟', '💯', '✅', '🆕',
// Musique // Musique
'🎵', '🎶', '🎸', '🎹', '🎧', '🎤', '🥁', '🎺', '🎻', '📻', '🎵', '🎶', '🎸', '🎹', '🎧', '🎤', '🎙️', '🥁', '🎺', '🎻', '📻',
// Gaming // Gaming
'🎮', '🕹️', '🎲', '🃏', '♟️', '👾', '🤖', '🎮', '🕹️', '🎲', '🃏', '♟️', '👾', '🤖',
// Sport // Sport
@@ -90,7 +90,8 @@ export const GROUP_ICON_KEYWORDS: Record<string, string> = {
'🎁': 'cadeau gift', '⚡': 'eclair lightning energie actu rapide', '🌟': 'etoile star brillant', '💯': 'cent parfait perfect', '🎁': 'cadeau gift', '⚡': 'eclair lightning energie actu rapide', '🌟': 'etoile star brillant', '💯': 'cent parfait perfect',
'✅': 'valide check ok', '🆕': 'nouveau new', '✅': 'valide check ok', '🆕': 'nouveau new',
'🎵': 'musique music note chanson song', '🎶': 'musique music notes concert', '🎸': 'guitare guitar rock metal', '🎵': 'musique music note chanson song', '🎶': 'musique music notes concert', '🎸': 'guitare guitar rock metal',
'🎹': 'piano clavier keyboard', '🎧': 'casque headphone audio ecoute', '🎤': 'micro microphone chant sing karaoke vocal', '🎹': 'piano clavier keyboard', '🎧': 'casque headphone audio ecoute', '🎤': 'micro microphone chant sing karaoke vocal',
'🎙️': 'micro studio podcast talk show interview balado radio voix',
'🥁': 'batterie drum', '🎺': 'trompette trumpet', '🎻': 'violon violin classique', '📻': 'radio', '🥁': 'batterie drum', '🎺': 'trompette trumpet', '🎻': 'violon violin classique', '📻': 'radio',
'🎮': 'jeu video game gaming manette controller esport', '🕹️': 'arcade retro joystick', '🎲': 'de dice jeu board', '🎮': 'jeu video game gaming manette controller esport', '🕹️': 'arcade retro joystick', '🎲': 'de dice jeu board',
'🃏': 'carte card poker', '♟️': 'echec chess strategie', '👾': 'alien pixel retrogaming invader', '🃏': 'carte card poker', '♟️': 'echec chess strategie', '👾': 'alien pixel retrogaming invader',
@@ -199,7 +200,7 @@ const GROUP_SUGGESTION_RULES: SuggestionRule[] = [
{ name: 'Gaming', icon: '🎮', color: 11, keywords: ['game', 'gaming', 'gamer', 'jeux video', 'jeux', 'esport', 'playthrough', 'speedrun', 'xbox', 'playstation', 'nintendo', 'steam', 'minecraft', 'fortnite', 'zelda', 'mario', 'retro', 'arcade', 'walkthrough', 'videogame', 'lets play'] }, { name: 'Gaming', icon: '🎮', color: 11, keywords: ['game', 'gaming', 'gamer', 'jeux video', 'jeux', 'esport', 'playthrough', 'speedrun', 'xbox', 'playstation', 'nintendo', 'steam', 'minecraft', 'fortnite', 'zelda', 'mario', 'retro', 'arcade', 'walkthrough', 'videogame', 'lets play'] },
{ name: 'Tech & Code', icon: '💻', color: 10, keywords: ['tech', 'dev', 'code', 'coding', 'programmation', 'programming', 'python', 'javascript', 'linux', 'devops', 'docker', 'kubernetes', 'intelligence artificielle', 'machine learning', 'data science', 'hardware', 'software', 'open source', 'github', 'cyber', 'gadget', 'smartphone', 'apple', 'android', 'windows', 'raspberry', 'arduino', 'robotique', 'informatique', 'developer', 'developpeur', 'web dev'] }, { name: 'Tech & Code', icon: '💻', color: 10, keywords: ['tech', 'dev', 'code', 'coding', 'programmation', 'programming', 'python', 'javascript', 'linux', 'devops', 'docker', 'kubernetes', 'intelligence artificielle', 'machine learning', 'data science', 'hardware', 'software', 'open source', 'github', 'cyber', 'gadget', 'smartphone', 'apple', 'android', 'windows', 'raspberry', 'arduino', 'robotique', 'informatique', 'developer', 'developpeur', 'web dev'] },
{ name: 'Science & Espace', icon: '🔭', color: 9, keywords: ['science', 'espace', 'space', 'nasa', 'astronomie', 'astrophysique', 'univers', 'cosmos', 'quantique', 'biologie', 'chimie', 'planete', 'physique'] }, { name: 'Science & Espace', icon: '🔭', color: 9, keywords: ['science', 'espace', 'space', 'nasa', 'astronomie', 'astrophysique', 'univers', 'cosmos', 'quantique', 'biologie', 'chimie', 'planete', 'physique'] },
{ name: 'Sport', icon: '⚽', color: 5, keywords: ['sport', 'foot', 'football', 'soccer', 'basket', 'tennis', 'rugby', 'hockey', 'golf', 'cyclisme', 'formule 1', 'league', 'club', 'olympique', 'nhl', 'nba'] }, { name: 'Sport', icon: '⚽', color: 5, keywords: ['sport', 'foot', 'football', 'soccer', 'basket', 'tennis', 'rugby', 'hockey', 'golf', 'cyclisme', 'formule 1', 'league', 'club', 'olympique', 'nhl', 'nba', 'lnh'] },
{ name: 'Cuisine', icon: '🍳', color: 3, keywords: ['cuisine', 'cooking', 'recette', 'recipe', 'food', 'chef', 'resto', 'patisserie', 'boulangerie', 'bbq', 'bouffe', 'miam'] }, { name: 'Cuisine', icon: '🍳', color: 3, keywords: ['cuisine', 'cooking', 'recette', 'recipe', 'food', 'chef', 'resto', 'patisserie', 'boulangerie', 'bbq', 'bouffe', 'miam'] },
{ name: 'Voyage', icon: '✈️', color: 8, keywords: ['travel', 'voyage', 'tourisme', 'vacances', 'backpack', 'avion', 'roadtrip', 'vanlife', 'expat', 'aventure', 'adventure', 'hotel'] }, { name: 'Voyage', icon: '✈️', color: 8, keywords: ['travel', 'voyage', 'tourisme', 'vacances', 'backpack', 'avion', 'roadtrip', 'vanlife', 'expat', 'aventure', 'adventure', 'hotel'] },
{ name: 'Cinéma & Séries', icon: '🎬', color: 0, keywords: ['cinema', 'film', 'movie', 'serie', 'series', 'episode', 'trailer', 'bande annonce', 'hollywood', 'anime', 'manga', 'cartoon', 'court metrage'] }, { name: 'Cinéma & Séries', icon: '🎬', color: 0, keywords: ['cinema', 'film', 'movie', 'serie', 'series', 'episode', 'trailer', 'bande annonce', 'hollywood', 'anime', 'manga', 'cartoon', 'court metrage'] },
@@ -212,7 +213,11 @@ const GROUP_SUGGESTION_RULES: SuggestionRule[] = [
{ name: 'Auto & Moto', icon: '🚗', color: 1, keywords: ['auto', 'voiture', 'car', 'moto', 'automobile', 'garage', 'mecanique', 'tesla', 'tuning'] }, { name: 'Auto & Moto', icon: '🚗', color: 1, keywords: ['auto', 'voiture', 'car', 'moto', 'automobile', 'garage', 'mecanique', 'tesla', 'tuning'] },
{ name: 'Nature & Animaux', icon: '🌿', color: 5, keywords: ['nature', 'animaux', 'animal', 'wildlife', 'peche', 'chasse', 'jardin', 'jardinage', 'ferme', 'chat', 'chien', 'oiseau'] }, { name: 'Nature & Animaux', icon: '🌿', color: 5, keywords: ['nature', 'animaux', 'animal', 'wildlife', 'peche', 'chasse', 'jardin', 'jardinage', 'ferme', 'chat', 'chien', 'oiseau'] },
{ name: 'Art & Design', icon: '🎨', color: 13, keywords: ['art', 'design', 'photo', 'photographie', 'dessin', 'peinture', 'architecture', 'deco', 'graphisme', 'illustration'] }, { name: 'Art & Design', icon: '🎨', color: 13, keywords: ['art', 'design', 'photo', 'photographie', 'dessin', 'peinture', 'architecture', 'deco', 'graphisme', 'illustration'] },
{ name: 'Vlogs & Lifestyle', icon: '📹', color: 15, keywords: ['vlog', 'lifestyle', 'quotidien', 'famille', 'daily', 'routine', 'blog', 'vlogger'] }, { name: 'Vlogs & Lifestyle', icon: '📹', color: 15, keywords: ['vlog', 'lifestyle', 'quotidien', 'daily', 'routine', 'blog', 'vlogger'] },
{ name: 'DIY & Bricolage', icon: '🛠️', color: 2, keywords: ['diy', 'bricolage', 'maker', 'makers', 'workshop', 'atelier', 'renovation', 'menuiserie', 'plomberie', 'electricite', 'soudure', 'brico'] },
{ name: 'Podcasts & Talk-shows', icon: '🎙️', color: 13, keywords: ['podcast', 'podcasts', 'talk show', 'interview', 'balado', 'balados'] },
{ name: 'Tests & Avis', icon: '📦', color: 16, keywords: ['review', 'reviews', 'test', 'unboxing', 'deballage', 'avis', 'comparatif', 'comparaison', 'hands on', 'prise en main', 'verdict'] },
{ name: 'Famille & Enfants', icon: '👶', color: 15, keywords: ['famille', 'family', 'enfant', 'enfants', 'kid', 'kids', 'bebe', 'parentalite', 'maman', 'papa'] },
]; ];
/** Normalisation insensible aux accents/casse pour le match des règles. */ /** Normalisation insensible aux accents/casse pour le match des règles. */
@@ -240,6 +245,22 @@ const TOPIC_STOPWORDS = new Set([
'their', 'what', 'when', 'where', 'which', 'while', 'about', 'into', 'over', 'their', 'what', 'when', 'where', 'which', 'while', 'about', 'into', 'over',
'after', 'before', 'tube', 'youtube', 'channel', 'chaine', 'chanel', 'after', 'before', 'tube', 'youtube', 'channel', 'chaine', 'chanel',
'official', 'officiel', 'network', 'production', 'streaming', 'official', 'officiel', 'network', 'production', 'streaming',
'day', 'live', 'net', 'one', 'world', 'com', 'www', 'org', 'hq', 'hd',
// Prénoms courants FR/EN : partagés par hasard, jamais des thèmes.
'vincent', 'dave', 'david', 'eric', 'thomas', 'matt', 'alex', 'christ', 'christophe',
'christian', 'marc', 'jean', 'pierre', 'michel', 'alain', 'julien', 'nicolas', 'max',
'sam', 'tom', 'mike', 'john', 'paul', 'luc', 'hugo', 'louis', 'emma', 'marie', 'sophie',
'julie', 'sarah', 'kevin', 'steve', 'bob', 'jim', 'joe', 'dan', 'ben', 'phil', 'fred',
'greg', 'chris', 'rob', 'robert', 'antoine', 'francois', 'patrick', 'stephane', 'olivier',
'sebastien', 'mathieu', 'jonathan', 'anthony', 'jerome', 'laurent', 'pascal', 'dominique',
'claude', 'daniel', 'martin', 'simon', 'felix', 'gabriel', 'emile', 'charles', 'henri',
'jacques', 'andre', 'philippe', 'thierry', 'bruno', 'lea', 'chloe', 'manon', 'camille',
'laura', 'melanie', 'isabelle', 'nathalie', 'caroline', 'virginie', 'stephanie', 'emilie',
'jeff', 'josh', 'nick', 'tony', 'ryan', 'jake', 'luke', 'owen', 'aaron', 'adam', 'brian',
'jason', 'scott', 'mark', 'peter', 'doug', 'frank', 'george', 'harry', 'jack', 'james',
'jerry', 'larry', 'terry', 'tim', 'todd', 'troy', 'wayne', 'will', 'zach', 'amy', 'anna',
'beth', 'emily', 'jess', 'jessica', 'karen', 'kate', 'lisa', 'mary', 'megan', 'rachel',
'sara', 'steven',
]); ]);
/** Plafond : au-delà, le sujet est trop générique pour être utile. */ /** Plafond : au-delà, le sujet est trop générique pour être utile. */
@@ -272,18 +293,11 @@ export function detectEmergentTopics(
const byWord = new Map<string, typeof items>(); const byWord = new Map<string, typeof items>();
for (const item of items) { for (const item of items) {
const hay = normalizeChannelText(`${item.title || ''} ${item.handle || ''} ${item.extra || ''}`); const hay = normalizeChannelText(`${item.title || ''} ${item.handle || ''} ${item.extra || ''}`);
const tokens = hay.split(/[^a-z0-9]+/).filter((w) => w.length >= 3 && !/^\d+$/.test(w) && !TOPIC_STOPWORDS.has(w)); // Tokens entiers uniquement (plus de fragments 'program'/'develop' : source de bruit).
const words = new Set<string>(); // Les collés type 'hockeyqc' sont couverts par les règles (préfixe) et l'analyse approfondie.
for (const t of tokens) { const words = new Set(
words.add(t); hay.split(/[^a-z0-9]+/).filter((w) => w.length >= 3 && !/^\d+$/.test(w) && !TOPIC_STOPWORDS.has(w)),
// Sous-chaînes longues : 'hockey' dans 'hockeyqc', 'tech' dans 'technology'. );
if (t.length >= 8) {
for (let len = 5; len <= t.length - 2; len++) {
const sub = t.slice(0, len);
if (!TOPIC_STOPWORDS.has(sub)) words.add(sub);
}
}
}
for (const w of words) { for (const w of words) {
const list = byWord.get(w) || []; const list = byWord.get(w) || [];
list.push(item); list.push(item);