14 Commits

Author SHA1 Message Date
putyy
04e4f0e9cc docs: Update readme 2025-05-07 14:23:13 +08:00
putyy
625cfbc474 perf: set page optimization 2025-05-07 11:05:59 +08:00
putyy
d8857bd4a2 perf: interception type optimization 2025-05-06 18:06:42 +08:00
putyy
a247c708f6 perf: unified response 2025-04-29 11:31:38 +08:00
putyy
85781a150a perf: optimization download、Proxy settings, add batch export 2025-04-29 11:15:13 +08:00
putyy
6086bd7086 feat: Added header cache, optional header settings... 2025-04-23 16:55:24 +08:00
putyy
0bb1a21a76 优化拦截标识 2025-04-22 15:59:59 +08:00
putyy
db21134550 优化下载、README等 2025-04-22 14:52:45 +08:00
putyy
3407490f82 Update README.md 2025-04-18 12:50:37 +08:00
putyy
dad06f6cd6 Merge pull request #158 from claviering/fix-NInput-disabled
input 组件disabled下内容看不清 #150
2025-03-13 16:11:58 +08:00
claviering
de70fc66b4 input 组件disabled下内容看不清 #150 2025-03-10 10:05:08 +08:00
putyy
2282f72b2f 更新readme 2025-03-06 22:05:53 +08:00
putyy
e00a7c9044 优化wx拦截 2025-02-13 09:24:00 +08:00
putyy
e79a7ba2fe 优化wx拦截 2025-02-13 09:23:39 +08:00
36 changed files with 1081 additions and 604 deletions

View File

@@ -1,44 +0,0 @@
name: Bug Report \ 问题反馈
description: Create a report to help us improve \ 帮助改进
labels: ["Bug"]
body:
- type: input
id: title
attributes:
label: Title \ 标题
description: A brief summary of the bug. \ 对于该错误的简要总结。
placeholder: Enter the bug title here. \ 在此输入错误标题。
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: To Reproduce \ 操作流程
description: Steps to reproduce the behaviour. \ 重现该行为的步骤。
placeholder: |
1. Go to '...' \ 1. 进入 '...'
2. Click on '....' \ 2. 点击 '....'
3. Scroll down to '....' \ 3. 向下滚动到 '....'
4. See error \ 4. 查看错误
validations:
required: true
- type: textarea
id: expected-behaviour
attributes:
label: Expected Behaviour \ 预期结果
description: A clear and concise description of what you expected to happen. \ 对您期望发生的事情的清晰简明描述。
placeholder: A clear and concise description of what you expected to happen. \ 对您期望发生的事情的清晰简明描述。
validations:
required: true
- type: textarea
id: software-version
attributes:
label: Software Version \ 软件版本
description: Please specify the version of the software you are using. \ 请指定您使用的软件版本。
placeholder: Enter the software version here. \ 在此输入软件版本。
validations:
required: true

View File

@@ -1,31 +0,0 @@
name: Feature Request \ 功能建议
description: Suggest an idea for this project \ 为这个项目提出一个新想法
labels: ["Enhancement"]
body:
- type: input
id: title
attributes:
label: Title \ 标题
description: A brief summary of your feature request. \ 对您功能建议的简要总结。
placeholder: Enter the feature title here. \ 在此输入功能标题。
validations:
required: true
- type: textarea
id: feature-suggestion
attributes:
label: Feature Suggestion \ 功能建议
description: A clear and concise description of the feature you would like to suggest. \ 对您想要建议的功能的清晰简明描述。
placeholder: Describe your feature suggestion here. \ 在此描述您的功能建议。
validations:
required: true
- type: textarea
id: proposed-solution
attributes:
label: Proposed Solution \ 你的方案
description: A clear and concise description of your proposed solution. \ 对您提议的解决方案的清晰简明描述。
placeholder: Describe your proposed solution here. \ 在此描述您的提议方案。
validations:
required: true

42
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,42 @@
name: "Bug Report \\ 问题反馈"
description: "Create a report to help us improve \\ 帮助改进"
labels: ["Bug"]
body:
- type: input
id: title
attributes:
label: "Title \\ 标题"
description: "A brief summary of the bug. \\ 对于该错误的简要总结。"
placeholder: "Enter the bug title here. \\ 在此输入错误标题。"
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: "To Reproduce \\ 操作流程"
description: "Steps to reproduce the behaviour. \\ 重现该行为的步骤。"
placeholder: |
1. Go to '...' \ 进入 '...'
2. Click on '....' \ 点击 '....'
validations:
required: true
- type: textarea
id: expected-behaviour
attributes:
label: "Expected Behaviour \\ 预期结果"
description: "A clear and concise description of what you expected to happen. \\ 对您期望发生的事情的清晰简明描述。"
placeholder: "A clear and concise description of what you expected to happen. \\ 对您期望发生的事情的清晰简明描述。"
validations:
required: true
- type: textarea
id: software-version
attributes:
label: "Software Version \\ 软件版本"
description: "Please specify the version of the software you are using. \\ 请指定您使用的软件版本。"
placeholder: "Enter the software version here. \\ 在此输入软件版本。"
validations:
required: true

View File

@@ -0,0 +1,31 @@
name: "Feature Request \\ 功能建议"
description: "Suggest an idea for this project \\ 为这个项目提出一个新想法"
labels: ["Enhancement"]
body:
- type: input
id: title
attributes:
label: "Title \\ 标题"
description: "A brief summary of your feature request. \\ 对您功能建议的简要总结。"
placeholder: "Enter the feature title here. \\ 在此输入功能标题。"
validations:
required: true
- type: textarea
id: feature-suggestion
attributes:
label: "Feature Suggestion \\ 功能建议"
description: "A clear and concise description of the feature you would like to suggest. \\ 对您想要建议的功能的清晰简明描述。"
placeholder: "Describe your feature suggestion here. \\ 在此描述您的功能建议。"
validations:
required: true
- type: textarea
id: proposed-solution
attributes:
label: "Proposed Solution \\ 你的方案"
description: "A clear and concise description of your proposed solution. \\ 对您提议的解决方案的清晰简明描述。"
placeholder: "Describe your proposed solution here. \\ 在此描述您的提议方案。"
validations:
required: true

104
README-EN.md Normal file
View File

@@ -0,0 +1,104 @@
<div align="center">
<a href="https://github.com/putyy/res-downloader"><img src="build/appicon.png" width="120"/></a>
<h1>res-downloader</h1>
<h4>📖 English | <a href="https://github.com/putyy/res-downloader/blob/master/README.md">中文</a></h4>
[![GitHub stars](https://img.shields.io/github/stars/putyy/res-downloader)](https://github.com/putyy/res-downloader/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/putyy/res-downloader)](https://github.com/putyy/res-downloader/fork)
[![GitHub release](https://img.shields.io/github/release/putyy/res-downloader)](https://github.com/putyy/res-downloader/releases)
![GitHub All Releases](https://img.shields.io/github/downloads/putyy/res-downloader/total)
[![License](https://img.shields.io/github/license/putyy/res-downloader)](https://github.com/putyy/res-downloader/blob/master/LICENSE)
</div>
---
### 🎉 Aixiang Resource Downloader
> A cross-platform resource downloader built with Go + [Wails](https://github.com/wailsapp/wails).
Clean UI, easy to use, and supports a wide range of resource sniffing and downloading.
## ✨ Features
- 🚀 **User-Friendly**: Simple operation with an intuitive and beautiful UI
- 🖥️ **Cross-Platform**: Available on Windows / macOS / Linux
- 🌐 **Supports Multiple Resource Types**: Video / Audio / Images / m3u8 / Live streams, and more
- 📱 **Wide Platform Compatibility**: Works with WeChat Channels, Mini Programs, Douyin, Kuaishou, Xiaohongshu, KuGou Music, QQ Music, and more
- 🌍 **Proxy Capture**: Built-in proxy allows fetching resources behind network restrictions
## 📚 Docs & Versions
- 📘 [Online Documentation (Chinese)](https://res.putyy.com/)
- 🧩 [Mini Version Ui Display using default browser](https://github.com/putyy/res-downloader) [Old Electron Version Support Win7](https://github.com/putyy/res-downloader/tree/old)
- 💬 [Join the User Group (Chinese)](https://www.putyy.com/app/admin/upload/img/20250418/6801d9554dc7.webp)
> *If full, you can add WeChat `AmorousWorld` with a note “From GitHub”*
## 🧩 Download Links
- 🆕 [Download from GitHub](https://github.com/putyy/res-downloader/releases)
- 🆕 [Download via Lanzou Cloud (Password: 9vs5)](https://wwjv.lanzoum.com/b04wgtfyb)
- ⚠️ *Windows 7 users: Please use version `2.3.0`*
## 🖼️ Preview
![Preview](docs/images/show.webp)
## 🚀 How to Use
> Follow these steps to use the software correctly:
1. During installation, be sure to **allow certificate installation** and **grant network access**
2. Launch the software → Click **"Start Proxy"** at the top left
3. Choose the resource types to capture (default is all)
4. Open the target content externally (WeChat, Mini App, Browser, etc.)
5. Return to the homepage to view the captured resource list
---
## ❓ FAQ
### 📺 m3u8 Video Resources
- Online Preview: [m3u8play](https://m3u8play.com/)
- Download Tool: [m3u8-down](https://m3u8-down.gowas.cn/)
### 📡 Live Stream Resources
- We recommend [OBS](https://obsproject.com/) for recording (search for setup tutorials)
### 🐢 Slow Downloads or Large File Failures?
- Recommended download managers:
- [Neat Download Manager](https://www.neatdownloadmanager.com/index.php/en/)
- [Motrix](https://motrix.app/download)
- For WeChat videos, click `Decrypt Video` after download
### 🧩 Unable to Intercept Resources?
- Check your system proxy settings:
Address: 127.0.0.1
Port: 8899
### 🌐 Can't Access Internet After Closing the App?
- Manually disable the system proxy settings
### 🧠 More Questions?
- [GitHub Issues](https://github.com/putyy/res-downloader/issues)
- [Aixiang Forum Thread (Chinese)](https://s.gowas.cn/d/4089)
## 💡 Principles & Motivation
This tool captures traffic via a local proxy and filters useful resources.
Its working principle is similar to tools like Fiddler, Charles, or browser DevTools, but with a more user-friendly display and enhanced filtering, making it suitable for everyday users with minimal tech background.
---
## ⚠️ Disclaimer
> This software is for educational and research purposes only.
Commercial or illegal use is strictly prohibited.
The author is not responsible for any consequences arising from misuse.

126
README.md
View File

@@ -1,51 +1,101 @@
## res-downloader
### 爱享素材下载器
<div align="center">
🎯 基于Go + [wails](https://github.com/wailsapp/wails)
📦 操作简单、可获取不同类型资源
🖥️ 支持Windows、Mac、Linux
🌐 支持视频、音频、图片、m3u8、直播流等常见网络资源
💪 支持微信视频号、小程序、抖音、快手、小红书、酷狗音乐、qq音乐等网络资源下载
👼 支持设置代理以获取特殊网络下的资源
<a href="https://github.com/putyy/res-downloader"><img src="build/appicon.png" width="120"/></a>
<h1>res-downloader</h1>
<h4>📖 中文 | <a href="https://github.com/putyy/res-downloader/blob/master/README-EN.md">English</a></h4>
## [在线文档](https://res.putyy.com/)、[加入群聊](https://qm.qq.com/q/ImE37ayJmc)、[Electron版](https://github.com/putyy/res-downloader/tree/old)
[![GitHub stars](https://img.shields.io/github/stars/putyy/res-downloader)](https://github.com/putyy/res-downloader/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/putyy/res-downloader)](https://github.com/putyy/res-downloader/fork)
[![GitHub release](https://img.shields.io/github/release/putyy/res-downloader)](https://github.com/putyy/res-downloader/releases)
![GitHub All Releases](https://img.shields.io/github/downloads/putyy/res-downloader/total)
[![License](https://img.shields.io/github/license/putyy/res-downloader)](https://github.com/putyy/res-downloader/blob/master/LICENSE)
## 软件下载(Win7下载2.3.0版本)
🆕 [github下载](https://github.com/putyy/res-downloader/releases)
🆕 [蓝奏云下载 密码:9vs5](https://wwjv.lanzoum.com/b04wgtfyb)
</div>
## 使用方法
> 0. 安装时一定要同意安装证书文件、一定要允许网络访问
> 1. 打开本软件 软件首页左上角点击 “启动代理”
> 2. 软件首页选择要获取的资源类型(默认选中的全部)
> 3. 打开要捕获的源, 如:视频号、网页、小程序等等
> 4. 返回软件首页即可看到资源列表
---
## 软件截图
![](docs/images/show.webp)
### 🎉 爱享素材下载器
## 常见问题
m3u8: 预览和下载:
> [下载](https://m3u8-down.gowas.cn/) [预览](https://m3u8play.com/)
> 一款基于 Go + [Wails](https://github.com/wailsapp/wails) 的跨平台资源下载工具,简洁易用,支持多种资源嗅探与下载。
直播流: 预览和录制:
> [使用obs进行预览和录制 使用教程自行百度]( https://obsproject.com/)
## ✨ 功能特色
下载慢、大视频下载失败
> 推荐使用如下工具加速下载,视频号可以下载完成后再到对应视频操作项选择 “视频解密(视频号)” 按钮
>> [Neat Download Manager](https://www.neatdownloadmanager.com/index.php/en/)、[Motrix](https://motrix.app/download)等软件进行下载
- 🚀 **简单易用**:操作简单,界面清晰美观
- 🖥️ **多平台支持**Windows / macOS / Linux
- 🌐 **多资源类型支持**:视频 / 音频 / 图片 / m3u8 / 直播流等
- 📱 **平台兼容广泛**支持微信视频号、小程序、抖音、快手、小红书、酷狗音乐、QQ音乐等
- 🌍 **代理抓包**:支持设置代理获取受限网络下的资源
打开本软件,无法正常拦截获取
> 检查系统代理是否正确设置 代理地址127.0.0.1 端口8899
## 📚 文档 & 版本
关闭软件后无法正常上网
> 手动关闭系统代理设置
- 📘 [在线文档](https://res.putyy.com/)
- 💬 [加入交流群](https://www.putyy.com/app/admin/upload/img/20250418/6801d9554dc7.webp)
- 🧩 [最新版](https://github.com/putyy/res-downloader/releases) [Mini版 使用默认浏览器展示UI](https://github.com/putyy/resd-mini) [Electron旧版 支持Win7](https://github.com/putyy/res-downloader/tree/old)
> *群满时可加微信 `AmorousWorld`,请备注“来源”*
其他问题
[github](https://github.com/putyy/res-downloader/issues) 、 [爱享论坛](https://s.gowas.cn/d/4089)
## 🧩 下载地址
## 实现 & 初衷
通过代理网络抓包拦截响应,筛选出有用的资源, 同fiddler、charles等抓包软件、浏览器F12打开控制也能达到目的只不过这些软件需要手动进行筛选对于小白用户上手还是有点难度本软件对部分资源做了特殊处理更适合大众用户所以就有了本项目。
- 🆕 [GitHub 下载](https://github.com/putyy/res-downloader/releases)
- 🆕 [蓝奏云下载密码9vs5](https://wwjv.lanzoum.com/b04wgtfyb)
- ⚠️ *Win7 用户请下载 `2.3.0` 版本*
## 免责声明
本软件用于学习研究使用,若因使用本软件造成的一切法律责任均与本人无关!
## 🖼️ 预览
![预览](docs/images/show.webp)
---
## 🚀 使用方法
> 请按以下步骤操作以正确使用软件:
1. 安装时务必 **允许安装证书文件****允许网络访问**
2. 打开软件 → 首页左上角点击 **“启动代理”**
3. 选择要获取的资源类型(默认全部)
4. 在外部打开资源页面(如视频号、小程序、网页等)
5. 返回软件首页,即可看到资源列表
## ❓ 常见问题
### 📺 m3u8 视频资源
- 在线预览:[m3u8play](https://m3u8play.com/)
- 视频下载:[m3u8-down](https://m3u8-down.gowas.cn/)
### 📡 直播流资源
- 推荐使用 [OBS](https://obsproject.com/) 进行录制(教程请百度)
### 🐢 下载慢、大文件失败?
- 推荐工具:
- [Neat Download Manager](https://www.neatdownloadmanager.com/index.php/en/)
- [Motrix](https://motrix.app/download)
- 视频号资源下载后可在操作项点击 `视频解密(视频号)`
### 🧩 软件无法拦截资源?
- 检查是否正确设置系统代理:
地址127.0.0.1
端口8899
### 🌐 关闭软件后无法上网?
- 手动关闭系统代理设置
### 🧠 更多问题
- [GitHub Issues](https://github.com/putyy/res-downloader/issues)
- [爱享论坛讨论帖](https://s.gowas.cn/d/4089)
## 💡 实现原理 & 初衷
本工具通过代理方式实现网络抓包,并筛选可用资源。与 Fiddler、Charles、浏览器 DevTools 原理类似,但对资源进行了更友好的筛选、展示和处理,大幅度降低了使用门槛,更适合大众用户使用。
---
## ⚠️ 免责声明
> 本软件仅供学习与研究用途,禁止用于任何商业或违法用途。
如因此产生的任何法律责任,概与作者无关!

View File

@@ -2,13 +2,13 @@
```bash
wails build -platform "darwin/universal"
create-dmg 'build/bin/res-downloader.app' --overwrite ./build/bin
mv -f "build/bin/res-downloader $(jq -r '.info.productVersion' wails.json).dmg" "build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json).dmg"
mv -f "build/bin/res-downloader $(jq -r '.info.productVersion' wails.json).dmg" "build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_mac.dmg"
```
## Windows
```bash
wails build -f -nsis -platform "windows/amd64" -webview2 Embed && mv -f "build/bin/res-downloader-amd64-installer.exe" "build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_win_x64.exe"
wails build -f -nsis -platform "windows/arm64" -webview2 Embed && mv -f "build/bin/res-downloader-arm64-installer.exe" "build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_win_arm.exe"
wails build -f -nsis -platform "windows/amd64" -webview2 Embed -skipbindings && mv -f "build/bin/res-downloader-amd64-installer.exe" "build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_win_amd64.exe"
wails build -f -nsis -platform "windows/arm64" -webview2 Embed -skipbindings && mv -f "build/bin/res-downloader-arm64-installer.exe" "build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_win_arm64.exe"
```
## Linux
@@ -20,12 +20,12 @@ docker build --network host -f build/linux/dockerfile -t res-downloader-amd-linu
docker run -it --name res-downloader-amd-build --network host --privileged -v ./:/www/res-downloader res-downloader-amd-linux /bin/bash
# 容器内
cd /www/res-downloader
wails build
wails build -platform "linux/amd64" -s -skipbindings
# 打包debian
cp build/bin/res-downloader build/linux/Debian/usr/local/bin/
echo "$(cat build/linux/Debian/DEBIAN/.control | sed -e "s/{{Version}}/$(jq -r '.info.productVersion' wails.json)/g")" > build/linux/Debian/DEBIAN/control
dpkg-deb --build ./build/linux/Debian build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_x64.deb
dpkg-deb --build ./build/linux/Debian build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_linux_amd64.deb
# 打包AppImage
cp build/bin/res-downloader build/linux/AppImage/usr/bin/
@@ -39,7 +39,9 @@ popd
wget -O ./build/bin/appimagetool-x86_64.AppImage https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage
chmod +x ./build/bin/appimagetool-x86_64.AppImage
./build/bin/appimagetool-x86_64.AppImage build/linux/AppImage build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_x64.AppImage
./build/bin/appimagetool-x86_64.AppImage build/linux/AppImage build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_linux_amd64.AppImage
mv -f build/bin/res-downloader build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_linux_amd64
```
> arm64
@@ -49,12 +51,14 @@ docker build --platform linux/arm64 --network host -f build/linux/dockerfile -t
docker run --platform linux/arm64 -it --name res-downloader-arm-build --network host --privileged -v ./:/www/res-downloader res-downloader-arm-linux /bin/bash
# 容器内
cd /www/res-downloader
wails build
wails build -platform "linux/arm64" -s -skipbindings
# 打包debian
cp build/bin/res-downloader build/linux/Debian/usr/local/bin/
echo "$(cat build/linux/Debian/DEBIAN/.control | sed -e "s/{{Version}}/$(jq -r '.info.productVersion' wails.json)/g")" > build/linux/Debian/DEBIAN/control
dpkg-deb --build ./build/linux/Debian build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_arm.deb
dpkg-deb --build ./build/linux/Debian build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_linux_arm64.deb
mv -f build/bin/res-downloader build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_linux_arm64
```
### Arch Linux

View File

@@ -1,4 +1,4 @@
FROM golang:1.23.4-bookworm
FROM golang:1.24.2-bookworm
WORKDIR /

View File

@@ -14,7 +14,7 @@
!define INFO_PRODUCTNAME "res-downloader"
!endif
!ifndef INFO_PRODUCTVERSION
!define INFO_PRODUCTVERSION "3.0.3"
!define INFO_PRODUCTVERSION "3.0.5"
!endif
!ifndef INFO_COPYRIGHT
!define INFO_COPYRIGHT "Copyright © 2023"

View File

@@ -133,7 +133,7 @@ func (a *App) Startup(ctx context.Context) {
if a.isInstall() {
return
}
err := os.MkdirAll(a.UserDir, os.ModePerm)
err := os.MkdirAll(a.UserDir, 0750)
if err != nil {
return
}
@@ -166,30 +166,28 @@ func (a *App) installCert() {
}
}
func (a *App) OpenSystemProxy() bool {
func (a *App) OpenSystemProxy() error {
if a.IsProxy {
return true
return nil
}
err := systemOnce.setProxy()
if err == nil {
a.IsProxy = true
return true
return nil
}
DialogErr("设置失败:" + err.Error())
return false
return err
}
func (a *App) UnsetSystemProxy() bool {
func (a *App) UnsetSystemProxy() error {
if !a.IsProxy {
return true
return nil
}
err := systemOnce.unsetProxy()
if err == nil {
a.IsProxy = false
return true
return nil
}
DialogErr("设置失败:" + err.Error())
return false
return err
}
func (a *App) isInstall() bool {
@@ -197,7 +195,7 @@ func (a *App) isInstall() bool {
}
func (a *App) lock() error {
err := os.WriteFile(a.LockFile, []byte("success"), 0777)
err := os.WriteFile(a.LockFile, []byte("success"), 0644)
if err != nil {
return err
}

View File

@@ -5,27 +5,39 @@ import (
"runtime"
"strconv"
"strings"
"sync"
)
type MimeInfo struct {
Type string `json:"Type"`
Suffix string `json:"Suffix"`
}
// Config struct
type Config struct {
storage *Storage
Theme string `json:"Theme"`
Host string `json:"Host"`
Port string `json:"Port"`
Quality int `json:"Quality"`
SaveDirectory string `json:"SaveDirectory"`
FilenameLen int `json:"FilenameLen"`
FilenameTime bool `json:"FilenameTime"`
UpstreamProxy string `json:"UpstreamProxy"`
OpenProxy bool `json:"OpenProxy"`
DownloadProxy bool `json:"DownloadProxy"`
AutoProxy bool `json:"AutoProxy"`
WxAction bool `json:"WxAction"`
TaskNumber int `json:"TaskNumber"`
UserAgent string `json:"UserAgent"`
Theme string `json:"Theme"`
Host string `json:"Host"`
Port string `json:"Port"`
Quality int `json:"Quality"`
SaveDirectory string `json:"SaveDirectory"`
FilenameLen int `json:"FilenameLen"`
FilenameTime bool `json:"FilenameTime"`
UpstreamProxy string `json:"UpstreamProxy"`
OpenProxy bool `json:"OpenProxy"`
DownloadProxy bool `json:"DownloadProxy"`
AutoProxy bool `json:"AutoProxy"`
WxAction bool `json:"WxAction"`
TaskNumber int `json:"TaskNumber"`
UserAgent string `json:"UserAgent"`
UseHeaders string `json:"UseHeaders"`
MimeMap map[string]MimeInfo `json:"MimeMap"`
}
var (
mimeMux sync.RWMutex
)
func initConfig() *Config {
if globalConfig == nil {
def := `
@@ -43,7 +55,67 @@ func initConfig() *Config {
"AutoProxy": true,
"WxAction": true,
"TaskNumber": __TaskNumber__,
"UserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36"
"UserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
"UseHeaders": "User-Agent,Referer,Authorization,Cookie",
"MimeMap": {
"image/png": { "Type": "image", "Suffix": ".png" },
"image/webp": { "Type": "image", "Suffix": ".webp" },
"image/jpeg": { "Type": "image", "Suffix": ".jpeg" },
"image/jpg": { "Type": "image", "Suffix": ".jpg" },
"image/gif": { "Type": "image", "Suffix": ".gif" },
"image/avif": { "Type": "image", "Suffix": ".avif" },
"image/bmp": { "Type": "image", "Suffix": ".bmp" },
"image/tiff": { "Type": "image", "Suffix": ".tiff" },
"image/heic": { "Type": "image", "Suffix": ".heic" },
"image/x-icon": { "Type": "image", "Suffix": ".ico" },
"image/svg+xml": { "Type": "image", "Suffix": ".svg" },
"image/vnd.adobe.photoshop": { "Type": "image", "Suffix": ".psd" },
"image/jp2": { "Type": "image", "Suffix": ".jp2" },
"image/jpeg2000": { "Type": "image", "Suffix": ".jp2" },
"image/apng": { "Type": "image", "Suffix": ".apng" },
"audio/mpeg": { "Type": "audio", "Suffix": ".mp3" },
"audio/mp3": { "Type": "audio", "Suffix": ".mp3" },
"audio/wav": { "Type": "audio", "Suffix": ".wav" },
"audio/aiff": { "Type": "audio", "Suffix": ".aiff" },
"audio/x-aiff": { "Type": "audio", "Suffix": ".aiff" },
"audio/aac": { "Type": "audio", "Suffix": ".aac" },
"audio/ogg": { "Type": "audio", "Suffix": ".ogg" },
"audio/flac": { "Type": "audio", "Suffix": ".flac" },
"audio/midi": { "Type": "audio", "Suffix": ".mid" },
"audio/x-midi": { "Type": "audio", "Suffix": ".mid" },
"audio/x-ms-wma": { "Type": "audio", "Suffix": ".wma" },
"audio/opus": { "Type": "audio", "Suffix": ".opus" },
"audio/webm": { "Type": "audio", "Suffix": ".webm" },
"audio/mp4": { "Type": "audio", "Suffix": ".m4a" },
"audio/amr": { "Type": "audio", "Suffix": ".amr" },
"video/mp4": { "Type": "video", "Suffix": ".mp4" },
"video/webm": { "Type": "video", "Suffix": ".webm" },
"video/ogg": { "Type": "video", "Suffix": ".ogv" },
"video/x-msvideo": { "Type": "video", "Suffix": ".avi" },
"video/mpeg": { "Type": "video", "Suffix": ".mpeg" },
"video/quicktime": { "Type": "video", "Suffix": ".mov" },
"video/x-ms-wmv": { "Type": "video", "Suffix": ".wmv" },
"video/3gpp": { "Type": "video", "Suffix": ".3gp" },
"video/x-matroska": { "Type": "video", "Suffix": ".mkv" },
"audio/video": { "Type": "live", "Suffix": ".flv" },
"video/x-flv": { "Type": "live", "Suffix": ".flv" },
"application/dash+xml": { "Type": "live", "Suffix": ".mpd" },
"application/vnd.apple.mpegurl": { "Type": "m3u8", "Suffix": ".m3u8" },
"application/x-mpegurl": { "Type": "m3u8", "Suffix": ".m3u8" },
"application/x-mpeg": { "Type": "m3u8", "Suffix": ".m3u8" },
"application/pdf": { "Type": "pdf", "Suffix": ".pdf" },
"application/vnd.ms-powerpoint": { "Type": "ppt", "Suffix": ".ppt" },
"application/vnd.openxmlformats-officedocument.presentationml.presentation": { "Type": "ppt", "Suffix": ".pptx" },
"application/vnd.ms-excel": { "Type": "xls", "Suffix": ".xls" },
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": { "Type": "xls", "Suffix": ".xlsx" },
"text/csv": { "Type": "xls", "Suffix": ".csv" },
"application/msword": { "Type": "doc", "Suffix": ".doc" },
"application/rtf": { "Type": "doc", "Suffix": ".rtf" },
"text/rtf": { "Type": "doc", "Suffix": ".rtf" },
"application/vnd.oasis.opendocument.text": { "Type": "doc", "Suffix": ".odt" },
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": { "Type": "doc", "Suffix": ".docx" },
"font/woff": { "Type": "font", "Suffix": ".woff" }
}
}
`
def = strings.ReplaceAll(def, "__TaskNumber__", strconv.Itoa(runtime.NumCPU()*2))
@@ -51,9 +123,23 @@ func initConfig() *Config {
storage: NewStorage("config.json", []byte(def)),
}
defaultMap := make(map[string]interface{})
_ = json.Unmarshal([]byte(def), &defaultMap)
data, err := globalConfig.storage.Load()
if err == nil {
_ = json.Unmarshal(data, &globalConfig)
var loadedMap map[string]interface{}
_ = json.Unmarshal(data, &loadedMap)
for key, val := range defaultMap {
if _, ok := loadedMap[key]; !ok {
loadedMap[key] = val
}
}
finalBytes, _ := json.Marshal(loadedMap)
_ = json.Unmarshal(finalBytes, &globalConfig)
} else {
globalLogger.Esg(err, "load config err")
}
@@ -77,9 +163,15 @@ func (c *Config) setConfig(config Config) {
c.AutoProxy = config.AutoProxy
c.TaskNumber = config.TaskNumber
c.WxAction = config.WxAction
c.UseHeaders = config.UseHeaders
if oldProxy != c.UpstreamProxy {
proxyOnce.setTransport()
}
mimeMux.Lock()
c.MimeMap = config.MimeMap
mimeMux.Unlock()
jsonData, err := json.Marshal(c)
if err == nil {
_ = globalConfig.storage.Store(jsonData)

View File

@@ -5,7 +5,6 @@ import (
"io"
"log"
"net/http"
"net/http/cookiejar"
"net/url"
"os"
"path/filepath"
@@ -13,7 +12,7 @@ import (
"sync"
)
type ProgressCallback func(totalDownloaded float64)
type ProgressCallback func(totalDownloaded float64, totalSize float64)
type DownloadTask struct {
taskID int
@@ -32,17 +31,19 @@ type FileDownloader struct {
totalTasks int
TotalSize int64
IsMultiPart bool
Headers map[string]string
DownloadTaskList []*DownloadTask
progressCallback ProgressCallback
}
func NewFileDownloader(url, filename string, totalTasks int) *FileDownloader {
func NewFileDownloader(url, filename string, totalTasks int, headers map[string]string) *FileDownloader {
return &FileDownloader{
Url: url,
FileName: filename,
totalTasks: totalTasks,
IsMultiPart: false,
TotalSize: 0,
Headers: headers,
DownloadTaskList: make([]*DownloadTask, 0),
}
}
@@ -52,11 +53,16 @@ func (fd *FileDownloader) buildClient() *http.Client {
if fd.ProxyUrl != nil {
transport.Proxy = http.ProxyURL(fd.ProxyUrl)
}
// Cookie handle
jar, _ := cookiejar.New(nil)
return &http.Client{
Transport: transport,
Jar: jar,
}
}
func (fd *FileDownloader) setHeaders(request *http.Request) {
for key, value := range fd.Headers {
if strings.Contains(globalConfig.UseHeaders, key) {
request.Header.Set(key, value)
}
}
}
@@ -76,38 +82,44 @@ func (fd *FileDownloader) init() error {
}
}
resp, e := http.Head(fd.Url)
if e != nil {
return e
request, err := http.NewRequest("HEAD", fd.Url, nil)
if err != nil {
return fmt.Errorf("create HEAD request failed: %w", err)
}
if _, ok := fd.Headers["User-Agent"]; !ok {
fd.Headers["User-Agent"] = globalConfig.UserAgent
}
if _, ok := fd.Headers["Referer"]; !ok {
fd.Headers["Referer"] = fd.Referer
}
fd.setHeaders(request)
resp, err := fd.buildClient().Do(request)
if err != nil {
return fmt.Errorf("HEAD request failed: %w", err)
}
defer resp.Body.Close()
fd.TotalSize = resp.ContentLength
if fd.TotalSize <= 0 {
return fmt.Errorf("invalid file")
return fmt.Errorf("invalid file size")
}
if resp.Header.Get("Accept-Ranges") == "bytes" && fd.TotalSize > 10485760 {
if resp.Header.Get("Accept-Ranges") == "bytes" && fd.TotalSize > 10*1024*1024 {
fd.IsMultiPart = true
}
resp.Body.Close()
fd.FileName = filepath.Clean(fd.FileName)
dir := filepath.Dir(fd.FileName)
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return err
}
fd.File, err = os.OpenFile(fd.FileName, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
return fmt.Errorf("文件初始化失败: %w", err)
return fmt.Errorf("file open failed: %w", err)
}
if err = fd.File.Truncate(fd.TotalSize); err != nil {
if err := fd.File.Truncate(fd.TotalSize); err != nil {
fd.File.Close()
return fmt.Errorf("文件大小设置失败: %w", err)
return fmt.Errorf("file truncate failed: %w", err)
}
return nil
}
@@ -118,96 +130,100 @@ func (fd *FileDownloader) createDownloadTasks() {
fd.totalTasks = int(fd.TotalSize)
}
eachSize := fd.TotalSize / int64(fd.totalTasks)
for i := 0; i < fd.totalTasks; i++ {
start := eachSize * int64(i)
end := eachSize*int64(i+1) - 1
if i == fd.totalTasks-1 {
end = fd.TotalSize - 1
}
fd.DownloadTaskList = append(fd.DownloadTaskList, &DownloadTask{
taskID: i,
rangeStart: eachSize * int64(i),
rangeEnd: eachSize*int64(i+1) - 1,
downloadedSize: 0,
isCompleted: false,
taskID: i,
rangeStart: start,
rangeEnd: end,
})
}
fd.DownloadTaskList[len(fd.DownloadTaskList)-1].rangeEnd = fd.TotalSize - 1
} else {
fd.DownloadTaskList = append(fd.DownloadTaskList, &DownloadTask{
taskID: 0,
rangeStart: 0,
rangeEnd: 0,
downloadedSize: 0,
isCompleted: false,
})
fd.DownloadTaskList = append(fd.DownloadTaskList, &DownloadTask{taskID: 0})
}
}
func (fd *FileDownloader) startDownload() {
waitGroup := &sync.WaitGroup{}
wg := &sync.WaitGroup{}
progressChan := make(chan int64)
for _, task := range fd.DownloadTaskList {
go fd.startDownloadTask(waitGroup, progressChan, task)
waitGroup.Add(1)
wg.Add(1)
go fd.startDownloadTask(wg, progressChan, task)
}
go func() {
waitGroup.Wait()
wg.Wait()
close(progressChan)
}()
if fd.progressCallback != nil {
totalDownloaded := int64(0)
for progress := range progressChan {
totalDownloaded += progress
fd.progressCallback(float64(totalDownloaded) * 100 / float64(fd.TotalSize))
for p := range progressChan {
totalDownloaded += p
fd.progressCallback(float64(totalDownloaded), float64(fd.TotalSize))
}
}
}
func (fd *FileDownloader) startDownloadTask(waitGroup *sync.WaitGroup, progressChan chan int64, task *DownloadTask) {
defer waitGroup.Done()
func (fd *FileDownloader) startDownloadTask(wg *sync.WaitGroup, progressChan chan int64, task *DownloadTask) {
defer wg.Done()
request, err := http.NewRequest("GET", fd.Url, nil)
if err != nil {
globalLogger.Error().Stack().Err(err).Msgf("任务%d创建请求出错", task.taskID)
return
}
request.Header.Set("User-Agent", globalConfig.UserAgent)
request.Header.Set("Referer", fd.Referer)
fd.setHeaders(request)
if fd.IsMultiPart {
request.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", task.rangeStart, task.rangeEnd))
rangeHeader := fmt.Sprintf("bytes=%d-%d", task.rangeStart, task.rangeEnd)
request.Header.Set("Range", rangeHeader)
}
resp, err := fd.buildClient().Do(request)
client := fd.buildClient()
resp, err := client.Do(request)
if err != nil {
log.Printf("任务%d发送下载请求出错%s", task.taskID, err)
return
}
defer resp.Body.Close()
buf := make([]byte, 8192)
for {
n, err := resp.Body.Read(buf)
if n > 0 {
_, err := fd.File.WriteAt(buf[:n], task.rangeStart+task.downloadedSize)
if err != nil {
log.Printf("任务%d写入文件时出现错误位置:%d, err: %s\n", task.taskID, task.rangeStart+task.downloadedSize, err)
remain := task.rangeEnd - (task.rangeStart + task.downloadedSize) + 1
n64 := int64(n)
if n64 > remain {
n = int(remain)
}
_, writeErr := fd.File.WriteAt(buf[:n], task.rangeStart+task.downloadedSize)
if writeErr != nil {
log.Printf("任务%d写入文件时出现错误位置:%d, err: %s\n", task.taskID, task.rangeStart+task.downloadedSize, writeErr)
return
}
downSize := int64(n)
task.downloadedSize += downSize
progressChan <- downSize
task.downloadedSize += n64
progressChan <- n64
if task.rangeStart+task.downloadedSize-1 >= task.rangeEnd {
task.isCompleted = true
break
}
}
if err != nil {
if err == io.EOF {
task.isCompleted = true
break
}
log.Printf("任务%d读取响应错误%s", task.taskID, err)
return
break
}
}
}
func (fd *FileDownloader) Start() error {
err := fd.init()
if err != nil {
if err := fd.init(); err != nil {
return err
}
fd.createDownloadTasks()

View File

@@ -10,11 +10,15 @@ import (
"net"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
sysRuntime "runtime"
"strings"
)
type respData map[string]interface{}
type ResponseData struct {
Code int `json:"code"`
Message string `json:"message"`
@@ -118,20 +122,54 @@ func (h *HttpServer) writeJson(w http.ResponseWriter, data ResponseData) {
}
}
func (h *HttpServer) error(w http.ResponseWriter, args ...interface{}) {
message := "ok"
var data interface{}
if len(args) > 0 {
message = args[0].(string)
}
if len(args) > 1 {
data = args[1]
}
h.writeJson(w, ResponseData{
Code: 0,
Message: message,
Data: data,
})
}
func (h *HttpServer) success(w http.ResponseWriter, args ...interface{}) {
message := "ok"
var data interface{}
if len(args) > 0 {
data = args[0]
}
if len(args) > 1 {
message = args[1].(string)
}
h.writeJson(w, ResponseData{
Code: 1,
Message: message,
Data: data,
})
}
func (h *HttpServer) openDirectoryDialog(w http.ResponseWriter, r *http.Request) {
folder, err := runtime.OpenDirectoryDialog(appOnce.ctx, runtime.OpenDialogOptions{
DefaultDirectory: "",
Title: "Select a folder",
})
if err != nil {
h.writeJson(w, ResponseData{Code: 0, Message: err.Error()})
h.error(w, err.Error())
return
}
h.writeJson(w, ResponseData{
Code: 1,
Data: map[string]interface{}{
"folder": folder,
},
h.success(w, respData{
"folder": folder,
})
}
@@ -146,14 +184,11 @@ func (h *HttpServer) openFileDialog(w http.ResponseWriter, r *http.Request) {
Title: "Select a file",
})
if err != nil {
h.writeJson(w, ResponseData{Code: 0, Message: err.Error()})
h.error(w, err.Error())
return
}
h.writeJson(w, ResponseData{
Code: 1,
Data: map[string]interface{}{
"file": filePath,
},
h.success(w, respData{
"file": filePath,
})
}
@@ -188,77 +223,87 @@ func (h *HttpServer) openFolder(w http.ResponseWriter, r *http.Request) {
cmd = exec.Command("pcmanfm", filePath)
if err := cmd.Start(); err != nil {
globalLogger.err(err)
h.writeJson(w, ResponseData{Code: 0, Message: err.Error()})
h.error(w, err.Error())
return
}
}
}
}
default:
h.writeJson(w, ResponseData{Code: 0, Message: "unsupported platform"})
h.error(w, "unsupported platform")
return
}
err = cmd.Start()
if err != nil {
globalLogger.err(err)
h.writeJson(w, ResponseData{Code: 0, Message: err.Error()})
h.error(w, err.Error())
return
}
h.writeJson(w, ResponseData{Code: 1})
h.success(w)
}
func (h *HttpServer) setSystemPassword(w http.ResponseWriter, r *http.Request) {
var data struct {
Password string `json:"password"`
}
err := json.NewDecoder(r.Body).Decode(&data)
if err != nil {
h.error(w, err.Error())
return
}
systemOnce.SetPassword(data.Password)
h.success(w)
}
func (h *HttpServer) openSystemProxy(w http.ResponseWriter, r *http.Request) {
appOnce.OpenSystemProxy()
h.writeJson(w, ResponseData{
Code: 1,
Data: map[string]bool{
err := appOnce.OpenSystemProxy()
if err != nil {
h.error(w, err.Error(), respData{
"isProxy": appOnce.IsProxy,
},
})
return
}
h.success(w, respData{
"isProxy": appOnce.IsProxy,
})
}
func (h *HttpServer) unsetSystemProxy(w http.ResponseWriter, r *http.Request) {
appOnce.UnsetSystemProxy()
h.writeJson(w, ResponseData{
Code: 1,
Data: map[string]bool{
err := appOnce.UnsetSystemProxy()
if err != nil {
h.error(w, err.Error(), respData{
"isProxy": appOnce.IsProxy,
},
})
return
}
h.success(w, respData{
"isProxy": appOnce.IsProxy,
})
}
func (h *HttpServer) isProxy(w http.ResponseWriter, r *http.Request) {
h.writeJson(w, ResponseData{
Code: 1,
Data: map[string]interface{}{
"isProxy": appOnce.IsProxy,
},
h.success(w, respData{
"isProxy": appOnce.IsProxy,
})
}
func (h *HttpServer) appInfo(w http.ResponseWriter, r *http.Request) {
h.writeJson(w, ResponseData{
Code: 1,
Data: appOnce,
})
h.success(w, appOnce)
}
func (h *HttpServer) getConfig(w http.ResponseWriter, r *http.Request) {
h.writeJson(w, ResponseData{
Code: 1,
Data: globalConfig,
})
h.success(w, globalConfig)
}
func (h *HttpServer) setConfig(w http.ResponseWriter, r *http.Request) {
var data Config
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
h.writeJson(w, ResponseData{Code: 0, Message: err.Error()})
h.error(w, err.Error())
return
}
globalConfig.setConfig(data)
h.writeJson(w, ResponseData{Code: 1})
h.success(w)
}
func (h *HttpServer) setType(w http.ResponseWriter, r *http.Request) {
@@ -274,12 +319,12 @@ func (h *HttpServer) setType(w http.ResponseWriter, r *http.Request) {
}
}
h.writeJson(w, ResponseData{Code: 1})
h.success(w)
}
func (h *HttpServer) clear(w http.ResponseWriter, r *http.Request) {
resourceOnce.clear()
h.writeJson(w, ResponseData{Code: 1})
h.success(w)
}
func (h *HttpServer) delete(w http.ResponseWriter, r *http.Request) {
@@ -290,7 +335,7 @@ func (h *HttpServer) delete(w http.ResponseWriter, r *http.Request) {
if err == nil && data.Sign != "" {
resourceOnce.delete(data.Sign)
}
h.writeJson(w, ResponseData{Code: 1})
h.success(w)
}
func (h *HttpServer) download(w http.ResponseWriter, r *http.Request) {
@@ -299,11 +344,11 @@ func (h *HttpServer) download(w http.ResponseWriter, r *http.Request) {
DecodeStr string `json:"decodeStr"`
}
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
h.writeJson(w, ResponseData{Code: 0, Message: err.Error()})
h.error(w, err.Error())
return
}
resourceOnce.download(data.MediaInfo, data.DecodeStr)
h.writeJson(w, ResponseData{Code: 1})
h.success(w)
}
func (h *HttpServer) wxFileDecode(w http.ResponseWriter, r *http.Request) {
@@ -313,18 +358,34 @@ func (h *HttpServer) wxFileDecode(w http.ResponseWriter, r *http.Request) {
DecodeStr string `json:"decodeStr"`
}
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
h.writeJson(w, ResponseData{Code: 0, Message: err.Error()})
h.error(w, err.Error())
return
}
savePath, err := resourceOnce.wxFileDecode(data.MediaInfo, data.Filename, data.DecodeStr)
if err != nil {
h.writeJson(w, ResponseData{Code: 0, Message: err.Error()})
h.error(w, err.Error())
return
}
h.writeJson(w, ResponseData{
Code: 1,
Data: map[string]string{
"save_path": savePath,
},
h.success(w, respData{
"save_path": savePath,
})
}
func (h *HttpServer) batchImport(w http.ResponseWriter, r *http.Request) {
var data struct {
Content string `json:"content"`
}
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
h.error(w, err.Error())
return
}
fileName := filepath.Join(globalConfig.SaveDirectory, "res-downloader-"+GetCurrentDateTimeFormatted()+".txt")
err := os.WriteFile(fileName, []byte(data.Content), 0644)
if err != nil {
h.error(w, err.Error())
return
}
h.success(w, respData{
"file_name": fileName,
})
}

View File

@@ -45,7 +45,7 @@ func NewLogger(logFile bool, logPath string) *Logger {
logfile *os.File
err error
)
logfile, err = os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
logfile, err = os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
panic(err)
}

View File

@@ -26,6 +26,8 @@ func HandleApi(w http.ResponseWriter, r *http.Request) bool {
switch r.URL.Path {
case "/api/preview":
httpServerOnce.preview(w, r)
case "/api/set-system-password":
httpServerOnce.setSystemPassword(w, r)
case "/api/proxy-open":
httpServerOnce.openSystemProxy(w, r)
case "/api/proxy-unset":
@@ -54,6 +56,8 @@ func HandleApi(w http.ResponseWriter, r *http.Request) bool {
httpServerOnce.download(w, r)
case "/api/wx-file-decode":
httpServerOnce.wxFileDecode(w, r)
case "/api/batch-import":
httpServerOnce.batchImport(w, r)
}
return true
}

View File

@@ -148,12 +148,12 @@ func (p *Proxy) handleWechatRequest(r *http.Request, ctx *goproxy.ProxyCtx) (*ht
if !ok {
return
}
resourceOnce.markMu.Lock()
defer resourceOnce.markMu.Unlock()
urlSign := Md5(rowUrl.(string))
if _, ok := resourceOnce.mark[urlSign]; ok {
if resourceOnce.mediaIsMarked(urlSign) {
return
}
id, err := gonanoid.New()
if err != nil {
id = urlSign
@@ -214,7 +214,7 @@ func (p *Proxy) handleWechatRequest(r *http.Request, ctx *goproxy.ProxyCtx) (*ht
res.OtherData["wx_file_formats"] = strings.Join(fileFormats, "#")
}
resourceOnce.mark[urlSign] = true
resourceOnce.markMedia(urlSign)
httpServerOnce.send("newResources", res)
}(body)
return r, p.buildEmptyResponse(r)
@@ -249,8 +249,10 @@ func (p *Proxy) httpResponseEvent(resp *http.Response, ctx *goproxy.ProxyCtx) *h
if strings.HasSuffix(host, "res.wx.qq.com") {
respTemp := resp
is := false
if strings.HasSuffix(respTemp.Request.URL.RequestURI(), ".js?v="+p.v()) {
respTemp = p.replaceWxJsContent(respTemp, ".js\"", ".js?v="+p.v()+"\"")
is = true
}
if strings.Contains(Path, "web/web-finder/res/js/virtual_svg-icons-register.publish") {
@@ -292,7 +294,9 @@ func (p *Proxy) httpResponseEvent(resp *http.Response, ctx *goproxy.ProxyCtx) *h
respTemp.Header.Set("Content-Length", fmt.Sprintf("%d", len(newBodyBytes)))
return respTemp
}
return respTemp
if is {
return respTemp
}
}
classify, suffix := TypeSuffix(resp.Header.Get("Content-Type"))
@@ -306,14 +310,11 @@ func (p *Proxy) httpResponseEvent(resp *http.Response, ctx *goproxy.ProxyCtx) *h
}
rawUrl := resp.Request.URL.String()
resourceOnce.markMu.Lock()
defer resourceOnce.markMu.Unlock()
isAll, _ := resourceOnce.getResType("all")
isClassify, _ := resourceOnce.getResType(classify)
urlSign := Md5(rawUrl)
if _, ok := resourceOnce.mark[urlSign]; !ok && (isAll || isClassify) {
if ok := resourceOnce.mediaIsMarked(urlSign); !ok && (isAll || isClassify) {
value, _ := strconv.ParseFloat(resp.Header.Get("content-length"), 64)
id, err := gonanoid.New()
if err != nil {
@@ -335,7 +336,13 @@ func (p *Proxy) httpResponseEvent(resp *http.Response, ctx *goproxy.ProxyCtx) *h
Description: "",
ContentType: resp.Header.Get("Content-Type"),
}
resourceOnce.mark[urlSign] = true
// Store entire request headers as JSON
if headers, err := json.Marshal(resp.Request.Header); err == nil {
res.OtherData["headers"] = string(headers)
}
resourceOnce.markMedia(urlSign)
httpServerOnce.send("newResources", res)
}
return resp

View File

@@ -2,6 +2,8 @@ package core
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/url"
"os"
@@ -26,16 +28,14 @@ type WxFileDecodeResult struct {
}
type Resource struct {
mark map[string]bool
markMu sync.RWMutex
resType map[string]bool
resTypeMu sync.RWMutex
mediaMark sync.Map
resType map[string]bool
resTypeMux sync.RWMutex
}
func initResource() *Resource {
if resourceOnce == nil {
resourceOnce = &Resource{
mark: make(map[string]bool),
resType: map[string]bool{
"all": true,
"image": true,
@@ -52,29 +52,25 @@ func initResource() *Resource {
return resourceOnce
}
func (r *Resource) getMark(key string) (bool, bool) {
r.markMu.RLock()
defer r.markMu.RUnlock()
value, ok := r.mark[key]
return value, ok
func (r *Resource) mediaIsMarked(key string) bool {
_, loaded := r.mediaMark.Load(key)
return loaded
}
func (r *Resource) setMark(key string, value bool) {
r.markMu.Lock()
defer r.markMu.Unlock()
r.mark[key] = value
func (r *Resource) markMedia(key string) {
r.mediaMark.Store(key, true)
}
func (r *Resource) getResType(key string) (bool, bool) {
r.resTypeMu.RLock()
defer r.resTypeMu.RUnlock()
r.resTypeMux.RLock()
defer r.resTypeMux.RUnlock()
value, ok := r.resType[key]
return value, ok
}
func (r *Resource) setResType(n []string) {
r.resTypeMu.Lock()
defer r.resTypeMu.Unlock()
r.resTypeMux.Lock()
defer r.resTypeMux.Unlock()
r.resType = map[string]bool{
"all": false,
"image": false,
@@ -93,15 +89,11 @@ func (r *Resource) setResType(n []string) {
}
func (r *Resource) clear() {
r.markMu.Lock()
defer r.markMu.Unlock()
r.mark = make(map[string]bool)
r.mediaMark.Clear()
}
func (r *Resource) delete(sign string) {
r.markMu.Lock()
defer r.markMu.Unlock()
delete(r.mark, sign)
r.mediaMark.Delete(sign)
}
func (r *Resource) download(mediaInfo MediaInfo, decodeStr string) {
@@ -152,9 +144,11 @@ func (r *Resource) download(mediaInfo MediaInfo, decodeStr string) {
}
}
downloader := NewFileDownloader(rawUrl, mediaInfo.SavePath, globalConfig.TaskNumber)
downloader.progressCallback = func(totalDownloaded float64) {
r.progressEventsEmit(mediaInfo, strconv.Itoa(int(totalDownloaded))+"%", DownloadStatusRunning)
headers, _ := r.parseHeaders(mediaInfo)
downloader := NewFileDownloader(rawUrl, mediaInfo.SavePath, globalConfig.TaskNumber, headers)
downloader.progressCallback = func(totalDownloaded, totalSize float64) {
r.progressEventsEmit(mediaInfo, strconv.Itoa(int(totalDownloaded*100/totalSize))+"%", DownloadStatusRunning)
}
err := downloader.Start()
if err != nil {
@@ -172,6 +166,27 @@ func (r *Resource) download(mediaInfo MediaInfo, decodeStr string) {
}(mediaInfo)
}
// 解析并组装 headers
func (r *Resource) parseHeaders(mediaInfo MediaInfo) (map[string]string, error) {
headers := make(map[string]string)
if hh, ok := mediaInfo.OtherData["headers"]; ok {
var tempHeaders map[string][]string
// 解析 JSON 字符串为 map[string][]string
if err := json.Unmarshal([]byte(hh), &tempHeaders); err != nil {
return headers, fmt.Errorf("parse headers JSON err: %v", err)
}
for key, values := range tempHeaders {
if len(values) > 0 {
headers[key] = values[0]
}
}
}
return headers, nil
}
func (r *Resource) wxFileDecode(mediaInfo MediaInfo, fileName, decodeStr string) (string, error) {
sourceFile, err := os.Open(fileName)
if err != nil {

View File

@@ -19,7 +19,7 @@ func NewStorage(filename string, def []byte) *Storage {
func (l *Storage) Load() ([]byte, error) {
if !FileExist(l.fileName) {
err := os.WriteFile(l.fileName, l.def, 0777)
err := os.WriteFile(l.fileName, l.def, 0644)
if err != nil {
return nil, err
}
@@ -33,7 +33,7 @@ func (l *Storage) Load() ([]byte, error) {
}
func (l *Storage) Store(data []byte) error {
if err := os.WriteFile(l.fileName, data, 0777); err != nil {
if err := os.WriteFile(l.fileName, data, 0644); err != nil {
return err
}
return nil

View File

@@ -7,6 +7,7 @@ import (
type SystemSetup struct {
CertFile string
Password string
}
func initSystem() *SystemSetup {
@@ -24,7 +25,7 @@ func (s *SystemSetup) initCert() ([]byte, error) {
return content, nil
}
if os.IsNotExist(err) {
err = os.WriteFile(s.CertFile, appOnce.PublicCrt, 0777)
err = os.WriteFile(s.CertFile, appOnce.PublicCrt, 0750)
if err != nil {
return nil, err
}
@@ -33,3 +34,7 @@ func (s *SystemSetup) initCert() ([]byte, error) {
return nil, err
}
}
func (s *SystemSetup) SetPassword(password string) {
s.Password = password
}

View File

@@ -9,15 +9,30 @@ import (
"strings"
)
func (s *SystemSetup) runCommand(args []string) ([]byte, error) {
if len(args) == 0 {
return nil, fmt.Errorf("no command provided")
}
var cmd *exec.Cmd
if s.Password != "" {
cmd = exec.Command("sudo", append([]string{"-S"}, args...)...)
cmd.Stdin = bytes.NewReader([]byte(s.Password + "\n"))
} else {
cmd = exec.Command(args[0], args[1:]...)
}
output, err := cmd.CombinedOutput()
return output, err
}
func (s *SystemSetup) getNetworkServices() ([]string, error) {
cmd := exec.Command("networksetup", "-listallnetworkservices")
output, err := cmd.Output()
output, err := s.runCommand([]string{"networksetup", "-listallnetworkservices"})
if err != nil {
return nil, fmt.Errorf("failed to execute command: %v", err)
}
services := strings.Split(string(output), "\n")
var activeServices []string
for _, service := range services {
service = strings.TrimSpace(service)
@@ -25,15 +40,12 @@ func (s *SystemSetup) getNetworkServices() ([]string, error) {
continue
}
// 检查服务是否活动
infoCmd := exec.Command("networksetup", "-getinfo", service)
infoOutput, err := infoCmd.Output()
infoOutput, err := s.runCommand([]string{"networksetup", "-getinfo", service})
if err != nil {
fmt.Printf("failed to get info for service %s: %v\n", service, err)
continue
}
// 如果输出中包含 "IP address:",说明服务是活动的
if strings.Contains(string(infoOutput), "IP address:") {
activeServices = append(activeServices, service)
}
@@ -53,16 +65,19 @@ func (s *SystemSetup) setProxy() error {
}
is := false
errs := ""
for _, serviceName := range services {
if err := exec.Command("networksetup", "-setwebproxy", serviceName, "127.0.0.1", globalConfig.Port).Run(); err != nil {
fmt.Println(err)
} else {
is = true
cmds := [][]string{
{"networksetup", "-setwebproxy", serviceName, "127.0.0.1", globalConfig.Port},
{"networksetup", "-setsecurewebproxy", serviceName, "127.0.0.1", globalConfig.Port},
}
if err := exec.Command("networksetup", "-setsecurewebproxy", serviceName, "127.0.0.1", globalConfig.Port).Run(); err != nil {
fmt.Println(err)
} else {
is = true
for _, args := range cmds {
if output, err := s.runCommand(args); err != nil {
errs = errs + "\n output" + string(output) + "err:" + err.Error()
fmt.Println("setProxy:", output, " err:", err.Error())
} else {
is = true
}
}
}
@@ -70,7 +85,7 @@ func (s *SystemSetup) setProxy() error {
return nil
}
return fmt.Errorf("failed to set proxy for any active network service")
return fmt.Errorf("failed to set proxy for any active network service, errs: %s", errs)
}
func (s *SystemSetup) unsetProxy() error {
@@ -80,16 +95,19 @@ func (s *SystemSetup) unsetProxy() error {
}
is := false
errs := ""
for _, serviceName := range services {
if err := exec.Command("networksetup", "-setwebproxystate", serviceName, "off").Run(); err != nil {
fmt.Println(err)
} else {
is = true
cmds := [][]string{
{"networksetup", "-setwebproxystate", serviceName, "off"},
{"networksetup", "-setsecurewebproxystate", serviceName, "off"},
}
if err := exec.Command("networksetup", "-setsecurewebproxystate", serviceName, "off").Run(); err != nil {
fmt.Println(err)
} else {
is = true
for _, args := range cmds {
if output, err := s.runCommand(args); err != nil {
errs = errs + "\n output" + string(output) + "err:" + err.Error()
fmt.Println("unsetProxy:", output, " err:", err.Error())
} else {
is = true
}
}
}
@@ -97,7 +115,7 @@ func (s *SystemSetup) unsetProxy() error {
return nil
}
return fmt.Errorf("failed to set proxy for any active network service")
return fmt.Errorf("failed to unset proxy for any active network service, errs: %s", errs)
}
func (s *SystemSetup) installCert() (string, error) {
@@ -112,10 +130,11 @@ func (s *SystemSetup) installCert() (string, error) {
return string(passwordOutput), err
}
password := bytes.TrimSpace(passwordOutput)
cmd := exec.Command("sudo", "-S", "security", "add-trusted-cert", "-d", "-r", "trustRoot", "-k", "/Library/Keychains/System.keychain", s.CertFile)
password := strings.TrimSpace(string(passwordOutput))
s.SetPassword(password)
cmd.Stdin = bytes.NewReader(append(password, '\n'))
cmd := exec.Command("sudo", "-S", "security", "add-trusted-cert", "-d", "-r", "trustRoot", "-k", "/Library/Keychains/System.keychain", s.CertFile)
cmd.Stdin = bytes.NewReader([]byte(password + "\n"))
output, err := cmd.CombinedOutput()
if err != nil {
return string(output), err

View File

@@ -40,69 +40,17 @@ func FileExist(file string) bool {
func CreateDirIfNotExist(dir string) error {
if _, err := os.Stat(dir); os.IsNotExist(err) {
return os.MkdirAll(dir, 0777)
return os.MkdirAll(dir, 0750)
}
return nil
}
func TypeSuffix(mime string) (string, string) {
switch strings.ToLower(mime) {
case "image/png",
"image/webp",
"image/jpeg",
"image/jpg",
"image/gif",
"image/avif",
"image/bmp",
"image/tiff",
"image/heic",
"image/x-icon",
"image/svg+xml",
"image/vnd.adobe.photoshop":
return "image", ".png"
case "audio/mpeg",
"audio/wav",
"audio/aiff",
"audio/x-aiff",
"audio/aac",
"audio/ogg",
"audio/flac",
"audio/midi",
"audio/x-midi",
"audio/x-ms-wma",
"audio/opus",
"audio/webm",
"audio/mp4",
"audio/mp3":
return "audio", ".mp3"
case "video/mp4",
"video/webm",
"video/ogg",
"video/x-msvideo",
"video/mpeg",
"video/quicktime",
"video/x-ms-wmv",
"video/3gpp",
"video/x-matroska":
return "video", ".mp4"
case "audio/video",
"video/x-flv":
return "live", ".mp4"
case "application/vnd.apple.mpegurl",
"application/x-mpegurl":
return "m3u8", ".m3u8"
case "application/pdf":
return "pdf", ".pdf"
case "application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.presentationml.presentation":
return "ppt", ".ppt"
case "application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
return "xls", ".xls"
case "application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
return "doc", ".doc"
mimeMux.RLock()
defer mimeMux.RUnlock()
mime = strings.ToLower(strings.Split(mime, ";")[0])
if v, ok := globalConfig.MimeMap[mime]; ok {
return v.Type, v.Suffix
}
return "", ""
}

View File

@@ -32,11 +32,13 @@ declare module 'vue' {
NModalProvider: typeof import('naive-ui')['NModalProvider']
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
NRadio: typeof import('naive-ui')['NRadio']
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect']
NSpace: typeof import('naive-ui')['NSpace']
NSwitch: typeof import('naive-ui')['NSwitch']
NTooltip: typeof import('naive-ui')['NTooltip']
Password: typeof import('./src/components/Password.vue')['default']
Preview: typeof import('./src/components/Preview.vue')['default']
ResAction: typeof import('./src/components/ResAction.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']

View File

@@ -1,6 +1,13 @@
import request from '@/api/request'
export default {
setSystemPassword(data: object) {
return request({
url: 'api/set-system-password',
method: 'post',
data: data
})
},
openSystemProxy() {
return request({
url: 'api/proxy-open',
@@ -93,4 +100,11 @@ export default {
data: data
})
},
batchImport(data: object) {
return request({
url: 'api/batch-import',
method: 'post',
data: data
})
},
}

View File

@@ -0,0 +1,46 @@
<template>
<n-modal
:show="showModal"
:on-update:show="changeShow"
style="--wails-draggable:no-drag"
preset="dialog"
title="管理员授权"
content=""
:show-icon="false"
:mask-closable="false"
class="rounded-lg"
>
<n-form>
<div class="text-red-500 text-base">
本操作需要管理员授权仅对本次运行期间有效
</div>
<n-form-item path="password" label="">
<n-input
v-model:value="password"
type="password"
placeholder="请输入你的电脑密码"
class="w-full"
/>
</n-form-item>
</n-form>
<template #action>
<div class="flex justify-end gap-4">
<n-button @click="emits('update:showModal', false)">取消操作</n-button>
<n-button type="primary" @click="emits('submit', password)">确认操作</n-button>
</div>
</template>
</n-modal>
</template>
<script setup lang="ts">
import {ref, computed} from 'vue'
import {NModal, NForm, NFormItem, NInput, NButton} from 'naive-ui'
const props = defineProps({
showModal: Boolean,
})
const password = ref("")
const emits = defineEmits(["update:showModal", "submit"])
const changeShow = (value: boolean) => emits("update:showModal", value)
</script>

View File

@@ -12,7 +12,7 @@
<NButton v-if="row.DecodeKey" type="warning" :tertiary="true" size="small" @click="action('decode')">
视频解密
</NButton>
<NButton v-if="isDebug" type="info" :tertiary="true" size="small" @click="action('json')">
<NButton type="info" :tertiary="true" size="small" @click="action('json')">
复制数据
</NButton>
<NButton type="error" :tertiary="true" size="small" @click="action('delete')">
@@ -22,8 +22,6 @@
</template>
<script setup lang="ts">
import {inject} from "vue"
const props = defineProps<{
row: any,
index: number,
@@ -31,8 +29,6 @@ const props = defineProps<{
const emits = defineEmits(["action"])
const isDebug = inject('isDebug')
const action = (type: string) => {
emits('action', props.row, props.index, type)
}

View File

@@ -2,7 +2,7 @@
<div class="flex pb-2 flex-col h-full min-w-[80px] border-r border-slate-100 dark:border-slate-900">
<Screen v-if="envInfo.platform!=='darwin'"></Screen>
<div class="w-full flex flex-row items-center justify-center pt-5 ml-[-5px]" :class="envInfo.platform==='darwin' ? 'pt-8' : 'pt-2'">
<img class="w-12 h-12 cursor-pointer" src="@/assets/image/logo.png" alt="res-downloader logo"/>
<img class="w-12 h-12 cursor-pointer" src="@/assets/image/logo.png" alt="res-downloader logo" @click="handleFooterUpdate('github')"/>
</div>
<main class="flex-1 flex-grow-1 mb-5 overflow-auto flex flex-col pt-1 items-center h-full">
<NScrollbar :size="1">
@@ -119,11 +119,11 @@ const footerOptions = ref([
},
])
const handleUpdateValue = (key: string, item: MenuOption) => {
const handleUpdateValue = (key: string, item?: MenuOption) => {
menuValue.value = key
return router.push({path: "/" + key})
}
const handleFooterUpdate = (key: string, item: MenuOption) => {
const handleFooterUpdate = (key: string, item?: MenuOption) => {
if (key === "about") {
showAppInfo.value = true
return
@@ -135,10 +135,10 @@ const handleFooterUpdate = (key: string, item: MenuOption) => {
}
if (key === "theme") {
if (globalConfig.value.Theme === "darkTheme") {
store.setConfig(Object.assign({}, globalConfig.value, {Theme: "lightTheme"}))
store.setConfig({Theme: "lightTheme"})
return
}
store.setConfig(Object.assign({}, globalConfig.value, {Theme: "darkTheme"}))
store.setConfig({Theme: "darkTheme"})
return
}

View File

@@ -27,6 +27,8 @@ export const useIndexStore = defineStore("index-store", () => {
WxAction: false,
TaskNumber: 8,
UserAgent: "",
UseHeaders: "",
MimeMap: {}
})
const envInfo = ref({
@@ -62,7 +64,7 @@ export const useIndexStore = defineStore("index-store", () => {
})
}
const setConfig = (formValue: appType.Config) => {
const setConfig = (formValue: Object) => {
globalConfig.value = Object.assign({}, globalConfig.value, formValue)
appApi.setConfig(globalConfig.value)
}

View File

@@ -6,6 +6,11 @@ export namespace appType {
Copyright: string
}
interface MimeMap {
Type: string
Suffix: string
}
interface Config {
Theme: string
Host: string
@@ -21,6 +26,8 @@ export namespace appType {
WxAction: boolean
TaskNumber: number
UserAgent: string
UseHeaders: string
MimeMap: {[key: string]: MimeMap}
}
interface MediaInfo {
@@ -56,4 +63,10 @@ export namespace appType {
type: string
event: any
}
interface Res<T = any> {
code: number;
message: string;
data: T; // T will be the specific type of your data
}
}

View File

@@ -1,13 +1,14 @@
<template>
<div class="flex flex-col px-5 py-5">
<div class="pb-2 z-40" @click="triggerEvent">
<div class="flex flex-col p-5">
<div class="pb-2 z-40">
<NSpace>
<NButton v-if="isProxy" secondary type="primary" @click.stop="close" style="--wails-draggable:no-drag">关闭代理</NButton>
<NButton v-else tertiary type="tertiary" @click.stop="open" style="--wails-draggable:no-drag">开启代理</NButton>
<NButton tertiary type="info" @click.stop="batchDown" style="--wails-draggable:no-drag">批量下载</NButton>
<NButton tertiary type="error" @click.stop="clear" style="--wails-draggable:no-drag">清空列表</NButton>
<NSelect style="min-width: 100px;--wails-draggable:no-drag" placeholder="拦截类型" v-model:value="resourcesType" multiple clearable :max-tag-count="3" :options="options"></NSelect>
<NButton v-if="isDebug" tertiary type="info" @click.stop="showImport=true" style="--wails-draggable:no-drag">导入数据</NButton>
<NSelect style="min-width: 100px;--wails-draggable:no-drag" placeholder="拦截类型" v-model:value="resourcesType" multiple clearable :max-tag-count="3" :options="classify"></NSelect>
<NButton tertiary type="info" @click.stop="batchDown" style="--wails-draggable:no-drag">批量下载</NButton>
<NButton tertiary type="info" @click.stop="batchImport" style="--wails-draggable:no-drag">批量导出</NButton>
<NButton tertiary type="info" @click.stop="showImport=true" style="--wails-draggable:no-drag">批量导入</NButton>
</NSpace>
</div>
<div class="flex-1">
@@ -27,12 +28,13 @@
<Preview v-model:showModal="showPreviewRow" :previewRow="previewRow"/>
<ShowLoading :loadingText="loadingText" :isLoading="loading"/>
<ImportJson v-model:showModal="showImport" @submit="handleImport"/>
<Password v-model:showModal="showPassword" @submit="handlePassword"/>
</div>
</template>
<script lang="ts" setup>
import {NButton, NImage, NTooltip} from "naive-ui"
import {computed, h, onMounted, ref, watch, provide} from "vue"
import {computed, h, onMounted, ref, watch} from "vue"
import type {appType} from "@/types/app"
import type {DataTableRowKey, ImageRenderToolbarProps} from "naive-ui"
@@ -47,6 +49,7 @@ import ResAction from "@/components/ResAction.vue"
import ImportJson from "@/components/ImportJson.vue"
import {useEventStore} from "@/stores/event"
import {BrowserOpenURL, ClipboardSetText} from "../../wailsjs/runtime"
import Password from "@/components/Password.vue"
const eventStore = useEventStore()
const isProxy = computed(() => {
@@ -58,37 +61,24 @@ const tableHeight = computed(() => {
return store.tableHeight - 132
})
const resourcesType = ref<string[]>(["all"])
const options = [
const classifyAlias: {[key: string]: string} = {
image: "图片",
audio: "音频",
video: "视频",
m3u8: "m3u8",
live: "直播流",
xls: "表格",
doc: "文档",
pdf: "pdf",
font: "字体"
}
const classify = 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: "文档"
}, {
value: "pdf",
label: "pdf"
}
]
])
const columns = ref<any[]>([
{
type: "selection",
@@ -100,15 +90,15 @@ const columns = ref<any[]>([
{
title: "类型",
key: "Classify",
filterOptions: Array.from(options).slice(1),
filterOptions: Array.from(classify.value).slice(1),
filterMultiple: true,
filter: (value: string, row: appType.MediaInfo) => {
return !!~row.Classify.indexOf(String(value))
},
render: (row: appType.MediaInfo) => {
for (const key in options) {
if (options[key].value === row.Classify) {
return options[key].label;
for (const key in classify.value) {
if (classify.value[key].value === row.Classify) {
return classify.value[key].label;
}
}
return row.Classify;
@@ -219,14 +209,12 @@ const showPreviewRow = ref(false)
const previewRow = ref<appType.MediaInfo>()
const loading = ref(false)
const loadingText = ref("")
const isDebug = ref(false)
const showImport = ref(false)
let clickCount = 0
let clickTimeout: any = null
provide('isDebug', isDebug);
const showPassword = ref(false)
onMounted(() => {
buildClassify()
const temp = localStorage.getItem("resources-type")
if (temp) {
resourcesType.value = JSON.parse(temp).res
@@ -281,11 +269,35 @@ onMounted(() => {
})
})
watch(()=>{
return store.globalConfig.MimeMap
}, ()=>{
buildClassify()
})
watch(resourcesType, (n, o) => {
localStorage.setItem("resources-type", JSON.stringify({res: resourcesType.value}))
appApi.setType(resourcesType.value)
})
const buildClassify = ()=>{
const mimeMap = store.globalConfig.MimeMap ?? {}
const seen = new Set()
classify.value = [
{value: "all", label: "全部"},
...Object.values(mimeMap)
.filter(({Type}) => {
if (seen.has(Type)) return false;
seen.add(Type);
return true;
})
.map(({Type}) => ({
value: Type,
label: classifyAlias[Type] ?? Type,
})),
]
}
const dataAction = (row: appType.MediaInfo, index: number, type: string) => {
switch (type) {
case "down":
@@ -361,6 +373,35 @@ const batchDown = async () => {
}
}
const batchImport = ()=>{
if (checkedRowKeysValue.value.length <= 0) {
window?.$message?.error('请选择需要导出的数据')
return
}
if (!store.globalConfig.SaveDirectory) {
window?.$message?.error("请设置保存目录")
return
}
loadingText.value = "导出中"
loading.value = true
let jsonData = []
for (let i = 0; i < data.value.length; i++) {
jsonData.push(encodeURIComponent(JSON.stringify(data.value[i])))
}
appApi.batchImport({content: jsonData.join("\n")}).then((res: appType.Res) => {
loading.value = false
if (res.code === 0) {
window?.$message?.error(res.message)
return
}
window?.$message?.success("导出成功")
window?.$message?.info("文件路径" + res.data?.file_name, {
duration: 5000
})
})
}
const uint8ArrayToBase64 = (bytes: any) => {
let binary = '';
const len = bytes.byteLength;
@@ -390,14 +431,14 @@ const download = (row: appType.MediaInfo, index: number) => {
loading.value = true
downIndex.value = index
if (row.DecodeKey) {
appApi.download({...row, decodeStr: uint8ArrayToBase64(getDecryptionArray(row.DecodeKey))}).then((res: any) => {
appApi.download({...row, decodeStr: uint8ArrayToBase64(getDecryptionArray(row.DecodeKey))}).then((res: appType.Res) => {
if (res.code === 0) {
loading.value = false
window?.$message?.error(res.message)
}
})
} else {
appApi.download({...row, decodeStr: ""}).then((res: any) => {
appApi.download({...row, decodeStr: ""}).then((res: appType.Res) => {
if (res.code === 0) {
loading.value = false
window?.$message?.error(res.message)
@@ -407,13 +448,25 @@ const download = (row: appType.MediaInfo, index: number) => {
}
const open = () => {
appApi.openSystemProxy().then((res: any) => {
appApi.openSystemProxy().then((res: appType.Res) => {
if (res.code === 0 ){
if (store.envInfo.platform === "darwin") {
showPassword.value = true
return
}
window?.$message?.error(res.message)
return
}
store.updateProxyStatus(res.data)
})
}
const close = () => {
appApi.unsetSystemProxy().then((res: any) => {
appApi.unsetSystemProxy().then((res: appType.Res) => {
if (res.code === 0 ){
window?.$message?.error(res.message)
return
}
store.updateProxyStatus(res.data)
})
}
@@ -429,7 +482,7 @@ const decodeWxFile = (row: appType.MediaInfo, index: number) => {
window?.$message?.error("无法解密")
return
}
appApi.openFileDialog().then((res: any) => {
appApi.openFileDialog().then((res: appType.Res) => {
if (res.code === 0) {
window?.$message?.error(res.message)
return
@@ -441,7 +494,7 @@ const decodeWxFile = (row: appType.MediaInfo, index: number) => {
...row,
filename: res.data.file,
decodeStr: uint8ArrayToBase64(getDecryptionArray(row.DecodeKey))
}).then((res: any) => {
}).then((res: appType.Res) => {
loading.value = false
if (res.code === 0) {
window?.$message?.error(res.message)
@@ -456,24 +509,6 @@ const decodeWxFile = (row: appType.MediaInfo, index: number) => {
})
}
const triggerEvent = ()=>{
if(isDebug.value) {
return
}
clickCount++
if (clickCount === 5) {
// 连续点击5次开启debug
isDebug.value = true
clickCount = 0
} else {
clearTimeout(clickTimeout);
clickTimeout = setTimeout(() => {
clickCount = 0
}, 1000)
}
}
const handleImport = (content: string)=>{
content.split("\n").forEach((line, index) => {
try {
@@ -491,4 +526,14 @@ const handleImport = (content: string)=>{
localStorage.setItem("resources-data", JSON.stringify(data.value))
showImport.value = false
}
const handlePassword = (password: string)=>{
appApi.setSystemPassword({password: password}).then((res: appType.Res)=>{
if (res.code === 0) {
window?.$message?.error(res.message)
return
}
open()
})
}
</script>

View File

@@ -1,5 +1,5 @@
<template>
<div class="h-full relative">
<div class="h-full relative p-5 overflow-y-auto [&::-webkit-scrollbar]:hidden">
<NForm
:model="formValue"
size="medium"
@@ -7,140 +7,175 @@
label-width="auto"
require-mark-placement="right-hanging"
style="--wails-draggable:no-drag"
class="px-5 py-5"
class="w-[700px]"
>
<NFormItem label="代理Host" path="Port" size="small">
<NInput v-model:value="formValue.Host" placeholder="127.0.0.1" style="width:256px"/>
<NFormItem label="Host" path="Host">
<NInput v-model:value="formValue.Host" placeholder="127.0.0.1"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="20" class="pl-1">
<HelpCircleOutline />
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
<span>如果不清楚保持默认就行修改后请重启软件</span>
如果不清楚保持默认修改后请重启软件
</NTooltip>
</NFormItem>
<NFormItem label="代理端口" path="Port" size="small">
<NInput v-model:value="formValue.Port" placeholder="8899" style="width:256px"/>
<NFormItem label="Port" path="Port">
<NInput v-model:value="formValue.Port" placeholder="8899"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="20" class="pl-1">
<HelpCircleOutline />
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
<span>如果不清楚保持默认就行修改后请重启软件</span>
如果不清楚保持默认修改后请重启软件
</NTooltip>
</NFormItem>
<NFormItem label="保存位置" path="SaveDirectory" size="small">
<NSpace>
<NInput v-model:value="formValue.SaveDirectory" disabled placeholder="保存位置" style="width:256px"/>
<NButton strong secondary type="success" @click="selectDir">选择</NButton>
</NSpace>
</NFormItem>
<NFormItem label="文件命名" path="FilenameLen" size="small">
<NInputNumber v-model:value="formValue.FilenameLen" :min="0" :max="9999" placeholder="0" style="width:256px"/>
<NSwitch class="pl-1" v-model:value="formValue.FilenameTime" aria-placeholder="随机数">
<template #checked>
</template>
<template #unchecked>
</template>
</NSwitch>
<NFormItem label="上游代理" path="UpstreamProxy">
<NInput v-model:value="formValue.UpstreamProxy" placeholder="例如: http://127.0.0.1:7890"/>
<NSwitch v-model:value="formValue.OpenProxy" class="ml-1"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="20" class="pl-1">
<HelpCircleOutline />
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
<span>输入框控制文件命名的长度(不含时间0为无效此选项有描述信息时有效)开关控制文件末尾是否添加时间标识</span>
可结合其他代理工具用于访问国外网站以及正常网络无法访问的资源
</NTooltip>
</NFormItem>
<NFormItem label="主题" path="theme" size="small">
<NRadio :checked="formValue.Theme === 'lightTheme'" value="lightTheme" name="theme" @change="handleChange">浅色主题</NRadio>
<NRadio :checked="formValue.Theme === 'darkTheme'" value="darkTheme" name="theme" @change="handleChange">深色主题</NRadio>
</NFormItem>
<NFormItem label="自动拦截" path="AutoProxy" size="small">
<NSwitch v-model:value="formValue.AutoProxy" />
<div class="grid grid-cols-3 gap-4">
<NFormItem label="下载代理" path="DownloadProxy">
<NSwitch v-model:value="formValue.DownloadProxy"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
进行下载时使用代理请求
</NTooltip>
</NFormItem>
<NFormItem label="自动拦截" path="AutoProxy">
<NSwitch v-model:value="formValue.AutoProxy"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
打开软件时自动启用拦截
</NTooltip>
</NFormItem>
<NFormItem label="全量拦截" path="WxAction">
<NSwitch v-model:value="formValue.WxAction"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
微信视频号是否全量拦截只拦截视频详情
</NTooltip>
</NFormItem>
</div>
<div class="grid grid-cols-2 gap-4">
<NFormItem label="保存位置" path="SaveDirectory">
<NInput :value="formValue.SaveDirectory" placeholder="保存位置"/>
<NButton strong secondary type="primary" @click="selectDir" class="ml-1">选择</NButton>
</NFormItem>
<NFormItem label="文件命名" path="FilenameLen">
<NInputNumber v-model:value="formValue.FilenameLen" :min="0" :max="9999" placeholder="0"/>
<NSwitch v-model:value="formValue.FilenameTime" class="ml-1"></NSwitch>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
输入框控制文件命名的长度(不含时间0为无效)开关控制文件末尾是否添加时间标识
</NTooltip>
</NFormItem>
</div>
<div class="grid grid-cols-3 gap-4">
<NFormItem label="主题" path="theme">
<NRadioGroup v-model:value="formValue.Theme" name="theme">
<NRadio value="lightTheme">浅色</NRadio>
<NRadio value="darkTheme">深色</NRadio>
</NRadioGroup>
</NFormItem>
<NFormItem label="清晰度" path="Quality">
<NSelect v-model:value="formValue.Quality" :options="options"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
视频号有效
</NTooltip>
</NFormItem>
<NFormItem label="连接数" path="TaskNumber">
<NInputNumber v-model:value="formValue.TaskNumber" :min="2" :max="64"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
如不清楚请保持默认通常CPU核心数*2用于分片下载
</NTooltip>
</NFormItem>
</div>
<NFormItem label="UserAgent" path="UserAgent">
<NInput v-model:value="formValue.UserAgent" placeholder="默认UserAgent"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="20" class="pl-1">
<HelpCircleOutline />
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
<span>打开软件时动启用拦截</span>
如不清楚请保持默认
</NTooltip>
</NFormItem>
<NFormItem label="清晰度" path="Quality" size="small">
<NSelect v-model:value="formValue.Quality" :options="options" class="w-64" />
<NFormItem label="Headers" path="Headers">
<NInput v-model:value="formValue.UseHeaders" placeholder="User-Agent,Referer,Authorization,Cookie"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="20" class="pl-1">
<HelpCircleOutline />
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
<span>视频号有效</span>
定义下载时可使用的header参数逗号分割
</NTooltip>
</NFormItem>
<NFormItem label="全量拦截" path="Quality" size="small">
<NSwitch v-model:value="formValue.WxAction" />
<NFormItem label="拦截规则" path="MimeMap">
<NInput
v-model:value="MimeMap"
type="textarea"
rows="11"
placeholder='{"content-type": { "Type": "分类名称","Suffix": "后缀"}}'
/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="20" class="pl-1">
<HelpCircleOutline />
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
<span>微信视频号是否全量拦截只拦截视频详情</span>
拦截规则JSON配置不清楚请勿改动
</NTooltip>
</NFormItem>
<NFormItem label="上游代理" path="UpstreamProxy" size="small">
<NInput v-model:value="formValue.UpstreamProxy" placeholder="例如: http://127.0.0.1:7890" style="width:256px"/>
<NSwitch class="pl-1" v-model:value="formValue.OpenProxy" />
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="20" class="pl-1">
<HelpCircleOutline />
</NIcon>
</template>
<span>可结合其他代理工具用于访问国外网站以及正常网络无法访问的资源(格式http://username:password@your.proxy.server:port)</span>
</NTooltip>
</NFormItem>
<NFormItem label="下载代理" path="DownloadProxy" size="small">
<NSwitch v-model:value="formValue.DownloadProxy" />
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="20" class="pl-1">
<HelpCircleOutline />
</NIcon>
</template>
<span>进行下载时使用代理请求</span>
</NTooltip>
</NFormItem>
<NFormItem label="连接数" path="TaskNumber" size="small">
<NInputNumber v-model:value="formValue.TaskNumber" :min="2" :max="64" class="w-64"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="20" class="pl-1">
<HelpCircleOutline />
</NIcon>
</template>
<span>如不清楚请保持默认通常CPU核心数*2用于分片下载</span>
</NTooltip>
</NFormItem>
<NFormItem label="UserAgent" path="UserAgent" size="small">
<NInput v-model:value="formValue.UserAgent" style="width:256px" placeholder=""/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="20" class="pl-1">
<HelpCircleOutline />
</NIcon>
</template>
<span>如不清楚请保持默认</span>
</NTooltip>
</NFormItem>
<NFormItem label=" " path="UserAgent" size="small">
<NButton strong secondary type="success" @click="save" class="w-20">保存</NButton>
</NFormItem>
</NForm>
</div>
</template>
@@ -175,15 +210,23 @@ const options = [
const formValue = ref<appType.Config>(Object.assign({}, store.globalConfig))
watch(()=>{
return store.globalConfig.Theme
}, ()=>{
formValue.value.Theme = store.globalConfig.Theme
const MimeMap = ref(formValue.value.MimeMap ? JSON.stringify(formValue.value.MimeMap, null, 2) : "")
watch(formValue.value, () => {
store.setConfig(formValue.value)
}, {deep: true})
watch(MimeMap, () => {
store.setConfig({
MimeMap: JSON.parse(MimeMap.value)
})
})
const handleChange = (e: Event)=>{
formValue.value.Theme = (e.target as HTMLInputElement).value
}
watch(() => {
return store.globalConfig.Theme
}, () => {
formValue.value.Theme = store.globalConfig.Theme
})
const selectDir = () => {
appApi.openDirectoryDialog().then((res: any) => {
@@ -194,9 +237,4 @@ const selectDir = () => {
window?.$message?.error(err)
});
}
const save = () => {
store.setConfig(formValue.value)
window?.$message?.success("保存成功")
}
</script>

View File

@@ -134,7 +134,7 @@ export function WindowIsFullscreen(): Promise<boolean>;
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
// Sets the width and height of the window.
export function WindowSetSize(width: number, height: number): Promise<Size>;
export function WindowSetSize(width: number, height: number): void;
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
// Gets the width and height of the window.

14
go.mod
View File

@@ -9,8 +9,8 @@ require (
github.com/matoous/go-nanoid/v2 v2.1.0
github.com/rs/zerolog v1.33.0
github.com/vrischmann/userdir v0.0.0-20151206171402-20f291cebd68
github.com/wailsapp/wails/v2 v2.9.2
golang.org/x/sys v0.28.0
github.com/wailsapp/wails/v2 v2.10.1
golang.org/x/sys v0.30.0
)
require (
@@ -30,13 +30,13 @@ require (
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/samber/lo v1.47.0 // indirect
github.com/samber/lo v1.49.1 // indirect
github.com/tkrajina/go-reflector v0.5.8 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/wailsapp/go-webview2 v1.0.18 // indirect
github.com/wailsapp/go-webview2 v1.0.19 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/text v0.22.0 // indirect
)

28
go.sum
View File

@@ -51,8 +51,8 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
@@ -63,17 +63,17 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/vrischmann/userdir v0.0.0-20151206171402-20f291cebd68 h1:Ah2/69Z24rwD6OByyOdpJDmttftz0FTF8Q4QZ/SF1E4=
github.com/vrischmann/userdir v0.0.0-20151206171402-20f291cebd68/go.mod h1:EqKqAeKddSL9XSGnfXd/7iLncccKhR16HBKVva7ENw8=
github.com/wailsapp/go-webview2 v1.0.18 h1:SSSCoLA+MYikSp1U0WmvELF/4c3x5kH8Vi31TKyZ4yk=
github.com/wailsapp/go-webview2 v1.0.18/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyTImA6NU=
github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v2 v2.9.2 h1:Xb5YRTos1w5N7DTMyYegWaGukCP2fIaX9WF21kPPF2k=
github.com/wailsapp/wails/v2 v2.9.2/go.mod h1:uehvlCwJSFcBq7rMCGfk4rxca67QQGsbg5Nm4m9UnBs=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
github.com/wailsapp/wails/v2 v2.10.1 h1:QWHvWMXII2nI/nXz77gpPG8P3ehl6zKe+u4su5BWIns=
github.com/wailsapp/wails/v2 v2.10.1/go.mod h1:zrebnFV6MQf9kx8HI4iAv63vsR5v67oS7GTEZ7Pz1TY=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -81,12 +81,12 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -61,7 +61,7 @@ func main() {
|_| \___| |___/ \__,_| \___/ \_/\_/ |_| |_| |_| \___/ \__ ,_| \__,_| \___| |_|`
log.Println(logo)
fmt.Println("version", app.Version)
fmt.Println("version:", app.Version)
fmt.Println("lockfile:", app.LockFile)
app.Startup(ctx)
},

View File

@@ -13,8 +13,8 @@
"info": {
"companyName": "res-downloader",
"productName": "res-downloader",
"productVersion": "3.0.3",
"productVersion": "3.0.6",
"copyright": "Copyright © 2023",
"comments": "This is a high-value, high-performance, and diverse resource downloader called res-downloader."
"comments": "This is a high-value high-performance and diverse resource downloader called res-downloader."
}
}