Greasy Fork

Greasy Fork is available in English.

치지직 편의성 패치

치지직 채팅의 닉네임 색상입히기 / 치즈 메시지 숨기기 / 사이드바 현재 활동, 시청자수 보기

目前为 2023-12-19 提交的版本。查看 最新版本

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