Skip to content

Latest commit

 

History

History
257 lines (193 loc) · 13.6 KB

File metadata and controls

257 lines (193 loc) · 13.6 KB

Rollcutter - Crunchyroll Censor Assistant

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.

Disclaimer

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.

What ships today

  • 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 / EDITING modes, 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.

Supported hosts

  • Content scripts and editor UI run on https://*.crunchyroll.com/*
  • Background message validation accepts HTTPS Crunchyroll origins only

Installation

From source

  1. Clone the repository.
  2. Open chrome://extensions/ in Chrome or Chromium.
  3. Enable Developer mode.
  4. Click Load unpacked and select this repository directory.

Configuration

Open the extension Options page and set:

  • API Base URL: GET endpoint used to fetch rules for the current streamId
  • API 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.

Usage

Playback

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.

Editor

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.

Global playback/editor shortcuts

These are the baseline global controls.

  • Toggle editor: backquote or PageUp
  • Seek −1s / +1s: j / k and mirrored f / d when the panel is visible in SELECTION, or when no segment-window remap applies
  • Seek −60s / +60s: J / K and mirrored F / D
  • Seek −1 frame / +1 frame: l / ; and mirrored a / s when the panel is visible in SELECTION, or when no segment-window remap applies
  • Seek −10s / +10s: L / : and mirrored A / S
  • New segment: n or b (defaults to none)
  • Preview current or selected segment: h or g
  • 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.

SELECTION mode

  • Move selection: ArrowUp / ArrowDown
  • Enter editing: Enter or Delete
  • Delete selected segment: y or t
  • 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

EDITING mode

  • Save changes: Enter or Delete
  • Cancel changes: Escape or '
  • Backquote / PageUp warn and keep the editor open if the current row has unsaved edits instead of auto-saving on hide
  • Cycle censor mode: Tab or \
  • Cycle to previous mode: Shift+Tab or |
  • Start time −1s / +1s: m / , with mirrors v / c
  • Start time −1 frame / +1 frame: . / / with mirrors z / x
  • End time −1s / +1s: u / i with mirrors r / e
  • End time −1 frame / +1 frame: o / p with mirrors q / 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: h or g
  • For non-frame segments, j / k and mirrored f / d shift both startTime and endTime by ±1 second, while l / ; and mirrored a / s shift the whole segment window by ±1 frame and seek playback to the updated start
  • frame mode reference time −1s / +1s: j / k with mirrors f / d
  • frame mode reference time −1 frame / +1 frame: l / ; with mirrors a / s
  • solid_color mode color presets: c / C
  • blur mode radius: b / B
  • Press Space to open the dialog for the currently selected field; submit the dialog with Enter or cancel it with Escape
  • The detail pane numbers the currently visible editable fields while you are in EDITING; use ArrowUp / ArrowDown to move the active field, press the matching digit shown next to a field to jump to it directly, use ArrowUp with no active field to select the last visible field and ArrowDown to select the first, and move past either end to clear the active field
  • ArrowLeft / ArrowRight adjust 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 skip segments is permissive only while editing that segment; selection-mode preview still honors skip behavior

Inline confirmations

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: Y or Enter
  • Cancel: N, Escape, or any other non-modifier key

Modifier shortcuts

  • Ctrl/Cmd+S: save the current edit session if you are editing a row, otherwise save local drafts
  • Ctrl/Cmd+E: export current segments to JSON
  • Ctrl/Cmd+I: import segments from JSON
  • Ctrl/Cmd+U: upload finalized segments to the configured API
  • Ctrl/Cmd+D: download segments from the configured API and replace or merge
  • Ctrl/Cmd+Z: undo
  • Ctrl/Cmd+Shift+Z or Ctrl/Cmd+Y: redo

Options page rule management

  • 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 Text action 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.

API contract

Fetch

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.

Upload

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.

Segment shape

{
  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;
}

Development

Commands

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.sh

Tests 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.

Project layout

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

Verification expectations

For behavior changes, do both:

  1. Run the available repo checks.
  2. Run npm run build if the change affects packaged/built runtime files.
  3. 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.

Related docs

  • QUICKSTART.md — fast install and usage path
  • FEATURES.md — canonical inventory of shipped capabilities
  • AGENTS.md — implementation guidance for coding agents
  • ASSUMPTIONS.md — design constraints
  • ROADMAP.md — true future work only
  • PRIVACY.md — data handling and permissions