mirror of
https://github.com/putyy/res-downloader.git
synced 2026-01-12 14:14:55 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
366c79fff2 | ||
|
|
525c8bb5a3 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -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
|
||||
16
README.md
16
README.md
@@ -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
|
||||
```
|
||||
|
||||
## 软件截图
|
||||

|
||||

|
||||
|
||||
## 使用方法
|
||||
> 1. 打开本软件
|
||||
> 2. 软件首页选择要获取的资源类型(默认选中的视频)
|
||||
> 3. 打开要捕获的源, 如:视频号、网页、小程序等等
|
||||
> 4. 返回软件首页即可看到要下载的资源
|
||||
|
||||
## 常见问题
|
||||
> 1. 无法拦截获取
|
||||
> > 手动检测系统代理是否设置正确 本软件代理地址: 127.0.0.1:8899
|
||||
> 2. 关闭软件后无法正常上网
|
||||
> > 手动关闭系统代理设置
|
||||
|
||||
## 实现原理
|
||||
> 通过代理网络抓包拦截响应,筛选出有用的资源,同fiddler、charles等抓包软件、浏览器F12打开控制也能达到目的,只不过这些软件需要手动进行筛选,对于小白用户上手还是有点难度,所以就有了本项目这样的软件。
|
||||
|
||||
|
||||
## 参考项目
|
||||
|
||||
- [WeChatVideoDownloader](https://github.com/lecepin/WeChatVideoDownloader) 原项目是react写的,本项目参考原项目用vue3重写了一下,核心逻辑没什么变化,主要是增加了一些新的功能,再次感谢!
|
||||
|
||||
3
components.d.ts
vendored
3
components.d.ts
vendored
@@ -14,13 +14,10 @@ declare module 'vue' {
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElMain: typeof import('element-plus/es')['ElMain']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
Footer: typeof import('./src/components/layout/Footer.vue')['default']
|
||||
Index: typeof import('./src/components/layout/Index.vue')['default']
|
||||
|
||||
@@ -22,7 +22,7 @@ export default {
|
||||
CERT_PUBLIC_PATH: path.join(EXECUTABLE_PATH, './keys/public.pem'),
|
||||
INSTALL_CERT_FLAG: path.join(HOME_PATH, './installed.lock'),
|
||||
WIN_CERT_INSTALL_HELPER: path.join(EXECUTABLE_PATH, './w_c.exe'),
|
||||
APP_CN_NAME: '资源下载器',
|
||||
APP_CN_NAME: '爱享素材下载器',
|
||||
APP_EN_NAME: 'ResDownloader',
|
||||
REGEDIT_VBS_PATH: path.join(EXECUTABLE_PATH, './regedit-vbs'),
|
||||
OPEN_SSL_BIN_PATH: path.join(EXECUTABLE_PATH, './openssl/openssl.exe'),
|
||||
|
||||
@@ -1,33 +1,22 @@
|
||||
import {ipcMain, dialog, BrowserWindow, app, shell} from 'electron'
|
||||
import {startServer} from './proxyServer'
|
||||
import {installCert, checkCertInstalled} from './cert'
|
||||
import {downloadFile} from './utils'
|
||||
import {downloadFile, decodeWxFile} 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"
|
||||
import {floor} from "lodash";
|
||||
|
||||
let getMac = require("getmac").default
|
||||
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) {
|
||||
return (size / 1048576).toFixed(2) + "MB"
|
||||
}
|
||||
if (size > 1024) {
|
||||
return (size / 1024).toFixed(2) + "KB"
|
||||
}
|
||||
return size + 'b'
|
||||
}
|
||||
let aesKey = "as5d45as4d6qe6wqfar6gt4749q6y7w6h34v64tv7t37ty5qwtv6t6qv"
|
||||
|
||||
const suffix = (type: string) => {
|
||||
switch (type) {
|
||||
@@ -77,96 +66,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
|
||||
@@ -195,6 +95,16 @@ export default function initIPC() {
|
||||
return result?.[0]
|
||||
})
|
||||
|
||||
ipcMain.handle('invoke_select_wx_file', async (event, {index, data}) => {
|
||||
// 选择下载位置
|
||||
const result = dialog.showOpenDialogSync({title: '保存', properties: ['openFile']})
|
||||
if (!result?.[0]) {
|
||||
return false
|
||||
}
|
||||
return decodeWxFile(result?.[0], data.decode_key, result?.[0].replace(".mp4", "_解密.mp4"))
|
||||
})
|
||||
|
||||
|
||||
ipcMain.handle('invoke_file_exists', async (event, {save_path, url}) => {
|
||||
let url_sign = hexMD5(url)
|
||||
let res = fs.existsSync(`${save_path}/${url_sign}.mp4`)
|
||||
@@ -219,9 +129,10 @@ export default function initIPC() {
|
||||
// 开始下载
|
||||
return downloadFile(
|
||||
down_url,
|
||||
data.decode_key,
|
||||
save_path_file,
|
||||
(res) => {
|
||||
return save_path_file
|
||||
win?.webContents.send('on_down_file_schedule', {schedule: floor(res * 100)})
|
||||
}
|
||||
).catch(err => {
|
||||
// console.log('invoke_down_file:err', err)
|
||||
@@ -257,8 +168,8 @@ export default function initIPC() {
|
||||
shell.openExternal(url).then(r => {})
|
||||
})
|
||||
|
||||
ipcMain.handle('invoke_open_dir', (event, {dir}) => {
|
||||
shell.openPath(dir).then(r => {})
|
||||
ipcMain.handle('invoke_open_file_dir', (event, {save_path}) => {
|
||||
shell.showItemInFolder(save_path)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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: '',
|
||||
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: '',
|
||||
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: '',
|
||||
description: '',
|
||||
uploader: '',
|
||||
})
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,53 @@ 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 decodeWxFile(fileName, decodeKey, fullFileName) {
|
||||
let xorStream = xorTransform(getDecryptionArray(decodeKey));
|
||||
let data = fs.createReadStream(fileName);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
data.on('error', err => reject(err));
|
||||
data.pipe(xorStream).pipe(
|
||||
fs.createWriteStream(fullFileName).on('finish', () => {
|
||||
resolve({
|
||||
fullFileName,
|
||||
});
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
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, decodeWxFile}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>资源下载器</title>
|
||||
<title>爱享素材下载器</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
4745
electron/wxjs/decrypt.js
Executable file
4745
electron/wxjs/decrypt.js
Executable file
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
|
||||
<title>资源下载器</title>
|
||||
<title>爱享素材下载器</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "res-downloader",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.4",
|
||||
"main": "dist-electron/main/index.js",
|
||||
"description": "Electron + Vue + Vite 实现的资源下载软件,支持微信视频号下载、抖音视频下载、快手视频下载、酷狗音乐下载等",
|
||||
"author": "putyy@qq.com",
|
||||
|
||||
BIN
public/show.jpg
Normal file
BIN
public/show.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
BIN
public/show.png
BIN
public/show.png
Binary file not shown.
|
Before Width: | Height: | Size: 125 KiB |
@@ -6,7 +6,7 @@ const jump = (scene: number)=>{
|
||||
switch (scene) {
|
||||
case 1:
|
||||
ipcRenderer.invoke('invoke_open_default_browser', {
|
||||
url: "https://github.com/putyy/res-downloader"
|
||||
url: "https://s.gowas.cn/d/4089-quan-ping-tai-zi-yuan-xia-zai-ruan-jian"
|
||||
})
|
||||
break;
|
||||
case 2:
|
||||
@@ -24,17 +24,30 @@ const jump = (scene: number)=>{
|
||||
url: "https://github.com/putyy/res-downloader/issues"
|
||||
})
|
||||
break;
|
||||
case 5:
|
||||
ipcRenderer.invoke('invoke_open_default_browser', {
|
||||
url: "https://haokawx.lot-ml.com/Product/Index/22550"
|
||||
})
|
||||
break;
|
||||
case 6:
|
||||
ipcRenderer.invoke('invoke_open_default_browser', {
|
||||
url: "https://github.com/putyy/res-downloader"
|
||||
})
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<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
|
||||
div.line
|
||||
a.item(@click="jump(3)") 图片无损压缩
|
||||
a.item(@click="jump(4)") 问题反馈
|
||||
a.item(@click="jump(5)") 流量卡推荐
|
||||
a.item(@click="jump(6)") 软件源码
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@@ -42,6 +55,7 @@ div.line
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
.item{
|
||||
padding: 0 5px;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import {inject, onMounted, ref, watch} from 'vue'
|
||||
import localStorageCache from "../../common/localStorage";
|
||||
|
||||
const appName = "资源下载器"
|
||||
const appName = "爱享素材"
|
||||
const sidebarCollapse = ref(inject('sidebarCollapse'))
|
||||
const defaultActive = ref("/index")
|
||||
|
||||
@@ -20,11 +20,7 @@ div.sidebar
|
||||
el-menu-item(key="1" index="/index")
|
||||
el-icon
|
||||
VideoCamera
|
||||
span 拦截下载
|
||||
el-menu-item(key="7" index="/vip-parse")
|
||||
el-icon
|
||||
VideoCamera
|
||||
span 影视解析
|
||||
span 嗅探
|
||||
el-menu-item(key="2" index="/about")
|
||||
el-icon
|
||||
Share
|
||||
|
||||
@@ -25,11 +25,6 @@ const routes = [
|
||||
name: 'Setting',
|
||||
component: () => import('./views/Setting.vue'),
|
||||
},
|
||||
{
|
||||
path: '/vip-parse',
|
||||
name: 'VipParse',
|
||||
component: () => import('./views/VipParse.vue'),
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
@@ -24,6 +24,22 @@ const jump = (scene: number)=>{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const str = "使用方法\n" +
|
||||
" 1. 打开本软件\n" +
|
||||
" 2. 软件首页选择要获取的资源类型(默认选中的视频)\n" +
|
||||
" 3. 打开要捕获的源, 如:视频号、网页、小程序等等\n" +
|
||||
" 4. 返回软件首页即可看到要下载的资源\n" +
|
||||
"常见问题\n" +
|
||||
" 1. 无法拦截获取\n" +
|
||||
" 手动检测系统代理是否设置正确 本软件代理地址: 127.0.0.1:8899\n" +
|
||||
" 2. 关闭软件后无法正常上网\n" +
|
||||
" 手动关闭系统代理设置\n" +
|
||||
"实现原理\n" +
|
||||
" 通过代理网络抓包拦截响应,筛选出有用的资源,\n" +
|
||||
" 同fiddler、charles等抓包软件、浏览器F12打开控制也能达到目的,\n" +
|
||||
" 只不过这些软件需要手动进行筛选,对于小白用户上手还是有点难度,本软件对部分资源做了特殊处理,\n" +
|
||||
" 更适合大众用户,所以就有了本项目这样的软件。\n"
|
||||
</script>
|
||||
<template lang="pug">
|
||||
div.about
|
||||
@@ -36,6 +52,9 @@ div.about
|
||||
el-button(@click="jump(3)") 获取更新
|
||||
div 4. 问题反馈
|
||||
el-button(@click="jump(4)") 点击前往
|
||||
div.more
|
||||
pre {{str}}
|
||||
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
@@ -50,5 +69,8 @@ div.about
|
||||
padding: .3rem;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.more{
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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[]>([])
|
||||
@@ -48,7 +56,7 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const loading = ElLoading.service({
|
||||
let loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: 'Loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)',
|
||||
@@ -79,8 +87,13 @@ onUnmounted(() => {
|
||||
// console.log(res)
|
||||
})
|
||||
|
||||
ipcRenderer.invoke('invoke_close_proxy').then((res) => {
|
||||
ipcRenderer.removeListener('on_down_file_schedule', (res) => {
|
||||
// console.log(res)
|
||||
})
|
||||
|
||||
// ipcRenderer.invoke('invoke_close_proxy').then((res) => {
|
||||
// })
|
||||
|
||||
localStorageCache.set("res-table-data", tableData.value, -1)
|
||||
localStorageCache.set("res-type", resType.value, -1)
|
||||
})
|
||||
@@ -94,7 +107,6 @@ const handleBatchDown = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let save_dir = localStorageCache.get("save_dir")
|
||||
|
||||
if (!save_dir) {
|
||||
@@ -111,6 +123,10 @@ const handleBatchDown = async () => {
|
||||
background: 'rgba(0, 0, 0, 0.7)',
|
||||
})
|
||||
|
||||
ipcRenderer.on('on_down_file_schedule', (res: any, data: any) => {
|
||||
loading.setText(`已下载 ${data.schedule}%`)
|
||||
})
|
||||
|
||||
for (const item of multipleSelection.value) {
|
||||
let result = await ipcRenderer.invoke('invoke_file_exists', {
|
||||
save_path: save_dir,
|
||||
@@ -174,6 +190,10 @@ const handleDown = async (index: number, row: any, high: boolean) => {
|
||||
return
|
||||
}
|
||||
|
||||
ipcRenderer.on('on_down_file_schedule', (res: any, data: any) => {
|
||||
loading.setText(`已下载 ${data.schedule}%`)
|
||||
})
|
||||
|
||||
ipcRenderer.invoke('invoke_down_file', {
|
||||
index: index,
|
||||
data: Object.assign({}, tableData.value[index]),
|
||||
@@ -191,18 +211,49 @@ const handleDown = async (index: number, row: any, high: boolean) => {
|
||||
}
|
||||
loading.close()
|
||||
}).catch((err) => {
|
||||
// console.log('invoke_down_file err', err)
|
||||
ElMessage({
|
||||
message: "下载失败",
|
||||
type: 'warning',
|
||||
})
|
||||
loading.close()
|
||||
})
|
||||
}
|
||||
|
||||
const decodeWxFile = (index: number) => {
|
||||
let loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: "解密中",
|
||||
background: 'rgba(0, 0, 0, 0.7)',
|
||||
})
|
||||
|
||||
ipcRenderer.invoke('invoke_select_wx_file', {
|
||||
index: index,
|
||||
data: Object.assign({}, tableData.value[index]),
|
||||
}).then((res) => {
|
||||
if (res !== false) {
|
||||
ElMessage({
|
||||
message: "解密成功: " + res.fullFileName,
|
||||
type: 'success',
|
||||
})
|
||||
tableData.value[index].progress_bar = "100%"
|
||||
tableData.value[index].save_path = res.fullFileName
|
||||
}else{
|
||||
ElMessage({
|
||||
message: "解密失败",
|
||||
type: 'warning',
|
||||
})
|
||||
}
|
||||
loading.close()
|
||||
}).catch((err) => {
|
||||
ElMessage({
|
||||
message: "解密失败",
|
||||
type: 'warning',
|
||||
})
|
||||
loading.close()
|
||||
})
|
||||
}
|
||||
|
||||
const handlePreview = (index: number, row: any) => {
|
||||
// console.log('row.down_url',row)
|
||||
ipcRenderer.invoke('invoke_resources_preview', {url: row.down_url}).catch(() => {
|
||||
})
|
||||
}
|
||||
@@ -231,19 +282,9 @@ const handleDel = (index: number)=>{
|
||||
tableData.value = arr
|
||||
}
|
||||
|
||||
const openDir = ()=>{
|
||||
let save_dir = localStorageCache.get("save_dir")
|
||||
|
||||
if (!save_dir) {
|
||||
ElMessage({
|
||||
message: '目录不存在',
|
||||
type: 'warning'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ipcRenderer.invoke('invoke_open_dir', {
|
||||
dir: save_dir
|
||||
const openFileDir = (index: number)=>{
|
||||
ipcRenderer.invoke('invoke_open_file_dir', {
|
||||
save_path: tableData.value[index].save_path
|
||||
})
|
||||
}
|
||||
|
||||
@@ -279,29 +320,31 @@ el-container.container
|
||||
el-button(@click="resType.m3u8=!resType.m3u8" :type="resType.m3u8 ? 'primary' : 'info'" ) m3u8
|
||||
a(style="color: red") 点击左边选项,选择需要拦截的资源类型
|
||||
el-main
|
||||
el-table(ref="multipleTableRef" @selection-change="handleSelectionChange" :data="tableData" max-height="100%" stripe style="max-content")
|
||||
el-table-column(type="selection" width="55")
|
||||
el-table-column(label="预览" show-overflow-tooltip width="350px")
|
||||
el-table(ref="multipleTableRef" @selection-change="handleSelectionChange" :data="tableData" max-height="100%" stripe)
|
||||
el-table-column(type="selection")
|
||||
el-table-column(label="预览" show-overflow-tooltip width="300px")
|
||||
template(#default="scope")
|
||||
div.show_res
|
||||
video(v-if="scope.row.type_str === 'video'" :src="scope.row.down_url" controls preload="none" style="width: 100%;height: auto;") 您的浏览器不支持 video 标签。
|
||||
video.video(v-if="scope.row.type_str === 'video'" :src="scope.row.down_url" controls preload="none") 您的浏览器不支持 video 标签。
|
||||
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")
|
||||
audio.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="资源大小")
|
||||
el-table-column(prop="save_path" label="保存目录")
|
||||
el-table-column(prop="progress_bar" label="下载进度")
|
||||
el-table-column(label="操作")
|
||||
el-table-column(label="操作" width="135px" )
|
||||
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(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)") 删 除
|
||||
el-button(link type="primary" @click="openDir()") 打开目录
|
||||
div.actions
|
||||
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)") {{scope.row.decode_key ? "解密下载(视频号)" : "下载"}}
|
||||
el-button(v-if="scope.row.decode_key" link type="primary" @click="decodeWxFile(scope.$index)") 视频解密(视频号)
|
||||
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)") 删除
|
||||
el-button(v-if="scope.row.save_path" link type="primary" @click="openFileDir(scope.$index)") 打开文件目录
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
@@ -336,11 +379,17 @@ el-container.container
|
||||
}
|
||||
|
||||
.show_res{
|
||||
width: 100%;
|
||||
height: auto;
|
||||
.img{
|
||||
width: 100px;
|
||||
height: auto;
|
||||
max-height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.actions{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import {onMounted, ref} from "vue"
|
||||
import {ElMessage} from "element-plus"
|
||||
import {ipcRenderer} from 'electron'
|
||||
import {onUnmounted} from "@vue/runtime-core"
|
||||
import localStorageCache from "../common/localStorage"
|
||||
|
||||
const parseUrls = ref([
|
||||
"https://www.8090g.cn/jiexi/?url=",
|
||||
"https://jx.m3u8.tv/jiexi/?url=",
|
||||
"https://www.playm3u8.cn/jiexi.php?url=",
|
||||
"https://www.8090.la/8090/?url=",
|
||||
"https://jx.xmflv.com/?url=",
|
||||
"https://www.8090g.cn/?url=",
|
||||
"https://dm.xmflv.com:4433/?url=",
|
||||
])
|
||||
|
||||
const useParseUrl = ref("")
|
||||
const playUrl = ref("")
|
||||
const iframeSrc = ref("")
|
||||
const descText = ref(
|
||||
"支持各大视频付费、VIP电影电视剧解析免费观看: 爱奇艺、优酷、腾讯、乐视、土豆、芒果等\r\n若视频播放异常或时长不对,请尝试【更换线路】或【退出软件重新打开】即可解决!\r\n如有线路不行,请把页面拉倒最下方,发邮件给站长!"
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
useParseUrl.value = parseUrls.value[0]
|
||||
let dataCache = localStorageCache.get("res-vip-parse-data")
|
||||
if (dataCache) {
|
||||
useParseUrl.value = dataCache.useParseUrl
|
||||
playUrl.value = dataCache.playUrl
|
||||
// iframeSrc.value = useParseUrl.value + encodeURI(playUrl.value)
|
||||
}
|
||||
})
|
||||
|
||||
const parsePlay = () => {
|
||||
if (!playUrl) {
|
||||
ElMessage({
|
||||
message: "请填写播放地址",
|
||||
type: 'warning',
|
||||
})
|
||||
return
|
||||
}
|
||||
iframeSrc.value = useParseUrl.value + encodeURI(playUrl.value)
|
||||
}
|
||||
|
||||
const parseFullPlay = () => {
|
||||
if (!playUrl) {
|
||||
ElMessage({
|
||||
message: "请填写播放地址",
|
||||
type: 'warning',
|
||||
})
|
||||
return
|
||||
}
|
||||
ipcRenderer.invoke('invoke_resources_preview', {url: useParseUrl.value + encodeURI(playUrl.value)}).catch(() => {
|
||||
})
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
localStorageCache.set("res-vip-parse-data", {useParseUrl: useParseUrl.value, playUrl: playUrl.value}, -1)
|
||||
})
|
||||
|
||||
</script>
|
||||
<template lang="pug">
|
||||
el-main.play-box
|
||||
iframe.iframe(:src="iframeSrc")
|
||||
el-form
|
||||
el-form-item(label="线路选择:")
|
||||
el-select(v-model="useParseUrl")
|
||||
el-option(v-for="(v, k) in parseUrls" :value="v" :label="'线路'+(k+1)")
|
||||
el-form-item(label="播放地址:")
|
||||
el-input(v-model="playUrl" type="textarea" placeholder="爱奇艺、优酷、腾讯、芒果、乐视、土豆")
|
||||
el-form-item
|
||||
el-button(type="primary" @click="parsePlay()") 立即播放
|
||||
el-button(type="primary" @click="parseFullPlay()") 全屏播放
|
||||
el-row.desc {{descText}}
|
||||
</template>
|
||||
<style scoped lang="less">
|
||||
.play-box {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.iframe {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
border: 0;
|
||||
background-color: #211f1f;
|
||||
width: 100%;
|
||||
height: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: red;
|
||||
font-size: 20px;
|
||||
white-space: pre-wrap;
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user