// ==UserScript== // @name 即梦图片/视频一键无水印下载 // @namespace http://tampermonkey.net/ // @version 1.0.4 // @description 在即梦(Dreamina)生成页识别图片/视频,并提供一键无水印下载按钮(含初始数据扫描) // @author damnsingle // @match https://jimeng.jianying.com/* // @match https://v2.jimeng.jianying.com/* // @match https://www.dreamina.com/* // @grant GM_download // @grant GM_xmlhttpRequest // @run-at document-start // @license AGPL-3.0 // ==/UserScript== (function () { 'use strict'; const PROCESSED_URLS = new Set(); const MAX_DEDUP_SIZE = 160; const imageCache = new Map(); const videoCache = new Map(); const injected = new WeakSet(); function uuid() { if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID(); return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } function stripImageWatermark(url) { if (!url) return url; try { const u = new URL(url); ['watermark', 'wm', 'mark', 'logo', 'label', 'sign', 'stamp', 'x-oss-process'].forEach(k => u.searchParams.delete(k)); return u.toString(); } catch { return url.replace(/([?&])(?:watermark|wm|mark|logo|label|sign|stamp|x-oss-process)=[^&]*/gi, '$1').replace(/\?&/, '?').replace(/[?&]$/, ''); } } function stripVideoWatermark(url) { if (!url) return url; return url.replace(/lr=video_gen_watermark(?:_dyn)?/g, 'lr=video_gen_no_watermark'); } function showToast(msg, type = 'info') { const colors = { success: '#10b981', error: '#ef4444', info: '#3b82f6' }; const t = document.createElement('div'); t.style.cssText = `position:fixed;right:20px;bottom:20px;z-index:100001;pointer-events:none;color:#fff;padding:10px 16px;border-radius:10px;font-size:13px;background:${colors[type] || colors.info};animation:jmToast 2.4s ease forwards;backdrop-filter:blur(8px);`; t.textContent = (type === 'error' ? '⚠️ ' : '✓ ') + msg; document.body.appendChild(t); if (!document.getElementById('jm-toast-css')) { const s = document.createElement('style'); s.id = 'jm-toast-css'; s.textContent = `@keyframes jmToast{0%{opacity:0;transform:translateY(10px)}12%{opacity:1;transform:translateY(0)}86%{opacity:1}100%{opacity:0;visibility:hidden}}`; document.head.appendChild(s); } setTimeout(() => t.remove(), 2500); } // 使用 GM_xmlhttpRequest 获取图片 blob(绕过 CORS) function gmFetchBlob(url) { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== 'function') return reject(new Error('GM_xmlhttpRequest not available')); GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'blob', onload: (resp) => { if (resp.status >= 200 && resp.status < 300) { resolve(resp.response); } else { reject(new Error('GM_xmlhttpRequest failed: ' + resp.status)); } }, onerror: (err) => reject(new Error('GM_xmlhttpRequest error: ' + (err.error || 'unknown'))) }); }); } async function downloadBlob(url, filename, convertToPng = false) { let blob; try { // 先尝试 GM_xmlhttpRequest(绕过 CORS) blob = await gmFetchBlob(url); } catch { // 降级到普通 fetch const resp = await fetch(url, { credentials: 'include' }); if (!resp.ok) throw new Error('fetch failed'); blob = await resp.blob(); } if (convertToPng) { blob = await convertImageBlobToPng(blob); filename = filename.replace(/\.\w+$/, '.png'); } const href = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = href; a.download = filename; a.rel = 'noopener'; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(href); } function convertImageBlobToPng(blob) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { const canvas = document.createElement('canvas'); canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); canvas.toBlob(b => b ? resolve(b) : reject(new Error('canvas toBlob failed')), 'image/png'); URL.revokeObjectURL(img.src); }; img.onerror = () => reject(new Error('image load failed')); img.src = URL.createObjectURL(blob); }); } function gmDownload(url, filename) { return new Promise((resolve, reject) => { if (typeof GM_download !== 'function') return reject(new Error('GM_download not available')); GM_download({ url, name: filename, saveAs: false, ondone: resolve, onerror: reject }); }); } async function safeDownload(url, filename, forcePng = false) { if (forcePng) { // 需要转 PNG:使用 fetch 方式下载并转换 await downloadBlob(url, filename, true); return; } // 不需要转格式:优先 GM_download try { await gmDownload(url, filename); } catch { await downloadBlob(url, filename); } } function guessImageExt(url) { if (!url) return 'png'; const u = url.split('?')[0].split('#')[0].toLowerCase(); if (u.endsWith('.webp')) return 'webp'; if (u.endsWith('.jpg') || u.endsWith('.jpeg')) return 'jpg'; if (u.endsWith('.png')) return 'png'; if (u.endsWith('.gif')) return 'gif'; if (u.endsWith('.avif')) return 'avif'; if (u.endsWith('.heic')) return 'heic'; if (u.endsWith('.bmp')) return 'bmp'; if (u.endsWith('.tiff') || u.endsWith('.tif')) return 'tiff'; // URL 没有扩展名时,根据 URL 特征猜测 if (/webp|\.w[0-9]/i.test(url)) return 'webp'; return 'png'; } function shouldAcceptImageUrl(u) { return typeof u === 'string' && u.startsWith('http') && /(\.(?:jpe?g|png|webp|gif|bmp|tiff|avif|heic)|image|img|pic|photo|oss|byteimg|bytecdn|snssdk|toutiao|dreamina)/i.test(u); } function shouldAcceptVideoUrl(u) { return typeof u === 'string' && u.startsWith('http') && (/\.mp4$/i.test(u) || /video|mp4|play|download/i.test(u)); } function extractMediaUrls(obj, depth = 0, found = { images: [], videos: [] }) { if (!obj || depth > 14 || typeof obj !== 'object') return found; if (Array.isArray(obj)) { for (const it of obj) extractMediaUrls(it, depth + 1, found); return found; } const pushVideo = (candidate) => { if (!candidate || typeof candidate !== 'string') return; const c = stripVideoWatermark(candidate); if (shouldAcceptVideoUrl(c)) found.videos.push(c); }; const pushImage = (candidate) => { if (!candidate || typeof candidate !== 'string') return; const c = stripImageWatermark(candidate); if (shouldAcceptImageUrl(c)) found.images.push(c); }; if (obj.scene_video_urls || obj.sceneVideoUrls || obj.video_info || obj.videoInfo) { const sv = obj.scene_video_urls || obj.sceneVideoUrls; pushVideo(sv?.download_url || sv?.downloadUrl); pushVideo(obj.video_url || obj.videoUrl || obj.link || obj.url); } if (obj.item_id && (obj.image_url || obj.imageUrl || obj.origin_url || obj.asset_url || obj.uri)) { pushImage(obj.image_url || obj.imageUrl || obj.origin_url || obj.asset_url || obj.uri); } for (const key of Object.keys(obj)) { const val = obj[key]; if (typeof val === 'string') { if (/image_url|imageUrl|origin_url|asset_url|image_ori_raw|download_url|downloadUrl|url|uri|link/i.test(key)) { pushImage(val); pushVideo(val); } } else if (val && typeof val === 'object') { extractMediaUrls(val, depth + 1, found); } } return found; } function storeImage(url) { if (url) imageCache.set(url, { url, cleanUrl: stripImageWatermark(url) }); } function storeVideo(url) { if (url) videoCache.set(url, { url, cleanUrl: stripVideoWatermark(url) }); } function markProcessed(u) { PROCESSED_URLS.add(u); if (PROCESSED_URLS.size > MAX_DEDUP_SIZE) { const first = PROCESSED_URLS.values().next().value; PROCESSED_URLS.delete(first); } } function setupInterceptors() { const origOpen = XMLHttpRequest.prototype.open; const origSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url, ...rest) { this._jmUrl = url; return origOpen.call(this, method, url, ...rest); }; XMLHttpRequest.prototype.send = function (...args) { this.addEventListener('load', () => { try { const u = this._jmUrl || ''; if (!u || PROCESSED_URLS.has(u)) return; const text = this.responseText || ''; if (!(text.includes('image_url') || text.includes('item_id') || text.includes('scene_video_urls') || text.includes('video_url'))) return; const json = JSON.parse(text); const found = extractMediaUrls(json); if (found.images.length) found.images.forEach(storeImage); if (found.videos.length) found.videos.forEach(storeVideo); markProcessed(u); if (found.images.length || found.videos.length) refreshUI(); } catch {} }); return origSend.apply(this, args); }; const origFetch = window.fetch; window.fetch = async function (...args) { const req = args[0]; const url = typeof req === 'string' ? req : req?.url; const resp = await origFetch.apply(this, args); try { if (url && !PROCESSED_URLS.has(url)) { const ct = resp.headers.get('content-type') || ''; if (ct.includes('application/json')) { const json = await resp.clone().json(); const found = extractMediaUrls(json); if (found.images.length) found.images.forEach(storeImage); if (found.videos.length) found.videos.forEach(storeVideo); markProcessed(url); if (found.images.length || found.videos.length) refreshUI(); } } } catch {} return resp; }; } function addStyle() { if (document.getElementById('jm-dl-style')) return; const s = document.createElement('style'); s.id = 'jm-dl-style'; s.textContent = ` .jm-dl-btn{position:absolute;top:10px;right:10px;z-index:9999;display:inline-flex;align-items:center;gap:6px;padding:6px 10px;background:rgba(0,0,0,0.6);color:#fff;border:none;border-radius:7px;font-size:11px;cursor:pointer;backdrop-filter:blur(4px);transition:.2s;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif} .jm-dl-btn:hover:not(:disabled){background:rgba(0,0,0,0.8);transform:scale(1.02)} .jm-dl-btn:active:not(:disabled){transform:scale(0.98)} .jm-dl-btn:disabled{opacity:.6;cursor:not-allowed} .jm-dl-btn.ok{background:rgba(16,185,129,.85)} .jm-dl-btn.err{background:rgba(239,68,68,.82)} .jm-media{position:relative} `; document.head.appendChild(s); } function ensureRelative(el) { const cs = getComputedStyle(el); if (cs.position === 'static') el.style.position = 'relative'; return el; } function parentCard(el) { if (!el) return document.body; const p = el.closest('[class*="card"],[class*="Card"],[class*="item"],[class*="Item"],[class*="result"],[class*="preview"],[class*="wrap"],[class*="container"]') || el.parentElement; return p || document.body; } function createDLButton(label, onClick) { const b = document.createElement('button'); b.className = 'jm-dl-btn'; b.textContent = label; b.onclick = async e => { e.preventDefault(); e.stopPropagation(); b.disabled = true; b.textContent = '下载中...'; try { await onClick(); b.textContent = '✓ 完成'; b.classList.add('ok'); } catch { b.textContent = '重试'; b.classList.add('err'); } setTimeout(() => { if (b.isConnected) { b.textContent = label; b.classList.remove('ok','err'); b.disabled = false; } }, 2000); }; return b; } function injectImage(img, url) { if (injected.has(img)) return; injected.add(img); const card = ensureRelative(parentCard(img)); card.classList.add('jm-media'); if (card.querySelector('.jm-dl-btn')) return; const clean = stripImageWatermark(url); // 原图按钮:下载并转换为 PNG 格式 card.appendChild(createDLButton('原图', async () => safeDownload(clean, `jimeng_${Date.now()}.png`, true))); } function injectVideo(el, url) { if (injected.has(el)) return; injected.add(el); const card = ensureRelative(parentCard(el)); card.classList.add('jm-media'); if (card.querySelector('.jm-dl-btn')) return; const clean = stripVideoWatermark(url); card.appendChild(createDLButton('视频', async () => safeDownload(clean, `jimeng_video_${Date.now()}.mp4`))); } function scanAndInject() { document.querySelectorAll('img').forEach(img => { const src = img.getAttribute('src') || img.currentSrc || ''; if (!src || src.startsWith('data:') || src.startsWith('blob:')) return; if (!shouldAcceptImageUrl(src)) return; const r = img.getBoundingClientRect(); if (r.width < 80 || r.height < 80) return; storeImage(src); injectImage(img, stripImageWatermark(src)); img.dataset.jmDone = '1'; }); document.querySelectorAll('video').forEach(v => { if (v.dataset.jmDone) return; const url = v.getAttribute('src') || v.currentSrc || ''; if (!url || url.startsWith('blob:')) return; storeVideo(url); const clean = stripVideoWatermark(url); injectVideo(v, clean); v.dataset.jmDone = '1'; }); videoCache.forEach(item => { const clean = item.cleanUrl || item.url; document.querySelectorAll(`video[src="${item.url}"]`).forEach(v => { if (v.dataset.jmDone) return; injectVideo(v, clean); v.dataset.jmDone = '1'; }); }); } function scanInitialData() { try { const router = window._ROUTER_DATA; if (router && typeof router === 'object') { const found = extractMediaUrls(router); if (found.images.length) found.images.forEach(storeImage); if (found.videos.length) found.videos.forEach(storeVideo); } const modern = window.__MODERN_SERVER_DATA__; if (modern && typeof modern === 'object') { const found2 = extractMediaUrls(modern); if (found2.images.length) found2.images.forEach(storeImage); if (found2.videos.length) found2.videos.forEach(storeVideo); } document.querySelectorAll('script[type="application/json"]').forEach(el => { try { const json = JSON.parse(el.textContent || '{}'); const found3 = extractMediaUrls(json); if (found3.images.length) found3.images.forEach(storeImage); if (found3.videos.length) found3.videos.forEach(storeVideo); } catch {} }); refreshUI(); } catch {} } function startObserver() { addStyle(); scanAndInject(); const mo = new MutationObserver(() => scanAndInject()); mo.observe(document.documentElement || document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['src', 'poster', 'class', 'style'] }); } let panel = null; function createPanel() { if (panel) return; panel = document.createElement('div'); panel.style.cssText = 'position:fixed;left:20px;bottom:20px;z-index:999999;width:140px;background:rgba(28,28,32,.95);color:#e5e5e5;border:1px solid rgba(255,255,255,.14);border-radius:16px;box-shadow:0 10px 30px rgba(0,0,0,.3);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;overflow:hidden;backdrop-filter:blur(10px)'; const header = document.createElement('div'); header.style.cssText = 'padding:10px 12px;display:flex;justify-content:space-between;align-items:center;background:rgba(255,255,255,.07);cursor:move;font-size:12px;user-select:none'; header.innerHTML = '📥 发现资源−✕'; const body = document.createElement('div'); body.id = 'jm-body'; body.style.padding = '12px'; body.innerHTML = `