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

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

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

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

You will need to install an extension such as Tampermonkey to install this script.

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

您需要先安装一个扩展,例如 篡改猴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();
    }
  });
})();