For years, my ritual for checking off a watched episode was: open TVTime, wait, error, tap Retry, wait again, finally mark it. Two Retry taps and forty seconds for a single checkbox. I kept using it because the concept was solid — a log of what I’m watching, where I am, some rough stats on how much of my life I’ve handed to streaming platforms. But my patience had run out.

The API that kept moving away

A few years back, I got a developer API key. The plan was to write a minimal client to mark episodes without ever touching the app’s UI. I made a few requests, it worked, but the slowness was coming from the API itself — a homemade client wouldn’t fix anything. I dropped the idea. A few months later the requests were blocked anyway — I never figured out whether my key had been silently revoked or the API had changed under me.

TVTime is a free app with no obvious monetization model; the API being the first casualty of someone trimming infrastructure costs made sense. I kept using the app, kept waiting.

A few years later I started playing with Home Assistant, and the idea of a dashboard showing my currently-watching list seemed obvious. I sent another API access request, citing the Home Assistant use case. The answer was clear: no more API keys for the public.

Second failure. Looking back, TVTime hadn’t really evolved in years — if anything, it had regressed.

The Sunday that broke the habit

The third signal arrived on a Sunday afternoon. Spinner. Error. Spinner. I closed the app and thought it was time to replace it.

I was tracking three things: anime, TV series, and movies. Anime was the priority — I watch more of it, release schedules are spread across multiple streaming platforms, and knowing what airs this week is genuinely useful. Two requirements would make a replacement worth building: a proper weekly airing calendar and multi-device sync (phone and laptop, at minimum).

I gave myself a weekend to build a POC. The stack I reached for was React 19, TypeScript, Vite, Firebase Auth, and Firestore. Not because it’s a fashionable combination, but because it solves the sync problem without running a backend. Firestore gives you a real-time document store; Firebase Auth handles sign-in across devices; the browser handles the rest. I’d rather pay Firestore’s latency than build and maintain an API server for a personal project with exactly one user.

The project is called Miru. In Japanese, it means “to watch.”

Three formats, three APIs

The first real decision was which APIs to use. Anime, series, and movies live in separate data ecosystems, and pretending otherwise would have cost weeks.

For anime, AniList is the obvious choice. It has a GraphQL API, covers essentially everything in the medium, and exposes airing schedules directly. A single query with a date range returns upcoming episodes for a list of anime IDs. One network call for the entire week. I hadn’t expected that to be so clean.

For series and movies, TMDB is the standard. Its v3 REST API is well documented, stable, and covers the long tail of international content that other databases miss. The trade-off is that there’s no bulk schedule endpoint — I call it once per series I’m currently watching, filter to the current week, and collect the results.

Episode titles for anime came from a third source: Jikan , which mirrors MyAnimeList. AniList doesn’t always have episode titles, especially for older shows. When a tracked item has a MAL ID (which AniList exposes), I fetch the episode list from Jikan and use those titles. It’s optional enrichment — the calendar works without it, but the difference between “Episode 7” and “The Third Betrayal” is worth a few extra requests.

One query for the whole week

The weekly calendar is the feature that justified the project. Every Sunday, see what’s airing in the next seven days without opening four different apps.

The AniList query looks like this:

query AiringSchedule($ids: [Int], $start: Int, $end: Int) {
  Page {
    airingSchedules(
      mediaId_in: $ids
      airingAt_greater: $start
      airingAt_lesser: $end
    ) {
      mediaId
      episode
      airingAt
    }
  }
}

On the TMDB side, the ten calls go out in parallel — tolerable, and rate limits are generous enough not to care. The Jikan enrichment runs last, with per-session caching to avoid fetching the same list twice.

No server, no ops

Firestore is the piece that makes this viable as a personal project. My library — which shows I’m tracking, where I am, which episodes I’ve marked — lives in a single Firestore document per user. It loads on auth, saves on change with a 1.5-second debounce to avoid hammering the free-tier quota, and syncs across devices automatically.

The security rules are two lines: read and write only when request.auth.uid == userId. No backend, no authorization middleware, no token rotation to manage.

The hosting follows the same logic: the app is deployed on Vercel , connected to the GitHub repository. Every push to main triggers a build and an automatic deployment. Zero server configuration, zero pipeline to maintain.

The one thing Firestore doesn’t solve is cold start. The first load on a new device waits for the Firebase SDK to initialize and the document to arrive. On a good connection it’s under a second. On a mobile network, it’s noticeable. I accept this — a personal tracker doesn’t need the architecture of a streaming platform. The day there are more users, the choices made here would be worth revisiting: a dedicated backend, caching, proper separation of concerns. That day isn’t today.

The extension that made migration possible

The thing I dreaded most about replacing TVTime was the initial import. I had a few hundred items tracked across several years. Migrating that manually was not going to happen.

The solution was a browser extension — not for ongoing use, but for the one-time migration. Content scripts injected on AniList , MyAnimeList, and Crunchyroll pages detect the media title from the URL slug, add a small “Add to Miru” button next to the existing UI, and pass the title to the app on click. The extension pre-fills the search dialog. You click, verify, confirm.

I migrated my AniList list in about twenty minutes. Better than I expected. The series and movies I had to add by hand, but those lists were shorter and the search is fast enough that it wasn’t painful.

The extension is a Manifest V3 IIFE build — three separate content scripts, one per platform, packaged for both Chrome and Firefox. Nothing complex, but it solved the real friction point that separates a migration that happens from one that doesn’t.

What it’s become

Miru has been running for about a week. The weekly calendar works. The stats page is rough but functional. The library search is fast. I’ve opened TVTime twice since, out of habit, and closed it immediately both times.

What I underestimated was the value of owning the data. The Firestore document is mine. I can export it, migrate it, query it, pipe it into Home Assistant. That was the original goal two years ago — a closed API later, I’ve finally got it.

What I overestimated was the complexity of the integrations. AniList and TMDB are genuinely good APIs. Their documentation matches the actual behavior, error responses say something useful, and rate limits are hard to hit in personal use. Three hours of experimentation was enough to build working clients for both.

The extension surprised me. I expected it to be the annoying part. It turned out to be the most satisfying: inject a button, extract a title, close a tab. Small scope, immediate payoff. The kind of tooling that only needs to work once but makes the difference between a migration that happens and one that doesn’t.

guillaumedelre/miru Media tracking app (anime, series, movies) with a weekly airing calendar.