7 Commits

Author SHA1 Message Date
putyy
04e4f0e9cc docs: Update readme 2025-05-07 14:23:13 +08:00
putyy
625cfbc474 perf: set page optimization 2025-05-07 11:05:59 +08:00
putyy
d8857bd4a2 perf: interception type optimization 2025-05-06 18:06:42 +08:00
putyy
a247c708f6 perf: unified response 2025-04-29 11:31:38 +08:00
putyy
85781a150a perf: optimization download、Proxy settings, add batch export 2025-04-29 11:15:13 +08:00
putyy
6086bd7086 feat: Added header cache, optional header settings... 2025-04-23 16:55:24 +08:00
putyy
0bb1a21a76 优化拦截标识 2025-04-22 15:59:59 +08:00
26 changed files with 812 additions and 502 deletions

View File

@@ -1,13 +1,24 @@
# res-downloader
<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.
### 📖 [中文](./README.md) | English
---
## ✨ Features
- 🚀 **User-Friendly**: Simple operation with an intuitive and beautiful UI
@@ -16,8 +27,6 @@ Clean UI, easy to use, and supports a wide range of resource sniffing and downlo
- 📱 **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/)
@@ -25,15 +34,16 @@ Clean UI, easy to use, and supports a wide range of resource sniffing and downlo
- 💬 [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
@@ -47,12 +57,6 @@ Clean UI, easy to use, and supports a wide range of resource sniffing and downlo
---
## 🖼️ Screenshot
![Screenshot](docs/images/show.webp)
---
## ❓ FAQ
### 📺 m3u8 Video Resources
@@ -86,8 +90,6 @@ Clean UI, easy to use, and supports a wide range of resource sniffing and downlo
- [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.

View File

@@ -1,12 +1,23 @@
# res-downloader
<div align="center">
<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 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) 的跨平台资源下载工具,简洁易用,支持多种资源嗅探与下载。
### 📖 中文 | [English](./README-EN.md)
---
## ✨ 功能特色
- 🚀 **简单易用**:操作简单,界面清晰美观
@@ -15,24 +26,25 @@
- 📱 **平台兼容广泛**支持微信视频号、小程序、抖音、快手、小红书、酷狗音乐、QQ音乐等
- 🌍 **代理抓包**:支持设置代理获取受限网络下的资源
---
## 📚 文档 & 版本
- 📘 [在线文档](https://res.putyy.com/)
- 🧩 [Mini版 UI使用默认浏览器展示](https://github.com/putyy/res-downloader) [Electron旧版 支持Win7](https://github.com/putyy/res-downloader/tree/old)
- 💬 [加入交流群](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` 版本*
---
## 🖼️ 预览
![预览](docs/images/show.webp)
---
## 🚀 使用方法
@@ -44,14 +56,6 @@
4. 在外部打开资源页面(如视频号、小程序、网页等)
5. 返回软件首页,即可看到资源列表
---
## 🖼️ 软件截图
![软件截图](docs/images/show.webp)
---
## ❓ 常见问题
### 📺 m3u8 视频资源
@@ -85,8 +89,6 @@
- [GitHub Issues](https://github.com/putyy/res-downloader/issues)
- [爱享论坛讨论帖](https://s.gowas.cn/d/4089)
---
## 💡 实现原理 & 初衷
本工具通过代理方式实现网络抓包,并筛选可用资源。与 Fiddler、Charles、浏览器 DevTools 原理类似,但对资源进行了更友好的筛选、展示和处理,大幅度降低了使用门槛,更适合大众用户使用。

View File

@@ -7,8 +7,8 @@ mv -f "build/bin/res-downloader $(jq -r '.info.productVersion' wails.json).dmg"
## Windows
```bash
wails build -f -nsis -platform "windows/amd64" -webview2 Embed && mv -f "build/bin/res-downloader-amd64-installer.exe" "build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_win_amd64.exe"
wails build -f -nsis -platform "windows/arm64" -webview2 Embed && mv -f "build/bin/res-downloader-arm64-installer.exe" "build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_win_arm64.exe"
wails build -f -nsis -platform "windows/amd64" -webview2 Embed -skipbindings && mv -f "build/bin/res-downloader-amd64-installer.exe" "build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_win_amd64.exe"
wails build -f -nsis -platform "windows/arm64" -webview2 Embed -skipbindings && mv -f "build/bin/res-downloader-arm64-installer.exe" "build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_win_arm64.exe"
```
## Linux
@@ -41,7 +41,7 @@ wget -O ./build/bin/appimagetool-x86_64.AppImage https://github.com/AppImage/App
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
cp build/bin/res-downloader build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_linux_amd64
mv -f build/bin/res-downloader build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_linux_amd64
```
> arm64
@@ -58,7 +58,7 @@ 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
cp build/bin/res-downloader build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_linux_arm64
mv -f build/bin/res-downloader build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_linux_arm64
```
### Arch Linux

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -148,12 +148,12 @@ func (p *Proxy) handleWechatRequest(r *http.Request, ctx *goproxy.ProxyCtx) (*ht
if !ok {
return
}
resourceOnce.markMu.Lock()
defer resourceOnce.markMu.Unlock()
urlSign := Md5(rowUrl.(string))
if _, ok := resourceOnce.mark[urlSign]; ok {
if resourceOnce.mediaIsMarked(urlSign) {
return
}
id, err := gonanoid.New()
if err != nil {
id = urlSign
@@ -214,7 +214,7 @@ func (p *Proxy) handleWechatRequest(r *http.Request, ctx *goproxy.ProxyCtx) (*ht
res.OtherData["wx_file_formats"] = strings.Join(fileFormats, "#")
}
resourceOnce.mark[urlSign] = true
resourceOnce.markMedia(urlSign)
httpServerOnce.send("newResources", res)
}(body)
return r, p.buildEmptyResponse(r)
@@ -310,14 +310,11 @@ func (p *Proxy) httpResponseEvent(resp *http.Response, ctx *goproxy.ProxyCtx) *h
}
rawUrl := resp.Request.URL.String()
resourceOnce.markMu.Lock()
defer resourceOnce.markMu.Unlock()
isAll, _ := resourceOnce.getResType("all")
isClassify, _ := resourceOnce.getResType(classify)
urlSign := Md5(rawUrl)
if _, ok := resourceOnce.mark[urlSign]; !ok && (isAll || isClassify) {
if ok := resourceOnce.mediaIsMarked(urlSign); !ok && (isAll || isClassify) {
value, _ := strconv.ParseFloat(resp.Header.Get("content-length"), 64)
id, err := gonanoid.New()
if err != nil {
@@ -339,7 +336,13 @@ func (p *Proxy) httpResponseEvent(resp *http.Response, ctx *goproxy.ProxyCtx) *h
Description: "",
ContentType: resp.Header.Get("Content-Type"),
}
resourceOnce.mark[urlSign] = true
// Store entire request headers as JSON
if headers, err := json.Marshal(resp.Request.Header); err == nil {
res.OtherData["headers"] = string(headers)
}
resourceOnce.markMedia(urlSign)
httpServerOnce.send("newResources", res)
}
return resp

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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