// ==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.0.1 // @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 // @downloadURL https://update.greasyfork.icu/scripts/541408/YouTube%20Community%20Post%20Image%20Downloader.user.js // @updateURL https://update.greasyfork.icu/scripts/541408/YouTube%20Community%20Post%20Image%20Downloader.meta.js // ==/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); }); } const observer = new MutationObserver(bindContextMenuEvents); observer.observe(document.body, { childList: true, subtree: true }); bindContextMenuEvents(); })();