// ==UserScript== // @name 숲 멀티뷰 시청자 제거기 // @namespace http://tampermonkey.net/ // @version 1.2 // @description 숲 생방송 시청자 목록을 가져오고 멀티뷰를 제거합니다. // @author asdi // @match https://play.sooplive.co.kr/* // @grant none // @downloadURL none // ==/UserScript== (function () { "use strict"; // 팝업창 생성 const popup = document.createElement("div"); popup.style.position = "fixed"; popup.style.top = "50%"; popup.style.left = "0"; popup.style.transform = "translateY(-50%)"; popup.style.zIndex = "1000"; popup.style.width = "350px"; popup.style.padding = "15px"; popup.style.backgroundColor = "#f8f9fa"; popup.style.border = "1px solid #ccc"; popup.style.borderRadius = "8px"; popup.style.boxShadow = "0 4px 8px rgba(0, 0, 0, 0.1)"; popup.style.fontSize = "14px"; popup.style.fontFamily = "Arial, sans-serif"; popup.style.color = "#333"; popup.innerHTML = `
닉네임 시청자 수 로그인 비로그인 비율 중복

닉네임 로그인 비로그인 멀티뷰
결과가 여기에 표시됩니다.
`; document.body.appendChild(popup); // 닫기 버튼 핸들러 document.getElementById("closePopup").addEventListener("click", () => { document.body.removeChild(popup); // 팝업창 삭제 }); // userIdList에서 괄호 부분을 무시하고 중복 없는 아이디 리스트를 구하는 함수 const getUniqueUserIdList = (userIdList) => { const uniqueUserIds = new Set(); userIdList.forEach((userId) => { const baseUserId = userId.replace(/\(\d+\)$/, ""); uniqueUserIds.add(baseUserId); }); return [...uniqueUserIds]; }; const countDuplicateUsers = (userIdList) => { const userCountMap = new Map(); userIdList.forEach((userId) => { const baseUserId = userId.replace(/\(\d+\)$/, ""); // 괄호 숫자 제거 userCountMap.set(baseUserId, (userCountMap.get(baseUserId) || 0) + 1); }); // 중복 2개 이상인 사용자만 필터링 const duplicateUsers = [...userCountMap].filter(([_, count]) => count >= 2); return duplicateUsers.length; }; // 닉네임 가져오기 버튼 핸들러 document.getElementById("getNickName").addEventListener("click", async () => { const nicknameElement = document.querySelector("a#infoNickName"); const nicknameResultsTable = document.getElementById( "nicknameResultsTable" ); if (nicknameElement) { const nickname = nicknameElement.textContent.trim(); try { liveView.Chat.chatUserListLayer.reconnect(); liveView.playerController.sendChUser(); const viewerText = document.getElementById("nAllViewer").textContent; const viewerNumber = parseInt(viewerText.replace(/,/g, "")); console.log("시청자:", viewerNumber); // 3초 대기 await new Promise((resolve) => setTimeout(resolve, 3000)); const userList = liveView.Chat.chatUserListLayer; // const userIdList = [ // ...Object.keys(userList.userListSeparatedByGrade.fan).slice(1), // ...Object.keys(userList.userListSeparatedByGrade.manager).slice(1), // ...Object.keys(userList.userListSeparatedByGrade.normal).slice(1), // ...Object.keys(userList.userListSeparatedByGrade.subscription).slice( // 1 // ), // ...Object.keys(userList.userListSeparatedByGrade.supporter).slice(1), // ...Object.keys(userList.userListSeparatedByGrade.vip).slice(1), // ]; const userIdList = [ ...userList.userListSeparatedByGrade.fan .slice(1) .map((user) => user.id), ...userList.userListSeparatedByGrade.manager .slice(1) .map((user) => user.id), ...userList.userListSeparatedByGrade.normal .slice(1) .map((user) => user.id), ...userList.userListSeparatedByGrade.subscription .slice(1) .map((user) => user.id), ...userList.userListSeparatedByGrade.supporter .slice(1) .map((user) => user.id), ...userList.userListSeparatedByGrade.vip .slice(1) .map((user) => user.id), ]; const subscriptionKeysSet = new Set( Object.keys(userList.userSubscriptionMonth) ); const loggedInCount = subscriptionKeysSet.size - 1; // 스트리머 -1 console.log("로그인: ", loggedInCount); console.log("채팅창인원: ", userIdList.length); const nonLoggedInCount = viewerNumber - userIdList.length > 0 ? viewerNumber - userIdList.length : 0; console.log("비로그인: ", nonLoggedInCount); // 19금방에서 시청자수보다 채팅창인원이 더많은경우 있음. const uniqueUserIdList = getUniqueUserIdList(userIdList); console.log("중복뺀 채팅창인원:", uniqueUserIdList.length); const loginRatio = ( (loggedInCount / (nonLoggedInCount + loggedInCount)) * 100 ).toFixed(2); const duplicateViewCount = userIdList.length - loggedInCount; console.log("중복:", duplicateViewCount); console.log("중복중인 인원:", countDuplicateUsers(userIdList)); // 테이블에 결과 표시 const row = document.createElement("tr"); row.innerHTML = ` ${nickname} ${viewerNumber.toLocaleString()} ${loggedInCount.toLocaleString()} ${nonLoggedInCount.toLocaleString()} ${loginRatio}% ${duplicateViewCount.toLocaleString()} `; nicknameResultsTable.querySelector("tbody").appendChild(row); // ID Map에 저장 const storedUserMap = JSON.parse(localStorage.getItem("userIdMap")) || []; const refinedUserMap = new Map(storedUserMap); function refineUserId(userId) { return userId.replace(/\(\d+\)$/, ""); // "(숫자)" 부분 제거 } userIdList.forEach((userId) => { const refinedId = refineUserId(userId); if (!refinedUserMap.has(refinedId)) { refinedUserMap.set(refinedId, userId); } else { const existingUserId = refinedUserMap.get(refinedId); const existingPriority = existingUserId.match(/\((\d+)\)$/); const currentPriority = userId.match(/\((\d+)\)$/); if ( !existingPriority || (currentPriority && parseInt(currentPriority[1]) < parseInt(existingPriority[1])) ) { refinedUserMap.set(refinedId, userId); } } }); // 로컬스토리지에 저장 localStorage.setItem("userIdMap", JSON.stringify([...refinedUserMap])); localStorage.setItem( "userIdList_" + nickname, JSON.stringify(userIdList) ); localStorage.setItem( "uniqueUserIdList_" + nickname, JSON.stringify(uniqueUserIdList) ); localStorage.setItem("nonLoggedInCount_" + nickname, nonLoggedInCount); } catch (error) { console.error("Error:", error); nicknameResultsTable.querySelector( "tbody" ).innerHTML = `Error: ${error.message}`; } } }); // 로컬스토리지에서 시청자 목록 불러오기 window.addEventListener("load", () => { const nicknameResultsTable = document.getElementById( "nicknameResultsTable" ); const storedNicknames = Object.keys(localStorage).filter((key) => key.startsWith("userIdList_") ); storedNicknames.forEach((key) => { const nickname = key.replace("userIdList_", ""); const userIdList = JSON.parse(localStorage.getItem(key)); const uniqueUserIdList = JSON.parse( localStorage.getItem("uniqueUserIdList_" + nickname) ); const nonLoggedInCount = parseInt(localStorage.getItem("nonLoggedInCount_" + nickname)) || 0; const viewerNumber = userIdList.length + nonLoggedInCount; const loggedInCount = uniqueUserIdList.length; const loginRatio = ( (loggedInCount / (nonLoggedInCount + loggedInCount)) * 100 ).toFixed(2); const duplicateViewCount = userIdList.length - loggedInCount; // 테이블에 표시 const row = document.createElement("tr"); row.innerHTML = ` ${nickname} ${viewerNumber.toLocaleString()} ${loggedInCount.toLocaleString()} ${nonLoggedInCount.toLocaleString()} ${loginRatio}% ${duplicateViewCount.toLocaleString()} `; nicknameResultsTable.querySelector("tbody").appendChild(row); }); }); // 총 시청자 수 구하기 버튼 핸들러 document.getElementById("totalViewerCount").addEventListener("click", () => { const totalViewerResults = document.getElementById("totalViewerResults"); const totalViewerResultsTable = document.getElementById( "totalViewerResultsTable" ); const nicknameKeys = Object.keys(localStorage).filter((key) => key.startsWith("userIdList_") ); if (nicknameKeys.length > 0) { const uniqueViewerLists = []; const storedUserMap = JSON.parse(localStorage.getItem("userIdMap")); const refinedUserMap = new Map(storedUserMap || []); const refinedViewerCounts = {}; // 정제된 아이디 기준 카운트 const nicknames = []; let totalViewerNumber = 0; let totalLiveNumber = 0; let totalNonLoggedInCount = 0; nicknameKeys.forEach((key) => { const nickname = key.replace("userIdList_", ""); const userIdList = JSON.parse(localStorage.getItem(key)); const uniqueUserIdList = JSON.parse( localStorage.getItem("uniqueUserIdList_" + nickname) ); const nonLoggedInCount = parseInt(localStorage.getItem("nonLoggedInCount_" + nickname)) || 0; totalNonLoggedInCount += nonLoggedInCount; let liveViewerCount = 0; uniqueViewerLists.push(new Set(uniqueUserIdList)); nicknames.push(nickname); totalViewerNumber += uniqueUserIdList.length; userIdList.forEach((id) => { const refinedId = id.replace(/\(\d+\)$/, ""); if (refinedUserMap.get(refinedId) === id) { liveViewerCount += 1; // liveViewerCount 증가 } }); // 테이블에 결과 표시 const row = document.createElement("tr"); row.innerHTML = ` ${nickname} ${liveViewerCount.toLocaleString()} ${nonLoggedInCount.toLocaleString()} ${(liveViewerCount + nonLoggedInCount).toLocaleString()} ${( uniqueUserIdList.length - liveViewerCount ).toLocaleString()} `; totalViewerResultsTable.querySelector("tbody").appendChild(row); totalLiveNumber += liveViewerCount; }); const unionSet = new Set(); uniqueViewerLists.forEach((set) => set.forEach((viewer) => unionSet.add(viewer)) ); const multiViewExcludedCount = unionSet.size; const idCounts = {}; uniqueViewerLists.forEach((set) => { set.forEach((viewer) => { idCounts[viewer] = (idCounts[viewer] || 0) + 1; }); }); const multiSetUsers = Object.keys(idCounts).filter( (viewer) => idCounts[viewer] > 1 ); const multiSetCount = multiSetUsers.length; const nicknameList = nicknames.join(", "); const now = new Date(); const formattedDate = `${now.getFullYear()}년 ${ now.getMonth() + 1 }월 ${now.getDate()}일 ${ ["일", "월", "화", "수", "목", "금", "토"][now.getDay()] } ${now.getHours()}:${now.getMinutes().toString().padStart(2, "0")}`; totalViewerResults.textContent = `집계 시각 : ${formattedDate} ${nicknameList} 의 멀티뷰 제외 로그인 시청자 수 : ${multiViewExcludedCount.toLocaleString()}명 ( 멀티뷰 포함 : ${totalViewerNumber.toLocaleString()}, 멀티뷰로 증가한 시청자수 : ${( totalViewerNumber - multiViewExcludedCount ).toLocaleString()}, 멀티뷰 중인 시청자 수 : ${multiSetCount.toLocaleString()} )`; } else { totalViewerResults.textContent = "저장된 시청자 목록이 없습니다."; } }); // 초기화 버튼 핸들러 document.getElementById("resetButton").addEventListener("click", () => { localStorage.clear(); document .getElementById("nicknameResultsTable") .querySelector("tbody").innerHTML = ""; document .getElementById("totalViewerResultsTable") .querySelector("tbody").innerHTML = ""; document.getElementById("totalViewerResults").textContent = "결과가 여기에 표시됩니다."; }); })();