feat(groups): anti-bruit prénoms, 4 thèmes, persistance analyse, fix filtre provider
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user