21 Commits
2.1.1 ... 2.3.0

Author SHA1 Message Date
putyy
d9259bb259 Merge pull request #114 from putyy/dev
win7支持
2024-12-04 16:17:08 +08:00
putyy
8756e5357a 修改Md 2024-12-04 16:15:20 +08:00
putyy
4c09b8f3be x 2024-12-04 15:47:38 +08:00
putyy
6f3e361ee6 x 2024-12-04 15:46:57 +08:00
putyy
93d9dbe32b win7支持,修复目录缓存失效 2024-12-02 17:36:51 +08:00
putyy
fd89b7125c Merge pull request #106 from putyy/dev
Dev
2024-11-18 22:05:10 +08:00
putyy
bb0e97c402 优化 2024-11-18 22:04:46 +08:00
putyy
edd097855b 优化代理启动、增加table筛选等 2024-11-18 17:41:43 +08:00
putyy
4d35c44247 Merge pull request #97 from putyy/dev
完善content type
2024-10-29 10:52:35 +08:00
putyy
d7e34d9c21 完善content type 2024-10-29 10:51:58 +08:00
putyy
fc06c29759 Merge pull request #95 from putyy/dev
Dev
2024-10-23 17:31:27 +08:00
putyy
c65702e215 修改md version 2024-10-23 17:30:39 +08:00
putyy
96300164da 修改md version 2024-10-23 17:28:34 +08:00
putyy
1b35475302 完善类型 2024-10-23 10:24:16 +08:00
putyy
240bae9be9 修改md 2024-10-15 13:42:52 +08:00
putyy
46b1592a7f 更新版本号 2024-10-15 13:37:52 +08:00
putyy
1cae714fd2 Merge pull request #90 from putyy/dev
完善资源类型、优化资源类型选择、优化table固定表头等
2024-10-15 11:19:48 +08:00
putyy
5e63624955 完善资源类型、优化资源类型选择、优化table固定表头等 2024-10-15 11:19:17 +08:00
putyy
7584156262 Update README.md 2024-10-10 09:05:09 +08:00
putyy
d23c09748b 调整打包 2024-09-04 09:20:20 +08:00
putyy
936adca3f2 更新执行文件 2024-09-03 16:43:24 +08:00
25 changed files with 627 additions and 577 deletions

View File

@@ -1,19 +1,20 @@
## res-downloader(爱享素材下载器) 【[点击加入群聊](https://qm.qq.com/q/W8mVeZideE)】
## res-downloader
### 爱享素材下载器【[加入群聊](https://qm.qq.com/q/mfDMSpCxQ4)】
🎯 基于 [electron-vite-vue](https://github.com/electron-vite/electron-vite-vue.git)
📦 操作简单、可获取不同类型的资源
🖥️ 支持Win10、Win11、Mac
🌐 支持视频、音频、图片、m3u8等网络资源下载
📦 操作简单、可获取不同类型的资源
🖥️ 支持Windows、Mac、Linux
🌐 支持视频、音频、图片、m3u8、直播流等常见网络资源
💪 支持微信视频号、小程序、抖音、快手、小红书、酷狗音乐、qq音乐等网络资源下载
👼 支持设置代理以获取特殊网络下的资源
👼 支持设置代理以获取特殊网络下的资源
## 软件下载
🆕 [github下载](https://github.com/putyy/res-downloader/releases)
🆕 [蓝奏云下载 密码:9vs5](https://wwjv.lanzoum.com/b04wgtfyb)
## 使用方法
> 0. 安装一定要同意安装证书文件,安装一定要同意安装证书文件,安装一定要同意安装证书文件!
> 1. 打开本软件
> 2. 软件首页选择要获取的资源类型(默认选中的视频
> 0. 安装一定要同意安装证书文件、使用时一定要允许网络访问
> 1. 打开本软件 软件首页左上角点击 “启动代理”
> 2. 软件首页选择要获取的资源类型(默认选中的全部
> 3. 打开要捕获的源, 如:视频号、网页、小程序等等
> 4. 返回软件首页即可看到资源列表
@@ -21,13 +22,16 @@
![](public/show.webp)
## 常见问题
下载慢、大视频下载失败
m3u8: 预览和下载:
> [下载](https://m3u8-down.gowas.cn/) [预览](https://m3u8play.com/)
直播流: 预览和录制:
> [使用obs进行预览和录制 使用教程自行百度]( https://obsproject.com/)
下载慢、大视频下载失败(最新版本已内置aria2下载器)
> 推荐使用如下工具加速下载,视频号可以下载完成后再到对应视频操作项选择 “视频解密(视频号)” 按钮
>> [Neat Download Manager](https://www.neatdownloadmanager.com/index.php/en/)、[Motrix](https://motrix.app/download)等软件进行下载
Win7无法使用
> 软件不支持,也无计划支持
打开本软件,无法正常拦截获取
> 检查系统代理是否正确设置 代理地址127.0.0.1 端口8899
@@ -36,28 +40,15 @@ Win7无法使用
打开本软件后无法上网
> 手动删除安装标识锁文件,之后再打开软件会进行检查证书是否正确安装
>> MAC: /Users/你的用户名称/.res-downloader@putyy/res-downloader-installed.lock
>> 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
其他问题
[github](https://github.com/putyy/res-downloader/issues) 、 [爱享论坛](https://s.gowas.cn/d/4089)
## 二次开发
> ps 打包慢的问题可以参考 https://www.putyy.com/articles/87
```sh
git clone https://github.com/putyy/res-downloader
cd res-downloader
yarn install
yarn run dev
# 打包mac
yarn run build --universal --mac
# 打包win
yarn run build --win
```
## 实现 & 初衷
通过代理网络抓包拦截响应,筛选出有用的资源, 同fiddler、charles等抓包软件、浏览器F12打开控制也能达到目的只不过这些软件需要手动进行筛选对于小白用户上手还是有点难度本软件对部分资源做了特殊处理更适合大众用户所以就有了本项目。
## 免责声明
本软件用于学习研究使用,若因使用本软件造成的一切法律责任均与本人无关!

6
components.d.ts vendored
View File

@@ -8,6 +8,9 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
ElButton: typeof import('element-plus/es')['ElButton']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElFooter: typeof import('element-plus/es')['ElFooter']
ElForm: typeof import('element-plus/es')['ElForm']
@@ -15,12 +18,11 @@ 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']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
Footer: typeof import('./src/components/layout/Footer.vue')['default']

View File

@@ -10,8 +10,7 @@
},
"files": [
"dist-electron",
"dist",
"electron/res/**/*"
"dist"
],
"mac": {
"icon": "electron/res/icon/mac.icns",

View File

@@ -36,16 +36,10 @@ export class Aria2RPC {
}
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); // 保留两位小数
const totalPieces = bitfield.length * 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)
}
}

View File

@@ -9,51 +9,62 @@ export function checkCertInstalled() {
return fs.existsSync(CONFIG.INSTALL_CERT_FLAG)
}
export async function installCert(checkInstalled = true) {
if (checkInstalled && checkCertInstalled()) {
return;
}
mkdirp.sync(path.dirname(CONFIG.INSTALL_CERT_FLAG))
export function installCert(checkInstalled = true) {
try {
if (checkInstalled && checkCertInstalled()) {
return;
}
mkdirp.sync(path.dirname(CONFIG.INSTALL_CERT_FLAG))
if (process.platform === 'darwin') {
return new Promise((resolve, reject) => {
clipboard.writeText(
`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",
message: `命令已复制到剪贴板,粘贴命令到终端并运行以安装并信任证书`,
});
reject()
});
} else if (process.platform === 'linux') {
return new Promise((resolve, reject) => {
clipboard.writeText(
"https://github.com/putyy/res-downloader/blob/master/electron/res/keys/public.pem",
)
dialog.showMessageBoxSync({
type: "info",
message: `Linux系统请手动安装证书已复制下载地址`,
});
reject()
});
} else {
return new Promise((resolve: any, reject) => {
const result = spawn.sync(CONFIG.WIN_CERT_INSTALL_HELPER, [
'-c',
'-add',
CONFIG.CERT_PUBLIC_PATH,
'-s',
'root',
]);
if (result.stdout.toString().indexOf('Succeeded') > -1) {
fs.writeFileSync(CONFIG.INSTALL_CERT_FLAG, '')
resolve()
} else {
reject()
}
})
if (process.platform === 'darwin') {
handleMacCertInstallation()
} else if (process.platform === 'win32') {
handleWindowsCertInstallation()
} else {
handleOtherCertInstallation()
}
} catch (e) {
handleOtherCertInstallation()
}
}
// MacOS 证书安装处理
function handleMacCertInstallation() {
clipboard.writeText(
`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',
message: '命令已复制到剪贴板,粘贴到终端并运行以安装并信任证书',
});
}
// Linux 证书安装处理
function handleOtherCertInstallation() {
clipboard.writeText(CONFIG.CERT_PUBLIC_PATH);
dialog.showMessageBoxSync({
type: "info",
message: `请手动安装证书,证书文件路径:${CONFIG.CERT_PUBLIC_PATH} 已复制到剪贴板`,
});
}
// Windows 证书安装处理
function handleWindowsCertInstallation() {
const result = spawn.sync(CONFIG.WIN_CERT_INSTALL_HELPER, [
'-c',
'-add',
CONFIG.CERT_PUBLIC_PATH,
'-s',
'root',
]);
if (result.stdout.toString().includes('Succeeded')) {
fs.writeFileSync(CONFIG.INSTALL_CERT_FLAG, '');
} else {
handleOtherCertInstallation();
}
}

View File

@@ -7,6 +7,8 @@ import {closeProxy} from "./setProxy"
import log from "electron-log"
import path from 'path'
import {spawn} from 'child_process'
import {startServer} from "./proxyServer";
import fs from "fs";
// The built directory structure
//
@@ -56,7 +58,19 @@ const preload = join(__dirname, '../preload/index.js')
const url = process.env.VITE_DEV_SERVER_URL
const indexHtml = join(process.env.DIST, 'index.html')
global.videoList = {}
global.isStartProxy = false
global.isSettingProxy = false
global.resdConfig = {
save_dir: "",
quality: "-1",
proxy: "",
port: 8899,
}
// app.whenReady().then(createWindow)
// 修复部分机型GPU主窗口空白错误
app.disableHardwareAcceleration();
app.on('window-all-closed', () => {
mainWindow = null
@@ -100,8 +114,10 @@ function createWindow() {
mainWindow = new BrowserWindow({
title: 'Main window',
icon: join(process.env.VITE_PUBLIC, 'favicon.ico'),
width: 800,
height: 600,
width: 1024,
minWidth: 960,
height: 768,
minHeight: 640,
webPreferences: {
preload,
// Warning: Enable nodeIntegration and disable contextIsolation is not secure in production
@@ -144,6 +160,8 @@ function createPreviewWindow(parent: BrowserWindow) {
parent: parent,
width: 600,
height: 400,
minWidth: 600,
minHeight: 400,
show: false,
// paintWhenInitiallyHidden: false,
webPreferences: {
@@ -157,14 +175,11 @@ function createPreviewWindow(parent: BrowserWindow) {
previewWin.setTitle("预览")
previewWin.on("page-title-updated", (event) => {
// 阻止该事件
event.preventDefault()
})
previewWin.on("close", (event) => {
// 不关闭窗口
event.preventDefault()
// 影藏窗口
previewWin.hide()
})
}
@@ -181,14 +196,12 @@ function createAria2Process() {
aria2Path = path.join(CONFIG.EXECUTABLE_PATH, `./${process.platform}/aria2` + (CONFIG.IS_DEV ? `/${process.arch}` : '/') + "/aria2c");
aria2Conf = path.join(CONFIG.EXECUTABLE_PATH, `./${process.platform}/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 失败")
console.log("start aria2 error")
}
if (CONFIG.IS_DEV) {
aria2Process.stdout.on('data', (data) => {
@@ -198,16 +211,35 @@ function createAria2Process() {
console.log(`aria2 error: ${data}`);
});
}
console.log("aria2 成功启动")
} catch (e) {
console.log(`aria2 process start err`, e);
}
}
function initConfig(){
const configPath = path.join(app.getPath('userData'), 'resd_config.json')
if (!fs.existsSync(configPath)) {
return
}
const buff = fs.readFileSync(configPath);
if (buff) {
try {
const jsonData = JSON.parse(buff)
global.resdConfig = Object.assign({}, global.resdConfig, jsonData)
if (!global.resdConfig.port) {
global.resdConfig.port = 8899
}
} catch (parseErr) {
}
}
}
app.whenReady().then(() => {
initIPC()
createWindow()
createPreviewWindow(mainWindow)
createAria2Process()
setWin(mainWindow, previewWin)
initConfig()
initIPC()
startServer(mainWindow)
createAria2Process()
})

View File

@@ -1,15 +1,18 @@
import {ipcMain, dialog, BrowserWindow, app, shell} from 'electron'
import {startServer} from './proxyServer'
import {installCert, checkCertInstalled} from './cert'
import {decodeWxFile, suffix, getCurrentDateTimeFormatted} from './utils'
import {decodeWxFile, typeSuffix, getCurrentDateTimeFormatted} from './utils'
// @ts-ignore
import {hexMD5} from '../../src/common/md5'
import {Aria2RPC} from './aria2Rpc'
import fs from "fs"
import urlTool from "url";
import {closeProxy, setProxy} from "./setProxy";
import path from 'path'
let win: BrowserWindow
let previewWin: BrowserWindow
let isStartProxy = false
const aria2RpcClient = new Aria2RPC()
export default function initIPC() {
@@ -20,23 +23,41 @@ export default function initIPC() {
ipcMain.handle('invoke_init_app', (event, arg) => {
// 开始 初始化应用 安装证书相关
installCert(false).then(r => {
})
installCert(false)
})
ipcMain.handle('invoke_start_proxy', (event, arg) => {
ipcMain.handle('invoke_set_config', (event, data) => {
const filePath = path.join(app.getPath('userData'), 'resd_config.json');
fs.writeFile(filePath, JSON.stringify(data), ()=>{})
global.resdConfig = Object.assign({}, global.resdConfig, data)
global.resdConfig.port = parseInt(global.resdConfig.port)
return true
})
ipcMain.handle('invoke_set_proxy', async (event, arg) => {
// 启动代理服务
if (isStartProxy) {
return
if (!global.isStartProxy) {
dialog.showMessageBoxSync({
type: "error",
message: "代理未启动",
});
return false
}
try {
if (arg.proxy) {
await setProxy('127.0.0.1', global.resdConfig.port)
}else{
await closeProxy('127.0.0.1', global.resdConfig.port)
}
return true
} catch (err) {
console.error(err);
dialog.showMessageBoxSync({
type: "error",
message: err.toString(),
});
return false
}
isStartProxy = true
return startServer({
win: win,
upstreamProxy: arg.upstream_proxy ? arg.upstream_proxy : "",
setProxyErrorCallback: err => {
console.log('setProxyErrorCallback', err)
},
})
})
ipcMain.handle('invoke_select_down_dir', async (event, arg) => {
@@ -71,7 +92,14 @@ export default function initIPC() {
resolve(false);
});
}
if (quality !== "-1" && data.decode_key && data.file_format) {
if (quality === "0" && data.decode_key) {
const urlInfo = urlTool.parse(down_url, true);
if (urlInfo.query["token"] && urlInfo.query["encfilekey"]) {
down_url = urlInfo.protocol + "//" + urlInfo.hostname + urlInfo.pathname.replace("251/20302", "251/20304") +
"?encfilekey=" + urlInfo.query["encfilekey"] +
"&token=" + urlInfo.query["token"]
}
} else if (quality !== "-1" && data.decode_key && data.file_format) {
const format = data.file_format.split('#');
const qualityMap = [
format[0],
@@ -81,7 +109,7 @@ export default function initIPC() {
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)
fileName = fileName + "_" + getCurrentDateTimeFormatted() + typeSuffix(data.type)[1]
let save_path_file = `${save_path}/${fileName}`
if (process.platform === 'win32') {
save_path_file = `${save_path}\\${fileName}`
@@ -93,18 +121,18 @@ export default function initIPC() {
return new Promise((resolve, reject) => {
if (down_url.includes("douyin")) {
headers['Referer'] = down_url
if (data?.referer) {
headers['Referer'] = data?.referer
}
aria2RpcClient.addUri([down_url], save_path, fileName, headers).then((response) => {
let currentGid = response.result // 保存当前下载的 gid
let currentGid = response.result
let progressIntervalId = null
// // 开始定时查询下载进度
progressIntervalId = setInterval(() => {
aria2RpcClient.tellStatus(currentGid).then((status) => {
if (status.result.status !== "complete") {
const progress = aria2RpcClient.calculateDownloadProgress(status.result.bitfield);
const progress = aria2RpcClient.calculateDownloadProgress(status.result.bitfield)
win?.webContents.send('on_down_file_schedule', {schedule: `已下载${progress}%`})
} else {
clearInterval(progressIntervalId);
@@ -113,26 +141,26 @@ export default function initIPC() {
decodeWxFile(save_path_file, data.decode_key, save_path_file.replace(".mp4", "_wx.mp4")).then((res) => {
fs.unlink(save_path_file, (err) => {
})
resolve(res);
resolve(res)
}).catch((error) => {
console.log("err:", error)
console.log("decodeWxFile err:", error)
resolve(false);
});
})
} else {
resolve({
fullFileName: save_path_file,
});
})
}
}
}).catch((error) => {
console.error(error);
clearInterval(progressIntervalId);
resolve(false);
console.log("aria2RpcClient err:", error)
clearInterval(progressIntervalId)
resolve(false)
});
}, 1000);
}, 1000)
}).catch((error) => {
console.log("err:", error)
resolve(false);
console.log("aria2RpcClient.addUri err:", error)
resolve(false)
});
});
});

View File

@@ -1,19 +1,14 @@
import fs from 'fs'
import log from 'electron-log'
import CONFIG from './const'
import {setProxy} from './setProxy'
import * as urlTool from "url"
import {toSize} from "./utils"
import {toSize, typeSuffix} from "./utils"
// @ts-ignore
import {hexMD5} from '../../src/common/md5'
import pkg from '../../package.json'
const hoXy = require('hoxy')
const port = 8899
global.videoList = {}
if (process.platform === 'win32') {
process.env.OPENSSL_BIN = CONFIG.OPEN_SSL_BIN_PATH
process.env.OPENSSL_CONF = CONFIG.OPEN_SSL_CNF_PATH
@@ -22,6 +17,7 @@ if (process.platform === 'win32') {
const resObject = {
url: "",
url_sign: "",
referer: "",
cover_url: "",
file_format: "",
platform: "",
@@ -34,207 +30,143 @@ const resObject = {
description: ""
}
const vv = hexMD5(pkg.version) + (CONFIG.IS_DEV ? Math.random() :"")
const vv = hexMD5(pkg.version) + (CONFIG.IS_DEV ? Math.random() : "")
export async function startServer({win, upstreamProxy, setProxyErrorCallback = f => f,}) {
return new Promise(async (resolve: any, reject) => {
try {
const proxy = hoXy.createServer({
upstreamProxy: upstreamProxy,
certAuthority: {
key: fs.readFileSync(CONFIG.CERT_PRIVATE_PATH),
cert: fs.readFileSync(CONFIG.CERT_PUBLIC_PATH),
},
export function startServer(win) {
try {
let upstreamProxy = ""
if (global.resdConfig.proxy && !global.resdConfig.proxy.includes(':' + global.resdConfig.port)) {
upstreamProxy = global.resdConfig?.proxy
}
const proxy = hoXy.createServer({
upstreamProxy: upstreamProxy,
certAuthority: {
key: fs.readFileSync(CONFIG.CERT_PRIVATE_PATH),
cert: fs.readFileSync(CONFIG.CERT_PUBLIC_PATH),
},
})
.listen(global.resdConfig.port, () => {
global.isStartProxy = true
})
.listen(port, () => {
setProxy('127.0.0.1', port)
.then((res) => {
resolve()
})
.catch((err) => {
setProxyErrorCallback(err)
reject('setting proxy err: ' + err.toString())
});
})
.on('error', err => {
setProxyErrorCallback(err)
reject('proxy service err: ' + err.toString())
})
.on('error', err => {
console.error("hoXy err:", err);
})
intercept(proxy, win)
} catch (e) {
console.error("--------------proxy catch err--------------", e);
}
}
function intercept(proxy, win) {
proxy.intercept(
{
phase: 'request',
hostname: 'res-downloader.666666.com',
as: 'json',
},
(req, res) => {
res.string = 'ok'
res.statusCode = 200
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,
referer: "",
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(
{
phase: 'request',
hostname: 'res-downloader.666666.com',
as: 'json',
},
(req, res) => {
res.string = 'ok'
res.statusCode = 200
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
proxy.intercept(
{
phase: 'response',
hostname: 'channels.weixin.qq.com',
as: 'string',
},
async (req, res) => {
if (req.url.includes('/web/pages/feed') || req.url.includes('/web/pages/home')) {
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(
{
phase: 'response',
},
async (req, res) => {
try {
// 拦截响应
const contentType = res?._data?.headers?.['content-type']
const [resType, suffix] = typeSuffix(contentType)
if (resType) {
const url_sign: string = hexMD5(req.fullUrl())
const res_url = req.fullUrl()
const urlInfo = urlTool.parse(res_url, true)
const contentLength = res?._data?.headers?.['content-length']
if (global.videoList.hasOwnProperty(url_sign) === false) {
global.videoList[url_sign] = res_url
let referer = req?._data?.headers?.['referer']
win.webContents.send('on_get_queue', Object.assign({}, resObject, {
url: res_url,
url_sign: url_sign,
url: media.url + media.urlToken,
cover_url: media.coverUrl,
file_format: media.spec.map((res)=> res.fileFormat).join('#'),
referer: referer ? referer : "",
platform: urlInfo.hostname,
size: toSize(media.fileSize),
type: "video/mp4",
type_str: 'video',
decode_key: media.decodeKey,
description: req.json.description,
size: toSize(contentLength ? contentLength : 0),
type: contentType,
type_str: resType,
}))
} catch (e) {
log.log(e.toString())
}
},
)
proxy.intercept(
{
phase: 'response',
hostname: 'channels.weixin.qq.com',
as: 'string',
},
async (req, res) => {
if (req.url.includes('/web/pages/feed') || req.url.includes('/web/pages/home')) {
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(
{
phase: 'response',
},
async (req, res) => {
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,
platform: urlInfo.hostname,
size: res?._data?.headers?.['content-length'] ? toSize(res?._data?.headers?.['content-length']) : 0,
type: ctype,
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)
}
})
} catch (e) {
log.log("--------------proxy response err--------------", e)
}
},
)
}

View File

@@ -38,7 +38,7 @@ export async function setProxy(host, port) {
} else if (process.platform === 'linux') {
dialog.showMessageBoxSync({
type: "info",
message: `请手动设置系统代理`,
message: `请手动设置系统代理 默认为: 127.0.0.1:8899`,
});
return new Promise((resolve, reject) => {})
} else {

View File

@@ -1,8 +1,9 @@
import fs from 'fs'
import {Transform } from 'stream'
import {Transform} from 'stream'
import {getDecryptionArray} from '../wxjs/decrypt.js'
const axios = require('axios')
function xorTransform(decryptionArray) {
let processedBytes = 0;
return new Transform({
@@ -32,7 +33,7 @@ function downloadFile(url, decodeKey, fullFileName, progressCallback) {
},
}
if (url.includes("douyin")){
if (url.includes("douyin")) {
config.headers['Referer'] = url
}
@@ -57,7 +58,7 @@ function downloadFile(url, decodeKey, fullFileName, progressCallback) {
});
}),
);
}else{
} else {
data.pipe(
fs.createWriteStream(fullFileName).on('finish', () => {
resolve({
@@ -97,8 +98,8 @@ function toSize(size: number) {
return size + 'b'
}
function suffix(type: string) {
switch (type) {
function typeSuffix(type: string) {
switch (type ? type.toLowerCase() : type) {
case "video/mp4":
case "video/webm":
case "video/ogg":
@@ -106,23 +107,25 @@ function suffix(type: string) {
case "video/mpeg":
case "video/quicktime":
case "video/x-ms-wmv":
case "video/x-flv":
case "video/3gpp":
case "video/x-matroska":
return ".mp4";
return ["video", ".mp4"];
case "audio/video":
case "video/x-flv":
return ["live", ".mp4"];
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/x-icon":
case "image/svg+xml":
case "image/vnd.adobe.photoshop":
return ".png";
return ["image", ".png"];
case "audio/mpeg":
case "audio/wav":
case "audio/aiff":
@@ -136,12 +139,25 @@ function suffix(type: string) {
case "audio/opus":
case "audio/webm":
case "audio/mp4":
return ".mp3";
case "audio/mp3":
case "audio/mp4;charset=UTF-8":
return ["audio", ".mp3"];
case "application/vnd.apple.mpegurl":
case "application/x-mpegURL":
return ".m3u8";
case "application/x-mpegurl":
return ["m3u8", ".m3u8"];
case "application/pdf":
return ["pdf", ".pdf"];
case "application/vnd.ms-powerpoint":
case "application/vnd.openxmlformats-officedocument.presentationml.presentation":
return ["ppt", ".ppt"];
case "application/vnd.ms-excel":
case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
return ["xls", ".xls"];
case "application/msword":
case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
return ["doc", ".doc"];
}
return ""
return ["", ""]
}
function getCurrentDateTimeFormatted() {
@@ -157,4 +173,4 @@ function getCurrentDateTimeFormatted() {
return `${year}${month}${day}${hours}${minutes}${seconds}`;
}
export {downloadFile, toSize, decodeWxFile, suffix, getCurrentDateTimeFormatted}
export {downloadFile, toSize, decodeWxFile, typeSuffix, getCurrentDateTimeFormatted}

0
electron/res/darwin/aria2/arm64/aria2c Normal file → Executable file
View File

0
electron/res/darwin/aria2/x64/aria2c Normal file → Executable file
View File

0
electron/res/win/aria2/aria2c.exe Normal file → Executable file
View File

View File

@@ -1,6 +1,6 @@
{
"name": "res-downloader",
"version": "2.1.1",
"version": "2.3.0",
"main": "dist-electron/main/index.js",
"description": "res-downloader(爱享素材下载器)支持视频号、小程序、抖音、快手、小红书、酷狗音乐、qq音乐、qq短视频等",
"homepage": "https://github.com/putyy/res-downloader",
@@ -32,8 +32,8 @@
"@vitejs/plugin-vue": "^4.3.3",
"cross-spawn": "^7.0.3",
"crypto-js": "^4.1.1",
"electron": "^26.0.0",
"electron-builder": "^24.6.3",
"electron": "^22.3.27",
"electron-builder": "^23.6.0",
"electron-is-dev": "^2.0.0",
"electron-log": "^4.4.8",
"element-plus": "^2.3.12",
@@ -58,8 +58,6 @@
},
"dependencies": {
"axios": "^1.5.0",
"electron-store": "^8.1.0",
"getmac": "^5.20.0",
"hoxy": "^3.3.1",
"tunnel-agent": "^0.6.0"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -1,8 +1,12 @@
<script setup lang="ts">
// @ts-ignore
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
</script>
<template>
<router-view></router-view>
<el-config-provider :locale="zhCn">
<router-view></router-view>
</el-config-provider>
</template>
<style>

View File

@@ -1,5 +0,0 @@
import request from './request'
export function getPackageJson() {
return request.get("https://github.com/putyy/res-downloader/raw/main/package.json")
}

View File

@@ -1,71 +0,0 @@
import axios from 'axios'
import {ElMessage} from 'element-plus'
import {hexMD5} from "./md5"
import localStorageCache from "./localStorage"
import _ from "lodash"
class RequestService {
private axios: any;
private requestList: any;
constructor() {
let that = this
that.requestList = []
that.axios = axios.create({
timeout: 60000, // 请求超时时间毫秒
})
// 请求拦截
that.axios.interceptors.request.use(
function (config: any) {
if (config.url.slice(0, 8) !== "https://") {
config.url = import.meta.env.VITE_APP_API + "/" + config.url
}
return config
},
function (error: any) {
return Promise.reject(error)
}
)
// 响应拦截
that.axios.interceptors.response.use(
function (response: any) {
return response
},
function (error: any) {
// console.log(error)
return Promise.reject(error)
}
)
}
get(url: string, data?: any) {
return this.axios.get(url, {params: data}).catch((err:any)=>{
console.log('get-err', err)
})
}
post(url: string, data: any, isHandle?: any) {
isHandle = isHandle || true
if (isHandle){
data = Object.keys(data).map(item => {
let value = data[item];
if (_.isArray(value) || _.isObject(value)) {
value = JSON.stringify(value)
}
return encodeURIComponent(item) + '=' + encodeURIComponent(value)
}).join('&');
}
return this.axios.post(url, data).catch((err:any)=>{
console.log('post-err', err)
})
}
axiosObj() {
return this.axios
}
}
const request = new RequestService()
export default request

View File

@@ -2,53 +2,17 @@
import {ipcRenderer} from 'electron'
import pkg from '../../../package.json'
const v = pkg.version
const jump = (scene: number)=>{
switch (scene) {
case 1:
ipcRenderer.invoke('invoke_open_default_browser', {
url: "https://s.gowas.cn/d/4089-quan-ping-tai-zi-yuan-xia-zai-ruan-jian"
})
break;
case 2:
ipcRenderer.invoke('invoke_open_default_browser', {
url: "https://s.gowas.cn"
})
break;
case 3:
ipcRenderer.invoke('invoke_open_default_browser', {
url: "https://i.gowas.cn"
})
break;
case 4:
ipcRenderer.invoke('invoke_open_default_browser', {
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"
})
break;
}
const jump = (url: string)=>{
ipcRenderer.invoke('invoke_open_default_browser', {
url: url
})
}
</script>
<template lang="pug">
div.line
a.item 当前版本: {{v}}
a.item 站长邮箱: gowas.work@gmail.com
a.item(@click="jump(1)") 获取更新
a.item(@click="jump(4)") 问题反馈
div.line
a.item 推荐:
a.item(@click="jump(5)") Ai助手(免费)
a.item(@click="jump(2)") 网盘资源
a.item(@click="jump(3)") 图片无损压缩
a.item(@click="jump(6)") 软件源码
a.item(@click="jump('https://s.gowas.cn/d/4089-quan-ping-tai-zi-yuan-xia-zai-ruan-jian')") 获取更新&问题反馈
a.item(@click="jump('https://github.com/putyy/res-downloader')") 软件源码
</template>
<style lang="less" scoped>

View File

@@ -16,8 +16,8 @@ el-container
keep-alive(v-if="route.meta.keepAlive")
component(:is="Component")
component(v-else :is="Component")
el-footer
Footer
el-footer
Footer
</template>
<style lang="less" scoped>
@@ -27,4 +27,9 @@ el-container
.el-main {
text-align: center;
}
.el-footer{
margin: unset !important;
padding: unset !important;
height: auto !important;
}
</style>

View File

@@ -1,14 +1,12 @@
<script setup lang="ts">
import {inject, onMounted, ref, watch} from 'vue'
import localStorageCache from "../../common/localStorage"
const appName = "爱享素材"
const sidebarCollapse = ref(inject('sidebarCollapse'))
const defaultActive = ref("/index")
onMounted(() => {
let lastRoute = localStorageCache.get('last-route')
defaultActive.value = lastRoute ? lastRoute : "/index"
defaultActive.value = "/index"
})
</script>
<template lang="pug">

View File

@@ -1,6 +1,4 @@
import {createMemoryHistory, createRouter} from 'vue-router'
// @ts-ignore
import localStorageCache from "./common/localStorage"
const routes = [
{

View File

@@ -30,6 +30,7 @@ const str = "使用方法\n" +
" 2. 软件首页选择要获取的资源类型(默认选中的视频)\n" +
" 3. 打开要捕获的源, 如:视频号、网页、小程序等等\n" +
" 4. 返回软件首页即可看到要下载的资源\n" +
" 5. 直播流复制的链接如何使用可以使用obs或者ffmpeg命令\n" +
"常见问题\n" +
" 1. 无法拦截获取\n" +
" 手动检测系统代理是否设置正确 本软件代理地址: 127.0.0.1:8899\n" +
@@ -56,8 +57,7 @@ div.about
el-button(@click="jump(3)") 获取更新
div 4. 问题反馈 &nbsp;
el-button(@click="jump(4)") 点击前往
div.more
pre {{str}}
div.more {{str}}
</template>
@@ -74,7 +74,9 @@ div.about
white-space: pre-wrap;
}
.more{
width: 100%; /* 设置容器宽度 */
white-space: pre-wrap;
overflow-wrap: break-word; /* 允许在单词边界内换行 */
}
}
</style>

View File

@@ -1,13 +1,15 @@
<script setup lang="ts">
import {ref, onMounted, onUnmounted, watch} from "vue"
import {ref, onMounted, onUnmounted, watch, computed} from "vue"
import {ipcRenderer} from 'electron'
import {ElMessage, ElLoading, ElTable} from "element-plus"
import localStorageCache from "../common/localStorage"
import {Delete, Promotion} from "@element-plus/icons-vue"
import {Delete, Filter, Promotion} from "@element-plus/icons-vue"
interface resData {
url: string,
url_sign: string,
referer: string,
cover_url: string,
size: any,
platform: string,
type: string,
@@ -18,26 +20,88 @@ interface resData {
description: string,
}
const tableData = ref<resData[]>([])
const resType = ref({
video: true,
audio: true,
image: false,
m3u8: false
const filtersAction = ref({
descInput: "",
descVisible: false,
descValue: "",
typeInput: [],
typeVisible: false,
typeValue: [],
})
const tableData = ref<resData[]>([])
const filteredData = computed(() => {
if (filtersAction.value.descValue && filtersAction.value.typeValue.length === 0) {
return tableData.value.filter((item: resData) => {
return item.description.includes(filtersAction.value.descValue)
});
}
if (!filtersAction.value.descValue && filtersAction.value.typeValue.length > 0) {
return tableData.value.filter((item: resData) => {
// @ts-ignore
return filtersAction.value.typeValue.includes(item.type_str)
});
}
if (filtersAction.value.descValue && filtersAction.value.typeValue.length > 0) {
return tableData.value.filter((item: resData) => {
// @ts-ignore
return item.description.includes(filtersAction.value.descValue) && filtersAction.value.typeValue.includes(item.type_str)
});
}
return tableData.value
});
const isInitApp = ref(false)
const isSetProxy = ref(false)
const multipleTableRef = ref<InstanceType<typeof ElTable>>()
const multipleSelection = ref<resData[]>([])
const loading = ref()
const resType = ref(["all"])
const typeOptions = ref([
{
value: "all",
label: "全部",
},
{
value: "image",
label: "图片",
}, {
value: "audio",
label: "音频"
}, {
value: "video",
label: "视频"
}, {
value: "m3u8",
label: "m3u8"
}, {
value: "live",
label: "直播流"
}, {
value: "xls",
label: "文档"
}, {
value: "doc",
label: "doc"
}, {
value: "pdf",
label: "pdf"
}
])
const typeFilters = ref(Array.from(typeOptions.value).slice(1))
const tableHeight = ref(400)
onMounted(() => {
let resTypeCache = localStorageCache.get("res-type")
let resTypeCache = localStorageCache.get("res-type-arr")
if (resTypeCache) {
resType.value = resTypeCache
resType.value = resTypeCache.split(",")
}
let tableDataCache = localStorageCache.get("res-table-data")
@@ -47,7 +111,7 @@ onMounted(() => {
ipcRenderer.on('on_get_queue', (res, data) => {
// @ts-ignore
if (resType.value.hasOwnProperty(data.type_str) && resType.value[data.type_str]) {
if (isSetProxy.value && resType.value.includes("all") || resType.value.includes(data.type_str)) {
tableData.value.push(data)
localStorageCache.set("res-table-data", tableData.value, -1)
}
@@ -64,21 +128,23 @@ onMounted(() => {
}
})
loading.value = ElLoading.service({
lock: true,
text: 'Loading',
background: 'rgba(0, 0, 0, 0.7)',
})
// loading.value = ElLoading.service({
// lock: true,
// text: 'Loading',
// background: 'rgba(0, 0, 0, 0.7)',
// })
ipcRenderer.invoke('invoke_start_proxy', {upstream_proxy: localStorageCache.get("upstream_proxy")}).then(() => {
loading.value.close()
}).catch((err) => {
ElMessage({
message: err,
type: 'warning',
})
loading.value.close()
})
// ipcRenderer.invoke('invoke_start_proxy', {upstream_proxy: localStorageCache.get("upstream_proxy")}).then(() => {
// loading.value.close()
// }).catch((err) => {
// ElMessage({
// message: err,
// type: 'warning',
// })
// loading.value.close()
// })
window.addEventListener("resize", handleResize);
handleResize()
})
onUnmounted(() => {
@@ -92,9 +158,14 @@ onUnmounted(() => {
})
watch(resType, (res, res1) => {
localStorageCache.set("res-type", resType.value, -1)
localStorageCache.set("res-type-arr", resType.value.join(","), -1)
}, {deep: true})
const handleResize = () => {
const height = document.documentElement.clientHeight || window.innerHeight;
tableHeight.value = height - 115
}
const handleSelectionChange = (val: resData[]) => {
multipleSelection.value = val
}
@@ -103,8 +174,9 @@ const handleBatchDown = async () => {
if (multipleSelection.value.length <= 0) {
return
}
const config = resdConfig()
let save_dir = localStorageCache.get("save_dir")
const save_dir = config?.save_dir
if (!save_dir) {
ElMessage({
@@ -119,7 +191,7 @@ const handleBatchDown = async () => {
text: '下载中',
background: 'rgba(0, 0, 0, 0.7)',
})
const quality = localStorageCache.get("quality") ? localStorageCache.get("quality") : -1
const quality = config?.quality ? config?.quality : -1
for (const item of multipleSelection.value) {
let downRes = await ipcRenderer.invoke('invoke_down_file', {
data: Object.assign({}, item),
@@ -136,9 +208,9 @@ const handleBatchDown = async () => {
multipleTableRef.value!.clearSelection()
}
const handleDown = async (index: number, row: any) => {
const save_dir = localStorageCache.get("save_dir")
const handleDown = (index: number, row: any) => {
const config = resdConfig()
const save_dir = config?.save_dir
if (!save_dir) {
ElMessage({
message: '请设置保存目录',
@@ -153,7 +225,7 @@ const handleDown = async (index: number, row: any) => {
background: 'rgba(0, 0, 0, 0.7)',
})
const quality = localStorageCache.get("quality") ? localStorageCache.get("quality") : -1
const quality = config?.quality ? config?.quality : -1
ipcRenderer.invoke('invoke_down_file', {
data: Object.assign({}, tableData.value[index]),
save_path: save_dir,
@@ -266,59 +338,127 @@ const handleInitApp = () => {
})
}
const setProxy = ()=>{
isSetProxy.value = !isSetProxy.value
ipcRenderer.invoke('invoke_set_proxy', {proxy: isSetProxy.value}).then((res) => {
if (!res) {
ElMessage({
type: "warning",
message: "设置系统代理失败",
})
}
}).catch((err) => {
ElMessage({
type: "warning",
message: err,
})
})
}
const handleFilter = (type: string)=>{
if (type === "desc") {
filtersAction.value.descValue = filtersAction.value.descInput
filtersAction.value.descVisible = false
return
}
filtersAction.value.typeValue = filtersAction.value.typeInput
filtersAction.value.typeVisible = false
}
const resdConfig = ()=>{
const cache = localStorageCache.get("resd_config")
if (cache) {
return JSON.parse(cache)
}
return null
}
</script>
<template lang="pug">
el-container.container
el-header
el-row
div
el-button(type="primary" @click="handleBatchDown") 批量下载
el-button(v-if="isInitApp" @click="handleInitApp")
el-icon
Promotion
p 安装检测(如果看到此按钮说明软件安装未完成则需要手动点击此按钮)
el-button(@click="handleClear")
el-icon
Delete
p 清空列表
el-button(@click="resType.video=!resType.video" :type="resType.video ? 'primary' : 'info'" ) 视频
el-button(@click="resType.audio=!resType.audio" :type="resType.audio ? 'primary' : 'info'" ) 音频
el-button(@click="resType.image=!resType.image" :type="resType.image ? 'primary' : 'info'" ) 图片
el-button(@click="resType.m3u8=!resType.m3u8" :type="resType.m3u8 ? 'primary' : 'info'" ) m3u8
a(style="color: red") &nbsp;&nbsp;&nbsp;点击左边选项选择需要拦截的资源类型
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="150px")
template(#default="scope")
div.show_res
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.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="操作" width="135px" )
template(#default="scope")
div.actions
template(v-if="scope.row.type_str !== 'm3u8'" )
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.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)") 打开文件目录
el-header(style="display:flex;align-items: center")
el-button(:type="isSetProxy ? 'primary' : 'info'" @click="setProxy") {{isSetProxy ? '关闭代理' : '启动代理'}}
el-button(type="primary" @click="handleBatchDown") 批量下载
el-button(v-if="isInitApp" @click="handleInitApp")
el-icon
Promotion
p 安装检测(如果看到此按钮说明软件安装未完成则需要手动点击此按钮)
el-button(@click="handleClear")
el-icon
Delete
p 清空列表
el-select(
v-model="resType"
multiple
collapse-tags
collapse-tags-tooltip
:max-collapse-tags="3"
placeholder="资源拦截类型"
style="width: auto;min-width:130px"
)
el-option(v-for="item in typeOptions"
:key="item.value"
:label="item.label"
:value="item.value")
el-table(ref="multipleTableRef" @selection-change="handleSelectionChange" :data="filteredData" :height="tableHeight" max-height="100%" stripe)
el-table-column(type="selection")
el-table-column(label="预览" show-overflow-tooltip width="150px")
template(#header)
div(style="display:flex;align-items: center")
span(:style="filtersAction.descValue ? 'color: #409eff' : ''") 信息
el-popover(:visible="filtersAction.descVisible")
div
el-input(v-model="filtersAction.descInput" placeholder="请输入")
div(style="margin-top:10px;display:flex;justify-content: center;")
el-button(size="small" @click="filtersAction.descVisible = false") 关闭
el-button(size="small" type="primary" @click="handleFilter('desc')") 筛选
template(#reference)
el-icon(@click="filtersAction.descVisible = true" :color="filtersAction.typeValue.length > 0 ? '#409eff' : ''")
Filter
template(#default="scope")
div.show_res
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.url" :type="scope.row.type")
div(v-if="scope.row.type_str !== 'video' && scope.row.type_str !== 'image' && scope.row.type_str !== 'audio'") {{scope.row.type_str}}类型无法预览
div {{scope.row.description}}
el-table-column(prop="type_str" label="类型" show-overflow-tooltip)
template(#header)
div(style="display:flex;align-items: center")
span(:style="filtersAction.typeValue.length > 0 ? 'color: #409eff' : ''") 类型
el-popover(:visible="filtersAction.typeVisible")
div
el-checkbox-group(v-model="filtersAction.typeInput" style="display:flex;flex-direction: column;")
el-checkbox(v-for="item in typeFilters" :label="item.label" :value="item.value")
div(style="margin-top:10px;display:flex;justify-content: center;")
el-button(size="small" @click="filtersAction.typeVisible = false") 关闭
el-button(size="small" type="primary" @click="handleFilter('type')") 筛选
template(#reference)
el-icon(@click="filtersAction.typeVisible = true" :color="filtersAction.typeValue.length > 0 ? '#409eff' : ''")
Filter
el-table-column(prop="platform" label="主机地址")
el-table-column(prop="size" label="资源大小")
el-table-column(prop="save_path" label="保存目录" width="135px" :show-overflow-tooltip="true")
el-table-column(prop="progress_bar" label="下载进度")
el-table-column(label="操作" width="135px" )
template(#default="scope")
div.actions
template(v-if="scope.row.type_str !== 'm3u8' && scope.row.type_str !== 'live'" )
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.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">
.container {
padding: 0.5rem;
.el-table{
padding-top: 1rem;
}
.el-button {
margin: 0.1rem;
}

View File

@@ -4,10 +4,15 @@ import {ipcRenderer} from "electron"
import localStorageCache from "../common/localStorage"
import {ElMessage} from "element-plus"
const saveDir = ref("")
const upstream_proxy = ref("")
const upstream_proxy_old = ref("")
const quality = ref("-1")
const formData = ref({
save_dir: "",
quality: "-1",
proxy: "",
port: "8899",
})
const proxy_old = ref("")
const port_old = ref("")
const qualityOptions = ref([
{
value: '-1',
@@ -25,27 +30,30 @@ const qualityOptions = ref([
])
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
const cache = localStorageCache.get("resd_config")
if (cache) {
formData.value = JSON.parse(cache)
}
proxy_old.value = formData.value.proxy
port_old.value = formData.value.port
})
const selectSaveDir = () => {
ipcRenderer.invoke('invoke_select_down_dir').then(save_path => {
if (save_path !== false) {
saveDir.value = save_path
ipcRenderer.invoke('invoke_select_down_dir').then(save_dir => {
console.log("save_dir", save_dir)
if (save_dir !== false) {
formData.value.save_dir = save_dir
}
})
}
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){
localStorageCache.set("resd_config", JSON.stringify(formData.value), -1)
ipcRenderer.invoke('invoke_set_config', Object.assign({}, formData.value))
if (proxy_old.value != formData.value.proxy || port_old.value != formData.value.port){
ipcRenderer.invoke('invoke_window_restart')
}
ElMessage({
message: "保存成功",
type: 'success',
@@ -55,16 +63,20 @@ const onSetting = () => {
</script>
<template lang="pug">
el-form(style="max-width: 600px")
el-form-item(label="代理端口")
el-input(v-model="formData.port" placeholder="默认: 8899" )
el-form-item(label="保存位置")
el-link(@click="selectSaveDir") {{saveDir ? saveDir : '选择'}}
div(style="display:flex;flex-direction: row;align-items: center;")
el-input(v-model="formData.save_dir" placeholder="请选择" disabled )
el-button(style="margin-left: 10px;" type="primary" @click="selectSaveDir") 选择
el-form-item(label="视频号画质")
el-select(v-model="quality" placeholder="请选择")
el-select(v-model="formData.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-input(v-model="formData.proxy" placeholder="例如: http://127.0.0.1:7890 修改此项需重启本软件,如不清楚用途请勿设置。" )
el-form-item
el-button(type="primary" @click="onSetting") 保存
</template>