CHZZK Direct Stream

fuck off chzzk

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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
          },
        })
      },
    })
  }
})()