// ==UserScript== // @name lck // @namespace lck // @version 2.02 // @description 롤캐 숙제 바로가기+직링보조 // @match https://lolcast.kr/* // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @downloadURL none // ==/UserScript== (function() { 'use strict'; // 기본 설정 const DEFAULT_SETTINGS = { youtube: true, chzzk: true, afreeca: true, flow: true, customInput: true // 주소 입력창 표시 여부 }; // 설정 불러오기 let settings = JSON.parse(GM_getValue('settings', JSON.stringify(DEFAULT_SETTINGS))); // 채널 정보 const CHANNELS = { youtube: { id: 'UCw1DsweY9b2AKGjV4kGJP1A', buttonLabel: '유튜브', color: '#FF0000', url: (id) => `/#/player/youtube/${id}` }, chzzk: { buttonLabel: '치지직', color: '#00FFA3', url: () => '/#/player/chzzk/9381e7d6816e6d915a44a13c0195b202' }, afreeca: { buttonLabel: '숲', color: '#2970B6', url: () => '/#/player/afreeca/aflol' }, flow: { buttonLabel: 'Flow', color: '#6A5ACD', url: () => '/#/player/flow' } }; // 유튜브 라이브 영상 ID 가져오기 async function fetchLiveVideoId(channelId) { const YOUTUBE_LIVE_URL = `https://www.youtube.com/channel/${channelId}/live`; return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: YOUTUBE_LIVE_URL, onload: function(response) { const videoIdMatch = response.responseText.match(/"videoId":"([\w-]+)"/); const isLiveNow = response.responseText.includes('"isLiveNow":true') || response.responseText.includes('"isLive":true'); const liveBroadcastContentMatch = response.responseText.match(/"liveBroadcastContent":"(\w+)"/); const isLiveBroadcast = liveBroadcastContentMatch && liveBroadcastContentMatch[1] === 'live'; if (videoIdMatch && videoIdMatch[1] && (isLiveNow || isLiveBroadcast)) { resolve(videoIdMatch[1]); } else { reject('No live video found.'); } }, onerror: reject }); }); } // URL 분석 및 리디렉션 function parseAndRedirect(url) { const youtubeShortRegex = /https:\/\/youtu\.be\/([\w-]+)/; const youtubeLongRegex = /https:\/\/www\.youtube\.com\/watch\?v=([\w-]+)/; const kickRegex = /https:\/\/kick\.com\/([\w-]+)/; const chzzkRegex = /https:\/\/chzzk\.naver\.com\/([\w-]+)/; const twitchRegex = /https:\/\/twitch\.tv\/([\w-]+)/; const afreecaRegex = /https:\/\/play\.sooplive\.co\.kr\/([\w-]+)/; if (youtubeShortRegex.test(url)) { const videoId = url.match(youtubeShortRegex)[1]; window.location.href = `https://lolcast.kr/#/player/youtube/${videoId}`; } else if (youtubeLongRegex.test(url)) { const videoId = url.match(youtubeLongRegex)[1]; window.location.href = `https://lolcast.kr/#/player/youtube/${videoId}`; } else if (kickRegex.test(url)) { const channelId = url.match(kickRegex)[1]; window.location.href = `https://lolcast.kr/#/player/kick/${channelId}`; } else if (chzzkRegex.test(url)) { const channelId = url.match(chzzkRegex)[1]; window.location.href = `https://lolcast.kr/#/player/chzzk/${channelId}`; } else if (twitchRegex.test(url)) { const channelId = url.match(twitchRegex)[1]; window.location.href = `https://lolcast.kr/#/player/twitch/${channelId}`; } else if (afreecaRegex.test(url)) { const channelId = url.match(afreecaRegex)[1]; window.location.href = `https://lolcast.kr/#/player/afreeca/${channelId}`; } else { alert('지원하지 않는 URL 형식입니다.'); } } // 설정 패널 생성 function createSettingsPanel() { const modal = document.createElement('div'); modal.style.position = 'fixed'; modal.style.top = '50%'; modal.style.left = '50%'; modal.style.transform = 'translate(-50%, -50%)'; modal.style.backgroundColor = 'rgba(255, 255, 255, 0.95)'; modal.style.padding = '20px'; modal.style.borderRadius = '8px'; modal.style.boxShadow = '0 0 10px rgba(0,0,0,0.3)'; modal.style.zIndex = '10000'; modal.style.display = 'none'; modal.id = 'settingsModal'; const header = document.createElement('div'); header.style.display = 'flex'; header.style.justifyContent = 'space-between'; header.style.alignItems = 'center'; header.style.marginBottom = '15px'; const title = document.createElement('h3'); title.textContent = '채널 설정'; title.style.margin = '0'; const closeBtn = document.createElement('button'); closeBtn.textContent = '×'; closeBtn.style.background = 'none'; closeBtn.style.border = 'none'; closeBtn.style.fontSize = '20px'; closeBtn.style.cursor = 'pointer'; closeBtn.onclick = () => modal.style.display = 'none'; header.appendChild(title); header.appendChild(closeBtn); const content = document.createElement('div'); content.style.display = 'flex'; content.style.flexDirection = 'column'; content.style.gap = '10px'; Object.keys(CHANNELS).forEach(channel => { const label = document.createElement('label'); label.style.display = 'flex'; label.style.alignItems = 'center'; label.style.gap = '8px'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.checked = settings[channel]; checkbox.onchange = (e) => { settings[channel] = e.target.checked; GM_setValue('settings', JSON.stringify(settings)); refreshControlPanel(); }; const text = document.createTextNode(CHANNELS[channel].buttonLabel); label.appendChild(checkbox); label.appendChild(text); content.appendChild(label); }); // 주소 입력창 표시 여부 설정 추가 const customInputLabel = document.createElement('label'); customInputLabel.style.display = 'flex'; customInputLabel.style.alignItems = 'center'; customInputLabel.style.gap = '8px'; const customInputCheckbox = document.createElement('input'); customInputCheckbox.type = 'checkbox'; customInputCheckbox.checked = settings.customInput; customInputCheckbox.onchange = (e) => { settings.customInput = e.target.checked; GM_setValue('settings', JSON.stringify(settings)); refreshControlPanel(); }; const customInputText = document.createTextNode('주소입력창'); customInputLabel.appendChild(customInputCheckbox); customInputLabel.appendChild(customInputText); content.appendChild(customInputLabel); modal.appendChild(header); modal.appendChild(content); document.body.appendChild(modal); // 드래그 기능 let isDragging = false; let offsetX, offsetY; header.addEventListener('mousedown', (e) => { isDragging = true; offsetX = e.clientX - modal.offsetLeft; offsetY = e.clientY - modal.offsetTop; }); document.addEventListener('mousemove', (e) => { if (isDragging) { modal.style.left = `${e.clientX - offsetX}px`; modal.style.top = `${e.clientY - offsetY}px`; } }); document.addEventListener('mouseup', () => { isDragging = false; }); return modal; } // 컨트롤 패널 새로고침 function refreshControlPanel() { const oldPanel = document.getElementById('controlPanel'); if (oldPanel) oldPanel.remove(); createControlPanel(); } // 컨트롤 패널 생성 function createControlPanel() { const controlPanel = document.createElement('div'); controlPanel.id = 'controlPanel'; controlPanel.style.position = 'fixed'; controlPanel.style.top = '380px'; controlPanel.style.left = '0'; controlPanel.style.padding = '7px'; controlPanel.style.borderRadius = '0 4px 4px 0'; controlPanel.style.zIndex = '9999'; controlPanel.style.display = 'flex'; controlPanel.style.flexDirection = 'row'; controlPanel.style.flexWrap = 'wrap'; controlPanel.style.gap = '6px'; controlPanel.style.background = 'transparent'; controlPanel.style.transition = 'all 0.3s ease'; controlPanel.style.maxWidth = '200px'; // 컨테이너 최대 너비 설정 // 위치 불러오기 const savedPosition = JSON.parse(localStorage.getItem('lckControlPanelPosition')); if (savedPosition) { controlPanel.style.left = savedPosition.left; controlPanel.style.top = savedPosition.top; } // 드래그 기능 let isDragging = false; let offsetX = 0; let offsetY = 0; controlPanel.addEventListener('mousedown', (e) => { isDragging = true; offsetX = e.clientX - controlPanel.offsetLeft; offsetY = e.clientY - controlPanel.offsetTop; }); document.addEventListener('mousemove', (e) => { if (isDragging) { controlPanel.style.left = (e.clientX - offsetX) + 'px'; controlPanel.style.top = (e.clientY - offsetY) + 'px'; } }); document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; localStorage.setItem('lckControlPanelPosition', JSON.stringify({ left: controlPanel.style.left, top: controlPanel.style.top })); } }); // 버튼 생성 함수 function createChannelButton(channel) { const button = document.createElement('button'); button.textContent = channel.buttonLabel; button.style.padding = '5px 11px'; button.style.color = channel.color; button.style.border = `1px solid ${channel.color}`; button.style.borderRadius = '3px'; button.style.cursor = 'pointer'; button.style.fontSize = '13px'; button.style.transition = 'all 0.2s'; button.style.background = 'transparent'; button.onmouseover = () => { button.style.background = channel.color; button.style.color = 'white'; }; button.onmouseout = () => { button.style.background = 'transparent'; button.style.color = channel.color; }; button.onclick = async () => { if (channel.buttonLabel === '유튜브') { try { const liveVideoId = await fetchLiveVideoId(channel.id); window.location.href = channel.url(liveVideoId); } catch { alert('유튜브 라이브 방송이 없습니다.'); } } else { window.location.href = channel.url(); } }; return button; } // 토글 버튼 생성 function createToggleButton() { const toggleButton = document.createElement('button'); toggleButton.textContent = '◀'; toggleButton.style.padding = '5px 11px'; toggleButton.style.color = '#888'; toggleButton.style.border = '1px solid #888'; toggleButton.style.borderRadius = '3px'; toggleButton.style.cursor = 'pointer'; toggleButton.style.fontSize = '13px'; toggleButton.style.transition = 'all 0.2s'; toggleButton.style.background = 'transparent'; toggleButton.onmouseover = () => { toggleButton.style.background = '#888'; toggleButton.style.color = 'white'; }; toggleButton.onmouseout = () => { toggleButton.style.background = 'transparent'; toggleButton.style.color = '#888'; }; toggleButton.onclick = () => { controlPanel.style.display = 'none'; }; return toggleButton; } // 설정 버튼 생성 const settingsButton = document.createElement('button'); settingsButton.textContent = '⚙'; settingsButton.style.padding = '5px 11px'; settingsButton.style.color = '#666'; settingsButton.style.border = '1px solid #666'; settingsButton.style.borderRadius = '3px'; settingsButton.style.cursor = 'pointer'; settingsButton.style.background = 'transparent'; settingsButton.onclick = () => { document.getElementById('settingsModal').style.display = 'block'; }; // 커스텀 입력 섹션 const customSection = document.createElement('div'); customSection.style.display = 'flex'; customSection.style.flexDirection = 'column'; customSection.style.gap = '5px'; customSection.style.marginTop = '10px'; customSection.style.width = '100%'; // 주소 입력창 표시 여부 설정 if (settings.customInput) { // 입력 필드 및 Go 버튼 const inputRow = document.createElement('div'); inputRow.style.display = 'flex'; inputRow.style.gap = '5px'; inputRow.style.width = '100%'; const input = document.createElement('input'); input.type = 'text'; input.placeholder = 'URL 입력'; input.style.padding = '5px'; input.style.border = '1px solid #ddd'; input.style.borderRadius = '3px'; input.style.flexGrow = '1'; const goBtn = document.createElement('button'); goBtn.textContent = 'Go'; goBtn.style.padding = '5px 11px'; goBtn.style.background = '#666'; goBtn.style.color = 'white'; goBtn.style.border = 'none'; goBtn.style.borderRadius = '3px'; goBtn.style.cursor = 'pointer'; goBtn.onclick = () => { const url = input.value.trim(); if (!url) { alert('URL을 입력해주세요.'); return; } parseAndRedirect(url); }; inputRow.appendChild(input); inputRow.appendChild(goBtn); customSection.appendChild(inputRow); } // 기존 버튼 추가 if (settings.youtube) controlPanel.appendChild(createChannelButton(CHANNELS.youtube)); if (settings.chzzk) controlPanel.appendChild(createChannelButton(CHANNELS.chzzk)); if (settings.afreeca) controlPanel.appendChild(createChannelButton(CHANNELS.afreeca)); if (settings.flow) controlPanel.appendChild(createChannelButton(CHANNELS.flow)); controlPanel.appendChild(settingsButton); controlPanel.appendChild(createToggleButton()); controlPanel.appendChild(customSection); document.body.appendChild(controlPanel); } // 초기화 createSettingsPanel(); createControlPanel(); })();