// ==UserScript== // @name SOOP Time Recorder // @namespace http://tampermonkey.net/ // @version 1.13 // @description SOOP 라이브 및 VOD에서 타임스탬프 기록 및 관리 // @author result41 // @match https://play.sooplive.com/* // @match https://stbbs.sooplive.com/vodclip/index.php/* // @match https://vod.sooplive.com/* // @grant GM_setValue // @grant GM_getValue // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/559104/SOOP%20Time%20Recorder.user.js // @updateURL https://update.greasyfork.icu/scripts/559104/SOOP%20Time%20Recorder.meta.js // ==/UserScript== (function() { 'use strict'; let historyWindow = null; const HOST = window.location.host; const IS_CLIP_POPUP = HOST === 'stbbs.sooplive.com'; const IS_VOD = HOST === 'vod.sooplive.com'; function showToast(message) { const existingToast = document.getElementById('soop_custom_toast'); if (existingToast) { existingToast.remove(); } if (IS_CLIP_POPUP) return; const toast = document.createElement('div'); toast.id = 'soop_custom_toast'; toast.style.position = 'fixed'; toast.style.bottom = '100px'; toast.style.left = '50%'; toast.style.transform = 'translateX(-50%)'; toast.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; toast.style.color = '#fff'; toast.style.padding = '10px 20px'; toast.style.borderRadius = '5px'; toast.style.zIndex = '999999'; toast.style.opacity = '0'; toast.style.transition = 'opacity 0.3s ease-in-out'; toast.style.fontSize = '14px'; toast.innerText = message; document.documentElement.appendChild(toast); setTimeout(() => { toast.style.opacity = '1'; }, 10); setTimeout(() => { toast.style.opacity = '0'; toast.addEventListener('transitionend', () => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } }); }, 3000); } // --- 통합 BJ ID 추출 함수 --- function getCurrentBjId() { if (IS_VOD) { const thumbLink = document.querySelector('.thumbnail_box .thumb'); if (thumbLink && thumbLink.href) { const parts = thumbLink.href.split('/').filter(part => part.length > 0); return parts[parts.length - 1]; } return null; } const urlMatch = window.location.href.match(/bj_id=([^&]+)/); if (urlMatch && urlMatch[1]) { return urlMatch[1]; } const pathMatch = window.location.pathname.match(/^\/([^\/]+)\/\d+/); if (pathMatch && pathMatch[1]) { return pathMatch[1]; } return null; } function getStorageKey(bjId) { return `soop_record_${bjId || 'default'}`; } function getCleanedHistory(bjId) { const key = getStorageKey(bjId); let storedData = JSON.parse(GM_getValue(key, '[]')); if (!Array.isArray(storedData)) { storedData = []; } const now = Date.now(); const validHistory = storedData.filter(item => { return !item.expiry || Number(item.expiry) > now; }); if (validHistory.length !== storedData.length) { GM_setValue(key, JSON.stringify(validHistory)); } return validHistory; } function setHistory(bjId, historyArray) { const key = getStorageKey(bjId); GM_setValue(key, JSON.stringify(historyArray)); } function deleteHistory() { if (!confirm("정말 모든 기록을 삭제하시겠습니까?")) { return; } const bjId = getCurrentBjId(); const key = getStorageKey(bjId); GM_setValue(key, null); showToast(`✔ 기록이 모두 삭제되었습니다.`); if (historyWindow && !historyWindow.closed) { historyWindow.close(); historyWindow = null; } } // --------------------------------------------- if (IS_CLIP_POPUP) { function waitForPopupElements() { const bjIdQuery = getCurrentBjId(); if (!bjIdQuery) return; let attempts = 0; const maxAttempts = 20; const interval = setInterval(() => { const titleElement = document.querySelector('.u_clip_title '); attempts++; if (titleElement) { clearInterval(interval); displayClipTimer(bjIdQuery, titleElement); } else if (attempts >= maxAttempts) { clearInterval(interval); } }, 500); } function displayClipTimer(bjId, titleElement) { const history = getCleanedHistory(bjId); if (history.length === 0) return; const latestRecord = history[history.length - 1]; if (!latestRecord.expiry) return; // 명시적 형변환 추가 const expiryTime = Number(latestRecord.expiry); const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000; const savedTimeMs = expiryTime - THIRTY_DAYS_MS; const now = Date.now(); const TEN_MINUTES_MS = 10 * 60 * 1000; const timeElapsed = now - savedTimeMs; if (timeElapsed >= 0 && timeElapsed < TEN_MINUTES_MS) { const timeLeftMs = TEN_MINUTES_MS - timeElapsed; const minutes = Math.floor(timeLeftMs / 60000); const seconds = Math.floor((timeLeftMs % 60000) / 1000); const timeString = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; const timerSpan = document.createElement('span'); timerSpan.style.marginRight = '10px'; timerSpan.style.fontWeight = 'bold'; timerSpan.style.color = '#8e958c'; timerSpan.innerText = `⏳ 마지막 Record 시간: ${timeString}`; titleElement.parentNode.insertBefore(timerSpan, titleElement); } } waitForPopupElements(); return; } // --- 메인 및 VOD 페이지 로직 --- const MAX_ATTEMPTS = 40; let attempts = 0; const loadInterval = setInterval(() => { if (attempts >= MAX_ATTEMPTS) { clearInterval(loadInterval); return; } if (IS_VOD) { const playerLists = document.querySelectorAll('.player_item_list'); if (playerLists.length > 0) { let injectedCount = 0; playerLists.forEach(list => { if (!list.querySelector('.soop_custom_buttons_vod')) { createButtons(list, 'vod_page'); } if (list.querySelector('.soop_custom_buttons_vod')) { injectedCount++; } }); if (injectedCount === playerLists.length) { clearInterval(loadInterval); return; } } } else { const broadState = document.querySelector('#broadInfo #broadState'); if (broadState && !document.getElementById('soop_custom_buttons')) { clearInterval(loadInterval); createButtons(broadState, 'broadState'); return; } const broadcastArea = document.querySelector('#broadcastArea'); if (broadcastArea && !document.getElementById('soop_custom_buttons')) { if (!broadState) { clearInterval(loadInterval); createButtons(broadcastArea, 'broadcastArea'); return; } } } attempts++; }, 500); // ---------------------------------------------------- window.addEventListener('message', (event) => { if (!event.data || event.data.source !== 'soop_time_recorder_popup') { return; } const { action, value, historyUpdates, id, newTime } = event.data; const bjId = getCurrentBjId(); switch (action) { case 'SAVE_ALL_MEMOS': if (historyUpdates && Array.isArray(historyUpdates)) { const currentHistory = getCleanedHistory(bjId); historyUpdates.forEach(update => { const index = currentHistory.findIndex(item => item.id === update.id); if (index > -1) { currentHistory[index].memo = update.memo; } }); setHistory(bjId, currentHistory); if (value === 'COPYING') { if (historyWindow && !historyWindow.closed && historyWindow.startCopy) { historyWindow.startCopy(); } } } break; case 'DELETE_SINGLE': if (id) { let currentHistory = getCleanedHistory(bjId); const originalLength = currentHistory.length; currentHistory = currentHistory.filter(item => item.id !== id); if (currentHistory.length !== originalLength) { setHistory(bjId, currentHistory); showToast("🗑️ 항목이 삭제되었습니다."); } } break; case 'UPDATE_TIME': if (id && newTime) { const currentHistory = getCleanedHistory(bjId); const index = currentHistory.findIndex(item => item.id === id); if (index > -1) { currentHistory[index].videoTime = newTime; setHistory(bjId, currentHistory); showToast(`✏️ 시간이 수정되었습니다: ${newTime}`); } } break; } }); // ---------------------------------------------------- // --- 버튼 및 팝업 관련 함수 정의 --- function createButtons(targetElement, insertType) { const container = document.createElement('span'); const btnRecord = document.createElement('button'); btnRecord.innerText = 'Record'; styleButton(btnRecord, '#4CAF50'); btnRecord.onclick = recordTime; const btnHistory = document.createElement('button'); btnHistory.innerText = 'History'; styleButton(btnHistory, '#2196F3'); btnHistory.onclick = openHistoryWindow; const btnDelete = document.createElement('button'); btnDelete.innerText = 'Delete All'; styleButton(btnDelete, '#f44336'); btnDelete.onclick = deleteHistory; if (insertType === 'broadState') { container.id = 'soop_custom_buttons'; container.style.marginLeft = '10px'; container.style.display = 'inline-flex'; container.style.alignItems = 'center'; container.style.gap = '5px'; container.style.position = 'relative'; container.appendChild(btnRecord); container.appendChild(btnHistory); container.appendChild(btnDelete); targetElement.parentNode.insertBefore(container, targetElement.nextSibling); } else if (insertType === 'broadcastArea') { container.id = 'soop_custom_buttons'; container.style.display = 'block'; container.style.position = 'absolute'; container.style.top = '10px'; container.style.right = '10px'; container.style.zIndex = '10'; const innerFlex = document.createElement('div'); innerFlex.style.display = 'flex'; innerFlex.style.gap = '5px'; container.appendChild(innerFlex); innerFlex.appendChild(btnRecord); innerFlex.appendChild(btnHistory); innerFlex.appendChild(btnDelete); targetElement.appendChild(container); } else if (insertType === 'vod_page') { container.className = 'soop_custom_buttons_vod'; container.style.display = 'inline-block'; container.style.marginRight = '10px'; container.style.verticalAlign = 'middle'; container.appendChild(btnHistory); container.appendChild(document.createTextNode(' ')); container.appendChild(btnDelete); btnHistory.style.marginRight = '4px'; targetElement.insertBefore(container, targetElement.firstChild); } } function recordTime() { const timeElement = document.querySelector('#broadInfo #time'); let currentTime = "00:00:00"; if (timeElement) { currentTime = timeElement.innerText.trim(); } else { showToast("시간 정보를 찾을 수 없습니다."); return; } if (currentTime === "00:00:00") { showToast("시간 정보를 찾을 수 없습니다."); return; } const now = new Date(); const timestamp = now.toLocaleTimeString(); const bjId = getCurrentBjId(); if (!bjId) { showToast("방송인 ID를 찾을 수 없어 기록할 수 없습니다."); return; } const key = getStorageKey(bjId); const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000; const expiryTime = now.getTime() + THIRTY_DAYS_MS; const history = getCleanedHistory(bjId); history.push({ id: Date.now().toString() + Math.random().toString(36).substring(2, 9), videoTime: currentTime, memo: "", savedAt: timestamp, expiry: expiryTime }); setHistory(bjId, history); showToast(`✔ 타임스탬프 저장됨: ${currentTime} `); if (historyWindow && !historyWindow.closed) { historyWindow.close(); historyWindow = null; openHistoryWindow(); } } function openHistoryWindow() { if (historyWindow && !historyWindow.closed) { historyWindow.focus(); return; } const bjId = getCurrentBjId(); if (!bjId) { showToast("BJ ID를 찾을 수 없어 History를 열 수 없습니다."); return; } const currentHistory = getCleanedHistory(bjId); const historyJson = JSON.stringify(currentHistory); // 데이터 이스케이프 간소화 (안전하게 스크립트 블록 내부에 주입) const escapedHistoryJson = historyJson .replace(/\\/g, '\\\\') .replace(/'/g, "\\'") .replace(/<\/script/gi, '<\\/script'); historyWindow = window.open('', 'SOOP_Time_History', 'width=450,height=550,scrollbars=no,resizable=yes'); if (!historyWindow) { alert('팝업 창이 차단되었습니다. 팝업 차단을 해제해 주세요.'); return; } const doc = historyWindow.document; doc.title = 'SOOP 타임스탬프 기록'; doc.write(`

SOOP 타임스탬프 기록 (${bjId})

`); doc.close(); } function styleButton(btn, bgColor) { btn.style.backgroundColor = bgColor; btn.style.color = 'white'; btn.style.border = 'none'; btn.style.padding = '4px 8px'; btn.style.borderRadius = '3px'; btn.style.cursor = 'pointer'; btn.style.fontSize = '12px'; btn.style.fontWeight = 'bold'; btn.style.fontFamily = 'dotum, sans-serif'; } })();