YouTube 社区贴文图片下载

右键Youtube社区帖子来自动批量下载图片

// ==UserScript==
// @name           YouTube 社区贴文图片下载
// @name:en        YouTube Community Post Image Downloader
// @description:en Right-click on a YouTube community post to automatically batch download images
// @description    右键Youtube社区帖子来自动批量下载图片
// @version        1.1.0
// @author         kaesinol
// @match          https://*.youtube.com/*/posts
// @match          https://*.youtube.com/post/*
// @match          https://*.youtube.com/@*
// @match          https://*.youtube.com/channel/*/posts
// @grant          GM_download
// @license        MIT
// @namespace https://greasyfork.org/users/1243515
// ==/UserScript==

(function () {
  'use strict';

  function downloadImage(url, filename) {
    GM_download({
      url: url,
      name: filename,
      onerror: err => console.error('Download failed:', err),
      ontimeout: () => console.warn('Download timeout:', url),
    });
  }

  function sanitizeFilename(name) {
    return name.replace(/[\\/:*?"<>|]+/g, '').slice(0, 100);
  }

  function getFileExtensionFromMime(mime) {
    const map = {
      'image/jpeg': 'jpg',
      'image/png': 'png',
      'image/webp': 'webp',
      'image/gif': 'gif'
    };
    return map[mime] || 'png'; // 默认 png
  }

  async function fetchMimeType(url) {
    try {
      const response = await fetch(url, { method: 'HEAD' });
      const mime = response.headers.get('Content-Type');
      return getFileExtensionFromMime(mime);
    } catch (e) {
      return 'png'; //fallback
    }
  }
  function getOriginalUrl(rawUrl) {
    if (!rawUrl) return rawUrl;
    const match = rawUrl.match(/(\S*?)-c-fcrop64=[^=]*/);
    let base = match ? match[1] : rawUrl;
    base = base.replace(/=s\d+/, '=s0');
    if (!base.includes('=s0')) base += '=s0';
    console.info(base);
    return base;

  }

  async function handleRightClick(event) {
    const container = event.currentTarget;

    // 找 #author-text 内的 href
    const authorLink = container.querySelector('#author-text');
    let authorHref = authorLink ? authorLink.getAttribute('href') || '' : '';
    authorHref = sanitizeFilename(authorHref || 'author');

    const imgs = container.querySelectorAll('#content-attachment img');
    const id = container.querySelector('a[href*="/post/"]').href.split('/post/')[1];
    imgs.forEach(async (img, i) => {
      const rawUrl = img.src || img.getAttribute('src');
      if (!rawUrl) return;
      const imgUrl = getOriginalUrl(rawUrl);
      const ext = await fetchMimeType(imgUrl);
      const filename = `${authorHref} - ${id} - ${i + 1}.${ext}`;
      console.info(filename);
      downloadImage(imgUrl, filename);
    });
    event.preventDefault();
  }

  function bindContextMenuEvents() {
    const posts = document.querySelectorAll('#body.style-scope.ytd-backstage-post-renderer');
    posts.forEach(post => {
      post.removeEventListener('contextmenu', handleRightClick);
      post.addEventListener('contextmenu', handleRightClick);
    });
  }
  setInterval(bindContextMenuEvents, 2000);
})();