<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Firebase on Guillaume Delré</title><link>https://guillaumedelre.github.io/fr/tags/firebase/</link><description>Recent content in Firebase on Guillaume Delré</description><generator>Hugo</generator><language>fr-FR</language><lastBuildDate>Sat, 06 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://guillaumedelre.github.io/fr/tags/firebase/index.xml" rel="self" type="application/rss+xml"/><item><title>J'ai arrêté d'attendre TVTime</title><link>https://guillaumedelre.github.io/fr/2026/06/06/jai-arr%C3%AAt%C3%A9-dattendre-tvtime/</link><pubDate>Sat, 06 Jun 2026 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2026/06/06/jai-arr%C3%AAt%C3%A9-dattendre-tvtime/</guid><description>Des années de spinners et une API fermée plus tard, j&amp;#39;ai reconstruit mon tracker depuis zéro avec React, Firebase et trois API publiques.</description><content:encoded><![CDATA[<p>Pendant des années, mon rituel pour cocher un épisode regardé ressemblait à : ouvrir TVTime, attendre, erreur, appuyer sur Réessayer, attendre encore, cocher enfin. Deux Réessayer et quarante secondes pour une case à cocher. Je continuais à l&rsquo;utiliser parce que le concept tenait la route — un journal de ce que je regarde, où j&rsquo;en suis, quelques stats vaguement déprimantes sur le temps que j&rsquo;ai confié aux plateformes de streaming. Mais ma patience avait atteint sa limite.</p>
<h2 id="lapi-qui-continuait-de-séloigner">L&rsquo;API qui continuait de s&rsquo;éloigner</h2>
<p>Il y a quelques années, j&rsquo;avais obtenu une clé d&rsquo;API développeur. L&rsquo;idée était d&rsquo;écrire un client minimal pour cocher des épisodes sans jamais toucher l&rsquo;interface de l&rsquo;app. J&rsquo;ai fait quelques requêtes, ça marchait, mais la lenteur venait de l&rsquo;API et non de l&rsquo;app — un client maison ne réglerait rien. J&rsquo;ai laissé tomber. Quelques mois plus tard, les requêtes étaient de toute façon bloquées — je n&rsquo;ai jamais su si ma clé avait été révoquée en silence ou si l&rsquo;API avait changé sans prévenir.</p>
<p>TVTime est une application gratuite sans modèle de monétisation évident — que l&rsquo;API soit la première victime d&rsquo;une réduction de coûts d&rsquo;infrastructure, c&rsquo;était compréhensible. J&rsquo;ai continué à utiliser l&rsquo;app, à attendre.</p>
<p>Quelques années plus tard, je commençais à jouer avec Home Assistant et l&rsquo;idée d&rsquo;un dashboard de mes épisodes en cours me paraissait évidente. J&rsquo;ai renvoyé une demande d&rsquo;accès API en mentionnant ce cas d&rsquo;usage. Cette fois la réponse était claire : plus de clé d&rsquo;API pour le public.</p>
<p>Second échec. Et en y repensant, TVTime n&rsquo;avait pas vraiment évolué depuis des années — si tant est qu&rsquo;il n&rsquo;avait pas régressé.</p>
<h2 id="le-dimanche-qui-a-rompu-lhabitude">Le dimanche qui a rompu l&rsquo;habitude</h2>
<p>Le troisième signal est arrivé un dimanche après-midi. Spinner. Erreur. Spinner. J&rsquo;ai fermé l&rsquo;app et j&rsquo;ai pensé qu&rsquo;il était temps de la remplacer.</p>
<p>Je suivais trois choses : des animés, des séries télévisées et des films. Les animés étaient la priorité — j&rsquo;en regarde davantage, le calendrier de diffusion est fragmenté sur plusieurs plateformes, et savoir ce qui sort dans la semaine est vraiment utile. Deux exigences rendraient un remplacement réellement utilisable : un calendrier hebdomadaire des sorties et une synchronisation multi-appareils (téléphone et ordinateur, au minimum).</p>
<p>Je me suis donné un week-end pour faire un POC. La stack sur laquelle je me suis appuyé : React 19, TypeScript, Vite, Firebase Auth et Firestore. Pas parce que c&rsquo;est une combinaison à la mode, mais parce qu&rsquo;elle résout le problème de synchronisation sans faire tourner un backend. Firestore fournit un document store en temps réel ; Firebase Auth gère la connexion entre les appareils ; le navigateur fait le reste. Je préférais payer la latence des lectures Firestore plutôt que de construire et maintenir un serveur API pour un projet personnel avec exactement un utilisateur.</p>
<p>Le projet s&rsquo;appelle Miru. En japonais, ça signifie « regarder ».</p>
<h2 id="trois-formats-trois-api">Trois formats, trois API</h2>
<p>La première vraie décision portait sur les API à utiliser. Les animés, les séries et les films vivent dans des écosystèmes de données séparés, et faire semblant du contraire m&rsquo;aurait coûté des semaines.</p>
<p>Pour les animés, <a href="https://anilist.gitbook.io/anilist-apiv2-docs/" target="_blank" rel="noopener noreferrer">AniList</a>
 s&rsquo;impose. Il expose une API GraphQL, couvre l&rsquo;essentiel du medium et inclut directement les calendriers de diffusion. Une seule requête avec une plage de dates retourne les prochains épisodes pour une liste d&rsquo;identifiants. Un seul appel réseau pour toute la semaine. Je ne m&rsquo;attendais pas à ce que ce soit aussi propre.</p>
<p>Pour les séries et les films, <a href="https://developer.themoviedb.org/docs/getting-started" target="_blank" rel="noopener noreferrer">TMDB</a>
 est la référence. Son API REST v3 est bien documentée, stable, et couvre le catalogue international que d&rsquo;autres bases de données ratent. La contrepartie : pas d&rsquo;endpoint de planning par lot. J&rsquo;appelle l&rsquo;endpoint de saison par série, je filtre sur la semaine en cours, et je collecte les résultats.</p>
<p>Les titres d&rsquo;épisodes pour les animés viennent d&rsquo;une troisième source : <a href="https://jikan.moe/" target="_blank" rel="noopener noreferrer">Jikan</a>
, qui agrège MyAnimeList. AniList n&rsquo;a pas toujours les titres d&rsquo;épisodes, surtout pour les séries plus anciennes. Quand un item suivi dispose d&rsquo;un MAL ID (qu&rsquo;AniList expose), je récupère la liste des épisodes depuis Jikan et j&rsquo;injecte les titres. C&rsquo;est un enrichissement optionnel — le calendrier fonctionne sans, mais la différence entre « Épisode 7 » et « La Troisième Trahison » vaut bien quelques requêtes supplémentaires.</p>
<h2 id="une-requête-pour-toute-la-semaine">Une requête pour toute la semaine</h2>
<p>Le calendrier hebdomadaire est la fonctionnalité qui justifie le projet. Chaque dimanche, je veux voir ce qui sort dans les sept prochains jours dans tout ce que je regarde, sans ouvrir quatre applications différentes.</p>
<p>Côté AniList, c&rsquo;est efficace. Une seule requête GraphQL prend la liste des identifiants AniList de mes animés en cours et une plage de dates, et retourne tous les épisodes prévus dans cette fenêtre. Une requête, toutes les données.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-graphql" data-lang="graphql"><span style="display:flex;"><span><span style="color:#66d9ef">query</span> <span style="color:#a6e22e">AiringSchedule</span>($ids: [Int], $start: <span style="color:#a6e22e">Int</span>, $end: <span style="color:#a6e22e">Int</span>) {
</span></span><span style="display:flex;"><span>  Page {
</span></span><span style="display:flex;"><span>    airingSchedules(
</span></span><span style="display:flex;"><span>      mediaId_in: $ids
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">airingAt_greater</span>: $start
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">airingAt_lesser</span>: $end
</span></span><span style="display:flex;"><span>    ) {
</span></span><span style="display:flex;"><span>      mediaId
</span></span><span style="display:flex;"><span>      episode
</span></span><span style="display:flex;"><span>      airingAt
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Côté TMDB, c&rsquo;est moins élégant. Pas d&rsquo;endpoint de planning par lot, donc j&rsquo;appelle l&rsquo;endpoint de saison par série et je filtre localement. Pour dix séries en cours, ça fait dix requêtes en parallèle — tolérable, et les limites de débit de TMDB sont suffisamment généreuses pour ne pas s&rsquo;en préoccuper.</p>
<p>L&rsquo;enrichissement Jikan tourne en dernier, après l&rsquo;assemblage des données AniList. Pour chaque épisode planifié où j&rsquo;ai un MAL ID, je récupère la liste depuis Jikan et j&rsquo;injecte le titre. Je mets ces données en cache par session pour éviter de récupérer la même liste deux fois.</p>
<h2 id="pas-de-serveur-pas-dops">Pas de serveur, pas d&rsquo;ops</h2>
<p>Firestore est l&rsquo;élément qui rend ce projet viable. Ma bibliothèque — quelles séries je suis, où j&rsquo;en suis, quels épisodes j&rsquo;ai cochés — tient dans un seul document Firestore par utilisateur. Il se charge à l&rsquo;authentification, se sauvegarde à chaque modification avec un debounce d'1,5 seconde pour ne pas saturer le quota gratuit, et se synchronise automatiquement entre les appareils.</p>
<p>Les règles de sécurité tiennent en deux lignes : lecture et écriture autorisées uniquement si <code>request.auth.uid == userId</code>. Pas de backend, pas de middleware d&rsquo;autorisation, pas de rotation de token à gérer.</p>
<p>L&rsquo;hébergement suit la même logique : l&rsquo;app est déployée sur <a href="https://vercel.com" target="_blank" rel="noopener noreferrer">Vercel</a>
, connectée au dépôt GitHub. Chaque push sur <code>main</code> déclenche un build et une mise en production automatique. Zéro configuration serveur, zéro pipeline à maintenir.</p>
<p>La seule chose que Firestore ne résout pas, c&rsquo;est le cold start. Le premier chargement sur un nouvel appareil attend l&rsquo;initialisation du SDK Firebase et l&rsquo;arrivée du document. Sur une bonne connexion, moins d&rsquo;une seconde. Sur un réseau mobile, ça se sent. J&rsquo;accepte ce compromis — un tracker personnel n&rsquo;a pas besoin de l&rsquo;architecture d&rsquo;une plateforme de streaming. Le jour où il y aurait davantage d&rsquo;utilisateurs, les choix faits ici mériteraient d&rsquo;être ré-étudiés : backend dédié, cache, séparation des responsabilités. Mais ce jour-là n&rsquo;est pas aujourd&rsquo;hui.</p>
<h2 id="lextension-qui-a-rendu-la-migration-possible">L&rsquo;extension qui a rendu la migration possible</h2>
<p>Ce que je redoutais le plus en remplaçant TVTime, c&rsquo;était l&rsquo;import initial. J&rsquo;avais quelques centaines d&rsquo;entrées suivies sur plusieurs années. Migrer ça manuellement, non merci.</p>
<p>La solution : une extension de navigateur — pas pour un usage courant, mais pour la migration unique. Des scripts de contenu injectés sur les pages <a href="https://anilist.gitbook.io/anilist-apiv2-docs/" target="_blank" rel="noopener noreferrer">AniList</a>
, MyAnimeList et Crunchyroll détectent le titre du media depuis le slug d&rsquo;URL, ajoutent un petit bouton « Ajouter à Miru » dans l&rsquo;interface existante, et transmettent le titre à l&rsquo;app au clic. L&rsquo;extension pré-remplit le dialogue de recherche. On clique, on vérifie, on confirme.</p>
<p>J&rsquo;ai migré ma liste AniList en vingt minutes environ. Mieux que prévu. Les séries et les films, j&rsquo;ai dû les faire à la main, mais ces listes étaient plus courtes et la recherche est assez rapide pour que ce ne soit pas pénible.</p>
<p>L&rsquo;extension est un build IIFE Manifest V3 — trois scripts de contenu séparés, un par plateforme, packagés pour Chrome et Firefox. Rien de complexe, mais ça a résolu le vrai point de friction qui sépare une migration qui se fait de celle qui ne se fait pas.</p>
<h2 id="ce-que-cest-devenu">Ce que c&rsquo;est devenu</h2>
<p>Miru tourne en bêta depuis environ une semaine. Le calendrier hebdomadaire fonctionne. La page de stats est brute mais fonctionnelle. La recherche dans la bibliothèque est rapide. J&rsquo;ai ouvert TVTime deux fois depuis lors, par réflexe, et refermé immédiatement les deux fois.</p>
<p>Ce que j&rsquo;avais sous-estimé, c&rsquo;est la valeur de posséder les données. Le document Firestore est le mien. Je peux l&rsquo;exporter, le migrer, l&rsquo;interroger, le brancher sur Home Assistant. C&rsquo;était l&rsquo;objectif d&rsquo;il y a deux ans — une API fermée plus tard, je l&rsquo;ai enfin.</p>
<p>Ce que j&rsquo;avais surestimé, c&rsquo;est la complexité des intégrations. AniList et TMDB sont de vraiment bonnes API. Leur documentation correspond au comportement réel, les réponses d&rsquo;erreur disent quelque chose d&rsquo;utile, et les limites de débit sont difficiles à atteindre en usage personnel. Trois heures d&rsquo;expérimentation ont suffi pour construire des clients fonctionnels pour les deux.</p>
<p>L&rsquo;extension m&rsquo;a surpris. Je m&rsquo;attendais à ce que ce soit la partie pénible. C&rsquo;est finalement la plus satisfaisante : injecter un bouton, extraire un titre, fermer un onglet. Périmètre réduit, résultat immédiat. Le genre d&rsquo;outil qui n&rsquo;a besoin de fonctionner qu&rsquo;une seule fois mais qui fait la différence entre une migration qui se fait et une qui ne se fait pas.</p>
<style>
.gh-card {
  display: block;
  border: 1px solid #d0d7de;
  padding: 16px;
  margin-top: 2em;
  border-radius: 6px;
  text-decoration: none !important;
}
.gh-card:hover { border-color: #8c959f; }
.gh-card,
.gh-card *,
.md-content .gh-card,
.md-content .gh-card * { text-decoration: none !important; }
.gh-card__head {
  display: flex;
  align-items: center;
  gap: 8px;
}
.gh-card__head svg {
  flex-shrink: 0;
  fill: #1f2328 !important;
}
.gh-card__repo {
  font-weight: 600;
  color: #1f2328 !important;
}
.gh-card__desc {
  margin: 8px 0 0;
  color: #59636e !important;
  font-size: 14px;
}
[data-theme=dark] .gh-card { border-color: #30363d; }
[data-theme=dark] .gh-card:hover { border-color: #6e7681; }
[data-theme=dark] .gh-card__head svg { fill: #e6edf3 !important; }
[data-theme=dark] .gh-card__repo { color: #e6edf3 !important; }
[data-theme=dark] .gh-card__desc { color: #8b949e !important; }
</style>
<a class="gh-card" href="https://github.com/guillaumedelre/miru" target="_blank" rel="noopener noreferrer">
  <span class="gh-card__head">
    <svg width="20" height="20" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
    <span class="gh-card__repo">guillaumedelre/miru</span>
  </span>
  <span class="gh-card__desc">Media tracking app (anime, series, movies) with a weekly airing calendar.</span>
</a>
]]></content:encoded></item></channel></rss>