crunchy-companion

Crunchy Companion

An all-in-one enhancement extension for Crunchyroll (Chrome / Edge, Manifest V3). Version 0.1.0.

It lives in a persistent Chrome side panel that adapts to what you’re doing: a live show companion while you watch, and a home dashboard everywhere else.

Crunchy Companion side panel next to a Crunchyroll episode
On Crunchyroll — the live show panel: now-playing hero, MyAnimeList sync, your episode/status/score, plus the show's synopsis, seasons, characters and reviews.
Crunchy Companion home dashboard next to another site
Anywhere else — the home dashboard: your skip stats and activity, a Resume card, Continue-watching, your MyAnimeList "watching" list, and what's trending this season.
Crunchy Companion settings panel
Settings — skip method, per-segment auto-skip toggles, playback options, and your MyAnimeList connection — all inline in the panel.
Crunchy Companion recent / continue-watching panel
Continue watching — your recently opened episodes, with one-click resume and per-entry remove.

This is a personal, client-side enhancement that only automates actions you can already perform yourself (clicking Skip / Next). It does not bypass paywalls, DRM, or advertising.

How skipping works

Crunchyroll publishes per-episode skip timings as static JSON — the same data that powers its own Skip Intro button:

https://static.crunchyroll.com/skip-events/production/{episodeId}.json

The extension supports two methods (Options → Skip method):

Because Crunchyroll is a single-page app, the content script watches History API navigation (and polls as a safety net) and re-initialises for each new episode without a page reload.

MyAnimeList sync

Users open Options → MyAnimeListConnect MyAnimeList, log into their own MAL account, and toggle Sync watched episodes on. Nothing else to set up.

Developer setup (one-time, to bake in the API client)

  1. At myanimelist.net/apiconfigCreate ID, set App Type = Other (a public client — no secret).
  2. Set App Redirect URL to exactly: https://jcfmdllkakmjkihgphmmimhiehcbbfei.chromiumapp.org/ (This ID is pinned by the key in the manifest, so it’s stable.)
  3. Paste the generated Client ID into src/shared/mal-config.ts (MAL_CLIENT_ID) and rebuild.

Auth is OAuth2 authorization-code + PKCE; tokens are stored locally and refreshed automatically. The client ID is safe to ship (it’s not a secret in PKCE flows); the signing key (mal-signing-key.pem) is gitignored.

Project layout

src/
├─ content/                # runs on the watch page (all frames)
│  ├─ index.ts             #   entry: wires the per-episode session together
│  ├─ navigation.ts        #   SPA episode-change detection (History API + poll)
│  ├─ player.ts            #   locate <video>, seek helper
│  ├─ meta.ts              #   scrape series/season/episode (JSON-LD, og:title)
│  ├─ skip-api.ts          #   ask the worker for skip-events data
│  ├─ skip-engine.ts       #   seek-mode auto-skip
│  ├─ dom-skip.ts          #   fallback: click the native skip button
│  ├─ autonext.ts          #   auto-play next episode
│  ├─ keep-watching.ts     #   dismiss "still watching?" / profile prompts
│  ├─ progress.ts          #   report the current episode to the tracker
│  └─ toast.ts             #   "Skipped X — Undo" overlay
├─ background/
│  └─ service-worker.ts    # skip-events fetch (avoids CORS) + MyAnimeList sync hub
├─ options/                # full settings page (fallback)
├─ sidepanel/              # the side panel: show view, home dashboard, settings,
│                          #   MAL card, rails (seasons/characters/reviews/trending)
├─ shared/                 # settings, messages, MAL client, tracker store,
│                          #   history, stats, runtime guards, types, parsers
└─ assets/icons/
scripts/
└─ build.mjs               # esbuild bundler + MV3 manifest generation → dist/

Build & load

npm install
npm run build    # type-checks (tsc --noEmit), then esbuild-bundles to dist/

Each entry is bundled as a single self-contained IIFE (no code-splitting, no dynamic import()) and the content script is declared directly in the manifest. This matters: Crunchyroll’s player runs in a cross-origin iframe (static.crunchyroll.com/.../player.html) with a strict CSP, and a dynamic-import-based content-script loader (e.g. @crxjs) gets blocked there — so the skip code would never run where the video actually is. Manifest-declared content scripts are injected by Chrome and bypass the page CSP.

Then in Chrome / Edge:

  1. Open chrome://extensions.
  2. Enable Developer mode (top-right).
  3. Click Load unpacked and select the dist/ folder.
  4. Open any Crunchyroll /watch/... episode.

After each rebuild, click the reload ↻ icon on the extension’s card in chrome://extensions so Chrome picks up the new dist/, then reload the Crunchyroll tab (content scripts aren’t re-injected into already-open tabs).

Click the toolbar icon to open the side panel (needs Chrome 114+); its Settings view covers everything, with the standalone options page as a fallback.

Verifying it works

License

MIT