您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
SOOP 방송 페이지 플레이어 하단 정보 바의 가장 왼쪽에 m3u8 링크 복사 버튼을 추가하고, 페이지 테마에 따라 디자인을 조정합니다.
当前为
// ==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(); } } })();