An all-in-one enhancement extension for Crunchyroll (Chrome / Edge, Manifest V3). Version 0.2.9.
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.
On Crunchyroll — the live show panel: now-playing hero, MyAnimeList sync, your episode/status/score, plus the show's synopsis, seasons, characters and reviews. |
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. |
Settings — skip method, per-segment auto-skip toggles, playback options (auto-next, auto-PiP), cloud sync, and your MyAnimeList connection — all inline in the panel. |
Continue watching — your recently opened episodes, with one-click resume and per-entry remove. |
chrome.storage.sync.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.
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):
<video> straight past each enabled segment. If an
episode has no published data, it falls back to clicking Crunchyroll’s
native skip button.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.
Users open Options → MyAnimeList → Connect MyAnimeList, log into their own MAL account, and toggle Sync watched episodes on. Nothing else to set up.
–/+ (and type-to-set) episode stepper, status dropdown (Watching
/ Completed / …), a 1–10 star rating, and a link to the entry on MAL.https://jcfmdllkakmjkihgphmmimhiehcbbfei.chromiumapp.org/
(This ID is pinned by the key in the manifest, so it’s stable.)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.
Users open Cloud sync (in the side panel settings or the options page) → Sign in with Google, and their settings, watch history, skip stats, and MAL mappings are backed up and merged across devices. The MyAnimeList token is not synced — it stays on the device.
Sync is non-destructive — two devices never clobber each other:
It runs on a 15-minute alarm, on startup, on a debounced local change, and on the
Sync now button. Each store is one JSON blob per user in a sync_blobs table,
scoped to the signed-in user by row-level security.
In your Supabase project’s SQL editor, create the table + RLS policies:
create table if not exists public.sync_blobs (
user_id uuid not null references auth.users(id) on delete cascade,
kind text not null check (kind in ('settings','history','stats','mappings')),
data jsonb not null default '{}'::jsonb,
updated_at timestamptz not null default now(),
primary key (user_id, kind)
);
alter table public.sync_blobs enable row level security;
create policy "own rows: select" on public.sync_blobs for select using (auth.uid() = user_id);
create policy "own rows: insert" on public.sync_blobs for insert with check (auth.uid() = user_id);
create policy "own rows: update" on public.sync_blobs for update using (auth.uid() = user_id) with check (auth.uid() = user_id);
create policy "own rows: delete" on public.sync_blobs for delete using (auth.uid() = user_id);
https://<project-ref>.supabase.co/auth/v1/callback).package.mjs strips the manifest key for the store build so dev
and store installs get different chromiumapp.org origins:
https://jcfmdllkakmjkihgphmmimhiehcbbfei.chromiumapp.org/
https://jbmbolipkbppndjookmhmpceipfekhmi.chromiumapp.org/
src/shared/supabase-config.ts,
and add the project origin to host_permissions in scripts/build.mjs, then
rebuild.Sign-in uses Google OAuth via chrome.identity.launchWebAuthFlow (implicit flow);
the session is stored locally and refreshed automatically. The anon key is safe to
ship — it’s public by design and guarded by the RLS above.
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
│ ├─ auto-pip.ts # auto Picture-in-Picture on tab switch
│ ├─ pip-button.ts # PiP button injected into the player control bar
│ ├─ pip-enable.ts # clear Crunchyroll's disablePictureInPicture flag
│ ├─ progress.ts # report the current episode to the tracker
│ └─ toast.ts # "Skipped X — Undo" overlay
├─ background/
│ └─ service-worker.ts # skip-events fetch (avoids CORS) + MAL sync + cloud sync
├─ 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, Supabase client + cloud-sync engine, parsers
└─ assets/icons/
scripts/
├─ build.mjs # esbuild bundler + MV3 manifest generation → dist/
└─ package.mjs # stage dist/ into a Chrome Web Store upload zip
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:
chrome://extensions.dist/ folder./watch/... episode.After each rebuild, click the reload ↻ icon on the extension’s card in
chrome://extensionsso Chrome picks up the newdist/, 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.
404s (normal for episodes with no published
data), and watched: … lines tracing each MyAnimeList sync.MIT