SOOP 채널 프로필 & 배너 이미지 다운로드

Soop 채널(방송국)의 프로필/배너 이미지를 닉네임 파일명으로 다운로드 (커스텀 토스트, 기본이미지 판별, toast-box 내부 append)

当前为 2025-09-16 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         SOOP 채널 프로필 & 배너 이미지 다운로드
// @namespace    http://tampermonkey.net/
// @version      3.0
// @description  Soop 채널(방송국)의 프로필/배너 이미지를 닉네임 파일명으로 다운로드 (커스텀 토스트, 기본이미지 판별, toast-box 내부 append)
// @author       WakViewer
// @match        https://www.sooplive.co.kr/station/*
// @icon         https://res.sooplive.co.kr/afreeca.ico
// @grant        GM_download
// @grant        unsafeWindow
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function() {
  'use strict';

  let shortcutKey = GM_getValue('shortcutKey', 'Y').toLowerCase();

  // ===== 정확한 위치(네가 준 셀렉터) =====
  const SELECTORS = {
    bannerDiv : '#layout_container__S7ueh > div.ChannelVisual_channelVisual__a2JA_ > div.TopBanner_TopBannerWrap__1Z87K',
    profileImg: '#layout_container__S7ueh > div.ChannelVisual_channelVisual__a2JA_ > div.ChannelVisual_channelInfoWrapper__ovnvh > div > div.StreamerInfo_streamer__ZhKoB > div.StreamerInfo_profileImg__EC1En > span > div > img',
    nicknameP : '#layout_container__S7ueh > div.ChannelVisual_channelVisual__a2JA_ > div.ChannelVisual_channelInfoWrapper__ovnvh > div > div.StreamerInfo_streamer__ZhKoB > div:nth-child(2) > div.StreamerInfo_nicknameWrapper__NFtU2 > p'
  };

  // 기본 이미지(있으면 "없음" 처리)
  const DEFAULTS = {
    profilePrefix: 'https://res.sooplive.co.kr/images/svg/thumb_profile.svg',
    bannerLight  : 'https://res.sooplive.co.kr/images/channel/ChannelVisualImageLight.jpg',
    bannerDark   : 'https://res.sooplive.co.kr/images/channel/ChannelVisualImageDark.jpg'
  };

  // ===== 토스트: body > div.toast-box 안에 자식으로 <div><p>message</p></div> 생성 =====
  function ensureToastBox() {
    let box = document.querySelector('body > div.toast-box');
    if (!box) {
      box = document.createElement('div');
      box.className = 'toast-box';
      document.body.appendChild(box);
    }
    return box;
  }

  function showToast(message) {
    const box = ensureToastBox();
    const item = document.createElement('div');
    const p = document.createElement('p');
    p.textContent = message;
    item.appendChild(p);
    box.appendChild(item);

    // 3초 뒤 사라짐
    setTimeout(() => {
      if (item && item.parentNode) item.parentNode.removeChild(item);
    }, 3000);
  }

  // ===== 유틸 =====
  function parseBgUrlFromStyle(styleStr) {
    if (!styleStr) return '';
    const m = styleStr.match(/url\((['"]?)(https?:\/\/[^)]+)\1\)/i);
    return m ? m[2] : '';
  }

  function ensureExtByUrl(baseName, url) {
    const m = url.match(/\.([a-z0-9]+)(?:[?#]|$)/i);
    const ext = (m ? m[1] : 'jpg').toLowerCase();
    return baseName.endsWith('.' + ext) ? baseName : `${baseName}.${ext}`;
  }

  function sanitizeFilenameBase(s) {
    // 윈도우 금지문자만 치환 (♥ 등 유니코드는 유지)
    return s.replace(/[\\/:*?"<>|]/g, '_').trim();
  }

  function getChannelIdFromUrl() {
    const parts = location.pathname.split('/').filter(Boolean); // ["station","<id>"]
    return parts[1] || 'streamer';
  }

  function getNickname() {
    // 닉네임 우선
    const nick = document.querySelector(SELECTORS.nicknameP)?.textContent?.trim();
    if (nick) return nick;
    // 폴백: 프로필 alt
    const alt = document.querySelector(SELECTORS.profileImg)?.getAttribute('alt')?.trim();
    if (alt) return alt;
    // 최종: station/<id>
    return getChannelIdFromUrl();
  }

  function downloadImage(url, filenameBase) {
    const safeBase = sanitizeFilenameBase(filenameBase);
    const name = ensureExtByUrl(safeBase, url);
    GM_download({
      url,
      name,
      onerror: (err) => {
        console.error(`Failed to download ${name}:`, err);
        // 토스트는 요청된 6개 메시지만 사용
      }
    });
  }

  // ===== 프로필 =====
  function downloadProfile() {
    const nick = getNickname(); // 예: 꽃유이♥
    const imgEl = document.querySelector(SELECTORS.profileImg);
    const src = imgEl?.src || '';

    // 기본 이미지면 없음 처리
    if (!src || src.startsWith(DEFAULTS.profilePrefix)) {
      showToast('프로필 이미지가 없습니다!');
      return;
    }

    downloadImage(src, `${nick} 프로필`);
    showToast('프로필 이미지 다운로드 완료!');
  }

  // ===== 배너 =====
  function downloadBanner() {
    const nick = getNickname();
    const el = document.querySelector(SELECTORS.bannerDiv);

    if (!el) {
      showToast('배너 이미지가 없습니다!');
      return;
    }

    const styleBg = el.getAttribute('style') || getComputedStyle(el).backgroundImage || '';
    const url = parseBgUrlFromStyle(styleBg);

    // 기본 배너면 없음 처리(라이트/다크)
    if (!url || url === DEFAULTS.bannerLight || url === DEFAULTS.bannerDark) {
      showToast('배너 이미지가 없습니다!');
      return;
    }

    downloadImage(url, `${nick} 배너`);
    showToast('배너 이미지 다운로드 완료!');
  }

  function downloadBoth() {
    downloadProfile();
    downloadBanner();
  }

  // ===== 단축키 설정 =====
  function setShortcutKey() {
    const newKey = prompt('새로운 단축키를 입력하세요!\n\n( 예: Y, Ctrl+Y, Alt+Y, F7 )', shortcutKey);
    if (newKey) {
      GM_setValue('shortcutKey', newKey.toLowerCase());
      shortcutKey = newKey.toLowerCase();
      showToast(`단축키 설정 완료: ${newKey}`);
    } else {
      showToast('단축키 설정이 취소되었습니다.');
    }
  }

  // ===== GM 메뉴 =====
  GM_registerMenuCommand('프로필/배너 모두 다운로드', downloadBoth);
  GM_registerMenuCommand('프로필 이미지만 다운로드', downloadProfile);
  GM_registerMenuCommand('배너 이미지만 다운로드', downloadBanner);
  GM_registerMenuCommand('단축키 설정', setShortcutKey);

  // ===== 단축키 =====
  document.addEventListener('keydown', function(event) {
    let keyPressed = '';
    if (event.ctrlKey) keyPressed += 'ctrl+';
    if (event.shiftKey) keyPressed += 'shift+';
    if (event.altKey) keyPressed += 'alt+';
    keyPressed += event.key.toLowerCase();

    if (keyPressed === shortcutKey) {
      downloadBoth();
    }
  });
})();