fuck off chzzk
// ==UserScript==
// @name CHZZK Direct Stream
// @description fuck off chzzk
// @name:ko 치지직 그리드 우회
// @version 0.0.4
// @namespace nomu523
// @match https://chzzk.naver.com/*
// @run-at document-start
// @grant unsafeWindow
// @license AGPL-3.0-or-later
// ==/UserScript==
(() => {
'use strict'
const root = typeof unsafeWindow === 'undefined' ? window : unsafeWindow
const jsonParse = root.JSON.parse
const jsonStringify = root.JSON.stringify
const Uint8Array = root.Uint8Array
const playbackFields = [
'livePlaybackJson',
'radioModePlaybackJson',
'previewPlaybackJson',
]
const cdnSuffixes = ['pstatic.net', 'navercdn.com']
const baitClasses = new root.Set([
'banner_ad_wrapper',
'TopAdBox',
'ad--dart',
'ad-container-side',
'ad-tag__wrapper',
'ad_regular1',
'ad_slot_inread',
'adguru-modal-popup',
'ads_box_headline',
'adsmedrect',
'advert-loader',
'advertisementContainer',
'advertising-top-box',
'artnet-ads-ad',
'dianomi_context',
'divAdright',
'footer__ads--content',
'header-article-ads',
'header-bannerad',
'incontentAd',
'js-anchor-ad',
'module-box-ads',
'moduletable_advertisement',
'news-ad-square-a',
'nfy-ad-wrapper',
'rekl_left',
'sidebar-advert',
'slideshow-ad-wrapper',
'widget--local-ads',
'zaf-adhesion-container',
'zerg-widget',
])
function isObject(value) {
return value !== null && typeof value === 'object'
}
function getDirectUrl(p2pPath) {
try {
const encoded = new root.URL(p2pPath, root.location.origin)
.searchParams
.get('cdn_url')
if (!encoded) return
const base64 = encoded
.replace(/ /g, '+')
.replace(/-/g, '+')
.replace(/_/g, '/')
.padEnd(Math.ceil(encoded.length / 4) * 4, '=')
const url = new root.URL(root.atob(base64))
if (cdnSuffixes.some(
suffix => url.hostname === suffix || url.hostname.endsWith(`.${suffix}`),
)) {
return url.href
}
} catch {}
}
function patchP2pPath(item) {
if (!isObject(item)) return false
let changed = false
const directUrl = getDirectUrl(item.p2pPath)
if (directUrl && item.path !== directUrl) {
item.path = directUrl
changed = true
}
for (const key of ['p2pPath', 'p2pPathUrlEncoding']) {
if (!(key in item)) continue
delete item[key]
changed = true
}
return changed
}
function patchPlayback(playback) {
if (!isObject(playback)) return false
let changed = false
if (isObject(playback.meta) && playback.meta.p2p !== false) {
playback.meta.p2p = false
changed = true
}
if (!Array.isArray(playback.media)) return changed
for (const media of playback.media) {
if (!['HLS', 'LLHLS'].includes(media?.mediaId)) continue
changed = patchP2pPath(media) || changed
if (!Array.isArray(media.encodingTrack)) continue
for (const track of media.encodingTrack) {
changed = patchP2pPath(track) || changed
}
}
return changed
}
function patchPayload(payload) {
if (!isObject(payload)) return false
let changed = patchPlayback(payload)
const isSchedule = Array.isArray(payload.adBreaks) && (
payload.head?.description === 'GFP Video Ad Schedule' ||
typeof payload.videoAdScheduleId === 'string'
)
const isWaterfall = Array.isArray(payload.ads) && (
payload.head?.description === 'Naver SSP Waterfall List' ||
typeof payload.adUnit === 'string' && typeof payload.adDivId === 'string'
)
if (isSchedule && payload.adBreaks.length) {
payload.adBreaks.length = 0
changed = true
root.console?.debug?.('[CHZZK Direct Stream] ad schedule cleared')
}
if (isWaterfall && payload.ads.length) {
payload.ads.length = 0
changed = true
root.console?.debug?.('[CHZZK Direct Stream] ad waterfall cleared')
}
if (isObject(payload.playerAdDisplayResponse)) {
Object.assign(payload.playerAdDisplayResponse, {
preRoll: false,
midRoll: false,
postRoll: false,
})
changed = true
}
if ('adControlType' in payload && ('event' in payload || 'adCount' in payload)) {
payload.id = null
payload.event = 'ABORT'
payload.adCount = 0
changed = true
}
if (
isObject(payload.livePlaybackJson) &&
('liveId' in payload.livePlaybackJson ||
'chatChannelId' in payload.livePlaybackJson)
) {
payload.livePlaybackJson.liveId = false
payload.livePlaybackJson.chatChannelId = false
changed = true
}
const isMedia = 'liveId' in payload ||
'videoNo' in payload ||
playbackFields.some(key => key in payload)
if (isMedia && payload.skipPreRollAd === false) {
payload.skipPreRollAd = true
changed = true
}
if (isMedia && 'dab' in payload && payload.dab !== false) {
payload.dab = false
changed = true
}
for (const key of playbackFields) {
if (typeof payload[key] !== 'string' || !payload[key]) continue
try {
const playback = Reflect.apply(jsonParse, root.JSON, [payload[key]])
if (!patchPlayback(playback)) continue
payload[key] = Reflect.apply(jsonStringify, root.JSON, [playback])
changed = true
} catch {}
}
return changed
}
function patchResponse(value) {
const rootChanged = patchPayload(value)
const contentChanged = patchPayload(value?.content)
return rootChanged || contentChanged
}
root.JSON.parse = new root.Proxy(jsonParse, {
apply(target, thisArg, args) {
const value = Reflect.apply(target, thisArg, args)
patchResponse(value)
return value
},
})
const responseJson = root.Response?.prototype?.json
if (responseJson) {
root.Response.prototype.json = function () {
return Reflect.apply(responseJson, this, arguments).then(value => {
patchResponse(value)
return value
})
}
}
const subtle = root.crypto?.subtle
const decrypt = subtle?.decrypt
if (decrypt && Uint8Array && root.TextDecoder && root.TextEncoder) {
const decoder = new root.TextDecoder('utf-8', { fatal: true })
const encoder = new root.TextEncoder()
function patchBuffer(buffer) {
if (
!(buffer instanceof root.ArrayBuffer) ||
buffer.byteLength < 2 ||
buffer.byteLength > 2 * 1024 * 1024
) {
return buffer
}
try {
const bytes = new Uint8Array(buffer)
let offset = bytes[0] === 0xef &&
bytes[1] === 0xbb &&
bytes[2] === 0xbf
? 3
: 0
const limit = Math.min(bytes.length, offset + 32)
while (
offset < limit &&
[0x09, 0x0a, 0x0d, 0x20].includes(bytes[offset])
) {
offset += 1
}
if (bytes[offset] !== 0x7b && bytes[offset] !== 0x5b) return buffer
const value = Reflect.apply(jsonParse, root.JSON, [
decoder.decode(bytes.subarray(offset)),
])
if (!patchResponse(value)) return buffer
return encoder.encode(Reflect.apply(jsonStringify, root.JSON, [value])).buffer
} catch {
return buffer
}
}
function patchedDecrypt() {
return Reflect.apply(decrypt, this, arguments).then(patchBuffer)
}
const prototype = Object.getPrototypeOf(subtle)
const descriptor = Object.getOwnPropertyDescriptor(prototype, 'decrypt')
try {
Object.defineProperty(prototype, 'decrypt', {
...descriptor,
value: patchedDecrypt,
})
} catch {
try {
subtle.decrypt = patchedDecrypt
} catch {}
}
}
const getComputedStyle = root.getComputedStyle
if (getComputedStyle) {
root.getComputedStyle = new root.Proxy(getComputedStyle, {
apply(target, thisArg, args) {
const style = Reflect.apply(target, thisArg, args)
const className = args[0]?.className
const isBait = typeof className === 'string' &&
className.split(/\s+/).some(name => baitClasses.has(name))
if (!isBait) return style
return new root.Proxy(style, {
get(targetStyle, key) {
if (key === 'display') return 'block'
const value = Reflect.get(targetStyle, key, targetStyle)
return typeof value === 'function' ? value.bind(targetStyle) : value
},
})
},
})
}
})()