// ==UserScript==
// @name 숲 멀티뷰 시청자 제거기
// @namespace http://tampermonkey.net/
// @version 1.2
// @description 숲 생방송 시청자 목록을 가져오고 멀티뷰를 제거합니다.
// @author asdi
// @match https://play.sooplive.co.kr/*
// @grant 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 = `
<div id="buttonContainer" style="display: flex; justify-content: space-between; margin-bottom: 10px;">
<button id="getNickName" style="padding: 8px 12px; background-color: #007BFF; color: white; border: none; border-radius: 4px; cursor: pointer; flex: 1; margin-right: 5px;">시청자 목록 가져오기</button>
<button id="totalViewerCount" style="padding: 8px 12px; background-color: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; flex: 1; margin-right: 5px;">멀티뷰 중복 제거</button>
<button id="resetButton" style="padding: 8px 12px; background-color: #ffc107; color: #212529; border: none; border-radius: 4px; cursor: pointer; flex: 1; margin-right: 5px;">초기화</button>
<button id="closePopup" style="padding: 8px 12px; background-color: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">닫기</button>
</div>
<div id="nicknameSection">
<table id="nicknameResultsTable" style="width: 100%; margin-top: 10px; border-collapse: collapse;">
<thead>
<tr style="background-color: #e9ecef; text-align: center; font-size: 14px;">
<th>닉네임</th>
<th>시청자 수</th>
<th>로그인</th>
<th>비로그인</th>
<th>비율</th>
<th>중복</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<hr style="margin: 15px 0; border-top: 1px solid #ddd;">
<div id="totalViewerSection">
<table id="totalViewerResultsTable" style="width: 100%; margin-top: 10px; border-collapse: collapse;">
<thead>
<tr style="background-color: #e9ecef; text-align: center; font-size: 14px;">
<th>닉네임</th>
<th>로그인</th>
<th>비로그인</th>
<th>합</th>
<th>멀티뷰</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<div id="totalViewerResults" style="margin-top: 10px; padding: 10px; background-color: #d1e7dd; border-radius: 4px; font-size: 12px; white-space: pre-wrap;">결과가 여기에 표시됩니다.</div>
</div>
`;
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 = `
<td>${nickname}</td>
<td>${viewerNumber.toLocaleString()}</td>
<td>${loggedInCount.toLocaleString()}</td>
<td>${nonLoggedInCount.toLocaleString()}</td>
<td>${loginRatio}%</td>
<td>${duplicateViewCount.toLocaleString()}</td>
`;
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 = `<tr><td colspan="6" style="text-align: center; padding: 10px; color: red;">Error: ${error.message}</td></tr>`;
}
}
});
// 로컬스토리지에서 시청자 목록 불러오기
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 = `
<td>${nickname}</td>
<td>${viewerNumber.toLocaleString()}</td>
<td>${loggedInCount.toLocaleString()}</td>
<td>${nonLoggedInCount.toLocaleString()}</td>
<td>${loginRatio}%</td>
<td>${duplicateViewCount.toLocaleString()}</td>
`;
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 = `
<td>${nickname}</td>
<td>${liveViewerCount.toLocaleString()}</td>
<td>${nonLoggedInCount.toLocaleString()}</td>
<td>${(liveViewerCount + nonLoggedInCount).toLocaleString()}</td>
<td>${(
uniqueUserIdList.length - liveViewerCount
).toLocaleString()}</td>
`;
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 =
"결과가 여기에 표시됩니다.";
});
})();