// ==UserScript== // @name SOOP(숲) - 게시글/다시보기 댓글 엑셀파일로 추출 // @namespace https://greasyfork.org/ko/scripts/520675 // @version 20250302 // @description SOOP 채널의 게시글이나 다시보기에서 댓글, 답글을 추출하여 엑셀파일로 저장합니다. // @author 0hawawa // @match https://vod.sooplive.co.kr/player/* // @include https://ch.sooplive.co.kr/*/post/* // @icon https://res.sooplive.co.kr/afreeca.ico // @grant GM_registerMenuCommand // @require https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; const CHAPI = String(atob("aHR0cHM6Ly9jaGFwaS5zb29wbGl2ZS5jby5rci9hcGk=")); // 모든 댓글 저장하는 리스트 let commentData = []; // 게시글 정보 async function getTitleName(streamerId, title_no){ try { const r = await fetch( `${CHAPI}/${streamerId}/title/${title_no}` ); const d = await r.json(); return d.title_name; } catch(e){ console.log(e); alert(e); } } // 댓글 수, 마지막 페이지 수 async function getCommentInfo(streamerId, title_no){ try { const r = await fetch( `${CHAPI}/${streamerId}/title/${title_no}/comment` ); const d = await r.json(); return d.meta.last_page } catch (e){ console.log(e); alert(e); } } // 댓글 처리 async function processComment(comment, isReply = false){ const { p_comment_no: pComntNo, c_comment_no: cComntNo, is_best_top: isBestTop, user_nick: userNick, user_id: userId, comment: comntTxt, like_cnt: likeCnt, reg_date: time, } = comment; const { is_manager: isManager, is_top_fan: isTopFan, is_fan: isFan, is_subscribe: isSubs, is_support: isSupp } = comment.badge || {}; commentData.push({ pComntNo: isReply ? ' └' : pComntNo, cComntNo, isBestTop: isBestTop === true ? '✔️' : '', userNick, userId, comntTxt, likeCnt, time, isManager: isManager === 1 ? '✔️' : '', isTopFan: isTopFan === 1 ? '✔️' : '', isFan: isFan === 1 ? '✔️' : '', isSubs: isSubs === 1 ? '✔️' : '', isSupp: isSupp === 1 ? '✔️' : '' }); } // 답글처리 async function handleReplies(id, title_no, pCommentNo){ try{ await fetch( `${CHAPI}/${id}/title/${title_no}/comment/${pCommentNo}/reply` ) .then(r => r.json()) .then(d => d.data.forEach( reply => processComment(reply, true) )) } catch(e){ console.log(e); alert(e); } } // 댓글정리 async function handleComments(d, id, title_no){ for (let comment of d.data){ await processComment(comment); if (comment.c_comment_cnt > 0){ await handleReplies(id, title_no, comment.p_comment_no); } } } async function dataToExcel(id, title_no){ try{ let progress = 0; const titleName = await getTitleName(id, title_no); const lastPage = await getCommentInfo(id, title_no); for (let page = 1; page <= lastPage; page++) { try{ await fetch(`${CHAPI}/${id}/title/${title_no}/comment?page=${page}`) .then(r => r.json()) .then(d => handleComments(d, id, title_no)) progress = ((page / lastPage) * 100).toFixed(2); console.log(`진행률: ${progress}%`); document.title = `진행률: ${progress}% - 댓글 추출 중`; } catch(e) { console.log(e); alert(e); } } const formattedData = commentData.map((comment, index) => ({ "순번": index + 1, "댓글번호": comment.pComntNo, "답글번호": comment.cComntNo, "인기댓글": comment.isBestTop, "닉네임": comment.userNick, "아이디": comment.userId, "댓글": comment.comntTxt, "좋아요": comment.likeCnt, "등록시간": comment.time, "매니저": comment.isManager, "열혈팬": comment.isTopFan, "정기구독": comment.isSubs, "팬가입": comment.isFan, "서포터": comment.isSupp })); const workbook = XLSX.utils.book_new(); const worksheet = XLSX.utils.json_to_sheet(formattedData); XLSX.utils.book_append_sheet(workbook, worksheet, "댓글"); const excelFileName = `${id}_${titleName}_댓글.xlsx`; // Excel 파일을 브라우저에서 다운로드 const wbout = XLSX.write(workbook, { bookType: 'xlsx', type: 'binary' }); const buffer = new ArrayBuffer(wbout.length); const view = new Uint8Array(buffer); for (let i = 0; i < wbout.length; i++) { view[i] = wbout.charCodeAt(i) & 0xFF; } const blob = new Blob([view], { type: "application/octet-stream" }); // FileSaver.js를 사용하여 파일 다운로드 saveAs(blob, excelFileName); if(parseFloat(progress) === 100.00){ document.title = "댓글 다운로드 완료!"; alert("댓글 다운로드 완료!"); } } catch (e){ console.error("파일 저장에 실패했습니다.", e); document.title = "파일 저장 실패"; alert("파일 저장 중 오류가 발생했습니다. 다시 시도해주세요."); } } function find_streamer_ID() { const element = document.querySelector('#player_area > div.wrapping.player_bottom > div > div:nth-child(1) > div.thumbnail_box > a'); const href = element.getAttribute('href'); streamerId = href.split('/')[3]; console.log('[스트리머 ID찾는 중 ...]'); if (streamerId === null || streamerId === 'N/A'){} else{ observer.disconnect(); console.log(`[DOM감지 종료!!] 스트리머 ID: ${streamerId}`); return streamerId; } } const currentUrl = new URL(window.location.href); const pathname = currentUrl.pathname; let streamerId = null; let title_no = null; const observer = new MutationObserver(find_streamer_ID); if(pathname.startsWith('/player/')){ title_no = pathname.split('/')[2]; observer.observe(document.body, { childList: true, subtree: true }); } else if (pathname.includes('/post/')){ streamerId = pathname.split('/')[1]; title_no = pathname.split('/')[3]; } async function main(){ if(streamerId === null){ streamerId = find_streamer_ID(); } console.log(`[스트리머 ID: ${streamerId}]\n[타이틀 번호: ${title_no}]`); commentData = []; await dataToExcel(streamerId, title_no); } GM_registerMenuCommand('Excel로 댓글 추출하기', function() { main(); }); })();