diff --git a/README.md b/README.md index ba5d5f5..046878a 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,45 @@ -# res-downloader -#### 爱享素材下载器 - -💪 支持视频、音频、图片、m3u8等网络资源下载 -📦 支持微信视频号、小程序、抖音、快手、小红书、酷狗音乐、qq音乐等网络资源下载 -🍊 支持设置代理以获取特殊网络下的资源 +# V2.0重磅更新,所见即所得! +## res-downloader(爱享素材下载器) +🖥️ 支持Win10、Win11、Mac +🌐 支持视频、音频、图片、m3u8等网络资源下载 +💪 支持微信视频号、小程序、抖音、快手、小红书、酷狗音乐、qq音乐等网络资源下载 +👼 支持设置代理以获取特殊网络下的资源 ## 软件下载 🆕 [github下载](https://github.com/putyy/res-downloader/releases) 🆕 [蓝奏云下载 密码:9vs5](https://wwjv.lanzoum.com/b04wgtfyb) - -## 二次开发 -> ps: 打包慢的问题可以参考 https://www.putyy.com/articles/87 -```sh -git clone https://github.com/putyy/res-downloader - -cd res-downloader - -yarn install - -yarn run dev - -# 打包mac -yarn run build --universal --mac - -# 打包win -yarn run build --win -``` - ## 使用方法 +> 0. 安装一定要同意安装证书文件,安装一定要同意安装证书文件,安装一定要同意安装证书文件! > 1. 打开本软件 > 2. 软件首页选择要获取的资源类型(默认选中的视频) > 3. 打开要捕获的源, 如:视频号、网页、小程序等等 -> 4. 返回软件首页即可看到要下载的资源 - -## 常见问题 -> 1. 无法拦截获取 -> > 手动检测系统代理是否设置正确 本软件代理地址: 127.0.0.1:8899 -> 2. 关闭软件后无法正常上网 -> > 手动关闭系统代理设置 -> 3. 视频号抓取流程 -> > 将需要下载的视频发给好友或者文件助手 再打开即可拦截,通常会出现解密下载按钮 -> > -> > 大视频可以复制链接通过其他工具加速下载,然后再通过对应的视频操作项进行"视频解密" +> 4. 返回软件首页即可看到资源列表 ## 软件截图 ![](public/show.webp) -## 实现原理 -> 通过代理网络抓包拦截响应,筛选出有用的资源,同fiddler、charles等抓包软件、浏览器F12打开控制也能达到目的,只不过这些软件需要手动进行筛选,对于小白用户上手还是有点难度,所以就有了本项目这样的软件。 +## 常见问题 +下载慢、大视频下载失败 +> 推荐使用如下工具加速下载,视频号可以下载完成后再到对应视频操作项选择 “视频解密(视频号)” 按钮 +>> [Neat Download Manager](https://www.neatdownloadmanager.com/index.php/en/)、[Motrix](https://motrix.app/download)等软件进行下载 + +Win7无法使用 +> 软件不支持,也无计划支持 + +打开本软件,无法正常拦截获取 +> 检查系统代理是否正确设置 代理地址:127.0.0.1 端口:8899 -## 参考项目 +关闭软件后无法正常上网 +> 手动关闭系统代理设置 -- [WeChatVideoDownloader](https://github.com/lecepin/WeChatVideoDownloader) 原项目是react写的,本项目参考原项目用vue3重写了一下,核心逻辑没什么变化,主要是增加了一些新的功能,再次感谢! +打开本软件后无法上网 +> 手动删除安装标识锁文件,之后再打开软件会进行检查证书是否正确安装 +>> MAC: /Users/你的用户名称/.res-downloader@putyy/res-downloader-installed.lock +>> Win: C:\Users\Admin\.res-downloader@putyy/res-downloader-installed.lock +#### 更多问题见: [issues](https://github.com/putyy/res-downloader/issues)、[爱享论坛](https://s.gowas.cn/d/4089-quan-ping-tai-zi-yuan-xia-zai-ruan-jian-zui-xin-ban-v106/171) + +## 免责声明 +本软件用于学习研究使用,若因使用本软件造成的一切法律责任均与本人无关! +``` diff --git a/electron/main/ipc.ts b/electron/main/ipc.ts index e2975ea..ea61bfd 100755 --- a/electron/main/ipc.ts +++ b/electron/main/ipc.ts @@ -5,16 +5,12 @@ import {downloadFile, decodeWxFile, suffix} from './utils' // @ts-ignore import {hexMD5} from '../../src/common/md5' import fs from "fs" -import CryptoJS from 'crypto-js' import {floor} from "lodash" -let getMac = require("getmac").default let win: BrowserWindow let previewWin: BrowserWindow let isStartProxy = false -let aesKey = "as5d45as4d6qe6wqfar6gt4749q6y7w6h34v64tv7t37ty5qwtv6t6qv" - export default function initIPC() { ipcMain.handle('invoke_app_is_init', async (event, arg) => { @@ -67,16 +63,15 @@ export default function initIPC() { return {is_file: res, fileName: `${save_path}/${fileName}.mp4`} }) - ipcMain.handle('invoke_down_file', async (event, {data, save_path, description}) => { - let down_url = data.down_url + ipcMain.handle('invoke_down_file', async (event, {data, save_path}) => { + let down_url = data.url if (!down_url) { return false } - - let fileName = description ? description.replace(/[^a-zA-Z\u4e00-\u9fa5]/g, '') : hexMD5(down_url); + let fileName = data?.description ? data.description.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '') : hexMD5(down_url); let save_path_file = `${save_path}/${fileName}` + suffix(data.type) - if (process.platform === 'win32'){ + if (process.platform === 'win32') { save_path_file = `${save_path}\\${fileName}` + suffix(data.type) } @@ -92,20 +87,10 @@ export default function initIPC() { win?.webContents.send('on_down_file_schedule', {schedule: floor(res * 100)}) } ).catch(err => { + // console.log("err:", err) return false }) - }) - - ipcMain.handle('invoke_get_mac', async (event) => { - let mac = getMac() - if (mac === "") { - return "" - } - return CryptoJS.AES.encrypt(mac, CryptoJS.enc.Hex.parse(aesKey), { - mode: CryptoJS.mode.ECB, - padding: CryptoJS.pad.Pkcs7 - }).ciphertext.toString() - }) + }); ipcMain.handle('invoke_resources_preview', async (event, {url}) => { if (!url) { @@ -128,6 +113,17 @@ export default function initIPC() { shell.showItemInFolder(save_path) }) + ipcMain.handle('invoke_file_del', (event, {url_sign}) => { + if (url_sign === "all"){ + global.videoList = {} + return + } + if (url_sign) { + delete global.videoList[url_sign] + return + } + }) + ipcMain.handle('invoke_window_restart', (event) => { app.relaunch() app.exit() diff --git a/electron/main/proxyServer.ts b/electron/main/proxyServer.ts index 0d88be3..893b0ed 100755 --- a/electron/main/proxyServer.ts +++ b/electron/main/proxyServer.ts @@ -7,78 +7,35 @@ import * as urlTool from "url" import {toSize} from "./utils" // @ts-ignore import {hexMD5} from '../../src/common/md5' +import pkg from '../../package.json' const hoXy = require('hoxy') const port = 8899 -let videoList = {} +global.videoList = {} if (process.platform === 'win32') { process.env.OPENSSL_BIN = CONFIG.OPEN_SSL_BIN_PATH process.env.OPENSSL_CONF = CONFIG.OPEN_SSL_CNF_PATH } -// setTimeout to allow working in macOS -// in windows: H5ExtTransfer:ok -// in macOS: finderH5ExtTransfer:ok - -const injection_script1 = ` -setTimeout(() => { - let receiver_url = "https://res-downloader.666666.com"; - - function send_response_if_is_video(response) { - if (response == undefined) return; - if (!response["err_msg"].includes("H5ExtTransfer:ok")) return; - let value = JSON.parse(response["jsapi_resp"]["resp_json"]); - if (value["object"] == undefined || value["object"]["object_desc"] == undefined || value["object"]["object_desc"]["media"].length == 0) { - return; - } - let media = value["object"]["object_desc"]["media"][0]; - let description = value["object"]["object_desc"]["description"].trim(); - let video_data = { - "decode_key": media["decode_key"], - "url": media["url"]+media["url_token"], - "size": media["file_size"], - "description": description, - "uploader": value["object"]["nickname"] - }; - fetch(receiver_url, { - method: "POST", - mode: "no-cors", - body: JSON.stringify(video_data), - }).then((resp) => { - // alert(\`video data for \${video_data["description"]} sent!\`); - }); - } - - function wrapper(name,origin) { - return function() { - let cmdName = arguments[0]; - if (arguments.length == 3) { - let original_callback = arguments[2]; - arguments[2] = async function () { - if (arguments.length == 1) { - send_response_if_is_video(arguments[0]); - } - return await original_callback.apply(this, arguments); - } - } else { - } - let result = origin.apply(this,arguments); - return result; - } - } +const resObject = { + url: "", + url_sign: "", + platform: "", + size: "", + type: "video/mp4", + type_str: 'video', + progress_bar: "", + save_path: "", + decode_key: "", + description: "" +} - window.WeixinJSBridge.invoke = wrapper("WeixinJSBridge.invoke", window.WeixinJSBridge.invoke); - window.wvds = true; -}, 200);`; +const vv = hexMD5(pkg.version) + (CONFIG.IS_DEV ? Math.random() :"") -export async function startServer({ - win, - upstreamProxy, - setProxyErrorCallback = f => f, - }) { +export async function startServer({win, upstreamProxy, setProxyErrorCallback = f => f,}) { return new Promise(async (resolve: any, reject) => { try { const proxy = hoXy.createServer({ @@ -94,14 +51,14 @@ export async function startServer({ resolve() }) .catch((err) => { - setProxyErrorCallback(err); - reject('setting proxy err: '+ err.toString()); + setProxyErrorCallback(err) + reject('setting proxy err: ' + err.toString()) }); }) .on('error', err => { - setProxyErrorCallback(err); - reject('proxy service err: ' + err.toString()); - }); + setProxyErrorCallback(err) + reject('proxy service err: ' + err.toString()) + }) proxy.intercept( @@ -113,26 +70,32 @@ export async function startServer({ (req, res) => { res.string = 'ok' res.statusCode = 200 - let url_sign: string = hexMD5(req.json.url) - let urlInfo = urlTool.parse(req.json.url, true) - win?.webContents?.send?.('on_get_queue', { - url_sign: url_sign, - url: req.json.url, - down_url: req.json.url, - high_url: '', - platform: urlInfo.hostname, - size: toSize(req.json.size ?? 0), - type: "video/mp4", - type_str: 'video', - progress_bar: '', - save_path: '', - downing: false, - decode_key: req.json.decode_key, - description: req.json.description, - uploader: '', - }) + try { + if (!req.json?.description || req.json?.media?.length <= 0) { + return + } + const media = req.json?.media[0] + const url_sign: string = hexMD5(media.url) + if (global.videoList.hasOwnProperty(url_sign) === true) { + return + } + const urlInfo = urlTool.parse(media.url, true) + global.videoList[url_sign] = media.url + win.webContents.send('on_get_queue', Object.assign({}, resObject, { + url_sign: url_sign, + url: media.url + media.urlToken, + platform: urlInfo.hostname, + size: media?.fileSize ? toSize(media.fileSize) : 0, + type: "video/mp4", + type_str: 'video', + decode_key: media?.decodeKey ? media?.decodeKey : '', + description: req.json.description, + })) + } catch (e) { + log.log(e.toString()) + } }, - ); + ) proxy.intercept( { @@ -142,10 +105,42 @@ export async function startServer({ }, async (req, res) => { if (req.url.includes('/web/pages/feed') || req.url.includes('/web/pages/home')) { - res.string = res.string.replace('', '\n\n'); - res.statusCode = 200; + res.string = res.string.replaceAll('.js"', '.js?v=' + vv + '"') + res.statusCode = 200 } }, + ) + + proxy.intercept( + { + phase: 'response', + as: 'string', + }, + async (req, res) => { + if (req.url.endsWith('.js?v=' + vv)) { + res.string = res.string.replaceAll('.js"', '.js?v=' + vv + '"'); + } + if (req.url.includes("web/web-finder/res/js/virtual_svg-icons-register.publish")) { + // console.log(res.string.match(/return\s*\{\s*width:([\s\S]*?)scalingInfo:([\s\S]*?)\}/)) +// res.string = res.string.replace( +// /return\s*{\s*width:(.*?)scalingInfo:(.*?)\s*}/, +// `var mediaInfo = {width:$1scalingInfo:$2}; +// console.log("mediaInfo", mediaInfo); +// console.log("this.objectDesc", this.objectDesc); +// return mediaInfo;` +// ) + res.string = res.string.replace(/get\s*media\s*\(\)\s*\{/, ` + get media(){ + if(this.objectDesc){ + fetch("https://res-downloader.666666.com", { + method: "POST", + mode: "no-cors", + body: JSON.stringify(this.objectDesc), + }); + }; + `) + } + } ); proxy.intercept( @@ -153,125 +148,94 @@ export async function startServer({ phase: 'response', }, async (req, res) => { - // 拦截响应 - let ctype = res?._data?.headers?.['content-type'] - let url_sign: string = hexMD5(req.fullUrl()) - let res_url = req.fullUrl() - let urlInfo = urlTool.parse(res_url, true) - switch (ctype) { - case "video/mp4": - case "video/webm": - case "video/ogg": - case "video/x-msvideo": - case "video/mpeg": - case "video/quicktime": - case "video/x-ms-wmv": - case "video/x-flv": - case "video/3gpp": - case "video/x-matroska": - if (videoList.hasOwnProperty(url_sign) === false) { - videoList[url_sign] = req.fullUrl() - let high_url = '' - let down_url = res_url - win?.webContents?.send?.('on_get_queue', { + try { + // 拦截响应 + const ctype = res?._data?.headers?.['content-type'] + const url_sign: string = hexMD5(req.fullUrl()) + const res_url = req.fullUrl() + const urlInfo = urlTool.parse(res_url, true) + switch (ctype) { + case "video/mp4": + case "video/webm": + case "video/ogg": + case "video/x-msvideo": + case "video/mpeg": + case "video/quicktime": + case "video/x-ms-wmv": + case "video/x-flv": + case "video/3gpp": + case "video/x-matroska": + if (global.videoList.hasOwnProperty(url_sign) === false) { + global.videoList[url_sign] = res_url + win.webContents.send('on_get_queue', Object.assign({}, resObject, { + url: res_url, + url_sign: url_sign, + platform: urlInfo.hostname, + size: toSize(res?._data?.headers?.['content-length'] ?? 0), + type: ctype, + type_str: 'video', + })) + } + break; + case "image/png": + case "image/webp": + case "image/jpeg": + case "image/jpg": + case "image/svg+xml": + case "image/gif": + case "image/avif": + case "image/bmp": + case "image/tiff": + case "image/x-icon": + case "image/heic": + case "image/vnd.adobe.photoshop": + win.webContents.send('on_get_queue', Object.assign({}, resObject, { + url: res_url, url_sign: url_sign, - url: down_url, - down_url: down_url, - high_url: high_url, platform: urlInfo.hostname, - size: toSize(res?._data?.headers?.['content-length'] ?? 0), + size: res?._data?.headers?.['content-length'] ? toSize(res?._data?.headers?.['content-length']) : 0, type: ctype, - type_str: 'video', - progress_bar: '', - save_path: '', - downing: false, - decode_key: '', - description: '', - uploader: '', - }) - } - break; - case "image/png": - case "image/webp": - case "image/jpeg": - case "image/jpg": - case "image/svg+xml": - case "image/gif": - case "image/avif": - case "image/bmp": - case "image/tiff": - case "image/x-icon": - case "image/heic": - case "image/vnd.adobe.photoshop": - win?.webContents?.send?.('on_get_queue', { - url_sign: url_sign, - url: res_url, - down_url: res_url, - high_url: '', - platform: urlInfo.hostname, - size: toSize(res?._data?.headers?.['content-length'] ?? 0), - type: ctype, - type_str: 'image', - progress_bar: '', - save_path: '', - downing: false, - decode_key: '', - description: '', - uploader: '', - }) - break; - case "audio/mpeg": - case "audio/wav": - case "audio/aiff": - case "audio/x-aiff": - case "audio/aac": - case "audio/ogg": - case "audio/flac": - case "audio/midi": - case "audio/x-midi": - case "audio/x-ms-wma": - case "audio/opus": - case "audio/webm": - case "audio/mp4": - win?.webContents?.send?.('on_get_queue', { - url_sign: url_sign, - url: res_url, - down_url: res_url, - high_url: '', - platform: urlInfo.hostname, - size: toSize(res?._data?.headers?.['content-length'] ?? 0), - type: ctype, - type_str: 'audio', - progress_bar: '', - save_path: '', - downing: false, - decode_key: '', - description: '', - uploader: '', - }) - break; - case "application/vnd.apple.mpegurl": - case "application/x-mpegURL": - win.webContents?.send?.('on_get_queue', { - url_sign: url_sign, - url: res_url, - down_url: res_url, - high_url: '', - platform: urlInfo.hostname, - size: toSize(res?._data?.headers?.['content-length'] ?? 0), - type: ctype, - type_str: 'm3u8', - progress_bar: '', - save_path: '', - downing: false, - decode_key: '', - description: '', - uploader: '', - }) - break; + type_str: 'image', + })) + break + case "audio/mpeg": + case "audio/wav": + case "audio/aiff": + case "audio/x-aiff": + case "audio/aac": + case "audio/ogg": + case "audio/flac": + case "audio/midi": + case "audio/x-midi": + case "audio/x-ms-wma": + case "audio/opus": + case "audio/webm": + case "audio/mp4": + win.webContents.send('on_get_queue', Object.assign({}, resObject, { + url: res_url, + url_sign: url_sign, + platform: urlInfo.hostname, + size: res?._data?.headers?.['content-length'] ? toSize(res?._data?.headers?.['content-length']) : 0, + type: ctype, + type_str: 'audio', + })) + break + case "application/vnd.apple.mpegurl": + case "application/x-mpegURL": + win.webContents.send('on_get_queue', Object.assign({}, resObject, { + url: res_url, + url_sign: url_sign, + platform: urlInfo.hostname, + size: res?._data?.headers?.['content-length'] ? toSize(res?._data?.headers?.['content-length']) : 0, + type: ctype, + type_str: 'm3u8', + })) + break + } + } catch (e) { + log.log(e.toString()) } - }, ) } catch (e) { diff --git a/electron/main/utils.ts b/electron/main/utils.ts index 9e0dfbc..632f440 100755 --- a/electron/main/utils.ts +++ b/electron/main/utils.ts @@ -25,13 +25,18 @@ function downloadFile(url, decodeKey, fullFileName, progressCallback) { if (decodeKey) { xorStream = xorTransform(getDecryptionArray(decodeKey)); } - - return axios.get(url, { + let config = { responseType: 'stream', headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36', }, - }).then(({data, headers}) => { + } + + if (url.includes("douyin")){ + config.headers['Referer'] = url + } + + return axios.get(url, config).then(({data, headers}) => { let currentLen = 0 const totalLen = headers['content-length'] @@ -95,25 +100,46 @@ function toSize(size: number) { function suffix(type: string) { switch (type) { case "video/mp4": + case "video/webm": + case "video/ogg": + case "video/x-msvideo": + case "video/mpeg": + case "video/quicktime": + case "video/x-ms-wmv": + case "video/x-flv": + case "video/3gpp": + case "video/x-matroska": return ".mp4"; case "image/png": - return ".png"; case "image/webp": - return ".webp"; + case "image/jpeg": + case "image/jpg": case "image/svg+xml": - return ".svg"; case "image/gif": - return ".gif"; + case "image/avif": + case "image/bmp": + case "image/tiff": + case "image/x-icon": + case "image/heic": + case "image/vnd.adobe.photoshop": + return ".png"; case "audio/mpeg": + case "audio/wav": + case "audio/aiff": + case "audio/x-aiff": + case "audio/aac": + case "audio/ogg": + case "audio/flac": + case "audio/midi": + case "audio/x-midi": + case "audio/x-ms-wma": + case "audio/opus": + case "audio/webm": + case "audio/mp4": return ".mp3"; case "application/vnd.apple.mpegurl": + case "application/x-mpegURL": return ".m3u8"; - case "image/jpeg": - return ".jpeg"; - case "image/jpg": - return ".jpg"; - case "image/avif": - return ".avif"; } return "" } diff --git a/package.json b/package.json index 1d603a7..49ab379 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "res-downloader", - "version": "1.0.6", + "version": "2.0.0", "main": "dist-electron/main/index.js", - "description": "Electron + Vue + Vite 实现的资源下载软件,支持微信视频号下载、抖音视频下载、快手视频下载、酷狗音乐下载等", + "description": "res-downloader(爱享素材下载器),支持视频号、小程序、抖音、快手、小红书、酷狗音乐、qq音乐下载等", "author": "putyy@qq.com", "license": "MIT", "private": true, diff --git a/public/show.webp b/public/show.webp index 7749793..3e5c320 100644 Binary files a/public/show.webp and b/public/show.webp differ diff --git a/src/components/layout/Footer.vue b/src/components/layout/Footer.vue index 224ca83..1b0cd8f 100755 --- a/src/components/layout/Footer.vue +++ b/src/components/layout/Footer.vue @@ -24,6 +24,11 @@ const jump = (scene: number)=>{ url: "https://s.gowas.cn/d/4089-quan-ping-tai-zi-yuan-xia-zai-ruan-jian" }) break; + case 5: + ipcRenderer.invoke('invoke_open_default_browser', { + url: "https://www.ais.do/ivi/rr2GaZ" + }) + break; case 6: ipcRenderer.invoke('invoke_open_default_browser', { url: "https://github.com/putyy/res-downloader" @@ -40,7 +45,8 @@ div.line a.item(@click="jump(4)") 问题反馈 div.line a.item 推荐: - a.item(@click="jump(2)") 云盘资源 + a.item(@click="jump(5)") Ai助手(免费) + a.item(@click="jump(2)") 网盘资源 a.item(@click="jump(3)") 图片无损压缩 a.item(@click="jump(6)") 软件源码 diff --git a/src/views/About.vue b/src/views/About.vue index a7e230e..2cd1254 100644 --- a/src/views/About.vue +++ b/src/views/About.vue @@ -47,7 +47,7 @@ const str = "使用方法\n" + @@ -375,15 +373,16 @@ el-container.container } } - .show_res{ + .show_res { width: 100%; height: auto; - .img{ + + .img { max-height: 200px; } } - .actions{ + .actions { display: flex; flex-direction: column; align-items: flex-start;