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

Soop 채널(방송국)의 프로필/배너 이미지를 닉네임 파일명으로 다운로드 (커스텀 토스트, 기본이미지 판별, toast-box 내부 append) - 프로필/닉네임 최신 셀렉터 반영 + 파일명 .png 고정 저장

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         SOOP 채널 프로필 & 배너 이미지 다운로드
// @namespace    http://tampermonkey.net/
// @version      3.2
// @description  Soop 채널(방송국)의 프로필/배너 이미지를 닉네임 파일명으로 다운로드 (커스텀 토스트, 기본이미지 판별, toast-box 내부 append) - 프로필/닉네임 최신 셀렉터 반영 + 파일명 .png 고정 저장
// @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',

    // 프로필 IMG: NEW(사이드바) 1순위 + OLD(상단 비주얼) 폴백
    profileImgList: [
      '#soop_wrap > div.__soopui__Sidebar-module__Sidebar___CjdhU.__soopui__Sidebar-module__Expanded___DPQe9.ServiceLeftMenu_ChannelServiceLeftMenu__C6J0o.ServiceLeftMenu_expanded__IfzLg > div.__soopui__InnerLnb-module__InnerLnb___qASDV.__soopui__InnerLnb-module__Expanded___hmDdf.ServiceLeftMenu_innerLnb__0hMfP > div.ProfileInfo_streamer__tEqni > div.ProfileInfo_profileImg__mz9Nz > a > span > div > img',
      '#layout_container__S7ueh > div.ChannelVisual_channelVisual__a2JA_ > div.ChannelVisual_channelInfoWrapper__ovnvh > div > div.StreamerInfo_streamer__ZhKoB > div.StreamerInfo_profileImg__EC1En > span > div > img'
    ],

    // 닉네임: NEW(사이드바) 1순위 + OLD 폴백
    nicknameList: [
      '#soop_wrap > div.__soopui__Sidebar-module__Sidebar___CjdhU.__soopui__Sidebar-module__Expanded___DPQe9.ServiceLeftMenu_ChannelServiceLeftMenu__C6J0o.ServiceLeftMenu_expanded__IfzLg > div.__soopui__InnerLnb-module__InnerLnb___qASDV.__soopui__InnerLnb-module__Expanded___hmDdf.ServiceLeftMenu_innerLnb__0hMfP > div.ProfileInfo_streamer__tEqni > div.ProfileInfo_nicknameWrapper__LdDE0 > p', // <p class="ProfileInfo_nick__ZOvu9">마동근</p>
      '#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'
  };

  // ===== 토스트 =====
  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);

    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] : '';
  }

  // (남겨두지만 현재는 .png 고정 저장을 사용)
  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 ensurePngName(base) {
    const safe = sanitizeFilenameBase(base);
    return safe.toLowerCase().endsWith('.png') ? safe : `${safe}.png`;
  }

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

  function queryFirst(selectorList) {
    for (const sel of selectorList) {
      const el = document.querySelector(sel);
      if (el) return el;
    }
    return null;
  }

  function getNickname() {
    // 1) 새 구조(사이드바)
    const nickEl = queryFirst(SELECTORS.nicknameList);
    const nickTxt = nickEl?.textContent?.trim();
    if (nickTxt) return nickTxt;

    // 2) 폴백: 프로필 alt
    const imgEl = queryFirst(SELECTORS.profileImgList);
    const alt = imgEl?.getAttribute('alt')?.trim();
    if (alt) return alt;

    // 3) 최종: station/<id>
    return getChannelIdFromUrl();
  }

  // ===== 이름을 .png로 고정해서 저장 =====
  function downloadImage(url, filenameBase) {
    const name = ensurePngName(filenameBase); // 확장자 .png 고정 (내용 포맷 변환 아님)
    GM_download({
      url,
      name,
      onerror: (err) => {
        console.error(`Failed to download ${name}:`, err);
      }
    });
  }

  // ===== 프로필 =====
  function downloadProfile() {
    const nick = getNickname(); // 예: 마동근
    const imgEl = queryFirst(SELECTORS.profileImgList);
    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();
    }
  });
})();