30 Commits
3.0.3 ... 3.0.6

Author SHA1 Message Date
putyy
da9039ea9c Merge pull request #187 from putyy/wails
perf: update build readme
2025-05-20 14:28:39 +08:00
putyy
dc46668f0d perf: update build readme 2025-05-20 14:24:32 +08:00
putyy
142a2a84b4 Merge pull request #186 from putyy/wails
merge
2025-05-20 13:55:10 +08:00
putyy
84b6a142fc perf: update doc, core optimize 2025-05-20 13:53:58 +08:00
putyy
a853f1d991 perf: Optimize proxy settings 2025-05-19 18:03:35 +08:00
putyy
dc261bb6ce perf: Simulate Mac operation icons 2025-05-16 16:41:17 +08:00
putyy
d0ea8e4fab Merge branch 'wails' of https://github.com/putyy/res-downloader into wails 2025-05-16 15:24:47 +08:00
putyy
c1cce920a4 perf: core optimize(add plugins) 2025-05-14 17:51:36 +08:00
putyy
f0495c6858 feat: add i18n(zh、en) 2025-05-12 17:44:48 +08:00
putyy
a37bde428d perf: set page optimization、API request method(adaptation linux)、Linux certificate installation optimization 2025-05-10 16:56:54 +08:00
putyy
7793f83ea3 perf: Install certificates and optimize proxy settings 2025-05-09 17:56:11 +08:00
putyy
29cc879b85 perf: set page optimization 2025-05-09 10:26:30 +08:00
putyy
abfdb76589 perf: set page optimization 2025-05-09 10:26:26 +08:00
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
3f07bae796 Merge pull request #176 from putyy/wails
feat: Added header cache, optional header settings...
2025-04-23 16:55:45 +08:00
putyy
6086bd7086 feat: Added header cache, optional header settings... 2025-04-23 16:55:24 +08:00
putyy
62bf0e2308 Merge pull request #175 from putyy/wails
优化拦截标识
2025-04-22 16:00:36 +08:00
putyy
0bb1a21a76 优化拦截标识 2025-04-22 15:59:59 +08:00
putyy
575e2d8904 Merge pull request #174 from putyy/wails
Wails
2025-04-22 14:54:11 +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
79 changed files with 2932 additions and 1791 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

1
.gitignore vendored
View File

@@ -1,5 +1,4 @@
.idea
test
build/bin
node_modules
frontend/dist

View File

@@ -20,7 +20,7 @@
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,

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 原理类似,但对资源进行了更友好的筛选、展示和处理,大幅度降低了使用门槛,更适合大众用户使用。
---
## ⚠️ 免责声明
> 本软件仅供学习与研究用途,禁止用于任何商业或违法用途。
如因此产生的任何法律责任,概与作者无关!

BIN
build/.DS_Store vendored

Binary file not shown.

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,26 +20,37 @@ 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/
# 复制WebKit相关文件
pushd build/linux/AppImage
find /usr/lib* -name WebKitNetworkProcess -exec mkdir -p $(dirname '{}') \; -exec cp --parents '{}' "." \; || true
find /usr/lib* -name WebKitWebProcess -exec mkdir -p $(dirname '{}') \; -exec cp --parents '{}' "." \; || true
find /usr/lib* -name libwebkit2gtkinjectedbundle.so -exec mkdir -p $(dirname '{}') \; -exec cp --parents '{}' "." \; || true
for f in WebKitNetworkProcess WebKitWebProcess libwebkit2gtkinjectedbundle.so; do
path=$(find /usr/lib* -name "$f" 2>/dev/null | head -n 1)
if [ -n "$path" ]; then
mkdir -p ./$(dirname "$path")
cp --parents "$path" .
else
echo "⚠️ $f not found, you may need to install libwebkit2gtk"
fi
done
popd
# 下载appimagetool
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 +60,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.6"
!endif
!ifndef INFO_COPYRIGHT
!define INFO_COPYRIGHT "Copyright © 2023"

72
core/aes.go Normal file
View File

@@ -0,0 +1,72 @@
package core
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"errors"
"io"
)
type AESCipher struct {
key []byte
}
func NewAESCipher(key string) *AESCipher {
return &AESCipher{key: []byte(key)}
}
func (a *AESCipher) Encrypt(plainText string) (string, error) {
block, err := aes.NewCipher(a.key)
if err != nil {
return "", err
}
padding := block.BlockSize() - len(plainText)%block.BlockSize()
padText := bytes.Repeat([]byte{byte(padding)}, padding)
plainText = plainText + string(padText)
cipherText := make([]byte, aes.BlockSize+len(plainText))
iv := cipherText[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return "", err
}
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(cipherText[aes.BlockSize:], []byte(plainText))
return base64.StdEncoding.EncodeToString(cipherText), nil
}
func (a *AESCipher) Decrypt(cipherText string) (string, error) {
cipherTextBytes, err := base64.StdEncoding.DecodeString(cipherText)
if err != nil {
return "", err
}
block, err := aes.NewCipher(a.key)
if err != nil {
return "", err
}
if len(cipherTextBytes) < aes.BlockSize {
return "", errors.New("ciphertext too short")
}
iv := cipherTextBytes[:aes.BlockSize]
cipherTextBytes = cipherTextBytes[aes.BlockSize:]
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(cipherTextBytes, cipherTextBytes)
padding := int(cipherTextBytes[len(cipherTextBytes)-1])
if padding > len(cipherTextBytes) || padding > aes.BlockSize {
return "", errors.New("padding size error")
}
plainText := cipherTextBytes[:len(cipherTextBytes)-padding]
return string(plainText), nil
}

View File

@@ -3,14 +3,13 @@ package core
import (
"context"
"embed"
"fmt"
"github.com/vrischmann/userdir"
"github.com/wailsapp/wails/v2/pkg/runtime"
"os"
"path/filepath"
"regexp"
sysRuntime "runtime"
"res-downloader/core/shared"
"strconv"
"strings"
"time"
)
@@ -25,7 +24,7 @@ type App struct {
LockFile string `json:"-"`
PublicCrt []byte `json:"-"`
PrivateKey []byte `json:"-"`
IsProxy bool `json:"-"`
IsProxy bool `json:"IsProxy"`
}
var (
@@ -52,8 +51,7 @@ func GetApp(assets embed.FS, wjs string) *App {
Version: version,
Description: "res-downloader是一款集网络资源嗅探 + 高速下载功能于一体的软件,高颜值、高性能和多样化,提供个人用户下载自己上传到各大平台的网络资源功能!",
Copyright: "Copyright © 2023~" + strconv.Itoa(time.Now().Year()),
PublicCrt: []byte(`
-----BEGIN CERTIFICATE-----
PublicCrt: []byte(`-----BEGIN CERTIFICATE-----
MIIDwzCCAqugAwIBAgIUFAnC6268dp/z1DR9E1UepiWgWzkwDQYJKoZIhvcNAQEL
BQAwcDELMAkGA1UEBhMCQ04xEjAQBgNVBAgMCUNob25ncWluZzESMBAGA1UEBwwJ
Q2hvbmdxaW5nMQ4wDAYDVQQKDAVnb3dhczEWMBQGA1UECwwNSVQgRGVwYXJ0bWVu
@@ -77,8 +75,7 @@ e3oowvgwikqm6XR6BEcRpPkztqcKST7jPFGHiXWsAqiibc+/plMW9qebhfMXEGhQ
D8HixYbEDg==
-----END CERTIFICATE-----
`),
PrivateKey: []byte(`
-----BEGIN PRIVATE KEY-----
PrivateKey: []byte(`-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDcDt23t6ioBoHG
/Y2mOjxntWQa9dP3eNl+mAC6425DlEtyc6czNAIKuuM9wt+wAwDQAgrd5RaxdcpJ
H1JlMkEtBFkIkdn0Ag98D7nwlVA9ON3xQi5Bkl+sN/oWOE8lOwvNyNNT6ZPu3qUS
@@ -109,6 +106,10 @@ ILKEQKmPPzKs7kp/7Nz+2cT3
`),
}
appOnce.UserDir = filepath.Join(userdir.GetConfigHome(), appOnce.AppName)
err := os.MkdirAll(appOnce.UserDir, 0750)
if err != nil {
fmt.Println("Mkdir UserDir err: ", err.Error())
}
appOnce.LockFile = filepath.Join(appOnce.UserDir, "install.lock")
initLogger()
initConfig()
@@ -123,22 +124,6 @@ ILKEQKmPPzKs7kp/7Nz+2cT3
func (a *App) Startup(ctx context.Context) {
a.ctx = ctx
go httpServerOnce.run()
time.AfterFunc(200*time.Millisecond, func() {
if globalConfig.AutoProxy {
appOnce.OpenSystemProxy()
}
})
go func() {
if a.isInstall() {
return
}
err := os.MkdirAll(a.UserDir, os.ModePerm)
if err != nil {
return
}
a.installCert()
}()
}
func (a *App) OnExit() {
@@ -146,58 +131,49 @@ func (a *App) OnExit() {
globalLogger.Close()
}
func (a *App) installCert() {
if res, err := systemOnce.installCert(); err != nil {
if sysRuntime.GOOS == "darwin" {
_ = runtime.ClipboardSetText(appOnce.ctx, `echo "输入本地登录密码" && sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "`+systemOnce.CertFile+`" && touch `+a.LockFile+` && echo "安装完成"`)
DialogErr("证书安装失败,请打开终端执行安装(命令已复制到剪切板),err:" + err.Error() + ", " + res)
} else if sysRuntime.GOOS == "windows" && strings.Contains(err.Error(), "Access is denied.") {
DialogErr("首次启用本软件,请使用鼠标右键选择以管理员身份运行")
} else if sysRuntime.GOOS == "linux" && strings.Contains(err.Error(), "Access is denied.") {
DialogErr("证书路径: " + systemOnce.CertFile + ", 请手动安装,安装完成后请执行: touch" + a.LockFile + " err:" + err.Error() + ", " + res)
} else {
globalLogger.Esg(err, res)
DialogErr("err:" + err.Error() + ", " + res)
}
func (a *App) installCert() (string, error) {
out, err := systemOnce.installCert()
if err != nil {
globalLogger.Esg(err, out)
return out, err
} else {
if err := a.lock(); err != nil {
globalLogger.err(err)
globalLogger.Err(err)
}
}
return out, nil
}
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 {
return FileExist(a.LockFile)
return shared.FileExist(a.LockFile)
}
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
}

16
core/bind.go Normal file
View File

@@ -0,0 +1,16 @@
package core
type Bind struct {
}
func NewBind() *Bind {
return &Bind{}
}
func (b *Bind) Config() *ResponseData {
return httpServerOnce.buildResp(1, "ok", globalConfig)
}
func (b *Bind) AppInfo() *ResponseData {
return httpServerOnce.buildResp(1, "ok", appOnce)
}

View File

@@ -5,27 +5,40 @@ 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"`
Locale string `json:"Locale"`
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 := `
@@ -33,6 +46,7 @@ func initConfig() *Config {
"Host": "127.0.0.1",
"Port": "8899",
"Theme": "lightTheme",
"Locale": "zh",
"Quality": 0,
"SaveDirectory": "",
"FilenameLen": 0,
@@ -40,10 +54,70 @@ func initConfig() *Config {
"UpstreamProxy": "",
"OpenProxy": false,
"DownloadProxy": false,
"AutoProxy": true,
"AutoProxy": false,
"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 +125,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")
}
@@ -66,6 +154,7 @@ func (c *Config) setConfig(config Config) {
c.Host = config.Host
c.Port = config.Port
c.Theme = config.Theme
c.Locale = config.Locale
c.Quality = config.Quality
c.SaveDirectory = config.SaveDirectory
c.FilenameLen = config.FilenameLen
@@ -77,11 +166,70 @@ 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)
}
}
func (c *Config) getConfig(key string) interface{} {
switch key {
case "Host":
return c.Host
case "Port":
return c.Port
case "Theme":
return c.Theme
case "Locale":
return c.Locale
case "Quality":
return c.Quality
case "SaveDirectory":
return c.SaveDirectory
case "FilenameLen":
return c.FilenameLen
case "FilenameTime":
return c.FilenameTime
case "UpstreamProxy":
return c.UpstreamProxy
case "UserAgent":
return c.UserAgent
case "OpenProxy":
return c.OpenProxy
case "DownloadProxy":
return c.DownloadProxy
case "AutoProxy":
return c.AutoProxy
case "TaskNumber":
return c.TaskNumber
case "WxAction":
return c.WxAction
case "UseHeaders":
return c.UseHeaders
case "MimeMap":
mimeMux.RLock()
defer mimeMux.RUnlock()
return c.MimeMap
default:
return nil
}
}
func (c *Config) typeSuffix(mime string) (string, string) {
mimeMux.RLock()
defer mimeMux.RUnlock()
mime = strings.ToLower(strings.Split(mime, ";")[0])
if v, ok := c.MimeMap[mime]; ok {
return v.Type, v.Suffix
}
return "", ""
}

View File

@@ -1,19 +1,30 @@
package core
import (
"errors"
"fmt"
"io"
"log"
"net/http"
"net/http/cookiejar"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
"time"
)
type ProgressCallback func(totalDownloaded float64)
const (
MaxRetries = 3 // 最大重试次数
RetryDelay = 3 * time.Second // 重试延迟
MinPartSize = 1 * 1024 * 1024 // 最小分片大小1MB
)
type ProgressCallback func(totalDownloaded float64, totalSize float64, taskID int, taskProgress float64)
type ProgressChan struct {
taskID int
bytes int64
}
type DownloadTask struct {
taskID int
@@ -21,6 +32,7 @@ type DownloadTask struct {
rangeEnd int64
downloadedSize int64
isCompleted bool
err error
}
type FileDownloader struct {
@@ -32,38 +44,49 @@ 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),
}
}
func (fd *FileDownloader) buildClient() *http.Client {
transport := &http.Transport{}
transport := &http.Transport{
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
}
if fd.ProxyUrl != nil {
transport.Proxy = http.ProxyURL(fd.ProxyUrl)
}
// Cookie handle
jar, _ := cookiejar.New(nil)
return &http.Client{
Transport: transport,
Jar: jar,
Timeout: 60 * time.Second,
}
}
func (fd *FileDownloader) setHeaders(request *http.Request) {
for key, value := range fd.Headers {
if strings.Contains(globalConfig.UseHeaders, key) {
request.Header.Set(key, value)
}
}
}
func (fd *FileDownloader) init() error {
parsedURL, err := url.Parse(fd.Url)
if err != nil {
return err
return fmt.Errorf("parse URL failed: %w", err)
}
if parsedURL.Scheme != "" && parsedURL.Host != "" {
fd.Referer = parsedURL.Scheme + "://" + parsedURL.Host + "/"
@@ -76,142 +99,268 @@ 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)
var resp *http.Response
for retries := 0; retries < MaxRetries; retries++ {
resp, err = fd.buildClient().Do(request)
if err == nil {
break
}
if retries < MaxRetries-1 {
time.Sleep(RetryDelay)
globalLogger.Warn().Msgf("HEAD request failed, retrying (%d/%d): %v", retries+1, MaxRetries, err)
}
}
if err != nil {
return fmt.Errorf("HEAD request failed after %d retries: %w", MaxRetries, err)
}
defer resp.Body.Close()
fd.TotalSize = resp.ContentLength
if fd.TotalSize <= 0 {
return fmt.Errorf("invalid file")
return errors.New("invalid file size")
}
if resp.Header.Get("Accept-Ranges") == "bytes" && fd.TotalSize > 10485760 {
if resp.Header.Get("Accept-Ranges") == "bytes" && fd.TotalSize > MinPartSize {
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
return fmt.Errorf("create directory failed: %w", 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
}
func (fd *FileDownloader) createDownloadTasks() {
if fd.IsMultiPart {
if int64(fd.totalTasks) > fd.TotalSize {
fd.totalTasks = int(fd.TotalSize)
if fd.totalTasks <= 0 {
fd.totalTasks = 4
}
eachSize := fd.TotalSize / int64(fd.totalTasks)
if eachSize < MinPartSize {
fd.totalTasks = int(fd.TotalSize / MinPartSize)
if fd.totalTasks < 1 {
fd.totalTasks = 1
}
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.totalTasks = 1
fd.DownloadTaskList = append(fd.DownloadTaskList, &DownloadTask{
taskID: 0,
rangeStart: 0,
rangeEnd: 0,
downloadedSize: 0,
isCompleted: false,
taskID: 0,
rangeStart: 0,
rangeEnd: fd.TotalSize - 1,
})
}
}
func (fd *FileDownloader) startDownload() {
waitGroup := &sync.WaitGroup{}
progressChan := make(chan int64)
func (fd *FileDownloader) startDownload() error {
wg := &sync.WaitGroup{}
progressChan := make(chan ProgressChan, len(fd.DownloadTaskList))
errorChan := make(chan error, len(fd.DownloadTaskList))
for _, task := range fd.DownloadTaskList {
go fd.startDownloadTask(waitGroup, progressChan, task)
waitGroup.Add(1)
wg.Add(1)
go fd.startDownloadTask(wg, progressChan, errorChan, task)
}
go func() {
waitGroup.Wait()
close(progressChan)
taskProgress := make([]int64, len(fd.DownloadTaskList))
totalDownloaded := int64(0)
for progress := range progressChan {
taskProgress[progress.taskID] += progress.bytes
totalDownloaded += progress.bytes
if fd.progressCallback != nil {
taskPercentage := float64(0)
if task := fd.DownloadTaskList[progress.taskID]; task != nil {
taskSize := task.rangeEnd - task.rangeStart + 1
if taskSize > 0 {
taskPercentage = float64(taskProgress[progress.taskID]) / float64(taskSize) * 100
}
}
fd.progressCallback(float64(totalDownloaded), float64(fd.TotalSize), progress.taskID, taskPercentage)
}
}
}()
if fd.progressCallback != nil {
totalDownloaded := int64(0)
for progress := range progressChan {
totalDownloaded += progress
fd.progressCallback(float64(totalDownloaded) * 100 / float64(fd.TotalSize))
}
go func() {
wg.Wait()
close(progressChan)
close(errorChan)
}()
var errArr []error
for err := range errorChan {
errArr = append(errArr, err)
}
if len(errArr) > 0 {
return fmt.Errorf("download failed with %d errors: %v", len(errArr), errArr[0])
}
if err := fd.verifyDownload(); err != nil {
return err
}
return nil
}
func (fd *FileDownloader) startDownloadTask(waitGroup *sync.WaitGroup, progressChan chan int64, task *DownloadTask) {
defer waitGroup.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)
if fd.IsMultiPart {
request.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", task.rangeStart, task.rangeEnd))
func (fd *FileDownloader) startDownloadTask(wg *sync.WaitGroup, progressChan chan ProgressChan, errorChan chan error, task *DownloadTask) {
defer wg.Done()
for retries := 0; retries < MaxRetries; retries++ {
err := fd.doDownloadTask(progressChan, task)
if err == nil {
task.isCompleted = true
return
}
task.err = err
globalLogger.Warn().Msgf("Task %d failed (attempt %d/%d): %v", task.taskID, retries+1, MaxRetries, err)
if retries < MaxRetries-1 {
time.Sleep(RetryDelay)
}
}
resp, err := fd.buildClient().Do(request)
errorChan <- fmt.Errorf("task %d failed after %d attempts: %v", task.taskID, MaxRetries, task.err)
}
func (fd *FileDownloader) doDownloadTask(progressChan chan ProgressChan, task *DownloadTask) error {
request, err := http.NewRequest("GET", fd.Url, nil)
if err != nil {
log.Printf("任务%d发送下载请求出错%s", task.taskID, err)
return
return fmt.Errorf("create request failed: %w", err)
}
fd.setHeaders(request)
if fd.IsMultiPart {
rangeStart := task.rangeStart + task.downloadedSize
rangeHeader := fmt.Sprintf("bytes=%d-%d", rangeStart, task.rangeEnd)
request.Header.Set("Range", rangeHeader)
}
client := fd.buildClient()
resp, err := client.Do(request)
if err != nil {
return fmt.Errorf("send request failed: %w", err)
}
defer resp.Body.Close()
buf := make([]byte, 8192)
if fd.IsMultiPart && resp.StatusCode != http.StatusPartialContent {
return fmt.Errorf("server does not support range requests, status: %d", resp.StatusCode)
} else if !fd.IsMultiPart && resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
buf := make([]byte, 32*1024)
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)
return
remain := task.rangeEnd - (task.rangeStart + task.downloadedSize) + 1
writeSize := int64(n)
if writeSize > remain {
writeSize = remain
}
_, writeErr := fd.File.WriteAt(buf[:writeSize], task.rangeStart+task.downloadedSize)
if writeErr != nil {
return fmt.Errorf("write file failed at offset %d: %w", task.rangeStart+task.downloadedSize, writeErr)
}
task.downloadedSize += writeSize
progressChan <- ProgressChan{taskID: task.taskID, bytes: writeSize}
if task.rangeStart+task.downloadedSize-1 >= task.rangeEnd {
return nil
}
downSize := int64(n)
task.downloadedSize += downSize
progressChan <- downSize
}
if err != nil {
if err == io.EOF {
task.isCompleted = true
break
expectedSize := task.rangeEnd - task.rangeStart + 1
if task.downloadedSize < expectedSize {
return fmt.Errorf("incomplete download: got %d bytes, expected %d", task.downloadedSize, expectedSize)
}
return nil
}
log.Printf("任务%d读取响应错误%s", task.taskID, err)
return
return fmt.Errorf("read response failed: %w", err)
}
}
}
func (fd *FileDownloader) Start() error {
err := fd.init()
func (fd *FileDownloader) verifyDownload() error {
for _, task := range fd.DownloadTaskList {
if !task.isCompleted {
return fmt.Errorf("task %d not completed", task.taskID)
}
expectedSize := task.rangeEnd - task.rangeStart + 1
if task.downloadedSize != expectedSize {
return fmt.Errorf("task %d size mismatch: got %d, expected %d", task.taskID, task.downloadedSize, expectedSize)
}
}
info, err := fd.File.Stat()
if err != nil {
return fmt.Errorf("get file info failed: %w", err)
}
if info.Size() != fd.TotalSize {
return fmt.Errorf("file size mismatch: got %d, expected %d", info.Size(), fd.TotalSize)
}
return nil
}
func (fd *FileDownloader) Start() error {
if err := fd.init(); err != nil {
return err
}
fd.createDownloadTasks()
fd.startDownload()
defer fd.File.Close()
return nil
err := fd.startDownload()
if fd.File != nil {
fd.File.Close()
}
return err
}

View File

@@ -10,11 +10,16 @@ import (
"net"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"res-downloader/core/shared"
sysRuntime "runtime"
"strings"
)
type respData map[string]interface{}
type ResponseData struct {
Code int `json:"code"`
Message string `json:"message"`
@@ -33,25 +38,31 @@ func initHttpServer() *HttpServer {
func (h *HttpServer) run() {
listener, err := net.Listen("tcp", globalConfig.Host+":"+globalConfig.Port)
if err != nil {
log.Fatalf("无法启动监听: %v", err)
globalLogger.Err(err)
log.Fatalf("Service cannot start: %v", err)
}
fmt.Println("服务已启动,监听 http://" + globalConfig.Host + ":" + globalConfig.Port)
if err := http.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Host == "127.0.0.1:"+globalConfig.Port && strings.Contains(r.URL.Path, "/cert") {
w.Header().Set("Content-Type", "application/x-x509-ca-data")
w.Header().Set("Content-Disposition", "attachment;filename=res-downloader-public.crt")
w.Header().Set("Content-Transfer-Encoding", "binary")
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(appOnce.PublicCrt)))
w.WriteHeader(http.StatusOK)
_, err = io.Copy(w, io.NopCloser(bytes.NewReader(appOnce.PublicCrt)))
fmt.Println("Service started, listening http://" + globalConfig.Host + ":" + globalConfig.Port)
if err1 := http.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Host == "127.0.0.1:"+globalConfig.Port && HandleApi(w, r) {
} else {
proxyOnce.Proxy.ServeHTTP(w, r) // 代理
}
})); err != nil {
fmt.Printf("服务器异常: %v", err)
})); err1 != nil {
globalLogger.Err(err1)
fmt.Printf("Service startup exception: %v", err1)
}
}
func (h *HttpServer) downCert(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/x-x509-ca-data")
w.Header().Set("Content-Disposition", "attachment;filename=res-downloader-public.crt")
w.Header().Set("Content-Transfer-Encoding", "binary")
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(appOnce.PublicCrt)))
w.WriteHeader(http.StatusOK)
io.Copy(w, io.NopCloser(bytes.NewReader(appOnce.PublicCrt)))
}
func (h *HttpServer) preview(w http.ResponseWriter, r *http.Request) {
realURL := r.URL.Query().Get("url")
if realURL == "" {
@@ -109,12 +120,47 @@ func (h *HttpServer) send(t string, data interface{}) {
runtime.EventsEmit(appOnce.ctx, "event", string(jsonData))
}
func (h *HttpServer) writeJson(w http.ResponseWriter, data ResponseData) {
func (h *HttpServer) writeJson(w http.ResponseWriter, data *ResponseData) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
err := json.NewEncoder(w).Encode(data)
if err != nil {
globalLogger.err(err)
globalLogger.Err(err)
}
}
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, h.buildResp(0, message, 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, h.buildResp(1, message, data))
}
func (h *HttpServer) buildResp(code int, message string, data interface{}) *ResponseData {
return &ResponseData{
Code: code,
Message: message,
Data: data,
}
}
@@ -124,14 +170,11 @@ func (h *HttpServer) openDirectoryDialog(w http.ResponseWriter, r *http.Request)
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 +189,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,
})
}
@@ -171,14 +211,10 @@ func (h *HttpServer) openFolder(w http.ResponseWriter, r *http.Request) {
switch sysRuntime.GOOS {
case "darwin":
// macOS
cmd = exec.Command("open", "-R", filePath)
case "windows":
// Windows
cmd = exec.Command("explorer", "/select,", filePath)
case "linux":
// linux
// 尝试使用不同的文件管理器
cmd = exec.Command("nautilus", filePath)
if err := cmd.Start(); err != nil {
cmd = exec.Command("thunar", filePath)
@@ -187,78 +223,110 @@ func (h *HttpServer) openFolder(w http.ResponseWriter, r *http.Request) {
if err := cmd.Start(); err != nil {
cmd = exec.Command("pcmanfm", filePath)
if err := cmd.Start(); err != nil {
globalLogger.err(err)
h.writeJson(w, ResponseData{Code: 0, Message: err.Error()})
globalLogger.Err(err)
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()})
globalLogger.Err(err)
h.error(w, err.Error())
return
}
h.writeJson(w, ResponseData{Code: 1})
h.success(w)
}
func (h *HttpServer) install(w http.ResponseWriter, r *http.Request) {
if appOnce.isInstall() {
h.success(w, respData{
"isPass": systemOnce.Password == "",
})
return
}
out, err := appOnce.installCert()
if err != nil {
h.error(w, err.Error()+"\n"+out, respData{
"isPass": systemOnce.Password == "",
})
return
}
h.success(w, respData{
"isPass": systemOnce.Password == "",
})
}
func (h *HttpServer) setSystemPassword(w http.ResponseWriter, r *http.Request) {
var data struct {
Password string `json:"password"`
IsCache bool `json:"isCache"`
}
err := json.NewDecoder(r.Body).Decode(&data)
if err != nil {
h.error(w, err.Error())
return
}
systemOnce.SetPassword(data.Password, data.IsCache)
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{
"isProxy": appOnce.IsProxy,
},
err := appOnce.OpenSystemProxy()
if err != nil {
h.error(w, err.Error(), respData{
"value": appOnce.IsProxy,
})
return
}
h.success(w, respData{
"value": appOnce.IsProxy,
})
}
func (h *HttpServer) unsetSystemProxy(w http.ResponseWriter, r *http.Request) {
appOnce.UnsetSystemProxy()
h.writeJson(w, ResponseData{
Code: 1,
Data: map[string]bool{
"isProxy": appOnce.IsProxy,
},
err := appOnce.UnsetSystemProxy()
if err != nil {
h.error(w, err.Error(), respData{
"value": appOnce.IsProxy,
})
return
}
h.success(w, respData{
"value": 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{
"value": 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 +342,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 +358,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 +367,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 +381,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-"+shared.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

@@ -6,6 +6,7 @@ import (
"io"
"os"
"path/filepath"
"res-downloader/core/shared"
)
type Logger struct {
@@ -15,7 +16,7 @@ type Logger struct {
func initLogger() *Logger {
if globalLogger == nil {
globalLogger = NewLogger(!IsDevelopment(), filepath.Join(appOnce.UserDir, "logs", "app.log"))
globalLogger = NewLogger(!shared.IsDevelopment(), filepath.Join(appOnce.UserDir, "logs", "app.log"))
}
return globalLogger
}
@@ -24,7 +25,7 @@ func (l *Logger) Close() {
_ = l.logFile.Close()
}
func (l *Logger) err(err error) {
func (l *Logger) Err(err error) {
l.Error().Stack().Err(err)
}
@@ -38,14 +39,14 @@ func NewLogger(logFile bool, logPath string) *Logger {
if logFile {
// log to file
logDir := filepath.Dir(logPath)
if err := CreateDirIfNotExist(logDir); err != nil {
if err := shared.CreateDirIfNotExist(logDir); err != nil {
panic(err)
}
var (
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

@@ -24,6 +24,10 @@ func HandleApi(w http.ResponseWriter, r *http.Request) bool {
return true
}
switch r.URL.Path {
case "/api/install":
httpServerOnce.install(w, r)
case "/api/set-system-password":
httpServerOnce.setSystemPassword(w, r)
case "/api/preview":
httpServerOnce.preview(w, r)
case "/api/proxy-open":
@@ -54,6 +58,10 @@ 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)
case "/api/cert":
httpServerOnce.downCert(w, r)
}
return true
}

View File

@@ -0,0 +1,78 @@
package plugins
import (
"encoding/json"
"github.com/elazarl/goproxy"
gonanoid "github.com/matoous/go-nanoid/v2"
"net/http"
"res-downloader/core/shared"
"strconv"
)
type DefaultPlugin struct {
bridge *shared.Bridge
}
func (p *DefaultPlugin) SetBridge(bridge *shared.Bridge) {
p.bridge = bridge
}
func (p *DefaultPlugin) Domains() []string {
return []string{"default"}
}
func (p *DefaultPlugin) OnRequest(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
return r, nil
}
func (p *DefaultPlugin) OnResponse(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
if resp == nil || resp.Request == nil || (resp.StatusCode != 200 && resp.StatusCode != 206) {
return resp
}
classify, suffix := p.bridge.TypeSuffix(resp.Header.Get("Content-Type"))
if classify == "" {
return resp
}
rawUrl := resp.Request.URL.String()
isAll, _ := p.bridge.GetResType("all")
isClassify, _ := p.bridge.GetResType(classify)
urlSign := shared.Md5(rawUrl)
if ok := p.bridge.MediaIsMarked(urlSign); !ok && (isAll || isClassify) {
value, _ := strconv.ParseFloat(resp.Header.Get("content-length"), 64)
id, err := gonanoid.New()
if err != nil {
id = urlSign
}
res := shared.MediaInfo{
Id: id,
Url: rawUrl,
UrlSign: urlSign,
CoverUrl: "",
Size: shared.FormatSize(value),
Domain: shared.GetTopLevelDomain(rawUrl),
Classify: classify,
Suffix: suffix,
Status: shared.DownloadStatusReady,
SavePath: "",
DecodeKey: "",
OtherData: map[string]string{},
Description: "",
ContentType: resp.Header.Get("Content-Type"),
}
// Store entire request headers as JSON
if headers, err := json.Marshal(resp.Request.Header); err == nil {
res.OtherData["headers"] = string(headers)
}
p.bridge.MarkMedia(urlSign)
go func(res shared.MediaInfo) {
p.bridge.Send("newResources", res)
}(res)
}
return resp
}

View File

@@ -0,0 +1,262 @@
package plugins
import (
"bytes"
"encoding/json"
"fmt"
"github.com/elazarl/goproxy"
gonanoid "github.com/matoous/go-nanoid/v2"
"io"
"net/http"
"regexp"
"res-downloader/core/shared"
"strconv"
"strings"
)
type QqPlugin struct {
bridge *shared.Bridge
}
func (p *QqPlugin) SetBridge(bridge *shared.Bridge) {
p.bridge = bridge
}
func (p *QqPlugin) Domains() []string {
return []string{"qq.com"}
}
func (p *QqPlugin) OnRequest(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
if strings.Contains(r.Host, "qq.com") && strings.Contains(r.URL.Path, "/res-downloader/wechat") {
if p.bridge.GetConfig("WxAction").(bool) && r.URL.Query().Get("type") == "1" {
return p.handleWechatRequest(r, ctx)
} else if !p.bridge.GetConfig("WxAction").(bool) && r.URL.Query().Get("type") == "2" {
return p.handleWechatRequest(r, ctx)
} else {
return r, p.buildEmptyResponse(r)
}
}
return nil, nil
}
func (p *QqPlugin) OnResponse(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
if resp.StatusCode != 200 && resp.StatusCode != 206 {
return nil
}
host := resp.Request.Host
Path := resp.Request.URL.Path
classify, _ := p.bridge.TypeSuffix(resp.Header.Get("Content-Type"))
if classify == "video" && strings.HasSuffix(host, "finder.video.qq.com") {
return resp
}
if strings.HasSuffix(host, "channels.weixin.qq.com") &&
(strings.Contains(Path, "/web/pages/feed") || strings.Contains(Path, "/web/pages/home")) {
return p.replaceWxJsContent(resp, ".js\"", ".js?v="+p.v()+"\"")
}
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") {
body, err := io.ReadAll(respTemp.Body)
if err != nil {
return respTemp
}
bodyStr := string(body)
newBody := regexp.MustCompile(`get\s*media\(\)\{`).
ReplaceAllString(bodyStr, `
get media(){
if(this.objectDesc){
fetch("https://wxapp.tc.qq.com/res-downloader/wechat?type=1", {
method: "POST",
mode: "no-cors",
body: JSON.stringify(this.objectDesc),
});
};
`)
newBody = regexp.MustCompile(`async\s*finderGetCommentDetail\((\w+)\)\s*\{return(.*?)\s*}\s*async`).
ReplaceAllString(newBody, `
async finderGetCommentDetail($1) {
var res = await$2;
if (res?.data?.object?.objectDesc) {
fetch("https://wxapp.tc.qq.com/res-downloader/wechat?type=2", {
method: "POST",
mode: "no-cors",
body: JSON.stringify(res.data.object.objectDesc),
});
}
return res;
}async
`)
newBodyBytes := []byte(newBody)
respTemp.Body = io.NopCloser(bytes.NewBuffer(newBodyBytes))
respTemp.ContentLength = int64(len(newBodyBytes))
respTemp.Header.Set("Content-Length", fmt.Sprintf("%d", len(newBodyBytes)))
return respTemp
}
if is {
return respTemp
}
}
return nil
}
func (p *QqPlugin) handleWechatRequest(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
body, err := io.ReadAll(r.Body)
if err != nil {
return r, p.buildEmptyResponse(r)
}
isAll, _ := p.bridge.GetResType("all")
isClassify, _ := p.bridge.GetResType("video")
if !isAll && !isClassify {
return r, p.buildEmptyResponse(r)
}
go p.handleMedia(body)
return r, p.buildEmptyResponse(r)
}
func (p *QqPlugin) handleMedia(body []byte) {
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return
}
mediaArr, ok := result["media"].([]interface{})
if !ok || len(mediaArr) == 0 {
return
}
firstMedia, ok := mediaArr[0].(map[string]interface{})
if !ok {
return
}
rawUrl, ok := firstMedia["url"].(string)
if !ok || rawUrl == "" {
return
}
urlSign := shared.Md5(rawUrl)
if p.bridge.MediaIsMarked(urlSign) {
return
}
id, err := gonanoid.New()
if err != nil {
id = urlSign
}
res := shared.MediaInfo{
Id: id,
Url: rawUrl,
UrlSign: urlSign,
CoverUrl: "",
Size: "0",
Domain: shared.GetTopLevelDomain(rawUrl),
Classify: "video",
Suffix: ".mp4",
Status: shared.DownloadStatusReady,
SavePath: "",
DecodeKey: "",
OtherData: map[string]string{},
Description: "",
ContentType: "video/mp4",
}
if mediaType, ok := firstMedia["mediaType"].(float64); ok && mediaType == 9 {
res.Classify = "image"
res.Suffix = ".png"
res.ContentType = "image/png"
}
if urlToken, ok := firstMedia["urlToken"].(string); ok {
res.Url += urlToken
}
switch size := firstMedia["fileSize"].(type) {
case float64:
res.Size = shared.FormatSize(size)
case string:
if value, err := strconv.ParseFloat(size, 64); err == nil {
res.Size = shared.FormatSize(value)
}
}
if coverUrl, ok := firstMedia["coverUrl"].(string); ok {
res.CoverUrl = coverUrl
}
if decodeKey, ok := firstMedia["decodeKey"].(string); ok {
res.DecodeKey = decodeKey
}
if desc, ok := result["description"].(string); ok {
res.Description = desc
}
if spec, ok := firstMedia["spec"].([]interface{}); ok {
var fileFormats []string
for _, item := range spec {
if m, ok := item.(map[string]interface{}); ok {
if format, ok := m["fileFormat"].(string); ok {
fileFormats = append(fileFormats, format)
}
}
}
res.OtherData["wx_file_formats"] = strings.Join(fileFormats, "#")
}
p.bridge.MarkMedia(urlSign)
go func(res shared.MediaInfo) {
p.bridge.Send("newResources", res)
}(res)
}
func (p *QqPlugin) buildEmptyResponse(r *http.Request) *http.Response {
body := "The content does not exist"
resp := &http.Response{
Status: http.StatusText(http.StatusOK),
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(body)),
ContentLength: int64(len(body)),
Request: r,
}
resp.Header.Set("Content-Type", "text/plain; charset=utf-8")
return resp
}
func (p *QqPlugin) replaceWxJsContent(resp *http.Response, old, new string) *http.Response {
body, err := io.ReadAll(resp.Body)
if err != nil {
return resp
}
bodyString := string(body)
newBodyString := strings.ReplaceAll(bodyString, old, new)
newBodyBytes := []byte(newBodyString)
resp.Body = io.NopCloser(bytes.NewBuffer(newBodyBytes))
resp.ContentLength = int64(len(newBodyBytes))
resp.Header.Set("Content-Length", fmt.Sprintf("%d", len(newBodyBytes)))
return resp
}
func (p *QqPlugin) v() string {
return p.bridge.GetVersion()
}

View File

@@ -1,22 +1,18 @@
package core
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"github.com/elazarl/goproxy"
gonanoid "github.com/matoous/go-nanoid/v2"
"io"
"net"
"net/http"
"net/url"
"regexp"
"strconv"
"res-downloader/core/plugins"
"res-downloader/core/shared"
"strings"
"time"
"github.com/elazarl/goproxy"
)
type Proxy struct {
@@ -42,6 +38,46 @@ type MediaInfo struct {
OtherData map[string]string
}
var pluginRegistry = make(map[string]shared.Plugin)
func init() {
ps := []shared.Plugin{
&plugins.QqPlugin{},
&plugins.DefaultPlugin{},
}
bridge := &shared.Bridge{
GetVersion: func() string {
return appOnce.Version
},
GetResType: func(key string) (bool, bool) {
return resourceOnce.getResType(key)
},
TypeSuffix: func(mine string) (string, string) {
return globalConfig.typeSuffix(mine)
},
MediaIsMarked: func(key string) bool {
return resourceOnce.mediaIsMarked(key)
},
MarkMedia: func(key string) {
resourceOnce.markMedia(key)
},
GetConfig: func(key string) interface{} {
return globalConfig.getConfig(key)
},
Send: func(t string, data interface{}) {
httpServerOnce.send(t, data)
},
}
for _, p := range ps {
p.SetBridge(bridge)
for _, domain := range p.Domains() {
pluginRegistry[domain] = p
}
}
}
func initProxy() *Proxy {
if proxyOnce == nil {
proxyOnce = &Proxy{}
@@ -53,7 +89,7 @@ func initProxy() *Proxy {
func (p *Proxy) Startup() {
err := p.setCa()
if err != nil {
DialogErr("启动代理服务失败" + err.Error())
DialogErr("Failed to start proxy service" + err.Error())
return
}
@@ -69,7 +105,7 @@ func (p *Proxy) Startup() {
func (p *Proxy) setCa() error {
ca, err := tls.X509KeyPair(appOnce.PublicCrt, appOnce.PrivateKey)
if err != nil {
DialogErr("启动代理服务失败1")
DialogErr("Failed to start proxy service 1")
return err
}
if ca.Leaf, err = x509.ParseCertificate(ca.Certificate[0]); err != nil {
@@ -104,257 +140,41 @@ func (p *Proxy) setTransport() {
p.Proxy.Tr = transport
}
func (p *Proxy) matchPlugin(host string) shared.Plugin {
domain := shared.GetTopLevelDomain(host)
if plugin, ok := pluginRegistry[domain]; ok {
return plugin
}
return nil
}
func (p *Proxy) httpRequestEvent(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
if strings.Contains(r.Host, "res-downloader.666666.com") && strings.Contains(r.URL.Path, "/wechat") {
if globalConfig.WxAction && r.URL.Query().Get("type") == "1" {
return p.handleWechatRequest(r, ctx)
} else if !globalConfig.WxAction && r.URL.Query().Get("type") == "2" {
return p.handleWechatRequest(r, ctx)
} else {
return r, p.buildEmptyResponse(r)
plugin := p.matchPlugin(r.Host)
if plugin != nil {
newReq, newResp := plugin.OnRequest(r, ctx)
if newResp != nil {
return newReq, newResp
}
if newReq != nil {
return newReq, nil
}
}
return r, nil
}
func (p *Proxy) handleWechatRequest(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
body, err := io.ReadAll(r.Body)
if err != nil {
fmt.Println(err)
return r, p.buildEmptyResponse(r)
}
isAll, _ := resourceOnce.getResType("all")
isClassify, _ := resourceOnce.getResType("video")
if !isAll && !isClassify {
return r, p.buildEmptyResponse(r)
}
go func(body []byte) {
var result map[string]interface{}
err = json.Unmarshal(body, &result)
if err != nil {
return
}
media, ok := result["media"].([]interface{})
if !ok || len(media) <= 0 {
return
}
firstMedia, ok := media[0].(map[string]interface{})
if !ok {
return
}
rowUrl, ok := firstMedia["url"]
if !ok {
return
}
resourceOnce.markMu.Lock()
defer resourceOnce.markMu.Unlock()
urlSign := Md5(rowUrl.(string))
if _, ok := resourceOnce.mark[urlSign]; ok {
return
}
id, err := gonanoid.New()
if err != nil {
id = urlSign
}
res := MediaInfo{
Id: id,
Url: rowUrl.(string),
UrlSign: urlSign,
CoverUrl: "",
Size: "0",
Domain: GetTopLevelDomain(rowUrl.(string)),
Classify: "video",
Suffix: ".mp4",
Status: DownloadStatusReady,
SavePath: "",
DecodeKey: "",
OtherData: map[string]string{},
Description: "",
ContentType: "video/mp4",
}
if mediaType, ok := firstMedia["mediaType"].(float64); ok && mediaType == 9 {
res.Classify = "image"
res.Suffix = ".png"
res.ContentType = "image/png"
}
if urlToken, ok := firstMedia["urlToken"].(string); ok {
res.Url = res.Url + urlToken
}
if fileSize, ok := firstMedia["fileSize"].(float64); ok {
res.Size = FormatSize(fileSize)
}
if coverUrl, ok := firstMedia["coverUrl"].(string); ok {
res.CoverUrl = coverUrl
}
if fileSize, ok := firstMedia["fileSize"].(string); ok {
value, err := strconv.ParseFloat(fileSize, 64)
if err == nil {
res.Size = FormatSize(value)
}
}
if decodeKey, ok := firstMedia["decodeKey"].(string); ok {
res.DecodeKey = decodeKey
}
if desc, ok := result["description"].(string); ok {
res.Description = desc
}
if spec, ok := firstMedia["spec"].([]interface{}); ok {
var fileFormats []string
for _, item := range spec {
if itemMap, ok := item.(map[string]interface{}); ok {
if format, exists := itemMap["fileFormat"].(string); exists {
fileFormats = append(fileFormats, format)
}
}
}
res.OtherData["wx_file_formats"] = strings.Join(fileFormats, "#")
}
resourceOnce.mark[urlSign] = true
httpServerOnce.send("newResources", res)
}(body)
return r, p.buildEmptyResponse(r)
}
func (p *Proxy) buildEmptyResponse(r *http.Request) *http.Response {
body := "内容不存在"
resp := &http.Response{
Status: "200 OK",
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(body)),
ContentLength: int64(len(body)),
Request: r,
}
resp.Header.Set("Content-Type", "text/plain")
return resp
return pluginRegistry["default"].OnRequest(r, ctx)
}
func (p *Proxy) httpResponseEvent(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
if resp == nil || resp.Request == nil || (resp.StatusCode != 200 && resp.StatusCode != 206) {
if resp == nil || resp.Request == nil {
return resp
}
host := resp.Request.Host
Path := resp.Request.URL.Path
if strings.HasSuffix(host, "channels.weixin.qq.com") &&
(strings.Contains(Path, "/web/pages/feed") || strings.Contains(Path, "/web/pages/home")) {
return p.replaceWxJsContent(resp, ".js\"", ".js?v="+p.v()+"\"")
}
if strings.HasSuffix(host, "res.wx.qq.com") {
respTemp := resp
if strings.HasSuffix(respTemp.Request.URL.RequestURI(), ".js?v="+p.v()) {
respTemp = p.replaceWxJsContent(respTemp, ".js\"", ".js?v="+p.v()+"\"")
plugin := p.matchPlugin(resp.Request.Host)
if plugin != nil {
newResp := plugin.OnResponse(resp, ctx)
if newResp != nil {
return newResp
}
if strings.Contains(Path, "web/web-finder/res/js/virtual_svg-icons-register.publish") {
body, err := io.ReadAll(respTemp.Body)
if err != nil {
return respTemp
}
bodyStr := string(body)
newBody := regexp.MustCompile(`get\s*media\(\)\{`).
ReplaceAllString(bodyStr, `
get media(){
if(this.objectDesc){
fetch("https://res-downloader.666666.com/wechat?type=1", {
method: "POST",
mode: "no-cors",
body: JSON.stringify(this.objectDesc),
});
};
`)
newBody = regexp.MustCompile(`async\s*finderGetCommentDetail\((\w+)\)\s*\{return(.*?)\s*}\s*async`).
ReplaceAllString(newBody, `
async finderGetCommentDetail($1) {
var res = await$2;
if (res?.data?.object?.objectDesc) {
fetch("https://res-downloader.666666.com/wechat?type=2", {
method: "POST",
mode: "no-cors",
body: JSON.stringify(res.data.object.objectDesc),
});
}
return res;
}async
`)
newBodyBytes := []byte(newBody)
respTemp.Body = io.NopCloser(bytes.NewBuffer(newBodyBytes))
respTemp.ContentLength = int64(len(newBodyBytes))
respTemp.Header.Set("Content-Length", fmt.Sprintf("%d", len(newBodyBytes)))
return respTemp
}
return respTemp
}
classify, suffix := TypeSuffix(resp.Header.Get("Content-Type"))
if classify == "" {
return resp
}
if classify == "video" && strings.HasSuffix(host, "finder.video.qq.com") {
//if !globalConfig.WxAction && classify == "video" && strings.HasSuffix(host, "finder.video.qq.com") {
return resp
}
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) {
value, _ := strconv.ParseFloat(resp.Header.Get("content-length"), 64)
id, err := gonanoid.New()
if err != nil {
id = urlSign
}
res := MediaInfo{
Id: id,
Url: rawUrl,
UrlSign: urlSign,
CoverUrl: "",
Size: FormatSize(value),
Domain: GetTopLevelDomain(rawUrl),
Classify: classify,
Suffix: suffix,
Status: DownloadStatusReady,
SavePath: "",
DecodeKey: "",
OtherData: map[string]string{},
Description: "",
ContentType: resp.Header.Get("Content-Type"),
}
resourceOnce.mark[urlSign] = true
httpServerOnce.send("newResources", res)
}
return resp
}
func (p *Proxy) replaceWxJsContent(resp *http.Response, old, new string) *http.Response {
body, err := io.ReadAll(resp.Body)
if err != nil {
return resp
}
bodyString := string(body)
newBodyString := strings.ReplaceAll(bodyString, old, new)
newBodyBytes := []byte(newBodyString)
resp.Body = io.NopCloser(bytes.NewBuffer(newBodyBytes))
resp.ContentLength = int64(len(newBodyBytes))
resp.Header.Set("Content-Length", fmt.Sprintf("%d", len(newBodyBytes)))
return resp
}
func (p *Proxy) v() string {
return appOnce.Version
return pluginRegistry["default"].OnResponse(resp, ctx)
}

View File

@@ -2,40 +2,33 @@ package core
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/url"
"os"
"path/filepath"
"regexp"
"res-downloader/core/shared"
"strconv"
"strings"
"sync"
)
const (
DownloadStatusReady string = "ready" // task create but not start
DownloadStatusRunning string = "running"
DownloadStatusError string = "error"
DownloadStatusDone string = "done"
DownloadStatusHandle string = "handle"
)
type WxFileDecodeResult struct {
SavePath string
Message string
}
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 +45,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 +82,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) {
@@ -110,7 +95,7 @@ func (r *Resource) download(mediaInfo MediaInfo, decodeStr string) {
}
go func(mediaInfo MediaInfo) {
rawUrl := mediaInfo.Url
fileName := Md5(rawUrl)
fileName := shared.Md5(rawUrl)
if mediaInfo.Description != "" {
fileName = regexp.MustCompile(`[^\w\p{Han}]`).ReplaceAllString(mediaInfo.Description, "")
fileLen := globalConfig.FilenameLen
@@ -125,7 +110,7 @@ func (r *Resource) download(mediaInfo MediaInfo, decodeStr string) {
}
if globalConfig.FilenameTime {
mediaInfo.SavePath = filepath.Join(globalConfig.SaveDirectory, fileName+"_"+GetCurrentDateTimeFormatted()+mediaInfo.Suffix)
mediaInfo.SavePath = filepath.Join(globalConfig.SaveDirectory, fileName+"_"+shared.GetCurrentDateTimeFormatted()+mediaInfo.Suffix)
} else {
mediaInfo.SavePath = filepath.Join(globalConfig.SaveDirectory, fileName+mediaInfo.Suffix)
}
@@ -152,9 +137,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, taskID int, taskProgress float64) {
r.progressEventsEmit(mediaInfo, strconv.Itoa(int(totalDownloaded*100/totalSize))+"%", shared.DownloadStatusRunning)
}
err := downloader.Start()
if err != nil {
@@ -162,23 +149,42 @@ func (r *Resource) download(mediaInfo MediaInfo, decodeStr string) {
return
}
if decodeStr != "" {
r.progressEventsEmit(mediaInfo, "解密中", DownloadStatusRunning)
r.progressEventsEmit(mediaInfo, "decrypting in progress", shared.DownloadStatusRunning)
if err := r.decodeWxFile(mediaInfo.SavePath, decodeStr); err != nil {
r.progressEventsEmit(mediaInfo, "解密出错"+err.Error())
r.progressEventsEmit(mediaInfo, "decryption error: "+err.Error())
return
}
}
r.progressEventsEmit(mediaInfo, "完成", DownloadStatusDone)
r.progressEventsEmit(mediaInfo, "complete", shared.DownloadStatusDone)
}(mediaInfo)
}
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
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 {
return "", err
}
defer sourceFile.Close()
mediaInfo.SavePath = strings.ReplaceAll(fileName, ".mp4", "_解密.mp4")
mediaInfo.SavePath = strings.ReplaceAll(fileName, ".mp4", "_decrypt.mp4")
destinationFile, err := os.Create(mediaInfo.SavePath)
if err != nil {
@@ -198,7 +204,7 @@ func (r *Resource) wxFileDecode(mediaInfo MediaInfo, fileName, decodeStr string)
}
func (r *Resource) progressEventsEmit(mediaInfo MediaInfo, args ...string) {
Status := DownloadStatusError
Status := shared.DownloadStatusError
Message := "ok"
if len(args) > 0 {

18
core/shared/base.go Normal file
View File

@@ -0,0 +1,18 @@
package shared
type MediaInfo struct {
Id string
Url string
UrlSign string
CoverUrl string
Size string
Domain string
Classify string
Suffix string
SavePath string
Status string
DecodeKey string
Description string
ContentType string
OtherData map[string]string
}

9
core/shared/const.go Normal file
View File

@@ -0,0 +1,9 @@
package shared
const (
DownloadStatusReady string = "ready" // task create but not start
DownloadStatusRunning string = "running"
DownloadStatusError string = "error"
DownloadStatusDone string = "done"
DownloadStatusHandle string = "handle"
)

23
core/shared/plugin.go Normal file
View File

@@ -0,0 +1,23 @@
package shared
import (
"github.com/elazarl/goproxy"
"net/http"
)
type Bridge struct {
GetVersion func() string
GetResType func(key string) (bool, bool)
TypeSuffix func(mime string) (string, string)
MediaIsMarked func(key string) bool
MarkMedia func(key string)
GetConfig func(key string) interface{}
Send func(t string, data interface{})
}
type Plugin interface {
SetBridge(*Bridge)
Domains() []string
OnRequest(*http.Request, *goproxy.ProxyCtx) (*http.Request, *http.Response)
OnResponse(*http.Response, *goproxy.ProxyCtx) *http.Response
}

70
core/shared/utils.go Normal file
View File

@@ -0,0 +1,70 @@
package shared
import (
"crypto/md5"
"encoding/hex"
"fmt"
"golang.org/x/net/publicsuffix"
"net/url"
"os"
"time"
)
func Md5(data string) string {
hashNew := md5.New()
hashNew.Write([]byte(data))
hash := hashNew.Sum(nil)
return hex.EncodeToString(hash)
}
func FormatSize(size float64) string {
if size > 1048576 {
return fmt.Sprintf("%.2fMB", float64(size)/1048576)
}
if size > 1024 {
return fmt.Sprintf("%.2fKB", float64(size)/1024)
}
return fmt.Sprintf("%.0fb", size)
}
func GetTopLevelDomain(rawURL string) string {
u, err := url.Parse(rawURL)
if err == nil && u.Host != "" {
rawURL = u.Host
}
domain, err := publicsuffix.EffectiveTLDPlusOne(rawURL)
if err != nil {
return rawURL
}
return domain
}
func FileExist(file string) bool {
info, err := os.Stat(file)
if err != nil {
return false
}
return !info.IsDir()
}
func CreateDirIfNotExist(dir string) error {
if _, err := os.Stat(dir); os.IsNotExist(err) {
return os.MkdirAll(dir, 0750)
}
return nil
}
func IsDevelopment() bool {
return os.Getenv("APP_ENV") == "development"
}
func GetCurrentDateTimeFormatted() string {
now := time.Now()
return fmt.Sprintf("%04d%02d%02d%02d%02d%02d",
now.Year(),
now.Month(),
now.Day(),
now.Hour(),
now.Minute(),
now.Second())
}

View File

@@ -3,6 +3,7 @@ package core
import (
"os"
"path"
"res-downloader/core/shared"
)
type Storage struct {
@@ -18,8 +19,8 @@ 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)
if !shared.FileExist(l.fileName) {
err := os.WriteFile(l.fileName, l.def, 0644)
if err != nil {
return nil, err
}
@@ -33,7 +34,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

@@ -1,19 +1,27 @@
package core
import (
"fmt"
"os"
"path/filepath"
"time"
)
type SystemSetup struct {
CertFile string
CertFile string
CacheFile string
Password string
aesCipher *AESCipher
}
func initSystem() *SystemSetup {
if systemOnce == nil {
systemOnce = &SystemSetup{
CertFile: filepath.Join(appOnce.UserDir, "cert.crt"),
aesCipher: NewAESCipher("resd48w2d7er95627d447c490a8f02ff"),
CertFile: filepath.Join(appOnce.UserDir, "cert.crt"),
CacheFile: filepath.Join(appOnce.UserDir, "pass.cache"),
}
systemOnce.checkPasswordFile()
}
return systemOnce
}
@@ -24,7 +32,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 +41,43 @@ func (s *SystemSetup) initCert() ([]byte, error) {
return nil, err
}
}
func (s *SystemSetup) SetPassword(password string, isCache bool) {
s.Password = password
if isCache {
encrypted, err := s.aesCipher.Encrypt(password)
if err == nil {
err1 := os.WriteFile(s.CacheFile, []byte(encrypted), 0750)
if err1 != nil {
fmt.Println("Failed to write password: ", err1.Error())
}
} else {
fmt.Println("Failed to Encrypt password: ", err.Error())
}
}
}
func (s *SystemSetup) checkPasswordFile() {
fileInfo, err := os.Stat(s.CacheFile)
if err != nil {
return
}
lastModified := fileInfo.ModTime()
oneMonthAgo := time.Now().AddDate(0, -1, 0)
if lastModified.Before(oneMonthAgo) {
os.Remove(s.CacheFile)
return
}
content, err := os.ReadFile(s.CacheFile)
if err != nil {
return
}
password, err := s.aesCipher.Decrypt(string(content))
if err != nil {
return
}
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)
}
@@ -52,25 +64,27 @@ func (s *SystemSetup) setProxy() error {
return err
}
is := false
isSuccess := false
var errs strings.Builder
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
commands := [][]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 _, cmd := range commands {
if output, err := s.runCommand(cmd); err != nil {
errs.WriteString(fmt.Sprintf("cmd: %v\noutput: %s\nerr: %s\n", cmd, output, err))
} else {
isSuccess = true
}
}
}
if is {
if isSuccess {
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 {
@@ -79,25 +93,27 @@ func (s *SystemSetup) unsetProxy() error {
return err
}
is := false
isSuccess := false
var errs strings.Builder
for _, serviceName := range services {
if err := exec.Command("networksetup", "-setwebproxystate", serviceName, "off").Run(); err != nil {
fmt.Println(err)
} else {
is = true
commands := [][]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 _, cmd := range commands {
if output, err := s.runCommand(cmd); err != nil {
errs.WriteString(fmt.Sprintf("cmd: %v\noutput: %s\nerr: %s\n", cmd, output, err))
} else {
isSuccess = true
}
}
}
if is {
if isSuccess {
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) {
@@ -105,18 +121,7 @@ func (s *SystemSetup) installCert() (string, error) {
if err != nil {
return "", err
}
getPasswordCmd := exec.Command("osascript", "-e", `tell app "System Events" to display dialog "请输入你的电脑密码,用于安装证书文件:" default answer "" with hidden answer`, "-e", `text returned of result`)
passwordOutput, err := getPasswordCmd.Output()
if err != nil {
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)
cmd.Stdin = bytes.NewReader(append(password, '\n'))
output, err := cmd.CombinedOutput()
output, err := s.runCommand([]string{"security", "add-trusted-cert", "-d", "-r", "trustRoot", "-k", "/Library/Keychains/System.keychain", s.CertFile})
if err != nil {
return string(output), err
}

View File

@@ -3,10 +3,43 @@
package core
import (
"bytes"
"fmt"
"os"
"os/exec"
"strings"
)
func (s *SystemSetup) getLinuxDistro() (string, error) {
data, err := os.ReadFile("/etc/os-release")
if err != nil {
return "", err
}
for _, line := range strings.Split(string(data), "\n") {
if strings.HasPrefix(line, "ID=") {
return strings.Trim(strings.TrimPrefix(line, "ID="), "\""), nil
}
}
return "", fmt.Errorf("could not determine linux distribution")
}
func (s *SystemSetup) runCommand(args []string, sudo bool) ([]byte, error) {
if len(args) == 0 {
return nil, fmt.Errorf("no command provided")
}
var cmd *exec.Cmd
if s.Password != "" && sudo {
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) setProxy() error {
commands := [][]string{
{"gsettings", "set", "org.gnome.system.proxy", "mode", "manual"},
@@ -15,23 +48,32 @@ func (s *SystemSetup) setProxy() error {
{"gsettings", "set", "org.gnome.system.proxy.https", "host", "127.0.0.1"},
{"gsettings", "set", "org.gnome.system.proxy.https", "port", globalConfig.Port},
}
is := false
isSuccess := false
var errs strings.Builder
for _, cmd := range commands {
if err := exec.Command(cmd[0], cmd[1:]...).Run(); err != nil {
fmt.Println(err)
if output, err := s.runCommand(cmd, false); err != nil {
errs.WriteString(fmt.Sprintf("cmd: %v\noutput: %s\nerr: %s\n", cmd, output, err))
} else {
is = true
isSuccess = true
}
}
if is {
if isSuccess {
return nil
}
return fmt.Errorf("Failed to activate proxy")
return fmt.Errorf("failed to set proxy:\n%s", errs.String())
}
func (s *SystemSetup) unsetProxy() error {
cmd := []string{"gsettings", "set", "org.gnome.system.proxy", "mode", "none"}
return exec.Command(cmd[0], cmd[1:]...).Run()
output, err := s.runCommand(cmd, false)
if err != nil {
return fmt.Errorf("failed to unset proxy: %s\noutput: %s", err.Error(), string(output))
}
return nil
}
func (s *SystemSetup) installCert() (string, error) {
@@ -40,35 +82,56 @@ func (s *SystemSetup) installCert() (string, error) {
return "", err
}
actions := [][]string{
{"/usr/local/share/ca-certificates/", "update-ca-certificates"},
{"/usr/share/ca-certificates/trust-source/anchors/", "update-ca-trust"},
{"/usr/share/ca-certificates/trust-source/anchors/", "trust extract-compat"},
{"/etc/pki/ca-trust/source/anchors/", "update-ca-trust"},
{"/etc/ssl/ca-certificates/", "update-ca-certificates"},
distro, err := s.getLinuxDistro()
if err != nil {
return "", fmt.Errorf("detect distro failed: %w", err)
}
is := false
certName := appOnce.AppName + ".crt"
for _, action := range actions {
dir := action[0]
if err := exec.Command("sudo", "cp", "-f", s.CertFile, dir+appOnce.AppName+".crt").Run(); err != nil {
fmt.Printf("Failed to copy to %s: %v\n", dir, err)
continue
var certPath string
if distro == "deepin" {
certDir := "/usr/share/ca-certificates/" + appOnce.AppName
certPath = certDir + "/" + certName
s.runCommand([]string{"mkdir", "-p", certDir}, true)
} else {
certPath = "/usr/local/share/ca-certificates/" + certName
}
var outs, errs strings.Builder
isSuccess := false
if output, err := s.runCommand([]string{"cp", "-f", s.CertFile, certPath}, true); err != nil {
errs.WriteString(fmt.Sprintf("copy cert failed: %s\n%s\n", err.Error(), output))
} else {
isSuccess = true
outs.Write(output)
}
if distro == "deepin" {
confPath := "/etc/ca-certificates.conf"
checkCmd := []string{"grep", "-qxF", certName, confPath}
if _, err := s.runCommand(checkCmd, true); err != nil {
echoCmd := []string{"bash", "-c", fmt.Sprintf("echo '%s' >> %s", certName, confPath)}
if output, err := s.runCommand(echoCmd, true); err != nil {
errs.WriteString(fmt.Sprintf("append conf failed: %s\n%s\n", err.Error(), output))
} else {
isSuccess = true
outs.Write(output)
}
}
cmd := action[1]
if err := exec.Command("sudo", cmd).Run(); err != nil {
fmt.Printf("Failed to refresh certificates using %s: %v\n", cmd, err)
continue
}
is = true
}
if !is {
return "", fmt.Errorf("Certificate installation failed")
if output, err := s.runCommand([]string{"update-ca-certificates"}, true); err != nil {
errs.WriteString(fmt.Sprintf("update failed: %s\n%s\n", err.Error(), output))
} else {
isSuccess = true
outs.Write(output)
}
return "", nil
if isSuccess {
return "", nil
}
return outs.String(), fmt.Errorf("certificate installation failed:\n%s", errs.String())
}

View File

@@ -1,19 +1,9 @@
package core
import (
"crypto/md5"
"encoding/hex"
"fmt"
"github.com/wailsapp/wails/v2/pkg/runtime"
"net/url"
"os"
"strings"
"time"
)
func Empty(data interface{}) {
}
func DialogErr(message string) {
_, _ = runtime.MessageDialog(appOnce.ctx, runtime.MessageDialogOptions{
Type: runtime.ErrorDialog,
@@ -22,136 +12,3 @@ func DialogErr(message string) {
DefaultButton: "Cancel",
})
}
func IsDevelopment() bool {
return os.Getenv("APP_ENV") == "development"
}
func FileExist(file string) bool {
_, err := os.Stat(file)
if err != nil {
return false
}
if os.IsNotExist(err) {
return false
}
return true
}
func CreateDirIfNotExist(dir string) error {
if _, err := os.Stat(dir); os.IsNotExist(err) {
return os.MkdirAll(dir, 0777)
}
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"
}
return "", ""
}
func BuildReferer(rawURL string) string {
u, err := url.Parse(rawURL)
if err != nil {
return ""
}
return u.Scheme + "://" + u.Host + "/"
}
func Md5(data string) string {
hashNew := md5.New()
hashNew.Write([]byte(data))
hash := hashNew.Sum(nil)
return hex.EncodeToString(hash)
}
func FormatSize(size float64) string {
if size > 1048576 {
return fmt.Sprintf("%.2fMB", float64(size)/1048576)
}
if size > 1024 {
return fmt.Sprintf("%.2fKB", float64(size)/1024)
}
return fmt.Sprintf("%.0fb", size)
}
func GetTopLevelDomain(rawURL string) string {
parsedURL, err := url.Parse(rawURL)
if err != nil {
return ""
}
host := parsedURL.Hostname()
parts := strings.Split(host, ".")
if len(parts) < 2 {
return ""
}
return strings.Join(parts[len(parts)-2:], ".")
}
func GetCurrentDateTimeFormatted() string {
now := time.Now()
return fmt.Sprintf("%04d%02d%02d%02d%02d%02d",
now.Year(),
now.Month(),
now.Day(),
now.Hour(),
now.Minute(),
now.Second())
}

View File

@@ -1,4 +1,7 @@
# res-downloader V3
<div align="center">
<a href="https://github.com/putyy/res-downloader"><img src="images/logo.png" width="120"/></a>
<h1><strong>res-downloader</strong></h1>
</div>
> 全新技术栈,更新、更小、更快、更稳

View File

@@ -1,4 +1,4 @@
* [论坛](https://s.gowas.cn/d/4089)
* [反馈](https://github.com/putyy/res-downloader/issues)
* [日志](https://github.com/putyy/res-downloader/releases)
* [QQ](https://qm.qq.com/q/ImE37ayJmc)
* [WX](https://www.putyy.com/app/admin/upload/img/20250418/6801d9554dc7.webp)

View File

@@ -3,7 +3,7 @@
![](images/examples-1.png ':size=50%')
## 拦截资源
### 视频号
### 视频号
- 打开视频号即可看到本软件中拦截到的资源
![](images/examples-2.webp ':size=50%')

BIN
docs/images/logo.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

View File

@@ -27,15 +27,13 @@
subMaxLevel: 4,
// 小屏设备下合并导航栏到侧边栏
mergeNavbar: true,
basePath: '/',
homepage: 'readme.md',
search: {
maxAge: 86400000,// 过期时间,单位毫秒,默认一天
paths: 'auto',// 注意:仅适用于 paths: 'auto' 模式
placeholder: '搜索',
// 支持本地化
placeholder: {
'/zh-cn/': '搜索',
'/': 'Type to search'
},
placeholder: '搜索',
noData: '找不到结果',
depth: 4,
hideOtherSidebarContent: false,

View File

@@ -10,8 +10,11 @@
- 双击下载好的dmg文件将res-downloader拖入应用即可如图:
![installation-mac-1.png](images/installation-mac-1.png ':size=50%')
## Linux安装过程
- 执行文件运行方式举例
## Linux安装过程(自行替换掉对应的安装文件目录)
- ubuntu安装deb文件
> sudo apt install res-downloader_3.0.2_linux_x64.deb
- 执行文件运行方式
> chmod +x ./res-downloader_3.0.2_linux_x64
> sudo ./res-downloader_3.0.2_linux_x64

View File

@@ -1,7 +1,3 @@
## 隐藏功能
- 导出\导入 数据1秒内连续点击空白处5次即可开启(单次有效)
![more-4.png](images/more-4.png ':size=30%')
## 清空列表、类型筛选
- 当资源列表过大时,无法快速找到需要的资源,这时可以先清空列表再去刷新需要的资源页面
- 资源列表过多,可以快速根据需要的资源类型进行筛选

View File

@@ -1,15 +1,63 @@
# 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="images/logo.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>
## 实现 & 初衷
?> 通过设置系统网络代理拦截响应,筛选出需要的资源, 同fiddler、charles等抓包软件、浏览器F12打开控制也能达到目的只不过这些软件需要手动进行筛选对于小白用户上手还是有点难度本软件对部分资源做了特殊处理更适合大众用户所以就有了本项目。
[![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>
---
### 🎉 爱享素材下载器
> 一款基于 Go + [Wails](https://github.com/wailsapp/wails) 的跨平台资源下载工具,简洁易用,支持多种资源嗅探与下载。
## ✨ 功能特色
- 🚀 **简单易用**:操作简单,界面清晰美观
- 🖥️ **多平台支持**Windows / macOS / Linux
- 🌐 **多资源类型支持**:视频 / 音频 / 图片 / m3u8 / 直播流等
- 📱 **平台兼容广泛**支持微信视频号、小程序、抖音、快手、小红书、酷狗音乐、QQ音乐等
- 🌍 **代理抓包**:支持设置代理获取受限网络下的资源
## 📚 文档 & 版本
- 📘 [在线文档](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/releases)
- 🆕 [蓝奏云下载密码9vs5](https://wwjv.lanzoum.com/b04wgtfyb)
- ⚠️ *Win7 用户请下载 `2.3.0` 版本*
## 🖼️ 预览
![预览](images/show.webp)
---
### 🧠 更多问题
- [GitHub Issues](https://github.com/putyy/res-downloader/issues)
- [爱享论坛讨论帖](https://s.gowas.cn/d/4089)
## 💡 实现原理 & 初衷
本工具通过代理方式实现网络抓包,并筛选可用资源。与 Fiddler、Charles、浏览器 DevTools 原理类似,但对资源进行了更友好的筛选、展示和处理,大幅度降低了使用门槛,更适合大众用户使用。
---
## ⚠️ 免责声明
> 本软件仅供学习与研究用途,禁止用于任何商业或违法用途。
如因此产生的任何法律责任,概与作者无关!

View File

@@ -1,3 +1,34 @@
## 视频号拦截了一大堆 找不到想要的
> 设置里面关闭全量拦截,将视频转发好友后打开
## 某某网址拦截不了?
> 本软件实现原理 & 初衷如下,并非万能的,所以有一些应用拦截不了很正常
```
本工具通过代理方式实现网络抓包,并筛选可用资源。与 Fiddler、Charles、浏览器 DevTools 原理类似,但对资源进行了更友好的筛选、展示和处理,大幅度降低了使用门槛,更适合大众用户使用。
```
## 软件打不开了?之前可以打开
> 删除对应目录, 然后重启
```
## Mac执行
rm -rf /Users/$(whoami)/Library/Preferences/res-downloader
## Windows手动删除以下目录Administrator为用户名 通常如下:
C:\Users\Administrator\AppData\Roaming\res-downloader
## Linux手动删除以下目录
/home/user/.config/res-downloader/home/user/.config/res-downloader
```
## 某应用只支持手机打开 如何拦截?
> 这里需要注意的是 应用使用http协议通讯才能拦截且安卓7.0以上系统不再信任用户CA证书 所以没法拦截,解决方案自行查找,
```
1. 将手机和电脑处于同一个网络
2. 在手机端安装res-downloader的证书
3. 将手机网络代理设置为res-downloader的代理
4. 正常使用
```
## Mac 提示“已损坏,无法打开”, 打开命令行执行如下命令:
> sudo xattr -d com.apple.quarantine /Applications/res-downloader.app
@@ -10,7 +41,7 @@
> 手动关闭系统代理设置
## 链接不是私密链接
> 通常是证书未正确安装,最新版证书下载:软件左下角?点击后有下载地址
> 通常是证书未正确安装,最新版证书下载:软件左下角 ?点击后有下载地址
> 根据自己系统进行安装证书操作(不懂的自行百度),手动安装需安装到受信任的根证书
- Mac手动安装证书(V3+版本支持),打开终端复制以下命令 粘贴到终端回车 按照提示输入密码,完成后再打开软件:

View File

@@ -31,12 +31,12 @@ declare module 'vue' {
NModal: typeof import('naive-ui')['NModal']
NModalProvider: typeof import('naive-ui')['NModalProvider']
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
NRadio: typeof import('naive-ui')['NRadio']
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

@@ -13,7 +13,9 @@
"flv.js": "^1.6.2",
"naive-ui": "^2.38.2",
"pinia": "^2.1.7",
"video.js": "^8.22.0",
"vue": "^3.2.37",
"vue-i18n": "^11.1.3",
"vue-router": "^4.3.3"
},
"devDependencies": {
@@ -23,14 +25,12 @@
"@vitejs/plugin-vue": "^3.0.3",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38",
"pug": "^3.0.3",
"sass": "^1.77.6",
"sass-loader": "^14.2.1",
"tailwindcss": "^3.4.4",
"typescript": "^4.6.4",
"unplugin-auto-import": "^0.18.3",
"unplugin-vue-components": "^0.27.4",
"video.js": "^8.19.1",
"vite": "^3.0.7",
"vue-tsc": "^1.8.27"
}
@@ -95,7 +95,6 @@
"version": "7.26.0",
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.26.0.tgz",
"integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
"dev": true,
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
@@ -198,6 +197,50 @@
"vue": ">=3"
}
},
"node_modules/@intlify/core-base": {
"version": "11.1.3",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.1.3.tgz",
"integrity": "sha512-cMuHunYO7LE80azTitcvEbs1KJmtd6g7I5pxlApV3Jo547zdO3h31/0uXpqHc+Y3RKt1wo2y68RGSx77Z1klyA==",
"license": "MIT",
"dependencies": {
"@intlify/message-compiler": "11.1.3",
"@intlify/shared": "11.1.3"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/message-compiler": {
"version": "11.1.3",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.1.3.tgz",
"integrity": "sha512-7rbqqpo2f5+tIcwZTAG/Ooy9C8NDVwfDkvSeDPWUPQW+Dyzfw2o9H103N5lKBxO7wxX9dgCDjQ8Umz73uYw3hw==",
"license": "MIT",
"dependencies": {
"@intlify/shared": "11.1.3",
"source-map-js": "^1.0.2"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/shared": {
"version": "11.1.3",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.1.3.tgz",
"integrity": "sha512-pTFBgqa/99JRA2H1qfyqv97MKWJrYngXBA/I0elZcYxvJgcCw3mApAoPW3mJ7vx3j+Ti0FyKUFZ4hWxdjKaxvA==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -714,10 +757,9 @@
"license": "MIT"
},
"node_modules/@videojs/http-streaming": {
"version": "3.16.2",
"resolved": "https://registry.npmmirror.com/@videojs/http-streaming/-/http-streaming-3.16.2.tgz",
"integrity": "sha512-fvt4ko7FknxiT9FnjyNQt6q2px+awrkM+Orv7IB/4gldvj94u4fowGfmNHynnvNTPgPkdxHklGmFLGfclYw8HA==",
"dev": true,
"version": "3.17.0",
"resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.17.0.tgz",
"integrity": "sha512-Ch1P3tvvIEezeZXyK11UfWgp4cWKX4vIhZ30baN/lRinqdbakZ5hiAI3pGjRy3d+q/Epyc8Csz5xMdKNNGYpcw==",
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.12.5",
@@ -739,9 +781,8 @@
},
"node_modules/@videojs/vhs-utils": {
"version": "4.1.1",
"resolved": "https://registry.npmmirror.com/@videojs/vhs-utils/-/vhs-utils-4.1.1.tgz",
"resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.1.1.tgz",
"integrity": "sha512-5iLX6sR2ownbv4Mtejw6Ax+naosGvoT9kY+gcuHzANyUZZ+4NpeNdKMUhb6ag0acYej1Y7cmr/F2+4PrggMiVA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5",
@@ -756,7 +797,6 @@
"version": "2.7.0",
"resolved": "https://registry.npmmirror.com/@videojs/xhr/-/xhr-2.7.0.tgz",
"integrity": "sha512-giab+EVRanChIupZK7gXjHy90y3nncA2phIOyG3Ne5fvpiMJzvqYwiTOnEVW2S4CoYcuKJkomat7bMXA/UoUZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.5.5",
@@ -943,32 +983,17 @@
},
"node_modules/@xmldom/xmldom": {
"version": "0.8.10",
"resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
"integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/acorn": {
"version": "7.4.1",
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-7.4.1.tgz",
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/aes-decrypter": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/aes-decrypter/-/aes-decrypter-4.0.2.tgz",
"resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-4.0.2.tgz",
"integrity": "sha512-lc+/9s6iJvuaRe5qDlMTpCFjnwpkeOXp8qP3oiZ5jsj1MRg+SBVUmmICrhxHvc8OELSmc+fEyyxAuppY6hrWzw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.12.5",
@@ -1031,20 +1056,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/asap": {
"version": "2.0.6",
"resolved": "https://registry.npmmirror.com/asap/-/asap-2.0.6.tgz",
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"dev": true,
"license": "MIT"
},
"node_modules/assert-never": {
"version": "1.4.0",
"resolved": "https://registry.npmmirror.com/assert-never/-/assert-never-1.4.0.tgz",
"integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==",
"dev": true,
"license": "MIT"
},
"node_modules/async-validator": {
"version": "4.2.5",
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
@@ -1106,19 +1117,6 @@
"proxy-from-env": "^1.1.0"
}
},
"node_modules/babel-walk": {
"version": "3.0.0-canary-5",
"resolved": "https://registry.npmmirror.com/babel-walk/-/babel-walk-3.0.0-canary-5.tgz",
"integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.9.6"
},
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1195,37 +1193,6 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
"integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.3.tgz",
"integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"get-intrinsic": "^1.2.6"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/camelcase-css": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/camelcase-css/-/camelcase-css-2.0.1.tgz",
@@ -1257,16 +1224,6 @@
],
"license": "CC-BY-4.0"
},
"node_modules/character-parser": {
"version": "2.2.0",
"resolved": "https://registry.npmmirror.com/character-parser/-/character-parser-2.2.0.tgz",
"integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-regex": "^1.0.3"
}
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz",
@@ -1339,17 +1296,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/constantinople": {
"version": "4.0.1",
"resolved": "https://registry.npmmirror.com/constantinople/-/constantinople-4.0.1.tgz",
"integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.6.0",
"@babel/types": "^7.6.1"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -1481,33 +1427,10 @@
"dev": true,
"license": "MIT"
},
"node_modules/doctypes": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/doctypes/-/doctypes-1.1.0.tgz",
"integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==",
"dev": true,
"license": "MIT"
},
"node_modules/dom-walk": {
"version": "0.1.2",
"resolved": "https://registry.npmmirror.com/dom-walk/-/dom-walk-0.1.2.tgz",
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==",
"dev": true
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
@@ -1542,39 +1465,6 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
"integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmmirror.com/es6-promise/-/es6-promise-4.2.8.tgz",
@@ -2147,31 +2037,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.2.6",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.2.6.tgz",
"integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"dunder-proto": "^1.0.0",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.0.0",
"function-bind": "^1.1.2",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmmirror.com/glob/-/glob-10.4.5.tgz",
@@ -2210,55 +2075,12 @@
"version": "4.4.0",
"resolved": "https://registry.npmmirror.com/global/-/global-4.4.0.tgz",
"integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
"dev": true,
"license": "MIT",
"dependencies": {
"min-document": "^2.19.0",
"process": "^0.11.10"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
@@ -2327,17 +2149,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-expression": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/is-expression/-/is-expression-4.0.0.tgz",
"integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^7.1.1",
"object-assign": "^4.1.1"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -2362,7 +2173,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/is-function/-/is-function-1.0.2.tgz",
"integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==",
"dev": true,
"license": "MIT"
},
"node_modules/is-glob": {
@@ -2388,32 +2198,6 @@
"node": ">=0.12.0"
}
},
"node_modules/is-promise": {
"version": "2.2.2",
"resolved": "https://registry.npmmirror.com/is-promise/-/is-promise-2.2.2.tgz",
"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
"dev": true,
"license": "MIT"
},
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz",
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"gopd": "^1.2.0",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
@@ -2447,13 +2231,6 @@
"jiti": "bin/jiti.js"
}
},
"node_modules/js-stringify": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/js-stringify/-/js-stringify-1.0.2.tgz",
"integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==",
"dev": true,
"license": "MIT"
},
"node_modules/js-tokens": {
"version": "9.0.1",
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.1.tgz",
@@ -2461,17 +2238,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/jstransformer": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/jstransformer/-/jstransformer-1.0.0.tgz",
"integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-promise": "^2.0.0",
"promise": "^7.0.1"
}
},
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.3.tgz",
@@ -2530,9 +2296,8 @@
},
"node_modules/m3u8-parser": {
"version": "7.2.0",
"resolved": "https://registry.npmmirror.com/m3u8-parser/-/m3u8-parser-7.2.0.tgz",
"resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-7.2.0.tgz",
"integrity": "sha512-CRatFqpjVtMiMaKXxNvuI3I++vUumIXVVT/JpCpdU/FynV/ceVw1qpPyyBNindL+JlPMSesx+WX1QJaZEJSaMQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.12.5",
@@ -2549,16 +2314,6 @@
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz",
@@ -2608,7 +2363,6 @@
"version": "2.19.0",
"resolved": "https://registry.npmmirror.com/min-document/-/min-document-2.19.0.tgz",
"integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==",
"dev": true,
"dependencies": {
"dom-walk": "^0.1.0"
}
@@ -2667,9 +2421,8 @@
},
"node_modules/mpd-parser": {
"version": "1.3.1",
"resolved": "https://registry.npmmirror.com/mpd-parser/-/mpd-parser-1.3.1.tgz",
"resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.3.1.tgz",
"integrity": "sha512-1FuyEWI5k2HcmhS1HkKnUAQV7yFPfXPht2DnRRGtoiiAAW+ESTbtEXIDpRkwdU+XyrQuwrIym7UkoPKsZ0SyFw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.12.5",
@@ -2697,9 +2450,8 @@
},
"node_modules/mux.js": {
"version": "7.1.0",
"resolved": "https://registry.npmmirror.com/mux.js/-/mux.js-7.1.0.tgz",
"resolved": "https://registry.npmjs.org/mux.js/-/mux.js-7.1.0.tgz",
"integrity": "sha512-NTxawK/BBELJrYsZThEulyUMDVlLizKdxyAsMuzoCD1eFj97BVaA8D/CvKsKu6FOLYkFojN5CbM9h++ZTZtknA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.11.2",
@@ -2953,9 +2705,8 @@
},
"node_modules/pkcs7": {
"version": "1.0.4",
"resolved": "https://registry.npmmirror.com/pkcs7/-/pkcs7-1.0.4.tgz",
"resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz",
"integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.5.5"
@@ -3129,164 +2880,17 @@
"version": "0.11.10",
"resolved": "https://registry.npmmirror.com/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/promise": {
"version": "7.3.1",
"resolved": "https://registry.npmmirror.com/promise/-/promise-7.3.1.tgz",
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
"dev": true,
"license": "MIT",
"dependencies": {
"asap": "~2.0.3"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/pug": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/pug/-/pug-3.0.3.tgz",
"integrity": "sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==",
"dev": true,
"license": "MIT",
"dependencies": {
"pug-code-gen": "^3.0.3",
"pug-filters": "^4.0.0",
"pug-lexer": "^5.0.1",
"pug-linker": "^4.0.0",
"pug-load": "^3.0.0",
"pug-parser": "^6.0.0",
"pug-runtime": "^3.0.1",
"pug-strip-comments": "^2.0.0"
}
},
"node_modules/pug-attrs": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/pug-attrs/-/pug-attrs-3.0.0.tgz",
"integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==",
"dev": true,
"license": "MIT",
"dependencies": {
"constantinople": "^4.0.1",
"js-stringify": "^1.0.2",
"pug-runtime": "^3.0.0"
}
},
"node_modules/pug-code-gen": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/pug-code-gen/-/pug-code-gen-3.0.3.tgz",
"integrity": "sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==",
"dev": true,
"license": "MIT",
"dependencies": {
"constantinople": "^4.0.1",
"doctypes": "^1.1.0",
"js-stringify": "^1.0.2",
"pug-attrs": "^3.0.0",
"pug-error": "^2.1.0",
"pug-runtime": "^3.0.1",
"void-elements": "^3.1.0",
"with": "^7.0.0"
}
},
"node_modules/pug-error": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/pug-error/-/pug-error-2.1.0.tgz",
"integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==",
"dev": true,
"license": "MIT"
},
"node_modules/pug-filters": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/pug-filters/-/pug-filters-4.0.0.tgz",
"integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==",
"dev": true,
"license": "MIT",
"dependencies": {
"constantinople": "^4.0.1",
"jstransformer": "1.0.0",
"pug-error": "^2.0.0",
"pug-walk": "^2.0.0",
"resolve": "^1.15.1"
}
},
"node_modules/pug-lexer": {
"version": "5.0.1",
"resolved": "https://registry.npmmirror.com/pug-lexer/-/pug-lexer-5.0.1.tgz",
"integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==",
"dev": true,
"license": "MIT",
"dependencies": {
"character-parser": "^2.2.0",
"is-expression": "^4.0.0",
"pug-error": "^2.0.0"
}
},
"node_modules/pug-linker": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/pug-linker/-/pug-linker-4.0.0.tgz",
"integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==",
"dev": true,
"license": "MIT",
"dependencies": {
"pug-error": "^2.0.0",
"pug-walk": "^2.0.0"
}
},
"node_modules/pug-load": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/pug-load/-/pug-load-3.0.0.tgz",
"integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"object-assign": "^4.1.1",
"pug-walk": "^2.0.0"
}
},
"node_modules/pug-parser": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/pug-parser/-/pug-parser-6.0.0.tgz",
"integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==",
"dev": true,
"license": "MIT",
"dependencies": {
"pug-error": "^2.0.0",
"token-stream": "1.0.0"
}
},
"node_modules/pug-runtime": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/pug-runtime/-/pug-runtime-3.0.1.tgz",
"integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==",
"dev": true,
"license": "MIT"
},
"node_modules/pug-strip-comments": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz",
"integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"pug-error": "^2.0.0"
}
},
"node_modules/pug-walk": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/pug-walk/-/pug-walk-2.0.0.tgz",
"integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==",
"dev": true,
"license": "MIT"
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -3336,7 +2940,6 @@
"version": "0.14.1",
"resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"dev": true,
"license": "MIT"
},
"node_modules/resolve": {
@@ -3822,13 +3425,6 @@
"node": ">=8.0"
}
},
"node_modules/token-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/token-stream/-/token-stream-1.0.0.tgz",
"integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==",
"dev": true,
"license": "MIT"
},
"node_modules/treemate": {
"version": "0.3.11",
"resolved": "https://registry.npmmirror.com/treemate/-/treemate-0.3.11.tgz",
@@ -4131,14 +3727,13 @@
}
},
"node_modules/video.js": {
"version": "8.21.0",
"resolved": "https://registry.npmmirror.com/video.js/-/video.js-8.21.0.tgz",
"integrity": "sha512-zcwerRb257QAuWfi8NH9yEX7vrGKFthjfcONmOQ4lxFRpDAbAi+u5LAjCjMWqhJda6zEmxkgdDpOMW3Y21QpXA==",
"dev": true,
"version": "8.22.0",
"resolved": "https://registry.npmjs.org/video.js/-/video.js-8.22.0.tgz",
"integrity": "sha512-xge2kpjsvC0zgFJ1cqt+wTqsi21+huFswlonPFh7qiplypsb4FN/D2Rz6bWdG/S9eQaPHfWHsarmJL/7D3DHoA==",
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.12.5",
"@videojs/http-streaming": "^3.16.2",
"@videojs/http-streaming": "^3.17.0",
"@videojs/vhs-utils": "^4.1.1",
"@videojs/xhr": "2.7.0",
"aes-decrypter": "^4.0.2",
@@ -4155,7 +3750,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-4.1.0.tgz",
"integrity": "sha512-TfrXJJg1Bv4t6TOCMEVMwF/CoS8iENYsWNKip8zfhB5kTcegiFYezEA0eHAJPU64ZC8NQbxQgOwAsYU8VXbOWA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"global": "^4.4.0"
@@ -4172,14 +3766,12 @@
"version": "4.2.0",
"resolved": "https://registry.npmmirror.com/videojs-font/-/videojs-font-4.2.0.tgz",
"integrity": "sha512-YPq+wiKoGy2/M7ccjmlvwi58z2xsykkkfNMyIg4xb7EZQQNwB71hcSsB3o75CqQV7/y5lXkXhI/rsGAS7jfEmQ==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/videojs-vtt.js": {
"version": "0.15.5",
"resolved": "https://registry.npmmirror.com/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz",
"integrity": "sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"global": "^4.3.1"
@@ -4235,16 +3827,6 @@
}
}
},
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/vooks": {
"version": "0.2.12",
"resolved": "https://registry.npmmirror.com/vooks/-/vooks-0.2.12.tgz",
@@ -4304,6 +3886,26 @@
}
}
},
"node_modules/vue-i18n": {
"version": "11.1.3",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.1.3.tgz",
"integrity": "sha512-Pcylh9z9S5+CJAqgbRZ3EKxFIBIrtY5YUppU722GIT65+Nukm0TCqiQegZnNLCZkXGthxe0cpqj0AoM51H+6Gw==",
"license": "MIT",
"dependencies": {
"@intlify/core-base": "11.1.3",
"@intlify/shared": "11.1.3",
"@vue/devtools-api": "^6.5.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
},
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/vue-router": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.0.tgz",
@@ -4395,22 +3997,6 @@
"node": ">= 8"
}
},
"node_modules/with": {
"version": "7.0.2",
"resolved": "https://registry.npmmirror.com/with/-/with-7.0.2.tgz",
"integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.9.6",
"@babel/types": "^7.9.6",
"assert-never": "^1.2.1",
"babel-walk": "3.0.0-canary-5"
},
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz",

View File

@@ -14,7 +14,9 @@
"flv.js": "^1.6.2",
"naive-ui": "^2.38.2",
"pinia": "^2.1.7",
"video.js": "^8.22.0",
"vue": "^3.2.37",
"vue-i18n": "^11.1.3",
"vue-router": "^4.3.3"
},
"devDependencies": {
@@ -24,14 +26,12 @@
"@vitejs/plugin-vue": "^3.0.3",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38",
"pug": "^3.0.3",
"sass": "^1.77.6",
"sass-loader": "^14.2.1",
"tailwindcss": "^3.4.4",
"typescript": "^4.6.4",
"unplugin-auto-import": "^0.18.3",
"unplugin-vue-components": "^0.27.4",
"video.js": "^8.19.1",
"vite": "^3.0.7",
"vue-tsc": "^1.8.27"
}

View File

@@ -1 +1 @@
4b201370b63f9befd97a0e875507a444
853e4a476a4f41b58875469cfb541133

View File

@@ -1,23 +1,32 @@
<template>
<NConfigProvider class="h-full" :theme="theme" :locale="zhCN">
<NConfigProvider class="h-full" :theme="theme" :locale="uiLocale">
<NaiveProvider>
<RouterView />
<RouterView/>
<ShowLoading :isLoading="loading"/>
<Password v-model:showModal="showPassword" @submit="handlePassword"/>
</NaiveProvider>
<NGlobalStyle />
<NModalProvider />
<NGlobalStyle/>
<NModalProvider/>
</NConfigProvider>
</template>
<script setup lang="ts">
import NaiveProvider from '@/components/NaiveProvider.vue'
import {darkTheme, lightTheme, zhCN} from 'naive-ui'
import {darkTheme, lightTheme, zhCN, enUS} from 'naive-ui'
import {useIndexStore} from "@/stores"
import {computed, onMounted} from "vue"
import {computed, onMounted, ref} from "vue"
import {useEventStore} from "@/stores/event"
import {appType} from "@/types/app";
import type {appType} from "@/types/app"
import appApi from "@/api/app"
import ShowLoading from "@/components/ShowLoading.vue"
import Password from "@/components/Password.vue"
import {useI18n} from 'vue-i18n'
const store = useIndexStore()
const eventStore = useEventStore()
const loading = ref(false)
const showPassword = ref(false)
const {t, locale} = useI18n()
const theme = computed(() => {
if (store.globalConfig.Theme === "darkTheme") {
@@ -28,31 +37,65 @@ const theme = computed(() => {
return lightTheme
})
const uiLocale = computed(() => {
locale.value = store.globalConfig.Locale
if (store.globalConfig.Locale === "zh") {
return zhCN
}
return enUS
})
onMounted(async () => {
await store.init()
loading.value = true
handleInstall().then((is: boolean)=>{
loading.value = false
})
eventStore.init()
eventStore.addHandle({
type: "message",
event: (res: appType.Message)=>{
event: (res: appType.Message) => {
switch (res?.code) {
case 0:
window?.$message?.error(res.message)
window.$message?.error(res.message)
break
case 1:
window?.$message?.success(res.message)
window.$message?.success(res.message)
break
}
}
})
})
eventStore.addHandle({
type: "updateProxyStatus",
event: (res: any)=>{
store.updateProxyStatus(res)
const handleInstall = async () => {
const res = await appApi.install()
if (res.code === 1) {
store.globalConfig.AutoProxy && store.openProxy()
return true
}
window.$message?.error(res.message, {duration: 5000})
if (store.envInfo.platform === 'windows' && res.message.includes('Access is denied')) {
window.$message?.error('首次启用本软件,请使用鼠标右键选择以管理员身份运行')
} else if (['darwin', 'linux'].includes(store.envInfo.platform)) {
showPassword.value = true
}
return false
}
const handlePassword = async (password: string, isCache: boolean) => {
const res = await appApi.setSystemPassword({password, isCache})
if (res.code === 0) {
window.$message?.error(res.message)
return
}
handleInstall().then((is: boolean)=>{
if (is) {
showPassword.value = false
}
})
})
</script>
<style scoped>
</style>
}
</script>

View File

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

View File

@@ -1,37 +1,39 @@
import axios from 'axios';
import type {AxiosResponse, InternalAxiosRequestConfig} from 'axios';
import type {AxiosResponse, InternalAxiosRequestConfig} from 'axios'
import axios from 'axios'
import {useIndexStore} from "@/stores";
import {computed} from "vue";
interface RequestOptions {
url: string;
method: 'get' | 'post' | 'put' | 'delete'; // 根据需要扩展
params?: Record<string, any>;
data?: Record<string, any>;
url: string
method: 'get' | 'post' | 'put' | 'delete'
params?: Record<string, any>
data?: Record<string, any>
}
const instance = axios.create({
baseURL: "/",
});
})
instance.interceptors.request.use(
(config: InternalAxiosRequestConfig<any>) => {
return config;
return config
},
(error) => {
return Promise.reject(error);
return Promise.reject(error)
}
);
)
instance.interceptors.response.use(
(response: AxiosResponse) => {
return response.data;
return response.data
},
(error) => {
return Promise.reject(error);
}
);
)
const request = ({url, method, params, data}: RequestOptions): Promise<any> => {
return instance({url, method, params, data});
};
return instance({url, method, params, data, baseURL: window.$baseUrl})
}
export default request;
export default request

View File

@@ -5,20 +5,20 @@
style="--wails-draggable:no-drag"
preset="card"
class="w-[640px]"
title="关于"
:title="t('footer.title')"
>
<div class="rounded p-5">
<div class="flex flex-col">
<div class="flex flex-row">
<div>
<div class="flex flex-row items-center">
<div class="flex flex-row items-end">
<div class="text-4xl font-bold">{{ store.appInfo.AppName }}</div>
<div class="text-xs pl-5 text-slate-400">
Version {{ store.appInfo.Version }}
</div>
</div>
<div class="text-slate-400 w-80 pt-2 pb-4">
{{ store.appInfo.Description }}
{{ t('footer.description') }}
</div>
</div>
<div class="pl-8">
@@ -28,29 +28,20 @@
</div>
<div class="flex flex-col">
<div class="text-2xl font-bold text-emerald-600">
支持市面上几乎所有的网络应用
{{ t('footer.support') }}
</div>
<div class="grid grid-cols-5 gap-2 text-sm m-4 text-slate-400">
<span>抖音</span>
<span>快手</span>
<span>小红书</span>
<span>视频号</span>
<span>小程序</span>
<span>公众号</span>
<span>酷狗音乐</span>
<span>QQ音乐</span>
<span>QQ微视</span>
<span>......</span>
<span v-for="item in t('footer.application').split(',')">{{ item }}</span>
</div>
</div>
<div class="flex w-full text-sm justify-between pt-8 text-slate-400">
<div>{{ store.appInfo.Copyright }}</div>
<div class="flex">
<button class="pl-4" @click="toWebsite('https://s.gowas.cn/d/4089')">论坛</button>
<button class="pl-4" @click="toWebsite('http://127.0.0.1:8899/cert')">证书</button>
<button class="pl-4" @click="toWebsite('https://github.com/putyy/res-downloader')">软件源码</button>
<button class="pl-4" @click="toWebsite('https://github.com/putyy/res-downloader/issues')">帮助支持</button>
<button class="pl-4" @click="toWebsite('https://github.com/putyy/res-downloader/releases')">更新日志</button>
<button class="pl-4" @click="toWebsite('https://s.gowas.cn/d/4089')">{{ t('footer.forum') }}</button>
<button class="pl-4" @click="toWebsite(certUrl)">{{ t('footer.cert') }}</button>
<button class="pl-4" @click="toWebsite('https://github.com/putyy/res-downloader')">{{ t('footer.source_code') }}</button>
<button class="pl-4" @click="toWebsite('https://github.com/putyy/res-downloader/issues')">{{ t('footer.help') }}</button>
<button class="pl-4" @click="toWebsite('https://github.com/putyy/res-downloader/releases')">{{ t('footer.update_log') }}</button>
</div>
</div>
</div>
@@ -60,10 +51,16 @@
<script lang="ts" setup>
import {useIndexStore} from "@/stores"
import {BrowserOpenURL} from "../../wailsjs/runtime"
import {computed} from "vue"
import {useI18n} from 'vue-i18n'
const {t} = useI18n()
const store = useIndexStore()
const props = defineProps(["showModal"])
const emits = defineEmits(["update:showModal"])
const certUrl = computed(()=>{
return store.baseUrl + "/api/cert"
})
const changeShow = (value: boolean) => {
emits('update:showModal', value)
}

View File

@@ -5,7 +5,7 @@
style="--wails-draggable:no-drag"
preset="card"
class="w-[640px]"
title="导入数据"
:title="t('index.batch_import')"
>
<NForm
size="medium"
@@ -15,17 +15,19 @@
style="--wails-draggable:no-drag"
>
<NFormItem>
<NInput type="textarea" v-model:value="content" rows="8" :autosize="false" placeholder="添加多个时,请确保每行只有一个(每个链接回车换行)"></NInput>
<NInput type="textarea" v-model:value="content" rows="8" :autosize="false" :placeholder="t('index.import_placeholder')"></NInput>
</NFormItem>
<NFormItem>
<NButton strong secondary type="success" @click="emits('submit', content)" class="w-20">提交</NButton>
<NButton strong secondary type="success" @click="emits('submit', content)" class="w-20">{{ t('common.submit') }}</NButton>
</NFormItem>
</NForm>
</NModal>
</template>
<script setup lang="ts">
import {ref} from "vue"
import {useI18n} from 'vue-i18n'
const {t} = useI18n()
const content = ref("")
const props = defineProps<{
showModal: boolean

View File

@@ -19,7 +19,6 @@ import {
useNotification,
} from "naive-ui"
function registerNaiveTools() {
window.$loadingBar = useLoadingBar()
window.$dialog = useDialog()

View File

@@ -0,0 +1,63 @@
<template>
<n-modal
:show="showModal"
:on-update:show="changeShow"
style="--wails-draggable:no-drag"
preset="dialog"
:title="t('components.password_title')"
content=""
:show-icon="false"
:mask-closable="false"
:close-on-esc="false"
class="rounded-lg"
>
<div>
<div class="text-red-500 text-base">
{{ t("components.password_tip") }}
</div>
<div class="mt-3">
<n-input
v-model:value="formValue.password"
type="password"
:placeholder="t('components.password_placeholder')"
class="w-full"
/>
</div>
<div class="mt-3 text-base">
<label>{{ t("components.password_cache") }}</label>
<NSwitch class="pl-1" v-model:value="formValue.cache" :aria-placeholder="t('components.password_cache')"/>
</div>
</div>
<template #action>
<n-button type="primary" @click="submit">{{ t("common.submit") }}</n-button>
</template>
</n-modal>
</template>
<script setup lang="ts">
import {reactive} from 'vue'
import {NButton, NInput, NModal} from 'naive-ui'
import {useI18n} from 'vue-i18n'
const {t} = useI18n()
defineProps({
showModal: Boolean,
})
const formValue = reactive({
password: "",
cache: false,
})
const emits = defineEmits(["update:showModal", "submit"])
const changeShow = (value: boolean) => emits("update:showModal", value)
const submit = () => {
if (!formValue.password) {
window.$message?.error(t("components.password_empty"))
return
}
emits('submit', formValue.password, formValue.cache)
}
</script>

View File

@@ -5,7 +5,7 @@
:on-update:show="changeShow"
preset="card"
class="w-[540px] h-auto"
title="预览"
:title="t('index.preview')"
display-directive="show"
:on-after-enter="onAfterEnter"
:on-after-leave="onAfterLeave"
@@ -30,7 +30,9 @@ import axios from "axios"
// @ts-ignore
import { getDecryptionArray } from '@/assets/js/decrypt.js'
import type Player from "video.js/dist/types/player"
import {useI18n} from 'vue-i18n'
const {t} = useI18n()
const videoPlayer = ref<HTMLElement | any>(null)
let player: Player | null = null
let flvPlayer: flvjs.Player | null = null

View File

@@ -1,29 +1,30 @@
<template>
<NSpace style="--wails-draggable:no-drag">
<NButton v-if="row.Classify != 'live' && row.Classify != 'm3u8'" type="success" :tertiary="true" size="small" @click="action('down')">
直接下载
{{ t("index.direct_download") }}
</NButton>
<NButton type="info" :tertiary="true" size="small" @click="action('copy')">
复制链接
{{ t("index.copy_link") }}
</NButton>
<NButton v-if="row.Classify != 'live' && row.Classify != 'm3u8'" type="info" :tertiary="true" size="small" @click="action('open')">
打开浏览
{{ t("index.open_link") }}
</NButton>
<NButton v-if="row.DecodeKey" type="warning" :tertiary="true" size="small" @click="action('decode')">
视频解密
{{ t("index.video_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')">
{{ t("index.copy_data") }}
</NButton>
<NButton type="error" :tertiary="true" size="small" @click="action('delete')">
删除
{{ t("common.delete") }}
</NButton>
</NSpace>
</template>
<script setup lang="ts">
import {inject} from "vue"
import {useI18n} from 'vue-i18n'
const {t} = useI18n()
const props = defineProps<{
row: any,
index: number,
@@ -31,8 +32,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

@@ -1,21 +1,32 @@
<template>
<div class="flex justify-around px-2 pt-3">
<button class="w-3 h-3 rounded-full flex items-center justify-center p-[2px] bg-[#fd5b5b]" @click="closeWindow">
<svg t="1729232659670" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11784" width="200" height="200">
<path d="M617.92 516.096l272 272-101.824 101.824-272-272-272 272-101.856-101.824 272-272-275.008-275.04L241.056 139.2l275.04 275.04 275.04-275.04 101.824 101.824-275.04 275.04z" fill="#2c2c2c" p-id="11785"/>
<button
class="w-4 h-4 rounded-full bg-[#ff5c57] flex items-center justify-center transition-all duration-200 hover:bg-[#e0443e] group"
@click="closeWindow"
:title="t('components.screen_close')"
>
<svg class="w-[0.7rem] h-[0.7rem] text-[#2c2c2c] opacity-0 group-hover:opacity-100 transition-opacity duration-150" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 6L18 18M6 18L18 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</button>
<button class="w-3 h-3 rounded-full bg-yellow-500 flex items-center justify-center p-[2px]" @click="minimizeWindow">
<svg t="1729223652686" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9954" width="200" height="200">
<path d="M892 472c22.1 0 40 17.9 40 40 0 21.9-17.6 39.6-39.3 40H132c-22.1 0-40-17.9-40-40 0-21.9 17.6-39.6 39.3-40H892z" fill="#111111" p-id="9955"/>
<path d="M892 472c22.1 0 40 17.9 40 40 0 21.9-17.6 39.6-39.3 40H132c-22.1 0-40-17.9-40-40 0-21.9 17.6-39.6 39.3-40H892z" fill="#111111" p-id="9956"/>
<button
class="w-4 h-4 rounded-full bg-[#ffbc38] flex items-center justify-center transition-all duration-200 hover:bg-[#e0a824] group"
@click="minimizeWindow"
:title="t('components.screen_minimize')"
>
<svg class="w-[0.7rem] h-[0.7rem] text-[#2c2c2c] opacity-0 group-hover:opacity-100 transition-opacity duration-150" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 12H20" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</button>
<button class="w-3 h-3 rounded-full bg-green-500 flex items-center justify-center" :class="isMaximized ? 'p-[2px]' : 'p-[3px]'" @click="maximizeWindow">
<svg v-if="isMaximized" t="1729223337944" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2168" width="200" height="200">
<button
class="w-4 h-4 rounded-full bg-[#28c840] flex items-center justify-center transition-all duration-200 hover:bg-[#1ea230] group"
@click="maximizeWindow"
:title="isMaximized ? t('components.screen_restore') : t('components.screen_maximize')"
>
<svg v-if="isMaximized" class="w-[0.5rem] h-[0.5rem] text-[#2c2c2c] opacity-0 group-hover:opacity-100 transition-opacity duration-150" t="1729223337944" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2168">
<path d="M965.6 512.8L564 512c-28.8 0-52 23.2-52 51.2l1.6 401.6c0 28.8 16.8 35.2 36.8 15.2l430.4-431.2c20-20 12.8-36-15.2-36zM510.4 58.4c0-28.8-16.8-35.2-36.8-15.2L44 474.4c-20 20-12.8 36.8 15.2 36.8l401.6 0.8c28.8 0 51.2-23.2 51.2-51.2l-1.6-402.4z" fill="#2c2c2c" p-id="2169"/>
</svg>
<svg v-else t="1729223289826" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1881" width="200" height="200">
<svg v-else class="w-[0.5rem] h-[0.5rem] text-[#2c2c2c] opacity-0 group-hover:opacity-100 transition-opacity duration-150" t="1729223289826" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1881">
<path d="M966.2236448 410.53297813l0 496.32142187a56.61582187 56.61582187 0 0 1-56.59306667 56.61582187l-496.32142293-1e-8a56.59306667 56.59306667 0 0 1-39.9815104-96.68835519L869.53528854 370.57422187a56.61582187 56.61582187 0 0 1 96.71111146 39.95875626z m-905.67111147 200.15786666L60.55253333 114.39217812a56.61582187 56.61582187 0 0 1 56.59306667-56.61582292l496.29866667 0a56.59306667 56.59306667 0 0 1 39.98151146 96.68835626l-496.2076448 496.20764374a56.61582187 56.61582187 0 0 1-96.68835519-39.9815104z" fill="#373C43" p-id="1882"/>
</svg>
</button>
@@ -25,8 +36,10 @@
<script lang="ts" setup>
import {ref} from "vue"
import {Quit, WindowFullscreen, WindowMinimise, WindowUnfullscreen} from "../../wailsjs/runtime"
import {useI18n} from 'vue-i18n'
const isMaximized = ref(false);
const {t} = useI18n()
const isMaximized = ref(false)
const closeWindow = () => {
Quit()

View File

@@ -1,15 +1,19 @@
<template>
<div class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50" v-if="isLoading">
<div class="flex flex-col items-center">
<svg class="animate-spin h-10 w-10 text-white mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<svg class="animate-spin h-10 w-10 text-white mb-4" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"></path>
</svg>
<span class="text-white">{{ loadingText }}</span>
<span class="text-white" v-if="loadingText">{{ loadingText }}</span>
</div>
</div>
</template>
<script lang="ts" setup>
const props = defineProps(["isLoading", "loadingText"])
defineProps<{
isLoading: boolean
loadingText?: string
}>()
</script>

View File

@@ -1,10 +1,10 @@
<template>
<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"/>
<div class="w-full flex flex-row items-center justify-center pt-5" :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" @click="handleFooterUpdate('github')"/>
</div>
<main class="flex-1 flex-grow-1 mb-5 overflow-auto flex flex-col pt-1 items-center h-full">
<main class="flex-1 flex-grow-1 mb-5 overflow-auto flex flex-col pt-1 items-center h-full" v-if="is">
<NScrollbar :size="1">
<NLayout has-sider>
<NLayoutSider
@@ -14,10 +14,11 @@
:on-after-enter="() => { showAppName = true }"
:on-after-leave="() => { showAppName = false }"
:collapsed-width="70"
:default-collapsed="true"
:width="120"
:collapsed="collapsed"
:width="140"
:native-scrollbar="false"
:inverted="inverted"
:on-update:collapsed="collapsedChange"
class="bg-inherit"
>
<NMenu
@@ -43,36 +44,43 @@
</NScrollbar>
</main>
</div>
<Footer v-model:showModal="showAppInfo" />
<Footer v-model:showModal="showAppInfo"/>
</template>
<script lang="ts" setup>
import {MenuOption} from "naive-ui"
import {NIcon} from "naive-ui"
import {computed, h, ref, watch} from "vue"
import {computed, h, onMounted, ref, watch} from "vue"
import {useRoute, useRouter} from "vue-router"
import {
CloudOutline,
SettingsOutline,
HelpCircleOutline,
MoonOutline, SunnyOutline, LogoGithub
MoonOutline,
SunnyOutline,
LanguageSharp,
LogoGithub
} from "@vicons/ionicons5"
import {useIndexStore} from "@/stores"
import Footer from "@/components/Footer.vue"
import Screen from "@/components/Screen.vue";
import {BrowserOpenURL} from "../../../wailsjs/runtime";
import Screen from "@/components/Screen.vue"
import {BrowserOpenURL} from "../../../wailsjs/runtime"
import {useI18n} from "vue-i18n"
const {t} = useI18n()
const route = useRoute()
const router = useRouter()
const inverted = ref(false)
const collapsed = ref(false)
const showAppName = ref(false)
const showAppInfo = ref(false)
const menuValue = ref(route.fullPath.substring(1))
const store = useIndexStore()
const is = ref(false)
const envInfo = store.envInfo
const globalConfig = computed(()=>{
const globalConfig = computed(() => {
return store.globalConfig
})
@@ -82,7 +90,15 @@ const theme = computed(() => {
watch(() => route.path, (newPath, oldPath) => {
menuValue.value = route.fullPath.substring(1)
});
})
onMounted(()=>{
const collapsedCache = localStorage.getItem("collapsed");
if (collapsedCache) {
collapsed.value = JSON.parse(collapsedCache).collapsed
}
is.value = true
})
const renderIcon = (icon: any) => {
return () => h(NIcon, null, {default: () => h(icon)})
@@ -90,40 +106,45 @@ const renderIcon = (icon: any) => {
const menuOptions = ref([
{
label: "拦截",
label: computed(() => t("menu.index")),
key: 'index',
icon: renderIcon(CloudOutline),
},
{
label: "设置",
label: computed(() => t("menu.setting")),
key: 'setting',
icon: renderIcon(SettingsOutline),
},
])
const footerOptions = ref([
{
label: "主题",
key: 'theme',
icon: theme,
},
{
label: "github",
key: 'github',
icon: renderIcon(LogoGithub),
},
{
label: "关于",
label: computed(() => t("menu.locale")),
key: 'locale',
icon: renderIcon(LanguageSharp),
},
{
label: computed(() => t("menu.theme")),
key: 'theme',
icon: theme,
},
{
label: computed(() => t("menu.about")),
key: 'about',
icon: renderIcon(HelpCircleOutline),
},
])
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
@@ -133,17 +154,33 @@ const handleFooterUpdate = (key: string, item: MenuOption) => {
BrowserOpenURL("https://github.com/putyy/res-downloader")
return
}
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
}
if (key === "locale") {
if (globalConfig.value.Locale === "zh") {
store.setConfig({Locale: "en"})
return
}
store.setConfig({Locale: "zh"})
return
}
menuValue.value = key
return router.push({path: "/" + key})
}
const collapsedChange = (value: boolean)=>{
console.log("collapsedChange",value)
collapsed.value = value
localStorage.setItem("collapsed", JSON.stringify({collapsed: value}))
}
</script>

14
frontend/src/i18n.ts Normal file
View File

@@ -0,0 +1,14 @@
import {createI18n} from 'vue-i18n'
import en from './locales/en.json'
import zh from './locales/zh.json'
const i18n = createI18n({
locale: 'zh',
fallbackLocale: 'en',
messages: {
en,
zh
}
})
export default i18n

View File

@@ -0,0 +1,113 @@
{
"common": {
"copy_success": "Copy Success",
"copy_fail": "Copy Failed",
"file_path": "File Path",
"loading": "Loading",
"select": "Select",
"submit": "Submit",
"delete": "Delete",
"yes": "Yes",
"no": "No"
},
"components": {
"password_title": "Admin Authorization",
"password_tip": "The password entered this time is only valid during this session, used for installing certificates or setting system proxy!",
"password_placeholder": "Please enter your computer password",
"password_cache": "Cache Password",
"password_empty": "Password cannot be empty",
"screen_minimize": "Minimize",
"screen_restore": "Restore",
"screen_maximize": "Maximize",
"screen_close": "Close"
},
"menu": {
"index": "Intercept",
"setting": "Setting",
"locale": "中文",
"theme": "Theme",
"about": "About"
},
"index": {
"open_grab": "Start Grabbing",
"close_grab": "Stop Grabbing",
"grab_type": "Grab Type",
"clear_list": "Clear List",
"batch_download": "Batch Download",
"batch_export": "Batch Export",
"batch_import": "Batch Import",
"import_success": "Export Success",
"all": "All",
"image": "Image",
"audio": "Audio",
"video": "Video",
"m3u8": "M3U8",
"live": "Live Stream",
"xls": "Spreadsheet",
"doc": "Document",
"pdf": "PDF",
"font": "Font",
"domain": "Domain",
"type": "Type",
"preview": "Preview",
"preview_tip": "Preview not supported",
"status": "Status",
"description": "Description",
"resource_size": "Resource Size",
"save_path": "Save Path",
"save_path_empty": "Please set save location",
"operation": "Operation",
"ready": "Ready",
"running": "Running",
"error": "Error",
"done": "Done",
"handle": "Post Processing",
"direct_download": "Download",
"download_success": "Download Success",
"copy_link": "Copy Link",
"copy_data": "Copy Data",
"open_link": "Open Link",
"open_file": "Open File",
"video_decode": "WxDecrypt",
"video_decode_loading": "Decrypting",
"video_decode_no": "Cannot Decrypt",
"video_decode_success": "Decrypt Success",
"use_data": "Please select required data",
"import_placeholder": "When adding multiple items, ensure each line contains only one (each link on a new line)",
"import_empty": "Please enter data to import"
},
"setting": {
"restart_tip": "Keep default if unsure, please restart software after modification",
"save_dir": "Save Directory",
"filename_rules": "Filename Rules",
"filename_rules_tip": "Input controls filename length (excluding timestamp, 0 means invalid, effective when description exists), switch controls whether to add timestamp at filename end",
"auto_proxy": "Auto Intercept",
"auto_proxy_tip": "Enable intercept when software starts",
"quality": "Quality",
"quality_value": "Default(Recommended),Ultra HD,High Quality,Medium Quality,Low Quality",
"quality_tip": "Effective for video accounts",
"full_intercept": "Full Intercept",
"full_intercept_tip": "Whether to fully intercept WeChat video accounts, No: only intercept video details",
"upstream_proxy": "Upstream Proxy",
"upstream_proxy_tip": "For combining with other proxy tools, format: http://username:password@your.proxy.server:port",
"download_proxy": "Download Proxy",
"download_proxy_tip": "Whether to use upstream proxy for downloads",
"user_agent_tip": "Keep default if unsure",
"connections": "Connections",
"connections_tip": "Keep default if unsure, usually CPU cores * 2, for faster downloads",
"use_headers_tip": "Define headers for downloads, comma separated",
"mime_map": "Intercept Rules",
"mime_map_tip": "JSON format, keep default if unsure"
},
"footer": {
"title": "About Us",
"description": "A software combining network resource sniffing + high-speed download functions, with high aesthetics, performance and diversity, providing personal users with the ability to download their own resources uploaded to various platforms!",
"support": "Supports almost all network applications on the market",
"application": "Douyin,Kuaishou,Xiaohongshu,Wechat,Mini Programs,Youtube,Kugou Music,QQ Music,QQ Weishi,......",
"forum": "Forum",
"cert": "Certificate",
"source_code": "Source Code",
"help": "Issues",
"update_log": "Update Log"
}
}

View File

@@ -0,0 +1,113 @@
{
"common": {
"copy_success": "复制成功",
"copy_fail": "复制失败",
"file_path": "文件路径",
"loading": "loading",
"select": "选择",
"submit": "提交",
"delete": "删除",
"yes": "是",
"no": "否"
},
"components": {
"password_title": "管理员授权",
"password_tip": "本次输入的密码仅在本次运行期间有效,用于安装证书或设置系统代理!",
"password_placeholder": "请输入你的电脑密码",
"password_cache": "缓存密码",
"password_empty": "密码不能为空",
"screen_minimize": "最小化",
"screen_restore": "还原",
"screen_maximize": "最大化",
"screen_close": "关闭"
},
"menu": {
"index": "获取资源",
"setting": "系统设置",
"locale": "English",
"theme": "主题更换",
"about": "关于我们"
},
"index": {
"open_grab": "开启抓取",
"close_grab": "关闭抓取",
"grab_type": "抓取类型",
"clear_list": "清空列表",
"batch_download": "批量下载",
"batch_export": "批量导出",
"batch_import": "批量导入",
"import_success": "导出成功",
"all": "全部",
"image": "图片",
"audio": "音频",
"video": "视频",
"m3u8": "m3u8",
"live": "直播流",
"xls": "表格",
"doc": "文档",
"pdf": "pdf",
"font": "字体",
"domain": "域",
"type": "类型",
"preview": "预览",
"preview_tip": "暂不支持预览",
"status": "状态",
"description": "描述",
"resource_size": "资源大小",
"save_path": "保存路径",
"save_path_empty": "请设置保存位置",
"operation": "操作",
"ready": "就绪",
"running": "运行中",
"error": "错误",
"done": "完成",
"handle": "后续处理",
"direct_download": "直接下载",
"download_success": "下载成功",
"copy_link": "复制链接",
"copy_data": "复制数据",
"open_link": "打开链接",
"open_file": "打开文件",
"video_decode": "视频解密",
"video_decode_loading": "解密中",
"video_decode_no": "无法解密",
"video_decode_success": "解密成功",
"use_data": "请选择需要的数据",
"import_placeholder": "添加多个时,请确保每行只有一个(每个链接回车换行)",
"import_empty": "请输入需要导入的数据"
},
"setting": {
"restart_tip": "如果不清楚保持默认就行,修改后请重启软件",
"save_dir": "保存目录",
"filename_rules": "文件命名",
"filename_rules_tip": "输入框控制文件命名的长度(不含时间、0为无效此选项有描述信息时有效),开关控制文件末尾是否添加时间标识",
"auto_proxy": "自动拦截",
"auto_proxy_tip": "打开软件时动启用拦截",
"quality": "清晰度",
"quality_value": "默认(推荐),超清,高画质,中画质,低画质",
"quality_tip": "视频号有效",
"full_intercept": "全量拦截",
"full_intercept_tip": "微信视频号是否全量拦截,否:只拦截视频详情",
"upstream_proxy": "上游代理",
"upstream_proxy_tip": "用于结合其他代理工具,格式: http://username:password@your.proxy.server:port",
"download_proxy": "下载代理",
"download_proxy_tip": "下载操作时是否使用上游代理",
"user_agent_tip": "如不清楚请保持默认",
"connections": "连接数",
"connections_tip": "如不清楚请保持默认通常CPU核心数*2用于加速下载",
"use_headers_tip": "定义下载时可使用的header参数逗号分割",
"mime_map": "拦截规则",
"mime_map_tip": "json格式如果不清楚保持默认就行"
},
"footer": {
"title": "关于我们",
"description": "一款集网络资源嗅探 + 高速下载功能于一体的软件,高颜值、高性能和多样化,提供个人用户下载自己上传到各大平台的网络资源功能!",
"support": "支持市面上几乎所有的网络应用",
"application": "抖音,快手,小红书,视频号,小程序,公众号,酷狗音乐,QQ音乐,QQ微视,......",
"forum": "论坛",
"cert": "证书",
"source_code": "软件源码",
"help": "帮助支持",
"update_log": "更新日志"
}
}

View File

@@ -1,14 +1,14 @@
import './assets/css/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import {createApp} from 'vue'
import {createPinia} from 'pinia'
import i18n from './i18n'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')
createApp(App)
.use(router)
.use(i18n)
.use(createPinia())
.mount('#app')

View File

@@ -16,7 +16,7 @@ const routes = [
{
path: "/setting",
name: "setting",
meta: {keepAlive: true},
meta: {keepAlive: false},
component: () => import("@/views/setting.vue"),
},
]

View File

@@ -6,7 +6,7 @@ import {appType} from "@/types/app"
export const useEventStore = defineStore('ws-store', () => {
const handles = ref<any>({})
const init = ()=>{
const init = () => {
EventsOn("event", (res: any) => {
const data = JSON.parse(res)
if (handles.value.hasOwnProperty(data.type)) {

View File

@@ -3,6 +3,8 @@ import {ref} from "vue"
import type {appType} from "@/types/app"
import appApi from "@/api/app"
import {Environment} from "../../wailsjs/runtime"
import * as bind from "../../wailsjs/go/core/Bind"
import {core} from "../../wailsjs/go/models"
export const useIndexStore = defineStore("index-store", () => {
const appInfo = ref<appType.App>({
@@ -14,6 +16,7 @@ export const useIndexStore = defineStore("index-store", () => {
const globalConfig = ref<appType.Config>({
Theme: "lightTheme",
Locale: "zh",
Host: "0.0.0.0",
Port: "8899",
Quality: 0,
@@ -27,6 +30,8 @@ export const useIndexStore = defineStore("index-store", () => {
WxAction: false,
TaskNumber: 8,
UserAgent: "",
UseHeaders: "",
MimeMap: {}
})
const envInfo = ref({
@@ -38,31 +43,29 @@ export const useIndexStore = defineStore("index-store", () => {
const tableHeight = ref(800)
const isProxy = ref(false)
const baseUrl = ref("")
const init = async () => {
Environment().then((res) => {
envInfo.value = res
})
await getAppInfo()
await appApi.getConfig().then((res) => {
await bind.AppInfo().then((res: core.ResponseData)=>{
appInfo.value = Object.assign({}, appInfo.value, res.data)
isProxy.value = res.data.IsProxy
})
await bind.Config().then((res: core.ResponseData)=>{
globalConfig.value = Object.assign({}, globalConfig.value, res.data)
})
setTimeout(() => {
appApi.isProxy().then((res: any) => {
isProxy.value = res.data.isProxy
})
}, 150)
baseUrl.value = "http://"+globalConfig.value.Host + ":" +globalConfig.value.Port
window.$baseUrl = baseUrl.value
window.addEventListener("resize", handleResize);
handleResize()
}
const getAppInfo = async () => {
await appApi.appInfo().then((res) => {
appInfo.value = Object.assign({}, appInfo.value, res.data)
})
}
const setConfig = (formValue: appType.Config) => {
const setConfig = (formValue: Object) => {
globalConfig.value = Object.assign({}, globalConfig.value, formValue)
appApi.setConfig(globalConfig.value)
}
@@ -71,9 +74,32 @@ export const useIndexStore = defineStore("index-store", () => {
tableHeight.value = document.documentElement.clientHeight || window.innerHeight
}
const updateProxyStatus = (res: any) => {
isProxy.value = res.isProxy
const openProxy = async () => {
return appApi.openSystemProxy().then(handleProxy)
}
return {appInfo, globalConfig, tableHeight, isProxy, envInfo, init, getAppInfo, setConfig, updateProxyStatus}
const unsetProxy = async () => {
return appApi.unsetSystemProxy().then(handleProxy)
}
const handleProxy = (res: appType.Res) => {
isProxy.value = res.data.value
if (res.code === 0) {
window?.$message?.error(res.message)
}
return res
}
return {
appInfo,
globalConfig,
tableHeight,
isProxy,
envInfo,
baseUrl,
init,
setConfig,
openProxy,
unsetProxy
}
})

View File

@@ -6,8 +6,14 @@ export namespace appType {
Copyright: string
}
interface MimeMap {
Type: string
Suffix: string
}
interface Config {
Theme: string
Locale: string
Host: string
Port: string
Quality: number
@@ -21,6 +27,8 @@ export namespace appType {
WxAction: boolean
TaskNumber: number
UserAgent: string
UseHeaders: string
MimeMap: { [key: string]: MimeMap }
}
interface MediaInfo {
@@ -37,7 +45,7 @@ export namespace appType {
DecodeKey: string
Description: string
ContentType: string
OtherData: {[key: string]: string}
OtherData: { [key: string]: string }
}
interface DownloadProgress {
@@ -56,4 +64,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,9 +1,9 @@
interface Window {
$loadingBar?: import('naive-ui').LoadingBarProviderInst;
$dialog?: import('naive-ui').DialogProviderInst;
$message?: import('naive-ui').MessageProviderInst;
$notification?: import('naive-ui').NotificationProviderInst;
$ws?: WebSocket;
$loadingBar?: import('naive-ui').LoadingBarProviderInst
$dialog?: import('naive-ui').DialogProviderInst
$message?: import('naive-ui').MessageProviderInst
$notification?: import('naive-ui').NotificationProviderInst
$baseUrl?: string
}
declare module '*.vue' {

View File

@@ -1,13 +1,27 @@
<template>
<div class="flex flex-col px-5 py-5">
<div class="pb-2 z-40" @click="triggerEvent">
<div class="h-full flex flex-col p-5 overflow-y-auto [&::-webkit-scrollbar]:hidden">
<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>
<NButton v-if="isProxy" secondary type="primary" @click.stop="close" style="--wails-draggable:no-drag">
{{ t("index.close_grab") }}
</NButton>
<NButton v-else tertiary type="tertiary" @click.stop="open" style="--wails-draggable:no-drag">
{{ t("index.open_grab") }}
</NButton>
<NButton tertiary type="error" @click.stop="clear" style="--wails-draggable:no-drag">
{{ t("index.clear_list") }}
</NButton>
<NSelect style="min-width: 100px;--wails-draggable:no-drag" :placeholder="t('index.grab_type')"
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">
{{ t("index.batch_download") }}
</NButton>
<NButton tertiary type="info" @click.stop="batchImport" style="--wails-draggable:no-drag">
{{ t("index.batch_export") }}
</NButton>
<NButton tertiary type="info" @click.stop="showImport=true" style="--wails-draggable:no-drag">
{{ t("index.batch_import") }}
</NButton>
</NSpace>
</div>
<div class="flex-1">
@@ -22,17 +36,19 @@
:height-for-row="()=> 48"
:checked-row-keys="checkedRowKeysValue"
@update:checked-row-keys="handleCheck"
style="--wails-draggable:no-drag"
/>
</div>
<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, reactive, watch} from "vue"
import type {appType} from "@/types/app"
import type {DataTableRowKey, ImageRenderToolbarProps} from "naive-ui"
@@ -42,12 +58,14 @@ import ShowLoading from "@/components/ShowLoading.vue"
import {getDecryptionArray} from '@/assets/js/decrypt.js'
import {useIndexStore} from "@/stores"
import appApi from "@/api/app"
import {DwStatus} from "@/const"
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"
import {useI18n} from 'vue-i18n'
const {t} = useI18n()
const eventStore = useEventStore()
const isProxy = computed(() => {
return store.isProxy
@@ -55,67 +73,66 @@ const isProxy = computed(() => {
const data = ref<any[]>([])
const store = useIndexStore()
const tableHeight = computed(() => {
return store.tableHeight - 132
return store.globalConfig.Locale === "zh" ? store.tableHeight - 130 : store.tableHeight - 151
})
const resourcesType = ref<string[]>(["all"])
const options = [
const classifyAlias: { [key: string]: any } = {
image: computed(() => t("index.image")),
audio: computed(() => t("index.audio")),
video: computed(() => t("index.video")),
m3u8: computed(() => t("index.m3u8")),
live: computed(() => t("index.live")),
xls: computed(() => t("index.xls")),
doc: computed(() => t("index.pdf")),
pdf: computed(() => t("index.pdf")),
font: computed(() => t("index.font"))
}
const dwStatus = computed<any>(() => {
return {
ready: t("common.ready"),
running: t("common.running"),
error: t("common.error"),
done: t("common.done"),
handle: t("common.handle")
}
})
const classify = ref([
{
value: "all",
label: "全部",
label: computed(() => t("index.all")),
},
{
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",
},
{
title: "",
title: computed(() => t("index.domain")),
key: "Domain",
},
{
title: "类型",
title: computed(() => t("index.type")),
key: "Classify",
filterOptions: Array.from(options).slice(1),
filterOptions: computed(() => 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;
}
},
{
title: "预览",
title: computed(() => t("index.preview")),
key: "Url",
width: 120,
render: (row: appType.MediaInfo) => {
@@ -148,9 +165,9 @@ const columns = ref<any[]>([
{
default: () => {
if (row.Classify === "audio" || row.Classify === "video" || row.Classify === "m3u8" || row.Classify === "live") {
return "预览"
return t("index.preview")
}
return "暂不支持预览"
return t("index.preview_tip")
}
}
),
@@ -158,14 +175,14 @@ const columns = ref<any[]>([
}
},
{
title: "状态",
title: computed(() => t("index.status")),
key: "Status",
render: (row: appType.MediaInfo) => {
return DwStatus[row.Status as keyof typeof DwStatus]
return dwStatus[row.Status as keyof typeof dwStatus]
}
},
{
title: "描述",
title: computed(() => t("index.description")),
key: "Description",
width: 150,
render: (row: appType.MediaInfo, index: number) => {
@@ -182,11 +199,11 @@ const columns = ref<any[]>([
}
},
{
title: "资源大小",
title: computed(() => t("index.resource_size")),
key: "Size"
},
{
title: "保存路径",
title: computed(() => t("index.save_path")),
key: "SavePath",
render(row: appType.MediaInfo, index: number) {
return h("a",
@@ -206,7 +223,7 @@ const columns = ref<any[]>([
}
},
{
title: "操作",
title: computed(() => t("index.operation")),
key: "actions",
render(row: appType.MediaInfo, index: number) {
return h(ResAction, {key: index, row: row, index: index, onAction: dataAction})
@@ -219,14 +236,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
@@ -270,7 +285,7 @@ onMounted(() => {
}
}
localStorage.setItem("resources-data", JSON.stringify(data.value))
window?.$message?.success("下载成功")
window?.$message?.success(t("index.download_success"))
break;
case "error":
loading.value = false
@@ -281,11 +296,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: computed(() => t("index.all"))},
...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":
@@ -294,18 +333,18 @@ const dataAction = (row: appType.MediaInfo, index: number, type: string) => {
case "copy":
ClipboardSetText(row.Url).then((is: boolean) => {
if (is) {
window?.$message?.success("复制成功")
window?.$message?.success(t("common.copy_success"))
} else {
window?.$message?.error("复制失败")
window?.$message?.error(t("common.copy_fail"))
}
})
break
case "json":
ClipboardSetText(encodeURIComponent(JSON.stringify(row))).then((is: boolean) => {
if (is) {
window?.$message?.success("复制成功")
window?.$message?.success(t("common.copy_success"))
} else {
window?.$message?.error("复制失败")
window?.$message?.error(t("common.copy_fail"))
}
})
break
@@ -350,7 +389,7 @@ const batchDown = async () => {
return
}
if (!store.globalConfig.SaveDirectory) {
window?.$message?.error("请设置保存位置")
window?.$message?.error(t("index.save_path_empty"))
return
}
for (let i = 0; i < data.value.length; i++) {
@@ -361,6 +400,35 @@ const batchDown = async () => {
}
}
const batchImport = () => {
if (checkedRowKeysValue.value.length <= 0) {
window?.$message?.error(t("index.use_data"))
return
}
if (!store.globalConfig.SaveDirectory) {
window?.$message?.error(t("index.save_path_empty"))
return
}
loadingText.value = t("common.loading")
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(t("index.import_success"))
window?.$message?.info(t("index.save_path") + "" + res.data?.file_name, {
duration: 5000
})
})
}
const uint8ArrayToBase64 = (bytes: any) => {
let binary = '';
const len = bytes.byteLength;
@@ -383,21 +451,24 @@ async function checkVariable() {
const download = (row: appType.MediaInfo, index: number) => {
if (!store.globalConfig.SaveDirectory) {
window?.$message?.error("请设置保存位置")
window?.$message?.error(t("index.save_path_empty"))
return
}
loadingText.value = "ready"
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,15 +478,18 @@ const download = (row: appType.MediaInfo, index: number) => {
}
const open = () => {
appApi.openSystemProxy().then((res: any) => {
store.updateProxyStatus(res.data)
store.openProxy().then((res: appType.Res) => {
if (res.code === 1) {
return
}
if (store.envInfo.platform === "darwin" || store.envInfo.platform === "linux") {
showPassword.value = true
}
})
}
const close = () => {
appApi.unsetSystemProxy().then((res: any) => {
store.updateProxyStatus(res.data)
})
store.unsetProxy()
}
const clear = () => {
@@ -426,22 +500,22 @@ const clear = () => {
const decodeWxFile = (row: appType.MediaInfo, index: number) => {
if (!row.DecodeKey) {
window?.$message?.error("无法解密")
window?.$message?.error(t("index.video_decode_no"))
return
}
appApi.openFileDialog().then((res: any) => {
appApi.openFileDialog().then((res: appType.Res) => {
if (res.code === 0) {
window?.$message?.error(res.message)
return
}
if (res.data.file) {
loadingText.value = "解密中"
loadingText.value = t("index.video_decode_loading")
loading.value = true
appApi.wxFileDecode({
...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)
@@ -450,31 +524,17 @@ const decodeWxFile = (row: appType.MediaInfo, index: number) => {
data.value[index].SavePath = res.data.save_path
data.value[index].Status = "done"
localStorage.setItem("resources-data", JSON.stringify(data.value))
window?.$message?.success("解密成功")
window?.$message?.success(t("index.video_decode_success"))
})
}
})
}
const triggerEvent = ()=>{
if(isDebug.value) {
const handleImport = (content: string) => {
if (!content) {
// window?.$message?.error(t("view_index.import_empty"))
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 {
let res = JSON.parse(decodeURIComponent(line))
@@ -484,11 +544,22 @@ const handleImport = (content: string)=>{
res.Status = "ready"
data.value.unshift(res)
}
}catch (e) {
} catch (e) {
console.log(e)
}
});
localStorage.setItem("resources-data", JSON.stringify(data.value))
showImport.value = false
}
const handlePassword = (password: string, isCache: boolean) => {
appApi.setSystemPassword({password: password, isCache: isCache}).then((res: appType.Res) => {
if (res.code === 0) {
window?.$message?.error(res.message)
return
}
showPassword.value = false
store.openProxy()
})
}
</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" :key="renderKey">
<NForm
:model="formValue"
size="medium"
@@ -7,140 +7,169 @@
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>
{{ t("setting.restart_tip") }}
</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>
{{ t("setting.restart_tip") }}
</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="t('setting.upstream_proxy')" 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>
{{ t("setting.upstream_proxy_tip") }}
</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 :label="t('setting.save_dir')" path="SaveDirectory">
<NInput :value="formValue.SaveDirectory" :placeholder="t('setting.save_dir')"/>
<NButton strong secondary type="primary" @click="selectDir" class="ml-1">{{ t('common.select') }}</NButton>
</NFormItem>
<NFormItem label="自动拦截" path="AutoProxy" size="small">
<NSwitch v-model:value="formValue.AutoProxy" />
<div class="grid grid-cols-2">
<NFormItem :label="t('setting.filename_rules')" 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>
{{ t("setting.filename_rules_tip") }}
</NTooltip>
</NFormItem>
<NFormItem :label="t('setting.quality')" 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>
{{ t("setting.quality_tip") }}
</NTooltip>
</NFormItem>
</div>
<div class="grid grid-cols-2 gap-4">
<NFormItem :label="t('setting.auto_proxy')" 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>
{{ t("setting.auto_proxy_tip") }}
</NTooltip>
</NFormItem>
<NFormItem :label="t('setting.full_intercept')" 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>
{{ t("setting.full_intercept_tip") }}
</NTooltip>
</NFormItem>
</div>
<div class="grid grid-cols-2 gap-4">
<NFormItem :label="t('setting.download_proxy')" 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>
{{ t("setting.download_proxy_tip") }}
</NTooltip>
</NFormItem>
<NFormItem :label="t('setting.connections')" 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>
{{ t("setting.connections_tip") }}
</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>
{{ t("setting.user_agent_tip") }}
</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>
{{ t("setting.use_headers_tip") }}
</NTooltip>
</NFormItem>
<NFormItem label="全量拦截" path="Quality" size="small">
<NSwitch v-model:value="formValue.WxAction" />
<NFormItem :label="t('setting.mime_map')" path="MimeMap">
<NInput
v-model:value="MimeMap"
type="textarea"
rows="11"
placeholder='{"video/mp4": { "Type": "video","Suffix": ".mp4"}}'
/>
<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>
{{ t("setting.mime_map_tip") }}
</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>
@@ -151,39 +180,43 @@ import {ref, watch} from "vue"
import {useIndexStore} from "@/stores"
import type {appType} from "@/types/app"
import appApi from "@/api/app"
import {computed} from "vue"
import {useI18n} from 'vue-i18n'
const {t} = useI18n()
const store = useIndexStore()
const options = [
{
value: 0,
label: "默认(推荐)"
}, {
value: 1,
label: "超清"
}, {
value: 2,
label: "高画质"
}, {
value: 3,
label: "中画质"
}, {
value: 4,
label: "低画质"
}
]
const options = computed(() =>
t("setting.quality_value")
.split(",")
.map((value, index) => ({ value: index, label: value }))
)
const formValue = ref<appType.Config>(Object.assign({}, store.globalConfig))
watch(()=>{
const MimeMap = ref(formValue.value.MimeMap ? JSON.stringify(formValue.value.MimeMap, null, 2) : "")
const renderKey = ref(999)
watch(formValue.value, () => {
store.setConfig(formValue.value)
}, {deep: true})
watch(MimeMap, () => {
store.setConfig({
MimeMap: JSON.parse(MimeMap.value)
})
})
watch(() => {
return store.globalConfig.Theme
}, ()=>{
}, () => {
formValue.value.Theme = store.globalConfig.Theme
})
const handleChange = (e: Event)=>{
formValue.value.Theme = (e.target as HTMLInputElement).value
}
watch(() => store.globalConfig.Locale, () => {
formValue.value.Locale = store.globalConfig.Locale
renderKey.value++
})
const selectDir = () => {
appApi.openDirectoryDialog().then((res: any) => {
@@ -194,9 +227,4 @@ const selectDir = () => {
window?.$message?.error(err)
});
}
const save = () => {
store.setConfig(formValue.value)
window?.$message?.success("保存成功")
}
</script>

View File

@@ -6,12 +6,7 @@ import Components from 'unplugin-vue-components/vite'
import {NaiveUiResolver} from 'unplugin-vue-components/resolvers'
// https://vitejs.dev/config/
// export default defineConfig({
// plugins: [vue()]
// })
export default defineConfig((env) => {
const viteEnv = loadEnv(env.mode, process.cwd())
return {
plugins: [
vue(),

7
frontend/wailsjs/go/core/Bind.d.ts vendored Executable file
View File

@@ -0,0 +1,7 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import {core} from '../models';
export function AppInfo():Promise<core.ResponseData>;
export function Config():Promise<core.ResponseData>;

View File

@@ -0,0 +1,11 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function AppInfo() {
return window['go']['core']['Bind']['AppInfo']();
}
export function Config() {
return window['go']['core']['Bind']['Config']();
}

21
frontend/wailsjs/go/models.ts Executable file
View File

@@ -0,0 +1,21 @@
export namespace core {
export class ResponseData {
code: number;
message: string;
data: any;
static createFrom(source: any = {}) {
return new ResponseData(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.code = source["code"];
this.message = source["message"];
this.data = source["data"];
}
}
}

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,9 @@ 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/net v0.35.0
golang.org/x/sys v0.30.0
)
require (
@@ -30,13 +31,12 @@ 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/text v0.22.0 // indirect
)

29
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,13 @@ 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=

13
main.go
View File

@@ -29,6 +29,7 @@ var wailsJson string
func main() {
// Create an instance of the app structure
app := core.GetApp(assets, wailsJson)
bind := core.NewBind()
isMac := runtime.GOOS == "darwin"
// menu
appMenu := menu.NewMenu()
@@ -41,10 +42,10 @@ func main() {
// Create application with options
err := wails.Run(&options.App{
Title: app.AppName,
Width: 1024,
Width: 1280,
MinWidth: 960,
Height: 768,
MinHeight: 640,
Height: 800,
MinHeight: 600,
Frameless: !isMac,
Menu: appMenu,
EnableDefaultContextMenu: true,
@@ -61,14 +62,16 @@ func main() {
|_| \___| |___/ \__,_| \___/ \_/\_/ |_| |_| |_| \___/ \__ ,_| \__,_| \___| |_|`
log.Println(logo)
fmt.Println("version", app.Version)
fmt.Println("version:", app.Version)
fmt.Println("lockfile:", app.LockFile)
app.Startup(ctx)
},
OnShutdown: func(ctx context.Context) {
app.OnExit()
},
Bind: []interface{}{},
Bind: []interface{}{
bind,
},
Mac: &mac.Options{
TitleBar: mac.TitleBarHiddenInset(),
About: &mac.AboutInfo{

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."
}
}