# dreamina-tool **Repository Path**: Andy688/dreamina-tool ## Basic Information - **Project Name**: dreamina-tool - **Description**: Dreamina/CapCut 无水印视频下载 - **Primary Language**: JavaScript - **License**: AGPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-04-27 - **Last Updated**: 2026-04-27 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Dreamina/CapCut 无水印视频下载脚本使用教程 本教程适用于当前目录下的 `水印,js` 脚本。脚本用于在 Dreamina/CapCut 页面中捕获接口返回的原始视频地址,并在视频区域显示一个「无水印视频下载」按钮。 ## 操作界面截图 ![Dreamina/CapCut 页面中的无水印视频下载按钮](src/222.png) ## 1. 准备工作 你需要先安装一个支持用户脚本的浏览器扩展。推荐使用脚本猫: - 脚本猫官网:https://scriptcat.org/ - 支持浏览器:Chrome、Edge、Firefox 等主流浏览器 安装完成后,浏览器右上角会出现脚本猫图标。 ## 2. 在脚本猫中新建脚本 1. 打开浏览器右上角的「脚本猫」图标。 2. 点击「管理面板」。 3. 点击「新建脚本」。 4. 选择「空白脚本」或「新建用户脚本」。 5. 删除编辑器里的默认内容。 6. 打开本项目中的 `水印,js` 文件,复制全部代码。 7. 粘贴到脚本猫编辑器中。 8. 点击「保存」。 完整js代码: ```js // ==UserScript== // @name Dreamina/CapCut 捕获无水印视频下载 // @namespace https://scriptcat.org/ // @version 1.2.1 // @description 从 Dreamina/CapCut 页面接口响应中捕获 video_model 内的原始视频地址并下载 // @match https://dreamina.capcut.com/* // @match https://www.dreamina.capcut.com/* // @run-at document-start // @inject-into page // @grant GM_download // @connect * // ==/UserScript== (function () { 'use strict'; const VIDEO_BUTTON_CLASS = 'dreamina-origin-video-download'; const LOCAL_ITEM_TEMPLATE_KEY = '__dreaminaLocalItemListRequestTemplate'; const FIRST_USE_TIP_KEY = '__dreaminaNoWatermarkFirstUseTipShown'; const DEBUG = false; const candidates = new Map(); let pendingDownloadButton = null; let pendingDownloadFileName = ''; let lastLocalItemListRequest = loadLocalItemListTemplate(); let lastLocalItemId = getTemplateItemId(lastLocalItemListRequest); installNetworkHook(); function addCandidates(items) { let added = false; for (const item of items || []) { const url = normalizeMediaUrl(item.url); if (!isProbablyVideo(url)) continue; if (!url || candidates.has(url)) continue; const normalizedItem = { ...item, url, }; candidates.set(url, normalizedItem); added = true; console.log('[Dreamina 捕获媒体]', normalizedItem); } if (added && pendingDownloadButton) { const button = pendingDownloadButton; const fileName = pendingDownloadFileName || ensureExt(getFileName(), 'mp4'); const item = getBestVideoCandidate(); pendingDownloadButton = null; pendingDownloadFileName = ''; if (item) { console.log('[Dreamina] 等待捕获完成,自动下载:', item); download(item.url, fileName, button); } } } let scanTimer = null; function boot() { if (!document.body) { setTimeout(boot, 300); return; } injectStyle(); scan(); new MutationObserver(() => { clearTimeout(scanTimer); scanTimer = setTimeout(scan, 500); }).observe(document.body, { childList: true, subtree: true, attributes: true, }); } function scan() { document.querySelectorAll('video').forEach(video => { if (video.dataset.dreaminaOriginButtonAdded === 'true') return; const wrapper = findGoodContainer(video); if (!wrapper || wrapper.querySelector('.' + VIDEO_BUTTON_CLASS)) return; if (getComputedStyle(wrapper).position === 'static') { wrapper.style.position = 'relative'; } const button = document.createElement('button'); button.type = 'button'; button.className = VIDEO_BUTTON_CLASS; button.textContent = '无水印视频下载'; button.dataset.normalText = '无水印视频下载'; button.title = '优先下载 video_model.video_list 中的原始视频地址'; button.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); requestDownload(button); }); wrapper.appendChild(button); video.dataset.dreaminaOriginButtonAdded = 'true'; }); } function getBestVideoCandidate() { const list = Array.from(candidates.values()) .filter(item => isProbablyVideo(item.url)) .map(item => ({ ...item, score: scoreCandidate(item), })) .sort((a, b) => b.score - a.score); console.log('[Dreamina] 视频候选排序:', list); const best = list[0]; if (!best) return null; if (best.score < 20) { return null; } return best; } function scoreCandidate(item) { const url = String(item.url || '').toLowerCase(); const path = String(item.path || '').toLowerCase(); let score = 0; if (/\.mp4(\?|$)|mime_type=video_mp4/i.test(url)) score += 80; if (/video|play|media/.test(url)) score += 40; if (/origin|original|source|src|download|main|watermark_free|no_watermark|without_watermark/.test(path)) score += 100; if (/origin|original|source|download|main/.test(url)) score += 60; if (/video_model.*video_list.*main_url/.test(path)) score += 500; if (/video_model.*video_list.*backup_url/.test(path)) score += 450; if (/decoded_base64/.test(path)) score += 160; if (/transcoded_video.*origin.*video_url/.test(path)) score -= 260; if (/display_watermark_aigc/.test(path)) score -= 500; if (/preview|watermark|wm|cover|poster|thumb|thumbnail/.test(path)) score -= 120; if (/watermark|preview|wm|cover|poster|thumb|thumbnail/.test(url)) score -= 120; if (/\.m3u8(\?|$)/i.test(url)) score -= 20; return score; } async function requestDownload(button) { const item = getBestVideoCandidate(); const fileName = ensureExt(getFileName(), 'mp4'); if (item) { console.log('[Dreamina] 选中下载地址:', item); download(item.url, fileName, button); return; } button.textContent = '请求原始地址...'; button.disabled = true; button.classList.add('is-waiting'); const requested = await requestLocalItemListFromTemplate(); const requestedItem = getBestVideoCandidate(); if (requestedItem) { button.disabled = false; console.log('[Dreamina] 自动接口请求成功,选中下载地址:', requestedItem); download(requestedItem.url, fileName, button); return; } button.disabled = false; pendingDownloadButton = button; pendingDownloadFileName = fileName; button.textContent = '等待官网接口...'; button.title = '现在点击网页自带下载入口,脚本捕获到原始地址后会自动下载'; button.classList.add('is-waiting'); showFallbackTip(requested); console.log('[Dreamina] 当前视频候选:', Array.from(candidates.values())); } function showFallbackTip(requested) { const firstUseTipShown = localStorage.getItem(FIRST_USE_TIP_KEY) === '1'; if (!requested && !firstUseTipShown) { localStorage.setItem(FIRST_USE_TIP_KEY, '1'); alert('首次使用提示:\n\n1. 先点击 Dreamina/CapCut 网页自带的「下载原视频」或「有水印下载」按钮。\n2. 脚本会捕获原始视频接口。\n3. 再点击蓝色「无水印视频下载」按钮,即可下载无水印版本。\n\n本次已进入等待状态:你现在点击一次网页自带下载按钮,脚本捕获到接口后会自动下载无水印版本。'); return; } alert((requested ? '自动请求没有拿到可用原始地址。' : '还没有可复用的下载接口模板。') + '\n\n已进入等待状态:现在点击一次网页自带的「下载原视频」或「有水印下载」按钮,脚本捕获到接口后会自动下载无水印版本。'); } function isProbablyVideo(url) { if (!/^https?:\/\//i.test(url)) return false; if (isProbablyImage(url)) return false; return /\.mp4(\?|$)/i.test(url) || /\.m3u8(\?|$)/i.test(url) || /mime_type=video_mp4/i.test(url) || /\/video\//i.test(url) || /\/video\/fplay\//i.test(url); } function normalizeMediaUrl(url) { if (!url) return ''; let normalized = String(url).trim(); if (/^http:\/\/[^/]+\/.*(?:mime_type=video_mp4|\/video\/)/i.test(normalized)) { normalized = normalized.replace(/^http:/i, 'https:'); } return normalized; } function isProbablyImage(url) { return /\.(png|jpe?g|webp|gif|avif|svg)(\?|$)/i.test(url) || /image_url|avatar|cover|poster|thumb|thumbnail|tplv-|\.image|\.webp/i.test(url); } function download(url, fileName, button) { const oldText = button.dataset.normalText || button.textContent; button.classList.remove('is-waiting'); button.textContent = '下载中...'; button.disabled = true; if (typeof GM_download !== 'function') { const a = document.createElement('a'); a.href = url; a.download = fileName; a.rel = 'noopener'; document.body.appendChild(a); a.click(); a.remove(); button.textContent = oldText; button.disabled = false; button.classList.remove('is-waiting'); return; } GM_download({ url, name: fileName, saveAs: false, onload: () => { button.textContent = oldText; button.disabled = false; button.classList.remove('is-waiting'); }, onerror: err => { button.textContent = oldText; button.disabled = false; button.classList.remove('is-waiting'); console.error('[Dreamina] 下载失败:', err, url); alert('下载失败。可以打开控制台查看捕获到的视频候选地址。'); }, }); } function installNetworkHook() { if (window.__dreaminaHookInstalled) return; window.__dreaminaHookInstalled = true; console.info('[Dreamina] 网络捕获已安装 v1.2.1'); const oldFetch = window.fetch; window.fetch = function () { const args = arguments; captureFetchRequest(args); return oldFetch.apply(this, args).then(response => { try { const url = response.url || String(args[0] || ''); if (!shouldParseResponse(url, response.headers && response.headers.get('content-type'))) { return; } response.clone().text().then(text => { parsePayload(text, 'fetch:' + url); }).catch(() => {}); } catch (e) {} return response; }); }; const oldOpen = XMLHttpRequest.prototype.open; const oldSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader; const oldSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url) { this.__dreaminaMethod = method; this.__dreaminaUrl = url; this.__dreaminaHeaders = {}; return oldOpen.apply(this, arguments); }; XMLHttpRequest.prototype.setRequestHeader = function (name, value) { try { this.__dreaminaHeaders = this.__dreaminaHeaders || {}; this.__dreaminaHeaders[name] = value; } catch (e) {} return oldSetRequestHeader.apply(this, arguments); }; XMLHttpRequest.prototype.send = function () { captureXhrRequest(this, arguments[0]); this.addEventListener('load', function () { try { const url = String(this.__dreaminaUrl || ''); const contentType = this.getResponseHeader && this.getResponseHeader('content-type'); if (!shouldParseResponse(url, contentType)) { return; } parsePayload(getXhrPayload(this), 'xhr:' + url); } catch (e) {} }); return oldSend.apply(this, arguments); }; } function captureFetchRequest(args) { try { const input = args[0]; const init = args[1] || {}; const url = typeof input === 'string' ? input : input && input.url; if (!/get_local_item_list/i.test(String(url || ''))) return; rememberLocalItemListTemplate({ url: String(url), method: init.method || 'POST', body: normalizeRequestBody(init.body), headers: headersToObject(init.headers), }); } catch (e) {} } function captureXhrRequest(xhr, body) { try { const url = String(xhr.__dreaminaUrl || ''); if (!/get_local_item_list/i.test(url)) return; rememberLocalItemListTemplate({ url, method: xhr.__dreaminaMethod || 'POST', body: normalizeRequestBody(body), headers: xhr.__dreaminaHeaders || {}, }); } catch (e) {} } function rememberLocalItemListTemplate(template) { if (!template || !template.url || !template.body) return; lastLocalItemListRequest = { url: template.url, method: template.method || 'POST', body: template.body, headers: template.headers || {}, savedAt: Date.now(), }; lastLocalItemId = getTemplateItemId(lastLocalItemListRequest) || lastLocalItemId; try { localStorage.setItem(LOCAL_ITEM_TEMPLATE_KEY, JSON.stringify(lastLocalItemListRequest)); } catch (e) {} console.info('[Dreamina] 已记录 get_local_item_list 请求模板:', lastLocalItemListRequest); } function loadLocalItemListTemplate() { try { const raw = localStorage.getItem(LOCAL_ITEM_TEMPLATE_KEY); return raw ? JSON.parse(raw) : null; } catch (e) { return null; } } async function requestLocalItemListFromTemplate() { const template = lastLocalItemListRequest || loadLocalItemListTemplate(); if (!template || !template.url || !template.body) { return false; } const body = buildLocalItemListBody(template.body); if (!body) { return false; } try { console.info('[Dreamina] 自动请求 get_local_item_list:', { url: template.url, body, }); const response = await fetch(template.url, { method: template.method || 'POST', headers: buildReplayHeaders(template.headers), body, credentials: 'include', }); const text = await response.text(); parsePayload(text, 'auto-fetch:' + template.url); return response.ok; } catch (error) { console.warn('[Dreamina] 自动请求 get_local_item_list 失败:', error); return false; } } function buildLocalItemListBody(templateBody) { let data; try { data = typeof templateBody === 'string' ? JSON.parse(templateBody) : JSON.parse(JSON.stringify(templateBody)); } catch (e) { return ''; } const itemId = inferCurrentItemId() || data.item_id_list && data.item_id_list[0]; if (!itemId) { return ''; } data.item_id_list = [String(itemId)]; data.pack_item_opt = data.pack_item_opt || { scene: 1, need_data_integrity: true, }; data.is_for_video_download = true; return JSON.stringify(data); } function inferCurrentItemId() { if (lastLocalItemId) { return lastLocalItemId; } try { const text = document.body ? document.body.innerText : ''; const ids = text.match(/\b7\d{18}\b/g); if (ids && ids.length) { return ids[0]; } } catch (e) {} try { const raw = lastLocalItemListRequest && lastLocalItemListRequest.body; const data = typeof raw === 'string' ? JSON.parse(raw) : raw; return data && data.item_id_list && data.item_id_list[0]; } catch (e) {} return ''; } function getTemplateItemId(template) { try { if (!template || !template.body) return ''; const data = typeof template.body === 'string' ? JSON.parse(template.body) : template.body; return data && data.item_id_list && data.item_id_list[0] ? String(data.item_id_list[0]) : ''; } catch (e) { return ''; } } function buildReplayHeaders(headers) { const result = { accept: 'application/json, text/plain, */*', 'content-type': 'application/json', }; Object.entries(headers || {}).forEach(([name, value]) => { const lower = String(name).toLowerCase(); if (/^(host|origin|referer|cookie|content-length)$/i.test(lower)) { return; } result[name] = value; }); return result; } function normalizeRequestBody(body) { if (!body) return ''; if (typeof body === 'string') { return body; } try { return JSON.stringify(body); } catch (e) { return ''; } } function headersToObject(headers) { const result = {}; if (!headers) { return result; } try { if (headers instanceof Headers) { headers.forEach((value, key) => { result[key] = value; }); return result; } if (Array.isArray(headers)) { headers.forEach(([key, value]) => { result[key] = value; }); return result; } return { ...headers, }; } catch (e) { return result; } } function getXhrPayload(xhr) { if (typeof xhr.response === 'string') { return xhr.response; } if (xhr.response && typeof xhr.response === 'object') { return xhr.response; } try { return xhr.responseText; } catch (e) { return ''; } } function shouldParseResponse(url, contentType) { const cleanUrl = String(url || ''); const cleanType = String(contentType || ''); if (!/json/i.test(cleanType) && !/get_local_item_list|download|export|task|item|aigc|generate|video/i.test(cleanUrl)) { return false; } if (/passport|user_info|account\/info|benefit|subscription|report|monitor|mcs-normal|slardar|get_image|image|template|material/i.test(cleanUrl)) { return DEBUG; } if (/get_local_item_list/i.test(cleanUrl)) { return true; } return /download|export|task|video/i.test(cleanUrl) && /mweb-api|dreamina|capcut|capcutapi/i.test(cleanUrl); } function parsePayload(payload, source) { if (!payload) return; let data = payload; if (typeof payload === 'string') { if (payload.length > 15000000) return; try { data = JSON.parse(payload); } catch (e) { return; } } const output = []; collectFromValue(data, source || 'response', output, 0); if (/get_local_item_list/i.test(source || '')) { lastLocalItemId = getFirstLocalItemId(data) || lastLocalItemId; console.info('[Dreamina] get_local_item_list 已解析,视频候选数:', output.filter(item => isProbablyVideo(item.url)).length, output); } addCandidates(output); } function getFirstLocalItemId(data) { try { const item = data && data.data && data.data.item_list && data.data.item_list[0]; return item && item.common_attr && item.common_attr.id ? String(item.common_attr.id) : ''; } catch (e) { return ''; } } function collectFromValue(value, path, output, depth) { if (!value || depth > 16) return; if (typeof value === 'string') { if (/^https?:\/\//i.test(value) && isProbablyVideo(value)) { output.push({ url: value, path: path, }); } const decodedUrl = decodeBase64Url(value); if (decodedUrl && /(mp4|m3u8|video|tos|media|play|download)/i.test(decodedUrl)) { output.push({ url: decodedUrl, path: path + '.decoded_base64', }); } const trimmed = value.trim(); if ((trimmed.charAt(0) === '{' || trimmed.charAt(0) === '[') && trimmed.length < 3000000) { try { collectFromValue(JSON.parse(trimmed), path + '.json', output, depth + 1); } catch (e) {} } return; } if (Array.isArray(value)) { value.forEach((item, index) => { collectFromValue(item, path + '[' + index + ']', output, depth + 1); }); return; } if (typeof value === 'object') { Object.keys(value).forEach(key => { collectFromValue(value[key], path ? path + '.' + key : key, output, depth + 1); }); } } function decodeBase64Url(value) { if (!value || value.length < 40 || value.length > 3000) return ''; if (!/^[A-Za-z0-9+/=_-]+$/.test(value)) return ''; try { let normalized = value.replace(/-/g, '+').replace(/_/g, '/'); while (normalized.length % 4) normalized += '='; let decoded = atob(normalized); try { decoded = decodeURIComponent(escape(decoded)); } catch (e) {} if (/^https?:\/\//i.test(decoded)) { return decoded; } } catch (e) {} return ''; } function findGoodContainer(el) { let node = el; for (let i = 0; i < 6 && node; i++) { const rect = node.getBoundingClientRect(); if (rect.width >= 240 && rect.height >= 160) { return node; } node = node.parentElement; } return el.parentElement; } function getFileName() { const text = findPromptText(); const time = getYmdHMS(); return sanitizeFileName((text || 'Dreamina无水印视频') + '-' + time); } function findPromptText() { const selectors = [ '[class*="prompt"]', '[data-testid*="prompt"]', 'textarea', 'input', ]; for (const selector of selectors) { for (const node of document.querySelectorAll(selector)) { const text = (node.value || node.textContent || '').trim(); if (text && text.length > 2 && text.length < 160 && !/prompt/i.test(text)) { return text; } } } return ''; } function sanitizeFileName(fileName) { return fileName .replace(/[\n\r]/g, '') .replace(/[\\/:*?"<>|]/g, '_') .replace(/\s+/g, ' ') .slice(0, 180); } function ensureExt(fileName, ext) { ext = ext.replace(/^\./, ''); return new RegExp('\\.' + ext + '$', 'i').test(fileName) ? fileName : fileName + '.' + ext; } function getYmdHMS() { const d = new Date(); return [ d.getFullYear(), String(d.getMonth() + 1).padStart(2, '0'), String(d.getDate()).padStart(2, '0'), String(d.getHours()).padStart(2, '0'), String(d.getMinutes()).padStart(2, '0'), String(d.getSeconds()).padStart(2, '0'), ].join(''); } function injectStyle() { if (document.querySelector('#dreamina-origin-download-style')) return; const style = document.createElement('style'); style.id = 'dreamina-origin-download-style'; style.textContent = ` .${VIDEO_BUTTON_CLASS} { position: absolute; top: 10px; right: 10px; z-index: 999999; border: 0; border-radius: 4px; background: #1677ff; color: #fff; padding: 8px 12px; font-size: 14px; line-height: 20px; cursor: pointer; box-shadow: 0 2px 8px rgba(0, 0, 0, .18); } .${VIDEO_BUTTON_CLASS}:hover { background: #0958d9; } .${VIDEO_BUTTON_CLASS}:disabled { cursor: not-allowed; opacity: .65; } .${VIDEO_BUTTON_CLASS}.is-waiting { background: #fa8c16; } `; document.head.appendChild(style); } boot(); })(); ``` 保存后,脚本猫会识别脚本头部信息: ```js // @match https://dreamina.capcut.com/* // @match https://www.dreamina.capcut.com/* // @grant GM_download // @connect * ``` 这些配置表示脚本只会在 Dreamina/CapCut 页面运行,并使用脚本猫的下载能力。 ## 3. 确认脚本已启用 进入脚本猫管理面板后,确认这个脚本处于启用状态。 脚本名称一般显示为: ```text Dreamina/CapCut 捕获无水印视频下载 ``` 如果脚本左侧或右侧有开关,请确保开关是打开的。 ## 4. 使用方法 1. 打开 Dreamina/CapCut 页面: ```text https://dreamina.capcut.com/ ``` 2. 登录你的账号。 3. 进入有视频预览的页面,等待页面加载完成。 4. 视频区域右上角会出现蓝色按钮: ```text 无水印视频下载 ``` 5. 点击该按钮。 如果脚本已经捕获到可用的原始视频地址,会立即开始下载。 ## 5. 首次使用时的操作 第一次使用时,脚本可能还没有捕获到 Dreamina/CapCut 的下载接口模板。此时点击「无水印视频下载」按钮后,可能会提示你: ![首次使用提示弹窗](src/111.png) ```text 现在点击一次网页自带的「下载原视频」或「有水印下载」按钮 ``` 请按下面步骤操作: 1. 先点击脚本生成的蓝色「无水印视频下载」按钮。 2. 如果出现提示,点击确认。 3. 再点击 Dreamina/CapCut 网页自带的「下载原视频」或「有水印下载」按钮。 4. 脚本会监听网页接口响应,捕获原始视频地址。 5. 捕获成功后,脚本会自动触发无水印视频下载。 之后脚本会把可复用的接口请求模板保存在浏览器本地,后续使用通常可以直接点击蓝色按钮下载。 ## 6. 下载文件名规则 脚本会尽量从页面里的提示词、输入框或相关文本中提取文件名。 如果没有找到合适文本,会使用默认名称: ```text Dreamina无水印视频-年月日时分秒.mp4 ``` 文件名中的非法字符会自动替换,避免保存失败。 ## 7. 常见问题 ### 页面上没有出现按钮 可以尝试: - 确认脚本猫中脚本已经启用。 - 确认当前网址是 `dreamina.capcut.com` 或 `www.dreamina.capcut.com`。 - 刷新页面。 - 等待视频真正加载出来后再看视频区域右上角。 ### 点击后一直显示等待官网接口 说明脚本暂时还没有捕获到可用的视频接口。 处理方法: 1. 保持当前页面不要关闭。 2. 点击 Dreamina/CapCut 页面自带的下载按钮。 3. 等待脚本自动捕获接口。 4. 捕获成功后会自动下载,或再次点击蓝色按钮。 ### 下载失败 可能原因: - 视频地址已经过期。 - 浏览器或脚本猫拦截了下载。 - 当前页面接口返回的数据发生变化。 - 网络连接不稳定。 可以尝试刷新页面后重新生成或重新打开视频,再点击下载按钮。 ### 浏览器提示跨域或连接权限 脚本头部已经包含: ```js // @connect * ``` 如果脚本猫弹出权限确认,请允许脚本访问相关域名,否则可能无法下载视频。 ## 8. 更新脚本 如果后续修改了 `水印,js`,需要同步更新脚本猫中的代码: 1. 打开脚本猫管理面板。 2. 找到「Dreamina/CapCut 捕获无水印视频下载」。 3. 点击编辑。 4. 删除旧代码。 5. 粘贴新的 `水印,js` 全部内容。 6. 保存。 7. 刷新 Dreamina/CapCut 页面。 ## 9. 注意事项 - 本脚本仅用于辅助下载你自己账号中可访问的视频内容。 - 请遵守 Dreamina/CapCut 平台规则以及相关版权要求。 - 如果 Dreamina/CapCut 更新了接口结构,脚本可能需要同步调整。 - 脚本会在浏览器本地保存一次下载接口请求模板,用于后续自动请求,不会上传到其它服务器。