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"
(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"
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
</button>
@@ -102,7 +102,22 @@ export class SubscriptionsComponent {
private deepCancel = false;
deepAnalyzing = signal<boolean>(false);
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() {
const extra = this.deepTexts();
@@ -146,6 +161,7 @@ export class SubscriptionsComponent {
const workers = Array.from({ length: 4 }, () => this.deepWorker(queue, texts, counter, targets.length));
await Promise.all(workers);
this.deepTexts.set(texts);
this.persistDeepTexts();
this.deepProgress.set(null);
this.deepAnalyzing.set(false);
}
@@ -470,7 +486,22 @@ export class SubscriptionsComponent {
const q = this.searchQuery().trim().toLowerCase();
let filtered = list;
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') {
const members = this.members();
+30 -16
View File
@@ -41,7 +41,7 @@ export const GROUP_ICONS = [
// Général
'📁', '⭐', '❤️', '🔥', '💡', '📌', '🎯', '🏆', '💎', '🎁', '⚡', '🌟', '💯', '✅', '🆕',
// Musique
'🎵', '🎶', '🎸', '🎹', '🎧', '🎤', '🥁', '🎺', '🎻', '📻',
'🎵', '🎶', '🎸', '🎹', '🎧', '🎤', '🎙️', '🥁', '🎺', '🎻', '📻',
// Gaming
'🎮', '🕹️', '🎲', '🃏', '♟️', '👾', '🤖',
// 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',
'✅': 'valide check ok', '🆕': 'nouveau new',
'🎵': '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',
'🎮': 'jeu video game gaming manette controller esport', '🕹️': 'arcade retro joystick', '🎲': 'de dice jeu board',
'🃏': '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: '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: '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: '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'] },
@@ -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: '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: '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. */
@@ -240,6 +245,22 @@ const TOPIC_STOPWORDS = new Set([
'their', 'what', 'when', 'where', 'which', 'while', 'about', 'into', 'over',
'after', 'before', 'tube', 'youtube', 'channel', 'chaine', 'chanel',
'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. */
@@ -272,18 +293,11 @@ export function detectEmergentTopics(
const byWord = new Map<string, typeof items>();
for (const item of items) {
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));
const words = new Set<string>();
for (const t of tokens) {
words.add(t);
// 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);
}
}
}
// Tokens entiers uniquement (plus de fragments 'program'/'develop' : source de bruit).
// Les collés type 'hockeyqc' sont couverts par les règles (préfixe) et l'analyse approfondie.
const words = new Set(
hay.split(/[^a-z0-9]+/).filter((w) => w.length >= 3 && !/^\d+$/.test(w) && !TOPIC_STOPWORDS.has(w)),
);
for (const w of words) {
const list = byWord.get(w) || [];
list.push(item);