// ==UserScript== // @name 치지직 편의성 패치 // @namespace https://yoonu.io/ // @version 0.2 // @description 치지직 채팅의 닉네임 색상입히기 / 치즈 메시지 숨기기 / 사이드바 현재 활동, 시청자수 보기 // @author Yoonu // @match https://chzzk.naver.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=chzzk.naver.com // @grant none // @license MIT // @downloadURL none // ==/UserScript== // 기능 설정 - 사용하고 싫으면 true / 사용하기 싫으면 false const COLORIZE_CHAT = true; // 채팅 닉네임 색상화 const REMOVE_CHEEZE_MESSAGE = true; // 치즈 메시지 제거하기 const VIEW_NAV_INFO = false; // 사이드바 정보 보기 // 닉네임 색상 목록 const COLOR_LIST = [ "rgb(255, 0, 0)", "rgb(0, 0, 255)", "rgb(0, 128, 0)", "rgb(178, 34, 34)", "rgb(255, 127, 80)", "rgb(154, 205, 50)", "rgb(255, 69, 0)", "rgb(46, 139, 87)", "rgb(218, 165, 32)", "rgb(210, 105, 30)", "rgb(95, 158, 160)", "rgb(30, 144, 255)", "rgb(255, 105, 180)", "rgb(138, 43, 226)", "rgb(0, 255, 127)" ]; const VIEWER_FONT_SIZE = "13px"; const CATEGORY_FONT_SIZE = "9px"; const getRandomNumber = (max, seed) => { if(seed.length === 0) return 0; let val = 0; for (let i = 0; i < seed.length; i++) val += seed.charCodeAt(i); return val % max; } const fetchApi = async (url) => { const options = { "credentials": "include" }; const response = await fetch(url, options); return await response.json(); } // 채팅 옵저버 const setChatObserver = (chatNode) => { const callback = (mutationList, observer) => { for (let mutation of mutationList) { if (mutation.type !== "childList") continue; for(let addedNode of mutation.addedNodes) { // 치즈 메시지 제거 if(REMOVE_CHEEZE_MESSAGE && addedNode.className?.includes("live_chatting_list_donation")) { addedNode.classList.add("blind"); continue; } // 닉네임 색상 입히기 const nameTextNode = addedNode.querySelector("span[class^='name_text']"); if (nameTextNode) { const nickname = nameTextNode.innerText; const num = getRandomNumber(COLOR_LIST .length, nickname); nameTextNode.style.color = COLOR_LIST [num]; } } } } const observer = new MutationObserver(callback); observer.observe(chatNode, { attributes: false, childList: true, subtree: true }); } // 사이드바 라이브 현황 읽어오기 const fetchLiveStatus = async (listNode, fixOrder) => { /* const followListUrl = "https://api.chzzk.naver.com/service/v1/channels/followings/live"; const recommendListUrl = "https://api.chzzk.naver.com/service/v1/home/recommendation-channels"; const data = await fetchApi(followListUrl); const followingList = data.content.followingList; */ if(!navigation.className.includes("navigator_is_expanded")) return; const links = listNode.querySelectorAll("a[href^='/live/']"); for(let link of links) { const streamerCode = link.pathname.split("/")[2]; const apiUrl = `https://api.chzzk.naver.com/polling/v1/channels/${streamerCode}/live-status`; const data = await fetchApi(apiUrl); const viewerCountClass = "navigator_viewer_count"; const categoryClass = "navigator_category"; let viewerCount = link.querySelector("." + viewerCountClass); if(!viewerCount) { viewerCount = document.createElement("div"); viewerCount.style.fontSize = VIEWER_FONT_SIZE; viewerCount.className = viewerCountClass; link.appendChild(viewerCount); } const strongWrapper = link.querySelector("strong[class^='navigator_name']"); let category = strongWrapper.querySelector("." + categoryClass); if(!category) { category = document.createElement("div"); category.appendChild(strongWrapper.childNodes[0].cloneNode(true)) category.style.fontSize = CATEGORY_FONT_SIZE; category.className = categoryClass; strongWrapper.appendChild(category); } viewerCount.innerHTML = data.content.concurrentUserCount; category.classList.toggle("blind", data.content.liveCategoryValue.trim() === ""); category.querySelector("span[class^='name_text'").innerHTML = data.content.liveCategoryValue; // 네비게이션바 전체를 접었다가 펼치면 순서가 바뀌는걸 수정 if(fixOrder) link.appendChild(viewerCount); }; } // 주기적으로 API 읽기 const fetchInterval = (listNode) => { fetchLiveStatus(listNode); fetchTimer = setTimeout(fetchInterval, 60 * 1000, listNode); } let layoutBody, navigation, fetchTimer; (function() { 'use strict'; const layoutCallback = () => { const chatWindow = layoutBody.querySelector("section > aside"); if(chatWindow) setChatObserver(chatWindow) } const navigationCallback = (mutationList, observer) => { for(let mutation of mutationList) { if(mutation.type === "childList"){ mutation.addedNodes.forEach(addedNode => fetchInterval(addedNode)); } else if(mutation.type === "attributes") { mutation.target.childNodes.forEach(childNode => fetchLiveStatus(childNode, true)); } else { console.log(mutation); } } // if(navigation.childNodes.length >= 2) // navigationObserver.disconnect(); } // 메인섹션(방송, 채팅) if(COLORIZE_CHAT) { layoutBody = document.getElementById("layout-body"); const layoutBodyObserver = new MutationObserver(layoutCallback); layoutBodyObserver.observe(layoutBody, { attributes: false, childList: true, subtree: false }); } // 네비게이션(사이드바) if(VIEW_NAV_INFO) { navigation = document.getElementById("navigation"); const navigationObserver = new MutationObserver(navigationCallback); navigationObserver.observe(navigation, { attributes: true, childList: true, subtree: false }); } })();