mirror of
https://github.com/putyy/res-downloader.git
synced 2026-01-12 22:34:56 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
619ac47962 | ||
|
|
6b79585564 | ||
|
|
c1f05876e3 | ||
|
|
2981518cf4 | ||
|
|
511111f874 |
71
README.md
71
README.md
@@ -1,14 +1,45 @@
|
||||
# res-downloader
|
||||
#### 爱享素材下载器
|
||||
|
||||
💪 支持视频、音频、图片、m3u8等网络资源下载
|
||||
📦 支持微信视频号、小程序、抖音、快手、小红书、酷狗音乐、qq音乐等网络资源下载
|
||||
🍊 支持设置代理以获取特殊网络下的资源
|
||||
## res-downloader(爱享素材下载器) 【[点击加入群聊](https://qm.qq.com/q/W8mVeZideE)】
|
||||
🎯 基于 [electron-vite-vue](https://github.com/electron-vite/electron-vite-vue.git)
|
||||
📦 操作简单、可获取不同类型的资源
|
||||
🖥️ 支持Win10、Win11、Mac
|
||||
🌐 支持视频、音频、图片、m3u8等网络资源下载
|
||||
💪 支持微信视频号、小程序、抖音、快手、小红书、酷狗音乐、qq音乐等网络资源下载
|
||||
👼 支持设置代理以获取特殊网络下的资源
|
||||
|
||||
## 软件下载
|
||||
🆕 [github下载](https://github.com/putyy/res-downloader/releases)
|
||||
🆕 [蓝奏云下载 密码:9vs5](https://wwjv.lanzoum.com/b04wgtfyb)
|
||||
|
||||
## 使用方法
|
||||
> 0. 安装一定要同意安装证书文件,安装一定要同意安装证书文件,安装一定要同意安装证书文件!
|
||||
> 1. 打开本软件
|
||||
> 2. 软件首页选择要获取的资源类型(默认选中的视频)
|
||||
> 3. 打开要捕获的源, 如:视频号、网页、小程序等等
|
||||
> 4. 返回软件首页即可看到资源列表
|
||||
|
||||
## 软件截图
|
||||

|
||||
|
||||
## 常见问题
|
||||
下载慢、大视频下载失败
|
||||
> 推荐使用如下工具加速下载,视频号可以下载完成后再到对应视频操作项选择 “视频解密(视频号)” 按钮
|
||||
>> [Neat Download Manager](https://www.neatdownloadmanager.com/index.php/en/)、[Motrix](https://motrix.app/download)等软件进行下载
|
||||
|
||||
Win7无法使用
|
||||
> 软件不支持,也无计划支持
|
||||
|
||||
打开本软件,无法正常拦截获取
|
||||
> 检查系统代理是否正确设置 代理地址:127.0.0.1 端口:8899
|
||||
|
||||
关闭软件后无法正常上网
|
||||
> 手动关闭系统代理设置
|
||||
|
||||
打开本软件后无法上网
|
||||
> 手动删除安装标识锁文件,之后再打开软件会进行检查证书是否正确安装
|
||||
>> MAC: /Users/你的用户名称/.res-downloader@putyy/res-downloader-installed.lock
|
||||
>> Win: C:\Users\Admin\.res-downloader@putyy/res-downloader-installed.lock
|
||||
|
||||
其他问题请留言 https://github.com/putyy/res-downloader/issues
|
||||
|
||||
## 二次开发
|
||||
> ps: 打包慢的问题可以参考 https://www.putyy.com/articles/87
|
||||
@@ -28,29 +59,5 @@ yarn run build --universal --mac
|
||||
yarn run build --win
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
> 1. 打开本软件
|
||||
> 2. 软件首页选择要获取的资源类型(默认选中的视频)
|
||||
> 3. 打开要捕获的源, 如:视频号、网页、小程序等等
|
||||
> 4. 返回软件首页即可看到要下载的资源
|
||||
|
||||
## 常见问题
|
||||
> 1. 无法拦截获取
|
||||
> > 手动检测系统代理是否设置正确 本软件代理地址: 127.0.0.1:8899
|
||||
> 2. 关闭软件后无法正常上网
|
||||
> > 手动关闭系统代理设置
|
||||
> 3. 视频号抓取流程
|
||||
> > 将需要下载的视频发给好友或者文件助手 再打开即可拦截,通常会出现解密下载按钮
|
||||
> >
|
||||
> > 大视频可以复制链接通过其他工具加速下载,然后再通过对应的视频操作项进行"视频解密"
|
||||
|
||||
## 软件截图
|
||||

|
||||
|
||||
## 实现原理
|
||||
> 通过代理网络抓包拦截响应,筛选出有用的资源,同fiddler、charles等抓包软件、浏览器F12打开控制也能达到目的,只不过这些软件需要手动进行筛选,对于小白用户上手还是有点难度,所以就有了本项目这样的软件。
|
||||
|
||||
## 参考项目
|
||||
|
||||
- [WeChatVideoDownloader](https://github.com/lecepin/WeChatVideoDownloader) 原项目是react写的,本项目参考原项目用vue3重写了一下,核心逻辑没什么变化,主要是增加了一些新的功能,再次感谢!
|
||||
|
||||
## 免责声明
|
||||
本软件用于学习研究使用,若因使用本软件造成的一切法律责任均与本人无关!
|
||||
|
||||
3
components.d.ts
vendored
3
components.d.ts
vendored
@@ -15,10 +15,13 @@ declare module 'vue' {
|
||||
ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElLink: typeof import('element-plus/es')['ElLink']
|
||||
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']
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json",
|
||||
"appId": "com.putyy.ResDownloader",
|
||||
"appId": "com.putyy.res-downloader",
|
||||
"asar": true,
|
||||
"directories": {
|
||||
"output": "release/${version}"
|
||||
@@ -14,7 +14,7 @@
|
||||
"electron/res/**/*"
|
||||
],
|
||||
"mac": {
|
||||
"icon": "electron/res/icon/icons/mac/icon.icns",
|
||||
"icon": "electron/res/icon/mac.icns",
|
||||
"artifactName": "${productName}_${version}.${arch}.${ext}",
|
||||
"singleArchFiles": "*",
|
||||
"target": [
|
||||
@@ -22,13 +22,27 @@
|
||||
"target": "dmg",
|
||||
"arch": [
|
||||
'x64',
|
||||
'arm64'
|
||||
'arm64',
|
||||
'universal'
|
||||
]
|
||||
}
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "electron/res/mac/aria2/aria2.conf",
|
||||
"to": "electron/res/mac/aria2/aria2.conf",
|
||||
"filter": ["**/*"]
|
||||
},
|
||||
{
|
||||
"from": "electron/res/mac/aria2/${arch}/aria2c",
|
||||
"to": "electron/res/mac/aria2/aria2c",
|
||||
"filter": ["**/*"],
|
||||
}
|
||||
]
|
||||
},
|
||||
"win": {
|
||||
"icon": "electron/res/icon/icons/win/icon.ico",
|
||||
"icon": "electron/res/icon/win.ico",
|
||||
"artifactName": "${productName}_${version}.${ext}",
|
||||
"target": [
|
||||
{
|
||||
"target": "nsis",
|
||||
@@ -37,7 +51,13 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"artifactName": "${productName}_${version}.${ext}"
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "electron/res/win",
|
||||
"to": "electron/res/win",
|
||||
"filter": ["**/*"],
|
||||
}
|
||||
]
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
@@ -47,6 +67,7 @@
|
||||
"deleteAppDataOnUninstall": false
|
||||
},
|
||||
"extraResources": [
|
||||
"electron/res"
|
||||
"electron/res/icon",
|
||||
"electron/res/keys",
|
||||
]
|
||||
}
|
||||
|
||||
51
electron/main/aria2Rpc.ts
Normal file
51
electron/main/aria2Rpc.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
const axios = require('axios')
|
||||
import CONFIG from './const'
|
||||
|
||||
export class Aria2RPC {
|
||||
constructor() {
|
||||
this.url = `http://127.0.0.1:${CONFIG.ARIA_PORT}/jsonrpc`
|
||||
this.id = 1
|
||||
}
|
||||
|
||||
call(method, params) {
|
||||
const requestData = {
|
||||
jsonrpc: "2.0",
|
||||
method: method,
|
||||
params: params,
|
||||
id: this.id++
|
||||
};
|
||||
return axios.post(this.url, requestData, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}).then((response)=>{
|
||||
return response.data
|
||||
})
|
||||
}
|
||||
|
||||
addUri(uri, dir, filename, headers = {}) {
|
||||
return this.call('aria2.addUri', [uri, {
|
||||
dir: dir,
|
||||
out: filename,
|
||||
headers: headers,
|
||||
}]);
|
||||
}
|
||||
|
||||
tellStatus(gid) {
|
||||
return this.call('aria2.tellStatus', [gid]);
|
||||
}
|
||||
|
||||
calculateDownloadProgress(bitfield) {
|
||||
// 将十六进制的 bitfield 转换为二进制字符串
|
||||
const totalPieces = bitfield.length * 4; // 每个十六进制字符对应 4 位
|
||||
const binaryString = bitfield.split('').map(hex => parseInt(hex, 16).toString(2).padStart(4, '0')).join('');
|
||||
|
||||
// 计算已下载的部分数
|
||||
const downloadedPieces = binaryString.split('').filter(bit => bit === '1').length;
|
||||
|
||||
// 计算进度百分比
|
||||
const progressPercentage = (downloadedPieces / totalPieces) * 100;
|
||||
|
||||
return progressPercentage.toFixed(2); // 保留两位小数
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export async function installCert(checkInstalled = true) {
|
||||
`echo "输入本地登录密码" && sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${CONFIG.CERT_PUBLIC_PATH}" && touch ${CONFIG.INSTALL_CERT_FLAG} && echo "安装完成"`,
|
||||
)
|
||||
dialog.showMessageBoxSync({
|
||||
type: 'info',
|
||||
type: "info",
|
||||
message: `命令已复制到剪贴板,粘贴命令到终端并运行以安装并信任证书`,
|
||||
});
|
||||
|
||||
|
||||
@@ -17,13 +17,14 @@ export default {
|
||||
IS_DEV: isDev,
|
||||
EXECUTABLE_PATH,
|
||||
HOME_PATH,
|
||||
APP_CN_NAME: '爱享素材下载器',
|
||||
APP_EN_NAME: 'ResDownloader',
|
||||
CERT_PRIVATE_PATH: path.join(EXECUTABLE_PATH, './keys/private.pem'),
|
||||
CERT_PUBLIC_PATH: path.join(EXECUTABLE_PATH, './keys/public.pem'),
|
||||
INSTALL_CERT_FLAG: path.join(HOME_PATH, './res-downloader-installed.lock'),
|
||||
WIN_CERT_INSTALL_HELPER: path.join(EXECUTABLE_PATH, './w_c.exe'),
|
||||
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'),
|
||||
OPEN_SSL_CNF_PATH: path.join(EXECUTABLE_PATH, './openssl/openssl.cnf'),
|
||||
WIN_CERT_INSTALL_HELPER: path.join(EXECUTABLE_PATH, './win/w_c.exe'),
|
||||
REGEDIT_VBS_PATH: path.join(EXECUTABLE_PATH, './win/regedit-vbs'),
|
||||
OPEN_SSL_BIN_PATH: path.join(EXECUTABLE_PATH, './win/openssl/openssl.exe'),
|
||||
OPEN_SSL_CNF_PATH: path.join(EXECUTABLE_PATH, './win/openssl/openssl.cnf'),
|
||||
ARIA_PORT: "18899",
|
||||
};
|
||||
|
||||
@@ -3,6 +3,10 @@ import {release} from 'node:os'
|
||||
import {join} from 'node:path'
|
||||
import CONFIG from './const'
|
||||
import initIPC, {setWin} from './ipc'
|
||||
import {closeProxy} from "./setProxy"
|
||||
import log from "electron-log"
|
||||
import path from 'path'
|
||||
import {spawn} from 'child_process'
|
||||
|
||||
// The built directory structure
|
||||
//
|
||||
@@ -45,6 +49,7 @@ process.on('unhandledRejection', () => {
|
||||
|
||||
let mainWindow: BrowserWindow | null = null
|
||||
let previewWin: BrowserWindow | null = null
|
||||
let aria2Process
|
||||
|
||||
// Here, you can also use other preload
|
||||
const preload = join(__dirname, '../preload/index.js')
|
||||
@@ -77,21 +82,16 @@ app.on('activate', () => {
|
||||
}
|
||||
})
|
||||
|
||||
// New window example arg: new windows url
|
||||
ipcMain.handle('open-win', (_, arg) => {
|
||||
const childWindow = new BrowserWindow({
|
||||
webPreferences: {
|
||||
preload,
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
},
|
||||
})
|
||||
|
||||
if (process.env.VITE_DEV_SERVER_URL) {
|
||||
childWindow.loadURL(`${url}#${arg}`)
|
||||
} else {
|
||||
childWindow.loadFile(indexHtml, {hash: arg})
|
||||
app.on('before-quit', async e => {
|
||||
e.preventDefault()
|
||||
try {
|
||||
await closeProxy()
|
||||
aria2Process && aria2Process.kill();
|
||||
log.log("--------------closeProxy success--------------")
|
||||
} catch (error) {
|
||||
log.log("--------------proxy catch err--------------", error)
|
||||
}
|
||||
app.exit()
|
||||
})
|
||||
|
||||
function createWindow() {
|
||||
@@ -169,9 +169,45 @@ function createPreviewWindow(parent: BrowserWindow) {
|
||||
})
|
||||
}
|
||||
|
||||
function createArua2Process() {
|
||||
// 根据操作系统选择 aria2 的路径
|
||||
try {
|
||||
let aria2Path, aria2Conf
|
||||
if (process.platform === 'win32') {
|
||||
// Windows
|
||||
aria2Path = path.join(CONFIG.EXECUTABLE_PATH, "./win/aria2/aria2c.exe")
|
||||
aria2Conf = path.join(CONFIG.EXECUTABLE_PATH, "./win/aria2/aria2.conf")
|
||||
} else {
|
||||
aria2Path = path.join(CONFIG.EXECUTABLE_PATH, "./mac/aria2" + (CONFIG.IS_DEV ? `/${process.arch}` : '/') + "/aria2c");
|
||||
aria2Conf = path.join(CONFIG.EXECUTABLE_PATH, "./mac/aria2/aria2.conf")
|
||||
}
|
||||
// 启动 aria2
|
||||
console.log("启动 aria2")
|
||||
aria2Process = spawn(aria2Path, [`--conf-path=${aria2Conf}`, `--rpc-listen-port=${CONFIG.ARIA_PORT}`], {
|
||||
windowsHide: false,
|
||||
stdio: CONFIG.IS_DEV ? 'pipe' : 'ignore'
|
||||
});
|
||||
if(!aria2Process){
|
||||
console.log("启动 aria2 失败")
|
||||
}
|
||||
if (CONFIG.IS_DEV) {
|
||||
aria2Process.stdout.on('data', (data) => {
|
||||
console.log(`aria2: ${data}`);
|
||||
});
|
||||
aria2Process.stderr.on('data', (data) => {
|
||||
console.log(`aria2 error: ${data}`);
|
||||
});
|
||||
}
|
||||
console.log("aria2 成功启动")
|
||||
} catch (e) {
|
||||
console.log(`aria2 process start err`, e);
|
||||
}
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
initIPC()
|
||||
createWindow()
|
||||
createPreviewWindow(mainWindow)
|
||||
createArua2Process()
|
||||
setWin(mainWindow, previewWin)
|
||||
})
|
||||
})
|
||||
@@ -1,30 +1,27 @@
|
||||
import {ipcMain, dialog, BrowserWindow, app, shell} from 'electron'
|
||||
import {startServer} from './proxyServer'
|
||||
import {installCert, checkCertInstalled} from './cert'
|
||||
import {downloadFile, decodeWxFile, suffix} from './utils'
|
||||
import {decodeWxFile, suffix, getCurrentDateTimeFormatted} from './utils'
|
||||
// @ts-ignore
|
||||
import {hexMD5} from '../../src/common/md5'
|
||||
import {Aria2RPC} from './aria2Rpc'
|
||||
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"
|
||||
const aria2RpcClient = new Aria2RPC()
|
||||
|
||||
export default function initIPC() {
|
||||
|
||||
ipcMain.handle('invoke_app_is_init', async (event, arg) => {
|
||||
// 初始化应用 安装证书相关
|
||||
return checkCertInstalled()
|
||||
})
|
||||
|
||||
ipcMain.handle('invoke_init_app', (event, arg) => {
|
||||
ipcMain.handle('invoke_init_app', (event, arg) => {
|
||||
// 开始 初始化应用 安装证书相关
|
||||
installCert(false).then(r => {})
|
||||
installCert(false).then(r => {
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.handle('invoke_start_proxy', (event, arg) => {
|
||||
@@ -37,7 +34,7 @@ export default function initIPC() {
|
||||
win: win,
|
||||
upstreamProxy: arg.upstream_proxy ? arg.upstream_proxy : "",
|
||||
setProxyErrorCallback: err => {
|
||||
|
||||
console.log('setProxyErrorCallback', err)
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -67,45 +64,78 @@ 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, quality}) => {
|
||||
let down_url = data.url
|
||||
if (!down_url) {
|
||||
return false
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(false);
|
||||
});
|
||||
}
|
||||
if (quality !== "-1" && data.decode_key && data.file_format) {
|
||||
const format = data.file_format.split('#');
|
||||
const qualityMap = [
|
||||
format[0],
|
||||
format[Math.floor(format.length / 2)],
|
||||
format[format.length - 1]
|
||||
];
|
||||
down_url += "&X-snsvideoflag=" + qualityMap[quality];
|
||||
}
|
||||
let fileName = data?.description ? data.description.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '') : hexMD5(down_url);
|
||||
fileName = fileName + "_" + getCurrentDateTimeFormatted() + suffix(data.type)
|
||||
let save_path_file = `${save_path}/${fileName}`
|
||||
if (process.platform === 'win32') {
|
||||
save_path_file = `${save_path}\\${fileName}`
|
||||
}
|
||||
|
||||
let fileName = description ? description.replace(/[^a-zA-Z\u4e00-\u9fa5]/g, '') : hexMD5(down_url);
|
||||
let save_path_file = `${save_path}/${fileName}` + suffix(data.type)
|
||||
if (process.platform === 'win32'){
|
||||
save_path_file = `${save_path}\\${fileName}` + suffix(data.type)
|
||||
let 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',
|
||||
}
|
||||
|
||||
if (fs.existsSync(save_path_file)) {
|
||||
return {fullFileName: save_path_file, totalLen: ""}
|
||||
}
|
||||
// 开始下载
|
||||
return downloadFile(
|
||||
down_url,
|
||||
data.decode_key,
|
||||
save_path_file,
|
||||
(res) => {
|
||||
win?.webContents.send('on_down_file_schedule', {schedule: floor(res * 100)})
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
if (down_url.includes("douyin")) {
|
||||
headers['Referer'] = down_url
|
||||
}
|
||||
).catch(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()
|
||||
})
|
||||
aria2RpcClient.addUri([down_url], save_path, fileName, headers).then((response) => {
|
||||
let currentGid = response.result // 保存当前下载的 gid
|
||||
let progressIntervalId = null
|
||||
// // 开始定时查询下载进度
|
||||
progressIntervalId = setInterval(() => {
|
||||
aria2RpcClient.tellStatus(currentGid).then((status) => {
|
||||
if (status.result.status !== "complete") {
|
||||
const progress = aria2RpcClient.calculateDownloadProgress(status.result.bitfield);
|
||||
win?.webContents.send('on_down_file_schedule', {schedule: `已下载${progress}%`})
|
||||
} else {
|
||||
clearInterval(progressIntervalId);
|
||||
if (data.decode_key) {
|
||||
win?.webContents.send('on_down_file_schedule', {schedule: `开始解密`})
|
||||
decodeWxFile(save_path_file, data.decode_key, save_path_file.replace(".mp4", "_wx.mp4")).then((res) => {
|
||||
fs.unlink(save_path_file, (err) => {
|
||||
})
|
||||
resolve(res);
|
||||
}).catch((error) => {
|
||||
console.log("err:", error)
|
||||
resolve(false);
|
||||
});
|
||||
} else {
|
||||
resolve({
|
||||
fullFileName: save_path_file,
|
||||
});
|
||||
}
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
clearInterval(progressIntervalId);
|
||||
resolve(false);
|
||||
});
|
||||
}, 1000);
|
||||
}).catch((error) => {
|
||||
console.log("err:", error)
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.handle('invoke_resources_preview', async (event, {url}) => {
|
||||
if (!url) {
|
||||
@@ -121,16 +151,28 @@ export default function initIPC() {
|
||||
})
|
||||
|
||||
ipcMain.handle('invoke_open_default_browser', (event, {url}) => {
|
||||
shell.openExternal(url).then(r => {})
|
||||
shell.openExternal(url).then(r => {
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.handle('invoke_open_file_dir', (event, {save_path}) => {
|
||||
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()
|
||||
app.relaunch()
|
||||
app.exit()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -7,78 +7,37 @@ 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 resObject = {
|
||||
url: "",
|
||||
url_sign: "",
|
||||
cover_url: "",
|
||||
file_format: "",
|
||||
platform: "",
|
||||
size: "",
|
||||
type: "video/mp4",
|
||||
type_str: 'video',
|
||||
progress_bar: "",
|
||||
save_path: "",
|
||||
decode_key: "",
|
||||
description: ""
|
||||
}
|
||||
|
||||
const injection_script1 = `
|
||||
setTimeout(() => {
|
||||
let receiver_url = "https://res-downloader.666666.com";
|
||||
const vv = hexMD5(pkg.version) + (CONFIG.IS_DEV ? Math.random() :"")
|
||||
|
||||
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({
|
||||
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 +53,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 +72,34 @@ 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?.media?.length <= 0) {
|
||||
return
|
||||
}
|
||||
const media = req.json?.media[0]
|
||||
const url_sign: string = hexMD5(media.url)
|
||||
if (!media?.decodeKey || 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,
|
||||
cover_url: media.coverUrl,
|
||||
file_format: media.spec.map((res)=> res.fileFormat).join('#'),
|
||||
platform: urlInfo.hostname,
|
||||
size: toSize(media.fileSize),
|
||||
type: "video/mp4",
|
||||
type_str: 'video',
|
||||
decode_key: media.decodeKey,
|
||||
description: req.json.description,
|
||||
}))
|
||||
} catch (e) {
|
||||
log.log(e.toString())
|
||||
}
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
proxy.intercept(
|
||||
{
|
||||
@@ -142,10 +109,35 @@ 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('</body>', '\n<script>' + injection_script1 + '</script>\n</body>');
|
||||
res.statusCode = 200;
|
||||
res.string = res.string.replaceAll('.js"', '.js?v=' + vv + '"')
|
||||
res.statusCode = 200
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
proxy.intercept(
|
||||
{
|
||||
phase: 'response',
|
||||
hostname: 'res.wx.qq.com',
|
||||
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")) {
|
||||
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,139 +145,97 @@ 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) {
|
||||
log.log("--------------proxy catch err--------------", e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
app.on('before-quit', async e => {
|
||||
e.preventDefault()
|
||||
try {
|
||||
await closeProxy()
|
||||
log.log("--------------closeProxy success--------------")
|
||||
} catch (error) {
|
||||
}
|
||||
app.exit()
|
||||
})
|
||||
}
|
||||
@@ -125,4 +125,4 @@ function getMacAvailableNetworks() {
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,27 +100,61 @@ 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 ""
|
||||
}
|
||||
|
||||
export {downloadFile, toSize, decodeWxFile, suffix}
|
||||
function getCurrentDateTimeFormatted() {
|
||||
const now = new Date();
|
||||
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0'); // 月份从0开始,所以要加1
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
const hours = String(now.getHours()).padStart(2, '0');
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||||
|
||||
return `${year}${month}${day}${hours}${minutes}${seconds}`;
|
||||
}
|
||||
|
||||
export {downloadFile, toSize, decodeWxFile, suffix, getCurrentDateTimeFormatted}
|
||||
|
||||
0
electron/res/icon/icons/mac/icon.icns → electron/res/icon/mac.icns
Executable file → Normal file
0
electron/res/icon/icons/mac/icon.icns → electron/res/icon/mac.icns
Executable file → Normal file
0
electron/res/icon/icons/win/icon.ico → electron/res/icon/win.ico
Executable file → Normal file
0
electron/res/icon/icons/win/icon.ico → electron/res/icon/win.ico
Executable file → Normal file
|
Before Width: | Height: | Size: 199 KiB After Width: | Height: | Size: 199 KiB |
25
electron/res/mac/aria2/aria2.conf
Normal file
25
electron/res/mac/aria2/aria2.conf
Normal file
@@ -0,0 +1,25 @@
|
||||
#允许rpc
|
||||
enable-rpc=true
|
||||
#允许非外部访问
|
||||
rpc-listen-all=true
|
||||
|
||||
#最大同时下载数(任务数), 路由建议值: 3
|
||||
max-concurrent-downloads=3
|
||||
#断点续传
|
||||
continue=true
|
||||
#同服务器连接数
|
||||
max-connection-per-server=10
|
||||
#最小文件分片大小, 下载线程数上限取决于能分出多少片, 对于小文件重要
|
||||
min-split-size=10M
|
||||
#单文件最大线程数, 路由建议值: 5
|
||||
split=10
|
||||
#下载速度限制
|
||||
max-overall-download-limit=0
|
||||
#单文件速度限制
|
||||
max-download-limit=0
|
||||
#上传速度限制
|
||||
max-overall-upload-limit=0
|
||||
#单文件速度限制
|
||||
max-upload-limit=0
|
||||
|
||||
check-certificate=false
|
||||
BIN
electron/res/mac/aria2/arm64/aria2c
Normal file
BIN
electron/res/mac/aria2/arm64/aria2c
Normal file
Binary file not shown.
BIN
electron/res/mac/aria2/x64/aria2c
Normal file
BIN
electron/res/mac/aria2/x64/aria2c
Normal file
Binary file not shown.
25
electron/res/win/aria2/aria2.conf
Normal file
25
electron/res/win/aria2/aria2.conf
Normal file
@@ -0,0 +1,25 @@
|
||||
#允许rpc
|
||||
enable-rpc=true
|
||||
#允许非外部访问
|
||||
rpc-listen-all=true
|
||||
|
||||
#最大同时下载数(任务数), 路由建议值: 3
|
||||
max-concurrent-downloads=3
|
||||
#断点续传
|
||||
continue=true
|
||||
#同服务器连接数
|
||||
max-connection-per-server=10
|
||||
#最小文件分片大小, 下载线程数上限取决于能分出多少片, 对于小文件重要
|
||||
min-split-size=10M
|
||||
#单文件最大线程数, 路由建议值: 5
|
||||
split=10
|
||||
#下载速度限制
|
||||
max-overall-download-limit=0
|
||||
#单文件速度限制
|
||||
max-download-limit=0
|
||||
#上传速度限制
|
||||
max-overall-upload-limit=0
|
||||
#单文件速度限制
|
||||
max-upload-limit=0
|
||||
|
||||
check-certificate=false
|
||||
BIN
electron/res/win/aria2/aria2c.exe
Normal file
BIN
electron/res/win/aria2/aria2c.exe
Normal file
Binary file not shown.
0
electron/res/openssl/HashInfo.txt → electron/res/win/openssl/HashInfo.txt
Executable file → Normal file
0
electron/res/openssl/HashInfo.txt → electron/res/win/openssl/HashInfo.txt
Executable file → Normal file
0
electron/res/openssl/OpenSSL License.txt → electron/res/win/openssl/OpenSSL License.txt
Executable file → Normal file
0
electron/res/openssl/OpenSSL License.txt → electron/res/win/openssl/OpenSSL License.txt
Executable file → Normal file
0
electron/res/openssl/ReadMe.txt → electron/res/win/openssl/ReadMe.txt
Executable file → Normal file
0
electron/res/openssl/ReadMe.txt → electron/res/win/openssl/ReadMe.txt
Executable file → Normal file
0
electron/res/openssl/libeay32.dll → electron/res/win/openssl/libeay32.dll
Executable file → Normal file
0
electron/res/openssl/libeay32.dll → electron/res/win/openssl/libeay32.dll
Executable file → Normal file
0
electron/res/openssl/openssl.cnf → electron/res/win/openssl/openssl.cnf
Executable file → Normal file
0
electron/res/openssl/openssl.cnf → electron/res/win/openssl/openssl.cnf
Executable file → Normal file
0
electron/res/openssl/openssl.exe → electron/res/win/openssl/openssl.exe
Executable file → Normal file
0
electron/res/openssl/openssl.exe → electron/res/win/openssl/openssl.exe
Executable file → Normal file
0
electron/res/openssl/ssleay32.dll → electron/res/win/openssl/ssleay32.dll
Executable file → Normal file
0
electron/res/openssl/ssleay32.dll → electron/res/win/openssl/ssleay32.dll
Executable file → Normal file
0
electron/res/regedit-vbs/1.wsf → electron/res/win/regedit-vbs/1.wsf
Executable file → Normal file
0
electron/res/regedit-vbs/1.wsf → electron/res/win/regedit-vbs/1.wsf
Executable file → Normal file
0
electron/res/regedit-vbs/ArchitectureAgnosticRegistry.vbs → electron/res/win/regedit-vbs/ArchitectureAgnosticRegistry.vbs
Executable file → Normal file
0
electron/res/regedit-vbs/ArchitectureAgnosticRegistry.vbs → electron/res/win/regedit-vbs/ArchitectureAgnosticRegistry.vbs
Executable file → Normal file
0
electron/res/regedit-vbs/ArchitectureSpecificRegistry.vbs → electron/res/win/regedit-vbs/ArchitectureSpecificRegistry.vbs
Executable file → Normal file
0
electron/res/regedit-vbs/ArchitectureSpecificRegistry.vbs → electron/res/win/regedit-vbs/ArchitectureSpecificRegistry.vbs
Executable file → Normal file
0
electron/res/regedit-vbs/JsonSafeTest.wsf → electron/res/win/regedit-vbs/JsonSafeTest.wsf
Executable file → Normal file
0
electron/res/regedit-vbs/JsonSafeTest.wsf → electron/res/win/regedit-vbs/JsonSafeTest.wsf
Executable file → Normal file
0
electron/res/regedit-vbs/regCreateKey.wsf → electron/res/win/regedit-vbs/regCreateKey.wsf
Executable file → Normal file
0
electron/res/regedit-vbs/regCreateKey.wsf → electron/res/win/regedit-vbs/regCreateKey.wsf
Executable file → Normal file
0
electron/res/regedit-vbs/regDeleteKey.wsf → electron/res/win/regedit-vbs/regDeleteKey.wsf
Executable file → Normal file
0
electron/res/regedit-vbs/regDeleteKey.wsf → electron/res/win/regedit-vbs/regDeleteKey.wsf
Executable file → Normal file
0
electron/res/regedit-vbs/regDeleteValue.wsf → electron/res/win/regedit-vbs/regDeleteValue.wsf
Executable file → Normal file
0
electron/res/regedit-vbs/regDeleteValue.wsf → electron/res/win/regedit-vbs/regDeleteValue.wsf
Executable file → Normal file
0
electron/res/regedit-vbs/regList.wsf → electron/res/win/regedit-vbs/regList.wsf
Executable file → Normal file
0
electron/res/regedit-vbs/regList.wsf → electron/res/win/regedit-vbs/regList.wsf
Executable file → Normal file
0
electron/res/regedit-vbs/regListStream.wsf → electron/res/win/regedit-vbs/regListStream.wsf
Executable file → Normal file
0
electron/res/regedit-vbs/regListStream.wsf → electron/res/win/regedit-vbs/regListStream.wsf
Executable file → Normal file
0
electron/res/regedit-vbs/regPutValue.wsf → electron/res/win/regedit-vbs/regPutValue.wsf
Executable file → Normal file
0
electron/res/regedit-vbs/regPutValue.wsf → electron/res/win/regedit-vbs/regPutValue.wsf
Executable file → Normal file
0
electron/res/regedit-vbs/regUtil.vbs → electron/res/win/regedit-vbs/regUtil.vbs
Executable file → Normal file
0
electron/res/regedit-vbs/regUtil.vbs → electron/res/win/regedit-vbs/regUtil.vbs
Executable file → Normal file
0
electron/res/regedit-vbs/util.vbs → electron/res/win/regedit-vbs/util.vbs
Executable file → Normal file
0
electron/res/regedit-vbs/util.vbs → electron/res/win/regedit-vbs/util.vbs
Executable file → Normal file
0
electron/res/regedit-vbs/wsRegReadList.wsf → electron/res/win/regedit-vbs/wsRegReadList.wsf
Executable file → Normal file
0
electron/res/regedit-vbs/wsRegReadList.wsf → electron/res/win/regedit-vbs/wsRegReadList.wsf
Executable file → Normal file
0
electron/res/regedit-vbs/wsRegReadListStream.wsf → electron/res/win/regedit-vbs/wsRegReadListStream.wsf
Executable file → Normal file
0
electron/res/regedit-vbs/wsRegReadListStream.wsf → electron/res/win/regedit-vbs/wsRegReadListStream.wsf
Executable file → Normal file
BIN
electron/res/win/w_c.exe
Normal file
BIN
electron/res/win/w_c.exe
Normal file
Binary file not shown.
10
package.json
10
package.json
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "res-downloader",
|
||||
"version": "1.0.6",
|
||||
"version": "2.1.0",
|
||||
"main": "dist-electron/main/index.js",
|
||||
"description": "Electron + Vue + Vite 实现的资源下载软件,支持微信视频号下载、抖音视频下载、快手视频下载、酷狗音乐下载等",
|
||||
"description": "res-downloader(爱享素材下载器),支持视频号、小程序、抖音、快手、小红书、酷狗音乐、qq音乐、qq短视频等",
|
||||
"author": "putyy@qq.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
@@ -19,8 +19,8 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build && electron-builder",
|
||||
"dev": "node script/hoxy.ts && vite",
|
||||
"build": "node script/hoxy.ts && vue-tsc --noEmit && vite build && electron-builder",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -50,7 +50,7 @@
|
||||
"vite-plugin-electron-renderer": "^0.14.5",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.4",
|
||||
"vue-tsc": "^1.8.8"
|
||||
"vue-tsc": "^2.0.29"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.5.0",
|
||||
|
||||
BIN
public/show.webp
BIN
public/show.webp
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 39 KiB |
7
script/hoxy.ts
Normal file
7
script/hoxy.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const currentDir = process.cwd();
|
||||
|
||||
// 移动文件
|
||||
fs.copyFile(currentDir + '/override/hoxy/lib/cycle.js', currentDir + '/node_modules/hoxy/lib/cycle.js', fs.constants.COPYFILE_FICLONE, (err) => {
|
||||
});
|
||||
@@ -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)") 软件源码
|
||||
</template>
|
||||
|
||||
@@ -12,7 +12,10 @@ el-container
|
||||
Sidebar
|
||||
el-container
|
||||
el-main
|
||||
router-view
|
||||
router-view(v-slot="{ Component, route }")
|
||||
keep-alive(v-if="route.meta.keepAlive")
|
||||
component(:is="Component")
|
||||
component(v-else :is="Component")
|
||||
el-footer
|
||||
Footer
|
||||
</template>
|
||||
|
||||
@@ -13,16 +13,19 @@ const routes = [
|
||||
{
|
||||
path: '/index',
|
||||
name: 'Index',
|
||||
meta: {keepAlive: true},
|
||||
component: () => import('./views/Index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
meta: {keepAlive: true},
|
||||
component: () => import('./views/About.vue'),
|
||||
},
|
||||
{
|
||||
path: '/setting',
|
||||
name: 'Setting',
|
||||
meta: {keepAlive: true},
|
||||
component: () => import('./views/Setting.vue'),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -47,7 +47,7 @@ const str = "使用方法\n" +
|
||||
</script>
|
||||
<template lang="pug">
|
||||
div.about
|
||||
div 1. 本软件免费、源代码已经开源,不会以任何形式收取费用。
|
||||
div 1. 本软件免费、代码已开源,不会以任何形式收取费用。
|
||||
el-button(@click="jump(3)") 查看源码
|
||||
div 2. m3u8复制的链接如何使用?
|
||||
el-button(@click="jump(1)") 在线下载
|
||||
@@ -77,4 +77,4 @@ div.about
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -6,20 +6,16 @@ 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,
|
||||
url_sign: string,
|
||||
size: any,
|
||||
platform: string,
|
||||
type: string,
|
||||
type_str: string,
|
||||
progress_bar: any,
|
||||
save_path: string,
|
||||
downing: boolean,
|
||||
decode_key: string,
|
||||
description: string,
|
||||
uploader: string,
|
||||
}
|
||||
|
||||
const tableData = ref<resData[]>([])
|
||||
@@ -58,7 +54,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
ipcRenderer.on('on_down_file_schedule', (res: any, data: any) => {
|
||||
loading.value && loading.value.setText(`已下载 ${data.schedule}%`)
|
||||
loading.value && loading.value.setText(`${data.schedule}%`)
|
||||
})
|
||||
|
||||
ipcRenderer.invoke('invoke_app_is_init').then((isInit: boolean) => {
|
||||
@@ -95,7 +91,7 @@ onUnmounted(() => {
|
||||
})
|
||||
})
|
||||
|
||||
watch(resType, (res, res1)=>{
|
||||
watch(resType, (res, res1) => {
|
||||
localStorageCache.set("res-type", resType.value, -1)
|
||||
}, {deep: true})
|
||||
|
||||
@@ -123,24 +119,12 @@ const handleBatchDown = async () => {
|
||||
text: '下载中',
|
||||
background: 'rgba(0, 0, 0, 0.7)',
|
||||
})
|
||||
|
||||
const quality = localStorageCache.get("quality") ? localStorageCache.get("quality") : -1
|
||||
for (const item of multipleSelection.value) {
|
||||
let result = await ipcRenderer.invoke('invoke_file_exists', {
|
||||
save_path: save_dir,
|
||||
url: item.url,
|
||||
})
|
||||
|
||||
if (result.is_file) {
|
||||
item.progress_bar = "100%"
|
||||
item.save_path = result.fileName
|
||||
continue
|
||||
}
|
||||
|
||||
let downRes = await ipcRenderer.invoke('invoke_down_file', {
|
||||
index: 0,
|
||||
data: Object.assign({}, item),
|
||||
save_path: save_dir,
|
||||
high: false
|
||||
quality: quality,
|
||||
})
|
||||
|
||||
if (downRes !== false) {
|
||||
@@ -154,9 +138,7 @@ const handleBatchDown = async () => {
|
||||
|
||||
|
||||
const handleDown = async (index: number, row: any) => {
|
||||
|
||||
let save_dir = localStorageCache.get("save_dir")
|
||||
|
||||
const save_dir = localStorageCache.get("save_dir")
|
||||
if (!save_dir) {
|
||||
ElMessage({
|
||||
message: '请设置保存目录',
|
||||
@@ -171,34 +153,17 @@ const handleDown = async (index: number, row: any) => {
|
||||
background: 'rgba(0, 0, 0, 0.7)',
|
||||
})
|
||||
|
||||
let result = await ipcRenderer.invoke('invoke_file_exists', {
|
||||
save_path: save_dir,
|
||||
url: row.high_url ? row.high_url : row.url,
|
||||
description: row.description
|
||||
})
|
||||
|
||||
if (result.is_file) {
|
||||
tableData.value[index].progress_bar = "100%"
|
||||
tableData.value[index].save_path = result.fileName
|
||||
ElMessage({
|
||||
message: "文件已存在(" + result.fileName + ")",
|
||||
type: 'warning',
|
||||
})
|
||||
loading.value.close()
|
||||
localStorageCache.set("res-table-data", tableData.value, -1)
|
||||
return
|
||||
}
|
||||
|
||||
const quality = localStorageCache.get("quality") ? localStorageCache.get("quality") : -1
|
||||
ipcRenderer.invoke('invoke_down_file', {
|
||||
data: Object.assign({}, tableData.value[index]),
|
||||
save_path: save_dir,
|
||||
description: row.description
|
||||
quality: quality,
|
||||
}).then((res) => {
|
||||
if (res !== false) {
|
||||
tableData.value[index].progress_bar = "100%"
|
||||
tableData.value[index].save_path = res.fullFileName
|
||||
localStorageCache.set("res-table-data", tableData.value, -1)
|
||||
}else{
|
||||
} else {
|
||||
ElMessage({
|
||||
message: "下载失败",
|
||||
type: 'warning',
|
||||
@@ -233,7 +198,7 @@ const decodeWxFile = (index: number) => {
|
||||
tableData.value[index].progress_bar = "100%"
|
||||
tableData.value[index].save_path = res.fullFileName
|
||||
localStorageCache.set("res-table-data", tableData.value, -1)
|
||||
}else{
|
||||
} else {
|
||||
ElMessage({
|
||||
message: "解密失败",
|
||||
type: 'warning',
|
||||
@@ -250,36 +215,42 @@ const decodeWxFile = (index: number) => {
|
||||
}
|
||||
|
||||
const handlePreview = (index: number, row: any) => {
|
||||
ipcRenderer.invoke('invoke_resources_preview', {url: row.down_url}).catch(() => {
|
||||
ipcRenderer.invoke('invoke_resources_preview', {url: row.url}).catch(() => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleClear = () => {
|
||||
tableData.value = []
|
||||
localStorageCache.del("res-table-data")
|
||||
ipcRenderer.invoke('invoke_file_del', {
|
||||
url_sign: "all"
|
||||
})
|
||||
}
|
||||
|
||||
const handleCopy = (text: string) => {
|
||||
let el = document.createElement('input')
|
||||
el.setAttribute('value', text)
|
||||
document.body.appendChild(el)
|
||||
el.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(el)
|
||||
ElMessage({
|
||||
message: "复制成功",
|
||||
type: 'success',
|
||||
})
|
||||
let el = document.createElement('input')
|
||||
el.setAttribute('value', text)
|
||||
document.body.appendChild(el)
|
||||
el.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(el)
|
||||
ElMessage({
|
||||
message: "复制成功",
|
||||
type: 'success',
|
||||
})
|
||||
}
|
||||
|
||||
const handleDel = (index: number)=>{
|
||||
const handleDel = (index: number) => {
|
||||
let arr = tableData.value
|
||||
arr.splice(index, 1);
|
||||
tableData.value = arr
|
||||
localStorageCache.set("res-table-data", tableData.value, -1)
|
||||
ipcRenderer.invoke('invoke_file_del', {
|
||||
url_sign: tableData.value[index].url_sign
|
||||
})
|
||||
}
|
||||
|
||||
const openFileDir = (index: number)=>{
|
||||
const openFileDir = (index: number) => {
|
||||
ipcRenderer.invoke('invoke_open_file_dir', {
|
||||
save_path: tableData.value[index].save_path
|
||||
})
|
||||
@@ -319,13 +290,13 @@ el-container.container
|
||||
el-main
|
||||
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")
|
||||
el-table-column(label="预览" show-overflow-tooltip width="150px")
|
||||
template(#default="scope")
|
||||
div.show_res
|
||||
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" crossorigin="anonymous")
|
||||
video.video(v-if="scope.row.type_str === 'video'" :src="scope.row.url" controls preload="none")
|
||||
img.img(v-if="scope.row.type_str === 'image'" :src="scope.row.url" crossorigin="anonymous")
|
||||
audio.audio(v-if="scope.row.type_str === 'audio'" controls preload="none")
|
||||
source(:src="scope.row.down_url" :type="scope.row.type")
|
||||
source(:src="scope.row.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="主机地址")
|
||||
@@ -336,10 +307,10 @@ el-container.container
|
||||
template(#default="scope")
|
||||
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)") {{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="handleDown(scope.$index, scope.row)") {{scope.row.decode_key || scope.row.decryptor_array ? "解密下载(视频号)" : "下载"}}
|
||||
el-button(v-if="scope.row.decode_key || scope.row.decryptor_array" 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="handleCopy(scope.row.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>
|
||||
@@ -375,15 +346,17 @@ el-container.container
|
||||
}
|
||||
}
|
||||
|
||||
.show_res{
|
||||
width: 100%;
|
||||
height: auto;
|
||||
.img{
|
||||
.show_res {
|
||||
.img {
|
||||
max-height: 200px;
|
||||
}
|
||||
.video {
|
||||
width: auto;
|
||||
max-height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.actions{
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
@@ -7,9 +7,26 @@ import {ElMessage} from "element-plus"
|
||||
const saveDir = ref("")
|
||||
const upstream_proxy = ref("")
|
||||
const upstream_proxy_old = ref("")
|
||||
const quality = ref("-1")
|
||||
const qualityOptions = ref([
|
||||
{
|
||||
value: '-1',
|
||||
label: '默认(推荐)'
|
||||
}, {
|
||||
value: '0',
|
||||
label: '高画质'
|
||||
}, {
|
||||
value: '1',
|
||||
label: '中画质'
|
||||
}, {
|
||||
value: '2',
|
||||
label: '低画质'
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
saveDir.value = localStorageCache.get("save_dir") ? localStorageCache.get("save_dir") : ""
|
||||
quality.value = localStorageCache.get("quality") ? localStorageCache.get("quality") : "-1"
|
||||
upstream_proxy.value = localStorageCache.get("upstream_proxy") ? localStorageCache.get("upstream_proxy") : ""
|
||||
upstream_proxy_old.value = upstream_proxy.value
|
||||
})
|
||||
@@ -25,22 +42,29 @@ const selectSaveDir = () => {
|
||||
const onSetting = () => {
|
||||
localStorageCache.set("save_dir", saveDir.value, -1)
|
||||
localStorageCache.set("upstream_proxy", upstream_proxy.value, -1)
|
||||
localStorageCache.set("quality", quality.value, -1)
|
||||
if (upstream_proxy_old.value != upstream_proxy.value){
|
||||
ipcRenderer.invoke('invoke_window_restart')
|
||||
}
|
||||
ElMessage({
|
||||
message: "操作成功",
|
||||
message: "保存成功",
|
||||
type: 'success',
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
<template lang="pug">
|
||||
el-form
|
||||
el-form(style="max-width: 600px")
|
||||
el-form-item(label="保存位置")
|
||||
el-button(@click="selectSaveDir") {{saveDir ? saveDir : '选择'}}
|
||||
el-form-item(label="代理服务")
|
||||
el-input(v-model="upstream_proxy" placeholder="例如: http://127.0.0.1:7890 修改此项需重启本软件" )
|
||||
el-link(@click="selectSaveDir") {{saveDir ? saveDir : '选择'}}
|
||||
el-form-item(label="视频号画质")
|
||||
el-select(v-model="quality" placeholder="请选择")
|
||||
el-option( v-for="item in qualityOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value")
|
||||
el-form-item(label="特殊代理")
|
||||
el-input(v-model="upstream_proxy" placeholder="例如: http://127.0.0.1:7890 修改此项需重启本软件,如不清楚用途请勿设置。" )
|
||||
el-form-item
|
||||
el-button(type="primary" @click="onSetting") 保存
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user