1 Commits
1.0.2 ... 1.0.3

Author SHA1 Message Date
putyy
525c8bb5a3 更新视频号下载 2023-12-29 11:10:17 +08:00
11 changed files with 5034 additions and 125 deletions

4
.gitignore vendored
View File

@@ -9,6 +9,7 @@ lerna-debug.log*
node_modules
dist
temp
dist-ssr
dist-electron
release
@@ -27,5 +28,4 @@ release
# lockfile
package-lock.json
pnpm-lock.yaml
yarn.lock
pnpm-lock.yaml

View File

@@ -11,6 +11,7 @@
## 二次开发
> ps 打包慢的问题可以参考 https://www.putyy.com/articles/87
```sh
git clone https://github.com/putyy/res-downloader
@@ -28,12 +29,23 @@ yarn run build --win
```
## 软件截图
![](public/show.png)
![](public/show.jpg)
## 使用方法
> 1. 打开本软件
> 2. 软件首页选择要获取的资源类型(默认选中的视频)
> 3. 打开要捕获的源, 如:视频号、网页、小程序等等
> 4. 返回软件首页即可看到要下载的资源
## 常见问题
> 1. 无法拦截获取
> > 手动检测系统代理是否设置正确 本软件代理地址: 127.0.0.1:8899
> 2. 关闭软件后无法正常上网
> > 手动关闭系统代理设置
## 实现原理
> 通过代理网络抓包拦截响应筛选出有用的资源同fiddler、charles等抓包软件、浏览器F12打开控制也能达到目的只不过这些软件需要手动进行筛选对于小白用户上手还是有点难度所以就有了本项目这样的软件。
## 参考项目
- [WeChatVideoDownloader](https://github.com/lecepin/WeChatVideoDownloader) 原项目是react写的本项目参考原项目用vue3重写了一下核心逻辑没什么变化主要是增加了一些新的功能再次感谢

View File

@@ -5,7 +5,6 @@ import {downloadFile} from './utils'
// @ts-ignore
import {hexMD5} from '../../src/common/md5'
import fs from "fs"
import * as urlTool from "url"
import CryptoJS from 'crypto-js'
import {closeProxy, setProxy} from "./setProxy"
import log from "electron-log"
@@ -15,9 +14,8 @@ let win: BrowserWindow
let previewWin: BrowserWindow
let isStartProxy = false
let isOpenProxy = false
let videoList = {}
let aesKey = "as5d45as4d6qe6wqfar6gt4749q6y7w6h34v64tv7t37ty5qwtv6t6qv"
let qqReg = RegExp("finder.video.qq.com")
const toSize = (size: number) => {
if (size > 1048576) {
@@ -77,96 +75,7 @@ export default function initIPC() {
isStartProxy = true
isOpenProxy = true
return startServer({
// @ts-ignore
interceptCallback: phase => async (req, res) => {
// 拦截响应
if (phase === 'response') {
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":
if (videoList.hasOwnProperty(url_sign) === false) {
videoList[url_sign] = req.fullUrl()
let high_url = ''
let down_url = res_url
if (qqReg.test(down_url)) {
down_url = down_url.replace("finder.video.qq.com/251/20302", "finder.video.qq.com/251/20304")
urlInfo = urlTool.parse(down_url, true)
high_url = urlInfo.protocol + "//" + urlInfo.hostname + urlInfo.pathname
+ '?encfilekey=' + urlInfo.query?.encfilekey
+ '&token=' + urlInfo.query?.token
}
win?.webContents?.send?.('on_get_queue', {
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),
type: ctype,
type_str: 'video',
progress_bar: '',
save_path: '',
downing: false
})
}
break;
case "image/png":
case "image/webp":
case "image/svg+xml":
case "image/gif":
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
})
break;
case "audio/mpeg":
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
})
break;
case "application/vnd.apple.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
})
break;
}
}
},
win: win,
setProxyErrorCallback: err => {
isStartProxy = false
isOpenProxy = false
@@ -219,6 +128,7 @@ export default function initIPC() {
// 开始下载
return downloadFile(
down_url,
data.decode_key,
save_path_file,
(res) => {
return save_path_file

View File

@@ -3,24 +3,83 @@ import log from 'electron-log'
import CONFIG from './const'
import {closeProxy, setProxy} from './setProxy'
import {app} from "electron"
import * as urlTool from "url"
import {toSize} from "./utils"
// @ts-ignore
import {hexMD5} from '../../src/common/md5'
const hoXy = require('hoxy')
const port = 8899
let 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;
}
}
window.WeixinJSBridge.invoke = wrapper("WeixinJSBridge.invoke", window.WeixinJSBridge.invoke);
window.wvds = true;
}, 200);`;
export async function startServer({
interceptCallback = f => f => f,
win,
setProxyErrorCallback = f => f,
}) {
// const port = await getPort()
const port = 8899
return new Promise(async (resolve: any, reject) => {
const proxy = hoXy
.createServer({
const proxy = hoXy.createServer({
certAuthority: {
key: fs.readFileSync(CONFIG.CERT_PRIVATE_PATH),
cert: fs.readFileSync(CONFIG.CERT_PUBLIC_PATH),
@@ -29,11 +88,11 @@ export async function startServer({
.listen(port, () => {
setProxy('127.0.0.1', port)
.then(() => {
log.log("--------------setProxy success--------------")
// log.log("--------------setProxy success--------------")
resolve()
})
.catch((err) => {
log.log("--------------setProxy error--------------")
// log.log("--------------setProxy error--------------")
// setProxyErrorCallback(data);
setProxyErrorCallback({});
reject('设置代理失败');
@@ -43,18 +102,150 @@ export async function startServer({
log.log("--------------proxy err--------------", err)
});
proxy.intercept(
{
phase: 'request',
hostname: 'res-downloader.666666.com',
as: 'json',
},
(req, res) => {
// console.log('req.json: ', req.json)
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: '',
})
},
);
proxy.intercept(
{
phase: 'response',
hostname: 'channels.weixin.qq.com',
as: 'string',
},
async (req, res) => {
// console.log('inject[channels.weixin.qq.com] req.url:', req.url);
if (req.url.includes('/web/pages/feed') || req.url.includes('/web/pages/home')) {
res.string = res.string.replace('</body>', '\n<script>' + injection_script1 + '</script>\n</body>');
res.statusCode = 200;
// console.log('inject[channels.weixin.qq.com]:', req.url, res.string.length);
}
},
interceptCallback('request'),
);
proxy.intercept(
{
phase: 'response',
},
interceptCallback('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":
if (videoList.hasOwnProperty(url_sign) === false) {
videoList[url_sign] = req.fullUrl()
let high_url = ''
let down_url = res_url
// console.log('down_url', down_url)
win?.webContents?.send?.('on_get_queue', {
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),
type: ctype,
type_str: 'video',
progress_bar: '',
save_path: '',
downing: false,
decode_key: '',
description: '',
uploader: '',
})
}
break;
case "image/png":
case "image/webp":
case "image/svg+xml":
case "image/gif":
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: req.json.decode_key,
description: '',
uploader: '',
})
break;
case "audio/mpeg":
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: req.json.decode_key,
description: '',
uploader: '',
})
break;
case "application/vnd.apple.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: req.json.decode_key,
description: '',
uploader: '',
})
break;
}
},
)
})
}

View File

@@ -1,8 +1,31 @@
import fs from 'fs'
import {Transform } from 'stream'
import {getDecryptionArray} from '../wxjs/decrypt'
const axios = require('axios')
function xorTransform(decryptionArray) {
let processedBytes = 0;
return new Transform({
transform(chunk, encoding, callback) {
if (processedBytes < decryptionArray.length) {
let remaining = Math.min(decryptionArray.length - processedBytes, chunk.length);
for (let i = 0; i < remaining; i++) {
chunk[i] = chunk[i] ^ decryptionArray[processedBytes + i];
}
processedBytes += remaining;
}
this.push(chunk);
callback();
}
});
}
function downloadFile(url, decodeKey, fullFileName, progressCallback) {
let xorStream = null
if (decodeKey) {
xorStream = xorTransform(getDecryptionArray(decodeKey));
}
function downloadFile(url, fullFileName, progressCallback) {
return axios.get(url, {
responseType: 'stream',
headers: {
@@ -20,17 +43,37 @@ function downloadFile(url, fullFileName, progressCallback) {
});
data.on('error', err => reject(err))
data.pipe(
fs.createWriteStream(fullFileName).on('finish', () => {
resolve({
fullFileName,
totalLen,
});
}),
);
if (xorStream) {
data.pipe(xorStream).pipe(
fs.createWriteStream(fullFileName).on('finish', () => {
resolve({
fullFileName,
totalLen,
});
}),
);
}else{
data.pipe(
fs.createWriteStream(fullFileName).on('finish', () => {
resolve({
fullFileName,
totalLen,
});
}),
);
}
});
});
}
export {downloadFile}
function toSize(size: number) {
if (size > 1048576) {
return (size / 1048576).toFixed(2) + "MB"
}
if (size > 1024) {
return (size / 1024).toFixed(2) + "KB"
}
return size + 'b'
}
export {downloadFile, toSize}

4745
electron/wxjs/decrypt.js Executable file

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{
"name": "res-downloader",
"version": "1.0.2",
"version": "1.0.3",
"main": "dist-electron/main/index.js",
"description": "Electron + Vue + Vite 实现的资源下载软件,支持微信视频号下载、抖音视频下载、快手视频下载、酷狗音乐下载等",
"author": "putyy@qq.com",

BIN
public/show.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

View File

@@ -30,10 +30,10 @@ const jump = (scene: number)=>{
<template lang="pug">
div.line
a.item 当前版本: {{v}}
a.item 站长邮箱: putyy@qq.com
a.item 站长邮箱: gowas.work@gmail.com
a.item(@click="jump(1)") 获取更新
a.item(@click="jump(2)") 云盘资源
a.item(@click="jump(3)") chatgpt
a.item(@click="jump(3)") 图片压缩
a.item(@click="jump(4)") 问题反馈
</template>

View File

@@ -4,15 +4,23 @@ import {ipcRenderer} from 'electron'
import {onUnmounted} from "@vue/runtime-core"
import {ElMessage, ElLoading, ElTable} from "element-plus"
import localStorageCache from "../common/localStorage"
import {Delete, Promotion} from "@element-plus/icons-vue";
interface resData {
url_sign: string,
url: string,
down_url: string,
high_url: string,
size: any,
platform: string,
type: string,
type_str: string,
progress_bar: any,
save_path: string,
downing: boolean
downing: boolean,
decode_key: string,
description: string,
uploader: string,
}
const tableData = ref<resData[]>([])
@@ -81,6 +89,7 @@ onUnmounted(() => {
ipcRenderer.invoke('invoke_close_proxy').then((res) => {
})
localStorageCache.set("res-table-data", tableData.value, -1)
localStorageCache.set("res-type", resType.value, -1)
})
@@ -94,7 +103,6 @@ const handleBatchDown = async () => {
return
}
let save_dir = localStorageCache.get("save_dir")
if (!save_dir) {
@@ -288,6 +296,7 @@ el-container.container
img.img(v-if="scope.row.type_str === 'image'" :src="scope.row.down_url")
audio(v-if="scope.row.type_str === 'audio'" controls preload="none")
source(:src="scope.row.down_url" :type="scope.row.type")
div {{scope.row.description}}
el-table-column(prop="type_str" label="类型" show-overflow-tooltip)
el-table-column(prop="platform" label="主机地址")
el-table-column(prop="size" label="资源大小")
@@ -296,8 +305,7 @@ el-container.container
el-table-column(label="操作")
template(#default="scope")
template(v-if="scope.row.type_str !== 'm3u8'" )
el-button(v-if="!scope.row.save_path" link type="primary" @click="handleDown(scope.$index, scope.row, false)") 下载
el-button(v-if="!scope.row.save_path && scope.row.high_url !='' " link type="primary" @click="handleDown(scope.$index, scope.row, true)") 高清下载
el-button(v-if="!scope.row.save_path" link type="primary" @click="handleDown(scope.$index, scope.row, false)") {{scope.row.decode_key ? "解密下载" : "下载"}}
el-button(link type="primary" @click="handlePreview(scope.$index, scope.row)") 窗口预览
el-button(link type="primary" @click="handleCopy(scope.row.down_url)") 复制链接
el-button(link type="primary" @click="handleDel(scope.$index)")