54 Commits
3.0.0 ... 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
putyy
791e50411d Update issue templates 2025-02-11 21:45:52 +08:00
putyy
df8eb0e4cd Update issue templates 2025-02-11 21:44:28 +08:00
putyy
3e291171c2 Update issue templates 2025-02-11 21:43:48 +08:00
putyy
bd8f8e80c9 Merge pull request #143 from putyy/wails
Wails
2025-02-11 21:42:19 +08:00
putyy
8ed7e144e1 修改doc、优化Mac设置代理等 2025-02-11 21:40:29 +08:00
putyy
69f8224453 完善文档、新增一些功能 2025-02-10 15:40:32 +08:00
putyy
3c0e51a9e2 文档 2025-02-09 18:58:09 +08:00
putyy
0a6679b983 新增docsify 2025-02-08 17:29:36 +08:00
putyy
6d1024806c Merge pull request #131 from putyy/wails
Wails
2025-01-22 16:19:44 +08:00
putyy
388e3f46a4 更新md 2025-01-22 16:18:32 +08:00
putyy
f7c8e9f7db 更新host 2025-01-21 10:37:10 +08:00
putyy
7ca484f45d Merge pull request #130 from putyy/wails
up:wx rule、downloads
2025-01-14 14:16:21 +08:00
putyy
cecb13fa90 up:wx rule、downloads 2025-01-14 13:59:22 +08:00
putyy
331478d370 Merge pull request #127 from taotieren/update-aur
update README.md
2025-01-11 13:16:40 +08:00
taotieren
2481407093 update README.md 2025-01-11 11:45:37 +08:00
putyy
e024a812d0 Merge pull request #124 from putyy/wails
update md
2025-01-10 17:36:10 +08:00
putyy
f5b5767997 update md 2025-01-10 17:35:39 +08:00
putyy
5e7022ec81 Merge pull request #123 from putyy/wails
Linux支持
2025-01-10 17:33:40 +08:00
putyy
e54177e8bf Linux支持 2025-01-10 17:33:05 +08:00
putyy
69cc5383d1 Merge pull request #122 from taotieren/fix-linux
Add Linux install
2025-01-09 11:30:43 +08:00
taotieren
11f88e86e3 Add Linux install 2025-01-09 11:27:07 +08:00
putyy
9e87e64223 优化 2024-12-24 14:39:13 +08:00
putyy
9ffef9db8e Delete .DS_Store 2024-12-23 17:27:38 +08:00
putyy
6d2705112d 优化 2024-12-23 16:59:26 +08:00
110 changed files with 3495 additions and 1792 deletions

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

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1 @@
blank_issues_enabled: 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

2
.gitignore vendored
View File

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

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,49 +1,101 @@
## res-downloader V3全新版来袭更快了
### 爱享素材下载器【[加入群聊](https://qm.qq.com/q/HS8FdhpZCK)】
🎯 基于Go + [wails](https://github.com/wailsapp/wails)
📦 操作简单、可获取不同类型资源
<div align="center">
🖥️ 支持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>
## 软件下载
🆕 [github下载](https://github.com/putyy/res-downloader/releases)
🆕 [蓝奏云下载 密码:9vs5](https://wwjv.lanzoum.com/b04wgtfyb)
[![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)
## 使用方法
> 0. 安装时一定要同意安装证书文件、一定要允许网络访问
> 1. 打开本软件 软件首页左上角点击 “启动代理”
> 2. 软件首页选择要获取的资源类型(默认选中的全部)
> 3. 打开要捕获的源, 如:视频号、网页、小程序等等
> 4. 返回软件首页即可看到资源列表
</div>
## 软件截图
![](preview/show.webp)
---
## 常见问题
m3u8: 预览和下载:
> [下载](https://m3u8-down.gowas.cn/) [预览](https://m3u8play.com/)
### 🎉 爱享素材下载器
直播流: 预览和录制:
> [使用obs进行预览和录制 使用教程自行百度]( https://obsproject.com/)
> 一款基于 Go + [Wails](https://github.com/wailsapp/wails) 的跨平台资源下载工具,简洁易用,支持多种资源嗅探与下载。
下载慢、大视频下载失败
> 推荐使用如下工具加速下载,视频号可以下载完成后再到对应视频操作项选择 “视频解密(视频号)” 按钮
>> [Neat Download Manager](https://www.neatdownloadmanager.com/index.php/en/)、[Motrix](https://motrix.app/download)等软件进行下载
## ✨ 功能特色
打开本软件,无法正常拦截获取
> 检查系统代理是否正确设置 代理地址127.0.0.1 端口8899
- 🚀 **简单易用**:操作简单,界面清晰美观
- 🖥️ **多平台支持**Windows / macOS / Linux
- 🌐 **多资源类型支持**:视频 / 音频 / 图片 / m3u8 / 直播流等
- 📱 **平台兼容广泛**支持微信视频号、小程序、抖音、快手、小红书、酷狗音乐、QQ音乐等
- 🌍 **代理抓包**:支持设置代理获取受限网络下的资源
关闭软件后无法正常上网
> 手动关闭系统代理设置
## 📚 文档 & 版本
其他问题
[github](https://github.com/putyy/res-downloader/issues) 、 [爱享论坛](https://s.gowas.cn/d/4089)
- 📘 [在线文档](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`,请备注“来源”*
## 实现 & 初衷
通过代理网络抓包拦截响应,筛选出有用的资源, 同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

@@ -1,35 +1,95 @@
# Build Directory
The build directory is used to house all the build files and assets for your application.
The structure is:
* bin - Output directory
* darwin - macOS specific files
* windows - Windows specific files
## Mac
The `darwin` directory holds files specific to Mac builds.
These may be customised and used as part of the build. To return these files to the default state, simply delete them
and
build with `wails build`.
The directory contains the following files:
- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`.
- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`.
```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)_mac.dmg"
```
## Windows
```bash
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"
```
The `windows` directory contains the manifest and rc files used when building with `wails build`.
These may be customised for your application. To return these files to the default state, simply delete them and
build with `wails build`.
## Linux
- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to
use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file
will be created using the `appicon.png` file in the build directory.
- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`.
- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer,
as well as the application itself (right click the exe -> properties -> details)
- `wails.exe.manifest` - The main application manifest file.
### docker方式
> x86_64
```bash
docker build --network host -f build/linux/dockerfile -t res-downloader-amd-linux .
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 -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)_linux_amd64.deb
# 打包AppImage
cp build/bin/res-downloader build/linux/AppImage/usr/bin/
# 复制WebKit相关文件
pushd build/linux/AppImage
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)_linux_amd64.AppImage
mv -f build/bin/res-downloader build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_linux_amd64
```
> arm64
```bash
# arm
docker build --platform linux/arm64 --network host -f build/linux/dockerfile -t res-downloader-arm-linux .
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 -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)_linux_arm64.deb
mv -f build/bin/res-downloader build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_linux_arm64
```
### Arch Linux
[![Packaging status](https://repology.org/badge/vertical-allrepos/res-downloader.svg)](https://repology.org/project/res-downloader/versions)
```bash
yay -Syu res-downloader
```
### Linux 本地编译
```bash
git clone https://github.com/putyy/res-downloader.git
cd res-downloader
# -- GO Proxy --
# 如果国内编译时 go 下载慢或报错,可以设置 go 国内代理加速,否则不用设置
export GO111MODULE=on
export GOPROXY=https://goproxy.cn,direct
# -- Go Proxy --
wails build
cd build
sudo install -Dvm755 bin/res-downloader -t /usr/bin
sudo install -Dvm644 appicon.png /usr/share/icons/hicolor/512x512/apps/res-downloader.png
sudo install -Dvm644 build/linux/Arch/res-downloader.desktop /usr/share/applications/res-downloader.desktop
```

5
build/linux/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
!.gitkeep
debian/usr/local/bin/*
debian/DEBIAN/control
AppImage/usr/bin/*
AppImage/usr/lib/*

View File

@@ -0,0 +1 @@
res-downloader.png

View File

@@ -0,0 +1,8 @@
[Desktop Entry]
Type=Application
Name=res-downloader
Comment=This is a high-value and high-performance and diverse resource downloader called res-downloader
Exec=/usr/bin/res-downloader
Icon=/usr/share/icons/hicolor/256x256/apps/res-downloader
Terminal=false
Categories=Utility;

Binary file not shown.

View File

View File

@@ -0,0 +1,8 @@
[Desktop Entry]
Type=Application
Name=res-downloader
Comment=This is a high-value and high-performance and diverse resource downloader called res-downloader
Exec=/usr/bin/res-downloader
Icon=/usr/share/icons/hicolor/256x256/apps/res-downloader
Terminal=false
Categories=Utility;

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@@ -0,0 +1,8 @@
[Desktop Entry]
Type=Application
Name=res-downloader
Comment=This is a high-value and high-performance and diverse resource downloader called res-downloader
Exec=res-downloader
Icon=res-downloader.png
Terminal=false
Categories=Utility;

BIN
build/linux/Debian/.DS_Store vendored Normal file

Binary file not shown.

BIN
build/linux/Debian/DEBIAN/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,9 @@
Package: res-downloader
Version: {{Version}}
Section: utils
Priority: optional
Architecture: amd64
Depends: libwebkit2gtk-4.0-37
Maintainer: putyy@qq.com
Homepage: https://github.com/putyy/res-downloader
Description: This is a high-value and high-performance and diverse resource downloader called res-downloader

BIN
build/linux/Debian/usr/local/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,8 @@
[Desktop Entry]
Type=Application
Name=res-downloader
Comment=This is a high-value and high-performance and diverse resource downloader called res-downloader
Exec=/usr/local/bin/res-downloader
Icon=/usr/share/icons/hicolor/256x256/apps/res-downloader.png
Terminal=false
Categories=Utility;

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

29
build/linux/dockerfile Normal file
View File

@@ -0,0 +1,29 @@
FROM golang:1.24.2-bookworm
WORKDIR /
RUN apt-get update && \
apt-get install -y --fix-missing \
build-essential \
git \
jq \
kmod \
fuse \
libgtk-3-dev \
libwebkit2gtk-4.0-dev \
nsis \
wget \
curl \
gnupg2 \
lsb-release \
libfuse-dev \
libfuse2 \
file \
&& rm -rf /var/lib/apt/lists/*
RUN curl -sL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y nodejs
RUN go install github.com/wailsapp/wails/v2/cmd/wails@latest
RUN wails doctor

View File

@@ -14,7 +14,7 @@
!define INFO_PRODUCTNAME "res-downloader"
!endif
!ifndef INFO_PRODUCTVERSION
!define INFO_PRODUCTVERSION "3.0.0"
!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,13 +3,13 @@ package core
import (
"context"
"embed"
"fmt"
"github.com/vrischmann/userdir"
"github.com/wailsapp/wails/v2/pkg/runtime"
"os"
"path/filepath"
sysRuntime "runtime"
"regexp"
"res-downloader/core/shared"
"strconv"
"strings"
"time"
)
@@ -24,7 +24,7 @@ type App struct {
LockFile string `json:"-"`
PublicCrt []byte `json:"-"`
PrivateKey []byte `json:"-"`
IsProxy bool `json:"-"`
IsProxy bool `json:"IsProxy"`
}
var (
@@ -37,16 +37,21 @@ var (
httpServerOnce *HttpServer
)
func GetApp(assets embed.FS) *App {
func GetApp(assets embed.FS, wjs string) *App {
if appOnce == nil {
matches := regexp.MustCompile(`"productVersion":\s*"([\d.]+)"`).FindStringSubmatch(wjs)
version := "1.0.1"
if len(matches) > 0 {
version = matches[1]
}
appOnce = &App{
assets: assets,
AppName: "res-downloader",
Version: "3.0.0",
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
@@ -70,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
@@ -102,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()
@@ -116,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() {
@@ -139,56 +131,49 @@ func (a *App) OnExit() {
globalLogger.Close()
}
func (a *App) installCert() {
if res, err := systemOnce.installCert(); err != nil {
if sysRuntime.GOOS == "darwin" {
DialogErr("证书安装失败,请手动执行安装命令(已复制到剪切板),err:" + err.Error() + ", " + res)
_ = runtime.ClipboardSetText(appOnce.ctx, `echo "输入本地登录密码" && sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "`+systemOnce.CertFile+`" && touch `+a.LockFile+` && echo "安装完成"`)
} else if sysRuntime.GOOS == "windows" && strings.Contains(err.Error(), "Access is denied.") {
DialogErr("首次启用本软件,请使用鼠标右键选择以管理员身份运行")
} 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("设置失败")
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("设置失败")
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,41 +5,119 @@ 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"`
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 := `
{
"Host": "0.0.0.0",
"Host": "127.0.0.1",
"Port": "8899",
"Theme": "lightTheme",
"Locale": "zh",
"Quality": 0,
"SaveDirectory": "",
"FilenameLen": 0,
"FilenameTime": true,
"UpstreamProxy": "",
"OpenProxy": false,
"DownloadProxy": false,
"AutoProxy": false,
"WxAction": 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))
@@ -47,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")
}
@@ -59,9 +151,14 @@ func initConfig() *Config {
func (c *Config) setConfig(config Config) {
oldProxy := c.UpstreamProxy
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
c.FilenameTime = config.FilenameTime
c.UpstreamProxy = config.UpstreamProxy
c.UserAgent = config.UserAgent
c.OpenProxy = config.OpenProxy
@@ -69,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,18 +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
@@ -20,6 +32,7 @@ type DownloadTask struct {
rangeEnd int64
downloadedSize int64
isCompleted bool
err error
}
type FileDownloader struct {
@@ -31,182 +44,323 @@ 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 + "/"
}
if globalConfig.DownloadProxy && globalConfig.UpstreamProxy != "" {
if globalConfig.DownloadProxy && globalConfig.UpstreamProxy != "" && !strings.Contains(globalConfig.UpstreamProxy, globalConfig.Port) {
proxyURL, err := url.Parse(globalConfig.UpstreamProxy)
if err == nil {
fd.ProxyUrl = proxyURL
}
}
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)
}
fd.TotalSize = resp.ContentLength
if _, ok := fd.Headers["User-Agent"]; !ok {
fd.Headers["User-Agent"] = globalConfig.UserAgent
}
if _, ok := fd.Headers["Referer"]; !ok {
fd.Headers["Referer"] = fd.Referer
}
if resp.Header.Get("Accept-Ranges") == "bytes" && fd.TotalSize > 10485760 {
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 errors.New("invalid file size")
}
if resp.Header.Get("Accept-Ranges") == "bytes" && fd.TotalSize > MinPartSize {
fd.IsMultiPart = true
}
resp.Body.Close()
fd.FileName = filepath.Clean(fd.FileName)
_, err = os.Stat(fd.FileName)
if err != nil {
if os.IsNotExist(err) {
fd.File, err = os.Create(fd.FileName)
if err != nil && fd.TotalSize > 0 {
err = fd.File.Truncate(fd.TotalSize)
}
}
} else {
fd.File, err = os.OpenFile(fd.FileName, os.O_RDWR, os.ModeAppend)
dir := filepath.Dir(fd.FileName)
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
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 err
return fmt.Errorf("file open failed: %w", err)
}
if err := fd.File.Truncate(fd.TotalSize); err != nil {
fd.File.Close()
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

@@ -1,6 +1,7 @@
package core
import (
"bytes"
"encoding/json"
"fmt"
"github.com/wailsapp/wails/v2/pkg/runtime"
@@ -9,26 +10,27 @@ 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"`
Data interface{} `json:"data"`
}
type HttpServer struct {
broadcast chan []byte
}
type HttpServer struct{}
func initHttpServer() *HttpServer {
if httpServerOnce == nil {
httpServerOnce = &HttpServer{
broadcast: make(chan []byte),
}
httpServerOnce = &HttpServer{}
}
return httpServerOnce
}
@@ -36,15 +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)
}
go h.handleMessages()
fmt.Println("服务已启动,监听 http://" + globalConfig.Host + ":" + globalConfig.Port)
if err := http.Serve(listener, proxyOnce.Proxy); err != nil {
fmt.Printf("服务器异常: %v", err)
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) // 代理
}
})); 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 == "" {
@@ -90,13 +108,6 @@ func (h *HttpServer) preview(w http.ResponseWriter, r *http.Request) {
return
}
func (h *HttpServer) handleMessages() {
for {
msg := <-h.broadcast
runtime.EventsEmit(appOnce.ctx, "event", string(msg))
}
}
func (h *HttpServer) send(t string, data interface{}) {
jsonData, err := json.Marshal(map[string]interface{}{
"type": t,
@@ -106,15 +117,50 @@ func (h *HttpServer) send(t string, data interface{}) {
fmt.Println("Error converting map to JSON:", err)
return
}
h.broadcast <- jsonData
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,94 +211,122 @@ 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) // 尝试Nautilus
cmd = exec.Command("nautilus", filePath)
if err := cmd.Start(); err != nil {
cmd = exec.Command("thunar", filePath) // 尝试Thunar
cmd = exec.Command("thunar", filePath)
if err := cmd.Start(); err != nil {
cmd = exec.Command("dolphin", filePath) // 尝试Dolphin
cmd = exec.Command("dolphin", filePath)
if err := cmd.Start(); err != nil {
cmd = exec.Command("pcmanfm", filePath) // 尝试PCManFM
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

@@ -15,15 +15,19 @@ func Middleware(next http.Handler) http.Handler {
}
func HandleApi(w http.ResponseWriter, r *http.Request) bool {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return true
}
if strings.HasPrefix(r.URL.Path, "/api") {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
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 {
@@ -95,7 +131,7 @@ func (p *Proxy) setTransport() {
IdleConnTimeout: 30 * time.Second,
}
if globalConfig.UpstreamProxy != "" && globalConfig.OpenProxy {
if globalConfig.UpstreamProxy != "" && globalConfig.OpenProxy && !strings.Contains(globalConfig.UpstreamProxy, globalConfig.Port) {
proxyURL, err := url.Parse(globalConfig.UpstreamProxy)
if err == nil {
transport.Proxy = http.ProxyURL(proxyURL)
@@ -104,273 +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") {
if 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 strings.Contains(r.URL.Path, "/cert") {
return p.handleCertRequest(r, ctx)
if newReq != nil {
return newReq, nil
}
}
return r, nil
}
func (p *Proxy) handleCertRequest(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
resp := &http.Response{
StatusCode: http.StatusOK,
ProtoMajor: 1,
ProtoMinor: 1,
Header: http.Header{
"Content-Type": []string{"application/x-x509-ca-data"},
"Content-Disposition": []string{"attachment;filename=z-der-data.crt"},
"Content-Transfer-Encoding": []string{"binary"},
},
Body: io.NopCloser(bytes.NewReader(appOnce.PublicCrt)),
Request: r,
}
return r, resp
}
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 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,16 +95,25 @@ 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
if fileLen <= 0 {
fileLen = 10
}
runes := []rune(fileName)
if len(runes) > 10 {
fileName = string(runes[:10])
if len(runes) > fileLen {
fileName = string(runes[:fileLen])
}
}
mediaInfo.SavePath = filepath.Join(globalConfig.SaveDirectory, fileName+"_"+GetCurrentDateTimeFormatted()+mediaInfo.Suffix)
if globalConfig.FilenameTime {
mediaInfo.SavePath = filepath.Join(globalConfig.SaveDirectory, fileName+"_"+shared.GetCurrentDateTimeFormatted()+mediaInfo.Suffix)
} else {
mediaInfo.SavePath = filepath.Join(globalConfig.SaveDirectory, fileName+mediaInfo.Suffix)
}
if strings.Contains(rawUrl, "qq.com") {
if globalConfig.Quality == 1 &&
@@ -143,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 {
@@ -153,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 {
@@ -189,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

@@ -5,94 +5,115 @@ package core
import (
"bytes"
"fmt"
"log"
"net"
"os/exec"
"strings"
)
func (s *SystemSetup) getActiveInterface() (string, error) {
interfaces, err := net.Interfaces()
if err != nil {
return "", err
func (s *SystemSetup) runCommand(args []string) ([]byte, error) {
if len(args) == 0 {
return nil, fmt.Errorf("no command provided")
}
for _, inter := range interfaces {
if inter.Flags&net.FlagUp != 0 && inter.Flags&net.FlagLoopback == 0 {
return inter.Name, nil
}
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:]...)
}
return "", fmt.Errorf("no active network interface found")
output, err := cmd.CombinedOutput()
return output, err
}
func (s *SystemSetup) getNetworkServiceName(interfaceName string) (string, error) {
cmd := exec.Command("networksetup", "-listallhardwareports")
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
return "", err
func (s *SystemSetup) getNetworkServices() ([]string, error) {
output, err := s.runCommand([]string{"networksetup", "-listallnetworkservices"})
if err != nil {
return nil, fmt.Errorf("failed to execute command: %v", err)
}
output := out.String()
lines := strings.Split(output, "\n")
var serviceName string
for _, line := range lines {
if strings.Contains(line, "Hardware Port:") {
serviceName = strings.TrimSpace(strings.Split(line, ":")[1])
services := strings.Split(string(output), "\n")
var activeServices []string
for _, service := range services {
service = strings.TrimSpace(service)
if service == "" || strings.Contains(service, "*") || strings.Contains(service, "Serial Port") {
continue
}
if strings.Contains(line, "Device: "+interfaceName) {
return serviceName, nil
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
}
if strings.Contains(string(infoOutput), "IP address:") {
activeServices = append(activeServices, service)
}
}
return "", fmt.Errorf("no matching network service found for interface %s", interfaceName)
if len(activeServices) == 0 {
return nil, fmt.Errorf("no active network services found")
}
return activeServices, nil
}
func (s *SystemSetup) setProxy() error {
interfaceName, err := s.getActiveInterface()
services, err := s.getNetworkServices()
if err != nil {
return err
}
serviceName, err := s.getNetworkServiceName(interfaceName)
if err != nil {
return err
}
commands := [][]string{
{"networksetup", "-setwebproxy", serviceName, "127.0.0.1", globalConfig.Port},
{"networksetup", "-setsecurewebproxy", serviceName, "127.0.0.1", globalConfig.Port},
}
for _, cmd := range commands {
if err := exec.Command(cmd[0], cmd[1:]...).Run(); err != nil {
return err
isSuccess := false
var errs strings.Builder
for _, serviceName := range services {
commands := [][]string{
{"networksetup", "-setwebproxy", serviceName, "127.0.0.1", globalConfig.Port},
{"networksetup", "-setsecurewebproxy", serviceName, "127.0.0.1", globalConfig.Port},
}
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
}
}
}
return nil
if isSuccess {
return nil
}
return fmt.Errorf("failed to set proxy for any active network service, errs:%s", errs)
}
func (s *SystemSetup) unsetProxy() error {
interfaceName, err := s.getActiveInterface()
services, err := s.getNetworkServices()
if err != nil {
return err
}
serviceName, err := s.getNetworkServiceName(interfaceName)
if err != nil {
return err
}
commands := [][]string{
{"networksetup", "-setwebproxystate", serviceName, "off"},
{"networksetup", "-setsecurewebproxystate", serviceName, "off"},
}
for _, cmd := range commands {
if err := exec.Command(cmd[0], cmd[1:]...).Run(); err != nil {
log.Println("UnsetProxy failed:", err)
return err
isSuccess := false
var errs strings.Builder
for _, serviceName := range services {
commands := [][]string{
{"networksetup", "-setwebproxystate", serviceName, "off"},
{"networksetup", "-setsecurewebproxystate", serviceName, "off"},
}
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
}
}
}
return nil
if isSuccess {
return nil
}
return fmt.Errorf("failed to unset proxy for any active network service, errs:%s", errs)
}
func (s *SystemSetup) installCert() (string, error) {
@@ -100,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"},
@@ -16,35 +49,89 @@ func (s *SystemSetup) setProxy() error {
{"gsettings", "set", "org.gnome.system.proxy.https", "port", globalConfig.Port},
}
isSuccess := false
var errs strings.Builder
for _, cmd := range commands {
if err := exec.Command(cmd[0], cmd[1:]...).Run(); err != nil {
return 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 {
isSuccess = true
}
}
return nil
if isSuccess {
return nil
}
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) {
certData, err := s.initCert()
if err != nil {
return "", err
}
destFile := "/usr/local/share/ca-certificates/" + appOnce.AppName + ".crt"
err = os.WriteFile(destFile, certData, 0644)
_, err := s.initCert()
if err != nil {
return "", err
}
cmd := exec.Command("sudo", "update-ca-certificates")
output, err := cmd.CombinedOutput()
distro, err := s.getLinuxDistro()
if err != nil {
return string(output), err
return "", fmt.Errorf("detect distro failed: %w", err)
}
return "", nil
certName := appOnce.AppName + ".crt"
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)
}
}
}
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)
}
if isSuccess {
return "", nil
}
return outs.String(), fmt.Errorf("certificate installation failed:\n%s", errs.String())
}

View File

@@ -1,154 +1,14 @@
package core
import (
"crypto/md5"
"encoding/hex"
"fmt"
"net/url"
"os"
"strings"
"time"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
func Empty(data interface{}) {
}
func DialogErr(message string) {
httpServerOnce.send("message", map[string]interface{}{
"code": 0,
"message": message,
_, _ = runtime.MessageDialog(appOnce.ctx, runtime.MessageDialogOptions{
Type: runtime.ErrorDialog,
Title: "Error",
Message: message,
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())
}

0
docs/.nojekyll Normal file
View File

12
docs/_coverpage.md Normal file
View File

@@ -0,0 +1,12 @@
<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>
> 全新技术栈,更新、更小、更快、更稳
### 简单、高效、轻便 (仅 ~10M)
[开始使用 Let Go](/readme.md)
[下载](/getting-started.md)

4
docs/_navbar.md Normal file
View File

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

6
docs/_sidebar.md Normal file
View File

@@ -0,0 +1,6 @@
* [简介](readme.md)
* [快速开始](getting-started.md)
* [安装指南](installation.md)
* [功能演示](examples.md)
* [更多说明](more.md)
* [常见问题](troubleshooting.md)

19
docs/examples.md Normal file
View File

@@ -0,0 +1,19 @@
## 开启代理
- 安装完成后开启代理,如图:
![](images/examples-1.png ':size=50%')
## 拦截资源
### 视频号
- 打开视频号即可看到本软件中拦截到的资源
![](images/examples-2.webp ':size=50%')
### 网页资源
- 浏览器打开或者其他软件内置浏览器打开的网页
- 这里演示打开百度这个网站https://www.baidu.com/
![](images/examples-4.png ':size=50%')
### 小程序、公众号、抖音、小红书、qq音乐、酷狗等应用内资源获取方式都差不多
## 下载资源到本地
- 选择你想下载的视频 点击下载即可
![](images/examples-3.png ':size=50%')

BIN
docs/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

14
docs/getting-started.md Normal file
View File

@@ -0,0 +1,14 @@
## 软件下载
🆕 [github下载](https://github.com/putyy/res-downloader/releases)
🆕 [蓝奏云下载 密码:9vs5](https://wwjv.lanzoum.com/b04wgtfyb)
!> Win7用户请使用2.3.0版本
## 使用方法
- 安装时一定要同意安装证书文件、一定要允许网络访问
- 打开本软件(win系统首次使用管理员打开-鼠标右键选择管理员打开)
- 打开后,左上角点击 “启动代理”
- 打开要捕获的源, 如:视频号、网页、小程序等等
- 返回软件首页即可看到资源列
![](images/show.webp ':size=50%')

BIN
docs/images/config.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
docs/images/examples-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
docs/images/examples-2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
docs/images/examples-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

BIN
docs/images/examples-4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

BIN
docs/images/logo.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

BIN
docs/images/more-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

BIN
docs/images/more-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

BIN
docs/images/more-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

BIN
docs/images/more-4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

50
docs/index.html Normal file
View File

@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>res-downloader</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="keywords" content="res-downloader,视频号下载,抖音下载,快手下载,小红书下载,万能下载器,爱享素材,爱享素材下载器">
<meta name="description" content="res-downloader是一款集网络资源嗅探 + 高速下载功能于一体的软件,高颜值、高性能和多样化,提供个人用户下载自己上传到各大平台的网络资源功能!">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/vue.css">
</head>
<body>
<div id="app"></div>
<script>
window.$docsify = {
name: 'res-downloader',
repo: 'https://github.com/putyy/res-downloader',
// 侧边栏支持默认加载的是项目根目录下的_sidebar.md文件
loadSidebar: true,
// 导航栏支持默认加载的是项目根目录下的_navbar.md文件
loadNavbar: true,
// 封面支持默认加载的是项目根目录下的_coverpage.md文件
coverpage: true,
// 最大支持渲染的标题层级
maxLevel: 5,
// 自定义侧边栏后默认不会再生成目录设置生成目录的最大层级建议配置为2-4
subMaxLevel: 4,
// 小屏设备下合并导航栏到侧边栏
mergeNavbar: true,
basePath: '/',
homepage: 'readme.md',
search: {
maxAge: 86400000,// 过期时间,单位毫秒,默认一天
paths: 'auto',// 注意:仅适用于 paths: 'auto' 模式
// 支持本地化
placeholder: '搜索',
noData: '找不到结果',
depth: 4,
hideOtherSidebarContent: false,
namespace: 'Docsify-Guide',
}
}
</script>
<script src="//cdn.jsdelivr.net/npm/docsify@4"></script>
<script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/emoji.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/zoom-image.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/search.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/docsify-copy-code/dist/docsify-copy-code.min.js"></script>
</body>
</html>

21
docs/installation.md Normal file
View File

@@ -0,0 +1,21 @@
## 下载安装文件
- windows下载.exe结尾的根据自己的系统架构下载合适的安装文件通常下载带有“x64-installer.exe”结尾的文件
- Mac下载.dmg结尾即可
- Linux根据系统类型下载对应的执行文件或安装文件
## Windows安装过程
- 双击下载好的exe 正常安装即可,首次打开记得右键管理员运行
## Mac安装过程
- 双击下载好的dmg文件将res-downloader拖入应用即可如图:
![installation-mac-1.png](images/installation-mac-1.png ':size=50%')
## 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

17
docs/more.md Normal file
View File

@@ -0,0 +1,17 @@
## 清空列表、类型筛选
- 当资源列表过大时,无法快速找到需要的资源,这时可以先清空列表再去刷新需要的资源页面
- 资源列表过多,可以快速根据需要的资源类型进行筛选
![more-4.png](images/more-1.png ':size=30%')
## 拦截想要的资源类型、批量下载
- 比如只需要视频时就选择视频类型,可以多选
![more-1.png](images/more-2.png ':size=30%')
## 复制链接、视频解密
- 复制链接可用于第三方软件进行下载,下载完成后对该视频解密,点击“视频解密”选择用其他软件下载完成后的视频文件进行解密
![more-3.png](images/more-3.png ':size=30%')
## 设置说明
!> 修改完成后记得点保存
- 几乎每项配置在软件中都有说明(鼠标悬浮在?处即可查看),此处就不进行多余讲解
![config.png](images/config.png ':size=30%')

63
docs/readme.md Normal file
View File

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

77
docs/troubleshooting.md Normal file
View File

@@ -0,0 +1,77 @@
## 视频号拦截了一大堆 找不到想要的
> 设置里面关闭全量拦截,将视频转发好友后打开
## 某某网址拦截不了?
> 本软件实现原理 & 初衷如下,并非万能的,所以有一些应用拦截不了很正常
```
本工具通过代理方式实现网络抓包,并筛选可用资源。与 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
## 打开本软件,无法正常拦截获取
> 检查系统证书是否安装
> 关闭网络防火墙
> 系统代理是否正确设置(代理地址127.0.0.1 端口8899)
## 关闭软件后无法正常上网
> 手动关闭系统代理设置
## 链接不是私密链接
> 通常是证书未正确安装,最新版证书下载:软件左下角 ?点击后有下载地址
> 根据自己系统进行安装证书操作(不懂的自行百度),手动安装需安装到受信任的根证书
- Mac手动安装证书(V3+版本支持),打开终端复制以下命令 粘贴到终端回车 按照提示输入密码,完成后再打开软件:
```shell
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /Users/$(whoami)/Library/Preferences/res-downloader/cert.crt && touch /Users/$(whoami)/Library/Preferences/res-downloader/install.lock && echo "安装完成"
```
## 拦截不到小程序中的资源
清理微信缓存,删除小程序后,重新打开
> 设置->存储空间->缓存
## 只拦截打开的视频号视频
关闭全量拦截,打开视频号视频详情,通常分享好友后打开的页面属于详情页
## 拦截视频号账号视频
打开对应作者个人主页,浏览即可
## 下载慢、大视频下载失败
推荐使用如下工具加速下载,视频号可以下载完成后再到对应视频操作项选择 “视频解密” 按钮
> [Neat Download Manager](https://www.neatdownloadmanager.com/index.php/en/)、[Motrix](https://motrix.app/download)等软件进行下载
## 直播流: 预览和录制:
> [使用obs进行预览和录制 使用教程自行百度, 点击下载obs]( https://obsproject.com/)
## m3u8: 预览和下载:
> [在线下载](https://m3u8-down.gowas.cn/)、[在线预览](https://m3u8play.com/)
## 安装证书后还会提示安装
使用命令行打开本软件,查看 “lockfile:” 这串字符后面的锁文件路径,然后创建该文件即可
例如 mac系统下终端执行如下命令即可创建
> touch /Users/你的用户名/Library/Preferences/res-downloader/install.lock
## 更多问题 请前往github进行[反馈](https://github.com/putyy/res-downloader/issues)

View File

@@ -8,6 +8,7 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
Footer: typeof import('./src/components/Footer.vue')['default']
ImportJson: typeof import('./src/components/ImportJson.vue')['default']
Index: typeof import('./src/components/layout/Index.vue')['default']
NaiveProvider: typeof import('./src/components/NaiveProvider.vue')['default']
NButton: typeof import('naive-ui')['NButton']
@@ -30,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

BIN
frontend/src/.DS_Store vendored Normal file

Binary file not shown.

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, watch} from "vue"
import type {wsType} from "@/types/ws"
import {computed, onMounted, ref} from "vue"
import {useEventStore} from "@/stores/event"
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: wsType.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,28 +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('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>
@@ -58,11 +50,17 @@
<script lang="ts" setup>
import {useIndexStore} from "@/stores"
import {BrowserOpenURL} from "../../wailsjs/runtime";
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

@@ -0,0 +1,38 @@
<template>
<NModal
:show="showModal"
:on-update:show="changeShow"
style="--wails-draggable:no-drag"
preset="card"
class="w-[640px]"
:title="t('index.batch_import')"
>
<NForm
size="medium"
label-placement="left"
label-width="auto"
require-mark-placement="right-hanging"
style="--wails-draggable:no-drag"
>
<NFormItem>
<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">{{ 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
}>()
const emits = defineEmits(["update:showModal", "submit"])
const changeShow = (value: boolean) => emits("update:showModal", value)
</script>

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,25 +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 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 {useI18n} from 'vue-i18n'
const {t} = useI18n()
const props = defineProps<{
row: any,
index: number,

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()
@@ -35,7 +48,6 @@ const minimizeWindow = () => {
WindowMinimise()
}
const maximizeWindow = () => {
console.log("maximizeWindow")
isMaximized.value = !isMaximized.value;
if (isMaximized.value) {
WindowFullscreen()

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,41 +44,61 @@
</NScrollbar>
</main>
</div>
<Footer v-model:showModal="showAppInfo" />
<Footer v-model:showModal="showAppInfo"/>
</template>
<script lang="ts" setup>
import type {MenuOption} from "naive-ui"
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
MoonOutline,
SunnyOutline,
LanguageSharp,
LogoGithub
} from "@vicons/ionicons5"
import {useIndexStore} from "@/stores"
import Footer from "@/components/Footer.vue"
import Screen from "@/components/Screen.vue";
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
})
const theme = computed(() => {
return store.globalConfig.Theme === "darkTheme" ? renderIcon(SunnyOutline) : renderIcon(MoonOutline)
})
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)})
@@ -85,12 +106,12 @@ 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),
},
@@ -98,37 +119,68 @@ const menuOptions = ref([
const footerOptions = ref([
{
label: "主题",
key: 'theme',
icon: renderIcon(MoonOutline),
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
}
if (key === "github") {
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>

View File

@@ -1,8 +1,6 @@
export const DwStatus = {
ready: "就绪",
running: "运行中",
pause: "暂停",
wait: "等待",
error: "错误",
done: "完成",
handle: "已下载,后续处理",

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

@@ -1,12 +1,12 @@
import {defineStore} from "pinia"
import {ref} from "vue"
import type {wsType} from "@/types/ws"
import {EventsOn} from "../../wailsjs/runtime"
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)) {
@@ -17,7 +17,7 @@ export const useEventStore = defineStore('ws-store', () => {
})
}
const addHandle = (handle: wsType.Handle) => {
const addHandle = (handle: appType.Handle) => {
handles.value[handle.type] = handle.event
}

View File

@@ -2,7 +2,9 @@ import {defineStore} from 'pinia'
import {ref} from "vue"
import type {appType} from "@/types/app"
import appApi from "@/api/app"
import {Environment} from "../../wailsjs/runtime";
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,17 +16,22 @@ export const useIndexStore = defineStore("index-store", () => {
const globalConfig = ref<appType.Config>({
Theme: "lightTheme",
Locale: "zh",
Host: "0.0.0.0",
Port: "8899",
Quality: 0,
SaveDirectory: "",
UpstreamProxy: "",
FilenameLen: 0,
FilenameTime: false,
OpenProxy: false,
DownloadProxy: false,
AutoProxy: false,
WxAction: false,
TaskNumber: 8,
UserAgent: "",
UseHeaders: "",
MimeMap: {}
})
const envInfo = ref({
@@ -36,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)
}
@@ -69,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,12 +6,20 @@ export namespace appType {
Copyright: string
}
interface MimeMap {
Type: string
Suffix: string
}
interface Config {
Theme: string
Locale: string
Host: string
Port: string
Quality: number
SaveDirectory: string
FilenameLen: number
FilenameTime: boolean
UpstreamProxy: string
OpenProxy: boolean
DownloadProxy: boolean
@@ -19,6 +27,8 @@ export namespace appType {
WxAction: boolean
TaskNumber: number
UserAgent: string
UseHeaders: string
MimeMap: { [key: string]: MimeMap }
}
interface MediaInfo {
@@ -35,7 +45,7 @@ export namespace appType {
DecodeKey: string
Description: string
ContentType: string
OtherData: {[key: string]: string}
OtherData: { [key: string]: string }
}
interface DownloadProgress {
@@ -44,4 +54,20 @@ export namespace appType {
Status: string
Message: string
}
interface Message {
code: number
message: string
}
interface Handle {
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,21 +0,0 @@
export namespace wsType {
interface Event {
type: string
event: any
data: any
}
interface Handle {
type: string
event: any
}
interface Message {
code: number
message: string
}
interface Clipboard {
content: string
}
}

View File

@@ -1,12 +1,27 @@
<template>
<div class="flex flex-col px-5 py-5">
<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="close" style="--wails-draggable:no-drag">关闭代理</NButton>
<NButton v-else tertiary type="tertiary" @click="open" style="--wails-draggable:no-drag">开启代理</NButton>
<NButton tertiary type="info" @click="batchDown" style="--wails-draggable:no-drag">批量下载</NButton>
<NButton tertiary type="error" @click="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="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">
@@ -21,16 +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} from "vue"
import {computed, h, onMounted, ref, reactive, watch} from "vue"
import type {appType} from "@/types/app"
import type {DataTableRowKey, ImageRenderToolbarProps} from "naive-ui"
@@ -40,84 +58,81 @@ 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 {useEventStore} from "@/stores/event";
import {BrowserOpenURL, ClipboardSetText} from "../../wailsjs/runtime";
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
})
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 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 resourcesType = ref<string[]>(["all"])
const options = [
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) => {
@@ -150,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")
}
}
),
@@ -160,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) => {
@@ -184,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",
@@ -208,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})
@@ -216,14 +231,17 @@ const columns = ref<any[]>([
}
])
const downIndex = ref(0)
const checkedRowKeysValue = ref<DataTableRowKey[]>([])
const showPreviewRow = ref(false)
const previewRow = ref<appType.MediaInfo>()
const loading = ref(false)
const loadingText = ref("")
const showImport = ref(false)
const showPassword = ref(false)
onMounted(() => {
buildClassify()
const temp = localStorage.getItem("resources-type")
if (temp) {
resourcesType.value = JSON.parse(temp).res
@@ -267,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
@@ -278,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":
@@ -291,9 +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(t("common.copy_success"))
} else {
window?.$message?.error(t("common.copy_fail"))
}
})
break
@@ -338,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++) {
@@ -349,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;
@@ -371,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)
@@ -395,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 = () => {
@@ -414,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)
@@ -438,10 +524,42 @@ 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 handleImport = (content: string) => {
if (!content) {
// window?.$message?.error(t("view_index.import_empty"))
return
}
content.split("\n").forEach((line, index) => {
try {
let res = JSON.parse(decodeURIComponent(line))
if (res && res?.Id) {
res.Id = res.Id + Math.floor(Math.random() * 100000)
res.SavePath = ""
res.Status = "ready"
data.value.unshift(res)
}
} 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>

Some files were not shown because too many files have changed in this diff Show More