Skip to content

Instantly share code, notes, and snippets.

@shinmai
Last active July 13, 2026 20:21
Show Gist options
  • Select an option

  • Save shinmai/35edc1522a966088c40d69f83287a285 to your computer and use it in GitHub Desktop.

Select an option

Save shinmai/35edc1522a966088c40d69f83287a285 to your computer and use it in GitHub Desktop.
Crunchyroll subtitle unfucker userscript
// ==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