SOOP Auto Next

자동 다음 영상

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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();
})();