Chrome extension (Manifest V3) for client-side censoring of predefined segments on Crunchyroll streams, with a keyboard-driven in-page editor for authoring and reviewing rules.
This project is provided under the MIT License and is offered AS IS, without warranty of any kind. It is intended as a client-side playback filtering and automation tool for viewers such as parents watching generally appropriate content with their kids while skipping or masking specific unwanted moments. Based on the code in this repository, it operates on the user's own browser session, rules, and playback state; it does not present itself as a media downloader, DRM bypass tool, or copyright-transfer mechanism. Users are responsible for complying with applicable laws, platform terms, and their own viewing decisions.
- Censor modes:
none,blur,skip,solid_color,silence,image,freeze_start,freeze_end,frame - Remote rule fetch through the background service worker, with 5-minute per-stream caching
- Local draft storage in
chrome.storage.local - Import, export, upload, and download flows for rule sets
- Keyboard-driven editor with
SELECTION/EDITINGmodes, mirrored shortcuts, preview, metadata editing, and undo/redo - Inline confirmation for destructive image-data loss inside the editor UI
- Extension-action context-menu toggle for playback censoring that leaves the editor usable
- URL-change handling for episode transitions on Crunchyroll pages
- Fail-soft playback behavior, input validation, and sender-origin validation
- Fail-closed hidden-watch protection when remote rules may be missing, playback timestamps become unreliable, or the extension goes stale on an already-open page
For the exhaustive shipped-feature inventory, see FEATURES.md.
- Content scripts and editor UI run on
https://*.crunchyroll.com/* - Background message validation accepts HTTPS Crunchyroll origins only
- Clone the repository.
- Open
chrome://extensions/in Chrome or Chromium. - Enable Developer mode.
- Click Load unpacked and select this repository directory.
Open the extension Options page and set:
API Base URL: GET endpoint used to fetch rules for the currentstreamIdAPI Key: optional credential, passed as a query parameter on fetch and as a Bearer token on upload
The options page requests host permission for the configured API origin before saving it.
On a Crunchyroll watch page, the content script derives streamId from location.origin + location.pathname, fetches remote segments through background.js, merges them with local drafts, and applies the active censor mode during playback.
If hidden watch-mode can no longer guarantee strict censoring because remote rules may be missing, playback time is no longer reliable, or the extension was reloaded out from under the page, Rollcutter pauses playback immediately and shows a dismissible warning before playback can continue. Dismissing that warning suppresses any further automatic interruptions on the same page until reload.
The popup shows the current stream ID and lets you enable or disable censoring for the active tab.
You can also right-click the extension icon and use the action context-menu toggle to disable playback censoring without disabling the editor.
Frame-step seeks use displayed-frame metadata from requestVideoFrameCallback when Chromium exposes it, but they will not step by less than 1/24s per keypress. When metadata is unavailable, controls fall back to the same 24fps heuristic (1/24s per step). This still does not guarantee exact encoded-frame stepping across all streams or encodes.
Press the physical backquote key (KeyboardEvent.code === "Backquote") or PageUp on a Crunchyroll page to toggle the editor. The manifest also exposes a browser command for toggling the editor: Alt+Shift+E by default.
These are the baseline global controls.
- Toggle editor: backquote or
PageUp - Seek −1s / +1s:
j/kand mirroredf/dwhen the panel is visible inSELECTION, or when no segment-window remap applies - Seek −60s / +60s:
J/Kand mirroredF/D - Seek −1 frame / +1 frame:
l/;and mirroreda/swhen the panel is visible inSELECTION, or when no segment-window remap applies - Seek −10s / +10s:
L/:and mirroredA/S - New segment:
norb(defaults tonone) - Preview current or selected segment:
horg - When the panel is hidden,
j/k/l/;keep seeking playback directly. The older hidden selected-segment shift behavior remains only on the mirrored left-hand shortcuts for now.
- Move selection:
ArrowUp/ArrowDown - Enter editing:
EnterorDelete - Delete selected segment:
yort - While the panel is visible, it actively reclaims focus after extension actions, user focus changes, or player focus drift; unhandled keys still pass through to the player instead of being swallowed by the editor
- Save changes:
EnterorDelete - Cancel changes:
Escapeor' - Backquote /
PageUpwarn and keep the editor open if the current row has unsaved edits instead of auto-saving on hide - Cycle censor mode:
Tabor\ - Cycle to previous mode:
Shift+Tabor| - Start time −1s / +1s:
m/,with mirrorsv/c - Start time −1 frame / +1 frame:
.//with mirrorsz/x - End time −1s / +1s:
u/iwith mirrorsr/e - End time −1 frame / +1 frame:
o/pwith mirrorsq/w - Held start/end second edits wait for the previous seek to settle before repeating again
- Held start/end frame edits cap repeated movement to about one second of media time per real second of holding
- Preview:
horg - For non-
framesegments,j/kand mirroredf/dshift bothstartTimeandendTimeby ±1 second, whilel/;and mirroreda/sshift the whole segment window by ±1 frame and seek playback to the updated start framemode reference time −1s / +1s:j/kwith mirrorsf/dframemode reference time −1 frame / +1 frame:l/;with mirrorsa/ssolid_colormode color presets:c/Cblurmode radius:b/B- Press
Spaceto open the dialog for the currently selected field; submit the dialog withEnteror cancel it withEscape - The detail pane numbers the currently visible editable fields while you are in
EDITING; useArrowUp/ArrowDownto move the active field, press the matching digit shown next to a field to jump to it directly, useArrowUpwith no active field to select the last visible field andArrowDownto select the first, and move past either end to clear the active field ArrowLeft/ArrowRightadjust or cycle the selected field value, wrapping at the ends instead of clearing field focus; severity and confidence still accelerate up to 30 steps per second while held- Preview of
skipsegments is permissive only while editing that segment; selection-mode preview still honors skip behavior
Deleting a segment with stored image data, or replacing an existing image value, opens an inline confirmation in the detail pane instead of a browser confirm() dialog.
- Confirm:
YorEnter - Cancel:
N,Escape, or any other non-modifier key
Ctrl/Cmd+S: save the current edit session if you are editing a row, otherwise save local draftsCtrl/Cmd+E: export current segments to JSONCtrl/Cmd+I: import segments from JSONCtrl/Cmd+U: upload finalized segments to the configured APICtrl/Cmd+D: download segments from the configured API and replace or mergeCtrl/Cmd+Z: undoCtrl/Cmd+Shift+ZorCtrl/Cmd+Y: redo
- The options page can export local rule bundles for the full library, a single series group, or a single episode.
- Options-page imports accept both standard JSON and JSON5 bundle files.
- Each scope now includes an
Edit Textaction that opens an inline JSON5 editor prefilled with the selected bundle for paste/edit/apply workflows. - Episode rows surface audio language and item count as separate badges for faster scanning.
background.js requests rules with:
GET <apiBaseUrl>?streamId=<streamId>&apiKey=<apiKey>
The response may be either a raw array of segments or an object with a segments array.
The editor uploads finalized segments with:
POST <apiBaseUrl>
Authorization: Bearer <apiKey>
Content-Type: application/json
{
"streamId": "https://www.crunchyroll.com/watch/ABCD1234/example-episode",
"segments": []
}Local draft IDs beginning with local or zero are skipped during upload.
{
id?: string;
streamId?: string;
startTime?: number;
endTime?: number;
length?: number;
startFrame?: number;
endFrame?: number;
frameCount?: number;
mode: "none" | "blur" | "skip" | "solid_color" | "silence" | "image" | "freeze_start" | "freeze_end" | "frame";
params?: {
colorHex?: string;
blurRadius?: number;
imageUrl?: string;
referenceTime?: number; // displayed-frame mediaTime anchor for frame mode
comment?: string;
};
categories?: string[];
category?: string | null;
severity?: number | null;
confidence?: number | null;
}npm run build
npm run lint
npm run typecheck
npm test
npm run test:fast
npm run test:segment
npm run test:playback
npm run test:extension-load
npm run test:extension-load:browser
npm run test:e2e:fast
npm run test:e2e
npm run test:e2e:live
./scripts/install-hooks.sh
./package.shTests include plain Node scripts in tests/ plus Playwright browser coverage in tests/e2e/. npm run test:fast runs every non-e2e Node test in one bundle, npm run test:e2e runs the full Playwright suite (both fast and live projects), and npm test runs test:fast followed by test:e2e, i.e. all existing tests. npm run build:dist compiles the TypeScript runtime sources into src/*.js for direct Chrome loading, then copies an unpacked-extension-ready build into dist/. npm run build performs that dist build only, so packaging does not require live network access.
The default Playwright e2e command, npm run test:e2e, runs all Playwright projects. npm run test:e2e:live runs only the live Crunchyroll project against real Crunchyroll. The live project spawns real Google Chrome (/usr/bin/google-chrome by default; override with ROLLCUTTER_CHROME_BIN) with the built dist/ extension installed at runtime via the CDP Extensions.loadUnpacked method, because Chrome 142+ removed the --load-extension switch from branded builds. Chrome is launched with both --remote-debugging-pipe (for the privileged Extensions.loadUnpacked call) and --remote-debugging-port=0 (for Playwright to drive the suite via connectOverCDP). The live project always runs headed and is wrapped in xvfb-run, so it runs unattended on Linux hosts that have xvfb installed (sudo apt-get install -y xvfb) without needing a desktop session. Authentication is profile-based: npm run e2e:auth-refresh opens a real, headed Chrome window pinned to the dedicated profile at .cache/rollcutter-chrome-profile/ (git-ignored) so the human can sign in once, pick the right Crunchyroll account profile, and pass any Cloudflare check; that primed profile is then reused by every live test run. Place a single ignored .env.local line CRUNCHYROLL_PROFILE=<tile-name> to auto-pick the matching Crunchyroll account profile in the in-app picker (falls back to the first tile when unset). No login credentials are stored in the repo. The fast Playwright command is npm run test:e2e:fast, which uses a synthetic Crunchyroll fixture, patches the page, and blocks unrelated network calls. Scenario titles retain surface tags such as @fast, @fixture, @editing, @watching, @navigation, @keyboard, and @safety.
Run ./scripts/install-hooks.sh once per clone to link .git/hooks/pre-commit and .git/hooks/pre-push to the repo-owned hook runners. The pre-commit hook runs the fast Playwright e2e gate after local build verification; the pre-push hook runs the full test suite (npm test) when the pushed refs include a tag.
rollcutter/
├── src/
│ ├── background.ts / background.js
│ ├── content.ts / content.js
│ ├── popup.html / popup.ts / popup.js
│ ├── options.html / options.ts / options.js
│ ├── types.ts / types.js
│ ├── injected/player-hook.ts / player-hook.js
│ ├── ui/editor.css
│ └── ui/editor.html
├── dist/
├── tests/
├── manifest.json
├── AGENTS.md
├── ASSUMPTIONS.md
├── QUICKSTART.md
└── ROADMAP.md
For behavior changes, do both:
- Run the available repo checks.
- Run
npm run buildif the change affects packaged/built runtime files. - Reload the unpacked extension in Chrome and manually verify playback/editor behavior on real Crunchyroll pages.
If Chrome reloads the extension underneath an already-open watch page and the old content script becomes stale, use Ctrl+F5 to hard-refresh the page instead of a normal reload.
QUICKSTART.md— fast install and usage pathFEATURES.md— canonical inventory of shipped capabilitiesAGENTS.md— implementation guidance for coding agentsASSUMPTIONS.md— design constraintsROADMAP.md— true future work onlyPRIVACY.md— data handling and permissions