diff --git a/src/components/library/subscriptions/subscriptions.component.html b/src/components/library/subscriptions/subscriptions.component.html index bcc0966..c7a3c87 100644 --- a/src/components/library/subscriptions/subscriptions.component.html +++ b/src/components/library/subscriptions/subscriptions.component.html @@ -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 diff --git a/src/components/library/subscriptions/subscriptions.component.ts b/src/components/library/subscriptions/subscriptions.component.ts index 2540e58..6377ff8 100644 --- a/src/components/library/subscriptions/subscriptions.component.ts +++ b/src/components/library/subscriptions/subscriptions.component.ts @@ -102,7 +102,22 @@ export class SubscriptionsComponent { private deepCancel = false; deepAnalyzing = signal(false); deepProgress = signal<{ done: number; total: number } | null>(null); - private deepTexts = signal>({}); + private static readonly DEEP_TEXTS_KEY = 'newtube.deepTexts.v1'; + private deepTexts = signal>((() => { + 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 + )[v] || v; + }; + filtered = filtered.filter((item) => alias(item.channel?.provider) === filter); } if (groupId !== 'all') { const members = this.members(); diff --git a/src/services/subscriptions.service.ts b/src/services/subscriptions.service.ts index bb13cde..8bd6ca0 100644 --- a/src/services/subscriptions.service.ts +++ b/src/services/subscriptions.service.ts @@ -41,7 +41,7 @@ export const GROUP_ICONS = [ // Général '📁', '⭐', '❤️', '🔥', '💡', '📌', '🎯', '🏆', '💎', '🎁', '⚡', '🌟', '💯', '✅', '🆕', // Musique - '🎵', '🎶', '🎸', '🎹', '🎧', '🎤', '🥁', '🎺', '🎻', '📻', + '🎵', '🎶', '🎸', '🎹', '🎧', '🎤', '🎙️', '🥁', '🎺', '🎻', '📻', // Gaming '🎮', '🕹️', '🎲', '🃏', '♟️', '👾', '🤖', // Sport @@ -90,7 +90,8 @@ export const GROUP_ICON_KEYWORDS: Record = { '🎁': '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(); 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(); - 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);