KometTube

우클릭 시 유튜브 썸네일 미니플레이어 생성

Fra 23.06.2025. Se den seneste versjonen.

// ==UserScript==
// @name         KometTube
// @namespace    Violentmonkey Scripts
// @version      250623.8
// @description  우클릭 시 유튜브 썸네일 미니플레이어 생성
// @match        *://www.youtube.com/*
// @grant        none
// @icon         https://i.imgur.com/3fkv1pI.png
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  let commentOpened = false;
  let originalPlayerWidth = null;

  function extractVideoId(url) {
    const match = url.match(/v=([^&]+)/) || url.match(/\/shorts\/([^/?]+)/);
    return match ? match[1] : null;
  }

  function copyToClipboard(text) {
    navigator.clipboard.writeText(text).then(() => {
      alert('링크가 복사되었습니다:\n' + text);
    });
  }

  function resizePlayer() {
    const wrapper = document.getElementById('vm-wrapper');
    const player = document.getElementById('vm-mini-player');
    if (!wrapper || !player) return;

    const vw = window.innerWidth;
    const vh = window.innerHeight;
    let width = Math.min(vw * (commentOpened ? 0.6 : 0.75), 960);
    let height = width * 9 / 16;

    if (height > vh * 0.75) {
      height = vh * 0.75;
      width = height * 16 / 9;
    }

    player.style.width = `${width}px`;
    player.style.maxHeight = `${vh * 0.75}px`;
    wrapper.style.transform = 'translate(-50%, -50%)';
  }

  function createMiniPlayer(videoId) {
    if (document.getElementById('vm-wrapper')) return;

    const overlay = document.createElement('div');
    overlay.id = 'vm-overlay';
    Object.assign(overlay.style, {
      position: 'fixed',
      top: 0,
      left: 0,
      width: '100vw',
      height: '100vh',
      background: 'rgba(0,0,0,0.6)',
      backdropFilter: 'blur(5px)',
      zIndex: 9998
    });

    const wrapper = document.createElement('div');
    wrapper.id = 'vm-wrapper';
    Object.assign(wrapper.style, {
      position: 'fixed',
      top: '50%',
      left: '50%',
      display: 'flex',
      alignItems: 'flex-start',
      gap: '12px',
      zIndex: 9999,
      transform: 'translate(-50%, -50%)'
    });

    const player = document.createElement('div');
    player.id = 'vm-mini-player';
    Object.assign(player.style, {
      background: '#111',
      padding: '12px',
      borderRadius: '8px',
      color: '#fff',
      display: 'flex',
      flexDirection: 'column',
      boxShadow: '0 0 20px rgba(0,0,0,0.5)',
      overflow: 'hidden'
    });

    const titleBar = document.createElement('div');
    titleBar.textContent = '제목 불러오는 중...';
    Object.assign(titleBar.style, {
      fontSize: '18px',
      fontWeight: 'bold',
      marginBottom: '8px',
      wordBreak: 'break-word',
      lineHeight: '1.4'
    });

    const iframeWrapper = document.createElement('div');
    Object.assign(iframeWrapper.style, {
      position: 'relative',
      paddingBottom: '56.25%',
      height: 0,
      marginBottom: '8px'
    });

    const iframe = document.createElement('iframe');
    iframe.src = `https://www.youtube.com/embed/${videoId}?autoplay=1`;
    iframe.allow = 'autoplay; encrypted-media';
    iframe.allowFullscreen = true;
    iframe.frameBorder = '0';
    Object.assign(iframe.style, {
      position: 'absolute',
      top: 0,
      left: 0,
      width: '100%',
      height: '100%'
    });

    iframeWrapper.appendChild(iframe);

    const btnRow = document.createElement('div');
    btnRow.style.marginTop = '4px';

    const btnCopy = makeButton('🔗 공유', () =>
      copyToClipboard(`https://youtu.be/${videoId}`)
    );

    const btnComments = makeButton('💬 댓글');
    [btnCopy, btnComments].forEach(btn => {
      btn.style.marginRight = '6px';
      btnRow.appendChild(btn);
    });

    player.appendChild(titleBar);
    player.appendChild(iframeWrapper);
    player.appendChild(btnRow);
    wrapper.appendChild(player);
    document.body.appendChild(overlay);
    document.body.appendChild(wrapper);

    window.addEventListener('resize', resizePlayer);
    resizePlayer();

    overlay.addEventListener('click', () => {
      overlay.remove();
      wrapper.remove();
      commentOpened = false;
    });

    fetch(`https://www.youtube.com/watch?v=${videoId}`)
      .then(res => res.text())
      .then(html => {
        const match = html.match(/var ytInitialPlayerResponse = (.*?});/s);
        if (!match) return;
        const data = JSON.parse(match[1]);
        const title = data.videoDetails?.title || '';
        const channel = data.videoDetails?.author || '';
        const chId = data.videoDetails?.channelId || '';
        const date = data.microformat?.playerMicroformatRenderer?.uploadDate || '';
        const isLive = data.videoDetails?.isLiveContent;

        titleBar.textContent = '';
        const link = document.createElement('a');
        link.href = `https://www.youtube.com/channel/${chId}`;
        link.target = '_blank';
        link.textContent = channel;
        link.style.color = '#4ea1f3';
        link.style.textDecoration = 'none';

        titleBar.append(link, ` - ${title}\t${date}`);

        if (!isLive) {
          btnComments.remove();
        } else {
          btnComments.onclick = () => toggleComments(videoId);
        }
      });
  }

  function toggleComments(videoId) {
    const wrapper = document.getElementById('vm-wrapper');
    const player = document.getElementById('vm-mini-player');
    const existing = document.getElementById('vm-comment-panel');

    if (!commentOpened && !existing) {
      commentOpened = true;
      originalPlayerWidth = player.offsetWidth;

      const panel = document.createElement('iframe');
      panel.id = 'vm-comment-panel';
      panel.src = `https://www.youtube.com/live_chat?v=${videoId}&embed_domain=${location.hostname}`;
      Object.assign(panel.style, {
        width: '320px',
        height: `${player.offsetHeight}px`,
        border: 'none',
        borderRadius: '8px',
        background: '#fff',
        boxShadow: '0 0 8px rgba(0,0,0,0.3)'
      });

      wrapper.appendChild(panel);
      player.style.width = `${originalPlayerWidth - 332}px`;
    } else {
      commentOpened = false;
      existing?.remove();
      player.style.width = `${originalPlayerWidth}px`;
    }

    resizePlayer();
  }

  function makeButton(text, onClick) {
    const btn = document.createElement('button');
    btn.textContent = text;
    Object.assign(btn.style, {
      cursor: 'pointer',
      padding: '4px 8px',
      borderRadius: '4px',
      border: '1px solid #444',
      background: '#222',
      color: '#fff'
    });
    if (onClick) btn.onclick = onClick;
    return btn;
  }

  document.addEventListener('contextmenu', e => {
    const link = e.target.closest('a');
    if (!link || !link.href) return;
    const videoId = extractVideoId(link.href);
    if (videoId) {
      e.preventDefault();
      createMiniPlayer(videoId);
    }
  }, true);
})();
长期地址
遇到问题?请前往 GitHub 提 Issues。