Last active
July 13, 2026 20:21
-
-
Save shinmai/35edc1522a966088c40d69f83287a285 to your computer and use it in GitHub Desktop.
Crunchyroll subtitle unfucker userscript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name CR Subtitle Unfucker | |
| // @description crunchyroll changed from aegisub to OOONA and made it everybodys problem | |
| // @version 0.0.1 | |
| // @namespace rip.shi.crsubfix | |
| // @author shi | |
| // @match https://www.crunchyroll.com/watch/* | |
| // @match https://static.crunchyroll.com/* | |
| // @run-at document-start | |
| // @grant none | |
| // @updateURL https://gist.github.com/shinmai/35edc1522a966088c40d69f83287a285/raw/crsubfix.user.js | |
| // @downloadURL https://gist.github.com/shinmai/35edc1522a966088c40d69f83287a285/raw/crsubfix.user.js | |
| // ==/UserScript== | |
| (() => { | |
| 'use strict'; | |
| const SUB_URL = /crunchyrollcdn\.com\/static\/.*\/closed-caption-.*\.vtt/, | |
| RULES = [ | |
| [/\{break\}/g, '\n'], | |
| [/\\N/g, '\n'], | |
| [/\\n/g, '\n'], | |
| [/\n{2,}/g, '\n'], | |
| ], | |
| isAllCapsLine = s => { | |
| const letters = s.replace(/<[^>]+>/g, '') | |
| return /[A-Z]/.test(letters) && !/[a-z]/.test(letters) | |
| }, | |
| sentenceCase = text => text.replace(/([^<>]+)(?=<|$)/g, seg => seg.toLowerCase().replace(/(^|[.!?]\s+|\n)([a-z])/g, (m, p, c) => p + c.toUpperCase()).replace(/\bi\b/g, 'I')), | |
| fixCueText = text => { | |
| let out = text | |
| for(const [re, rep] of RULES) out = out.replace(re, rep) | |
| if(isAllCapsLine(out)) out = sentenceCase(out) | |
| return out | |
| }, | |
| processVtt = vtt => { | |
| const lines = vtt.split('\n') | |
| let inCue = false | |
| for(let i = 0; i < lines.length; i++) { | |
| if(lines[i].includes('-->')) { inCue = true; continue } | |
| if(inCue) { | |
| if(lines[i].trim() === '') { inCue = false; continue } | |
| lines[i] = fixCueText(lines[i]) | |
| } | |
| } | |
| return lines.join('\n') | |
| }, | |
| transformBody = body => { | |
| try { | |
| let text = body | |
| let wasB64 = false | |
| if(!text.trimStart().startsWith('WEBVTT')) { | |
| try { | |
| const dec = atob(text.trim()) | |
| if(!dec.trimStart().startsWith('WEBVTT')) return null | |
| text = dec | |
| wasB64 = true | |
| } catch { return null } | |
| } | |
| let fixed = processVtt(text) | |
| if(wasB64) fixed = btoa(unescape(encodeURIComponent(fixed))) | |
| return fixed | |
| } catch { return null } | |
| }, | |
| XHR = XMLHttpRequest.prototype, | |
| origOpen = XHR.open, | |
| protoResponseText = Object.getOwnPropertyDescriptor(XHR, 'responseText').get, | |
| protoResponse = Object.getOwnPropertyDescriptor(XHR, 'response').get | |
| XHR.open = function (method, url, ...rest) { | |
| if (SUB_URL.test(String(url))) { | |
| const xhr = this | |
| let cached, cachedFor = null | |
| const getFixed = raw => { | |
| if(raw !== cachedFor) { | |
| cachedFor = raw | |
| const t = transformBody(raw) | |
| cached = t === null?raw:t | |
| } | |
| return cached | |
| } | |
| Object.defineProperty(xhr, 'responseText', { | |
| get() { | |
| const raw = protoResponseText.call(xhr) | |
| if (xhr.readyState !== 4) return raw | |
| return getFixed(raw) | |
| } | |
| }) | |
| Object.defineProperty(xhr, 'response', { | |
| get() { | |
| if (xhr.responseType && xhr.responseType !== 'text') { return protoResponse.call(xhr) } | |
| const raw = protoResponse.call(xhr) | |
| if (xhr.readyState !== 4) return raw | |
| return getFixed(raw) | |
| } | |
| }) | |
| } | |
| return origOpen.call(this, method, url, ...rest) | |
| } | |
| })() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment