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
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