- // ==UserScript==
- // @name SOOP (숲) - 다시보기 채팅창 부검기
- // @name:ko SOOP (숲) - 다시보기 채팅창 부검기
- // @namespace https://gf.qytechs.cn/ko/scripts/488057
- // @version 20241015
- // @description VOD 채팅창에서 채팅, 별풍선, 대결미션, 도전미션 로그를 다운로드
- // @description:ko VOD 채팅창에서 채팅, 별풍선, 대결미션, 도전미션 로그를 다운로드
- // @author You
- // @match https://vod.sooplive.co.kr/player/*
- // @icon https://res.sooplive.co.kr/afreeca.ico
- // @run-at document-end
- // @license MIT
- // @grant GM_registerMenuCommand
- // ==/UserScript==
-
- (function() {
- 'use strict';
-
- let accumulatedTextData = '';
- let balloonCutoff = 1;
-
- function secondsToHMS(seconds) {
- if(seconds < 0){
- return `[00:00:00]`;
- }
- seconds = Math.floor(seconds);
-
- const hours = Math.floor(seconds / 3600);
- const minutes = Math.floor((seconds % 3600) / 60);
- const remainingSeconds = seconds % 60;
-
- const formattedHours = String(hours).padStart(2, '0');
- const formattedMinutes = String(minutes).padStart(2, '0');
- const formattedSeconds = String(remainingSeconds).padStart(2, '0');
-
- return `[${formattedHours}:${formattedMinutes}:${formattedSeconds}]`;
- }
-
- // XML을 JSON으로 변환하는 함수
- function xmlToJson(xml) {
- var obj = {};
-
- if (xml.nodeType === 1) {
- // element 노드인 경우
- if (xml.attributes.length > 0) {
- obj["@attributes"] = {};
- for (var j = 0; j < xml.attributes.length; j++) {
- var attribute = xml.attributes.item(j);
- obj["@attributes"][attribute.nodeName] = attribute.nodeValue;
- }
- }
- } else if (xml.nodeType === 3) {
- // text 노드인 경우
- obj = xml.nodeValue;
- } else if (xml.nodeType === 4) {
- // CDATA 노드인 경우
- obj = xml.nodeValue;
- }
-
- // CDATA 노드 처리
- if (xml.nodeType === 4) {
- obj = xml.nodeValue;
- }
-
- // 하위 노드가 있는 경우
- if (xml.hasChildNodes()) {
- for (var i = 0; i < xml.childNodes.length; i++) {
- var item = xml.childNodes.item(i);
- var nodeName = item.nodeName;
- if (typeof(obj[nodeName]) === "undefined") {
- obj[nodeName] = xmlToJson(item);
- } else {
- if (typeof(obj[nodeName].push) === "undefined") {
- var old = obj[nodeName];
- obj[nodeName] = [];
- obj[nodeName].push(old);
- }
- obj[nodeName].push(xmlToJson(item));
- }
- }
- }
- return obj;
- }
-
- function removeTextAfterRoot(jsonData) {
- if (!jsonData || typeof jsonData !== 'object') {
- return jsonData;
- }
-
- const rootKeys = Object.keys(jsonData);
-
- // "root" 다음에 바로 오는 "#text"를 제거합니다.
- if (rootKeys.length === 1 && rootKeys[0] === 'root') {
- const rootObj = jsonData.root;
-
- // 만약 "root" 객체 안에 "#text"가 있다면 제거합니다.
- if (rootObj && Array.isArray(rootObj['#text'])) {
- delete rootObj['#text'];
- }
- }
-
- return jsonData;
- }
-
- async function fetchChatData(url) {
- try {
- const response = await fetch(url, {
- cache: "force-cache" // 항상 캐시를 사용하도록 설정
- });
- const data = await response.text();
- const parser = new DOMParser();
- const xmlDoc = parser.parseFromString(data, "text/xml");
- const jsonData = xmlToJson(xmlDoc);
- const modifiedJsonData = removeTextAfterRoot(jsonData);
- return modifiedJsonData;
- } catch (error) {
- console.error('데이터를 불러오는 중 오류가 발생했습니다:', error);
- throw error;
- }
- }
-
- async function retrieveAndLogChatData(url, startTime, cmd, accumulatedTime) {
- try {
- const chatData = await fetchChatData(`${url}&startTime=${startTime}`);
- let textData = '';
-
- switch (true) {
- case (cmd === "getChatLog"):
- textData = convertChatObjToText(chatData, accumulatedTime, '', '');
- break;
- case (cmd === "getBalloonLog"):
- textData = convertBalloonObjToText(chatData, accumulatedTime);
- break;
- case (cmd === "getChallengeMissionLog"):
- textData = convertChallengeMissionObjToText(chatData, accumulatedTime);
- break;
- case (cmd === "getBattleMissionLog"):
- textData = convertBattleMissionObjToText(chatData, accumulatedTime);
- break;
- case cmd.includes("getChatLogByID"):
- textData = convertChatObjToText(chatData, accumulatedTime, cmd.split('getChatLogByID_')[1], '');
- break;
- case cmd.includes("getChatLogByWord"):
- textData = convertChatObjToText(chatData, accumulatedTime, '', cmd.split('getChatLogByWord_')[1]);
- break;
- default:
- console.error('잘못된 명령입니다:', cmd);
- return;
- }
-
- if (textData) {
- accumulatedTextData += textData; // 텍스트 데이터를 누적
- }
- } catch (error) {
- console.error('채팅 데이터를 가져오는 중 오류가 발생했습니다:', error);
- }
- }
-
- function generateFileName(bjid, videoid, cmd) {
- let fileType = "";
- switch (true) {
- case (cmd === "getChatLog"):
- fileType = "채팅_전체";
- break;
- case (cmd === "getBalloonLog"):
- fileType = `별풍선_전체_${balloonCutoff}개이상`;
- break;
- case (cmd === "getChallengeMissionLog"):
- fileType = `도전미션_전체_${balloonCutoff}개이상`;
- break;
- case (cmd === "getBattleMissionLog"):
- fileType = `배틀미션_전체_${balloonCutoff}개이상`;
- break;
- case cmd.includes("getChatLogByID"):
- fileType = `채팅_${cmd.split('getChatLogByID_')[1]}`;
- break;
- case cmd.includes("getChatLogByWord"):
- fileType = `채팅_단어_${cmd.split('getChatLogByWord_')[1]}`;
- break;
- }
- return `${bjid}_${videoid}_${fileType}.txt`;
- }
-
- async function retrieveChatDataForDuration(duration, fileInfoKey, cmd, isLastIteration, accumulatedTime) {
- const url = fileInfoKey.indexOf("clip_") !== -1 ?
- `https://vod-normal-kr-cdn-z01.sooplive.co.kr/${fileInfoKey.split("_").join("/")}_c.xml?type=clip&rowKey=${fileInfoKey}_c` :
- `https://videoimg.sooplive.co.kr/php/ChatLoadSplit.php?rowKey=${fileInfoKey}_c`;
- const bjid = vodCore.config.copyright.user_id || vodCore.config.bjId;
- const filename = generateFileName(bjid, vodCore.config.titleNo, cmd);
- const intervalDuration = 300; // 300초마다 채팅 데이터 가져오기
- let currentSeconds = 0;
-
- while (currentSeconds <= duration) {
- document.title = `채팅 데이터를 받는 중... ${parseInt((currentSeconds+accumulatedTime)/vodCore.config.totalFileDuration*100)}%`;
- await retrieveAndLogChatData(url, currentSeconds, cmd, accumulatedTime);
- currentSeconds += intervalDuration;
-
- if (currentSeconds > duration && isLastIteration) {
- // 마지막 반복이면서 현재 시간이 지속 시간을 초과하면 저장
- if(accumulatedTextData.length > 0) {
- saveTextToFile(accumulatedTextData, filename)
- } else {
- alert('저장할 데이터가 없습니다.');
- }
- }
- }
- }
-
- async function saveTextToFile(textData, fileName) {
- const blob = new Blob([textData], { type: 'text/plain' });
- const blobUrl = URL.createObjectURL(blob);
- const link = document.createElement('a');
- link.href = blobUrl;
- link.download = fileName;
- link.click();
- URL.revokeObjectURL(blobUrl);
- }
-
- function convertChatObjToText(jsonData, accumulatedTime, targetid, targetword) {
-
- if (Array.isArray(jsonData.root.chat)) {
- // 배열일 경우
-
- const chatArray = jsonData.root.chat;
- let text = '';
-
- chatArray.forEach(chatObj => {
- const t = chatObj.t ? secondsToHMS(parseFloat(chatObj.t['#text']) + accumulatedTime) : '';
- const u = chatObj.u ? chatObj.u['#text'].split('(')[0] : '';
- const n = chatObj.n ? chatObj.n['#cdata-section'] : '';
- const m = chatObj.m ? chatObj.m['#cdata-section'] : '';
-
- if(targetid.length > 0){
- if(targetid === u) text += `${t} ${n}(${u}): ${m}\n`;
- } else if(targetword.length > 0){
- if(m.includes(targetword)) text += `${t} ${n}(${u}): ${m}\n`;
- } else {
- text += `${t} ${n}(${u}): ${m}\n`;
- }
-
- });
-
- return text;
- } else if (typeof jsonData.root.chat === 'object') {
- // 객체일 경우
-
- const chatObj = jsonData.root.chat;
- let text = '';
-
- const t = chatObj.t ? secondsToHMS(parseFloat(chatObj.t['#text']) + accumulatedTime) : '';
- const u = chatObj.u ? chatObj.u['#text'].split('(')[0] : '';
- const n = chatObj.n ? chatObj.n['#cdata-section'] : '';
- const m = chatObj.m ? chatObj.m['#cdata-section'] : '';
-
- if(targetid.length > 0){
- if(targetid === u) text += `${t} ${n}(${u}): ${m}\n`;
- } else if(targetword.length > 0){
- if(m.includes(targetword)) text += `${t} ${n}(${u}): ${m}\n`;
- } else {
- text += `${t} ${n}(${u}): ${m}\n`;
- }
-
- return text;
- } else {
- return '';
- }
- }
-
- function convertBalloonObjToText(jsonData, accumulatedTime) {
-
- if (Array.isArray(jsonData.root.balloon)) {
- // 배열일 경우
- const balloonArray = jsonData.root.balloon;
- let text = '';
-
- balloonArray.forEach(balloonObj => {
- const t = balloonObj.t ? secondsToHMS(parseFloat(balloonObj.t['#text']) + accumulatedTime) : '';
- const u = balloonObj.u ? balloonObj.u['#text'].split('(')[0] : '';
- const n = balloonObj.n ? balloonObj.n['#cdata-section'] : '';
- const c = balloonObj.c ? balloonObj.c['#text'] : '';
-
- if(balloonCutoff <= parseInt(c)) text += `${t} ${n}(${u}): ${c}\n`;
- });
-
- return text;
- } else if (typeof jsonData.root.balloon === 'object') {
- // 객체일 경우
- const balloonObj = jsonData.root.balloon;
- let text = '';
-
- const t = balloonObj.t ? secondsToHMS(parseFloat(balloonObj.t['#text']) + accumulatedTime) : '';
- const u = balloonObj.u ? balloonObj.u['#text'].split('(')[0] : '';
- const n = balloonObj.n ? balloonObj.n['#cdata-section'] : '';
- const c = balloonObj.c ? balloonObj.c['#text'] : '';
-
- if(balloonCutoff <= parseInt(c)) text += `${t} ${n}(${u}): ${c}\n`;
-
- return text;
- } else {
- return '';
- }
- }
-
- function convertChallengeMissionObjToText(jsonData, accumulatedTime) {
-
- if (Array.isArray(jsonData.root.challenge_mission)) {
- // 배열일 경우
- const challengeMissionArray = jsonData.root.challenge_mission;
- let text = '';
-
- challengeMissionArray.forEach(cmObj => {
- const t = cmObj.t ? secondsToHMS(parseFloat(cmObj.t['#text']) + accumulatedTime) : '';
- const u = cmObj.u ? cmObj.u['#text'].split('(')[0] : '';
- const n = cmObj.n ? cmObj.n['#cdata-section'] : '';
- const c = cmObj.c ? cmObj.c['#text'] : '';
- const title = cmObj.title ? cmObj.title['#cdata-section'] : '';
-
- if(balloonCutoff <= parseInt(c)) text += `${t} ${n}(${u}): ${c}, ${title}\n`;
- });
-
- return text;
- } else if (typeof jsonData.root.challenge_mission === 'object') {
- // 객체일 경우
- const cmObj = jsonData.root.challenge_mission;
- let text = '';
-
- const t = cmObj.t ? secondsToHMS(parseFloat(cmObj.t['#text']) + accumulatedTime) : '';
- const u = cmObj.u ? cmObj.u['#text'].split('(')[0] : '';
- const n = cmObj.n ? cmObj.n['#cdata-section'] : '';
- const c = cmObj.c ? cmObj.c['#text'] : '';
- const title = cmObj.title ? cmObj.title['#cdata-section'] : '';
-
- if(balloonCutoff <= parseInt(c)) text += `${t} ${n}(${u}): ${c}, ${title}\n`;
- return text;
- } else {
- return '';
- }
-
- }
-
- function convertBattleMissionObjToText(jsonData, accumulatedTime) {
-
- if (Array.isArray(jsonData.root.battle_mission)) {
- // 배열일 경우
- const battleMissionArray = jsonData.root.battle_mission;
- let text = '';
-
- battleMissionArray.forEach(bmObj => {
- const t = bmObj.t ? secondsToHMS(parseFloat(bmObj.t['#text']) + accumulatedTime) : '';
- const u = bmObj.u ? bmObj.u['#text'].split('(')[0] : '';
- const n = bmObj.n ? bmObj.n['#cdata-section'] : '';
- const c = bmObj.c ? bmObj.c['#text'] : '';
- const title = bmObj.title ? bmObj.title['#cdata-section'] : '';
-
- if(balloonCutoff <= parseInt(c)) text += `${t} ${n}(${u}): ${c}, ${title}\n`;
- });
-
- return text;
- } else if (typeof jsonData.root.battle_mission === 'object') {
- // 객체일 경우
- const bmObj = jsonData.root.battle_mission;
- let text = '';
-
- const t = bmObj.t ? secondsToHMS(parseFloat(bmObj.t['#text']) + accumulatedTime) : '';
- const u = bmObj.u ? bmObj.u['#text'].split('(')[0] : '';
- const n = bmObj.n ? bmObj.n['#cdata-section'] : '';
- const c = bmObj.c ? bmObj.c['#text'] : '';
- const title = bmObj.title ? bmObj.title['#cdata-section'] : '';
-
- if(balloonCutoff <= parseInt(c)) text += `${t} ${n}(${u}): ${c}, ${title}\n`;
-
- return text;
- } else {
- return '';
- }
-
- }
-
- // 변수가 정의될 때까지 시도하는 함수
- function waitForVariable() {
- return new Promise((resolve, reject) => {
- let elapsedTime = 0; // 경과 시간 변수 초기화
-
- const interval = setInterval(() => {
- elapsedTime += 1000; // 1초씩 경과 시간 증가
-
- // 변수가 정의되었는지 확인
- if (typeof vodCore !== 'undefined' && vodCore !== null) {
- clearInterval(interval); // 변수가 정의되면 setInterval 중지
- resolve(vodCore); // Promise를 성공 상태로 전이
- }
-
- // 최대 20초까지 기다린 후에도 변수가 정의되지 않으면 중단
- if (elapsedTime >= 20000) {
- clearInterval(interval); // 지정된 시간이 경과하면 setInterval 중지
- reject(new Error('변수가 20초 안에 선언되지 않았습니다.')); // Promise를 거부 상태로 전이
- }
- }, 1000); // 1초마다 변수 확인
- });
- }
-
- async function getChatLog(cmd) {
- try {
- accumulatedTextData = '';
- let accumulatedTime = 0;
- const vodCore = await waitForVariable();
- const itemsCount = vodCore.fileItems.length;
- for (const [index, item] of vodCore.fileItems.entries()) {
- const startTime = performance.now(); // 요청 시작 시간 기록
- const isLastIteration = index === itemsCount - 1; // 현재 아이템이 마지막 아이템인지 확인
- await retrieveChatDataForDuration(item.duration, item.fileInfoKey, cmd, isLastIteration, accumulatedTime);
- accumulatedTime += parseInt(item.duration);
- const endTime = performance.now(); // 요청 종료 시간 기록
- const elapsedTime = endTime - startTime; // 요청에 걸린 시간 계산
-
- // 만약 요청에 걸린 시간이 500ms를 초과하지 않으면 남은 시간을 기다리지 않고 다음으로 넘어갑니다.
- if (elapsedTime < 500) {
- await new Promise(resolve => setTimeout(resolve, 500 - elapsedTime));
- }
-
- }
- document.title = '모든 작업이 완료되었습니다.';
- } catch (error) {
- console.error('전체 프로세스 중 오류 발생:', error);
- }
- }
-
- GM_registerMenuCommand('전체 채팅 로그 저장', function() {
- getChatLog("getChatLog");
- });
- GM_registerMenuCommand('전체 별풍선 로그 저장', function() {
- var balloonCutoffInput = prompt('몇 개 이상의 별풍선만 기록할까요?', 1);
- if (parseInt(balloonCutoffInput) > 0){
- balloonCutoff = balloonCutoffInput;
- getChatLog("getBalloonLog");
- }
- });
- GM_registerMenuCommand('전체 도전 미션 로그 저장', function() {
- var balloonCutoffInput = prompt('몇 개 이상의 별풍선만 기록할까요?', 1);
- if (parseInt(balloonCutoffInput) > 0){
- balloonCutoff = balloonCutoffInput;
- getChatLog("getChallengeMissionLog");
- }
- });
- GM_registerMenuCommand('전체 대결 미션 로그 저장', function() {
- var balloonCutoffInput = prompt('몇 개 이상의 별풍선만 기록할까요?', 1);
- if (parseInt(balloonCutoffInput) > 0){
- balloonCutoff = balloonCutoffInput;
- getChatLog("getBattleMissionLog");
- }
- });
- GM_registerMenuCommand('특정 ID 채팅 로그 저장', function() {
- var targetUseridInput = prompt('ID를 입력하세요', '');
- if (targetUseridInput.length > 0){
- const targetUserid = targetUseridInput.split('(')[0];
- getChatLog(`getChatLogByID_${targetUserid}`);
- }
- });
- GM_registerMenuCommand('내 채팅 로그 저장', function() {
- var myidInput = vodCore.config.loginId;
- if (myidInput && myidInput.length > 0){
- const targetUserid = myidInput.split('(')[0];
- getChatLog(`getChatLogByID_${targetUserid}`);
- } else {
- alert('로그인 상태가 아닙니다.');
- }
- });
- GM_registerMenuCommand('특정 단어를 포함한 채팅 로그 저장', function() {
- var targetWordInput = prompt('단어를 입력하세요', '');
- if (targetWordInput && targetWordInput.length > 0){
- const targetWord = targetWordInput;
- getChatLog(`getChatLogByWord_${targetWord}`);
- }
- });
-
- })();