您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
디시인사이드 INE 갤러리의 영상을 재생합니다.
// ==UserScript== // @name INE live player // @version 0.2.2 // @description 디시인사이드 INE 갤러리의 영상을 재생합니다. // @author Kak-ine // @match https://*.dcinside.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=dcinside.com // @grant none // @license MIT // @namespace https://greasyfork.org/ko/scripts/523536-dc-streaming // ==/UserScript== // TODO: 디시인사이드 화면 없애고 영상 플레이어만 띄우는 옵션 추가 (async () => { 'use strict'; const galleryBaseUrl = 'https://gall.dcinside.com/mini/board/lists?id=ineviolet'; const maxRetries = 5; const keyword = "아이네 -"; let currentIndex = -1; let isHidden = false; // 🔥 숨김 상태 여부 저장 let shuffledItems = []; // 🔥 새로 추가: { title: ..., videoUrl: ... } 형태의 배열 const videoItems = []; // 📌 랜덤 딜레이 (2~5초) const delay = (min = 2000, max = 5000) => new Promise(resolve => setTimeout(resolve, Math.floor(Math.random() * (max - min + 1)) + min)); // 📌 도메인 변경 함수 (dcm6 → dcm1) function replaceDomain(videoUrl) { return videoUrl.replace('dcm6', 'dcm1'); } // 📌 최대 페이지 수 자동 추출 const fetchMaxPageNumber = async (retryCount = 0) => { try { const response = await fetch(galleryBaseUrl); const text = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(text, 'text/html'); const totalPageElement = doc.querySelector('span.num.total_page'); return parseInt(totalPageElement.textContent.trim()); } catch (error) { if (retryCount < maxRetries) { await delay(); return fetchMaxPageNumber(retryCount + 1); } else { return 1; } } }; // 📌 동영상 링크 추출 const fetchVideoUrl = async (postUrl, retryCount = 0) => { try { const response = await fetch(postUrl); const text = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(text, 'text/html'); const iframeElement = doc.querySelector('iframe[id^="movieIcon"]'); if (iframeElement) { const iframeSrc = iframeElement.getAttribute('src'); const iframeResponse = await fetch(iframeSrc); const iframeText = await iframeResponse.text(); const iframeDoc = parser.parseFromString(iframeText, 'text/html'); const videoElement = iframeDoc.querySelector('video.dc_mv source'); return videoElement ? replaceDomain(videoElement.getAttribute('src')) : null; } return null; } catch (error) { console.warn(`❌ 비디오 링크 수집 실패: ${error.message}, retryCount: ${retryCount}`); if (retryCount > 0) { retryCount--; await delay(); return fetchVideoUrl(postUrl, retryCount) } } return null; }; // 📌 게시글 링크 수집 (순차적 페이지 + 재시도 기능) const fetchPostLinksSeq = async (maxPageNumber, retryCount = 0) => { let i = 0; let retry = 0 for (i = 0; i < maxPageNumber; i++) { const PageUrl = `${galleryBaseUrl}&page=${i + 1}`; try { const response = await fetch(PageUrl, { headers: { 'User-Agent': navigator.userAgent } }); // await delay(); if (!response.ok) throw new Error(`응답 실패 (상태 코드: ${response.status})`); const text = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(text, 'text/html'); const links = doc.querySelectorAll('a[href*="/mini/board/view"]'); const postLinks = []; links.forEach(link => { const href = link.getAttribute('href'); const title = link.textContent.trim() || ""; // 🔥 "아이네" 포함 제목만 수집 if (href && title.includes(keyword)) { // 갤러리 글 주소 const postUrl = `https://gall.dcinside.com${href}`; postLinks.push({ postUrl, title }); } }); if (postLinks.length === 0) throw new Error('게시글 링크를 찾을 수 없음'); console.log(`📄 ${PageUrl} 페이지에서 ${postLinks.length}개의 게시글 링크 수집 완료`); // 🔥 각 postUrl에서 videoUrl 추출, videoItems에 저장 for (const item of postLinks) { const videoUrl = await fetchVideoUrl(item.postUrl, retryCount); await delay(); if (videoUrl) { videoItems.push({ title: item.title, videoUrl: videoUrl }); console.log(item.title, videoUrl); } } console.log(`📄 ${PageUrl} 페이지에서 ${videoItems.length}개의 동영상 링크 수집 완료`); } catch (error) { console.warn(`❌ 게시글 링크 수집 실패: ${error.message}, retryCount: ${retry}`); if (retry >= retryCount) { retry = 0 continue } i--; retry++; } } }; // 📌 동영상 재생 function playVideo(videoUrl) { const existingVideo = document.getElementById('autoPlayedVideo'); if (existingVideo) existingVideo.remove(); const videoPlayer = document.createElement('video'); videoPlayer.id = 'autoPlayedVideo'; videoPlayer.src = videoUrl; videoPlayer.controls = true; videoPlayer.autoplay = true; videoPlayer.muted = false; videoPlayer.volume = 0.5; videoPlayer.style.position = 'fixed'; videoPlayer.style.bottom = '100px'; videoPlayer.style.right = '20px'; videoPlayer.style.width = '480px'; videoPlayer.style.zIndex = 9999; videoPlayer.style.boxShadow = '0px 0px 10px rgba(0, 0, 0, 0.5)'; videoPlayer.style.borderRadius = '10px'; // 📌 숨김 상태일 때 영상도 숨김 처리 videoPlayer.style.display = isHidden ? 'none' : 'block'; document.body.appendChild(videoPlayer); videoPlayer.onended = () => { playNextVideo(); // 🔥 자동으로 다음 영상 재생 }; } // Fisher–Yates shuffle 예시 function shuffleArray(array) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } } function shufflePlay() { if (shuffledItems.length <= 1) return; // 곡이 1개 이하라면 셔플 불필요 // 1) 현재 재생 중인 곡을 변수에 저장 const currentTrack = shuffledItems[currentIndex]; // 2) 배열에서 제거 // (splice로 해당 인덱스의 요소를 추출) shuffledItems.splice(currentIndex, 1); // 3) 나머지 곡들 무작위 셔플 // (Fisher–Yates 알고리즘 등) shuffleArray(shuffledItems); // 4) 다시 currentIndex 위치에 삽입 shuffledItems.splice(currentIndex, 0, currentTrack); // console.log('✅ 셔플(현재 곡 유지) 완료:', shuffledItems.map(item=>item.title)); // 필요 시 UI 갱신 createPlaylistUI(); } const playPreviousVideo = () => { currentIndex--; if (currentIndex < 0) { console.log("❌ 이전 영상이 없습니다."); currentIndex = 0; return; } playVideo(shuffledItems[currentIndex].videoUrl); createPlaylistUI(); } // 📌 다음 영상 재생 function playNextVideo() { // 순서대로 재생하기 위해 currentIndex 증가 currentIndex++; // 범위 체크: 인덱스가 videoItems 길이를 초과하면 더 이상 영상 없음 if (currentIndex >= videoItems.length) { currentIndex = 0 } // 해당 index의 영상 불러오기 const item = shuffledItems[currentIndex]; console.log(`▶ [${currentIndex}] ${item.title} 재생`); playVideo(item.videoUrl); // 🔥 재생 목록 UI 갱신 createPlaylistUI(); } // 📌 재생/일시정지 버튼 상태 토글 (아이콘 변경) function togglePlayPause() { const video = document.getElementById('autoPlayedVideo'); const playPauseButton = document.getElementById('playPauseButton'); if (video) { if (video.paused) { video.play(); playPauseButton.innerText = '⏸'; // 🔥 일시정지 아이콘으로 변경 } else { video.pause(); playPauseButton.innerText = '▶'; // 🔥 재생 아이콘으로 변경 } } else { playNextVideo(); playPauseButton.innerText = '⏸'; // 🔥 재생 목록 UI 갱신 createPlaylistUI(); } } function createPlaylistUI() { // 기존 UI 제거 const existing = document.getElementById('playlistContainer'); if (existing) existing.remove(); const container = document.createElement('div'); container.id = 'playlistContainer'; container.style.position = 'fixed'; container.style.bottom = '10px'; container.style.padding = '10px'; container.style.width = '250px'; container.style.border = '1px solid #ccc'; container.style.borderRadius = '8px'; container.style.background = 'rgba(255, 255, 255, 0.8)'; container.style.zIndex = 9999; if (isHidden) { container.style.right = '70px'; } else { container.style.right = '230px'; } // 스크롤 영역 설정 container.style.maxHeight = '60px'; container.style.overflowY = 'auto'; container.style.scrollBehavior = 'smooth'; // 스무스 스크롤 const list = document.createElement('ul'); list.style.margin = '0'; list.style.padding = '0 0 0 20px'; let activeLi = null; // 현재 곡에 해당하는 <li>를 저장할 변수 for (let i = 0; i < shuffledItems.length; i++) { const item = shuffledItems[i]; const pli = document.createElement('li'); const cleanedTitle = item.title.replace(/^아이네 - /, ""); pli.textContent = cleanedTitle; // 현재 곡 배경 강조 if (i === currentIndex) { pli.style.backgroundColor = '#cceeff'; pli.style.fontWeight = 'bold'; pli.classList.add('activeSong'); // 식별용 클래스 activeLi = pli; // 아래에서 scrollIntoView()에 사용 } // 곡 클릭 시 pli.addEventListener('click', () => { currentIndex = i; playVideo(item.videoUrl); createPlaylistUI(); }); list.appendChild(pli); } container.appendChild(list); document.body.appendChild(container); // 📌 UI 생성 후, 현재 곡이 있는 li 위치로 스크롤 이동 if (activeLi) { activeLi.scrollIntoView({ behavior: 'smooth', block: 'center' }); } } // 📌 Fancy 버튼 컨트롤 패널 + 버튼 디자인 개선 function createFancyControlPanel() { const controlPanel = document.createElement('div'); controlPanel.id = 'fancyControlPanel'; controlPanel.style.position = 'fixed'; controlPanel.style.bottom = '40px'; controlPanel.style.right = '-250px'; // 📌 숨김 상태 controlPanel.style.display = 'flex'; controlPanel.style.gap = '0px'; controlPanel.style.padding = '5px'; // controlPanel.style.background = 'rgba(0, 0, 0, 0.3)'; controlPanel.style.borderRadius = '30px'; controlPanel.style.boxShadow = '0px 4px 15px rgba(0, 0, 0, 0.3)'; controlPanel.style.zIndex = '10000'; controlPanel.style.width = '180px'; controlPanel.style.transition = 'right 0.3s ease'; // 📌 펼치기 버튼 (📂) const expandButton = document.createElement('button'); expandButton.id = 'expandControlPanel'; expandButton.innerText = '⬅︎'; // 아이콘 변경 expandButton.style.position = 'fixed'; expandButton.style.bottom = '40px'; expandButton.style.right = '20px'; expandButton.style.padding = '10px'; expandButton.style.fontSize = '18px'; expandButton.style.backgroundColor = 'rgba(0, 0, 0, 0.6)'; expandButton.style.color = '#ffffff'; // expandButton.style.border = 'none'; expandButton.style.borderRadius = '50%'; expandButton.style.cursor = 'pointer'; expandButton.style.boxShadow = '0px 2px 6px rgba(0, 0, 0, 0.3)'; expandButton.style.zIndex = '10001'; // 📌 펼치기 버튼 클릭 시 패널 열기 expandButton.addEventListener('click', () => { controlPanel.style.right = '20px'; expandButton.style.display = 'none'; isHidden = false; // 🔥 숨김 상태 해제 // 🔥 영상도 같이 표시 const videoPlayer = document.getElementById('autoPlayedVideo'); if (videoPlayer) { videoPlayer.style.display = 'block'; // 플레이리스트 위치 조절 (isHidden에 의해 위치 조절) createPlaylistUI() } }); // 📌 버튼 목록 (숨기기 버튼 포함) const buttons = [ { id: 'prevVideoButton', text: '⏮', action: playPreviousVideo }, { id: 'playPauseButton', text: '▶', action: togglePlayPause }, // 🔥 상태에 따라 변경 { id: 'nextVideoButton', text: '⏭', action: playNextVideo }, { id: 'nextVideoButton', text: '🔀', action: shufflePlay }, { id: 'hidePanelButton', text: '➡︎', action: () => { // 📂 숨기기 버튼으로 변경 controlPanel.style.right = '-250px'; expandButton.style.display = 'block'; // 🔥 영상도 같이 숨기기 const videoPlayer = document.getElementById('autoPlayedVideo'); if (videoPlayer) { videoPlayer.style.display = 'none'; } isHidden = true; // 🔥 숨김 상태 유지 // 플레이리스트만 표시되게 갱신 (isHidden에 의해 위치 조절) createPlaylistUI() }} ]; // 📌 버튼 생성 및 디자인 적용 buttons.forEach(btn => { const button = document.createElement('button'); button.id = btn.id; button.innerText = btn.text; button.style.width = '45px'; button.style.height = '45px'; button.style.fontSize = '20px'; button.style.backgroundColor = 'rgba(255, 255, 255, 0.1)'; button.style.color = '#000'; // button.style.border = '1px solid rgba(0, 0, 0, 0.3)'; button.style.borderRadius = '50%'; button.style.cursor = 'pointer'; button.style.boxShadow = 'none'; button.style.transition = 'transform 0.2s ease, background-color 0.2s ease'; // 📌 버튼 호버 효과 (부드러운 확대 + 색상 변경) button.addEventListener('mouseover', () => { button.style.transform = 'scale(1.2)'; //button.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; button.style.color = '#ffffff'; }); button.addEventListener('mouseout', () => { button.style.transform = 'scale(1)'; // button.style.backgroundColor = 'rgba(255, 255, 255, 0.1)'; button.style.color = '#000'; }); button.addEventListener('click', btn.action); controlPanel.appendChild(button); }); // 📌 버튼 및 패널 추가 document.body.appendChild(controlPanel); document.body.appendChild(expandButton); } // ✅ Base64 디코딩 함수 function decodeBase64(data) { return decodeURIComponent(escape(atob(data))); } const response = await fetch('https://kak-ine.github.io/data/videos.json'); const fetchedItems = await response.json(); // ✅ 배열 전체 디코딩 const decodedData = fetchedItems.map(item => ({ title: decodeBase64(item.title), videoUrl: decodeBase64(item.videoUrl) })); // DONE: Github Action에서 DB에 daily update 하도록 자동화 // // ///////////////// Depecated //////////////////////// // // 미니 갤러리 크롤링하여 비디오 링크 수집 // const maxPageNumber = await fetchMaxPageNumber(); // await fetchPostLinksSeq(maxPageNumber, 5); // console.log('수집된 videoItems:', videoItems); // ///////////////// Depecated //////////////////////// // DB로 부터 로드 videoItems.push(...decodedData); shuffledItems = videoItems.slice() createFancyControlPanel(); })();