자동 다음 영상
// ==UserScript==
// @name SOOP Auto Next
// @namespace https://vod.sooplive.com/
// @version 3.1
// @description 자동 다음 영상
// @author you
// @match https://vod.sooplive.com/player/*
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function () {
"use strict";
/* =========================================================
설정값 (Tampermonkey 저장소 사용 - 팝업이 없으므로
설정 UI를 페이지 위에 iOS 스타일 패널로 직접 그려줍니다)
========================================================= */
const DEFAULTS = { enabled: true, seconds: 60, endOnly: false };
function getSettings() {
return {
enabled: GM_getValue("enabled", DEFAULTS.enabled),
seconds: GM_getValue("seconds", DEFAULTS.seconds),
endOnly: GM_getValue("endOnly", DEFAULTS.endOnly),
};
}
function setSetting(key, value) {
GM_setValue(key, value);
}
/* =========================================================
스타일 (iOS 느낌: 라운드, 블러, SF 폰트, 파란/그린 포인트)
========================================================= */
GM_addStyle(`
#san-fab {
position: fixed;
right: 20px;
bottom: 90px;
width: 52px;
height: 52px;
border-radius: 50%;
background: rgba(255,255,255,0.7);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
box-shadow: 0 4px 18px rgba(0,0,0,0.25);
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
cursor: pointer;
z-index: 999999;
user-select: none;
transition: transform 0.15s ease;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif;
}
#san-fab:active { transform: scale(0.9); }
#san-backdrop {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.25);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease;
z-index: 999998;
}
#san-backdrop.show { opacity: 1; pointer-events: auto; }
#san-sheet {
position: fixed;
left: 50%;
bottom: 0;
width: min(92vw, 380px);
transform: translate(-50%, 100%);
transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);
z-index: 999999;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif;
padding-bottom: 24px;
}
#san-sheet.show { transform: translate(-50%, 0); }
.san-handle {
width: 36px;
height: 5px;
border-radius: 999px;
background: rgba(255,255,255,0.5);
margin: 10px auto 8px;
}
.san-title {
text-align: center;
color: #fff;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.3px;
margin-bottom: 10px;
opacity: 0.85;
}
.san-group {
background: rgba(30,30,32,0.72);
backdrop-filter: blur(24px) saturate(180%);
-webkit-backdrop-filter: blur(24px) saturate(180%);
border-radius: 16px;
margin: 0 14px 12px;
overflow: hidden;
}
.san-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 13px 16px;
border-bottom: 0.5px solid rgba(255,255,255,0.12);
}
.san-row:last-child { border-bottom: none; }
.san-label {
color: #fff;
font-size: 15px;
font-weight: 400;
}
.san-sub {
color: rgba(255,255,255,0.5);
font-size: 12px;
margin-top: 2px;
}
.san-switch {
position: relative;
width: 51px;
height: 31px;
flex-shrink: 0;
}
.san-switch input { display: none; }
.san-switch .san-slider {
position: absolute;
inset: 0;
background: rgba(120,120,128,0.32);
border-radius: 999px;
transition: 0.2s;
}
.san-switch .san-slider::before {
content: "";
position: absolute;
width: 27px;
height: 27px;
left: 2px;
top: 2px;
background: #fff;
border-radius: 50%;
box-shadow: 0 3px 8px rgba(0,0,0,0.2);
transition: 0.2s;
}
.san-switch input:checked + .san-slider {
background: #34c759;
}
.san-switch input:checked + .san-slider::before {
transform: translateX(20px);
}
.san-slider-row {
padding: 12px 16px 16px;
display: flex;
flex-direction: column;
gap: 8px;
}
.san-slider-top {
display: flex;
justify-content: space-between;
align-items: baseline;
}
.san-value {
color: #0a84ff;
font-size: 15px;
font-weight: 600;
}
input[type="range"].san-range {
-webkit-appearance: none;
width: 100%;
height: 4px;
border-radius: 999px;
background: rgba(120,120,128,0.32);
accent-color: #0a84ff;
}
input[type="range"].san-range::-webkit-slider-thumb {
-webkit-appearance: none;
width: 22px;
height: 22px;
border-radius: 50%;
background: #fff;
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
cursor: pointer;
}
/* 영상 위 진행 오버레이 (더 심플/미니멀하게) */
#san-overlay {
position: absolute;
top: 14px;
left: 50%;
transform: translateX(-50%) scale(0.96);
opacity: 0;
width: 46%;
min-width: 160px;
z-index: 9999;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif;
}
#san-overlay.show {
opacity: 1;
transform: translateX(-50%) scale(1);
}
#san-overlay-inner {
background: rgba(28,28,30,0.6);
backdrop-filter: blur(16px) saturate(180%);
-webkit-backdrop-filter: blur(16px) saturate(180%);
padding: 9px 12px;
border-radius: 14px;
}
#san-progress-track {
height: 4px;
background: rgba(255,255,255,0.25);
border-radius: 999px;
overflow: hidden;
}
#san-progress-fill {
height: 100%;
width: 100%;
border-radius: 999px;
background: #0a84ff;
transition: width 1s linear;
}
#san-time-text {
text-align: center;
font-size: 11px;
margin-top: 5px;
color: rgba(255,255,255,0.85);
font-weight: 500;
letter-spacing: 0.2px;
}
`);
/* =========================================================
iOS 스타일 설정 패널 (플로팅 버튼 -> 바텀시트)
========================================================= */
function buildSettingsUI() {
if (document.getElementById("san-fab")) return;
const fab = document.createElement("div");
fab.id = "san-fab";
fab.textContent = "⏱";
document.body.appendChild(fab);
const backdrop = document.createElement("div");
backdrop.id = "san-backdrop";
document.body.appendChild(backdrop);
const sheet = document.createElement("div");
sheet.id = "san-sheet";
sheet.innerHTML = `
<div class="san-handle"></div>
<div class="san-title">AUTO NEXT</div>
<div class="san-group">
<div class="san-row">
<div>
<div class="san-label">자동 실행</div>
</div>
<label class="san-switch">
<input type="checkbox" id="san-enabled">
<span class="san-slider"></span>
</label>
</div>
<div class="san-row">
<div>
<div class="san-label">영상 종료 시에만 이동</div>
</div>
<label class="san-switch">
<input type="checkbox" id="san-endonly">
<span class="san-slider"></span>
</label>
</div>
</div>
<div class="san-group">
<div class="san-slider-row">
<div class="san-slider-top">
<span class="san-label">대기 시간</span>
<span class="san-value"><span id="san-seconds-val">60</span>초</span>
</div>
<input type="range" class="san-range" id="san-seconds" min="1" max="100">
</div>
</div>
`;
document.body.appendChild(sheet);
const enabledEl = sheet.querySelector("#san-enabled");
const endOnlyEl = sheet.querySelector("#san-endonly");
const secondsEl = sheet.querySelector("#san-seconds");
const secondsValEl = sheet.querySelector("#san-seconds-val");
function refreshUI() {
const s = getSettings();
enabledEl.checked = s.enabled;
endOnlyEl.checked = s.endOnly;
secondsEl.value = s.seconds;
secondsValEl.textContent = s.seconds;
}
refreshUI();
function openSheet() {
refreshUI();
backdrop.classList.add("show");
sheet.classList.add("show");
}
function closeSheet() {
backdrop.classList.remove("show");
sheet.classList.remove("show");
}
fab.addEventListener("click", () => {
if (sheet.classList.contains("show")) closeSheet();
else openSheet();
});
backdrop.addEventListener("click", closeSheet);
enabledEl.addEventListener("change", () => {
setSetting("enabled", enabledEl.checked);
resetAll();
const v = getVideo();
if (v) startTimer(v);
});
endOnlyEl.addEventListener("change", () => {
setSetting("endOnly", endOnlyEl.checked);
resetAll();
const v = getVideo();
if (v) startTimer(v);
});
secondsEl.addEventListener("input", () => {
const val = parseInt(secondsEl.value, 10);
secondsValEl.textContent = val;
setSetting("seconds", val);
});
secondsEl.addEventListener("change", () => {
resetAll();
const v = getVideo();
if (v) startTimer(v);
});
}
/* =========================================================
자동 다음영상 핵심 로직 (기존 content.js 로직 이식)
========================================================= */
let timer = null;
let interval = null;
let overlay = null;
let currentVideo = null;
let startTime = 0;
let duration = 0;
let isRunning = false;
function getVideo() {
return document.querySelector("#video");
}
function goNextVideo() {
const nextBtn = document.querySelector(".play.next");
if (nextBtn) {
document.getElementById("player")?.dispatchEvent(
new MouseEvent("mousemove", { bubbles: true })
);
setTimeout(() => {
try {
nextBtn.click();
} catch {}
}, 200);
}
}
function resetAll() {
if (timer) clearTimeout(timer);
if (interval) clearInterval(interval);
timer = null;
interval = null;
startTime = 0;
isRunning = false;
if (overlay) {
overlay.remove();
overlay = null;
}
}
function waitForPlayer(callback) {
const check = setInterval(() => {
const player = document.getElementById("player");
if (player) {
clearInterval(check);
callback(player);
}
}, 200);
}
function createOverlay() {
if (overlay) return;
const player = document.getElementById("player");
if (!player) {
waitForPlayer(() => createOverlay());
return;
}
overlay = document.createElement("div");
overlay.id = "san-overlay";
overlay.innerHTML = `
<div id="san-overlay-inner">
<div id="san-progress-track">
<div id="san-progress-fill"></div>
</div>
<div id="san-time-text">60s</div>
</div>
`;
player.appendChild(overlay);
requestAnimationFrame(() => {
overlay.classList.add("show");
});
}
function startProgress() {
if (interval) clearInterval(interval);
interval = setInterval(() => {
const elapsed = Math.floor((Date.now() - startTime) / 1000);
const remaining = duration - elapsed;
if (remaining <= 0) {
clearInterval(interval);
goNextVideo();
return;
}
const fill = document.getElementById("san-progress-fill");
const text = document.getElementById("san-time-text");
if (fill) fill.style.width = (remaining / duration) * 100 + "%";
if (text) text.textContent = remaining + "s";
}, 1000);
}
function startTimer(video) {
const { enabled, seconds, endOnly } = getSettings();
if (!enabled || endOnly) {
resetAll();
return;
}
if (isRunning) return;
isRunning = true;
duration = seconds;
if (!startTime) {
startTime = Date.now();
}
createOverlay();
startProgress();
video.addEventListener("ended", () => {
const s = getSettings();
if (s.enabled && s.endOnly) goNextVideo();
});
}
function watchVideoChange() {
const observer = new MutationObserver(() => {
const video = getVideo();
if (video && video !== currentVideo) {
resetAll();
currentVideo = video;
startTimer(video);
video.addEventListener("play", () => {
startTimer(video);
});
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
}
function init() {
buildSettingsUI();
watchVideoChange();
const video = getVideo();
if (video) {
currentVideo = video;
startTimer(video);
video.addEventListener("play", () => {
startTimer(video);
});
}
}
init();
})();