ENFRKOantony langlois
projets

YouTube Channel Folders

visiterextension chromeSvelteBrowser ExtensionSupabase

Extension de navigateur qui injecte un système de dossiers dans YouTube lui-même · Svelte dans un Shadow DOM chevauchant une SPA hostile · récupération depuis le RSS public, sans clé API, sans connexion · synchronisation multi-appareils optionnelle derrière un Sync Code de type bearer

Le problème à résoudre

En 2014, j'utilisais la page « abonnements » comme source principale de vidéos à regarder, mais aujourd'hui ce n'est qu'un gigantesque fouillis de toutes les chaînes auxquelles je me suis abonné.

Je suis un gros utilisateur de YouTube, et ma façon de l'utiliser a beaucoup changé au fil des années, à cause d'une pile massive d'abonnements et d'un algorithme qui se soucie moins de mes abonnements que des vidéos virales tournant autour de mes centres d'intérêt du moment.

Ce n'est pas entièrement négatif, et il est plutôt doué pour vous rendre très intéressé dès la seconde où vous ouvrez la page d'accueil. Mais le problème qui vient avec, c'est que je rate souvent des vidéos des chaînes auxquelles je suis abonné, surtout parce que je me retrouve dans un cycle de vidéos qui n'étaient pas dans leur niche, et cela alimente un effet boule de neige qui m'éloigne de ces chaînes.

Et je voulais faire revivre cet usage de la page d'abonnements YouTube, tout en me permettant de choisir une niche précise de vidéos à regarder sans être distrait par d'autres vidéos. Il me fallait donc un moyen de ranger les chaînes YouTube d'une même niche dans un flux commun où piocher. Par exemple si je voulais suivre l'actu tech, ou l'actu escalade, ou le contenu d'un jeu vidéo en particulier.

Mon idée

Pouvoir créer des dossiers, auxquels on ajoute des chaînes YouTube, et à l'intérieur de ce dossier un flux de vidéos exclusif à ces chaînes. Tout en pouvant filtrer les shorts, qui polluent vraiment le flux la plupart du temps.

Comme pour toutes mes extensions Chrome, je ne veux pas que l'utilisateur ait à se connecter dans l'extension. Cela ajoute un niveau de complexité et des limitations, mais pour moi il est important que cela paraisse sûr à l'utilisateur et que ce soit aussi indépendant de son compte, ce qui veut dire que peu importe le compte YouTube qu'il utilise, les dossiers restent les mêmes.

Architecture

Svelte
Frontend · injection barre latérale
Liste des dossiers
entrypoints/youtube.content
Injectée dans la barre latérale gauche de YouTube · pastilles de non-lus · lanceur flottant de repli
Svelte
Frontend · vue pleine page
Flux du dossier
youtube.content · overlay host
Masque la zone de contenu de YouTube · restauré par Retour / rechargement via le hash de l'URL
Svelte
Frontend · bouton de page
Bouton Ajouter-au-dossier
beside the Subscribe · bell cluster
Ré-injecté par un MutationObserver quand son ancre se re-rend · ouvre le Sélecteur de dossier
chrome.alarms
Core · service worker MV3
Worker d'arrière-plan
entrypoints/background.ts
Dort entre les alarmes · se réveille pour les messages et le tick de 30 min
@handle → UCid resolverpoll RSS poli · ETag · backoffcalcul des non-luspush / pull de sync
no auth
Externe · RSS public
YouTube RSS
/feeds/videos.xml?channel_id=…
≈15 entrées les plus récentes par chaîne · ETag / 304 respectés
browser
Stockage · local-first
chrome.storage.local
folders · resolver cache · unread · link
Source de vérité locale · lue par l'UI et le worker indifféremment
anon key
Stockage · backend de sync
Supabase Postgres
public.synced_folders
Lignes indexées par SHA-256(Sync Code) · tombstones · last-write-wins

Un clic dans la Liste des dossiers ajoute une entrée d'historique et ouvre le Flux du dossier par-dessus la zone de contenu de YouTube, l'id du dossier dans le hash de l'URL pour que Retour et rechargement le restaurent. Le flux demande au Worker d'arrière-plan via un message runtime de récupérer le videos.xml de chaque chaîne depuis le RSS de YouTube (les content scripts ne peuvent pas faire de fetch cross-origin) et fusionne les entrées du plus récent au plus ancien, plafonné aux alentours de 100. Les dossiers eux-mêmes vivent dans chrome.storage.local, lus et écrits directement par l'UI. Toutes les ~30 min un tick chrome.alarms réveille le worker pour re-poller les chaînes dédupliquées et allumer les pastilles de non-lus ; quand un Sync Code est lié, les changements sont poussés vers Supabase Postgres indexés par le SHA-256 du code.

Déploiement : Extension → Chrome Web Store + Edge Add-ons · MV3, construit avec WXT · Backend de sync → offre gratuite Supabase · ping keep-alive launchd deux fois par semaine

Résultats

  • 0 · clé API, quota ou connexion · récupération via RSS public
  • 30 min · cadence de poll des non-lus · le worker dort entre les alarmes
  • 256 bits · Sync Code · le serveur ne stocke que son SHA-256
  • 10 ADR · chaque revirement consigné · 2 décisions remplacées

Compétences démontrées : cycle de vie du service worker MV3 (alarmes, pas de timers) · polling HTTP conditionnel (ETag / 304, jitter, backoff) · fusion par union + last-write-wins + sync par tombstones · isolation de style Shadow DOM dans une page hôte · injection DOM pilotée par MutationObserver · tests unitaires de logique DOM avec Vitest + happy-dom

Problèmes et solutions

Tout découle d'une seule contrainte : vivre à l'intérieur de la page de YouTube, sur les données de YouTube, sans la permission ni l'API de YouTube.

█ Pas d'API, pas de connexion : le RSS comme seule source de données

Problème : Un flux par dossier a besoin des données de publication pour chaque chaîne. La Data API officielle exige une clé API, des quotas et une connexion Google, et cramer du quota par utilisateur tue une extension gratuite.

Chaque chaîne expose un flux RSS public (/feeds/videos.xml). L'extension le récupère côté client, depuis le propre navigateur de l'utilisateur, donc il n'y a aucun serveur, aucune clé et aucune connexion nulle part.

  • Data API de YouTube rejetée : le calcul des quotas échoue même avec un nombre modeste d'utilisateurs et impose un compte à tout le monde
  • Limites du RSS acceptées en connaissance de cause : ~15 entrées par chaîne, Shorts et lives pas distinguables de façon fiable
  • Un produit à coût nul et sans identifiant, qu'aucun changement de quota ne peut révoquer

▤ Survivre au DOM de YouTube en re-rendu permanent

Problème : YouTube est une SPA qui reconstruit son header et son contenu en permanence ; les boutons injectés disparaissent en pleine session, et le CSS de la page déborde sur tout ce qui est monté naïvement.

Toute l'UI est du Svelte dans un Shadow DOM (frontière de style scellée), et le bouton Ajouter-au-dossier est maintenu en vie par un MutationObserver sur le sous-arbre du header qui le ré-injecte dès que son ancre réapparaît.

  • Calendriers de retry fixes rejetés : soit ils pollent à l'infini, soit ils abandonnent juste avant que l'ancre ne monte
  • Les popovers se rendent dans un hôte d'overlay monté sur le body, pas dans le propre shadow root du bouton, pour que le header transformé de YouTube ne puisse pas les rogner
  • Compromis : couplage aux ancres DOM de YouTube, adouci par un lanceur flottant de repli quand l'ancre de la barre latérale est absente

▒ Sync sans comptes : un Sync Code de type bearer

Problème : La sync multi-appareils implique d'ordinaire de l'auth : flux email, OAuth, réinitialisation de mot de passe. C'est une surface énorme pour synchroniser une liste de dossiers.

Un Sync Code CSPRNG de 256 bits est l'identité tout entière. Le serveur indexe les dossiers par SHA-256 du code, donc la base de données ne détient jamais l'identifiant ; la première liaison fusionne les appareils par union, puis last-write-wins par dossier avec des suppressions marquées par tombstones.

  • Vrais comptes rejetés pour la v1, mais le schéma est façonné pour qu'une identité plus forte puisse remplacer le code plus tard
  • Tombstones plutôt que suppressions de lignes, pour qu'une suppression sur un appareil batte une copie périmée au lieu d'être silencieusement ressuscitée
  • Compromis énoncés d'emblée : perdre le code, c'est perdre le lien cloud ; les dossiers sont en clair côté serveur pour garder ouvertes de futures fonctionnalités serveur

▓ Un polling poli qui n'expose pas les utilisateurs au rate-limit

Problème : Les pastilles de non-lus nécessitent des vérifications RSS en arrière-plan depuis l'IP propre de chaque utilisateur. Les boucles naïves déclenchent des 429 de YouTube, et un service worker MV3 ne peut pas faire tourner de boucle du tout : Chrome le tue en quelques secondes.

Un unique tick chrome.alarms toutes les ~30 min réveille le worker ; les chaînes sont dédupliquées entre dossiers et récupérées avec des requêtes conditionnelles ETag, une concurrence plafonnée, du jitter et du backoff sur 429.

  • Poller côté serveur rejeté : aucun serveur n'existe, et en ajouter un recréerait le problème de coût et d'identifiant
  • Timers de type setInterval rejetés : les alarmes survivent au sommeil du worker, les timers meurent avec lui
  • Jusqu'à 30 min de latence sur les non-lus acceptées comme le juste prix de la politesse

▞ Un flux pleine page qui respecte le bouton Retour

Problème : Le flux a démarré comme un tiroir latéral : à l'étroit, et détruit par toute navigation, si bien qu'appuyer sur Retour après avoir regardé une vidéo déposait l'utilisateur sur la page en dessous au lieu de son flux.

Le flux occupe désormais la zone de contenu de YouTube comme une page native ; l'id du dossier vit dans le hash de l'URL, une entrée d'historique est ajoutée à l'ouverture, et Retour / avant / rechargement restaurent le flux. La zone de contenu derrière lui est masquée et son défilement verrouillé, pas seulement recouverte.

  • Ma propre décision antérieure du tiroir remplacée (ADR 0004 → 0007 / 0010) une fois que l'usage réel a montré que le tiroir se battait avec le site
  • Masquer la zone plutôt que la recouvrir stoppe le rendu en arrière-plan et le défilement qui transperce
  • Le masthead et la barre latérale restent actifs, si bien que ça se lit comme la propre page Abonnements de YouTube réduite à un seul dossier

░ S'aligner sur le thème de YouTube, pas sur celui de l'OS

Problème : prefers-color-scheme ment ici : YouTube laisse les utilisateurs choisir un thème de site indépendant de l'OS, donc une extension pilotée par l'OS rend des panneaux clairs sur une page sombre.

L'extension observe l'attribut dark sur l'élément racine de YouTube et reflète ce que la page affiche réellement, basculant en direct quand l'utilisateur change le thème du site.

  • Media query standard rejetée précisément parce que la page hôte, pas l'OS, est la source de vérité
  • Un seul MutationObserver sur un unique attribut, donc le coût est négligeable
  • Un embarquement au pixel près quelle que soit la façon dont l'utilisateur mêle thèmes OS et site