// ==UserScript==
// @name SOOP (숲) - m3u8 링크 복사 버튼 추가
// @namespace http://tampermonkey.net/
// @version 1.0
// @description SOOP 방송 페이지 플레이어 하단 정보 바의 가장 왼쪽에 m3u8 링크 복사 버튼을 추가하고, 페이지 테마에 따라 디자인을 조정합니다.
// @author Your Name
// @icon https://www.google.com/s2/favicons?sz=256&domain=sooplive.co.kr
// @match https://play.sooplive.co.kr/*/*
// @exclude https://play.sooplive.co.kr/*/embed*
// @grant GM_setClipboard
// @grant GM_addStyle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// --- 유틸리티 함수들 ---
const getBroadAid2 = async (id, broadNumber) => {
const data = {
bid: id, bno: broadNumber, from_api: '0', mode: 'landing',
player_type: 'html5', pwd: '', stream_type: 'common',
quality: 'original', type: 'aid'
};
const requestOptions = {
method: 'POST', body: new URLSearchParams(data), credentials: 'include'
};
try {
const response = await fetch('https://live.sooplive.co.kr/afreeca/player_live_api.php', requestOptions);
const result = await response.json();
// console.log("[getBroadAid2] AID 응답:", result);
return result.CHANNEL.AID || null;
} catch (error) {
console.error('[getBroadAid2] AID 가져오기 오류:', error);
return null;
}
};
function getCurrentBroadcastInfoFromUrl() {
const pathParts = window.location.pathname.split('/');
if (pathParts.length >= 3 && pathParts[1] && pathParts[2] && !isNaN(parseInt(pathParts[2]))) {
const userId = pathParts[1];
const broadNo = parseInt(pathParts[2]);
return { userId, broadNo };
}
console.error("[getCurrentBroadcastInfoFromUrl] URL에서 방송 정보를 가져올 수 없습니다.");
return null;
}
function showToastMessage(message, autoHide = true) {
let toast = document.querySelector('.m3u8-toast-message-userscript');
if (toast) { toast.remove(); }
toast = document.createElement('div');
toast.className = 'm3u8-toast-message-userscript';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => { toast.classList.add('show'); }, 10);
if (autoHide) {
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } }, 500);
}, 3000);
}
}
// --- 전역 변수 ---
let currentBroadcastInfo = null;
let copyButtonLiElement = null;
let copyButtonElement = null;
// --- SVG 아이콘 ---
const linkIconSvg = `
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
`;
// --- 테마 및 스타일 관련 함수 ---
function getCurrentTheme() {
return document.documentElement.getAttribute('dark') === 'true' ? 'dark' : 'light';
}
function updateButtonAppearance() {
if (!copyButtonLiElement || !copyButtonElement) return;
const theme = getCurrentTheme();
const svgIcon = copyButtonElement.querySelector('svg');
if (!svgIcon) return;
if (theme === 'dark') {
copyButtonLiElement.style.backgroundColor = 'rgba(70, 70, 77, 0.8)'; // 다크 모드 버튼 배경 (약간 투명도 조절 가능)
svgIcon.style.stroke = '#e0e0e0'; // 다크 모드 아이콘 색상
copyButtonElement.onmouseover = () => {
copyButtonLiElement.style.backgroundColor = 'rgba(85, 85, 92, 0.9)';
svgIcon.style.stroke = '#ffffff';
};
copyButtonElement.onmouseout = () => {
copyButtonLiElement.style.backgroundColor = 'rgba(70, 70, 77, 0.8)';
svgIcon.style.stroke = '#e0e0e0';
};
} else { // light theme
copyButtonLiElement.style.backgroundColor = 'rgba(225, 226, 230, 0.9)'; // 라이트 모드 버튼 배경
svgIcon.style.stroke = '#4a4a52'; // 라이트 모드 아이콘 색상
copyButtonElement.onmouseover = () => {
copyButtonLiElement.style.backgroundColor = 'rgba(205, 206, 212, 1)';
svgIcon.style.stroke = '#1a1a1c';
};
copyButtonElement.onmouseout = () => {
copyButtonLiElement.style.backgroundColor = 'rgba(225, 226, 230, 0.9)';
svgIcon.style.stroke = '#4a4a52';
};
}
}
function applyGlobalStyles() {
if (document.getElementById('m3u8UserScriptGlobalStyle')) return;
const styleElement = document.createElement('style');
styleElement.id = 'm3u8UserScriptGlobalStyle';
styleElement.textContent = `
#m3u8CopyButtonUserScriptItem {
display: inline-flex; /* 내부 버튼 정렬을 위해 flex로 변경 */
align-items: center;
justify-content: center;
vertical-align: middle;
margin-right: 8px; /* 오른쪽 아이콘과의 간격 (가장 왼쪽이므로 margin-left는 불필요) */
border-radius: 50%;
width: 30px;
height: 30px;
transition: background-color 0.2s ease;
/* SOOP의 다른 li.donation, li.adballoon 등과 유사한 스타일을 위해 클래스 추가 고려 */
/* 예: buttonItem.classList.add("donation"); copyButtonLiElement.classList.add("m3u8-custom-icon"); */
}
#m3u8CopyButtonUserScriptButton {
background-color: transparent !important;
border: none !important;
cursor: pointer !important;
padding: 0 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: 100%;
height: 100%;
border-radius: 50%;
}
#m3u8CopyButtonUserScriptButton svg {
width: 18px !important;
height: 18px !important;
vertical-align: middle;
transition: stroke 0.2s ease;
}
.m3u8-toast-message-userscript {
position: fixed; bottom: 70px; left: 50%; transform: translateX(-50%);
background-color: rgba(0, 0, 0, 0.85); color: white; padding: 12px 22px;
border-radius: 6px; z-index: 9999999; font-size: 14px; opacity: 0;
transition: opacity 0.4s ease-in-out; box-shadow: 0 3px 8px rgba(0,0,0,0.3);
text-align: center;
}
.m3u8-toast-message-userscript.show { opacity: 1; }
`;
document.head.appendChild(styleElement);
}
// --- 핵심 로직 ---
async function handleCopyButtonClick() {
if (!currentBroadcastInfo) { showToastMessage("방송 정보를 가져올 수 없습니다."); return; }
showToastMessage("m3u8 링크 추출 중...", false);
const aid = await getBroadAid2(currentBroadcastInfo.userId, currentBroadcastInfo.broadNo);
if (aid) {
const m3u8Link = `https://live-global-cdn-v02.sooplive.co.kr/live-stm-12/auth_playlist.m3u8?aid=${aid}`;
GM_setClipboard(m3u8Link, 'text');
showToastMessage("m3u8 링크가 클립보드에 복사되었습니다.");
} else {
showToastMessage("m3u8 링크 추출에 실패했습니다. (AID 오류)");
}
}
function ensureCopyLinkButton() {
if (!currentBroadcastInfo) return;
// 대상 위치 탐색 (우선순위: column[number="2"] -> column[number="1"] player_item_list -> column[number="1"] depend_item)
let itemListContainer = document.querySelector('.broadcast_information .column[number="2"] .player_item_list ul');
if (!itemListContainer) {
itemListContainer = document.querySelector('.broadcast_information .column[number="1"] .player_item_list ul');
}
if (!itemListContainer) {
itemListContainer = document.querySelector('.broadcast_information .column[number="1"] ul.depend_item');
}
if (!itemListContainer) {
// console.warn("[ensureCopyLinkButton] 대상 아이콘 목록 ul을 찾을 수 없어 실행 중단.");
return;
}
copyButtonLiElement = document.getElementById('m3u8CopyButtonUserScriptItem');
if (!copyButtonLiElement || !itemListContainer.contains(copyButtonLiElement)) {
if (copyButtonLiElement && !itemListContainer.contains(copyButtonLiElement)) {
copyButtonLiElement.remove();
}
copyButtonLiElement = document.createElement('li');
copyButtonLiElement.id = 'm3u8CopyButtonUserScriptItem';
// SOOP의 다른 아이콘 li 요소들(예: li.donation)의 클래스를 참고하여 추가하면 레이아웃에 도움이 될 수 있습니다.
// 예: copyButtonLiElement.classList.add("donation"); // 이렇게 하면 기존 donation 스타일이 적용될 수 있음 (주의)
// copyButtonLiElement.classList.add("m3u8-custom-item"); // 커스텀 클래스 추가
copyButtonElement = document.createElement('button');
copyButtonElement.id = 'm3u8CopyButtonUserScriptButton';
copyButtonElement.innerHTML = linkIconSvg;
copyButtonElement.title = 'm3u8 링크 복사';
copyButtonElement.removeEventListener('click', handleCopyButtonClick);
copyButtonElement.addEventListener('click', handleCopyButtonClick);
copyButtonLiElement.appendChild(copyButtonElement);
// 가장 왼쪽에 삽입 (첫 번째 자식으로)
if (itemListContainer.firstChild) {
itemListContainer.insertBefore(copyButtonLiElement, itemListContainer.firstChild);
} else {
itemListContainer.appendChild(copyButtonLiElement);
}
console.log("[ensureCopyLinkButton] 버튼 아이템 가장 왼쪽에 추가/재배치 완료.");
}
updateButtonAppearance();
}
function initializeScript() {
console.log(`SOOP m3u8 복사 스크립트 초기화 (v${GM_info.script.version})`);
currentBroadcastInfo = getCurrentBroadcastInfoFromUrl();
if (!currentBroadcastInfo) { showToastMessage("방송 정보를 URL에서 인식할 수 없어 스크립트를 시작할 수 없습니다."); return; }
console.log("[initializeScript] 초기 방송 정보:", currentBroadcastInfo);
applyGlobalStyles();
const themeObserver = new MutationObserver(() => {
updateButtonAppearance();
});
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['dark'] });
const broadcastInfoArea = document.querySelector('.broadcast_information');
if (!broadcastInfoArea) {
console.error("[initializeScript] .broadcast_information 영역을 찾을 수 없어 Observer 시작 불가. 폴백 시도.");
let fallbackAttempts = 0;
const fallbackInterval = setInterval(() => {
fallbackAttempts++;
const foundArea = document.querySelector('.broadcast_information');
if (foundArea || fallbackAttempts > 20) {
clearInterval(fallbackInterval);
if(foundArea) initializeScript(); // 재시도 (무한루프 방지 필요)
return;
}
ensureCopyLinkButton(); // 영역 못찾아도 버튼 추가 시도
}, 500);
return;
}
const mainObserver = new MutationObserver(() => {
ensureCopyLinkButton();
});
mainObserver.observe(broadcastInfoArea, { childList: true, subtree: true });
setTimeout(() => {
ensureCopyLinkButton();
updateButtonAppearance();
}, 1000);
console.log("[initializeScript] 초기화 완료. Observer 감시 시작됨.");
}
// --- 스크립트 실행 조건 ---
const currentPath = window.location.pathname;
const pathParts = currentPath.split('/');
const isTargetPage = window.location.href.startsWith("https://play.sooplive.co.kr/") &&
!currentPath.includes("/embed") &&
pathParts.length >= 3 && pathParts[1] &&
!isNaN(parseInt(pathParts[2]));
if (isTargetPage) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeScript);
} else {
initializeScript();
}
}
})();